summaryrefslogtreecommitdiff
path: root/app/assets
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2022-12-20 14:22:11 +0000
committerGitLab Bot <gitlab-bot@gitlab.com>2022-12-20 14:22:11 +0000
commit0c872e02b2c822e3397515ec324051ff540f0cd5 (patch)
treece2fb6ce7030e4dad0f4118d21ab6453e5938cdd /app/assets
parentf7e05a6853b12f02911494c4b3fe53d9540d74fc (diff)
downloadgitlab-ce-0c872e02b2c822e3397515ec324051ff540f0cd5.tar.gz
Add latest changes from gitlab-org/gitlab@15-7-stable-eev15.7.0-rc42
Diffstat (limited to 'app/assets')
-rw-r--r--app/assets/images/web-ide-promo-popover.svg101
-rw-r--r--app/assets/javascripts/admin/background_migrations/components/database_listbox.vue6
-rw-r--r--app/assets/javascripts/admin/broadcast_messages/components/base.vue5
-rw-r--r--app/assets/javascripts/admin/broadcast_messages/components/datetime_picker.vue47
-rw-r--r--app/assets/javascripts/admin/broadcast_messages/components/message_form.vue225
-rw-r--r--app/assets/javascripts/admin/broadcast_messages/components/message_form_group.vue34
-rw-r--r--app/assets/javascripts/admin/broadcast_messages/components/messages_table.vue19
-rw-r--r--app/assets/javascripts/admin/broadcast_messages/constants.js35
-rw-r--r--app/assets/javascripts/admin/broadcast_messages/edit.js43
-rw-r--r--app/assets/javascripts/admin/broadcast_messages/index.js5
-rw-r--r--app/assets/javascripts/alert_management/components/alert_management_table.vue7
-rw-r--r--app/assets/javascripts/alerts_settings/components/alerts_form.vue2
-rw-r--r--app/assets/javascripts/alerts_settings/components/alerts_settings_form.vue6
-rw-r--r--app/assets/javascripts/alerts_settings/components/alerts_settings_wrapper.vue1
-rw-r--r--app/assets/javascripts/analytics/cycle_analytics/components/base.vue (renamed from app/assets/javascripts/cycle_analytics/components/base.vue)6
-rw-r--r--app/assets/javascripts/analytics/cycle_analytics/components/filter_bar.vue (renamed from app/assets/javascripts/cycle_analytics/components/filter_bar.vue)55
-rw-r--r--app/assets/javascripts/analytics/cycle_analytics/components/formatted_stage_count.vue (renamed from app/assets/javascripts/cycle_analytics/components/formatted_stage_count.vue)0
-rw-r--r--app/assets/javascripts/analytics/cycle_analytics/components/metric_tile.vue (renamed from app/assets/javascripts/cycle_analytics/components/metric_tile.vue)0
-rw-r--r--app/assets/javascripts/analytics/cycle_analytics/components/path_navigation.vue (renamed from app/assets/javascripts/cycle_analytics/components/path_navigation.vue)3
-rw-r--r--app/assets/javascripts/analytics/cycle_analytics/components/stage_table.vue (renamed from app/assets/javascripts/cycle_analytics/components/stage_table.vue)2
-rw-r--r--app/assets/javascripts/analytics/cycle_analytics/components/total_time.vue (renamed from app/assets/javascripts/cycle_analytics/components/total_time.vue)0
-rw-r--r--app/assets/javascripts/analytics/cycle_analytics/components/value_stream_filters.vue (renamed from app/assets/javascripts/cycle_analytics/components/value_stream_filters.vue)0
-rw-r--r--app/assets/javascripts/analytics/cycle_analytics/constants.js (renamed from app/assets/javascripts/cycle_analytics/constants.js)0
-rw-r--r--app/assets/javascripts/analytics/cycle_analytics/index.js (renamed from app/assets/javascripts/cycle_analytics/index.js)2
-rw-r--r--app/assets/javascripts/analytics/cycle_analytics/store/actions.js (renamed from app/assets/javascripts/cycle_analytics/store/actions.js)0
-rw-r--r--app/assets/javascripts/analytics/cycle_analytics/store/getters.js (renamed from app/assets/javascripts/cycle_analytics/store/getters.js)0
-rw-r--r--app/assets/javascripts/analytics/cycle_analytics/store/index.js (renamed from app/assets/javascripts/cycle_analytics/store/index.js)0
-rw-r--r--app/assets/javascripts/analytics/cycle_analytics/store/mutation_types.js (renamed from app/assets/javascripts/cycle_analytics/store/mutation_types.js)0
-rw-r--r--app/assets/javascripts/analytics/cycle_analytics/store/mutations.js (renamed from app/assets/javascripts/cycle_analytics/store/mutations.js)0
-rw-r--r--app/assets/javascripts/analytics/cycle_analytics/store/state.js (renamed from app/assets/javascripts/cycle_analytics/store/state.js)2
-rw-r--r--app/assets/javascripts/analytics/cycle_analytics/utils.js (renamed from app/assets/javascripts/cycle_analytics/utils.js)0
-rw-r--r--app/assets/javascripts/api/analytics_api.js5
-rw-r--r--app/assets/javascripts/artifacts/graphql/queries/get_job_artifacts.query.graphql1
-rw-r--r--app/assets/javascripts/awards_handler.js8
-rw-r--r--app/assets/javascripts/badges/components/badge_form.vue12
-rw-r--r--app/assets/javascripts/badges/constants.js8
-rw-r--r--app/assets/javascripts/batch_comments/components/draft_note.vue50
-rw-r--r--app/assets/javascripts/behaviors/copy_code.js5
-rw-r--r--app/assets/javascripts/behaviors/index.js1
-rw-r--r--app/assets/javascripts/behaviors/markdown/init_gfm.js13
-rw-r--r--app/assets/javascripts/behaviors/markdown/render_gfm.js77
-rw-r--r--app/assets/javascripts/behaviors/markdown/render_math.js6
-rw-r--r--app/assets/javascripts/behaviors/markdown/render_observability.js33
-rw-r--r--app/assets/javascripts/behaviors/preview_markdown.js1
-rw-r--r--app/assets/javascripts/behaviors/shortcuts/shortcuts_issuable.js2
-rw-r--r--app/assets/javascripts/blob/components/blob_header.vue2
-rw-r--r--app/assets/javascripts/blob/openapi/index.js2
-rw-r--r--app/assets/javascripts/blob/viewer/index.js2
-rw-r--r--app/assets/javascripts/blob_edit/blob_bundle.js2
-rw-r--r--app/assets/javascripts/blob_edit/edit_blob.js1
-rw-r--r--app/assets/javascripts/boards/components/board_content.vue10
-rw-r--r--app/assets/javascripts/boards/components/board_content_sidebar.vue25
-rw-r--r--app/assets/javascripts/boards/components/board_list.vue4
-rw-r--r--app/assets/javascripts/boards/components/boards_selector.vue2
-rw-r--r--app/assets/javascripts/boards/components/issue_board_filtered_search.vue30
-rw-r--r--app/assets/javascripts/boards/components/sidebar/board_sidebar_time_tracker.vue3
-rw-r--r--app/assets/javascripts/boards/issue_board_filters.js6
-rw-r--r--app/assets/javascripts/boards/stores/actions.js1
-rw-r--r--app/assets/javascripts/branches/components/sort_dropdown.vue30
-rw-r--r--app/assets/javascripts/branches/init_new_branch_ref_selector.js25
-rw-r--r--app/assets/javascripts/ci/ci_lint/components/ci_lint.vue (renamed from app/assets/javascripts/ci_lint/components/ci_lint.vue)4
-rw-r--r--app/assets/javascripts/ci/ci_lint/index.js (renamed from app/assets/javascripts/ci_lint/index.js)2
-rw-r--r--app/assets/javascripts/ci/pipeline_editor/components/code_snippet_alert/code_snippet_alert.vue (renamed from app/assets/javascripts/pipeline_editor/components/code_snippet_alert/code_snippet_alert.vue)0
-rw-r--r--app/assets/javascripts/ci/pipeline_editor/components/code_snippet_alert/constants.js (renamed from app/assets/javascripts/pipeline_editor/components/code_snippet_alert/constants.js)0
-rw-r--r--app/assets/javascripts/ci/pipeline_editor/components/commit/commit_form.vue (renamed from app/assets/javascripts/pipeline_editor/components/commit/commit_form.vue)0
-rw-r--r--app/assets/javascripts/ci/pipeline_editor/components/commit/commit_section.vue (renamed from app/assets/javascripts/pipeline_editor/components/commit/commit_section.vue)0
-rw-r--r--app/assets/javascripts/ci/pipeline_editor/components/drawer/cards/first_pipeline_card.vue (renamed from app/assets/javascripts/pipeline_editor/components/drawer/cards/first_pipeline_card.vue)0
-rw-r--r--app/assets/javascripts/ci/pipeline_editor/components/drawer/cards/getting_started_card.vue (renamed from app/assets/javascripts/pipeline_editor/components/drawer/cards/getting_started_card.vue)0
-rw-r--r--app/assets/javascripts/ci/pipeline_editor/components/drawer/cards/pipeline_config_reference_card.vue (renamed from app/assets/javascripts/pipeline_editor/components/drawer/cards/pipeline_config_reference_card.vue)0
-rw-r--r--app/assets/javascripts/ci/pipeline_editor/components/drawer/cards/visualize_and_lint_card.vue (renamed from app/assets/javascripts/pipeline_editor/components/drawer/cards/visualize_and_lint_card.vue)0
-rw-r--r--app/assets/javascripts/ci/pipeline_editor/components/drawer/pipeline_editor_drawer.vue (renamed from app/assets/javascripts/pipeline_editor/components/drawer/pipeline_editor_drawer.vue)0
-rw-r--r--app/assets/javascripts/ci/pipeline_editor/components/drawer/ui/demo_job_pill.vue (renamed from app/assets/javascripts/pipeline_editor/components/drawer/ui/demo_job_pill.vue)0
-rw-r--r--app/assets/javascripts/ci/pipeline_editor/components/editor/ci_config_merged_preview.vue (renamed from app/assets/javascripts/pipeline_editor/components/editor/ci_config_merged_preview.vue)0
-rw-r--r--app/assets/javascripts/ci/pipeline_editor/components/editor/ci_editor_header.vue (renamed from app/assets/javascripts/pipeline_editor/components/editor/ci_editor_header.vue)4
-rw-r--r--app/assets/javascripts/ci/pipeline_editor/components/editor/text_editor.vue (renamed from app/assets/javascripts/pipeline_editor/components/editor/text_editor.vue)0
-rw-r--r--app/assets/javascripts/ci/pipeline_editor/components/file_nav/branch_switcher.vue (renamed from app/assets/javascripts/pipeline_editor/components/file_nav/branch_switcher.vue)10
-rw-r--r--app/assets/javascripts/ci/pipeline_editor/components/file_nav/pipeline_editor_file_nav.vue (renamed from app/assets/javascripts/pipeline_editor/components/file_nav/pipeline_editor_file_nav.vue)2
-rw-r--r--app/assets/javascripts/ci/pipeline_editor/components/file_tree/container.vue (renamed from app/assets/javascripts/pipeline_editor/components/file_tree/container.vue)0
-rw-r--r--app/assets/javascripts/ci/pipeline_editor/components/file_tree/file_item.vue (renamed from app/assets/javascripts/pipeline_editor/components/file_tree/file_item.vue)0
-rw-r--r--app/assets/javascripts/ci/pipeline_editor/components/header/pipeline_editor_header.vue (renamed from app/assets/javascripts/pipeline_editor/components/header/pipeline_editor_header.vue)0
-rw-r--r--app/assets/javascripts/ci/pipeline_editor/components/header/pipeline_editor_mini_graph.vue (renamed from app/assets/javascripts/pipeline_editor/components/header/pipeline_editor_mini_graph.vue)0
-rw-r--r--app/assets/javascripts/ci/pipeline_editor/components/header/pipeline_status.vue (renamed from app/assets/javascripts/pipeline_editor/components/header/pipeline_status.vue)4
-rw-r--r--app/assets/javascripts/ci/pipeline_editor/components/header/validation_segment.vue (renamed from app/assets/javascripts/pipeline_editor/components/header/validation_segment.vue)2
-rw-r--r--app/assets/javascripts/ci/pipeline_editor/components/lint/ci_lint_results.vue (renamed from app/assets/javascripts/pipeline_editor/components/lint/ci_lint_results.vue)0
-rw-r--r--app/assets/javascripts/ci/pipeline_editor/components/lint/ci_lint_results_param.vue (renamed from app/assets/javascripts/pipeline_editor/components/lint/ci_lint_results_param.vue)0
-rw-r--r--app/assets/javascripts/ci/pipeline_editor/components/lint/ci_lint_results_value.vue (renamed from app/assets/javascripts/pipeline_editor/components/lint/ci_lint_results_value.vue)0
-rw-r--r--app/assets/javascripts/ci/pipeline_editor/components/lint/ci_lint_warnings.vue (renamed from app/assets/javascripts/pipeline_editor/components/lint/ci_lint_warnings.vue)0
-rw-r--r--app/assets/javascripts/ci/pipeline_editor/components/pipeline_editor_tabs.vue (renamed from app/assets/javascripts/pipeline_editor/components/pipeline_editor_tabs.vue)0
-rw-r--r--app/assets/javascripts/ci/pipeline_editor/components/popovers/file_tree_popover.vue (renamed from app/assets/javascripts/pipeline_editor/components/popovers/file_tree_popover.vue)0
-rw-r--r--app/assets/javascripts/ci/pipeline_editor/components/popovers/validate_pipeline_popover.vue (renamed from app/assets/javascripts/pipeline_editor/components/popovers/validate_pipeline_popover.vue)0
-rw-r--r--app/assets/javascripts/ci/pipeline_editor/components/popovers/walkthrough_popover.vue (renamed from app/assets/javascripts/pipeline_editor/components/popovers/walkthrough_popover.vue)0
-rw-r--r--app/assets/javascripts/ci/pipeline_editor/components/ui/confirm_unsaved_changes_dialog.vue (renamed from app/assets/javascripts/pipeline_editor/components/ui/confirm_unsaved_changes_dialog.vue)0
-rw-r--r--app/assets/javascripts/ci/pipeline_editor/components/ui/editor_tab.vue (renamed from app/assets/javascripts/pipeline_editor/components/ui/editor_tab.vue)4
-rw-r--r--app/assets/javascripts/ci/pipeline_editor/components/ui/pipeline_editor_empty_state.vue (renamed from app/assets/javascripts/pipeline_editor/components/ui/pipeline_editor_empty_state.vue)2
-rw-r--r--app/assets/javascripts/ci/pipeline_editor/components/ui/pipeline_editor_messages.vue (renamed from app/assets/javascripts/pipeline_editor/components/ui/pipeline_editor_messages.vue)0
-rw-r--r--app/assets/javascripts/ci/pipeline_editor/components/validate/ci_validate.vue (renamed from app/assets/javascripts/pipeline_editor/components/validate/ci_validate.vue)0
-rw-r--r--app/assets/javascripts/ci/pipeline_editor/constants.js (renamed from app/assets/javascripts/pipeline_editor/constants.js)0
-rw-r--r--app/assets/javascripts/ci/pipeline_editor/graphql/mutations/client/lint_ci.mutation.graphql (renamed from app/assets/javascripts/pipeline_editor/graphql/mutations/client/lint_ci.mutation.graphql)0
-rw-r--r--app/assets/javascripts/ci/pipeline_editor/graphql/mutations/client/update_app_status.mutation.graphql (renamed from app/assets/javascripts/pipeline_editor/graphql/mutations/client/update_app_status.mutation.graphql)0
-rw-r--r--app/assets/javascripts/ci/pipeline_editor/graphql/mutations/client/update_current_branch.mutation.graphql (renamed from app/assets/javascripts/pipeline_editor/graphql/mutations/client/update_current_branch.mutation.graphql)0
-rw-r--r--app/assets/javascripts/ci/pipeline_editor/graphql/mutations/client/update_last_commit_branch.mutation.graphql (renamed from app/assets/javascripts/pipeline_editor/graphql/mutations/client/update_last_commit_branch.mutation.graphql)0
-rw-r--r--app/assets/javascripts/ci/pipeline_editor/graphql/mutations/client/update_pipeline_etag.mutation.graphql (renamed from app/assets/javascripts/pipeline_editor/graphql/mutations/client/update_pipeline_etag.mutation.graphql)0
-rw-r--r--app/assets/javascripts/ci/pipeline_editor/graphql/mutations/commit_ci_file.mutation.graphql (renamed from app/assets/javascripts/pipeline_editor/graphql/mutations/commit_ci_file.mutation.graphql)0
-rw-r--r--app/assets/javascripts/ci/pipeline_editor/graphql/queries/available_branches.query.graphql (renamed from app/assets/javascripts/pipeline_editor/graphql/queries/available_branches.query.graphql)0
-rw-r--r--app/assets/javascripts/ci/pipeline_editor/graphql/queries/blob_content.query.graphql (renamed from app/assets/javascripts/pipeline_editor/graphql/queries/blob_content.query.graphql)0
-rw-r--r--app/assets/javascripts/ci/pipeline_editor/graphql/queries/ci_config.query.graphql (renamed from app/assets/javascripts/pipeline_editor/graphql/queries/ci_config.query.graphql)0
-rw-r--r--app/assets/javascripts/ci/pipeline_editor/graphql/queries/client/app_status.query.graphql (renamed from app/assets/javascripts/pipeline_editor/graphql/queries/client/app_status.query.graphql)0
-rw-r--r--app/assets/javascripts/ci/pipeline_editor/graphql/queries/client/current_branch.query.graphql (renamed from app/assets/javascripts/pipeline_editor/graphql/queries/client/current_branch.query.graphql)0
-rw-r--r--app/assets/javascripts/ci/pipeline_editor/graphql/queries/client/last_commit_branch.query.graphql (renamed from app/assets/javascripts/pipeline_editor/graphql/queries/client/last_commit_branch.query.graphql)0
-rw-r--r--app/assets/javascripts/ci/pipeline_editor/graphql/queries/client/pipeline_etag.query.graphql (renamed from app/assets/javascripts/pipeline_editor/graphql/queries/client/pipeline_etag.query.graphql)0
-rw-r--r--app/assets/javascripts/ci/pipeline_editor/graphql/queries/get_starter_template.query.graphql (renamed from app/assets/javascripts/pipeline_editor/graphql/queries/get_starter_template.query.graphql)0
-rw-r--r--app/assets/javascripts/ci/pipeline_editor/graphql/queries/latest_commit_sha.query.graphql (renamed from app/assets/javascripts/pipeline_editor/graphql/queries/latest_commit_sha.query.graphql)0
-rw-r--r--app/assets/javascripts/ci/pipeline_editor/graphql/queries/pipeline.query.graphql (renamed from app/assets/javascripts/pipeline_editor/graphql/queries/pipeline.query.graphql)0
-rw-r--r--app/assets/javascripts/ci/pipeline_editor/graphql/resolvers.js (renamed from app/assets/javascripts/pipeline_editor/graphql/resolvers.js)0
-rw-r--r--app/assets/javascripts/ci/pipeline_editor/graphql/typedefs.graphql (renamed from app/assets/javascripts/pipeline_editor/graphql/typedefs.graphql)0
-rw-r--r--app/assets/javascripts/ci/pipeline_editor/index.js (renamed from app/assets/javascripts/pipeline_editor/index.js)0
-rw-r--r--app/assets/javascripts/ci/pipeline_editor/pipeline_editor_app.vue (renamed from app/assets/javascripts/pipeline_editor/pipeline_editor_app.vue)0
-rw-r--r--app/assets/javascripts/ci/pipeline_editor/pipeline_editor_home.vue (renamed from app/assets/javascripts/pipeline_editor/pipeline_editor_home.vue)0
-rw-r--r--app/assets/javascripts/ci/pipeline_schedules/components/pipeline_schedules_form.vue311
-rw-r--r--app/assets/javascripts/ci/pipeline_schedules/constants.js2
-rw-r--r--app/assets/javascripts/ci/pipeline_schedules/mount_pipeline_schedules_form_app.js24
-rw-r--r--app/assets/javascripts/ci/reports/codequality_report/components/codequality_issue_body.vue (renamed from app/assets/javascripts/reports/codequality_report/components/codequality_issue_body.vue)4
-rw-r--r--app/assets/javascripts/ci/reports/codequality_report/constants.js (renamed from app/assets/javascripts/reports/codequality_report/constants.js)34
-rw-r--r--app/assets/javascripts/ci/reports/codequality_report/store/actions.js (renamed from app/assets/javascripts/reports/codequality_report/store/actions.js)0
-rw-r--r--app/assets/javascripts/ci/reports/codequality_report/store/getters.js (renamed from app/assets/javascripts/reports/codequality_report/store/getters.js)0
-rw-r--r--app/assets/javascripts/ci/reports/codequality_report/store/index.js (renamed from app/assets/javascripts/reports/codequality_report/store/index.js)0
-rw-r--r--app/assets/javascripts/ci/reports/codequality_report/store/mutation_types.js (renamed from app/assets/javascripts/reports/codequality_report/store/mutation_types.js)0
-rw-r--r--app/assets/javascripts/ci/reports/codequality_report/store/mutations.js (renamed from app/assets/javascripts/reports/codequality_report/store/mutations.js)0
-rw-r--r--app/assets/javascripts/ci/reports/codequality_report/store/state.js (renamed from app/assets/javascripts/reports/codequality_report/store/state.js)0
-rw-r--r--app/assets/javascripts/ci/reports/codequality_report/store/utils/codequality_parser.js (renamed from app/assets/javascripts/reports/codequality_report/store/utils/codequality_parser.js)0
-rw-r--r--app/assets/javascripts/ci/reports/components/grouped_issues_list.vue (renamed from app/assets/javascripts/reports/components/grouped_issues_list.vue)2
-rw-r--r--app/assets/javascripts/ci/reports/components/issue_body.js (renamed from app/assets/javascripts/reports/components/issue_body.js)2
-rw-r--r--app/assets/javascripts/ci/reports/components/issue_status_icon.vue (renamed from app/assets/javascripts/reports/components/issue_status_icon.vue)0
-rw-r--r--app/assets/javascripts/ci/reports/components/issues_list.vue (renamed from app/assets/javascripts/reports/components/issues_list.vue)4
-rw-r--r--app/assets/javascripts/ci/reports/components/report_item.vue (renamed from app/assets/javascripts/reports/components/report_item.vue)2
-rw-r--r--app/assets/javascripts/ci/reports/components/report_link.vue (renamed from app/assets/javascripts/reports/components/report_link.vue)0
-rw-r--r--app/assets/javascripts/ci/reports/components/report_section.vue (renamed from app/assets/javascripts/reports/components/report_section.vue)0
-rw-r--r--app/assets/javascripts/ci/reports/components/summary_row.vue (renamed from app/assets/javascripts/reports/components/summary_row.vue)0
-rw-r--r--app/assets/javascripts/ci/reports/constants.js (renamed from app/assets/javascripts/reports/constants.js)0
-rw-r--r--app/assets/javascripts/ci/runner/admin_runner_show/admin_runner_show_app.vue53
-rw-r--r--app/assets/javascripts/ci/runner/admin_runner_show/index.js2
-rw-r--r--app/assets/javascripts/ci/runner/admin_runners/admin_runners_app.vue17
-rw-r--r--app/assets/javascripts/ci/runner/components/cells/runner_status_cell.vue6
-rw-r--r--app/assets/javascripts/ci/runner/components/cells/runner_summary_cell.vue (renamed from app/assets/javascripts/ci/runner/components/cells/runner_stacked_summary_cell.vue)15
-rw-r--r--app/assets/javascripts/ci/runner/components/runner_detail.vue2
-rw-r--r--app/assets/javascripts/ci/runner/components/runner_groups.vue2
-rw-r--r--app/assets/javascripts/ci/runner/components/runner_job_status_badge.vue55
-rw-r--r--app/assets/javascripts/ci/runner/components/runner_list.vue11
-rw-r--r--app/assets/javascripts/ci/runner/components/runner_projects.vue2
-rw-r--r--app/assets/javascripts/ci/runner/components/runner_type_tabs.vue9
-rw-r--r--app/assets/javascripts/ci/runner/components/search_tokens/paused_token_config.js4
-rw-r--r--app/assets/javascripts/ci/runner/components/search_tokens/status_token_config.js4
-rw-r--r--app/assets/javascripts/ci/runner/components/search_tokens/tag_token_config.js4
-rw-r--r--app/assets/javascripts/ci/runner/components/stat/runner_count.vue4
-rw-r--r--app/assets/javascripts/ci/runner/components/stat/runner_stats.vue41
-rw-r--r--app/assets/javascripts/ci/runner/constants.js11
-rw-r--r--app/assets/javascripts/ci/runner/graphql/list/list_item_shared.fragment.graphql1
-rw-r--r--app/assets/javascripts/ci/runner/group_runners/group_runners_app.vue3
-rw-r--r--app/assets/javascripts/ci/runner/runner_search_utils.js1
-rw-r--r--app/assets/javascripts/ci_secure_files/components/secure_files_list.vue4
-rw-r--r--app/assets/javascripts/ci_variable_list/components/ci_group_variables.vue1
-rw-r--r--app/assets/javascripts/ci_variable_list/components/ci_project_variables.vue1
-rw-r--r--app/assets/javascripts/ci_variable_list/components/ci_variable_modal.vue43
-rw-r--r--app/assets/javascripts/ci_variable_list/components/ci_variable_settings.vue13
-rw-r--r--app/assets/javascripts/ci_variable_list/components/ci_variable_shared.vue10
-rw-r--r--app/assets/javascripts/ci_variable_list/components/ci_variable_table.vue71
-rw-r--r--app/assets/javascripts/ci_variable_list/constants.js18
-rw-r--r--app/assets/javascripts/ci_variable_list/graphql/mutations/group_add_variable.mutation.graphql1
-rw-r--r--app/assets/javascripts/ci_variable_list/graphql/mutations/group_delete_variable.mutation.graphql1
-rw-r--r--app/assets/javascripts/ci_variable_list/graphql/mutations/group_update_variable.mutation.graphql1
-rw-r--r--app/assets/javascripts/ci_variable_list/graphql/mutations/project_add_variable.mutation.graphql1
-rw-r--r--app/assets/javascripts/ci_variable_list/graphql/mutations/project_delete_variable.mutation.graphql1
-rw-r--r--app/assets/javascripts/ci_variable_list/graphql/mutations/project_update_variable.mutation.graphql1
-rw-r--r--app/assets/javascripts/ci_variable_list/graphql/queries/group_variables.query.graphql2
-rw-r--r--app/assets/javascripts/ci_variable_list/graphql/queries/project_variables.query.graphql2
-rw-r--r--app/assets/javascripts/ci_variable_list/graphql/queries/variables.query.graphql1
-rw-r--r--app/assets/javascripts/clusters_list/clusters_util.js8
-rw-r--r--app/assets/javascripts/clusters_list/components/agent_token.vue11
-rw-r--r--app/assets/javascripts/clusters_list/components/available_agents_dropdown.vue103
-rw-r--r--app/assets/javascripts/clusters_list/constants.js6
-rw-r--r--app/assets/javascripts/constants.js3
-rw-r--r--app/assets/javascripts/content_editor/components/bubble_menus/code_block_bubble_menu.vue4
-rw-r--r--app/assets/javascripts/content_editor/components/content_editor.vue14
-rw-r--r--app/assets/javascripts/content_editor/components/formatting_toolbar.vue (renamed from app/assets/javascripts/content_editor/components/top_toolbar.vue)4
-rw-r--r--app/assets/javascripts/content_editor/components/suggestions_dropdown.vue4
-rw-r--r--app/assets/javascripts/content_editor/components/toolbar_more_dropdown.vue3
-rw-r--r--app/assets/javascripts/content_editor/extensions/code_block_highlight.js4
-rw-r--r--app/assets/javascripts/content_editor/extensions/comment.js49
-rw-r--r--app/assets/javascripts/content_editor/extensions/image.js18
-rw-r--r--app/assets/javascripts/content_editor/extensions/reference_label.js2
-rw-r--r--app/assets/javascripts/content_editor/services/create_content_editor.js2
-rw-r--r--app/assets/javascripts/content_editor/services/gl_api_markdown_deserializer.js5
-rw-r--r--app/assets/javascripts/content_editor/services/markdown_serializer.js3
-rw-r--r--app/assets/javascripts/content_editor/services/serialization_helpers.js27
-rw-r--r--app/assets/javascripts/crm/components/crm_form.vue (renamed from app/assets/javascripts/crm/components/form.vue)0
-rw-r--r--app/assets/javascripts/crm/contacts/components/contact_form_wrapper.vue6
-rw-r--r--app/assets/javascripts/crm/organizations/components/organization_form_wrapper.vue6
-rw-r--r--app/assets/javascripts/custom_metrics/components/custom_metrics_form_fields.vue5
-rw-r--r--app/assets/javascripts/deploy_tokens/components/new_deploy_token.vue208
-rw-r--r--app/assets/javascripts/deploy_tokens/deploy_token_translations.js41
-rw-r--r--app/assets/javascripts/deprecated_jquery_dropdown/render.js2
-rw-r--r--app/assets/javascripts/deprecated_notes.js28
-rw-r--r--app/assets/javascripts/design_management/components/design_notes/design_discussion.vue2
-rw-r--r--app/assets/javascripts/design_management/components/design_notes/design_note.vue12
-rw-r--r--app/assets/javascripts/design_management/components/design_todo_button.vue2
-rw-r--r--app/assets/javascripts/design_management/components/upload/design_version_dropdown.vue92
-rw-r--r--app/assets/javascripts/design_management/pages/design/index.vue2
-rw-r--r--app/assets/javascripts/diffs/components/commit_item.vue5
-rw-r--r--app/assets/javascripts/diffs/components/diff_code_quality.vue39
-rw-r--r--app/assets/javascripts/diffs/components/diff_discussion_reply.vue17
-rw-r--r--app/assets/javascripts/diffs/components/diff_expansion_cell.vue5
-rw-r--r--app/assets/javascripts/diffs/components/diff_file.vue9
-rw-r--r--app/assets/javascripts/diffs/components/diff_file_header.vue4
-rw-r--r--app/assets/javascripts/diffs/components/diff_view.vue16
-rw-r--r--app/assets/javascripts/diffs/i18n.js3
-rw-r--r--app/assets/javascripts/diffs/index.js3
-rw-r--r--app/assets/javascripts/diffs/store/actions.js35
-rw-r--r--app/assets/javascripts/diffs/utils/merge_request.js18
-rw-r--r--app/assets/javascripts/editor/components/source_editor_toolbar.vue13
-rw-r--r--app/assets/javascripts/editor/components/source_editor_toolbar_button.vue17
-rw-r--r--app/assets/javascripts/editor/constants.js104
-rw-r--r--app/assets/javascripts/editor/extensions/source_editor_markdown_ext.js48
-rw-r--r--app/assets/javascripts/editor/extensions/source_editor_markdown_livepreview_ext.js3
-rw-r--r--app/assets/javascripts/editor/schema/ci.json213
-rw-r--r--app/assets/javascripts/environments/components/deploy_board.vue2
-rw-r--r--app/assets/javascripts/environments/environment_details/constants.js47
-rw-r--r--app/assets/javascripts/environments/environment_details/index.vue118
-rw-r--r--app/assets/javascripts/environments/graphql/queries/environment_details.query.graphql48
-rw-r--r--app/assets/javascripts/environments/helpers/deployment_data_transformation_helper.js62
-rw-r--r--app/assets/javascripts/environments/mount_show.js30
-rw-r--r--app/assets/javascripts/error_tracking/components/error_tracking_list.vue4
-rw-r--r--app/assets/javascripts/error_tracking/components/stacktrace_entry.vue3
-rw-r--r--app/assets/javascripts/feature_flags/components/feature_flags_table.vue14
-rw-r--r--app/assets/javascripts/feature_flags/components/strategies/flexible_rollout.vue2
-rw-r--r--app/assets/javascripts/feature_flags/components/strategies/percent_rollout.vue2
-rw-r--r--app/assets/javascripts/feature_flags/components/strategy_label.vue29
-rw-r--r--app/assets/javascripts/feature_flags/utils.js16
-rw-r--r--app/assets/javascripts/feature_highlight/feature_highlight_popover.vue9
-rw-r--r--app/assets/javascripts/filtered_search/add_extra_tokens_for_merge_requests.js31
-rw-r--r--app/assets/javascripts/filtered_search/available_dropdown_mappings.js34
-rw-r--r--app/assets/javascripts/filtered_search/constants.js15
-rw-r--r--app/assets/javascripts/filtered_search/dropdown_utils.js3
-rw-r--r--app/assets/javascripts/filtered_search/filtered_search_manager.js12
-rw-r--r--app/assets/javascripts/filtered_search/filtered_search_visual_tokens.js7
-rw-r--r--app/assets/javascripts/filtered_search/issuable_filtered_search_token_keys.js51
-rw-r--r--app/assets/javascripts/filtered_search/visual_token_value.js3
-rw-r--r--app/assets/javascripts/flash.js8
-rw-r--r--app/assets/javascripts/frequent_items/components/frequent_items_list_item.vue5
-rw-r--r--app/assets/javascripts/gfm_auto_complete.js19
-rw-r--r--app/assets/javascripts/gitlab_pages/components/pages_pipeline_wizard.vue2
-rw-r--r--app/assets/javascripts/gitlab_version_check/components/security_patch_upgrade_alert.vue76
-rw-r--r--app/assets/javascripts/gitlab_version_check/components/security_patch_upgrade_alert_modal.vue160
-rw-r--r--app/assets/javascripts/gitlab_version_check/constants.js22
-rw-r--r--app/assets/javascripts/gitlab_version_check/index.js116
-rw-r--r--app/assets/javascripts/gitlab_version_check/utils.js18
-rw-r--r--app/assets/javascripts/graphql_shared/fragments/alert.fragment.graphql1
-rw-r--r--app/assets/javascripts/graphql_shared/mutations/alert_status_update.mutation.graphql1
-rw-r--r--app/assets/javascripts/graphql_shared/possible_types.json7
-rw-r--r--app/assets/javascripts/graphql_shared/queries/alert_details.query.graphql1
-rw-r--r--app/assets/javascripts/groups/components/app.vue75
-rw-r--r--app/assets/javascripts/groups/components/empty_states/archived_projects_empty_state.vue21
-rw-r--r--app/assets/javascripts/groups/components/empty_states/shared_projects_empty_state.vue21
-rw-r--r--app/assets/javascripts/groups/components/empty_states/subgroups_and_projects_empty_state.vue (renamed from app/assets/javascripts/groups/components/empty_state.vue)1
-rw-r--r--app/assets/javascripts/groups/components/group_item.vue12
-rw-r--r--app/assets/javascripts/groups/components/group_name_and_path.vue2
-rw-r--r--app/assets/javascripts/groups/components/groups.vue23
-rw-r--r--app/assets/javascripts/groups/components/overview_tabs.vue35
-rw-r--r--app/assets/javascripts/groups/components/transfer_group_form.vue1
-rw-r--r--app/assets/javascripts/header.js21
-rw-r--r--app/assets/javascripts/header_search/components/app.vue3
-rw-r--r--app/assets/javascripts/header_search/components/header_search_autocomplete_items.vue2
-rw-r--r--app/assets/javascripts/header_search/constants.js3
-rw-r--r--app/assets/javascripts/ide/components/commit_sidebar/form.vue5
-rw-r--r--app/assets/javascripts/ide/components/commit_sidebar/success_message.vue2
-rw-r--r--app/assets/javascripts/ide/components/error_message.vue5
-rw-r--r--app/assets/javascripts/ide/components/jobs/detail.vue5
-rw-r--r--app/assets/javascripts/ide/components/new_dropdown/index.vue2
-rw-r--r--app/assets/javascripts/ide/components/new_dropdown/upload.vue4
-rw-r--r--app/assets/javascripts/ide/components/panes/right.vue17
-rw-r--r--app/assets/javascripts/ide/components/pipelines/list.vue26
-rw-r--r--app/assets/javascripts/ide/components/repo_editor.vue42
-rw-r--r--app/assets/javascripts/ide/components/switch_editors/switch_editors_view.vue103
-rw-r--r--app/assets/javascripts/ide/components/terminal/empty_state.vue5
-rw-r--r--app/assets/javascripts/ide/constants.js3
-rw-r--r--app/assets/javascripts/ide/index.js5
-rw-r--r--app/assets/javascripts/ide/init_gitlab_web_ide.js98
-rw-r--r--app/assets/javascripts/ide/lib/diff/controller.js2
-rw-r--r--app/assets/javascripts/ide/lib/editor_options.js11
-rw-r--r--app/assets/javascripts/ide/lib/gitlab_web_ide/get_base_config.js12
-rw-r--r--app/assets/javascripts/ide/lib/gitlab_web_ide/index.js2
-rw-r--r--app/assets/javascripts/ide/lib/gitlab_web_ide/setup_root_element.js14
-rw-r--r--app/assets/javascripts/ide/remote/index.js40
-rw-r--r--app/assets/javascripts/ide/services/index.js2
-rw-r--r--app/assets/javascripts/ide/stores/modules/terminal/actions/session_controls.js4
-rw-r--r--app/assets/javascripts/ide/stores/modules/terminal/messages.js4
-rw-r--r--app/assets/javascripts/ide/stores/state.js1
-rw-r--r--app/assets/javascripts/import_entities/components/group_dropdown.vue44
-rw-r--r--app/assets/javascripts/import_entities/components/import_status.vue2
-rw-r--r--app/assets/javascripts/import_entities/constants.js6
-rw-r--r--app/assets/javascripts/import_entities/import_groups/components/import_table.vue40
-rw-r--r--app/assets/javascripts/import_entities/import_groups/components/import_target_cell.vue5
-rw-r--r--app/assets/javascripts/import_entities/import_groups/graphql/client_factory.js10
-rw-r--r--app/assets/javascripts/import_entities/import_groups/graphql/queries/available_namespaces.query.graphql6
-rw-r--r--app/assets/javascripts/import_entities/import_groups/index.js2
-rw-r--r--app/assets/javascripts/import_entities/import_projects/components/import_projects_table.vue18
-rw-r--r--app/assets/javascripts/import_entities/import_projects/components/provider_repo_table_row.vue52
-rw-r--r--app/assets/javascripts/import_entities/import_projects/index.js16
-rw-r--r--app/assets/javascripts/import_entities/import_projects/store/actions.js86
-rw-r--r--app/assets/javascripts/import_entities/import_projects/store/getters.js2
-rw-r--r--app/assets/javascripts/import_entities/import_projects/store/mutation_types.js8
-rw-r--r--app/assets/javascripts/import_entities/import_projects/store/mutations.js30
-rw-r--r--app/assets/javascripts/import_entities/import_projects/store/state.js5
-rw-r--r--app/assets/javascripts/import_entities/import_projects/utils.js5
-rw-r--r--app/assets/javascripts/incidents/components/incidents_list.vue2
-rw-r--r--app/assets/javascripts/incidents_settings/incidents_settings_service.js4
-rw-r--r--app/assets/javascripts/integrations/edit/components/dynamic_field.vue28
-rw-r--r--app/assets/javascripts/integrations/edit/components/integration_form.vue149
-rw-r--r--app/assets/javascripts/integrations/edit/components/integration_form_actions.vue143
-rw-r--r--app/assets/javascripts/integrations/edit/index.js1
-rw-r--r--app/assets/javascripts/invite_members/components/import_project_members_modal.vue23
-rw-r--r--app/assets/javascripts/invite_members/components/invite_group_notification.vue37
-rw-r--r--app/assets/javascripts/invite_members/components/invite_groups_modal.vue37
-rw-r--r--app/assets/javascripts/invite_members/components/invite_members_modal.vue161
-rw-r--r--app/assets/javascripts/invite_members/components/invite_modal_base.vue107
-rw-r--r--app/assets/javascripts/invite_members/components/members_token_select.vue17
-rw-r--r--app/assets/javascripts/invite_members/constants.js11
-rw-r--r--app/assets/javascripts/invite_members/init_import_project_members_modal.js4
-rw-r--r--app/assets/javascripts/invite_members/init_invite_groups_modal.js5
-rw-r--r--app/assets/javascripts/invite_members/init_invite_members_modal.js1
-rw-r--r--app/assets/javascripts/invite_members/utils/trigger_successful_invite_alert.js23
-rw-r--r--app/assets/javascripts/issuable/bulk_update_sidebar/constants.js23
-rw-r--r--app/assets/javascripts/issuable/bulk_update_sidebar/index.js75
-rw-r--r--app/assets/javascripts/issuable/components/related_issuable_item.vue10
-rw-r--r--app/assets/javascripts/issuable/index.js15
-rw-r--r--app/assets/javascripts/issuable/issuable_bulk_update_actions.js (renamed from app/assets/javascripts/issuable/bulk_update_sidebar/issuable_bulk_update_actions.js)4
-rw-r--r--app/assets/javascripts/issuable/issuable_bulk_update_sidebar.js (renamed from app/assets/javascripts/issuable/bulk_update_sidebar/issuable_bulk_update_sidebar.js)10
-rw-r--r--app/assets/javascripts/issuable/issuable_label_selector.js56
-rw-r--r--app/assets/javascripts/issues/create_merge_request_dropdown.js8
-rw-r--r--app/assets/javascripts/issues/dashboard/components/issues_dashboard_app.vue271
-rw-r--r--app/assets/javascripts/issues/dashboard/index.js26
-rw-r--r--app/assets/javascripts/issues/dashboard/queries/get_issues.query.graphql36
-rw-r--r--app/assets/javascripts/issues/index.js2
-rw-r--r--app/assets/javascripts/issues/issue.js6
-rw-r--r--app/assets/javascripts/issues/list/components/empty_state_with_any_issues.vue53
-rw-r--r--app/assets/javascripts/issues/list/components/empty_state_without_any_issues.vue110
-rw-r--r--app/assets/javascripts/issues/list/components/issue_card_statistics.vue56
-rw-r--r--app/assets/javascripts/issues/list/components/issues_list_app.vue360
-rw-r--r--app/assets/javascripts/issues/list/components/new_issue_dropdown.vue4
-rw-r--r--app/assets/javascripts/issues/list/constants.js155
-rw-r--r--app/assets/javascripts/issues/list/index.js25
-rw-r--r--app/assets/javascripts/issues/list/queries/get_issues.query.graphql3
-rw-r--r--app/assets/javascripts/issues/list/utils.js64
-rw-r--r--app/assets/javascripts/issues/manual_ordering.js4
-rw-r--r--app/assets/javascripts/issues/related_merge_requests/store/actions.js4
-rw-r--r--app/assets/javascripts/issues/show/components/app.vue8
-rw-r--r--app/assets/javascripts/issues/show/components/description.vue14
-rw-r--r--app/assets/javascripts/issues/show/components/fields/description.vue1
-rw-r--r--app/assets/javascripts/issues/show/components/form.vue2
-rw-r--r--app/assets/javascripts/issues/show/components/header_actions.vue15
-rw-r--r--app/assets/javascripts/issues/show/components/incidents/constants.js1
-rw-r--r--app/assets/javascripts/issues/show/components/incidents/edit_timeline_event.vue2
-rw-r--r--app/assets/javascripts/issues/show/components/incidents/graphql/queries/create_timeline_event.mutation.graphql6
-rw-r--r--app/assets/javascripts/issues/show/components/incidents/graphql/queries/get_alert.graphql1
-rw-r--r--app/assets/javascripts/issues/show/components/incidents/graphql/queries/get_timeline_events.query.graphql6
-rw-r--r--app/assets/javascripts/issues/show/components/incidents/incident_tabs.vue73
-rw-r--r--app/assets/javascripts/issues/show/components/incidents/timeline_events_form.vue73
-rw-r--r--app/assets/javascripts/issues/show/components/incidents/timeline_events_item.vue59
-rw-r--r--app/assets/javascripts/issues/show/components/incidents/timeline_events_list.vue5
-rw-r--r--app/assets/javascripts/issues/show/components/incidents/timeline_events_tab.vue7
-rw-r--r--app/assets/javascripts/issues/show/components/locked_warning.vue27
-rw-r--r--app/assets/javascripts/issues/show/components/title.vue3
-rw-r--r--app/assets/javascripts/jira_connect/branches/components/source_branch_dropdown.vue55
-rw-r--r--app/assets/javascripts/jira_connect/subscriptions/constants.js2
-rw-r--r--app/assets/javascripts/jira_connect/subscriptions/pages/sign_in/sign_in_gitlab_multiversion/index.vue42
-rw-r--r--app/assets/javascripts/jira_connect/subscriptions/pages/sign_in/sign_in_gitlab_multiversion/setup_instructions.vue35
-rw-r--r--app/assets/javascripts/jira_connect/subscriptions/pages/sign_in/sign_in_gitlab_multiversion/version_select_form.vue1
-rw-r--r--app/assets/javascripts/jobs/components/filtered_search/jobs_filtered_search.vue18
-rw-r--r--app/assets/javascripts/jobs/components/job/empty_state.vue31
-rw-r--r--app/assets/javascripts/jobs/components/job/graphql/mutations/job_retry_with_variables.mutation.graphql16
-rw-r--r--app/assets/javascripts/jobs/components/job/graphql/queries/get_job.query.graphql17
-rw-r--r--app/assets/javascripts/jobs/components/job/job_app.vue25
-rw-r--r--app/assets/javascripts/jobs/components/job/legacy_manual_variables_form.vue192
-rw-r--r--app/assets/javascripts/jobs/components/job/manual_variables_form.vue163
-rw-r--r--app/assets/javascripts/jobs/components/job/sidebar/job_sidebar_retry_button.vue29
-rw-r--r--app/assets/javascripts/jobs/components/job/sidebar/legacy_sidebar_header.vue104
-rw-r--r--app/assets/javascripts/jobs/components/job/sidebar/sidebar.vue21
-rw-r--r--app/assets/javascripts/jobs/components/job/sidebar/sidebar_header.vue87
-rw-r--r--app/assets/javascripts/jobs/components/job/sidebar/sidebar_job_details_container.vue7
-rw-r--r--app/assets/javascripts/jobs/components/job/sidebar/trigger_block.vue6
-rw-r--r--app/assets/javascripts/jobs/constants.js15
-rw-r--r--app/assets/javascripts/jobs/index.js9
-rw-r--r--app/assets/javascripts/labels/labels_select.js2
-rw-r--r--app/assets/javascripts/language_switcher/components/app.vue49
-rw-r--r--app/assets/javascripts/language_switcher/constants.js1
-rw-r--r--app/assets/javascripts/language_switcher/index.js23
-rw-r--r--app/assets/javascripts/lib/dompurify.js2
-rw-r--r--app/assets/javascripts/lib/utils/common_utils.js27
-rw-r--r--app/assets/javascripts/lib/utils/confirm_via_gl_modal/confirm_modal.vue5
-rw-r--r--app/assets/javascripts/lib/utils/constants.js1
-rw-r--r--app/assets/javascripts/lib/utils/create_and_submit_form.js26
-rw-r--r--app/assets/javascripts/lib/utils/dom_utils.js21
-rw-r--r--app/assets/javascripts/lib/utils/http_status.js56
-rw-r--r--app/assets/javascripts/lib/utils/poll.js4
-rw-r--r--app/assets/javascripts/lib/utils/url_utility.js2
-rw-r--r--app/assets/javascripts/listbox/index.js4
-rw-r--r--app/assets/javascripts/listbox/redirect_behavior.js2
-rw-r--r--app/assets/javascripts/main.js17
-rw-r--r--app/assets/javascripts/members/components/avatars/user_avatar.vue8
-rw-r--r--app/assets/javascripts/members/components/filter_sort/members_filtered_search_bar.vue6
-rw-r--r--app/assets/javascripts/members/constants.js8
-rw-r--r--app/assets/javascripts/merge_conflicts/components/inline_conflict_lines.vue2
-rw-r--r--app/assets/javascripts/merge_conflicts/components/parallel_conflict_lines.vue2
-rw-r--r--app/assets/javascripts/merge_request.js8
-rw-r--r--app/assets/javascripts/merge_request_tabs.js63
-rw-r--r--app/assets/javascripts/merge_requests/components/sticky_header.vue11
-rw-r--r--app/assets/javascripts/merge_requests/components/target_project_dropdown.vue87
-rw-r--r--app/assets/javascripts/ml/experiment_tracking/components/experiment.vue36
-rw-r--r--app/assets/javascripts/ml/experiment_tracking/components/incubation_alert.vue6
-rw-r--r--app/assets/javascripts/ml/experiment_tracking/components/ml_candidate.vue94
-rw-r--r--app/assets/javascripts/ml/experiment_tracking/components/ml_experiment.vue59
-rw-r--r--app/assets/javascripts/monitoring/components/charts/empty_chart.vue4
-rw-r--r--app/assets/javascripts/monitoring/components/dashboard.vue6
-rw-r--r--app/assets/javascripts/monitoring/components/dashboard_actions_menu.vue2
-rw-r--r--app/assets/javascripts/monitoring/components/dashboard_header.vue3
-rw-r--r--app/assets/javascripts/monitoring/components/dashboard_panel.vue8
-rw-r--r--app/assets/javascripts/monitoring/components/dashboard_panel_builder.vue2
-rw-r--r--app/assets/javascripts/monitoring/components/duplicate_dashboard_form.vue8
-rw-r--r--app/assets/javascripts/monitoring/components/group_empty_state.vue3
-rw-r--r--app/assets/javascripts/monitoring/components/refresh_button.vue16
-rw-r--r--app/assets/javascripts/monitoring/components/variables_section.vue7
-rw-r--r--app/assets/javascripts/monitoring/csv_export.js2
-rw-r--r--app/assets/javascripts/monitoring/requests/index.js9
-rw-r--r--app/assets/javascripts/monitoring/utils.js1
-rw-r--r--app/assets/javascripts/mr_notes/discussion_counter.js28
-rw-r--r--app/assets/javascripts/mr_notes/index.js36
-rw-r--r--app/assets/javascripts/mr_notes/init.js52
-rw-r--r--app/assets/javascripts/mr_notes/init_count.js13
-rw-r--r--app/assets/javascripts/mr_notes/init_notes.js33
-rw-r--r--app/assets/javascripts/nav/components/new_nav_toggle.vue71
-rw-r--r--app/assets/javascripts/new_branch_form.js8
-rw-r--r--app/assets/javascripts/notebook/cells/markdown.vue2
-rw-r--r--app/assets/javascripts/notebook/cells/output/html.vue10
-rw-r--r--app/assets/javascripts/notebook/cells/output/latex.vue2
-rw-r--r--app/assets/javascripts/notebook/cells/output/markdown.vue4
-rw-r--r--app/assets/javascripts/notes/components/comment_form.vue6
-rw-r--r--app/assets/javascripts/notes/components/diff_discussion_header.vue3
-rw-r--r--app/assets/javascripts/notes/components/diff_with_note.vue3
-rw-r--r--app/assets/javascripts/notes/components/note_actions.vue3
-rw-r--r--app/assets/javascripts/notes/components/note_body.vue10
-rw-r--r--app/assets/javascripts/notes/components/note_header.vue10
-rw-r--r--app/assets/javascripts/notes/components/note_signed_out_widget.vue2
-rw-r--r--app/assets/javascripts/notes/components/noteable_discussion.vue6
-rw-r--r--app/assets/javascripts/notes/components/noteable_note.vue13
-rw-r--r--app/assets/javascripts/notes/components/notes_app.vue89
-rw-r--r--app/assets/javascripts/notes/index.js53
-rw-r--r--app/assets/javascripts/notes/stores/actions.js67
-rw-r--r--app/assets/javascripts/notes/stores/getters.js20
-rw-r--r--app/assets/javascripts/notes/stores/modules/index.js1
-rw-r--r--app/assets/javascripts/notes/stores/mutation_types.js1
-rw-r--r--app/assets/javascripts/notes/stores/mutations.js3
-rw-r--r--app/assets/javascripts/observability/components/observability_app.vue71
-rw-r--r--app/assets/javascripts/observability/components/skeleton/dashboards.vue29
-rw-r--r--app/assets/javascripts/observability/components/skeleton/explore.vue27
-rw-r--r--app/assets/javascripts/observability/components/skeleton/index.vue89
-rw-r--r--app/assets/javascripts/observability/components/skeleton/manage.vue25
-rw-r--r--app/assets/javascripts/observability/constants.js16
-rw-r--r--app/assets/javascripts/packages_and_registries/container_registry/explorer/components/details_page/delete_alert.vue14
-rw-r--r--app/assets/javascripts/packages_and_registries/container_registry/explorer/components/details_page/tags_list.vue2
-rw-r--r--app/assets/javascripts/packages_and_registries/container_registry/explorer/constants/details.js6
-rw-r--r--app/assets/javascripts/packages_and_registries/container_registry/explorer/pages/details.vue13
-rw-r--r--app/assets/javascripts/packages_and_registries/container_registry/explorer/pages/list.vue2
-rw-r--r--app/assets/javascripts/packages_and_registries/harbor_registry/pages/details.vue4
-rw-r--r--app/assets/javascripts/packages_and_registries/harbor_registry/utils.js2
-rw-r--r--app/assets/javascripts/packages_and_registries/infrastructure_registry/list/components/packages_list_app.vue6
-rw-r--r--app/assets/javascripts/packages_and_registries/infrastructure_registry/list/stores/actions.js3
-rw-r--r--app/assets/javascripts/packages_and_registries/package_registry/components/list/package_list_row.vue5
-rw-r--r--app/assets/javascripts/packages_and_registries/package_registry/components/list/package_search.vue20
-rw-r--r--app/assets/javascripts/packages_and_registries/package_registry/graphql/fragments/package_data.fragment.graphql3
-rw-r--r--app/assets/javascripts/packages_and_registries/package_registry/graphql/queries/get_package_details.query.graphql1
-rw-r--r--app/assets/javascripts/packages_and_registries/package_registry/pages/details.vue15
-rw-r--r--app/assets/javascripts/packages_and_registries/shared/components/package_path.vue2
-rw-r--r--app/assets/javascripts/packages_and_registries/shared/constants/package_registry.js2
-rw-r--r--app/assets/javascripts/packages_and_registries/shared/utils.js2
-rw-r--r--app/assets/javascripts/pages/admin/application_settings/general/components/signup_form.vue10
-rw-r--r--app/assets/javascripts/pages/admin/application_settings/signup_restrictions.js1
-rw-r--r--app/assets/javascripts/pages/admin/broadcast_messages/edit/index.js8
-rw-r--r--app/assets/javascripts/pages/admin/broadcast_messages/index/index.js (renamed from app/assets/javascripts/pages/admin/broadcast_messages/index.js)2
-rw-r--r--app/assets/javascripts/pages/admin/dashboard/index.js3
-rw-r--r--app/assets/javascripts/pages/admin/projects/index/components/delete_project_modal.vue3
-rw-r--r--app/assets/javascripts/pages/dashboard/todos/index/todos.js50
-rw-r--r--app/assets/javascripts/pages/groups/merge_requests/index.js8
-rw-r--r--app/assets/javascripts/pages/help/index/index.js2
-rw-r--r--app/assets/javascripts/pages/import/fogbugz/new_user_map/components/user_select.vue8
-rw-r--r--app/assets/javascripts/pages/import/gitlab_projects/new/index.js2
-rw-r--r--app/assets/javascripts/pages/import/manifest/new/index.js3
-rw-r--r--app/assets/javascripts/pages/import/phabricator/new/index.js3
-rw-r--r--app/assets/javascripts/pages/projects/branches/new/index.js7
-rw-r--r--app/assets/javascripts/pages/projects/ci/lints/show/index.js2
-rw-r--r--app/assets/javascripts/pages/projects/ci/pipeline_editor/show/index.js2
-rw-r--r--app/assets/javascripts/pages/projects/commits/show/index.js3
-rw-r--r--app/assets/javascripts/pages/projects/cycle_analytics/show/index.js2
-rw-r--r--app/assets/javascripts/pages/projects/environments/show/index.js3
-rw-r--r--app/assets/javascripts/pages/projects/forks/new/components/fork_form.vue87
-rw-r--r--app/assets/javascripts/pages/projects/graphs/components/code_coverage.vue32
-rw-r--r--app/assets/javascripts/pages/projects/merge_requests/creations/new/compare.js5
-rw-r--r--app/assets/javascripts/pages/projects/merge_requests/creations/new/index.js27
-rw-r--r--app/assets/javascripts/pages/projects/merge_requests/diffs/index.js5
-rw-r--r--app/assets/javascripts/pages/projects/merge_requests/index/index.js9
-rw-r--r--app/assets/javascripts/pages/projects/merge_requests/init_merge_request.js2
-rw-r--r--app/assets/javascripts/pages/projects/merge_requests/page.js45
-rw-r--r--app/assets/javascripts/pages/projects/merge_requests/show/index.js48
-rw-r--r--app/assets/javascripts/pages/projects/ml/candidates/show/index.js27
-rw-r--r--app/assets/javascripts/pages/projects/ml/experiments/show/index.js4
-rw-r--r--app/assets/javascripts/pages/projects/new/index.js2
-rw-r--r--app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/interval_pattern_input.vue13
-rw-r--r--app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/timezone_dropdown.js82
-rw-r--r--app/assets/javascripts/pages/projects/project.js27
-rw-r--r--app/assets/javascripts/pages/projects/settings/merge_requests/index.js3
-rw-r--r--app/assets/javascripts/pages/projects/shared/permissions/components/settings_panel.vue206
-rw-r--r--app/assets/javascripts/pages/projects/shared/web_ide_link/index.js11
-rw-r--r--app/assets/javascripts/pages/projects/tags/new/index.js4
-rw-r--r--app/assets/javascripts/pages/registrations/new/index.js3
-rw-r--r--app/assets/javascripts/pages/sessions/new/index.js2
-rw-r--r--app/assets/javascripts/pages/shared/wikis/components/wiki_content.vue9
-rw-r--r--app/assets/javascripts/pages/shared/wikis/render_gfm_facade.js5
-rw-r--r--app/assets/javascripts/pages/web_ide/remote_ide/index.js3
-rw-r--r--app/assets/javascripts/performance_bar/components/detailed_metric.vue23
-rw-r--r--app/assets/javascripts/performance_bar/components/performance_bar_app.vue4
-rw-r--r--app/assets/javascripts/performance_bar/components/request_warning.vue5
-rw-r--r--app/assets/javascripts/performance_bar/constants.js10
-rw-r--r--app/assets/javascripts/pipeline_new/components/legacy_pipeline_new_form.vue490
-rw-r--r--app/assets/javascripts/pipeline_new/components/pipeline_new_form.vue4
-rw-r--r--app/assets/javascripts/pipeline_new/index.js50
-rw-r--r--app/assets/javascripts/pipeline_wizard/components/step_nav.vue3
-rw-r--r--app/assets/javascripts/pipelines/components/graph/graph_component_wrapper.vue7
-rw-r--r--app/assets/javascripts/pipelines/components/graph/job_item.vue2
-rw-r--r--app/assets/javascripts/pipelines/components/jobs/failed_jobs_table.vue5
-rw-r--r--app/assets/javascripts/pipelines/components/jobs_shared/action_component.vue1
-rw-r--r--app/assets/javascripts/pipelines/components/jobs_shared/job_name_component.vue2
-rw-r--r--app/assets/javascripts/pipelines/components/pipeline_mini_graph/job_item.vue8
-rw-r--r--app/assets/javascripts/pipelines/components/pipeline_mini_graph/pipeline_mini_graph.vue14
-rw-r--r--app/assets/javascripts/pipelines/components/pipeline_mini_graph/pipeline_stage.vue12
-rw-r--r--app/assets/javascripts/pipelines/components/pipeline_mini_graph/pipeline_stages.vue14
-rw-r--r--app/assets/javascripts/pipelines/components/pipelines_list/empty_state/pipelines_ci_templates.vue2
-rw-r--r--app/assets/javascripts/pipelines/components/pipelines_list/pipelines_filtered_search.vue12
-rw-r--r--app/assets/javascripts/pipelines/components/pipelines_list/pipelines_table.vue4
-rw-r--r--app/assets/javascripts/pipelines/components/test_reports/test_reports.vue14
-rw-r--r--app/assets/javascripts/pipelines/mixins/pipelines_mixin.js2
-rw-r--r--app/assets/javascripts/pipelines/pipeline_details_bundle.js65
-rw-r--r--app/assets/javascripts/pipelines/pipeline_details_dag.js42
-rw-r--r--app/assets/javascripts/pipelines/pipeline_details_failed_jobs.js36
-rw-r--r--app/assets/javascripts/pipelines/pipeline_details_graph.js35
-rw-r--r--app/assets/javascripts/pipelines/pipeline_details_jobs.js34
-rw-r--r--app/assets/javascripts/pipelines/pipeline_test_details.js40
-rw-r--r--app/assets/javascripts/popovers/components/popovers.vue5
-rw-r--r--app/assets/javascripts/profile/account/components/update_username.vue3
-rw-r--r--app/assets/javascripts/projects/commit/components/branches_dropdown.vue7
-rw-r--r--app/assets/javascripts/projects/commit/components/form_modal.vue12
-rw-r--r--app/assets/javascripts/projects/commit/init_revert_commit_modal.js1
-rw-r--r--app/assets/javascripts/projects/commits/index.js31
-rw-r--r--app/assets/javascripts/projects/compare/components/repo_dropdown.vue2
-rw-r--r--app/assets/javascripts/projects/compare/components/revision_card.vue2
-rw-r--r--app/assets/javascripts/projects/default_project_templates.js8
-rw-r--r--app/assets/javascripts/projects/new/components/app.vue2
-rw-r--r--app/assets/javascripts/projects/new/components/new_project_url_select.vue23
-rw-r--r--app/assets/javascripts/projects/new/constants.js2
-rw-r--r--app/assets/javascripts/projects/new/index.js2
-rw-r--r--app/assets/javascripts/projects/project_name_rules.js28
-rw-r--r--app/assets/javascripts/projects/project_new.js14
-rw-r--r--app/assets/javascripts/projects/settings/access_dropdown.js2
-rw-r--r--app/assets/javascripts/projects/settings/branch_rules/components/view/constants.js1
-rw-r--r--app/assets/javascripts/projects/settings/branch_rules/components/view/index.vue40
-rw-r--r--app/assets/javascripts/projects/settings/branch_rules/mount_branch_rules.js9
-rw-r--r--app/assets/javascripts/projects/settings/branch_rules/queries/branch_rules_details.query.graphql1
-rw-r--r--app/assets/javascripts/projects/settings/repository/branch_rules/app.vue11
-rw-r--r--app/assets/javascripts/projects/settings/repository/branch_rules/components/branch_rule.vue57
-rw-r--r--app/assets/javascripts/projects/settings/repository/branch_rules/graphql/queries/branch_rules.query.graphql24
-rw-r--r--app/assets/javascripts/projects/settings/utils.js17
-rw-r--r--app/assets/javascripts/projects/settings_service_desk/components/service_desk_root.vue5
-rw-r--r--app/assets/javascripts/protected_tags/protected_tag_create.js2
-rw-r--r--app/assets/javascripts/releases/components/app_index.vue1
-rw-r--r--app/assets/javascripts/releases/components/release_block.vue15
-rw-r--r--app/assets/javascripts/releases/components/release_block_footer.vue34
-rw-r--r--app/assets/javascripts/releases/graphql/fragments/release_for_editing.fragment.graphql1
-rw-r--r--app/assets/javascripts/releases/util.js3
-rw-r--r--app/assets/javascripts/repository/components/last_commit.vue12
-rw-r--r--app/assets/javascripts/repository/components/preview/index.vue8
-rw-r--r--app/assets/javascripts/repository/components/table/index.vue10
-rw-r--r--app/assets/javascripts/repository/components/table/row.vue34
-rw-r--r--app/assets/javascripts/repository/components/tree_content.vue2
-rw-r--r--app/assets/javascripts/repository/constants.js1
-rw-r--r--app/assets/javascripts/repository/index.js31
-rw-r--r--app/assets/javascripts/repository/queries/commit.query.graphql7
-rw-r--r--app/assets/javascripts/repository/utils/ref_switcher_utils.js30
-rw-r--r--app/assets/javascripts/search/sidebar/components/confidentiality_filter.vue12
-rw-r--r--app/assets/javascripts/search/sidebar/components/results_filters.vue27
-rw-r--r--app/assets/javascripts/search/sidebar/components/scope_navigation.vue47
-rw-r--r--app/assets/javascripts/search/sidebar/components/status_filter.vue12
-rw-r--r--app/assets/javascripts/search/sidebar/constants/index.js2
-rw-r--r--app/assets/javascripts/search/topbar/components/app.vue71
-rw-r--r--app/assets/javascripts/search/topbar/components/searchable_dropdown_item.vue3
-rw-r--r--app/assets/javascripts/search/topbar/constants.js2
-rw-r--r--app/assets/javascripts/search/topbar/index.js20
-rw-r--r--app/assets/javascripts/security_configuration/components/training_provider_list.vue4
-rw-r--r--app/assets/javascripts/self_monitor/components/self_monitor_form.vue13
-rw-r--r--app/assets/javascripts/self_monitor/store/actions.js8
-rw-r--r--app/assets/javascripts/sentry/constants.js1
-rw-r--r--app/assets/javascripts/sentry/index.js16
-rw-r--r--app/assets/javascripts/sentry/legacy_index.js34
-rw-r--r--app/assets/javascripts/sentry/legacy_sentry_config.js64
-rw-r--r--app/assets/javascripts/sentry/sentry_browser_wrapper.js27
-rw-r--r--app/assets/javascripts/sentry/sentry_config.js39
-rw-r--r--app/assets/javascripts/set_status_modal/set_status_form.vue4
-rw-r--r--app/assets/javascripts/set_status_modal/set_status_modal_wrapper.vue4
-rw-r--r--app/assets/javascripts/sidebar/components/assignees/assignees_realtime.vue2
-rw-r--r--app/assets/javascripts/sidebar/components/assignees/issuable_assignees.vue2
-rw-r--r--app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.vue4
-rw-r--r--app/assets/javascripts/sidebar/components/assignees/sidebar_assignees_widget.vue8
-rw-r--r--app/assets/javascripts/sidebar/components/confidential/sidebar_confidentiality_form.vue2
-rw-r--r--app/assets/javascripts/sidebar/components/confidential/sidebar_confidentiality_widget.vue4
-rw-r--r--app/assets/javascripts/sidebar/components/copy/copy_email_to_clipboard.vue (renamed from app/assets/javascripts/sidebar/components/copy_email_to_clipboard.vue)2
-rw-r--r--app/assets/javascripts/sidebar/components/copy/copyable_field.vue (renamed from app/assets/javascripts/vue_shared/components/sidebar/copyable_field.vue)0
-rw-r--r--app/assets/javascripts/sidebar/components/copy/sidebar_reference_widget.vue (renamed from app/assets/javascripts/sidebar/components/reference/sidebar_reference_widget.vue)4
-rw-r--r--app/assets/javascripts/sidebar/components/crm_contacts/crm_contacts.vue4
-rw-r--r--app/assets/javascripts/sidebar/components/date/sidebar_date_widget.vue10
-rw-r--r--app/assets/javascripts/sidebar/components/incidents/constants.js25
-rw-r--r--app/assets/javascripts/sidebar/components/incidents/escalation_status.vue9
-rw-r--r--app/assets/javascripts/sidebar/components/incidents/sidebar_escalation_status.vue9
-rw-r--r--app/assets/javascripts/sidebar/components/incidents/utils.js5
-rw-r--r--app/assets/javascripts/sidebar/components/labels/labels_select_vue/constants.js (renamed from app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/constants.js)0
-rw-r--r--app/assets/javascripts/sidebar/components/labels/labels_select_vue/dropdown_button.vue (renamed from app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_button.vue)2
-rw-r--r--app/assets/javascripts/sidebar/components/labels/labels_select_vue/dropdown_contents.vue (renamed from app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_contents.vue)2
-rw-r--r--app/assets/javascripts/sidebar/components/labels/labels_select_vue/dropdown_contents_create_view.vue (renamed from app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_create_view.vue)2
-rw-r--r--app/assets/javascripts/sidebar/components/labels/labels_select_vue/dropdown_contents_labels_view.vue (renamed from app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_labels_view.vue)2
-rw-r--r--app/assets/javascripts/sidebar/components/labels/labels_select_vue/dropdown_title.vue (renamed from app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_title.vue)2
-rw-r--r--app/assets/javascripts/sidebar/components/labels/labels_select_vue/dropdown_value.vue (renamed from app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_value.vue)2
-rw-r--r--app/assets/javascripts/sidebar/components/labels/labels_select_vue/dropdown_value_collapsed.vue (renamed from app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_value_collapsed.vue)2
-rw-r--r--app/assets/javascripts/sidebar/components/labels/labels_select_vue/label_item.vue (renamed from app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/label_item.vue)2
-rw-r--r--app/assets/javascripts/sidebar/components/labels/labels_select_vue/labels_select_root.vue (renamed from app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/labels_select_root.vue)2
-rw-r--r--app/assets/javascripts/sidebar/components/labels/labels_select_vue/store/actions.js (renamed from app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/store/actions.js)0
-rw-r--r--app/assets/javascripts/sidebar/components/labels/labels_select_vue/store/getters.js (renamed from app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/store/getters.js)0
-rw-r--r--app/assets/javascripts/sidebar/components/labels/labels_select_vue/store/index.js (renamed from app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/store/index.js)0
-rw-r--r--app/assets/javascripts/sidebar/components/labels/labels_select_vue/store/mutation_types.js (renamed from app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/store/mutation_types.js)0
-rw-r--r--app/assets/javascripts/sidebar/components/labels/labels_select_vue/store/mutations.js (renamed from app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/store/mutations.js)0
-rw-r--r--app/assets/javascripts/sidebar/components/labels/labels_select_vue/store/state.js (renamed from app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/store/state.js)0
-rw-r--r--app/assets/javascripts/sidebar/components/labels/labels_select_widget/constants.js (renamed from app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/constants.js)0
-rw-r--r--app/assets/javascripts/sidebar/components/labels/labels_select_widget/dropdown_contents.vue (renamed from app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents.vue)7
-rw-r--r--app/assets/javascripts/sidebar/components/labels/labels_select_widget/dropdown_contents_create_view.vue (renamed from app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_create_view.vue)2
-rw-r--r--app/assets/javascripts/sidebar/components/labels/labels_select_widget/dropdown_contents_labels_view.vue (renamed from app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_labels_view.vue)2
-rw-r--r--app/assets/javascripts/sidebar/components/labels/labels_select_widget/dropdown_footer.vue (renamed from app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_footer.vue)0
-rw-r--r--app/assets/javascripts/sidebar/components/labels/labels_select_widget/dropdown_header.vue (renamed from app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_header.vue)0
-rw-r--r--app/assets/javascripts/sidebar/components/labels/labels_select_widget/dropdown_value.vue (renamed from app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_value.vue)0
-rw-r--r--app/assets/javascripts/sidebar/components/labels/labels_select_widget/embedded_labels_list.vue73
-rw-r--r--app/assets/javascripts/sidebar/components/labels/labels_select_widget/graphql/create_label.mutation.graphql (renamed from app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/graphql/create_label.mutation.graphql)0
-rw-r--r--app/assets/javascripts/sidebar/components/labels/labels_select_widget/graphql/epic_labels.query.graphql (renamed from app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/graphql/epic_labels.query.graphql)0
-rw-r--r--app/assets/javascripts/sidebar/components/labels/labels_select_widget/graphql/epic_update_labels.mutation.graphql (renamed from app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/graphql/epic_update_labels.mutation.graphql)0
-rw-r--r--app/assets/javascripts/sidebar/components/labels/labels_select_widget/graphql/group_labels.query.graphql (renamed from app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/graphql/group_labels.query.graphql)0
-rw-r--r--app/assets/javascripts/sidebar/components/labels/labels_select_widget/graphql/issue_labels.query.graphql (renamed from app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/graphql/issue_labels.query.graphql)0
-rw-r--r--app/assets/javascripts/sidebar/components/labels/labels_select_widget/graphql/merge_request_labels.query.graphql (renamed from app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/graphql/merge_request_labels.query.graphql)0
-rw-r--r--app/assets/javascripts/sidebar/components/labels/labels_select_widget/graphql/project_labels.query.graphql (renamed from app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/graphql/project_labels.query.graphql)0
-rw-r--r--app/assets/javascripts/sidebar/components/labels/labels_select_widget/label_item.vue (renamed from app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/label_item.vue)0
-rw-r--r--app/assets/javascripts/sidebar/components/labels/labels_select_widget/labels_select_root.vue (renamed from app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/labels_select_root.vue)77
-rw-r--r--app/assets/javascripts/sidebar/components/labels/labels_select_widget/utils.js (renamed from app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/utils.js)0
-rw-r--r--app/assets/javascripts/sidebar/components/lock/issuable_lock_form.vue6
-rw-r--r--app/assets/javascripts/sidebar/components/move/issuable_move_dropdown.vue (renamed from app/assets/javascripts/vue_shared/components/sidebar/issuable_move_dropdown.vue)0
-rw-r--r--app/assets/javascripts/sidebar/components/move/move_issues_button.vue (renamed from app/assets/javascripts/issuable/bulk_update_sidebar/components/move_issues_button.vue)8
-rw-r--r--app/assets/javascripts/sidebar/components/participants/sidebar_participants_widget.vue2
-rw-r--r--app/assets/javascripts/sidebar/components/reviewers/reviewers.vue20
-rw-r--r--app/assets/javascripts/sidebar/components/reviewers/sidebar_reviewers.vue16
-rw-r--r--app/assets/javascripts/sidebar/components/severity/constants.js41
-rw-r--r--app/assets/javascripts/sidebar/components/severity/sidebar_severity.vue4
-rw-r--r--app/assets/javascripts/sidebar/components/sidebar_dropdown_widget.vue2
-rw-r--r--app/assets/javascripts/sidebar/components/status/status_dropdown.vue (renamed from app/assets/javascripts/issuable/bulk_update_sidebar/components/status_dropdown.vue)2
-rw-r--r--app/assets/javascripts/sidebar/components/subscriptions/sidebar_subscriptions_widget.vue6
-rw-r--r--app/assets/javascripts/sidebar/components/subscriptions/subscriptions_dropdown.vue (renamed from app/assets/javascripts/issuable/bulk_update_sidebar/components/subscriptions_dropdown.vue)2
-rw-r--r--app/assets/javascripts/sidebar/components/time_tracking/constants.js1
-rw-r--r--app/assets/javascripts/sidebar/components/time_tracking/create_timelog_form.vue227
-rw-r--r--app/assets/javascripts/sidebar/components/time_tracking/help_state.vue5
-rw-r--r--app/assets/javascripts/sidebar/components/time_tracking/report.vue4
-rw-r--r--app/assets/javascripts/sidebar/components/time_tracking/sidebar_time_tracking.vue6
-rw-r--r--app/assets/javascripts/sidebar/components/time_tracking/time_tracker.vue40
-rw-r--r--app/assets/javascripts/sidebar/components/todo_toggle/sidebar_todo_widget.vue6
-rw-r--r--app/assets/javascripts/sidebar/components/todo_toggle/todo_button.vue (renamed from app/assets/javascripts/vue_shared/components/sidebar/todo_toggle/todo_button.vue)2
-rw-r--r--app/assets/javascripts/sidebar/components/toggle/toggle_sidebar.vue (renamed from app/assets/javascripts/vue_shared/components/sidebar/toggle_sidebar.vue)0
-rw-r--r--app/assets/javascripts/sidebar/constants.js187
-rw-r--r--app/assets/javascripts/sidebar/mount_milestone_sidebar.js1
-rw-r--r--app/assets/javascripts/sidebar/mount_sidebar.js100
-rw-r--r--app/assets/javascripts/sidebar/queries/create_timelog.mutation.graphql17
-rw-r--r--app/assets/javascripts/sidebar/queries/delete_timelog.mutation.graphql (renamed from app/assets/javascripts/sidebar/components/time_tracking/graphql/mutations/delete_timelog.mutation.graphql)0
-rw-r--r--app/assets/javascripts/sidebar/queries/get_alert_assignees.query.graphql (renamed from app/assets/javascripts/vue_shared/components/sidebar/queries/get_alert_assignees.query.graphql)1
-rw-r--r--app/assets/javascripts/sidebar/queries/get_issue_assignees.query.graphql (renamed from app/assets/javascripts/vue_shared/components/sidebar/queries/get_issue_assignees.query.graphql)0
-rw-r--r--app/assets/javascripts/sidebar/queries/get_issue_crm_contacts.query.graphql (renamed from app/assets/javascripts/sidebar/components/crm_contacts/queries/get_issue_crm_contacts.query.graphql)0
-rw-r--r--app/assets/javascripts/sidebar/queries/get_issue_participants.query.graphql (renamed from app/assets/javascripts/vue_shared/components/sidebar/queries/get_issue_participants.query.graphql)0
-rw-r--r--app/assets/javascripts/sidebar/queries/get_issue_timelogs.query.graphql (renamed from app/assets/javascripts/vue_shared/components/sidebar/queries/get_issue_timelogs.query.graphql)0
-rw-r--r--app/assets/javascripts/sidebar/queries/get_merge_request_reviewers.query.graphql (renamed from app/assets/javascripts/vue_shared/components/sidebar/queries/get_merge_request_reviewers.query.graphql)0
-rw-r--r--app/assets/javascripts/sidebar/queries/get_mr_assignees.query.graphql (renamed from app/assets/javascripts/vue_shared/components/sidebar/queries/get_mr_assignees.query.graphql)0
-rw-r--r--app/assets/javascripts/sidebar/queries/get_mr_participants.query.graphql (renamed from app/assets/javascripts/vue_shared/components/sidebar/queries/get_mr_participants.query.graphql)0
-rw-r--r--app/assets/javascripts/sidebar/queries/get_mr_timelogs.query.graphql (renamed from app/assets/javascripts/vue_shared/components/sidebar/queries/get_mr_timelogs.query.graphql)0
-rw-r--r--app/assets/javascripts/sidebar/queries/issue_crm_contacts.fragment.graphql (renamed from app/assets/javascripts/sidebar/components/crm_contacts/queries/issue_crm_contacts.fragment.graphql)0
-rw-r--r--app/assets/javascripts/sidebar/queries/issue_crm_contacts.subscription.graphql (renamed from app/assets/javascripts/sidebar/components/crm_contacts/queries/issue_crm_contacts.subscription.graphql)0
-rw-r--r--app/assets/javascripts/sidebar/queries/merge_request_reviewers.subscription.graphql (renamed from app/assets/javascripts/vue_shared/components/sidebar/queries/merge_request_reviewers.subscription.graphql)0
-rw-r--r--app/assets/javascripts/sidebar/queries/move_issue.mutation.graphql (renamed from app/assets/javascripts/issuable/bulk_update_sidebar/components/graphql/mutations/move_issue.mutation.graphql)0
-rw-r--r--app/assets/javascripts/sidebar/queries/update_issuable_severity.mutation.graphql (renamed from app/assets/javascripts/sidebar/components/severity/graphql/mutations/update_issuable_severity.mutation.graphql)0
-rw-r--r--app/assets/javascripts/sidebar/queries/update_issue_assignees.mutation.graphql (renamed from app/assets/javascripts/vue_shared/components/sidebar/queries/update_issue_assignees.mutation.graphql)0
-rw-r--r--app/assets/javascripts/sidebar/queries/update_issue_lock.mutation.graphql (renamed from app/assets/javascripts/sidebar/components/lock/mutations/update_issue_lock.mutation.graphql)0
-rw-r--r--app/assets/javascripts/sidebar/queries/update_merge_request_lock.mutation.graphql (renamed from app/assets/javascripts/sidebar/components/lock/mutations/update_merge_request_lock.mutation.graphql)0
-rw-r--r--app/assets/javascripts/sidebar/queries/update_mr_assignees.mutation.graphql (renamed from app/assets/javascripts/vue_shared/components/sidebar/queries/update_mr_assignees.mutation.graphql)0
-rw-r--r--app/assets/javascripts/sidebar/sidebar_mediator.js15
-rw-r--r--app/assets/javascripts/sidebar/utils.js (renamed from app/assets/javascripts/vue_shared/components/sidebar/todo_toggle/utils.js)5
-rw-r--r--app/assets/javascripts/snippets/components/snippet_description_view.vue2
-rw-r--r--app/assets/javascripts/surveys/merge_request_experience/app.vue5
-rw-r--r--app/assets/javascripts/tags/init_new_tag_ref_selector.js23
-rw-r--r--app/assets/javascripts/terms/components/app.vue12
-rw-r--r--app/assets/javascripts/terraform/components/init_command_modal.vue8
-rw-r--r--app/assets/javascripts/tooltips/components/tooltips.vue3
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/approvals/approvals.vue7
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/deployment/memory_usage.vue6
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/extensions/base.vue21
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/extensions/child_content.vue12
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/mr_widget_author.vue18
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue8
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/mr_widget_related_links.vue9
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/state_container.vue2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/merge_checks_failed.vue15
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_closed.vue64
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/nothing_to_merge.vue5
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue52
-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.vue6
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/widget/action_buttons.vue134
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/widget/app.vue3
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/widget/dynamic_content.vue11
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/widget/status_icon.vue4
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/widget/widget.vue44
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/widget/widget_content_row.vue35
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/extensions/code_quality/constants.js31
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/extensions/code_quality/index.js88
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/i18n.js7
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue38
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/queries/get_state.subscription.graphql7
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/queries/states/ready_to_merge.fragment.graphql39
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/queries/states/ready_to_merge.subscription.graphql9
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/queries/states/ready_to_merge_merge_request.fragment.graphql39
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js8
-rw-r--r--app/assets/javascripts/vue_shared/alert_details/components/alert_details.vue12
-rw-r--r--app/assets/javascripts/vue_shared/alert_details/components/alert_status.vue2
-rw-r--r--app/assets/javascripts/vue_shared/alert_details/components/sidebar/sidebar_assignees.vue2
-rw-r--r--app/assets/javascripts/vue_shared/alert_details/components/sidebar/sidebar_header.vue2
-rw-r--r--app/assets/javascripts/vue_shared/alert_details/components/system_notes/system_note.vue5
-rw-r--r--app/assets/javascripts/vue_shared/alert_details/graphql/mutations/alert_set_assignees.mutation.graphql1
-rw-r--r--app/assets/javascripts/vue_shared/components/actions_button.vue112
-rw-r--r--app/assets/javascripts/vue_shared/components/awards_list.vue10
-rw-r--r--app/assets/javascripts/vue_shared/components/blob_viewers/rich_viewer.vue2
-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_highlighted.vue4
-rw-r--r--app/assets/javascripts/vue_shared/components/confirm_danger/confirm_danger_modal.vue10
-rw-r--r--app/assets/javascripts/vue_shared/components/confirm_modal.vue3
-rw-r--r--app/assets/javascripts/vue_shared/components/content_viewer/viewers/markdown_viewer.vue8
-rw-r--r--app/assets/javascripts/vue_shared/components/date_time_picker/date_time_picker.vue1
-rw-r--r--app/assets/javascripts/vue_shared/components/dismissible_alert.vue3
-rw-r--r--app/assets/javascripts/vue_shared/components/filtered_search_bar/constants.js57
-rw-r--r--app/assets/javascripts/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue14
-rw-r--r--app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/base_token.vue6
-rw-r--r--app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/crm_contact_token.vue4
-rw-r--r--app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/crm_organization_token.vue4
-rw-r--r--app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/emoji_token.vue4
-rw-r--r--app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/label_token.vue4
-rw-r--r--app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/release_token.vue4
-rw-r--r--app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/user_token.vue (renamed from app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/author_token.vue)50
-rw-r--r--app/assets/javascripts/vue_shared/components/group_select/group_select.vue8
-rw-r--r--app/assets/javascripts/vue_shared/components/header_ci_component.vue12
-rw-r--r--app/assets/javascripts/vue_shared/components/help_popover.vue5
-rw-r--r--app/assets/javascripts/vue_shared/components/listbox_input/listbox_input.stories.js26
-rw-r--r--app/assets/javascripts/vue_shared/components/listbox_input/listbox_input.vue110
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/apply_suggestion.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/field.vue11
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/field_view.vue10
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/markdown_editor.vue6
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/suggestion_diff_row.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/suggestions.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown_drawer/markdown_drawer.stories.js (renamed from app/assets/javascripts/vue_shared/components/markdown_drawer/makrdown_drawer.stories.js)0
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown_drawer/markdown_drawer.vue21
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown_drawer/utils/fetch.js2
-rw-r--r--app/assets/javascripts/vue_shared/components/metric_images/metric_images_tab.vue7
-rw-r--r--app/assets/javascripts/vue_shared/components/notes/placeholder_note.vue3
-rw-r--r--app/assets/javascripts/vue_shared/components/notes/system_note.vue17
-rw-r--r--app/assets/javascripts/vue_shared/components/paginated_table_with_search_and_tabs/paginated_table_with_search_and_tabs.vue45
-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/registry_search.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/todo_toggle/todo_button.stories.js21
-rw-r--r--app/assets/javascripts/vue_shared/components/source_viewer/components/chunk.vue13
-rw-r--r--app/assets/javascripts/vue_shared/components/source_viewer/components/chunk_line.vue4
-rw-r--r--app/assets/javascripts/vue_shared/components/source_viewer/plugins/link_dependencies.js2
-rw-r--r--app/assets/javascripts/vue_shared/components/source_viewer/plugins/utils/go_sum_linker.js34
-rw-r--r--app/assets/javascripts/vue_shared/components/source_viewer/source_viewer.vue5
-rw-r--r--app/assets/javascripts/vue_shared/components/user_popover/user_popover.vue4
-rw-r--r--app/assets/javascripts/vue_shared/components/web_ide_link.vue161
-rw-r--r--app/assets/javascripts/vue_shared/constants.js4
-rw-r--r--app/assets/javascripts/vue_shared/issuable/create/components/issuable_form.vue4
-rw-r--r--app/assets/javascripts/vue_shared/issuable/create/components/issuable_label_selector.vue92
-rw-r--r--app/assets/javascripts/vue_shared/issuable/list/components/issuable_item.vue4
-rw-r--r--app/assets/javascripts/vue_shared/issuable/list/components/issuable_list_root.vue1
-rw-r--r--app/assets/javascripts/vue_shared/issuable/show/components/issuable_description.vue12
-rw-r--r--app/assets/javascripts/vue_shared/issuable/show/components/issuable_title.vue10
-rw-r--r--app/assets/javascripts/vue_shared/new_namespace/components/legacy_container.vue3
-rw-r--r--app/assets/javascripts/vue_shared/new_namespace/components/welcome.vue2
-rw-r--r--app/assets/javascripts/vue_shared/new_namespace/new_namespace_page.vue3
-rw-r--r--app/assets/javascripts/vue_shared/security_reports/security_reports_app.vue4
-rw-r--r--app/assets/javascripts/vue_shared/security_reports/store/getters.js2
-rw-r--r--app/assets/javascripts/vue_shared/security_reports/store/utils.js8
-rw-r--r--app/assets/javascripts/webhooks/components/push_events.vue2
-rw-r--r--app/assets/javascripts/webhooks/constants.js4
-rw-r--r--app/assets/javascripts/whats_new/components/feature.vue5
-rw-r--r--app/assets/javascripts/work_items/components/notes/system_note.vue229
-rw-r--r--app/assets/javascripts/work_items/components/work_item_assignees.vue1
-rw-r--r--app/assets/javascripts/work_items/components/work_item_description.vue89
-rw-r--r--app/assets/javascripts/work_items/components/work_item_description_rendered.vue16
-rw-r--r--app/assets/javascripts/work_items/components/work_item_detail.vue241
-rw-r--r--app/assets/javascripts/work_items/components/work_item_detail_modal.vue7
-rw-r--r--app/assets/javascripts/work_items/components/work_item_information.vue53
-rw-r--r--app/assets/javascripts/work_items/components/work_item_labels.vue6
-rw-r--r--app/assets/javascripts/work_items/components/work_item_links/index.js2
-rw-r--r--app/assets/javascripts/work_items/components/work_item_links/okr_actions_split_button.vue66
-rw-r--r--app/assets/javascripts/work_items/components/work_item_links/work_item_link_child.vue248
-rw-r--r--app/assets/javascripts/work_items/components/work_item_links/work_item_link_child_metadata.vue123
-rw-r--r--app/assets/javascripts/work_items/components/work_item_links/work_item_links.vue111
-rw-r--r--app/assets/javascripts/work_items/components/work_item_links/work_item_links_form.vue86
-rw-r--r--app/assets/javascripts/work_items/components/work_item_links/work_item_tree.vue244
-rw-r--r--app/assets/javascripts/work_items/components/work_item_links/work_item_tree_children.vue68
-rw-r--r--app/assets/javascripts/work_items/components/work_item_milestone.vue10
-rw-r--r--app/assets/javascripts/work_items/components/work_item_notes.vue109
-rw-r--r--app/assets/javascripts/work_items/components/work_item_type_icon.vue5
-rw-r--r--app/assets/javascripts/work_items/constants.js51
-rw-r--r--app/assets/javascripts/work_items/graphql/discussion.fragment.graphql12
-rw-r--r--app/assets/javascripts/work_items/graphql/milestone.fragment.graphql3
-rw-r--r--app/assets/javascripts/work_items/graphql/work_item_links.query.graphql3
-rw-r--r--app/assets/javascripts/work_items/graphql/work_item_metadata_widgets.fragment.graphql29
-rw-r--r--app/assets/javascripts/work_items/graphql/work_item_notes.query.graphql27
-rw-r--r--app/assets/javascripts/work_items/graphql/work_item_notes_by_iid.query.graphql32
-rw-r--r--app/assets/javascripts/work_items/graphql/work_item_tree.query.graphql53
-rw-r--r--app/assets/javascripts/work_items/graphql/work_item_widgets.fragment.graphql28
-rw-r--r--app/assets/javascripts/work_items/index.js12
-rw-r--r--app/assets/javascripts/work_items/pages/work_item_root.vue6
-rw-r--r--app/assets/javascripts/work_items/utils.js6
-rw-r--r--app/assets/stylesheets/_page_specific_files.scss4
-rw-r--r--app/assets/stylesheets/components/content_editor.scss12
-rw-r--r--app/assets/stylesheets/components/ref_selector.scss2
-rw-r--r--app/assets/stylesheets/fonts.scss32
-rw-r--r--app/assets/stylesheets/framework/common.scss4
-rw-r--r--app/assets/stylesheets/framework/dropdowns.scss14
-rw-r--r--app/assets/stylesheets/framework/emojis.scss4
-rw-r--r--app/assets/stylesheets/framework/filters.scss5
-rw-r--r--app/assets/stylesheets/framework/forms.scss38
-rw-r--r--app/assets/stylesheets/framework/header.scss57
-rw-r--r--app/assets/stylesheets/framework/kbd.scss4
-rw-r--r--app/assets/stylesheets/framework/mixins.scss2
-rw-r--r--app/assets/stylesheets/framework/sidebar.scss644
-rw-r--r--app/assets/stylesheets/framework/typography.scss15
-rw-r--r--app/assets/stylesheets/framework/variables.scss71
-rw-r--r--app/assets/stylesheets/page_bundles/_pipeline_mixins.scss23
-rw-r--r--app/assets/stylesheets/page_bundles/alert_management_details.scss2
-rw-r--r--app/assets/stylesheets/page_bundles/boards.scss21
-rw-r--r--app/assets/stylesheets/page_bundles/clusters.scss8
-rw-r--r--app/assets/stylesheets/page_bundles/ide.scss14
-rw-r--r--app/assets/stylesheets/page_bundles/incidents.scss9
-rw-r--r--app/assets/stylesheets/page_bundles/issuable.scss183
-rw-r--r--app/assets/stylesheets/page_bundles/issuable_list.scss96
-rw-r--r--app/assets/stylesheets/page_bundles/merge_requests.scss69
-rw-r--r--app/assets/stylesheets/page_bundles/milestone.scss10
-rw-r--r--app/assets/stylesheets/page_bundles/oncall_schedules.scss2
-rw-r--r--app/assets/stylesheets/page_bundles/pipelines.scss39
-rw-r--r--app/assets/stylesheets/page_bundles/search.scss (renamed from app/assets/stylesheets/pages/search.scss)97
-rw-r--r--app/assets/stylesheets/page_bundles/settings.scss209
-rw-r--r--app/assets/stylesheets/page_bundles/todos.scss81
-rw-r--r--app/assets/stylesheets/page_bundles/tree.scss15
-rw-r--r--app/assets/stylesheets/page_bundles/users.scss (renamed from app/assets/stylesheets/pages/users.scss)2
-rw-r--r--app/assets/stylesheets/page_bundles/work_items.scss4
-rw-r--r--app/assets/stylesheets/pages/colors.scss8
-rw-r--r--app/assets/stylesheets/pages/commits.scss5
-rw-r--r--app/assets/stylesheets/pages/events.scss2
-rw-r--r--app/assets/stylesheets/pages/issuable.scss912
-rw-r--r--app/assets/stylesheets/pages/login.scss2
-rw-r--r--app/assets/stylesheets/pages/merge_requests.scss6
-rw-r--r--app/assets/stylesheets/pages/ml_experiment_tracking.scss6
-rw-r--r--app/assets/stylesheets/pages/monitor.scss5
-rw-r--r--app/assets/stylesheets/pages/note_form.scss3
-rw-r--r--app/assets/stylesheets/pages/notes.scss38
-rw-r--r--app/assets/stylesheets/pages/projects.scss87
-rw-r--r--app/assets/stylesheets/pages/settings.scss228
-rw-r--r--app/assets/stylesheets/startup/startup-dark.scss394
-rw-r--r--app/assets/stylesheets/startup/startup-general.scss141
-rw-r--r--app/assets/stylesheets/startup/startup-signin.scss23
-rw-r--r--app/assets/stylesheets/themes/_dark.scss182
-rw-r--r--app/assets/stylesheets/themes/dark_mode_overrides.scss143
-rw-r--r--app/assets/stylesheets/utilities.scss12
895 files changed, 13634 insertions, 7673 deletions
diff --git a/app/assets/images/web-ide-promo-popover.svg b/app/assets/images/web-ide-promo-popover.svg
new file mode 100644
index 00000000000..3ced89860da
--- /dev/null
+++ b/app/assets/images/web-ide-promo-popover.svg
@@ -0,0 +1,101 @@
+<svg width="280" height="140" viewBox="0 0 280 140" fill="none" xmlns="http://www.w3.org/2000/svg">
+<g clip-path="url(#clip0_187_122567)">
+<g clip-path="url(#clip1_187_122567)">
+<circle cx="189.5" cy="-42.5" r="131.5" fill="url(#paint0_radial_187_122567)"/>
+<circle cx="-41.5" cy="-97.5" r="198.5" fill="url(#paint1_radial_187_122567)"/>
+<circle cx="309.5" cy="-7.5" r="121.5" fill="url(#paint2_radial_187_122567)"/>
+<g filter="url(#filter0_b_187_122567)">
+<path d="M0 4C0 1.79086 1.79086 0 4 0H276C278.209 0 280 1.79086 280 4V130H0V4Z" fill="white" fill-opacity="0.01"/>
+</g>
+</g>
+<path d="M183.948 47.9647H100.897V100.482H183.948V47.9647Z" fill="white"/>
+<path d="M100.64 47.9647H184.06V98.9817C184.06 104.1 179.908 108.256 174.793 108.256H100.638V47.9647H100.64Z" fill="white"/>
+<path d="M184.314 34.7452H100.64V47.9676H184.314V34.7452Z" fill="#AEA5D6"/>
+<path d="M109.594 43.2574C110.644 43.2574 111.495 42.4056 111.495 41.3549C111.495 40.3043 110.644 39.4525 109.594 39.4525C108.544 39.4525 107.693 40.3043 107.693 41.3549C107.693 42.4056 108.544 43.2574 109.594 43.2574Z" fill="#10B1B1"/>
+<path d="M116.482 43.2574C117.532 43.2574 118.383 42.4056 118.383 41.3549C118.383 40.3043 117.532 39.4525 116.482 39.4525C115.432 39.4525 114.581 40.3043 114.581 41.3549C114.581 42.4056 115.432 43.2574 116.482 43.2574Z" fill="#A888F4"/>
+<path d="M123.368 43.2574C124.418 43.2574 125.269 42.4056 125.269 41.3549C125.269 40.3043 124.418 39.4525 123.368 39.4525C122.318 39.4525 121.467 40.3043 121.467 41.3549C121.467 42.4056 122.318 43.2574 123.368 43.2574Z" fill="#FF9D73"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M100.556 34.4038H165.377V35.0858H101.238V61.7159H100.556V34.4038Z" fill="#171321"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M101.238 66.3383V83.4486H100.556V66.3383H101.238Z" fill="#171321"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M183.721 99.0231V89.5341H184.403V99.0231C184.403 104.311 180.118 108.599 174.834 108.599H120.244V107.917H174.834C179.741 107.917 183.721 103.935 183.721 99.0231Z" fill="#171321"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M183.721 83.1296V62.2422H184.403V83.1296H183.721Z" fill="#171321"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M109.596 39.7936C108.734 39.7936 108.036 40.4924 108.036 41.355C108.036 42.2176 108.734 42.9164 109.596 42.9164C110.457 42.9164 111.155 42.2176 111.155 41.355C111.155 40.4924 110.457 39.7936 109.596 39.7936ZM107.354 41.355C107.354 40.1162 108.357 39.1116 109.596 39.1116C110.834 39.1116 111.837 40.1162 111.837 41.355C111.837 42.5938 110.834 43.5985 109.596 43.5985C108.357 43.5985 107.354 42.5938 107.354 41.355Z" fill="#171321"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M116.48 39.7936C115.619 39.7936 114.92 40.4924 114.92 41.355C114.92 42.2176 115.619 42.9164 116.48 42.9164C117.342 42.9164 118.04 42.2176 118.04 41.355C118.04 40.4924 117.342 39.7936 116.48 39.7936ZM114.238 41.355C114.238 40.1162 115.242 39.1116 116.48 39.1116C117.719 39.1116 118.722 40.1162 118.722 41.355C118.722 42.5938 117.719 43.5985 116.48 43.5985C115.242 43.5985 114.238 42.5938 114.238 41.355Z" fill="#171321"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M123.367 39.7936C122.506 39.7936 121.808 40.4924 121.808 41.355C121.808 42.2176 122.506 42.9164 123.367 42.9164C124.229 42.9164 124.927 42.2176 124.927 41.355C124.927 40.4924 124.229 39.7936 123.367 39.7936ZM121.125 41.355C121.125 40.1162 122.129 39.1116 123.367 39.1116C124.606 39.1116 125.609 40.1162 125.609 41.355C125.609 42.5938 124.606 43.5985 123.367 43.5985C122.129 43.5985 121.125 42.5938 121.125 41.355Z" fill="#171321"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M160.466 48.3058H100.897V47.6238H160.466V48.3058Z" fill="#171321"/>
+<path d="M173.511 80.7124H152.485V83.4598H173.511V80.7124Z" fill="#D0C5E2"/>
+<path d="M149.08 80.7009H111.703V83.4484H149.08V80.7009Z" fill="#E7E4F2"/>
+<path d="M111.702 68.5293H132.728V65.7819H111.702V68.5293Z" fill="#E7E4F2"/>
+<path d="M136.131 68.5336H173.508V65.7861H136.131V68.5336Z" fill="#AEA5D6"/>
+<path d="M111.703 75.9889H120.838V73.2415H111.703V75.9889Z" fill="#AEA5D6"/>
+<path d="M155.891 75.9978H173.512V73.2504H155.891V75.9978Z" fill="#E7E4F2"/>
+<path d="M124.244 75.9978H152.485V73.2504H124.244V75.9978Z" fill="#D0C5E2"/>
+<path d="M141.099 58.3217H124.332V61.0691H141.099V58.3217Z" fill="#D0C5E2"/>
+<path d="M173.512 58.3217H144.31V61.0691H173.512V58.3217Z" fill="#E7E4F2"/>
+<path d="M120.926 58.3198H111.703V61.0673H120.926V58.3198Z" fill="#AEA5D6"/>
+<path d="M144.115 90.9242H160.882V88.1768H144.115V90.9242Z" fill="#AEA5D6"/>
+<path d="M111.703 90.908H140.905V88.1605H111.703V90.908Z" fill="#D0C5E2"/>
+<path d="M164.288 90.9242H173.512V88.1768H164.288V90.9242Z" fill="#D0C5E2"/>
+<path d="M173.508 95.6178H158.224V98.3652H173.508V95.6178Z" fill="#D0C5E2"/>
+<path d="M154.82 95.6199H111.703V98.3673H154.82V95.6199Z" fill="#E7E4F2"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M167.801 34.8965L168.463 35.0598L162.398 59.6461L189.091 57.5112L189.145 58.1911L161.509 60.4014L167.801 34.8965Z" fill="#171321"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M134.795 81.6335L109.428 116.595L108.876 116.195L133.312 82.5167L92.5055 87.8873L92.4165 87.2111L134.795 81.6335Z" fill="#171321"/>
+<path d="M187.019 57.9366C197.646 57.9366 206.262 49.3147 206.262 38.6791C206.262 28.0435 197.646 19.4216 187.019 19.4216C176.392 19.4216 167.777 28.0435 167.777 38.6791C167.777 49.3147 176.392 57.9366 187.019 57.9366Z" fill="white"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M187.019 19.6948C176.543 19.6948 168.05 28.1943 168.05 38.6794C168.05 49.1646 176.543 57.6641 187.019 57.6641C197.495 57.6641 205.989 49.1646 205.989 38.6794C205.989 28.1943 197.495 19.6948 187.019 19.6948ZM167.504 38.6794C167.504 27.8934 176.241 19.1492 187.019 19.1492C197.797 19.1492 206.534 27.8934 206.534 38.6794C206.534 49.4655 197.797 58.2097 187.019 58.2097C176.241 58.2097 167.504 49.4655 167.504 38.6794Z" fill="#171321"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M212.255 38.6791C212.255 24.7294 200.956 13.4218 187.018 13.4218V12.677C201.368 12.677 213 24.3186 213 38.6791H212.255Z" fill="#AEA5D6"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M67.0944 108.321C67.0944 122.27 78.3936 133.578 92.3316 133.578V134.323C77.9817 134.323 66.3496 122.681 66.3496 108.321H67.0944Z" fill="#AEA5D6"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M181.794 28.4035C183.212 28.0221 184.673 28.8643 185.054 30.2837L184.395 30.4605C184.112 29.4049 183.026 28.7787 181.971 29.0622L181.794 28.4035Z" fill="#171321"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M192.066 29.0622C191.011 28.7787 189.925 29.4049 189.642 30.4605L188.983 30.2837C189.364 28.8643 190.824 28.0221 192.243 28.4035L192.066 29.0622Z" fill="#171321"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M195.892 33.5398V36.1862L192.04 37.4786L191.823 36.832L195.21 35.6956V33.5398H195.892Z" fill="#171321"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M195.213 44.8355L192.045 43.7774L192.262 43.1305L195.895 44.3442V46.991H195.213V44.8355Z" fill="#171321"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M178.827 35.6957V33.5398H178.145V36.1861L181.995 37.4786L182.212 36.832L178.827 35.6957Z" fill="#171321"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M181.776 43.1305L181.992 43.7773L178.827 44.8354V46.991H178.145V44.3443L181.776 43.1305Z" fill="#171321"/>
+<path d="M182.675 34.3942L181.129 37.5941C179.03 41.9415 182.192 46.9908 187.019 46.9908C191.845 46.9908 195.008 41.9415 192.908 37.5941L191.363 34.3942" fill="#A888F4"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M186.678 46.9908V34.3942H187.36V46.9908H186.678Z" fill="#171321"/>
+<path d="M191.25 34.3939H182.788V33.2534C182.788 28.2918 191.25 28.2987 191.25 33.2534V34.3939Z" fill="#7759C1"/>
+<path d="M92.4612 126.064C103.088 126.064 111.704 117.442 111.704 106.806C111.704 96.1706 103.088 87.5487 92.4612 87.5487C81.8339 87.5487 73.2188 96.1706 73.2188 106.806C73.2188 117.442 81.8339 126.064 92.4612 126.064Z" fill="white"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M92.4604 87.89C82.0217 87.89 73.559 96.359 73.559 106.806C73.559 117.254 82.0217 125.723 92.4604 125.723C102.899 125.723 111.362 117.254 111.362 106.806C111.362 96.359 102.899 87.89 92.4604 87.89ZM72.877 106.806C72.877 95.9828 81.6445 87.208 92.4604 87.208C103.276 87.208 112.044 95.9828 112.044 106.806C112.044 117.63 103.276 126.405 92.4604 126.405C81.6445 126.405 72.877 117.63 72.877 106.806Z" fill="#171321"/>
+<path d="M98.2885 105.28H95.9653V101.385C95.9653 99.3302 94.2951 97.6587 92.2419 97.6587C90.1887 97.6587 88.5184 99.3302 88.5184 101.385V105.28H86.1953V101.385C86.1953 98.0489 88.9083 95.3337 92.2419 95.3337C95.5754 95.3337 98.2885 98.0489 98.2885 101.385V105.28Z" fill="#7759C1"/>
+<path d="M100.894 104.186H83.3677V116.836H100.894V104.186Z" fill="#A888F4"/>
+<path d="M92.1343 111.015C92.8235 111.015 93.3823 110.456 93.3823 109.766C93.3823 109.076 92.8235 108.517 92.1343 108.517C91.445 108.517 90.8862 109.076 90.8862 109.766C90.8862 110.456 91.445 111.015 92.1343 111.015Z" fill="#171321"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M91.792 112.209V110.722H92.474V112.209H91.792Z" fill="#171321"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M87.2401 53.2567V60.9103C87.2401 61.9966 88.1212 62.878 89.2061 62.878H121.061C122.523 62.878 123.709 64.0653 123.709 65.5278V67.1601H123.027V65.5278C123.027 64.4415 122.146 63.5601 121.061 63.5601H89.2061C87.7441 63.5601 86.5581 62.3728 86.5581 60.9103V53.2567H87.2401Z" fill="#171321"/>
+<path d="M123.368 67.735C123.685 67.735 123.942 67.4776 123.942 67.1601C123.942 66.8426 123.685 66.5852 123.368 66.5852C123.051 66.5852 122.793 66.8426 122.793 67.1601C122.793 67.4776 123.051 67.735 123.368 67.735Z" fill="#171321"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M123.368 66.9262C123.239 66.9262 123.134 67.0306 123.134 67.16C123.134 67.2895 123.239 67.3939 123.368 67.3939C123.496 67.3939 123.601 67.2895 123.601 67.16C123.601 67.0306 123.496 66.9262 123.368 66.9262ZM122.452 67.16C122.452 66.6545 122.862 66.2441 123.368 66.2441C123.873 66.2441 124.283 66.6545 124.283 67.16C124.283 67.6656 123.873 68.0759 123.368 68.0759C122.862 68.0759 122.452 67.6656 122.452 67.16Z" fill="#171321"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M167.208 82.5551V84.88C167.208 85.5854 167.778 86.1551 168.482 86.1551H193.305V86.8371H168.482C167.4 86.8371 166.526 85.9616 166.526 84.88V82.5551H167.208Z" fill="#171321"/>
+<path d="M166.867 83.13C167.184 83.13 167.441 82.8726 167.441 82.5551C167.441 82.2376 167.184 81.9802 166.867 81.9802C166.55 81.9802 166.292 82.2376 166.292 82.5551C166.292 82.8726 166.55 83.13 166.867 83.13Z" fill="#171321"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M166.866 82.3209C166.737 82.3209 166.633 82.4254 166.633 82.5548C166.633 82.6842 166.737 82.7887 166.866 82.7887C166.995 82.7887 167.1 82.6842 167.1 82.5548C167.1 82.4255 166.995 82.3209 166.866 82.3209ZM165.951 82.5548C165.951 82.0492 166.36 81.6389 166.866 81.6389C167.372 81.6389 167.782 82.0492 167.782 82.5548C167.782 83.0605 167.372 83.4707 166.866 83.4707C166.36 83.4707 165.951 83.0604 165.951 82.5548Z" fill="#171321"/>
+<path d="M86.9023 37.1553C82.8536 41.5743 77.6099 40.6 77.6099 40.6V48.5768C77.6099 49.962 77.8867 51.3404 78.4796 52.5917C80.8973 57.6918 86.9023 59.5873 86.9023 59.5873C86.9023 59.5873 92.9051 57.6918 95.3251 52.5917C95.918 51.3404 96.1948 49.962 96.1948 48.5768V40.6C96.1948 40.6 90.9511 41.5743 86.9023 37.1553V37.1553Z" fill="#10B1B1"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M90.6411 45.6638L85.4674 50.8415L83.1592 48.5289L83.6419 48.0471L85.4677 49.8764L90.1586 45.1818L90.6411 45.6638Z" fill="#171321"/>
+<path d="M197.955 76.3168C193.902 80.7427 188.651 79.7684 188.651 79.7684V87.7567C188.651 89.1443 188.928 90.5249 189.521 91.7786C191.943 96.8856 197.955 98.7834 197.955 98.7834C197.955 98.7834 203.967 96.8856 206.39 91.7786C206.985 90.5249 207.259 89.1443 207.259 87.7567V79.7684C207.259 79.7684 202.009 80.7427 197.955 76.3168Z" fill="#FC6D26"/>
+<path d="M197.19 83.1044L197.566 88.4584H198.343L198.719 83.1044H197.19Z" fill="#171321"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M196.806 82.7468H199.102L198.676 88.8156H197.232L196.806 82.7468ZM197.572 83.4616L197.898 88.1009H198.009L198.335 83.4616H197.572Z" fill="#171321"/>
+<path d="M198.343 90.2083H197.566V91.1248H198.343V90.2083Z" fill="#171321"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M197.208 89.851H198.7V91.4823H197.208V89.851ZM197.922 90.5657V90.7675H197.985V90.5657H197.922Z" fill="#171321"/>
+</g>
+<defs>
+<filter id="filter0_b_187_122567" x="-50" y="-50" width="380" height="230" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
+<feFlood flood-opacity="0" result="BackgroundImageFix"/>
+<feGaussianBlur in="BackgroundImageFix" stdDeviation="25"/>
+<feComposite in2="SourceAlpha" operator="in" result="effect1_backgroundBlur_187_122567"/>
+<feBlend mode="normal" in="SourceGraphic" in2="effect1_backgroundBlur_187_122567" result="shape"/>
+</filter>
+<radialGradient id="paint0_radial_187_122567" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(189.5 -42.5) rotate(89.5818) scale(125.986)">
+<stop stop-color="#7759C2"/>
+<stop offset="1" stop-color="#7759C2" stop-opacity="0"/>
+</radialGradient>
+<radialGradient id="paint1_radial_187_122567" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(-41.5 -97.5) rotate(89.5818) scale(190.176)">
+<stop stop-color="#D64028"/>
+<stop offset="1" stop-color="#D64028" stop-opacity="0"/>
+</radialGradient>
+<radialGradient id="paint2_radial_187_122567" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(309.5 -7.5) rotate(89.5818) scale(116.405)">
+<stop stop-color="#EF76F1"/>
+<stop offset="1" stop-color="#EF76F1" stop-opacity="0"/>
+</radialGradient>
+<clipPath id="clip0_187_122567">
+<rect width="280" height="140" fill="white"/>
+</clipPath>
+<clipPath id="clip1_187_122567">
+<path d="M0 4C0 1.79086 1.79086 0 4 0H276C278.209 0 280 1.79086 280 4V140H0V4Z" fill="white"/>
+</clipPath>
+</defs>
+</svg>
diff --git a/app/assets/javascripts/admin/background_migrations/components/database_listbox.vue b/app/assets/javascripts/admin/background_migrations/components/database_listbox.vue
index 80c216024a0..8e814cd55ef 100644
--- a/app/assets/javascripts/admin/background_migrations/components/database_listbox.vue
+++ b/app/assets/javascripts/admin/background_migrations/components/database_listbox.vue
@@ -1,5 +1,5 @@
<script>
-import { GlListbox } from '@gitlab/ui';
+import { GlCollapsibleListbox } from '@gitlab/ui';
import { s__ } from '~/locale';
import { setUrlParams, visitUrl } from '~/lib/utils/url_utility';
@@ -9,7 +9,7 @@ export default {
database: s__('BackgroundMigrations|Database'),
},
components: {
- GlListbox,
+ GlCollapsibleListbox,
},
props: {
databases: {
@@ -39,7 +39,7 @@ export default {
<label id="label" class="gl-font-weight-bold gl-mr-4 gl-mb-0">{{
$options.i18n.database
}}</label>
- <gl-listbox
+ <gl-collapsible-listbox
v-model="selected"
:items="databases"
right
diff --git a/app/assets/javascripts/admin/broadcast_messages/components/base.vue b/app/assets/javascripts/admin/broadcast_messages/components/base.vue
index b7bafe46327..f869d21d55f 100644
--- a/app/assets/javascripts/admin/broadcast_messages/components/base.vue
+++ b/app/assets/javascripts/admin/broadcast_messages/components/base.vue
@@ -5,14 +5,18 @@ import { buildUrlWithCurrentLocation } from '~/lib/utils/common_utils';
import { createAlert, VARIANT_DANGER } from '~/flash';
import { s__ } from '~/locale';
import axios from '~/lib/utils/axios_utils';
+import { NEW_BROADCAST_MESSAGE } from '../constants';
+import MessageForm from './message_form.vue';
import MessagesTable from './messages_table.vue';
const PER_PAGE = 20;
export default {
name: 'BroadcastMessagesBase',
+ NEW_BROADCAST_MESSAGE,
components: {
GlPagination,
+ MessageForm,
MessagesTable,
},
@@ -97,6 +101,7 @@ export default {
<template>
<div>
+ <message-form :broadcast-message="$options.NEW_BROADCAST_MESSAGE" />
<messages-table
v-if="hasVisibleMessages"
:messages="visibleMessages"
diff --git a/app/assets/javascripts/admin/broadcast_messages/components/datetime_picker.vue b/app/assets/javascripts/admin/broadcast_messages/components/datetime_picker.vue
new file mode 100644
index 00000000000..07814ef2511
--- /dev/null
+++ b/app/assets/javascripts/admin/broadcast_messages/components/datetime_picker.vue
@@ -0,0 +1,47 @@
+<script>
+import { GlDatepicker, GlFormInput } from '@gitlab/ui';
+import { dateToTimeInputValue, timeToHoursMinutes } from '~/lib/utils/datetime/date_format_utility';
+
+export default {
+ name: 'DatetimePicker',
+ components: {
+ GlDatepicker,
+ GlFormInput,
+ },
+ props: {
+ value: {
+ type: Date,
+ required: true,
+ },
+ },
+ computed: {
+ date: {
+ get() {
+ return this.value;
+ },
+ set(val) {
+ const dup = new Date(this.value.getTime());
+ dup.setFullYear(val.getFullYear(), val.getMonth(), val.getDate());
+ this.$emit('input', dup);
+ },
+ },
+ time: {
+ get() {
+ return dateToTimeInputValue(this.value);
+ },
+ set(val) {
+ const dup = new Date(this.value.getTime());
+ const { hours, minutes } = timeToHoursMinutes(val);
+ dup.setHours(hours, minutes);
+ this.$emit('input', dup);
+ },
+ },
+ },
+};
+</script>
+<template>
+ <div class="gl-display-flex gl-gap-3 gl-align-items-center">
+ <gl-datepicker v-model="date" />
+ <gl-form-input v-model="time" size="sm" type="time" data-testid="time-picker" />
+ </div>
+</template>
diff --git a/app/assets/javascripts/admin/broadcast_messages/components/message_form.vue b/app/assets/javascripts/admin/broadcast_messages/components/message_form.vue
new file mode 100644
index 00000000000..36796708e78
--- /dev/null
+++ b/app/assets/javascripts/admin/broadcast_messages/components/message_form.vue
@@ -0,0 +1,225 @@
+<script>
+import {
+ GlButton,
+ GlBroadcastMessage,
+ GlForm,
+ GlFormCheckbox,
+ GlFormCheckboxGroup,
+ GlFormInput,
+ GlFormSelect,
+ GlFormText,
+ GlFormTextarea,
+} from '@gitlab/ui';
+import axios from '~/lib/utils/axios_utils';
+import { s__ } from '~/locale';
+import { createAlert, VARIANT_DANGER } from '~/flash';
+import { redirectTo } from '~/lib/utils/url_utility';
+import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
+import { BROADCAST_MESSAGES_PATH, THEMES, TYPES, TYPE_BANNER } from '../constants';
+import MessageFormGroup from './message_form_group.vue';
+import DatetimePicker from './datetime_picker.vue';
+
+const FORM_HEADERS = { headers: { 'Content-Type': 'application/json; charset=utf-8' } };
+
+export default {
+ name: 'MessageForm',
+ components: {
+ DatetimePicker,
+ GlButton,
+ GlBroadcastMessage,
+ GlForm,
+ GlFormCheckbox,
+ GlFormCheckboxGroup,
+ GlFormInput,
+ GlFormSelect,
+ GlFormText,
+ GlFormTextarea,
+ MessageFormGroup,
+ },
+ mixins: [glFeatureFlagsMixin()],
+ inject: ['targetAccessLevelOptions'],
+ i18n: {
+ message: s__('BroadcastMessages|Message'),
+ messagePlaceholder: s__('BroadcastMessages|Your message here'),
+ type: s__('BroadcastMessages|Type'),
+ theme: s__('BroadcastMessages|Theme'),
+ dismissable: s__('BroadcastMessages|Dismissable'),
+ dismissableDescription: s__('BroadcastMessages|Allow users to dismiss the broadcast message'),
+ targetRoles: s__('BroadcastMessages|Target roles'),
+ targetRolesDescription: s__(
+ 'BroadcastMessages|The broadcast message displays only to users in projects and groups who have these roles.',
+ ),
+ targetPath: s__('BroadcastMessages|Target Path'),
+ targetPathDescription: s__('BroadcastMessages|Paths can contain wildcards, like */welcome'),
+ startsAt: s__('BroadcastMessages|Starts at'),
+ endsAt: s__('BroadcastMessages|Ends at'),
+ add: s__('BroadcastMessages|Add broadcast message'),
+ addError: s__('BroadcastMessages|There was an error adding broadcast message.'),
+ update: s__('BroadcastMessages|Update broadcast message'),
+ updateError: s__('BroadcastMessages|There was an error updating broadcast message.'),
+ },
+ messageThemes: THEMES,
+ messageTypes: TYPES,
+ props: {
+ broadcastMessage: {
+ type: Object,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ loading: false,
+ message: this.broadcastMessage.message,
+ type: this.broadcastMessage.broadcastType,
+ theme: this.broadcastMessage.theme,
+ dismissable: this.broadcastMessage.dismissable || false,
+ targetPath: this.broadcastMessage.targetPath,
+ targetAccessLevels: this.broadcastMessage.targetAccessLevels,
+ targetAccessLevelOptions: this.targetAccessLevelOptions.map(([text, value]) => ({
+ text,
+ value,
+ })),
+ startsAt: new Date(this.broadcastMessage.startsAt.getTime()),
+ endsAt: new Date(this.broadcastMessage.endsAt.getTime()),
+ };
+ },
+ computed: {
+ isBanner() {
+ return this.type === TYPE_BANNER;
+ },
+ messageBlank() {
+ return this.message.trim() === '';
+ },
+ messagePreview() {
+ return this.messageBlank ? this.$options.i18n.messagePlaceholder : this.message;
+ },
+ isAddForm() {
+ return !this.broadcastMessage.id;
+ },
+ formPath() {
+ return this.isAddForm
+ ? BROADCAST_MESSAGES_PATH
+ : `${BROADCAST_MESSAGES_PATH}/${this.broadcastMessage.id}`;
+ },
+ formPayload() {
+ return JSON.stringify({
+ message: this.message,
+ broadcast_type: this.type,
+ theme: this.theme,
+ dismissable: this.dismissable,
+ target_path: this.targetPath,
+ target_access_levels: this.targetAccessLevels,
+ starts_at: this.startsAt.toISOString(),
+ ends_at: this.endsAt.toISOString(),
+ });
+ },
+ },
+ methods: {
+ async onSubmit() {
+ this.loading = true;
+
+ const success = await this.submitForm();
+ if (success) {
+ redirectTo(BROADCAST_MESSAGES_PATH);
+ } else {
+ this.loading = false;
+ }
+ },
+
+ async submitForm() {
+ const requestMethod = this.isAddForm ? 'post' : 'patch';
+
+ try {
+ await axios[requestMethod](this.formPath, this.formPayload, FORM_HEADERS);
+ } catch (e) {
+ const message = this.isAddForm
+ ? this.$options.i18n.addError
+ : this.$options.i18n.updateError;
+ createAlert({ message, variant: VARIANT_DANGER });
+ return false;
+ }
+ return true;
+ },
+ },
+};
+</script>
+<template>
+ <gl-form @submit.prevent="onSubmit">
+ <gl-broadcast-message class="gl-my-6" :type="type" :theme="theme" :dismissible="dismissable">
+ {{ messagePreview }}
+ </gl-broadcast-message>
+
+ <message-form-group :label="$options.i18n.message" label-for="message-textarea">
+ <gl-form-textarea
+ id="message-textarea"
+ v-model="message"
+ size="sm"
+ :placeholder="$options.i18n.messagePlaceholder"
+ />
+ </message-form-group>
+
+ <message-form-group :label="$options.i18n.type" label-for="type-select">
+ <gl-form-select id="type-select" v-model="type" :options="$options.messageTypes" />
+ </message-form-group>
+
+ <template v-if="isBanner">
+ <message-form-group :label="$options.i18n.theme" label-for="theme-select">
+ <gl-form-select
+ id="theme-select"
+ v-model="theme"
+ :options="$options.messageThemes"
+ data-testid="theme-select"
+ />
+ </message-form-group>
+
+ <message-form-group :label="$options.i18n.dismissable" label-for="dismissable-checkbox">
+ <gl-form-checkbox
+ id="dismissable-checkbox"
+ v-model="dismissable"
+ class="gl-mt-3"
+ data-testid="dismissable-checkbox"
+ >
+ <span>{{ $options.i18n.dismissableDescription }}</span>
+ </gl-form-checkbox>
+ </message-form-group>
+ </template>
+
+ <message-form-group
+ v-if="glFeatures.roleTargetedBroadcastMessages"
+ :label="$options.i18n.targetRoles"
+ data-testid="target-roles-checkboxes"
+ >
+ <gl-form-checkbox-group v-model="targetAccessLevels" :options="targetAccessLevelOptions" />
+ <gl-form-text>
+ {{ $options.i18n.targetRolesDescription }}
+ </gl-form-text>
+ </message-form-group>
+
+ <message-form-group :label="$options.i18n.targetPath" label-for="target-path-input">
+ <gl-form-input id="target-path-input" v-model="targetPath" />
+ <gl-form-text>
+ {{ $options.i18n.targetPathDescription }}
+ </gl-form-text>
+ </message-form-group>
+
+ <message-form-group :label="$options.i18n.startsAt">
+ <datetime-picker v-model="startsAt" />
+ </message-form-group>
+
+ <message-form-group :label="$options.i18n.endsAt">
+ <datetime-picker v-model="endsAt" />
+ </message-form-group>
+
+ <div class="form-actions gl-mb-3">
+ <gl-button
+ type="submit"
+ variant="confirm"
+ :loading="loading"
+ :disabled="messageBlank"
+ data-testid="submit-button"
+ >
+ {{ isAddForm ? $options.i18n.add : $options.i18n.update }}
+ </gl-button>
+ </div>
+ </gl-form>
+</template>
diff --git a/app/assets/javascripts/admin/broadcast_messages/components/message_form_group.vue b/app/assets/javascripts/admin/broadcast_messages/components/message_form_group.vue
new file mode 100644
index 00000000000..eec51c0c28b
--- /dev/null
+++ b/app/assets/javascripts/admin/broadcast_messages/components/message_form_group.vue
@@ -0,0 +1,34 @@
+<script>
+import { GlFormGroup } from '@gitlab/ui';
+
+export default {
+ name: 'MessageFormGroup',
+ components: {
+ GlFormGroup,
+ },
+ props: {
+ label: {
+ type: String,
+ required: true,
+ },
+ labelFor: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ },
+};
+</script>
+<template>
+ <div>
+ <gl-form-group
+ :label="label"
+ :label-for="labelFor"
+ label-cols-sm="2"
+ label-class="gl-mt-3"
+ label-align-sm="right"
+ >
+ <slot></slot>
+ </gl-form-group>
+ </div>
+</template>
diff --git a/app/assets/javascripts/admin/broadcast_messages/components/messages_table.vue b/app/assets/javascripts/admin/broadcast_messages/components/messages_table.vue
index 1408312d3e4..a523dd3b391 100644
--- a/app/assets/javascripts/admin/broadcast_messages/components/messages_table.vue
+++ b/app/assets/javascripts/admin/broadcast_messages/components/messages_table.vue
@@ -1,6 +1,8 @@
<script>
-import { GlButton, GlTableLite, GlSafeHtmlDirective } from '@gitlab/ui';
+import { GlButton, GlTableLite } from '@gitlab/ui';
+import SafeHtml from '~/vue_shared/directives/safe_html';
import { __ } from '~/locale';
+import { formatDate } from '~/lib/utils/datetime/date_format_utility';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
const DEFAULT_TD_CLASSES = 'gl-vertical-align-middle!';
@@ -12,7 +14,7 @@ export default {
GlTableLite,
},
directives: {
- SafeHtml: GlSafeHtmlDirective,
+ SafeHtml,
},
mixins: [glFeatureFlagsMixin()],
i18n: {
@@ -77,6 +79,11 @@ export default {
safeHtmlConfig: {
ADD_TAGS: ['use'],
},
+ methods: {
+ formatDate(dateString) {
+ return formatDate(new Date(dateString));
+ },
+ },
};
</script>
<template>
@@ -90,6 +97,14 @@ export default {
<div v-safe-html:[$options.safeHtmlConfig]="preview"></div>
</template>
+ <template #cell(starts_at)="{ item: { starts_at } }">
+ {{ formatDate(starts_at) }}
+ </template>
+
+ <template #cell(ends_at)="{ item: { ends_at } }">
+ {{ formatDate(ends_at) }}
+ </template>
+
<template #cell(buttons)="{ item: { id, edit_path, disable_delete } }">
<gl-button
icon="pencil"
diff --git a/app/assets/javascripts/admin/broadcast_messages/constants.js b/app/assets/javascripts/admin/broadcast_messages/constants.js
new file mode 100644
index 00000000000..6250d5a943d
--- /dev/null
+++ b/app/assets/javascripts/admin/broadcast_messages/constants.js
@@ -0,0 +1,35 @@
+import { s__ } from '~/locale';
+
+export const BROADCAST_MESSAGES_PATH = '/admin/broadcast_messages';
+
+export const TYPE_BANNER = 'banner';
+export const TYPE_NOTIFICATION = 'notification';
+
+export const TYPES = [
+ { value: TYPE_BANNER, text: s__('BroadcastMessages|Banner') },
+ { value: TYPE_NOTIFICATION, text: s__('BroadcastMessages|Notification') },
+];
+
+export const THEMES = [
+ { value: 'indigo', text: s__('BroadcastMessages|Indigo') },
+ { value: 'light-indigo', text: s__('BroadcastMessages|Light Indigo') },
+ { value: 'blue', text: s__('BroadcastMessages|Blue') },
+ { value: 'light-blue', text: s__('BroadcastMessages|Light Blue') },
+ { value: 'green', text: s__('BroadcastMessages|Green') },
+ { value: 'light-green', text: s__('BroadcastMessages|Light Green') },
+ { value: 'red', text: s__('BroadcastMessages|Red') },
+ { value: 'light-red', text: s__('BroadcastMessages|Light Red') },
+ { value: 'dark', text: s__('BroadcastMessages|Dark') },
+ { value: 'light', text: s__('BroadcastMessages|Light') },
+];
+
+export const NEW_BROADCAST_MESSAGE = {
+ message: '',
+ broadcastType: TYPES[0].value,
+ theme: THEMES[0].value,
+ dismissable: false,
+ targetPath: '',
+ targetAccessLevels: [],
+ startsAt: new Date(),
+ endsAt: new Date(),
+};
diff --git a/app/assets/javascripts/admin/broadcast_messages/edit.js b/app/assets/javascripts/admin/broadcast_messages/edit.js
new file mode 100644
index 00000000000..70a270f7a56
--- /dev/null
+++ b/app/assets/javascripts/admin/broadcast_messages/edit.js
@@ -0,0 +1,43 @@
+import Vue from 'vue';
+import MessageForm from './components/message_form.vue';
+
+export default () => {
+ const el = document.querySelector('#js-broadcast-message');
+ const {
+ id,
+ message,
+ broadcastType,
+ theme,
+ dismissable,
+ targetAccessLevels,
+ targetAccessLevelOptions,
+ targetPath,
+ startsAt,
+ endsAt,
+ } = el.dataset;
+
+ return new Vue({
+ el,
+ name: 'EditBroadcastMessage',
+ provide: {
+ targetAccessLevelOptions: JSON.parse(targetAccessLevelOptions),
+ },
+ render(createElement) {
+ return createElement(MessageForm, {
+ props: {
+ broadcastMessage: {
+ id: parseInt(id, 10),
+ message,
+ broadcastType,
+ theme,
+ dismissable: dismissable === 'true',
+ targetAccessLevels: JSON.parse(targetAccessLevels),
+ targetPath,
+ startsAt: new Date(startsAt),
+ endsAt: new Date(endsAt),
+ },
+ },
+ });
+ },
+ });
+};
diff --git a/app/assets/javascripts/admin/broadcast_messages/index.js b/app/assets/javascripts/admin/broadcast_messages/index.js
index 81952d2033e..fd8b2aad4ec 100644
--- a/app/assets/javascripts/admin/broadcast_messages/index.js
+++ b/app/assets/javascripts/admin/broadcast_messages/index.js
@@ -3,11 +3,14 @@ import BroadcastMessagesBase from './components/base.vue';
export default () => {
const el = document.querySelector('#js-broadcast-messages');
- const { page, messagesCount, messages } = el.dataset;
+ const { page, targetAccessLevelOptions, messagesCount, messages } = el.dataset;
return new Vue({
el,
name: 'BroadcastMessages',
+ provide: {
+ targetAccessLevelOptions: JSON.parse(targetAccessLevelOptions),
+ },
render(createElement) {
return createElement(BroadcastMessagesBase, {
props: {
diff --git a/app/assets/javascripts/alert_management/components/alert_management_table.vue b/app/assets/javascripts/alert_management/components/alert_management_table.vue
index c0cac958a42..5229d4c9ae2 100644
--- a/app/assets/javascripts/alert_management/components/alert_management_table.vue
+++ b/app/assets/javascripts/alert_management/components/alert_management_table.vue
@@ -17,6 +17,7 @@ import { fetchPolicies } from '~/lib/graphql';
import { joinPaths, visitUrl } from '~/lib/utils/url_utility';
import { s__, __, n__ } from '~/locale';
import AlertStatus from '~/vue_shared/alert_details/components/alert_status.vue';
+import { TOKEN_TYPE_ASSIGNEE } from '~/vue_shared/components/filtered_search_bar/constants';
import {
tdClass,
thClass,
@@ -96,6 +97,7 @@ export default {
sortable: true,
},
],
+ filterSearchTokens: [TOKEN_TYPE_ASSIGNEE],
severityLabels: SEVERITY_LEVELS,
statusTabs: ALERTS_STATUS_TABS,
components: {
@@ -294,9 +296,7 @@ export default {
:status-tabs="$options.statusTabs"
:track-views-options="$options.trackAlertListViewsOptions"
:server-error-message="serverErrorMessage"
- :filter-search-tokens="/* eslint-disable @gitlab/vue-no-new-non-primitive-in-template */ [
- 'assignee_username',
- ] /* eslint-enable @gitlab/vue-no-new-non-primitive-in-template */"
+ :filter-search-tokens="$options.filterSearchTokens"
filter-search-key="alerts"
@page-changed="pageChanged"
@tabs-changed="statusChanged"
@@ -312,6 +312,7 @@ export default {
<template #table>
<gl-table
class="alert-management-table"
+ data-qa-selector="alert_table_container"
:items="
alerts
? alerts.list
diff --git a/app/assets/javascripts/alerts_settings/components/alerts_form.vue b/app/assets/javascripts/alerts_settings/components/alerts_form.vue
index 388d925196b..a0d5cb7f4c3 100644
--- a/app/assets/javascripts/alerts_settings/components/alerts_form.vue
+++ b/app/assets/javascripts/alerts_settings/components/alerts_form.vue
@@ -83,7 +83,7 @@ export default {
</p>
<form ref="settingsForm" @submit.prevent="updateAlertsIntegrationSettings">
<gl-form-group class="gl-pl-0">
- <gl-form-checkbox v-model="createIssueEnabled" data-qa-selector="create_issue_checkbox">
+ <gl-form-checkbox v-model="createIssueEnabled" data-qa-selector="create_incident_checkbox">
<span>{{ $options.i18n.createIncident.label }}</span>
</gl-form-checkbox>
</gl-form-group>
diff --git a/app/assets/javascripts/alerts_settings/components/alerts_settings_form.vue b/app/assets/javascripts/alerts_settings/components/alerts_settings_form.vue
index 03bc4b825ae..65c3bc732ed 100644
--- a/app/assets/javascripts/alerts_settings/components/alerts_settings_form.vue
+++ b/app/assets/javascripts/alerts_settings/components/alerts_settings_form.vue
@@ -430,6 +430,7 @@ export default {
v-model="integrationForm.type"
:disabled="isSelectDisabled"
class="gl-max-w-full"
+ data-qa-selector="integration_type_dropdown"
:options="integrationTypesOptions"
/>
@@ -461,6 +462,7 @@ export default {
v-model="integrationForm.name"
type="text"
:placeholder="$options.i18n.integrationFormSteps.nameIntegration.placeholder"
+ data-qa-selector="integration_name_field"
@input="validateName"
/>
</gl-form-group>
@@ -483,6 +485,7 @@ export default {
v-model="integrationForm.active"
:is-loading="loading"
:label="$options.i18n.integrationFormSteps.nameIntegration.activeToggle"
+ data-qa-selector="active_toggle_container"
class="gl-mt-4 gl-font-weight-normal"
/>
</gl-form-group>
@@ -594,6 +597,7 @@ export default {
category="secondary"
class="gl-ml-3 js-no-auto-disable"
data-testid="integration-form-test-and-submit"
+ data-qa-selector="save_and_create_alert_button"
@click="submit(true)"
>
{{ $options.i18n.saveAndTestIntegration }}
@@ -695,6 +699,7 @@ export default {
:debounce="$options.JSON_VALIDATE_DELAY"
rows="6"
max-rows="10"
+ data-qa-selector="test_payload_field"
@input="validateJson(false)"
/>
</gl-form-group>
@@ -706,6 +711,7 @@ export default {
data-testid="send-test-alert"
variant="confirm"
class="js-no-auto-disable"
+ data-qa-selector="send_test_alert_button"
@click="isFormDirty ? null : sendTestAlert()"
>
{{ $options.i18n.send }}
diff --git a/app/assets/javascripts/alerts_settings/components/alerts_settings_wrapper.vue b/app/assets/javascripts/alerts_settings/components/alerts_settings_wrapper.vue
index bf456b6adaa..010cb5721a1 100644
--- a/app/assets/javascripts/alerts_settings/components/alerts_settings_wrapper.vue
+++ b/app/assets/javascripts/alerts_settings/components/alerts_settings_wrapper.vue
@@ -375,6 +375,7 @@ export default {
category="secondary"
variant="confirm"
data-testid="add-integration-btn"
+ data-qa-selector="add_integration_button"
class="gl-mt-3"
@click="setFormVisibility(true)"
>
diff --git a/app/assets/javascripts/cycle_analytics/components/base.vue b/app/assets/javascripts/analytics/cycle_analytics/components/base.vue
index f06544f50c6..a688e2f497b 100644
--- a/app/assets/javascripts/cycle_analytics/components/base.vue
+++ b/app/assets/javascripts/analytics/cycle_analytics/components/base.vue
@@ -5,9 +5,9 @@ import { getCookie, setCookie } from '~/lib/utils/common_utils';
import ValueStreamMetrics from '~/analytics/shared/components/value_stream_metrics.vue';
import { VSA_METRICS_GROUPS } from '~/analytics/shared/constants';
import { toYmd } from '~/analytics/shared/utils';
-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 PathNavigation from '~/analytics/cycle_analytics/components/path_navigation.vue';
+import StageTable from '~/analytics/cycle_analytics/components/stage_table.vue';
+import ValueStreamFilters from '~/analytics/cycle_analytics/components/value_stream_filters.vue';
import UrlSync from '~/vue_shared/components/url_sync.vue';
import { __ } from '~/locale';
import { SUMMARY_METRICS_REQUEST, METRICS_REQUESTS } from '../constants';
diff --git a/app/assets/javascripts/cycle_analytics/components/filter_bar.vue b/app/assets/javascripts/analytics/cycle_analytics/components/filter_bar.vue
index 0ad325a8523..54b632968e2 100644
--- a/app/assets/javascripts/cycle_analytics/components/filter_bar.vue
+++ b/app/assets/javascripts/analytics/cycle_analytics/components/filter_bar.vue
@@ -1,12 +1,16 @@
<script>
import { mapActions, mapState } from 'vuex';
import {
- OPERATOR_IS_ONLY,
- DEFAULT_NONE_ANY,
+ OPERATORS_IS,
+ OPTIONS_NONE_ANY,
TOKEN_TITLE_ASSIGNEE,
TOKEN_TITLE_AUTHOR,
TOKEN_TITLE_LABEL,
TOKEN_TITLE_MILESTONE,
+ TOKEN_TYPE_ASSIGNEE,
+ TOKEN_TYPE_AUTHOR,
+ TOKEN_TYPE_LABEL,
+ TOKEN_TYPE_MILESTONE,
} from '~/vue_shared/components/filtered_search_bar/constants';
import FilteredSearchBar from '~/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue';
import {
@@ -14,7 +18,7 @@ import {
processFilters,
filterToQueryObject,
} from '~/vue_shared/components/filtered_search_bar/filtered_search_utils';
-import AuthorToken from '~/vue_shared/components/filtered_search_bar/tokens/author_token.vue';
+import UserToken from '~/vue_shared/components/filtered_search_bar/tokens/user_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';
import UrlSync from '~/vue_shared/components/url_sync.vue';
@@ -47,45 +51,45 @@ export default {
{
icon: 'clock',
title: TOKEN_TITLE_MILESTONE,
- type: 'milestone',
+ type: TOKEN_TYPE_MILESTONE,
token: MilestoneToken,
initialMilestones: this.milestonesData,
unique: true,
symbol: '%',
- operators: OPERATOR_IS_ONLY,
+ operators: OPERATORS_IS,
fetchMilestones: this.fetchMilestones,
},
{
icon: 'labels',
title: TOKEN_TITLE_LABEL,
- type: 'labels',
+ type: TOKEN_TYPE_LABEL,
token: LabelToken,
- defaultLabels: DEFAULT_NONE_ANY,
+ defaultLabels: OPTIONS_NONE_ANY,
initialLabels: this.labelsData,
unique: false,
symbol: '~',
- operators: OPERATOR_IS_ONLY,
+ operators: OPERATORS_IS,
fetchLabels: this.fetchLabels,
},
{
icon: 'pencil',
title: TOKEN_TITLE_AUTHOR,
- type: 'author',
- token: AuthorToken,
- initialAuthors: this.authorsData,
+ type: TOKEN_TYPE_AUTHOR,
+ token: UserToken,
+ initialUsers: this.authorsData,
unique: true,
- operators: OPERATOR_IS_ONLY,
- fetchAuthors: this.fetchAuthors,
+ operators: OPERATORS_IS,
+ fetchUsers: this.fetchAuthors,
},
{
icon: 'user',
title: TOKEN_TITLE_ASSIGNEE,
- type: 'assignees',
- token: AuthorToken,
- initialAuthors: this.assigneesData,
+ type: TOKEN_TYPE_ASSIGNEE,
+ token: UserToken,
+ initialUsers: this.assigneesData,
unique: false,
- operators: OPERATOR_IS_ONLY,
- fetchAuthors: this.fetchAssignees,
+ operators: OPERATORS_IS,
+ fetchUsers: this.fetchAssignees,
},
];
},
@@ -108,14 +112,19 @@ export default {
]),
initialFilterValue() {
return prepareTokens({
- milestone: this.selectedMilestone,
- author: this.selectedAuthor,
- assignees: this.selectedAssigneeList,
- labels: this.selectedLabelList,
+ [TOKEN_TYPE_MILESTONE]: this.selectedMilestone,
+ [TOKEN_TYPE_AUTHOR]: this.selectedAuthor,
+ [TOKEN_TYPE_ASSIGNEE]: this.selectedAssigneeList,
+ [TOKEN_TYPE_LABEL]: this.selectedLabelList,
});
},
handleFilter(filters) {
- const { labels, milestone, author, assignees } = processFilters(filters);
+ const {
+ [TOKEN_TYPE_LABEL]: labels,
+ [TOKEN_TYPE_MILESTONE]: milestone,
+ [TOKEN_TYPE_AUTHOR]: author,
+ [TOKEN_TYPE_ASSIGNEE]: assignees,
+ } = processFilters(filters);
this.setFilters({
selectedAuthor: author ? author[0] : null,
diff --git a/app/assets/javascripts/cycle_analytics/components/formatted_stage_count.vue b/app/assets/javascripts/analytics/cycle_analytics/components/formatted_stage_count.vue
index b622b0441e2..b622b0441e2 100644
--- a/app/assets/javascripts/cycle_analytics/components/formatted_stage_count.vue
+++ b/app/assets/javascripts/analytics/cycle_analytics/components/formatted_stage_count.vue
diff --git a/app/assets/javascripts/cycle_analytics/components/metric_tile.vue b/app/assets/javascripts/analytics/cycle_analytics/components/metric_tile.vue
index a5c20b237b3..a5c20b237b3 100644
--- a/app/assets/javascripts/cycle_analytics/components/metric_tile.vue
+++ b/app/assets/javascripts/analytics/cycle_analytics/components/metric_tile.vue
diff --git a/app/assets/javascripts/cycle_analytics/components/path_navigation.vue b/app/assets/javascripts/analytics/cycle_analytics/components/path_navigation.vue
index 72a7659aac0..ac41bc4917c 100644
--- a/app/assets/javascripts/cycle_analytics/components/path_navigation.vue
+++ b/app/assets/javascripts/analytics/cycle_analytics/components/path_navigation.vue
@@ -1,5 +1,6 @@
<script>
-import { GlPath, GlPopover, GlSkeletonLoader, GlSafeHtmlDirective as SafeHtml } from '@gitlab/ui';
+import { GlPath, GlPopover, GlSkeletonLoader } from '@gitlab/ui';
+import SafeHtml from '~/vue_shared/directives/safe_html';
import Tracking from '~/tracking';
import { OVERVIEW_STAGE_ID } from '../constants';
import FormattedStageCount from './formatted_stage_count.vue';
diff --git a/app/assets/javascripts/cycle_analytics/components/stage_table.vue b/app/assets/javascripts/analytics/cycle_analytics/components/stage_table.vue
index f1fdffd4b72..78ac29426d9 100644
--- a/app/assets/javascripts/cycle_analytics/components/stage_table.vue
+++ b/app/assets/javascripts/analytics/cycle_analytics/components/stage_table.vue
@@ -8,7 +8,7 @@ import {
GlTable,
GlBadge,
} from '@gitlab/ui';
-import FormattedStageCount from '~/cycle_analytics/components/formatted_stage_count.vue';
+import FormattedStageCount from '~/analytics/cycle_analytics/components/formatted_stage_count.vue';
import { __ } from '~/locale';
import Tracking from '~/tracking';
import {
diff --git a/app/assets/javascripts/cycle_analytics/components/total_time.vue b/app/assets/javascripts/analytics/cycle_analytics/components/total_time.vue
index 725952c3518..725952c3518 100644
--- a/app/assets/javascripts/cycle_analytics/components/total_time.vue
+++ b/app/assets/javascripts/analytics/cycle_analytics/components/total_time.vue
diff --git a/app/assets/javascripts/cycle_analytics/components/value_stream_filters.vue b/app/assets/javascripts/analytics/cycle_analytics/components/value_stream_filters.vue
index 17decb6b448..17decb6b448 100644
--- a/app/assets/javascripts/cycle_analytics/components/value_stream_filters.vue
+++ b/app/assets/javascripts/analytics/cycle_analytics/components/value_stream_filters.vue
diff --git a/app/assets/javascripts/cycle_analytics/constants.js b/app/assets/javascripts/analytics/cycle_analytics/constants.js
index 2758d686fb1..2758d686fb1 100644
--- a/app/assets/javascripts/cycle_analytics/constants.js
+++ b/app/assets/javascripts/analytics/cycle_analytics/constants.js
diff --git a/app/assets/javascripts/cycle_analytics/index.js b/app/assets/javascripts/analytics/cycle_analytics/index.js
index 3da8696edeb..df161f7e563 100644
--- a/app/assets/javascripts/cycle_analytics/index.js
+++ b/app/assets/javascripts/analytics/cycle_analytics/index.js
@@ -3,7 +3,7 @@ import {
extractFilterQueryParameters,
extractPaginationQueryParameters,
} from '~/analytics/shared/utils';
-import Translate from '../vue_shared/translate';
+import Translate from '~/vue_shared/translate';
import CycleAnalytics from './components/base.vue';
import createStore from './store';
import { buildCycleAnalyticsInitialData } from './utils';
diff --git a/app/assets/javascripts/cycle_analytics/store/actions.js b/app/assets/javascripts/analytics/cycle_analytics/store/actions.js
index 4a201e00582..4a201e00582 100644
--- a/app/assets/javascripts/cycle_analytics/store/actions.js
+++ b/app/assets/javascripts/analytics/cycle_analytics/store/actions.js
diff --git a/app/assets/javascripts/cycle_analytics/store/getters.js b/app/assets/javascripts/analytics/cycle_analytics/store/getters.js
index 83068cabf0f..83068cabf0f 100644
--- a/app/assets/javascripts/cycle_analytics/store/getters.js
+++ b/app/assets/javascripts/analytics/cycle_analytics/store/getters.js
diff --git a/app/assets/javascripts/cycle_analytics/store/index.js b/app/assets/javascripts/analytics/cycle_analytics/store/index.js
index 76e3e835016..76e3e835016 100644
--- a/app/assets/javascripts/cycle_analytics/store/index.js
+++ b/app/assets/javascripts/analytics/cycle_analytics/store/index.js
diff --git a/app/assets/javascripts/cycle_analytics/store/mutation_types.js b/app/assets/javascripts/analytics/cycle_analytics/store/mutation_types.js
index 9376d81f317..9376d81f317 100644
--- a/app/assets/javascripts/cycle_analytics/store/mutation_types.js
+++ b/app/assets/javascripts/analytics/cycle_analytics/store/mutation_types.js
diff --git a/app/assets/javascripts/cycle_analytics/store/mutations.js b/app/assets/javascripts/analytics/cycle_analytics/store/mutations.js
index 8567529caf2..8567529caf2 100644
--- a/app/assets/javascripts/cycle_analytics/store/mutations.js
+++ b/app/assets/javascripts/analytics/cycle_analytics/store/mutations.js
diff --git a/app/assets/javascripts/cycle_analytics/store/state.js b/app/assets/javascripts/analytics/cycle_analytics/store/state.js
index 8d662333afa..00dd2e53883 100644
--- a/app/assets/javascripts/cycle_analytics/store/state.js
+++ b/app/assets/javascripts/analytics/cycle_analytics/store/state.js
@@ -1,7 +1,7 @@
import {
PAGINATION_SORT_FIELD_END_EVENT,
PAGINATION_SORT_DIRECTION_DESC,
-} from '~/cycle_analytics/constants';
+} from '~/analytics/cycle_analytics/constants';
export default () => ({
id: null,
diff --git a/app/assets/javascripts/cycle_analytics/utils.js b/app/assets/javascripts/analytics/cycle_analytics/utils.js
index 428bb11b950..428bb11b950 100644
--- a/app/assets/javascripts/cycle_analytics/utils.js
+++ b/app/assets/javascripts/analytics/cycle_analytics/utils.js
diff --git a/app/assets/javascripts/api/analytics_api.js b/app/assets/javascripts/api/analytics_api.js
index 15457f28eff..66ed30130bb 100644
--- a/app/assets/javascripts/api/analytics_api.js
+++ b/app/assets/javascripts/api/analytics_api.js
@@ -7,6 +7,11 @@ const PROJECT_VSA_PATH_BASE = '/:request_path/-/analytics/value_stream_analytics
const PROJECT_VSA_STAGES_PATH = `${PROJECT_VSA_PATH_BASE}/:value_stream_id/stages`;
const PROJECT_VSA_STAGE_DATA_PATH = `${PROJECT_VSA_STAGES_PATH}/:stage_id`;
+export const LEAD_TIME_METRIC_TYPE = 'lead_time';
+export const CYCLE_TIME_METRIC_TYPE = 'cycle_time';
+export const ISSUES_METRIC_TYPE = 'issues';
+export const DEPLOYS_METRIC_TYPE = 'deploys';
+
export const METRIC_TYPE_SUMMARY = 'summary';
export const METRIC_TYPE_TIME_SUMMARY = 'time_summary';
diff --git a/app/assets/javascripts/artifacts/graphql/queries/get_job_artifacts.query.graphql b/app/assets/javascripts/artifacts/graphql/queries/get_job_artifacts.query.graphql
index 89a24d7891e..9777153999e 100644
--- a/app/assets/javascripts/artifacts/graphql/queries/get_job_artifacts.query.graphql
+++ b/app/assets/javascripts/artifacts/graphql/queries/get_job_artifacts.query.graphql
@@ -10,6 +10,7 @@ query getJobArtifacts(
project(fullPath: $projectPath) {
id
jobs(
+ withArtifacts: true
statuses: [SUCCESS, FAILED]
first: $firstPageSize
last: $lastPageSize
diff --git a/app/assets/javascripts/awards_handler.js b/app/assets/javascripts/awards_handler.js
index 9ab1d6bfd80..1855fb9ed8c 100644
--- a/app/assets/javascripts/awards_handler.js
+++ b/app/assets/javascripts/awards_handler.js
@@ -2,7 +2,7 @@
import { GlBreakpointInstance as bp } from '@gitlab/ui/dist/utils';
import $ from 'jquery';
-import { uniq } from 'lodash';
+import { uniq, escape } from 'lodash';
import { getEmojiScoreWithIntent } from '~/emoji/utils';
import { getCookie, setCookie, scrollToElement } from '~/lib/utils/common_utils';
import * as Emoji from '~/emoji';
@@ -149,7 +149,7 @@ export class AwardsHandler {
let frequentlyUsedCatgegory = '';
if (frequentlyUsedEmojis.length > 0) {
frequentlyUsedCatgegory = this.renderCategory('Frequently used', frequentlyUsedEmojis, {
- menuListClass: 'frequent-emojis',
+ frequentEmojis: true,
});
}
@@ -228,9 +228,9 @@ export class AwardsHandler {
renderCategory(name, emojiList, opts = {}) {
return `
<h5 class="emoji-menu-title">
- ${name}
+ ${escape(name)}
</h5>
- <ul class="clearfix emoji-menu-list ${opts.menuListClass || ''}">
+ <ul class="clearfix emoji-menu-list ${opts.frequentEmojis ? 'frequent-emojis' : ''}">
${emojiList
.map(
(emojiName) => `
diff --git a/app/assets/javascripts/badges/components/badge_form.vue b/app/assets/javascripts/badges/components/badge_form.vue
index f68666f8a0c..c95c90d5daf 100644
--- a/app/assets/javascripts/badges/components/badge_form.vue
+++ b/app/assets/javascripts/badges/components/badge_form.vue
@@ -1,10 +1,12 @@
<script>
-import { GlLoadingIcon, GlFormInput, GlFormGroup, GlButton, GlSafeHtmlDirective } from '@gitlab/ui';
+import { GlLoadingIcon, GlFormInput, GlFormGroup, GlButton } from '@gitlab/ui';
import { escape, debounce } from 'lodash';
import { mapActions, mapState } from 'vuex';
+import SafeHtml from '~/vue_shared/directives/safe_html';
import { createAlert, VARIANT_INFO } from '~/flash';
import { s__, sprintf } from '~/locale';
import createEmptyBadge from '../empty_badge';
+import { PLACEHOLDERS } from '../constants';
import Badge from './badge.vue';
const badgePreviewDelayInMilliseconds = 1500;
@@ -19,7 +21,7 @@ export default {
GlFormGroup,
},
directives: {
- SafeHtml: GlSafeHtmlDirective,
+ SafeHtml,
},
props: {
isEditing: {
@@ -49,9 +51,9 @@ export default {
return this.badgeInAddForm;
},
helpText() {
- const placeholders = ['project_path', 'project_id', 'default_branch', 'commit_sha']
- .map((placeholder) => `<code>%{${placeholder}}</code>`)
- .join(', ');
+ const placeholders = PLACEHOLDERS.map((placeholder) => `<code>%{${placeholder}}</code>`).join(
+ ', ',
+ );
return sprintf(
s__('Badges|Supported %{docsLinkStart}variables%{docsLinkEnd}: %{placeholders}'),
{
diff --git a/app/assets/javascripts/badges/constants.js b/app/assets/javascripts/badges/constants.js
index 8fbe3db5ef1..709436abca6 100644
--- a/app/assets/javascripts/badges/constants.js
+++ b/app/assets/javascripts/badges/constants.js
@@ -1,2 +1,10 @@
export const GROUP_BADGE = 'group';
export const PROJECT_BADGE = 'project';
+export const PLACEHOLDERS = [
+ 'project_path',
+ 'project_title',
+ 'project_name',
+ 'project_id',
+ 'default_branch',
+ 'commit_sha',
+];
diff --git a/app/assets/javascripts/batch_comments/components/draft_note.vue b/app/assets/javascripts/batch_comments/components/draft_note.vue
index e5408d0734a..5bb310afac7 100644
--- a/app/assets/javascripts/batch_comments/components/draft_note.vue
+++ b/app/assets/javascripts/batch_comments/components/draft_note.vue
@@ -1,6 +1,7 @@
<script>
-import { GlButton, GlSafeHtmlDirective, GlBadge } from '@gitlab/ui';
+import { GlButton, GlBadge } from '@gitlab/ui';
import { mapActions, mapGetters, mapState } from 'vuex';
+import SafeHtml from '~/vue_shared/directives/safe_html';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import NoteableNote from '~/notes/components/noteable_note.vue';
import PublishButton from './publish_button.vue';
@@ -13,7 +14,7 @@ export default {
GlBadge,
},
directives: {
- SafeHtml: GlSafeHtmlDirective,
+ SafeHtml,
},
mixins: [glFeatureFlagMixin()],
props: {
@@ -84,32 +85,25 @@ export default {
};
</script>
<template>
- <article
- class="draft-note-component note-wrapper"
- @mouseenter="handleMouseEnter(draft)"
- @mouseleave="handleMouseLeave(draft)"
+ <noteable-note
+ :note="draft"
+ :line="line"
+ :discussion-root="true"
+ :class="{ 'gl-mb-0!': glFeatures.mrReviewSubmitComment }"
+ class="draft-note-component draft-note"
+ @handleEdit="handleEditing"
+ @cancelForm="handleNotEditing"
+ @updateSuccess="handleNotEditing"
+ @handleDeleteNote="deleteDraft"
+ @handleUpdateNote="update"
+ @toggleResolveStatus="toggleResolveDiscussion(draft.id)"
+ @mouseenter.native="handleMouseEnter(draft)"
+ @mouseleave.native="handleMouseLeave(draft)"
>
- <ul class="notes draft-notes">
- <noteable-note
- :note="draft"
- :line="line"
- :discussion-root="true"
- :class="{ 'gl-mb-0!': glFeatures.mrReviewSubmitComment }"
- class="draft-note"
- @handleEdit="handleEditing"
- @cancelForm="handleNotEditing"
- @updateSuccess="handleNotEditing"
- @handleDeleteNote="deleteDraft"
- @handleUpdateNote="update"
- @toggleResolveStatus="toggleResolveDiscussion(draft.id)"
- >
- <template #note-header-info>
- <gl-badge variant="warning" class="gl-mr-2">{{ __('Pending') }}</gl-badge>
- </template>
- </noteable-note>
- </ul>
-
- <template v-if="!isEditingDraft">
+ <template #note-header-info>
+ <gl-badge variant="warning" class="gl-mr-2">{{ __('Pending') }}</gl-badge>
+ </template>
+ <template v-if="!isEditingDraft" #after-note-body>
<div
v-if="draftCommands"
v-safe-html:[$options.safeHtmlConfig]="draftCommands"
@@ -133,5 +127,5 @@ export default {
</gl-button>
</p>
</template>
- </article>
+ </noteable-note>
</template>
diff --git a/app/assets/javascripts/behaviors/copy_code.js b/app/assets/javascripts/behaviors/copy_code.js
index ae186aba32d..0c81ae63f21 100644
--- a/app/assets/javascripts/behaviors/copy_code.js
+++ b/app/assets/javascripts/behaviors/copy_code.js
@@ -7,7 +7,10 @@ class CopyCodeButton extends HTMLElement {
connectedCallback() {
this.for = uniqueId('code-');
- this.parentNode.querySelector('pre').setAttribute('id', this.for);
+ const target = this.parentNode.querySelector('pre');
+ if (!target) return;
+
+ target.setAttribute('id', this.for);
this.appendChild(this.createButton());
}
diff --git a/app/assets/javascripts/behaviors/index.js b/app/assets/javascripts/behaviors/index.js
index 30160248a77..220064e6673 100644
--- a/app/assets/javascripts/behaviors/index.js
+++ b/app/assets/javascripts/behaviors/index.js
@@ -1,6 +1,5 @@
import $ from 'jquery';
import './autosize';
-import './markdown/render_gfm';
import initCollapseSidebarOnWindowResize from './collapse_sidebar_on_window_resize';
import initCopyToClipboard from './copy_to_clipboard';
import installGlEmojiElement from './gl_emoji';
diff --git a/app/assets/javascripts/behaviors/markdown/init_gfm.js b/app/assets/javascripts/behaviors/markdown/init_gfm.js
new file mode 100644
index 00000000000..d9c7cee50da
--- /dev/null
+++ b/app/assets/javascripts/behaviors/markdown/init_gfm.js
@@ -0,0 +1,13 @@
+import $ from 'jquery';
+import { renderGFM } from '~/behaviors/markdown/render_gfm';
+
+$.fn.renderGFM = function plugin() {
+ this.get().forEach(renderGFM);
+ return this;
+};
+requestIdleCallback(
+ () => {
+ renderGFM(document.body);
+ },
+ { timeout: 500 },
+);
diff --git a/app/assets/javascripts/behaviors/markdown/render_gfm.js b/app/assets/javascripts/behaviors/markdown/render_gfm.js
index a08cf48c327..2eab5b84e3e 100644
--- a/app/assets/javascripts/behaviors/markdown/render_gfm.js
+++ b/app/assets/javascripts/behaviors/markdown/render_gfm.js
@@ -1,45 +1,52 @@
-import $ from 'jquery';
import syntaxHighlight from '~/syntax_highlight';
import highlightCurrentUser from './highlight_current_user';
import { renderKroki } from './render_kroki';
import renderMath from './render_math';
import renderSandboxedMermaid from './render_sandboxed_mermaid';
import renderMetrics from './render_metrics';
+import renderObservability from './render_observability';
import { renderJSONTable } from './render_json_table';
-// Render GitLab flavoured Markdown
-//
-// Delegates to syntax highlight and render math & mermaid diagrams.
-//
-$.fn.renderGFM = function renderGFM() {
- syntaxHighlight(this.find('.js-syntax-highlight').get());
- renderKroki(this.find('.js-render-kroki[hidden]').get());
- renderMath(this.find('.js-render-math'));
- renderSandboxedMermaid(this.find('.js-render-mermaid').get());
- renderJSONTable(
- Array.from(this.find('[lang="json"][data-lang-params="table"]').get()).map((e) => e.parentNode),
- );
-
- highlightCurrentUser(this.find('.gfm-project_member').get());
+function initPopovers(elements) {
+ if (!elements.length) return;
+ import(/* webpackChunkName: 'IssuablePopoverBundle' */ '~/issuable/popover')
+ .then(({ default: initIssuablePopovers }) => {
+ initIssuablePopovers(elements);
+ })
+ .catch(() => {});
+}
- const issuablePopoverElements = this.find('.gfm-issue, .gfm-merge_request').get();
- if (issuablePopoverElements.length) {
- import(/* webpackChunkName: 'IssuablePopoverBundle' */ '~/issuable/popover')
- .then(({ default: initIssuablePopovers }) => {
- initIssuablePopovers(issuablePopoverElements);
- })
- .catch(() => {});
- }
-
- renderMetrics(this.find('.js-render-metrics').get());
- return this;
-};
+// Render GitLab flavoured Markdown
+export function renderGFM(element) {
+ const [
+ highlightEls,
+ krokiEls,
+ mathEls,
+ mermaidEls,
+ tableEls,
+ userEls,
+ popoverEls,
+ metricsEls,
+ observabilityEls,
+ ] = [
+ '.js-syntax-highlight',
+ '.js-render-kroki[hidden]',
+ '.js-render-math',
+ '.js-render-mermaid',
+ '[lang="json"][data-lang-params="table"]',
+ '.gfm-project_member',
+ '.gfm-issue, .gfm-merge_request',
+ '.js-render-metrics',
+ '.js-render-observability',
+ ].map((selector) => Array.from(element.querySelectorAll(selector)));
-$(() => {
- window.requestIdleCallback(
- () => {
- $('body').renderGFM();
- },
- { timeout: 500 },
- );
-});
+ syntaxHighlight(highlightEls);
+ renderKroki(krokiEls);
+ renderMath(mathEls);
+ renderSandboxedMermaid(mermaidEls);
+ renderJSONTable(tableEls.map((e) => e.parentNode));
+ highlightCurrentUser(userEls);
+ renderMetrics(metricsEls);
+ renderObservability(observabilityEls);
+ initPopovers(popoverEls);
+}
diff --git a/app/assets/javascripts/behaviors/markdown/render_math.js b/app/assets/javascripts/behaviors/markdown/render_math.js
index ac41af4df7a..7852a909160 100644
--- a/app/assets/javascripts/behaviors/markdown/render_math.js
+++ b/app/assets/javascripts/behaviors/markdown/render_math.js
@@ -175,14 +175,14 @@ class SafeMathRenderer {
}
}
-export default function renderMath($els) {
- if (!$els.length) return;
+export default function renderMath(elements) {
+ if (!elements.length) return;
Promise.all([
import(/* webpackChunkName: 'katex' */ 'katex'),
import(/* webpackChunkName: 'katex' */ 'katex/dist/katex.min.css'),
])
.then(([katex]) => {
- const renderer = new SafeMathRenderer($els.get(), katex);
+ const renderer = new SafeMathRenderer(elements, katex);
renderer.render();
renderer.attachEvents();
})
diff --git a/app/assets/javascripts/behaviors/markdown/render_observability.js b/app/assets/javascripts/behaviors/markdown/render_observability.js
new file mode 100644
index 00000000000..704d85cf22e
--- /dev/null
+++ b/app/assets/javascripts/behaviors/markdown/render_observability.js
@@ -0,0 +1,33 @@
+import Vue from 'vue';
+import { darkModeEnabled } from '~/lib/utils/color_utils';
+import { setUrlParams } from '~/lib/utils/url_utility';
+
+export function getFrameSrc(url) {
+ return `${setUrlParams({ theme: darkModeEnabled() ? 'dark' : 'light' }, url)}&kiosk`;
+}
+
+const mountVueComponent = (element) => {
+ const url = [element.dataset.frameUrl];
+
+ return new Vue({
+ el: element,
+ render(h) {
+ return h('iframe', {
+ style: {
+ height: '366px',
+ width: '768px',
+ },
+ attrs: {
+ src: getFrameSrc(url),
+ frameBorder: '0',
+ },
+ });
+ },
+ });
+};
+
+export default function renderObservability(elements) {
+ elements.forEach((element) => {
+ mountVueComponent(element);
+ });
+}
diff --git a/app/assets/javascripts/behaviors/preview_markdown.js b/app/assets/javascripts/behaviors/preview_markdown.js
index 68f5180cc03..86a05f24dfc 100644
--- a/app/assets/javascripts/behaviors/preview_markdown.js
+++ b/app/assets/javascripts/behaviors/preview_markdown.js
@@ -4,6 +4,7 @@ import $ from 'jquery';
import { createAlert } from '~/flash';
import axios from '~/lib/utils/axios_utils';
import { __ } from '~/locale';
+import '~/behaviors/markdown/init_gfm';
// MarkdownPreview
//
diff --git a/app/assets/javascripts/behaviors/shortcuts/shortcuts_issuable.js b/app/assets/javascripts/behaviors/shortcuts/shortcuts_issuable.js
index 97ba9e15c0f..64297da39cd 100644
--- a/app/assets/javascripts/behaviors/shortcuts/shortcuts_issuable.js
+++ b/app/assets/javascripts/behaviors/shortcuts/shortcuts_issuable.js
@@ -3,7 +3,7 @@ import ClipboardJS from 'clipboard';
import Mousetrap from 'mousetrap';
import { getSelectedFragment } from '~/lib/utils/common_utils';
import { isElementVisible } from '~/lib/utils/dom_utils';
-import { DEBOUNCE_DROPDOWN_DELAY } from '~/vue_shared/components/sidebar/labels_select_widget/constants';
+import { DEBOUNCE_DROPDOWN_DELAY } from '~/sidebar/components/labels/labels_select_widget/constants';
import toast from '~/vue_shared/plugins/global_toast';
import { s__ } from '~/locale';
import Sidebar from '~/right_sidebar';
diff --git a/app/assets/javascripts/blob/components/blob_header.vue b/app/assets/javascripts/blob/components/blob_header.vue
index 716321430d2..361d736f740 100644
--- a/app/assets/javascripts/blob/components/blob_header.vue
+++ b/app/assets/javascripts/blob/components/blob_header.vue
@@ -1,5 +1,5 @@
<script>
-import DefaultActions from './blob_header_default_actions.vue';
+import DefaultActions from 'jh_else_ce/blob/components/blob_header_default_actions.vue';
import BlobFilepath from './blob_header_filepath.vue';
import ViewerSwitcher from './blob_header_viewer_switcher.vue';
import { SIMPLE_BLOB_VIEWER } from './constants';
diff --git a/app/assets/javascripts/blob/openapi/index.js b/app/assets/javascripts/blob/openapi/index.js
index 24a54358de5..8cfdc00bb40 100644
--- a/app/assets/javascripts/blob/openapi/index.js
+++ b/app/assets/javascripts/blob/openapi/index.js
@@ -5,7 +5,7 @@ const createSandbox = () => {
const iframeEl = document.createElement('iframe');
setAttributes(iframeEl, {
src: '/-/sandbox/swagger',
- sandbox: 'allow-scripts allow-popups',
+ sandbox: 'allow-scripts allow-popups allow-forms',
frameBorder: 0,
width: '100%',
// The height will be adjusted dynamically.
diff --git a/app/assets/javascripts/blob/viewer/index.js b/app/assets/javascripts/blob/viewer/index.js
index 8d323c335d3..439c4258805 100644
--- a/app/assets/javascripts/blob/viewer/index.js
+++ b/app/assets/javascripts/blob/viewer/index.js
@@ -1,5 +1,5 @@
import $ from 'jquery';
-import '~/behaviors/markdown/render_gfm';
+import '~/behaviors/markdown/init_gfm';
import { createAlert } from '~/flash';
import { __ } from '~/locale';
import {
diff --git a/app/assets/javascripts/blob_edit/blob_bundle.js b/app/assets/javascripts/blob_edit/blob_bundle.js
index 4741dd53708..509d399273d 100644
--- a/app/assets/javascripts/blob_edit/blob_bundle.js
+++ b/app/assets/javascripts/blob_edit/blob_bundle.js
@@ -66,7 +66,7 @@ export default () => {
})
.catch((e) =>
createAlert({
- message: e,
+ message: e.message,
}),
);
diff --git a/app/assets/javascripts/blob_edit/edit_blob.js b/app/assets/javascripts/blob_edit/edit_blob.js
index 97d8b206307..46b3f16df77 100644
--- a/app/assets/javascripts/blob_edit/edit_blob.js
+++ b/app/assets/javascripts/blob_edit/edit_blob.js
@@ -9,6 +9,7 @@ import { addEditorMarkdownListeners } from '~/lib/utils/text_markdown';
import { insertFinalNewline } from '~/lib/utils/text_utility';
import TemplateSelectorMediator from '../blob/file_template_mediator';
import { BLOB_EDITOR_ERROR, BLOB_PREVIEW_ERROR } from './constants';
+import '~/behaviors/markdown/init_gfm';
export default class EditBlob {
// The options object has:
diff --git a/app/assets/javascripts/boards/components/board_content.vue b/app/assets/javascripts/boards/components/board_content.vue
index 150378f7a7d..ca86894ca40 100644
--- a/app/assets/javascripts/boards/components/board_content.vue
+++ b/app/assets/javascripts/boards/components/board_content.vue
@@ -1,8 +1,10 @@
<script>
import { GlAlert } from '@gitlab/ui';
+import { breakpoints } from '@gitlab/ui/dist/utils';
import { sortBy, throttle } from 'lodash';
import Draggable from 'vuedraggable';
import { mapState, mapGetters, mapActions } from 'vuex';
+import { contentTop } from '~/lib/utils/common_utils';
import { s__ } from '~/locale';
import { formatBoardLists } from 'ee_else_ce/boards/boards_util';
import BoardAddNewColumn from 'ee_else_ce/boards/components/board_add_new_column.vue';
@@ -114,6 +116,8 @@ export default {
group: 'boards-list',
tag: 'div',
value: this.boardListsToUse,
+ delay: 100,
+ delayOnTouchOnly: true,
};
return this.canDragColumns ? options : {};
@@ -142,7 +146,11 @@ export default {
el.scrollTo({ left: el.scrollWidth, behavior: 'smooth' });
},
setBoardHeight() {
- this.boardHeight = `${window.innerHeight - this.$el.getBoundingClientRect().top}px`;
+ if (window.innerWidth < breakpoints.md) {
+ this.boardHeight = `${window.innerHeight - contentTop()}px`;
+ } else {
+ this.boardHeight = `${window.innerHeight - this.$el.getBoundingClientRect().top}px`;
+ }
},
},
};
diff --git a/app/assets/javascripts/boards/components/board_content_sidebar.vue b/app/assets/javascripts/boards/components/board_content_sidebar.vue
index 00b4e6c96a9..392a73b5859 100644
--- a/app/assets/javascripts/boards/components/board_content_sidebar.vue
+++ b/app/assets/javascripts/boards/components/board_content_sidebar.vue
@@ -14,8 +14,8 @@ import SidebarDateWidget from '~/sidebar/components/date/sidebar_date_widget.vue
import SidebarSeverity from '~/sidebar/components/severity/sidebar_severity.vue';
import SidebarSubscriptionsWidget from '~/sidebar/components/subscriptions/sidebar_subscriptions_widget.vue';
import SidebarTodoWidget from '~/sidebar/components/todo_toggle/sidebar_todo_widget.vue';
-import SidebarLabelsWidget from '~/vue_shared/components/sidebar/labels_select_widget/labels_select_root.vue';
-import { LabelType } from '~/vue_shared/components/sidebar/labels_select_widget/constants';
+import SidebarLabelsWidget from '~/sidebar/components/labels/labels_select_widget/labels_select_root.vue';
+import { LabelType } from '~/sidebar/components/labels/labels_select_widget/constants';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
export default {
@@ -32,10 +32,12 @@ export default {
SidebarTodoWidget,
SidebarSeverity,
MountingPortal,
+ SidebarHealthStatusWidget: () =>
+ import('ee_component/sidebar/components/health_status/sidebar_health_status_widget.vue'),
+ SidebarIterationWidget: () =>
+ import('ee_component/sidebar/components/iteration/sidebar_iteration_widget.vue'),
SidebarWeightWidget: () =>
import('ee_component/sidebar/components/weight/sidebar_weight_widget.vue'),
- IterationSidebarDropdownWidget: () =>
- import('ee_component/sidebar/components/iteration_sidebar_dropdown_widget.vue'),
},
mixins: [glFeatureFlagMixin()],
inject: {
@@ -51,6 +53,9 @@ export default {
weightFeatureAvailable: {
default: false,
},
+ healthStatusFeatureAvailable: {
+ default: false,
+ },
allowLabelEdit: {
default: false,
},
@@ -115,6 +120,7 @@ export default {
'setActiveItemConfidential',
'setActiveBoardItemLabels',
'setActiveItemWeight',
+ 'setActiveItemHealthStatus',
]),
handleClose() {
this.toggleBoardItem({ boardItem: this.activeBoardItem, sidebarType: this.sidebarType });
@@ -143,7 +149,7 @@ export default {
<gl-drawer
v-bind="$attrs"
:open="showSidebar"
- class="boards-sidebar gl-absolute"
+ class="boards-sidebar"
variant="sidebar"
@close="handleClose"
>
@@ -187,7 +193,7 @@ export default {
:issuable-type="issuableType"
data-testid="sidebar-milestones"
/>
- <iteration-sidebar-dropdown-widget
+ <sidebar-iteration-widget
v-if="iterationFeatureAvailable && !isIncidentSidebar"
:iid="activeBoardItem.iid"
:workspace-path="projectPathForActiveIssue"
@@ -236,6 +242,13 @@ export default {
:issuable-type="issuableType"
@weightUpdated="setActiveItemWeight($event)"
/>
+ <sidebar-health-status-widget
+ v-if="healthStatusFeatureAvailable"
+ :iid="activeBoardItem.iid"
+ :full-path="fullPath"
+ :issuable-type="issuableType"
+ @statusUpdated="setActiveItemHealthStatus($event)"
+ />
<sidebar-confidentiality-widget
:iid="activeBoardItem.iid"
:full-path="fullPath"
diff --git a/app/assets/javascripts/boards/components/board_list.vue b/app/assets/javascripts/boards/components/board_list.vue
index 816b22e4dc6..215691c7ba2 100644
--- a/app/assets/javascripts/boards/components/board_list.vue
+++ b/app/assets/javascripts/boards/components/board_list.vue
@@ -133,6 +133,8 @@ export default {
'ghost-class': 'board-card-drag-active',
'data-list-id': this.list.id,
value: this.boardItems,
+ delay: 100,
+ delayOnTouchOnly: true,
};
return this.canMoveIssue ? options : {};
@@ -317,7 +319,7 @@ export default {
>
<!-- TODO: remove the condition when https://gitlab.com/gitlab-org/gitlab/-/issues/377862 is resolved -->
<board-card-move-to-position
- v-if="!isEpicBoard"
+ v-if="!isEpicBoard && !disabled"
:item="item"
:index="index"
:list="list"
diff --git a/app/assets/javascripts/boards/components/boards_selector.vue b/app/assets/javascripts/boards/components/boards_selector.vue
index eaf3facb450..4f90d77c0be 100644
--- a/app/assets/javascripts/boards/components/boards_selector.vue
+++ b/app/assets/javascripts/boards/components/boards_selector.vue
@@ -237,7 +237,7 @@ export default {
:text="board.name"
@show="loadBoards"
>
- <p class="gl-new-dropdown-header-top" @mousedown.prevent>
+ <p class="gl-dropdown-header-top" @mousedown.prevent>
{{ s__('IssueBoards|Switch board') }}
</p>
<gl-search-box-by-type ref="searchBox" v-model="filterTerm" class="m-2" />
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 605e11d1590..bc68c2e0e99 100644
--- a/app/assets/javascripts/boards/components/issue_board_filtered_search.vue
+++ b/app/assets/javascripts/boards/components/issue_board_filtered_search.vue
@@ -12,8 +12,8 @@ import { TYPE_USER } from '~/graphql_shared/constants';
import { convertToGraphQLId } from '~/graphql_shared/utils';
import { __ } from '~/locale';
import {
- OPERATOR_IS_AND_IS_NOT,
- OPERATOR_IS_ONLY,
+ OPERATORS_IS_NOT,
+ OPERATORS_IS,
TOKEN_TITLE_ASSIGNEE,
TOKEN_TITLE_AUTHOR,
TOKEN_TITLE_CONFIDENTIAL,
@@ -31,7 +31,7 @@ import {
TOKEN_TYPE_RELEASE,
TOKEN_TYPE_TYPE,
} from '~/vue_shared/components/filtered_search_bar/constants';
-import AuthorToken from '~/vue_shared/components/filtered_search_bar/tokens/author_token.vue';
+import UserToken from '~/vue_shared/components/filtered_search_bar/tokens/user_token.vue';
import EmojiToken from '~/vue_shared/components/filtered_search_bar/tokens/emoji_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';
@@ -60,7 +60,7 @@ export default {
tokensCE() {
const { issue, incident } = this.$options.i18n;
const { types } = this.$options;
- const { fetchAuthors, fetchLabels } = issueBoardFilters(
+ const { fetchUsers, fetchLabels } = issueBoardFilters(
this.$apollo,
this.fullPath,
this.boardType,
@@ -71,28 +71,28 @@ export default {
icon: 'user',
title: TOKEN_TITLE_ASSIGNEE,
type: TOKEN_TYPE_ASSIGNEE,
- operators: OPERATOR_IS_AND_IS_NOT,
- token: AuthorToken,
+ operators: OPERATORS_IS_NOT,
+ token: UserToken,
unique: true,
- fetchAuthors,
- preloadedAuthors: this.preloadedAuthors(),
+ fetchUsers,
+ preloadedUsers: this.preloadedUsers(),
},
{
icon: 'pencil',
title: TOKEN_TITLE_AUTHOR,
type: TOKEN_TYPE_AUTHOR,
- operators: OPERATOR_IS_AND_IS_NOT,
+ operators: OPERATORS_IS_NOT,
symbol: '@',
- token: AuthorToken,
+ token: UserToken,
unique: true,
- fetchAuthors,
- preloadedAuthors: this.preloadedAuthors(),
+ fetchUsers,
+ preloadedUsers: this.preloadedUsers(),
},
{
icon: 'labels',
title: TOKEN_TITLE_LABEL,
type: TOKEN_TYPE_LABEL,
- operators: OPERATOR_IS_AND_IS_NOT,
+ operators: OPERATORS_IS_NOT,
token: LabelToken,
unique: false,
symbol: '~',
@@ -128,7 +128,7 @@ export default {
title: TOKEN_TITLE_CONFIDENTIAL,
unique: true,
token: GlFilteredSearchToken,
- operators: OPERATOR_IS_ONLY,
+ operators: OPERATORS_IS,
options: [
{ icon: 'eye-slash', value: 'yes', title: __('Yes') },
{ icon: 'eye', value: 'no', title: __('No') },
@@ -186,7 +186,7 @@ export default {
},
methods: {
...mapActions(['fetchMilestones']),
- preloadedAuthors() {
+ preloadedUsers() {
return gon?.current_user_id
? [
{
diff --git a/app/assets/javascripts/boards/components/sidebar/board_sidebar_time_tracker.vue b/app/assets/javascripts/boards/components/sidebar/board_sidebar_time_tracker.vue
index a35b3f14be4..b70294c9db3 100644
--- a/app/assets/javascripts/boards/components/sidebar/board_sidebar_time_tracker.vue
+++ b/app/assets/javascripts/boards/components/sidebar/board_sidebar_time_tracker.vue
@@ -6,7 +6,7 @@ export default {
components: {
IssuableTimeTracker,
},
- inject: ['timeTrackingLimitToHours'],
+ inject: ['timeTrackingLimitToHours', 'canUpdate'],
computed: {
...mapGetters(['activeBoardItem']),
initialTimeTracking() {
@@ -34,5 +34,6 @@ export default {
:limit-to-hours="timeTrackingLimitToHours"
:initial-time-tracking="initialTimeTracking"
:show-collapsed="false"
+ :can-add-time-entries="canUpdate"
/>
</template>
diff --git a/app/assets/javascripts/boards/issue_board_filters.js b/app/assets/javascripts/boards/issue_board_filters.js
index 699d7e12de4..4bfd92fb748 100644
--- a/app/assets/javascripts/boards/issue_board_filters.js
+++ b/app/assets/javascripts/boards/issue_board_filters.js
@@ -14,13 +14,13 @@ export default function issueBoardFilters(apollo, fullPath, boardType) {
return isGroupBoard ? groupBoardMembers : projectBoardMembers;
};
- const fetchAuthors = (authorsSearchTerm) => {
+ const fetchUsers = (usersSearchTerm) => {
return apollo
.query({
query: boardAssigneesQuery(),
variables: {
fullPath,
- search: authorsSearchTerm,
+ search: usersSearchTerm,
},
})
.then(({ data }) => data.workspace?.assignees.nodes.map(({ user }) => user));
@@ -42,6 +42,6 @@ export default function issueBoardFilters(apollo, fullPath, boardType) {
return {
fetchLabels,
- fetchAuthors,
+ fetchUsers,
};
}
diff --git a/app/assets/javascripts/boards/stores/actions.js b/app/assets/javascripts/boards/stores/actions.js
index e5437690fd4..07b127d86e2 100644
--- a/app/assets/javascripts/boards/stores/actions.js
+++ b/app/assets/javascripts/boards/stores/actions.js
@@ -928,4 +928,5 @@ export default {
// EE action needs CE empty equivalent
setActiveItemWeight: () => {},
+ setActiveItemHealthStatus: () => {},
};
diff --git a/app/assets/javascripts/branches/components/sort_dropdown.vue b/app/assets/javascripts/branches/components/sort_dropdown.vue
index 5f782b5e652..263efcaa788 100644
--- a/app/assets/javascripts/branches/components/sort_dropdown.vue
+++ b/app/assets/javascripts/branches/components/sort_dropdown.vue
@@ -1,5 +1,5 @@
<script>
-import { GlDropdown, GlDropdownItem, GlSearchBoxByClick } from '@gitlab/ui';
+import { GlCollapsibleListbox, GlSearchBoxByClick } from '@gitlab/ui';
import { mergeUrlParams, visitUrl, getParameterValues } from '~/lib/utils/url_utility';
import { s__ } from '~/locale';
@@ -10,8 +10,7 @@ export default {
searchPlaceholder: s__('Branches|Filter by branch name'),
},
components: {
- GlDropdown,
- GlDropdownItem,
+ GlCollapsibleListbox,
GlSearchBoxByClick,
},
inject: ['projectBranchesFilteredPath', 'sortOptions', 'mode'],
@@ -28,6 +27,9 @@ export default {
selectedSortMethodName() {
return this.sortOptions[this.selectedKey];
},
+ listboxItems() {
+ return Object.entries(this.sortOptions).map(([value, text]) => ({ value, text }));
+ },
},
created() {
const sortValue = getParameterValues('sort');
@@ -42,9 +44,6 @@ export default {
}
},
methods: {
- isSortMethodSelected(sortKey) {
- return sortKey === this.selectedKey;
- },
visitUrlFromOption(sortKey) {
this.selectedKey = sortKey;
const urlParams = {};
@@ -70,20 +69,15 @@ export default {
data-testid="branch-search"
@submit="visitUrlFromOption(selectedKey)"
/>
- <gl-dropdown
+
+ <gl-collapsible-listbox
v-if="shouldShowDropdown"
- :text="selectedSortMethodName"
+ v-model="selectedKey"
+ :items="listboxItems"
+ :toggle-text="selectedSortMethodName"
class="gl-mr-3"
data-testid="branches-dropdown"
- >
- <gl-dropdown-item
- v-for="(value, key) in sortOptions"
- :key="key"
- :is-checked="isSortMethodSelected(key)"
- is-check-item
- @click="visitUrlFromOption(key)"
- >{{ value }}</gl-dropdown-item
- >
- </gl-dropdown>
+ @select="visitUrlFromOption(selectedKey)"
+ />
</div>
</template>
diff --git a/app/assets/javascripts/branches/init_new_branch_ref_selector.js b/app/assets/javascripts/branches/init_new_branch_ref_selector.js
new file mode 100644
index 00000000000..aad3fbb9982
--- /dev/null
+++ b/app/assets/javascripts/branches/init_new_branch_ref_selector.js
@@ -0,0 +1,25 @@
+import Vue from 'vue';
+import RefSelector from '~/ref/components/ref_selector.vue';
+
+export default function initNewBranchRefSelector() {
+ const el = document.querySelector('.js-new-branch-ref-selector');
+
+ if (!el) {
+ return false;
+ }
+
+ const { projectId, defaultBranchName, hiddenInputName } = el.dataset;
+
+ return new Vue({
+ el,
+ render(createComponent) {
+ return createComponent(RefSelector, {
+ props: {
+ value: defaultBranchName,
+ name: hiddenInputName,
+ projectId,
+ },
+ });
+ },
+ });
+}
diff --git a/app/assets/javascripts/ci_lint/components/ci_lint.vue b/app/assets/javascripts/ci/ci_lint/components/ci_lint.vue
index 8db4cba529f..49a314e067c 100644
--- a/app/assets/javascripts/ci_lint/components/ci_lint.vue
+++ b/app/assets/javascripts/ci/ci_lint/components/ci_lint.vue
@@ -1,7 +1,7 @@
<script>
import { GlButton, GlFormCheckbox, GlIcon, GlLink, GlAlert } from '@gitlab/ui';
-import CiLintResults from '~/pipeline_editor/components/lint/ci_lint_results.vue';
-import lintCiMutation from '~/pipeline_editor/graphql/mutations/client/lint_ci.mutation.graphql';
+import CiLintResults from '~/ci/pipeline_editor/components/lint/ci_lint_results.vue';
+import lintCiMutation from '~/ci/pipeline_editor/graphql/mutations/client/lint_ci.mutation.graphql';
import SourceEditor from '~/vue_shared/components/source_editor.vue';
export default {
diff --git a/app/assets/javascripts/ci_lint/index.js b/app/assets/javascripts/ci/ci_lint/index.js
index 274aab45deb..382059eb17e 100644
--- a/app/assets/javascripts/ci_lint/index.js
+++ b/app/assets/javascripts/ci/ci_lint/index.js
@@ -1,7 +1,7 @@
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import createDefaultClient from '~/lib/graphql';
-import { resolvers } from '~/pipeline_editor/graphql/resolvers';
+import { resolvers } from '~/ci/pipeline_editor/graphql/resolvers';
import CiLint from './components/ci_lint.vue';
diff --git a/app/assets/javascripts/pipeline_editor/components/code_snippet_alert/code_snippet_alert.vue b/app/assets/javascripts/ci/pipeline_editor/components/code_snippet_alert/code_snippet_alert.vue
index 7b33d98bca0..7b33d98bca0 100644
--- a/app/assets/javascripts/pipeline_editor/components/code_snippet_alert/code_snippet_alert.vue
+++ b/app/assets/javascripts/ci/pipeline_editor/components/code_snippet_alert/code_snippet_alert.vue
diff --git a/app/assets/javascripts/pipeline_editor/components/code_snippet_alert/constants.js b/app/assets/javascripts/ci/pipeline_editor/components/code_snippet_alert/constants.js
index e4fd423249b..e4fd423249b 100644
--- a/app/assets/javascripts/pipeline_editor/components/code_snippet_alert/constants.js
+++ b/app/assets/javascripts/ci/pipeline_editor/components/code_snippet_alert/constants.js
diff --git a/app/assets/javascripts/pipeline_editor/components/commit/commit_form.vue b/app/assets/javascripts/ci/pipeline_editor/components/commit/commit_form.vue
index 4775836fcc6..4775836fcc6 100644
--- a/app/assets/javascripts/pipeline_editor/components/commit/commit_form.vue
+++ b/app/assets/javascripts/ci/pipeline_editor/components/commit/commit_form.vue
diff --git a/app/assets/javascripts/pipeline_editor/components/commit/commit_section.vue b/app/assets/javascripts/ci/pipeline_editor/components/commit/commit_section.vue
index 9cbf60b1c8f..9cbf60b1c8f 100644
--- a/app/assets/javascripts/pipeline_editor/components/commit/commit_section.vue
+++ b/app/assets/javascripts/ci/pipeline_editor/components/commit/commit_section.vue
diff --git a/app/assets/javascripts/pipeline_editor/components/drawer/cards/first_pipeline_card.vue b/app/assets/javascripts/ci/pipeline_editor/components/drawer/cards/first_pipeline_card.vue
index 0b57433e894..0b57433e894 100644
--- a/app/assets/javascripts/pipeline_editor/components/drawer/cards/first_pipeline_card.vue
+++ b/app/assets/javascripts/ci/pipeline_editor/components/drawer/cards/first_pipeline_card.vue
diff --git a/app/assets/javascripts/pipeline_editor/components/drawer/cards/getting_started_card.vue b/app/assets/javascripts/ci/pipeline_editor/components/drawer/cards/getting_started_card.vue
index d2682cf6326..d2682cf6326 100644
--- a/app/assets/javascripts/pipeline_editor/components/drawer/cards/getting_started_card.vue
+++ b/app/assets/javascripts/ci/pipeline_editor/components/drawer/cards/getting_started_card.vue
diff --git a/app/assets/javascripts/pipeline_editor/components/drawer/cards/pipeline_config_reference_card.vue b/app/assets/javascripts/ci/pipeline_editor/components/drawer/cards/pipeline_config_reference_card.vue
index bc9203b9c5b..bc9203b9c5b 100644
--- a/app/assets/javascripts/pipeline_editor/components/drawer/cards/pipeline_config_reference_card.vue
+++ b/app/assets/javascripts/ci/pipeline_editor/components/drawer/cards/pipeline_config_reference_card.vue
diff --git a/app/assets/javascripts/pipeline_editor/components/drawer/cards/visualize_and_lint_card.vue b/app/assets/javascripts/ci/pipeline_editor/components/drawer/cards/visualize_and_lint_card.vue
index aeeb52319d2..aeeb52319d2 100644
--- a/app/assets/javascripts/pipeline_editor/components/drawer/cards/visualize_and_lint_card.vue
+++ b/app/assets/javascripts/ci/pipeline_editor/components/drawer/cards/visualize_and_lint_card.vue
diff --git a/app/assets/javascripts/pipeline_editor/components/drawer/pipeline_editor_drawer.vue b/app/assets/javascripts/ci/pipeline_editor/components/drawer/pipeline_editor_drawer.vue
index 375db7f3054..375db7f3054 100644
--- a/app/assets/javascripts/pipeline_editor/components/drawer/pipeline_editor_drawer.vue
+++ b/app/assets/javascripts/ci/pipeline_editor/components/drawer/pipeline_editor_drawer.vue
diff --git a/app/assets/javascripts/pipeline_editor/components/drawer/ui/demo_job_pill.vue b/app/assets/javascripts/ci/pipeline_editor/components/drawer/ui/demo_job_pill.vue
index 049504181c4..049504181c4 100644
--- a/app/assets/javascripts/pipeline_editor/components/drawer/ui/demo_job_pill.vue
+++ b/app/assets/javascripts/ci/pipeline_editor/components/drawer/ui/demo_job_pill.vue
diff --git a/app/assets/javascripts/pipeline_editor/components/editor/ci_config_merged_preview.vue b/app/assets/javascripts/ci/pipeline_editor/components/editor/ci_config_merged_preview.vue
index 42e2d34fa3a..42e2d34fa3a 100644
--- a/app/assets/javascripts/pipeline_editor/components/editor/ci_config_merged_preview.vue
+++ b/app/assets/javascripts/ci/pipeline_editor/components/editor/ci_config_merged_preview.vue
diff --git a/app/assets/javascripts/pipeline_editor/components/editor/ci_editor_header.vue b/app/assets/javascripts/ci/pipeline_editor/components/editor/ci_editor_header.vue
index 189690ce2c3..201fba837e2 100644
--- a/app/assets/javascripts/pipeline_editor/components/editor/ci_editor_header.vue
+++ b/app/assets/javascripts/ci/pipeline_editor/components/editor/ci_editor_header.vue
@@ -43,9 +43,7 @@ export default {
</script>
<template>
- <div
- class="gl-bg-gray-10 gl-display-flex gl-p-3 gl-gap-3 gl-border-solid gl-border-gray-100 gl-border-1"
- >
+ <div class="gl-display-flex gl-p-3 gl-gap-3 gl-border-solid gl-border-gray-100 gl-border-1">
<gl-button
:href="$options.TEMPLATE_REPOSITORY_URL"
size="small"
diff --git a/app/assets/javascripts/pipeline_editor/components/editor/text_editor.vue b/app/assets/javascripts/ci/pipeline_editor/components/editor/text_editor.vue
index 255e3cb31f1..255e3cb31f1 100644
--- a/app/assets/javascripts/pipeline_editor/components/editor/text_editor.vue
+++ b/app/assets/javascripts/ci/pipeline_editor/components/editor/text_editor.vue
diff --git a/app/assets/javascripts/pipeline_editor/components/file_nav/branch_switcher.vue b/app/assets/javascripts/ci/pipeline_editor/components/file_nav/branch_switcher.vue
index 1f8ddae3696..ef9acc1f8f1 100644
--- a/app/assets/javascripts/pipeline_editor/components/file_nav/branch_switcher.vue
+++ b/app/assets/javascripts/ci/pipeline_editor/components/file_nav/branch_switcher.vue
@@ -16,11 +16,11 @@ import {
BRANCH_PAGINATION_LIMIT,
BRANCH_SEARCH_DEBOUNCE,
DEFAULT_FAILURE,
-} from '~/pipeline_editor/constants';
-import updateCurrentBranchMutation from '~/pipeline_editor/graphql/mutations/client/update_current_branch.mutation.graphql';
-import getAvailableBranchesQuery from '~/pipeline_editor/graphql/queries/available_branches.query.graphql';
-import getCurrentBranch from '~/pipeline_editor/graphql/queries/client/current_branch.query.graphql';
-import getLastCommitBranch from '~/pipeline_editor/graphql/queries/client/last_commit_branch.query.graphql';
+} from '~/ci/pipeline_editor/constants';
+import updateCurrentBranchMutation from '~/ci/pipeline_editor/graphql/mutations/client/update_current_branch.mutation.graphql';
+import getAvailableBranchesQuery from '~/ci/pipeline_editor/graphql/queries/available_branches.query.graphql';
+import getCurrentBranch from '~/ci/pipeline_editor/graphql/queries/client/current_branch.query.graphql';
+import getLastCommitBranch from '~/ci/pipeline_editor/graphql/queries/client/last_commit_branch.query.graphql';
export default {
i18n: {
diff --git a/app/assets/javascripts/pipeline_editor/components/file_nav/pipeline_editor_file_nav.vue b/app/assets/javascripts/ci/pipeline_editor/components/file_nav/pipeline_editor_file_nav.vue
index 8e95fad1e48..84c29e48114 100644
--- a/app/assets/javascripts/pipeline_editor/components/file_nav/pipeline_editor_file_nav.vue
+++ b/app/assets/javascripts/ci/pipeline_editor/components/file_nav/pipeline_editor_file_nav.vue
@@ -1,6 +1,6 @@
<script>
import { GlButton } from '@gitlab/ui';
-import getAppStatus from '~/pipeline_editor/graphql/queries/client/app_status.query.graphql';
+import getAppStatus from '~/ci/pipeline_editor/graphql/queries/client/app_status.query.graphql';
import { EDITOR_APP_STATUS_EMPTY, EDITOR_APP_STATUS_LOADING } from '../../constants';
import FileTreePopover from '../popovers/file_tree_popover.vue';
import BranchSwitcher from './branch_switcher.vue';
diff --git a/app/assets/javascripts/pipeline_editor/components/file_tree/container.vue b/app/assets/javascripts/ci/pipeline_editor/components/file_tree/container.vue
index 280cd729a43..280cd729a43 100644
--- a/app/assets/javascripts/pipeline_editor/components/file_tree/container.vue
+++ b/app/assets/javascripts/ci/pipeline_editor/components/file_tree/container.vue
diff --git a/app/assets/javascripts/pipeline_editor/components/file_tree/file_item.vue b/app/assets/javascripts/ci/pipeline_editor/components/file_tree/file_item.vue
index 786d483b5b9..786d483b5b9 100644
--- a/app/assets/javascripts/pipeline_editor/components/file_tree/file_item.vue
+++ b/app/assets/javascripts/ci/pipeline_editor/components/file_tree/file_item.vue
diff --git a/app/assets/javascripts/pipeline_editor/components/header/pipeline_editor_header.vue b/app/assets/javascripts/ci/pipeline_editor/components/header/pipeline_editor_header.vue
index ec6ee52b6b2..ec6ee52b6b2 100644
--- a/app/assets/javascripts/pipeline_editor/components/header/pipeline_editor_header.vue
+++ b/app/assets/javascripts/ci/pipeline_editor/components/header/pipeline_editor_header.vue
diff --git a/app/assets/javascripts/pipeline_editor/components/header/pipeline_editor_mini_graph.vue b/app/assets/javascripts/ci/pipeline_editor/components/header/pipeline_editor_mini_graph.vue
index feadc60a22a..feadc60a22a 100644
--- a/app/assets/javascripts/pipeline_editor/components/header/pipeline_editor_mini_graph.vue
+++ b/app/assets/javascripts/ci/pipeline_editor/components/header/pipeline_editor_mini_graph.vue
diff --git a/app/assets/javascripts/pipeline_editor/components/header/pipeline_status.vue b/app/assets/javascripts/ci/pipeline_editor/components/header/pipeline_status.vue
index 137dfca68d6..372f04075ab 100644
--- a/app/assets/javascripts/pipeline_editor/components/header/pipeline_status.vue
+++ b/app/assets/javascripts/ci/pipeline_editor/components/header/pipeline_status.vue
@@ -3,8 +3,8 @@ import { GlButton, GlIcon, GlLink, GlLoadingIcon, GlSprintf, GlTooltipDirective
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { truncateSha } from '~/lib/utils/text_utility';
import { s__ } from '~/locale';
-import getPipelineQuery from '~/pipeline_editor/graphql/queries/pipeline.query.graphql';
-import getPipelineEtag from '~/pipeline_editor/graphql/queries/client/pipeline_etag.query.graphql';
+import getPipelineQuery from '~/ci/pipeline_editor/graphql/queries/pipeline.query.graphql';
+import getPipelineEtag from '~/ci/pipeline_editor/graphql/queries/client/pipeline_etag.query.graphql';
import {
getQueryHeaders,
toggleQueryPollingByVisibility,
diff --git a/app/assets/javascripts/pipeline_editor/components/header/validation_segment.vue b/app/assets/javascripts/ci/pipeline_editor/components/header/validation_segment.vue
index 610a570c4ce..84c0eef441f 100644
--- a/app/assets/javascripts/pipeline_editor/components/header/validation_segment.vue
+++ b/app/assets/javascripts/ci/pipeline_editor/components/header/validation_segment.vue
@@ -1,7 +1,7 @@
<script>
import { GlIcon, GlLink, GlLoadingIcon } from '@gitlab/ui';
import { __, s__, sprintf } from '~/locale';
-import getAppStatus from '~/pipeline_editor/graphql/queries/client/app_status.query.graphql';
+import getAppStatus from '~/ci/pipeline_editor/graphql/queries/client/app_status.query.graphql';
import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate/tooltip_on_truncate.vue';
import {
EDITOR_APP_STATUS_EMPTY,
diff --git a/app/assets/javascripts/pipeline_editor/components/lint/ci_lint_results.vue b/app/assets/javascripts/ci/pipeline_editor/components/lint/ci_lint_results.vue
index 0f19b9386e6..0f19b9386e6 100644
--- a/app/assets/javascripts/pipeline_editor/components/lint/ci_lint_results.vue
+++ b/app/assets/javascripts/ci/pipeline_editor/components/lint/ci_lint_results.vue
diff --git a/app/assets/javascripts/pipeline_editor/components/lint/ci_lint_results_param.vue b/app/assets/javascripts/ci/pipeline_editor/components/lint/ci_lint_results_param.vue
index 49225a7cac7..49225a7cac7 100644
--- a/app/assets/javascripts/pipeline_editor/components/lint/ci_lint_results_param.vue
+++ b/app/assets/javascripts/ci/pipeline_editor/components/lint/ci_lint_results_param.vue
diff --git a/app/assets/javascripts/pipeline_editor/components/lint/ci_lint_results_value.vue b/app/assets/javascripts/ci/pipeline_editor/components/lint/ci_lint_results_value.vue
index ef2be2a5fba..ef2be2a5fba 100644
--- a/app/assets/javascripts/pipeline_editor/components/lint/ci_lint_results_value.vue
+++ b/app/assets/javascripts/ci/pipeline_editor/components/lint/ci_lint_results_value.vue
diff --git a/app/assets/javascripts/pipeline_editor/components/lint/ci_lint_warnings.vue b/app/assets/javascripts/ci/pipeline_editor/components/lint/ci_lint_warnings.vue
index ac0332cb0bd..ac0332cb0bd 100644
--- a/app/assets/javascripts/pipeline_editor/components/lint/ci_lint_warnings.vue
+++ b/app/assets/javascripts/ci/pipeline_editor/components/lint/ci_lint_warnings.vue
diff --git a/app/assets/javascripts/pipeline_editor/components/pipeline_editor_tabs.vue b/app/assets/javascripts/ci/pipeline_editor/components/pipeline_editor_tabs.vue
index ed5466ff99c..ed5466ff99c 100644
--- a/app/assets/javascripts/pipeline_editor/components/pipeline_editor_tabs.vue
+++ b/app/assets/javascripts/ci/pipeline_editor/components/pipeline_editor_tabs.vue
diff --git a/app/assets/javascripts/pipeline_editor/components/popovers/file_tree_popover.vue b/app/assets/javascripts/ci/pipeline_editor/components/popovers/file_tree_popover.vue
index efa6a54c638..efa6a54c638 100644
--- a/app/assets/javascripts/pipeline_editor/components/popovers/file_tree_popover.vue
+++ b/app/assets/javascripts/ci/pipeline_editor/components/popovers/file_tree_popover.vue
diff --git a/app/assets/javascripts/pipeline_editor/components/popovers/validate_pipeline_popover.vue b/app/assets/javascripts/ci/pipeline_editor/components/popovers/validate_pipeline_popover.vue
index 4730a521227..4730a521227 100644
--- a/app/assets/javascripts/pipeline_editor/components/popovers/validate_pipeline_popover.vue
+++ b/app/assets/javascripts/ci/pipeline_editor/components/popovers/validate_pipeline_popover.vue
diff --git a/app/assets/javascripts/pipeline_editor/components/popovers/walkthrough_popover.vue b/app/assets/javascripts/ci/pipeline_editor/components/popovers/walkthrough_popover.vue
index c636d8b8e34..c636d8b8e34 100644
--- a/app/assets/javascripts/pipeline_editor/components/popovers/walkthrough_popover.vue
+++ b/app/assets/javascripts/ci/pipeline_editor/components/popovers/walkthrough_popover.vue
diff --git a/app/assets/javascripts/pipeline_editor/components/ui/confirm_unsaved_changes_dialog.vue b/app/assets/javascripts/ci/pipeline_editor/components/ui/confirm_unsaved_changes_dialog.vue
index bc076fbe349..bc076fbe349 100644
--- a/app/assets/javascripts/pipeline_editor/components/ui/confirm_unsaved_changes_dialog.vue
+++ b/app/assets/javascripts/ci/pipeline_editor/components/ui/confirm_unsaved_changes_dialog.vue
diff --git a/app/assets/javascripts/pipeline_editor/components/ui/editor_tab.vue b/app/assets/javascripts/ci/pipeline_editor/components/ui/editor_tab.vue
index 65f399d1912..22b82f2e96f 100644
--- a/app/assets/javascripts/pipeline_editor/components/ui/editor_tab.vue
+++ b/app/assets/javascripts/ci/pipeline_editor/components/ui/editor_tab.vue
@@ -41,7 +41,9 @@ import { __, s__ } from '~/locale';
export default {
i18n: {
- invalid: __('Your CI/CD configuration syntax is invalid. View Lint tab for more details.'),
+ invalid: __(
+ 'Your CI/CD configuration syntax is invalid. Select the Validate tab for more details.',
+ ),
unavailable: __(
"We're experiencing difficulties and this tab content is currently unavailable.",
),
diff --git a/app/assets/javascripts/pipeline_editor/components/ui/pipeline_editor_empty_state.vue b/app/assets/javascripts/ci/pipeline_editor/components/ui/pipeline_editor_empty_state.vue
index 7d2b9cd3d42..d7b8e7151d9 100644
--- a/app/assets/javascripts/pipeline_editor/components/ui/pipeline_editor_empty_state.vue
+++ b/app/assets/javascripts/ci/pipeline_editor/components/ui/pipeline_editor_empty_state.vue
@@ -1,7 +1,7 @@
<script>
import { GlButton, GlSprintf } from '@gitlab/ui';
import { __ } from '~/locale';
-import PipelineEditorFileNav from '~/pipeline_editor/components/file_nav/pipeline_editor_file_nav.vue';
+import PipelineEditorFileNav from '~/ci/pipeline_editor/components/file_nav/pipeline_editor_file_nav.vue';
export default {
components: {
diff --git a/app/assets/javascripts/pipeline_editor/components/ui/pipeline_editor_messages.vue b/app/assets/javascripts/ci/pipeline_editor/components/ui/pipeline_editor_messages.vue
index c72cff4c6f8..c72cff4c6f8 100644
--- a/app/assets/javascripts/pipeline_editor/components/ui/pipeline_editor_messages.vue
+++ b/app/assets/javascripts/ci/pipeline_editor/components/ui/pipeline_editor_messages.vue
diff --git a/app/assets/javascripts/pipeline_editor/components/validate/ci_validate.vue b/app/assets/javascripts/ci/pipeline_editor/components/validate/ci_validate.vue
index 83fcab4b343..83fcab4b343 100644
--- a/app/assets/javascripts/pipeline_editor/components/validate/ci_validate.vue
+++ b/app/assets/javascripts/ci/pipeline_editor/components/validate/ci_validate.vue
diff --git a/app/assets/javascripts/pipeline_editor/constants.js b/app/assets/javascripts/ci/pipeline_editor/constants.js
index dd25c4d433b..dd25c4d433b 100644
--- a/app/assets/javascripts/pipeline_editor/constants.js
+++ b/app/assets/javascripts/ci/pipeline_editor/constants.js
diff --git a/app/assets/javascripts/pipeline_editor/graphql/mutations/client/lint_ci.mutation.graphql b/app/assets/javascripts/ci/pipeline_editor/graphql/mutations/client/lint_ci.mutation.graphql
index 2d42ebb6ac3..2d42ebb6ac3 100644
--- a/app/assets/javascripts/pipeline_editor/graphql/mutations/client/lint_ci.mutation.graphql
+++ b/app/assets/javascripts/ci/pipeline_editor/graphql/mutations/client/lint_ci.mutation.graphql
diff --git a/app/assets/javascripts/pipeline_editor/graphql/mutations/client/update_app_status.mutation.graphql b/app/assets/javascripts/ci/pipeline_editor/graphql/mutations/client/update_app_status.mutation.graphql
index 7487e328668..7487e328668 100644
--- a/app/assets/javascripts/pipeline_editor/graphql/mutations/client/update_app_status.mutation.graphql
+++ b/app/assets/javascripts/ci/pipeline_editor/graphql/mutations/client/update_app_status.mutation.graphql
diff --git a/app/assets/javascripts/pipeline_editor/graphql/mutations/client/update_current_branch.mutation.graphql b/app/assets/javascripts/ci/pipeline_editor/graphql/mutations/client/update_current_branch.mutation.graphql
index b722c147f5f..b722c147f5f 100644
--- a/app/assets/javascripts/pipeline_editor/graphql/mutations/client/update_current_branch.mutation.graphql
+++ b/app/assets/javascripts/ci/pipeline_editor/graphql/mutations/client/update_current_branch.mutation.graphql
diff --git a/app/assets/javascripts/pipeline_editor/graphql/mutations/client/update_last_commit_branch.mutation.graphql b/app/assets/javascripts/ci/pipeline_editor/graphql/mutations/client/update_last_commit_branch.mutation.graphql
index 9561312f2b6..9561312f2b6 100644
--- a/app/assets/javascripts/pipeline_editor/graphql/mutations/client/update_last_commit_branch.mutation.graphql
+++ b/app/assets/javascripts/ci/pipeline_editor/graphql/mutations/client/update_last_commit_branch.mutation.graphql
diff --git a/app/assets/javascripts/pipeline_editor/graphql/mutations/client/update_pipeline_etag.mutation.graphql b/app/assets/javascripts/ci/pipeline_editor/graphql/mutations/client/update_pipeline_etag.mutation.graphql
index 9025f00b343..9025f00b343 100644
--- a/app/assets/javascripts/pipeline_editor/graphql/mutations/client/update_pipeline_etag.mutation.graphql
+++ b/app/assets/javascripts/ci/pipeline_editor/graphql/mutations/client/update_pipeline_etag.mutation.graphql
diff --git a/app/assets/javascripts/pipeline_editor/graphql/mutations/commit_ci_file.mutation.graphql b/app/assets/javascripts/ci/pipeline_editor/graphql/mutations/commit_ci_file.mutation.graphql
index 3495ca51283..3495ca51283 100644
--- a/app/assets/javascripts/pipeline_editor/graphql/mutations/commit_ci_file.mutation.graphql
+++ b/app/assets/javascripts/ci/pipeline_editor/graphql/mutations/commit_ci_file.mutation.graphql
diff --git a/app/assets/javascripts/pipeline_editor/graphql/queries/available_branches.query.graphql b/app/assets/javascripts/ci/pipeline_editor/graphql/queries/available_branches.query.graphql
index 359b4a846c7..359b4a846c7 100644
--- a/app/assets/javascripts/pipeline_editor/graphql/queries/available_branches.query.graphql
+++ b/app/assets/javascripts/ci/pipeline_editor/graphql/queries/available_branches.query.graphql
diff --git a/app/assets/javascripts/pipeline_editor/graphql/queries/blob_content.query.graphql b/app/assets/javascripts/ci/pipeline_editor/graphql/queries/blob_content.query.graphql
index 5928d90f7c4..5928d90f7c4 100644
--- a/app/assets/javascripts/pipeline_editor/graphql/queries/blob_content.query.graphql
+++ b/app/assets/javascripts/ci/pipeline_editor/graphql/queries/blob_content.query.graphql
diff --git a/app/assets/javascripts/pipeline_editor/graphql/queries/ci_config.query.graphql b/app/assets/javascripts/ci/pipeline_editor/graphql/queries/ci_config.query.graphql
index 5354ed7c2d5..5354ed7c2d5 100644
--- a/app/assets/javascripts/pipeline_editor/graphql/queries/ci_config.query.graphql
+++ b/app/assets/javascripts/ci/pipeline_editor/graphql/queries/ci_config.query.graphql
diff --git a/app/assets/javascripts/pipeline_editor/graphql/queries/client/app_status.query.graphql b/app/assets/javascripts/ci/pipeline_editor/graphql/queries/client/app_status.query.graphql
index 0df8cafa3cb..0df8cafa3cb 100644
--- a/app/assets/javascripts/pipeline_editor/graphql/queries/client/app_status.query.graphql
+++ b/app/assets/javascripts/ci/pipeline_editor/graphql/queries/client/app_status.query.graphql
diff --git a/app/assets/javascripts/pipeline_editor/graphql/queries/client/current_branch.query.graphql b/app/assets/javascripts/ci/pipeline_editor/graphql/queries/client/current_branch.query.graphql
index 1f4f9d26f24..1f4f9d26f24 100644
--- a/app/assets/javascripts/pipeline_editor/graphql/queries/client/current_branch.query.graphql
+++ b/app/assets/javascripts/ci/pipeline_editor/graphql/queries/client/current_branch.query.graphql
diff --git a/app/assets/javascripts/pipeline_editor/graphql/queries/client/last_commit_branch.query.graphql b/app/assets/javascripts/ci/pipeline_editor/graphql/queries/client/last_commit_branch.query.graphql
index a83129759de..a83129759de 100644
--- a/app/assets/javascripts/pipeline_editor/graphql/queries/client/last_commit_branch.query.graphql
+++ b/app/assets/javascripts/ci/pipeline_editor/graphql/queries/client/last_commit_branch.query.graphql
diff --git a/app/assets/javascripts/pipeline_editor/graphql/queries/client/pipeline_etag.query.graphql b/app/assets/javascripts/ci/pipeline_editor/graphql/queries/client/pipeline_etag.query.graphql
index 8df6e74a5d9..8df6e74a5d9 100644
--- a/app/assets/javascripts/pipeline_editor/graphql/queries/client/pipeline_etag.query.graphql
+++ b/app/assets/javascripts/ci/pipeline_editor/graphql/queries/client/pipeline_etag.query.graphql
diff --git a/app/assets/javascripts/pipeline_editor/graphql/queries/get_starter_template.query.graphql b/app/assets/javascripts/ci/pipeline_editor/graphql/queries/get_starter_template.query.graphql
index a34c8f365f4..a34c8f365f4 100644
--- a/app/assets/javascripts/pipeline_editor/graphql/queries/get_starter_template.query.graphql
+++ b/app/assets/javascripts/ci/pipeline_editor/graphql/queries/get_starter_template.query.graphql
diff --git a/app/assets/javascripts/pipeline_editor/graphql/queries/latest_commit_sha.query.graphql b/app/assets/javascripts/ci/pipeline_editor/graphql/queries/latest_commit_sha.query.graphql
index d62fda40237..d62fda40237 100644
--- a/app/assets/javascripts/pipeline_editor/graphql/queries/latest_commit_sha.query.graphql
+++ b/app/assets/javascripts/ci/pipeline_editor/graphql/queries/latest_commit_sha.query.graphql
diff --git a/app/assets/javascripts/pipeline_editor/graphql/queries/pipeline.query.graphql b/app/assets/javascripts/ci/pipeline_editor/graphql/queries/pipeline.query.graphql
index 021b858d72e..021b858d72e 100644
--- a/app/assets/javascripts/pipeline_editor/graphql/queries/pipeline.query.graphql
+++ b/app/assets/javascripts/ci/pipeline_editor/graphql/queries/pipeline.query.graphql
diff --git a/app/assets/javascripts/pipeline_editor/graphql/resolvers.js b/app/assets/javascripts/ci/pipeline_editor/graphql/resolvers.js
index fa1c70c1994..fa1c70c1994 100644
--- a/app/assets/javascripts/pipeline_editor/graphql/resolvers.js
+++ b/app/assets/javascripts/ci/pipeline_editor/graphql/resolvers.js
diff --git a/app/assets/javascripts/pipeline_editor/graphql/typedefs.graphql b/app/assets/javascripts/ci/pipeline_editor/graphql/typedefs.graphql
index 508ff22c46e..508ff22c46e 100644
--- a/app/assets/javascripts/pipeline_editor/graphql/typedefs.graphql
+++ b/app/assets/javascripts/ci/pipeline_editor/graphql/typedefs.graphql
diff --git a/app/assets/javascripts/pipeline_editor/index.js b/app/assets/javascripts/ci/pipeline_editor/index.js
index 6d91c339833..6d91c339833 100644
--- a/app/assets/javascripts/pipeline_editor/index.js
+++ b/app/assets/javascripts/ci/pipeline_editor/index.js
diff --git a/app/assets/javascripts/pipeline_editor/pipeline_editor_app.vue b/app/assets/javascripts/ci/pipeline_editor/pipeline_editor_app.vue
index ff848a973e3..ff848a973e3 100644
--- a/app/assets/javascripts/pipeline_editor/pipeline_editor_app.vue
+++ b/app/assets/javascripts/ci/pipeline_editor/pipeline_editor_app.vue
diff --git a/app/assets/javascripts/pipeline_editor/pipeline_editor_home.vue b/app/assets/javascripts/ci/pipeline_editor/pipeline_editor_home.vue
index 1972125ed56..1972125ed56 100644
--- a/app/assets/javascripts/pipeline_editor/pipeline_editor_home.vue
+++ b/app/assets/javascripts/ci/pipeline_editor/pipeline_editor_home.vue
diff --git a/app/assets/javascripts/ci/pipeline_schedules/components/pipeline_schedules_form.vue b/app/assets/javascripts/ci/pipeline_schedules/components/pipeline_schedules_form.vue
index 6e24ac6b8d4..a4ef7827f73 100644
--- a/app/assets/javascripts/ci/pipeline_schedules/components/pipeline_schedules_form.vue
+++ b/app/assets/javascripts/ci/pipeline_schedules/components/pipeline_schedules_form.vue
@@ -1,18 +1,321 @@
<script>
-import { GlForm } from '@gitlab/ui';
+import {
+ GlButton,
+ GlDropdown,
+ GlDropdownItem,
+ GlFormCheckbox,
+ GlForm,
+ GlFormGroup,
+ GlFormInput,
+ GlFormTextarea,
+ GlLink,
+ GlSprintf,
+} from '@gitlab/ui';
+import { uniqueId } from 'lodash';
+import Vue from 'vue';
+import { __, s__ } from '~/locale';
+import { REF_TYPE_BRANCHES, REF_TYPE_TAGS } from '~/ref/constants';
+import RefSelector from '~/ref/components/ref_selector.vue';
+import TimezoneDropdown from '~/vue_shared/components/timezone_dropdown/timezone_dropdown.vue';
+import IntervalPatternInput from '~/pages/projects/pipeline_schedules/shared/components/interval_pattern_input.vue';
+import { VARIABLE_TYPE, FILE_TYPE } from '../constants';
export default {
components: {
+ GlButton,
+ GlDropdown,
+ GlDropdownItem,
GlForm,
+ GlFormCheckbox,
+ GlFormGroup,
+ GlFormInput,
+ GlFormTextarea,
+ GlLink,
+ GlSprintf,
+ RefSelector,
+ TimezoneDropdown,
+ IntervalPatternInput,
},
- inject: {
- fullPath: {
+ inject: [
+ 'fullPath',
+ 'projectId',
+ 'defaultBranch',
+ 'cron',
+ 'cronTimezone',
+ 'dailyLimit',
+ 'settingsLink',
+ ],
+ props: {
+ timezoneData: {
+ type: Array,
+ required: true,
+ },
+ refParam: {
+ type: String,
+ required: false,
default: '',
},
},
+ data() {
+ return {
+ refValue: {
+ shortName: this.refParam,
+ // this is needed until we add support for ref type in url query strings
+ // ensure default branch is called with full ref on load
+ // https://gitlab.com/gitlab-org/gitlab/-/issues/287815
+ fullName: this.refParam === this.defaultBranch ? `refs/heads/${this.refParam}` : undefined,
+ },
+ description: '',
+ scheduleRef: this.defaultBranch,
+ activated: true,
+ timezone: this.cronTimezone,
+ formCiVariables: {},
+ // TODO: Add the GraphQL query to help populate the predefined variables
+ // app/assets/javascripts/pipeline_new/components/pipeline_new_form.vue#131
+ predefinedValueOptions: {},
+ };
+ },
+ i18n: {
+ activated: __('Activated'),
+ cronTimezone: s__('PipelineSchedules|Cron timezone'),
+ description: s__('PipelineSchedules|Description'),
+ shortDescriptionPipeline: s__(
+ 'PipelineSchedules|Provide a short description for this pipeline',
+ ),
+ savePipelineSchedule: s__('PipelineSchedules|Save pipeline schedule'),
+ cancel: __('Cancel'),
+ targetBranchTag: __('Select target branch or tag'),
+ intervalPattern: s__('PipelineSchedules|Interval Pattern'),
+ variablesDescription: s__(
+ 'Pipeline|Specify variable values to be used in this run. The values specified in %{linkStart}CI/CD settings%{linkEnd} will be used by default.',
+ ),
+ removeVariableLabel: s__('CiVariables|Remove variable'),
+ variables: s__('Pipeline|Variables'),
+ },
+ typeOptions: {
+ [VARIABLE_TYPE]: __('Variable'),
+ [FILE_TYPE]: __('File'),
+ },
+ formElementClasses: 'gl-md-mr-3 gl-mb-3 gl-flex-basis-quarter gl-flex-shrink-0 gl-flex-grow-0',
+ computed: {
+ dropdownTranslations() {
+ return {
+ dropdownHeader: this.$options.i18n.targetBranchTag,
+ };
+ },
+ refFullName() {
+ return this.refValue.fullName;
+ },
+ variables() {
+ return this.formCiVariables[this.refFullName]?.variables ?? [];
+ },
+ descriptions() {
+ return this.formCiVariables[this.refFullName]?.descriptions ?? {};
+ },
+ typeOptionsListbox() {
+ return [
+ {
+ text: __('Variable'),
+ value: VARIABLE_TYPE,
+ },
+ {
+ text: __('File'),
+ value: FILE_TYPE,
+ },
+ ];
+ },
+ getEnabledRefTypes() {
+ return [REF_TYPE_BRANCHES, REF_TYPE_TAGS];
+ },
+ },
+ created() {
+ Vue.set(this.formCiVariables, this.refFullName, {
+ variables: [],
+ descriptions: {},
+ });
+
+ this.addEmptyVariable(this.refFullName);
+ },
+ methods: {
+ addEmptyVariable(refValue) {
+ const { variables } = this.formCiVariables[refValue];
+
+ const lastVar = variables[variables.length - 1];
+ if (lastVar?.key === '' && lastVar?.value === '') {
+ return;
+ }
+
+ variables.push({
+ uniqueId: uniqueId(`var-${refValue}`),
+ variable_type: VARIABLE_TYPE,
+ key: '',
+ value: '',
+ });
+ },
+ setVariableAttribute(key, attribute, value) {
+ const { variables } = this.formCiVariables[this.refFullName];
+ const variable = variables.find((v) => v.key === key);
+ variable[attribute] = value;
+ },
+ shouldShowValuesDropdown(key) {
+ return this.predefinedValueOptions[key]?.length > 1;
+ },
+ removeVariable(index) {
+ this.variables.splice(index, 1);
+ },
+ canRemove(index) {
+ return index < this.variables.length - 1;
+ },
+ },
};
</script>
<template>
- <gl-form />
+ <div class="col-lg-8">
+ <gl-form>
+ <!--Description-->
+ <gl-form-group :label="$options.i18n.description" label-for="schedule-description">
+ <gl-form-input
+ id="schedule-description"
+ v-model="description"
+ type="text"
+ :placeholder="$options.i18n.shortDescriptionPipeline"
+ data-testid="schedule-description"
+ />
+ </gl-form-group>
+ <!--Interval Pattern-->
+ <gl-form-group :label="$options.i18n.intervalPattern" label-for="schedule-interval">
+ <interval-pattern-input
+ id="schedule-interval"
+ :initial-cron-interval="cron"
+ :daily-limit="dailyLimit"
+ :send-native-errors="false"
+ />
+ </gl-form-group>
+ <!--Timezone-->
+ <gl-form-group :label="$options.i18n.cronTimezone" label-for="schedule-timezone">
+ <timezone-dropdown
+ id="schedule-timezone"
+ :value="timezone"
+ :timezone-data="timezoneData"
+ name="schedule-timezone"
+ />
+ </gl-form-group>
+ <!--Branch/Tag Selector-->
+ <gl-form-group :label="$options.i18n.targetBranchTag" label-for="schedule-target-branch-tag">
+ <ref-selector
+ id="schedule-target-branch-tag"
+ :enabled-ref-types="getEnabledRefTypes"
+ :project-id="projectId"
+ :value="scheduleRef"
+ :use-symbolic-ref-names="true"
+ :translations="dropdownTranslations"
+ class="gl-w-full"
+ />
+ </gl-form-group>
+ <!--Variable List-->
+ <gl-form-group :label="$options.i18n.variables">
+ <div
+ v-for="(variable, index) in variables"
+ :key="variable.uniqueId"
+ class="gl-mb-3 gl-pb-2"
+ data-testid="ci-variable-row"
+ data-qa-selector="ci_variable_row_container"
+ >
+ <div
+ class="gl-display-flex gl-align-items-stretch gl-flex-direction-column gl-md-flex-direction-row"
+ >
+ <gl-dropdown
+ :text="$options.typeOptions[variable.variable_type]"
+ :class="$options.formElementClasses"
+ data-testid="pipeline-form-ci-variable-type"
+ >
+ <gl-dropdown-item
+ v-for="type in Object.keys($options.typeOptions)"
+ :key="type"
+ @click="setVariableAttribute(variable.key, 'variable_type', type)"
+ >
+ {{ $options.typeOptions[type] }}
+ </gl-dropdown-item>
+ </gl-dropdown>
+ <gl-form-input
+ v-model="variable.key"
+ :placeholder="s__('CiVariables|Input variable key')"
+ :class="$options.formElementClasses"
+ data-testid="pipeline-form-ci-variable-key"
+ data-qa-selector="ci_variable_key_field"
+ @change="addEmptyVariable(refFullName)"
+ />
+ <gl-dropdown
+ v-if="shouldShowValuesDropdown(variable.key)"
+ :text="variable.value"
+ :class="$options.formElementClasses"
+ class="gl-flex-grow-1 gl-mr-0!"
+ data-testid="pipeline-form-ci-variable-value-dropdown"
+ >
+ <gl-dropdown-item
+ v-for="value in predefinedValueOptions[variable.key]"
+ :key="value"
+ data-testid="pipeline-form-ci-variable-value-dropdown-items"
+ @click="setVariableAttribute(variable.key, 'value', value)"
+ >
+ {{ value }}
+ </gl-dropdown-item>
+ </gl-dropdown>
+ <gl-form-textarea
+ v-else
+ v-model="variable.value"
+ :placeholder="s__('CiVariables|Input variable value')"
+ class="gl-mb-3 gl-h-7!"
+ :style="$options.textAreaStyle"
+ :no-resize="false"
+ data-testid="pipeline-form-ci-variable-value"
+ data-qa-selector="ci_variable_value_field"
+ />
+
+ <template v-if="variables.length > 1">
+ <gl-button
+ v-if="canRemove(index)"
+ class="gl-md-ml-3 gl-mb-3"
+ data-testid="remove-ci-variable-row"
+ variant="danger"
+ category="secondary"
+ icon="clear"
+ :aria-label="$options.i18n.removeVariableLabel"
+ @click="removeVariable(index)"
+ />
+ <gl-button
+ v-else
+ class="gl-md-ml-3 gl-mb-3 gl-display-none gl-md-display-block gl-visibility-hidden"
+ icon="clear"
+ :aria-label="$options.i18n.removeVariableLabel"
+ />
+ </template>
+ </div>
+ <div v-if="descriptions[variable.key]" class="gl-text-gray-500 gl-mb-3">
+ {{ descriptions[variable.key] }}
+ </div>
+ </div>
+
+ <template #description
+ ><gl-sprintf :message="$options.i18n.variablesDescription">
+ <template #link="{ content }">
+ <gl-link :href="settingsLink">{{ content }}</gl-link>
+ </template>
+ </gl-sprintf></template
+ >
+ </gl-form-group>
+ <!--Activated-->
+ <gl-form-checkbox id="schedule-active" v-model="activated" class="gl-mb-3">{{
+ $options.i18n.activated
+ }}</gl-form-checkbox>
+
+ <gl-button type="submit" variant="confirm" data-testid="schedule-submit-button">{{
+ $options.i18n.savePipelineSchedule
+ }}</gl-button>
+ <gl-button type="reset" data-testid="schedule-cancel-button">{{
+ $options.i18n.cancel
+ }}</gl-button>
+ </gl-form>
+ </div>
</template>
diff --git a/app/assets/javascripts/ci/pipeline_schedules/constants.js b/app/assets/javascripts/ci/pipeline_schedules/constants.js
new file mode 100644
index 00000000000..b4ab1143f60
--- /dev/null
+++ b/app/assets/javascripts/ci/pipeline_schedules/constants.js
@@ -0,0 +1,2 @@
+export const VARIABLE_TYPE = 'env_var';
+export const FILE_TYPE = 'file';
diff --git a/app/assets/javascripts/ci/pipeline_schedules/mount_pipeline_schedules_form_app.js b/app/assets/javascripts/ci/pipeline_schedules/mount_pipeline_schedules_form_app.js
index d83417ab84a..445161f99cb 100644
--- a/app/assets/javascripts/ci/pipeline_schedules/mount_pipeline_schedules_form_app.js
+++ b/app/assets/javascripts/ci/pipeline_schedules/mount_pipeline_schedules_form_app.js
@@ -16,7 +16,16 @@ export default (selector) => {
return false;
}
- const { fullPath } = containerEl.dataset;
+ const {
+ fullPath,
+ cron,
+ dailyLimit,
+ timezoneData,
+ cronTimezone,
+ projectId,
+ defaultBranch,
+ settingsLink,
+ } = containerEl.dataset;
return new Vue({
el: containerEl,
@@ -24,9 +33,20 @@ export default (selector) => {
apolloProvider,
provide: {
fullPath,
+ projectId,
+ defaultBranch,
+ dailyLimit: dailyLimit ?? '',
+ cronTimezone: cronTimezone ?? '',
+ cron: cron ?? '',
+ settingsLink,
},
render(createElement) {
- return createElement(PipelineSchedulesForm);
+ return createElement(PipelineSchedulesForm, {
+ props: {
+ timezoneData: JSON.parse(timezoneData),
+ refParam: defaultBranch,
+ },
+ });
},
});
};
diff --git a/app/assets/javascripts/reports/codequality_report/components/codequality_issue_body.vue b/app/assets/javascripts/ci/reports/codequality_report/components/codequality_issue_body.vue
index fb2ef850e4f..5a7ee9c9b28 100644
--- a/app/assets/javascripts/reports/codequality_report/components/codequality_issue_body.vue
+++ b/app/assets/javascripts/ci/reports/codequality_report/components/codequality_issue_body.vue
@@ -5,8 +5,8 @@
*/
import { GlIcon, GlTooltipDirective } from '@gitlab/ui';
import { s__ } from '~/locale';
-import ReportLink from '~/reports/components/report_link.vue';
-import { STATUS_SUCCESS, STATUS_NEUTRAL } from '~/reports/constants';
+import ReportLink from '~/ci/reports/components/report_link.vue';
+import { STATUS_SUCCESS, STATUS_NEUTRAL } from '~/ci/reports/constants';
import { SEVERITY_CLASSES, SEVERITY_ICONS } from '../constants';
export default {
diff --git a/app/assets/javascripts/reports/codequality_report/constants.js b/app/assets/javascripts/ci/reports/codequality_report/constants.js
index 0c472b24471..5e81245037f 100644
--- a/app/assets/javascripts/reports/codequality_report/constants.js
+++ b/app/assets/javascripts/ci/reports/codequality_report/constants.js
@@ -16,12 +16,7 @@ export const SEVERITY_ICONS = {
unknown: 'severity-unknown',
};
-// This is the icons mapping for the code Quality Merge-Request Widget Extension
-// once the refactor_mr_widgets_extensions flag is activated the above SEVERITY_ICONS
-// need be removed and this variable needs to be rename to SEVERITY_ICONS
-// Rollout Issue: https://gitlab.com/gitlab-org/gitlab/-/issues/341759
-
-export const SEVERITY_ICONS_EXTENSION = {
+export const SEVERITY_ICONS_MR_WIDGET = {
info: 'severityInfo',
minor: 'severityLow',
major: 'severityMedium',
@@ -29,3 +24,30 @@ export const SEVERITY_ICONS_EXTENSION = {
blocker: 'severityCritical',
unknown: 'severityUnknown',
};
+
+export const SEVERITIES = {
+ info: {
+ class: SEVERITY_CLASSES.info,
+ name: SEVERITY_ICONS.info,
+ },
+ minor: {
+ class: SEVERITY_CLASSES.minor,
+ name: SEVERITY_ICONS.minor,
+ },
+ major: {
+ class: SEVERITY_CLASSES.major,
+ name: SEVERITY_ICONS.major,
+ },
+ critical: {
+ class: SEVERITY_CLASSES.critical,
+ name: SEVERITY_ICONS.critical,
+ },
+ blocker: {
+ class: SEVERITY_CLASSES.blocker,
+ name: SEVERITY_ICONS.blocker,
+ },
+ unknown: {
+ class: SEVERITY_CLASSES.unknown,
+ name: SEVERITY_ICONS.unknown,
+ },
+};
diff --git a/app/assets/javascripts/reports/codequality_report/store/actions.js b/app/assets/javascripts/ci/reports/codequality_report/store/actions.js
index 04aca11b945..04aca11b945 100644
--- a/app/assets/javascripts/reports/codequality_report/store/actions.js
+++ b/app/assets/javascripts/ci/reports/codequality_report/store/actions.js
diff --git a/app/assets/javascripts/reports/codequality_report/store/getters.js b/app/assets/javascripts/ci/reports/codequality_report/store/getters.js
index 70d11e96a54..70d11e96a54 100644
--- a/app/assets/javascripts/reports/codequality_report/store/getters.js
+++ b/app/assets/javascripts/ci/reports/codequality_report/store/getters.js
diff --git a/app/assets/javascripts/reports/codequality_report/store/index.js b/app/assets/javascripts/ci/reports/codequality_report/store/index.js
index 5bfcd69edec..5bfcd69edec 100644
--- a/app/assets/javascripts/reports/codequality_report/store/index.js
+++ b/app/assets/javascripts/ci/reports/codequality_report/store/index.js
diff --git a/app/assets/javascripts/reports/codequality_report/store/mutation_types.js b/app/assets/javascripts/ci/reports/codequality_report/store/mutation_types.js
index c362c973ae1..c362c973ae1 100644
--- a/app/assets/javascripts/reports/codequality_report/store/mutation_types.js
+++ b/app/assets/javascripts/ci/reports/codequality_report/store/mutation_types.js
diff --git a/app/assets/javascripts/reports/codequality_report/store/mutations.js b/app/assets/javascripts/ci/reports/codequality_report/store/mutations.js
index 249c2f35c0b..249c2f35c0b 100644
--- a/app/assets/javascripts/reports/codequality_report/store/mutations.js
+++ b/app/assets/javascripts/ci/reports/codequality_report/store/mutations.js
diff --git a/app/assets/javascripts/reports/codequality_report/store/state.js b/app/assets/javascripts/ci/reports/codequality_report/store/state.js
index f68dbc2a5fa..f68dbc2a5fa 100644
--- a/app/assets/javascripts/reports/codequality_report/store/state.js
+++ b/app/assets/javascripts/ci/reports/codequality_report/store/state.js
diff --git a/app/assets/javascripts/reports/codequality_report/store/utils/codequality_parser.js b/app/assets/javascripts/ci/reports/codequality_report/store/utils/codequality_parser.js
index 417297df43c..417297df43c 100644
--- a/app/assets/javascripts/reports/codequality_report/store/utils/codequality_parser.js
+++ b/app/assets/javascripts/ci/reports/codequality_report/store/utils/codequality_parser.js
diff --git a/app/assets/javascripts/reports/components/grouped_issues_list.vue b/app/assets/javascripts/ci/reports/components/grouped_issues_list.vue
index ca369022938..b21a486e259 100644
--- a/app/assets/javascripts/reports/components/grouped_issues_list.vue
+++ b/app/assets/javascripts/ci/reports/components/grouped_issues_list.vue
@@ -1,6 +1,6 @@
<script>
import { s__ } from '~/locale';
-import ReportItem from '~/reports/components/report_item.vue';
+import ReportItem from '~/ci/reports/components/report_item.vue';
import SmartVirtualList from '~/vue_shared/components/smart_virtual_list.vue';
export default {
diff --git a/app/assets/javascripts/reports/components/issue_body.js b/app/assets/javascripts/ci/reports/components/issue_body.js
index 4f418216024..daff1be30ff 100644
--- a/app/assets/javascripts/reports/components/issue_body.js
+++ b/app/assets/javascripts/ci/reports/components/issue_body.js
@@ -1,4 +1,4 @@
-import IssueStatusIcon from '~/reports/components/issue_status_icon.vue';
+import IssueStatusIcon from '~/ci/reports/components/issue_status_icon.vue';
export const components = {
CodequalityIssueBody: () => import('../codequality_report/components/codequality_issue_body.vue'),
diff --git a/app/assets/javascripts/reports/components/issue_status_icon.vue b/app/assets/javascripts/ci/reports/components/issue_status_icon.vue
index bd41b8d23f1..bd41b8d23f1 100644
--- a/app/assets/javascripts/reports/components/issue_status_icon.vue
+++ b/app/assets/javascripts/ci/reports/components/issue_status_icon.vue
diff --git a/app/assets/javascripts/reports/components/issues_list.vue b/app/assets/javascripts/ci/reports/components/issues_list.vue
index 9df0a1953b6..ababd4b5e49 100644
--- a/app/assets/javascripts/reports/components/issues_list.vue
+++ b/app/assets/javascripts/ci/reports/components/issues_list.vue
@@ -1,6 +1,6 @@
<script>
-import ReportItem from '~/reports/components/report_item.vue';
-import { STATUS_FAILED, STATUS_NEUTRAL, STATUS_SUCCESS } from '~/reports/constants';
+import ReportItem from '~/ci/reports/components/report_item.vue';
+import { STATUS_FAILED, STATUS_NEUTRAL, STATUS_SUCCESS } from '~/ci/reports/constants';
import SmartVirtualList from '~/vue_shared/components/smart_virtual_list.vue';
const wrapIssueWithState = (status, isNew = false) => (issue) => ({
diff --git a/app/assets/javascripts/reports/components/report_item.vue b/app/assets/javascripts/ci/reports/components/report_item.vue
index 918263bfb5c..97d4ac7bf6f 100644
--- a/app/assets/javascripts/reports/components/report_item.vue
+++ b/app/assets/javascripts/ci/reports/components/report_item.vue
@@ -4,7 +4,7 @@ import {
componentNames,
iconComponents,
iconComponentNames,
-} from 'ee_else_ce/reports/components/issue_body';
+} from 'ee_else_ce/ci/reports/components/issue_body';
export default {
name: 'ReportItem',
diff --git a/app/assets/javascripts/reports/components/report_link.vue b/app/assets/javascripts/ci/reports/components/report_link.vue
index 1f68f79e487..1f68f79e487 100644
--- a/app/assets/javascripts/reports/components/report_link.vue
+++ b/app/assets/javascripts/ci/reports/components/report_link.vue
diff --git a/app/assets/javascripts/reports/components/report_section.vue b/app/assets/javascripts/ci/reports/components/report_section.vue
index 468c8916b8d..468c8916b8d 100644
--- a/app/assets/javascripts/reports/components/report_section.vue
+++ b/app/assets/javascripts/ci/reports/components/report_section.vue
diff --git a/app/assets/javascripts/reports/components/summary_row.vue b/app/assets/javascripts/ci/reports/components/summary_row.vue
index ee55368c829..ee55368c829 100644
--- a/app/assets/javascripts/reports/components/summary_row.vue
+++ b/app/assets/javascripts/ci/reports/components/summary_row.vue
diff --git a/app/assets/javascripts/reports/constants.js b/app/assets/javascripts/ci/reports/constants.js
index bad6fa1e7b9..bad6fa1e7b9 100644
--- a/app/assets/javascripts/reports/constants.js
+++ b/app/assets/javascripts/ci/reports/constants.js
diff --git a/app/assets/javascripts/ci/runner/admin_runner_show/admin_runner_show_app.vue b/app/assets/javascripts/ci/runner/admin_runner_show/admin_runner_show_app.vue
index 9fa4b521ebc..66d790acb00 100644
--- a/app/assets/javascripts/ci/runner/admin_runner_show/admin_runner_show_app.vue
+++ b/app/assets/javascripts/ci/runner/admin_runner_show/admin_runner_show_app.vue
@@ -1,5 +1,6 @@
<script>
-import { GlBadge, GlTabs, GlTab, GlTooltipDirective } from '@gitlab/ui';
+import { GlBadge, GlTabs, GlTab } from '@gitlab/ui';
+import VueRouter from 'vue-router';
import { createAlert, VARIANT_SUCCESS } from '~/flash';
import { TYPE_CI_RUNNER } from '~/graphql_shared/constants';
import { convertToGraphQLId } from '~/graphql_shared/utils';
@@ -11,11 +12,28 @@ import RunnerPauseButton from '../components/runner_pause_button.vue';
import RunnerHeader from '../components/runner_header.vue';
import RunnerDetails from '../components/runner_details.vue';
import RunnerJobs from '../components/runner_jobs.vue';
-import { I18N_DETAILS, I18N_FETCH_ERROR } from '../constants';
+import { I18N_DETAILS, I18N_JOBS, I18N_FETCH_ERROR } from '../constants';
import runnerQuery from '../graphql/show/runner.query.graphql';
import { captureException } from '../sentry_utils';
import { saveAlertToLocalStorage } from '../local_storage_alert/save_alert_to_local_storage';
+const ROUTE_DETAILS = 'details';
+const ROUTE_JOBS = 'jobs';
+
+const routes = [
+ {
+ path: '/',
+ name: ROUTE_DETAILS,
+ component: RunnerDetails,
+ },
+ {
+ path: '/jobs',
+ name: ROUTE_JOBS,
+ component: RunnerJobs,
+ },
+ { path: '*', redirect: { name: ROUTE_DETAILS } },
+];
+
export default {
name: 'AdminRunnerShowApp',
components: {
@@ -26,12 +44,10 @@ export default {
RunnerEditButton,
RunnerPauseButton,
RunnerHeader,
- RunnerDetails,
- RunnerJobs,
- },
- directives: {
- GlTooltip: GlTooltipDirective,
},
+ router: new VueRouter({
+ routes,
+ }),
props: {
runnerId: {
type: String,
@@ -72,11 +88,17 @@ export default {
jobCount() {
return formatJobCount(this.runner?.jobCount);
},
+ tabIndex() {
+ return routes.findIndex(({ name }) => name === this.$route.name);
+ },
},
errorCaptured(error) {
this.reportToSentry(error);
},
methods: {
+ goTo(name) {
+ this.$router.push({ name });
+ },
reportToSentry(error) {
captureException({ error, component: this.$options.name });
},
@@ -85,7 +107,10 @@ export default {
redirectTo(this.runnersPath);
},
},
+ ROUTE_DETAILS,
+ ROUTE_JOBS,
I18N_DETAILS,
+ I18N_JOBS,
};
</script>
<template>
@@ -98,15 +123,13 @@ export default {
</template>
</runner-header>
- <gl-tabs>
- <gl-tab>
+ <gl-tabs :value="tabIndex">
+ <gl-tab @click="goTo($options.ROUTE_DETAILS)">
<template #title>{{ $options.I18N_DETAILS }}</template>
-
- <runner-details v-if="runner" :runner="runner" />
</gl-tab>
- <gl-tab>
+ <gl-tab @click="goTo($options.ROUTE_JOBS)">
<template #title>
- {{ s__('Runners|Jobs') }}
+ {{ $options.I18N_JOBS }}
<gl-badge
v-if="jobCount"
data-testid="job-count-badge"
@@ -116,9 +139,9 @@ export default {
{{ jobCount }}
</gl-badge>
</template>
-
- <runner-jobs v-if="runner" :runner="runner" />
</gl-tab>
+
+ <router-view v-if="runner" :runner="runner" />
</gl-tabs>
</div>
</template>
diff --git a/app/assets/javascripts/ci/runner/admin_runner_show/index.js b/app/assets/javascripts/ci/runner/admin_runner_show/index.js
index ea455416648..cbd25819303 100644
--- a/app/assets/javascripts/ci/runner/admin_runner_show/index.js
+++ b/app/assets/javascripts/ci/runner/admin_runner_show/index.js
@@ -1,10 +1,12 @@
import Vue from 'vue';
import VueApollo from 'vue-apollo';
+import VueRouter from 'vue-router';
import createDefaultClient from '~/lib/graphql';
import { showAlertFromLocalStorage } from '../local_storage_alert/show_alert_from_local_storage';
import AdminRunnerShowApp from './admin_runner_show_app.vue';
Vue.use(VueApollo);
+Vue.use(VueRouter);
export const initAdminRunnerShow = (selector = '#js-admin-runner-show') => {
showAlertFromLocalStorage();
diff --git a/app/assets/javascripts/ci/runner/admin_runners/admin_runners_app.vue b/app/assets/javascripts/ci/runner/admin_runners/admin_runners_app.vue
index 2915e460085..3bd20dff9cc 100644
--- a/app/assets/javascripts/ci/runner/admin_runners/admin_runners_app.vue
+++ b/app/assets/javascripts/ci/runner/admin_runners/admin_runners_app.vue
@@ -23,6 +23,7 @@ import RunnerStats from '../components/stat/runner_stats.vue';
import RunnerPagination from '../components/runner_pagination.vue';
import RunnerTypeTabs from '../components/runner_type_tabs.vue';
import RunnerActionsCell from '../components/cells/runner_actions_cell.vue';
+import RunnerJobStatusBadge from '../components/runner_job_status_badge.vue';
import { pausedTokenConfig } from '../components/search_tokens/paused_token_config';
import { statusTokenConfig } from '../components/search_tokens/status_token_config';
@@ -48,6 +49,7 @@ export default {
RunnerPagination,
RunnerTypeTabs,
RunnerActionsCell,
+ RunnerJobStatusBadge,
},
mixins: [glFeatureFlagMixin()],
inject: ['emptyStateSvgPath', 'emptyStateFilteredSvgPath'],
@@ -69,6 +71,9 @@ export default {
apollo: {
runners: {
query: allRunnersQuery,
+ context: {
+ isSingleRequest: true,
+ },
fetchPolicy: fetchPolicies.NETWORK_ONLY,
variables() {
return this.variables;
@@ -134,6 +139,12 @@ export default {
this.reportToSentry(error);
},
methods: {
+ jobsUrl(runner) {
+ const url = new URL(runner.adminUrl);
+ url.hash = '#/jobs';
+
+ return url.href;
+ },
onToggledPaused() {
// When a runner becomes Paused, the tab count can
// become stale, refetch outdated counts.
@@ -208,6 +219,12 @@ export default {
<runner-name :runner="runner" />
</gl-link>
</template>
+ <template #runner-job-status-badge="{ runner }">
+ <runner-job-status-badge
+ :href="jobsUrl(runner)"
+ :job-status="runner.jobExecutionStatus"
+ />
+ </template>
<template #runner-actions-cell="{ runner }">
<runner-actions-cell
:runner="runner"
diff --git a/app/assets/javascripts/ci/runner/components/cells/runner_status_cell.vue b/app/assets/javascripts/ci/runner/components/cells/runner_status_cell.vue
index 67b9b0a266f..cfbe37f5ba2 100644
--- a/app/assets/javascripts/ci/runner/components/cells/runner_status_cell.vue
+++ b/app/assets/javascripts/ci/runner/components/cells/runner_status_cell.vue
@@ -7,8 +7,6 @@ import RunnerPausedBadge from '../runner_paused_badge.vue';
export default {
components: {
RunnerStatusBadge,
- RunnerUpgradeStatusBadge: () =>
- import('ee_component/ci/runner/components/runner_upgrade_status_badge.vue'),
RunnerPausedBadge,
},
directives: {
@@ -34,10 +32,6 @@ export default {
:runner="runner"
class="gl-display-inline-block gl-max-w-full gl-text-truncate"
/>
- <runner-upgrade-status-badge
- :runner="runner"
- class="gl-display-inline-block gl-max-w-full gl-text-truncate"
- />
<runner-paused-badge
v-if="paused"
class="gl-display-inline-block gl-max-w-full gl-text-truncate"
diff --git a/app/assets/javascripts/ci/runner/components/cells/runner_stacked_summary_cell.vue b/app/assets/javascripts/ci/runner/components/cells/runner_summary_cell.vue
index 1e44d5fccc2..4a72023b6a0 100644
--- a/app/assets/javascripts/ci/runner/components/cells/runner_stacked_summary_cell.vue
+++ b/app/assets/javascripts/ci/runner/components/cells/runner_summary_cell.vue
@@ -6,9 +6,11 @@ import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue';
import RunnerName from '../runner_name.vue';
import RunnerTags from '../runner_tags.vue';
import RunnerTypeBadge from '../runner_type_badge.vue';
+import RunnerJobStatusBadge from '../runner_job_status_badge.vue';
import { formatJobCount } from '../../utils';
import {
+ I18N_NO_DESCRIPTION,
I18N_LOCKED_RUNNER_DESCRIPTION,
I18N_VERSION_LABEL,
I18N_LAST_CONTACT_LABEL,
@@ -25,6 +27,7 @@ export default {
RunnerName,
RunnerTags,
RunnerTypeBadge,
+ RunnerJobStatusBadge,
RunnerUpgradeStatusIcon: () =>
import('ee_component/ci/runner/components/runner_upgrade_status_icon.vue'),
TooltipOnTruncate,
@@ -44,6 +47,7 @@ export default {
},
},
i18n: {
+ I18N_NO_DESCRIPTION,
I18N_LOCKED_RUNNER_DESCRIPTION,
I18N_VERSION_LABEL,
I18N_LAST_CONTACT_LABEL,
@@ -75,12 +79,21 @@ export default {
</gl-sprintf>
</div>
<div class="gl-text-secondary gl-mx-2" aria-hidden="true">·</div>
- <tooltip-on-truncate class="gl-text-truncate gl-display-block" :title="runner.description">
+ <tooltip-on-truncate
+ v-if="runner.description"
+ class="gl-text-truncate gl-display-block"
+ :title="runner.description"
+ >
{{ runner.description }}
</tooltip-on-truncate>
+ <span v-else class="gl-text-secondary">{{ $options.i18n.I18N_NO_DESCRIPTION }}</span>
</div>
<div>
+ <slot :runner="runner" name="runner-job-status-badge">
+ <runner-job-status-badge :job-status="runner.jobExecutionStatus" />
+ </slot>
+
<runner-summary-field icon="clock">
<gl-sprintf :message="$options.i18n.I18N_LAST_CONTACT_LABEL">
<template #timeAgo>
diff --git a/app/assets/javascripts/ci/runner/components/runner_detail.vue b/app/assets/javascripts/ci/runner/components/runner_detail.vue
index c260670b517..9e8055a8432 100644
--- a/app/assets/javascripts/ci/runner/components/runner_detail.vue
+++ b/app/assets/javascripts/ci/runner/components/runner_detail.vue
@@ -49,7 +49,7 @@ export default {
<template v-if="value || $scopedSlots.value">
<slot name="value">{{ value }}</slot>
</template>
- <span v-else class="gl-text-gray-500">{{ emptyValue }}</span>
+ <span v-else class="gl-text-secondary">{{ emptyValue }}</span>
</dd>
</div>
</template>
diff --git a/app/assets/javascripts/ci/runner/components/runner_groups.vue b/app/assets/javascripts/ci/runner/components/runner_groups.vue
index c3b35bd52a9..8501d165157 100644
--- a/app/assets/javascripts/ci/runner/components/runner_groups.vue
+++ b/app/assets/javascripts/ci/runner/components/runner_groups.vue
@@ -32,6 +32,6 @@ export default {
:avatar-url="group.avatarUrl"
/>
</template>
- <span v-else class="gl-text-gray-500">{{ __('None') }}</span>
+ <span v-else class="gl-text-secondary">{{ __('None') }}</span>
</div>
</template>
diff --git a/app/assets/javascripts/ci/runner/components/runner_job_status_badge.vue b/app/assets/javascripts/ci/runner/components/runner_job_status_badge.vue
new file mode 100644
index 00000000000..1e52acecfb8
--- /dev/null
+++ b/app/assets/javascripts/ci/runner/components/runner_job_status_badge.vue
@@ -0,0 +1,55 @@
+<script>
+import { GlBadge, GlTooltipDirective } from '@gitlab/ui';
+import {
+ I18N_JOB_STATUS_RUNNING,
+ I18N_JOB_STATUS_IDLE,
+ JOB_STATUS_RUNNING,
+ JOB_STATUS_IDLE,
+} from '../constants';
+
+export default {
+ components: {
+ GlBadge,
+ },
+ directives: {
+ GlTooltip: GlTooltipDirective,
+ },
+ props: {
+ jobStatus: {
+ required: false,
+ default: null,
+ type: String,
+ },
+ },
+ computed: {
+ badge() {
+ switch (this.jobStatus) {
+ case JOB_STATUS_RUNNING:
+ return {
+ classes: 'gl-text-blue-600! gl-border gl-border-blue-600!',
+ label: I18N_JOB_STATUS_RUNNING,
+ };
+ case JOB_STATUS_IDLE:
+ return {
+ classes: 'gl-text-gray-700! gl-border gl-border-gray-500!',
+ label: I18N_JOB_STATUS_IDLE,
+ };
+ default:
+ return null;
+ }
+ },
+ },
+};
+</script>
+<template>
+ <gl-badge
+ v-if="badge"
+ v-bind="$attrs"
+ size="sm"
+ class="gl-mr-3 gl-bg-transparent!"
+ variant="muted"
+ :class="badge.classes"
+ >
+ {{ badge.label }}
+ </gl-badge>
+</template>
diff --git a/app/assets/javascripts/ci/runner/components/runner_list.vue b/app/assets/javascripts/ci/runner/components/runner_list.vue
index e895537dcdc..b2aad0aac4f 100644
--- a/app/assets/javascripts/ci/runner/components/runner_list.vue
+++ b/app/assets/javascripts/ci/runner/components/runner_list.vue
@@ -7,7 +7,7 @@ import checkedRunnerIdsQuery from '../graphql/list/checked_runner_ids.query.grap
import { formatJobCount, tableField } from '../utils';
import RunnerBulkDelete from './runner_bulk_delete.vue';
import RunnerBulkDeleteCheckbox from './runner_bulk_delete_checkbox.vue';
-import RunnerStackedSummaryCell from './cells/runner_stacked_summary_cell.vue';
+import RunnerSummaryCell from './cells/runner_summary_cell.vue';
import RunnerStatusPopover from './runner_status_popover.vue';
import RunnerStatusCell from './cells/runner_status_cell.vue';
import RunnerOwnerCell from './cells/runner_owner_cell.vue';
@@ -28,7 +28,7 @@ export default {
RunnerBulkDelete,
RunnerBulkDeleteCheckbox,
RunnerStatusPopover,
- RunnerStackedSummaryCell,
+ RunnerSummaryCell,
RunnerStatusCell,
RunnerOwnerCell,
},
@@ -154,11 +154,14 @@ export default {
</template>
<template #cell(summary)="{ item, index }">
- <runner-stacked-summary-cell :runner="item">
+ <runner-summary-cell :runner="item">
<template #runner-name="{ runner }">
<slot name="runner-name" :runner="runner" :index="index"></slot>
</template>
- </runner-stacked-summary-cell>
+ <template #runner-job-status-badge="{ runner }">
+ <slot name="runner-job-status-badge" :runner="runner" :index="index"></slot>
+ </template>
+ </runner-summary-cell>
</template>
<template #head(owner)="{ label }">
diff --git a/app/assets/javascripts/ci/runner/components/runner_projects.vue b/app/assets/javascripts/ci/runner/components/runner_projects.vue
index 84008e8eee8..4a6e90b44a9 100644
--- a/app/assets/javascripts/ci/runner/components/runner_projects.vue
+++ b/app/assets/javascripts/ci/runner/components/runner_projects.vue
@@ -133,7 +133,7 @@ export default {
:is-owner="isOwner(project.id)"
/>
</template>
- <div v-else class="gl-py-5 gl-text-gray-500">{{ $options.I18N_NO_PROJECTS_FOUND }}</div>
+ <div v-else class="gl-py-5 gl-text-secondary">{{ $options.I18N_NO_PROJECTS_FOUND }}</div>
<runner-pagination
:disabled="loading"
diff --git a/app/assets/javascripts/ci/runner/components/runner_type_tabs.vue b/app/assets/javascripts/ci/runner/components/runner_type_tabs.vue
index 584236168ac..70226074993 100644
--- a/app/assets/javascripts/ci/runner/components/runner_type_tabs.vue
+++ b/app/assets/javascripts/ci/runner/components/runner_type_tabs.vue
@@ -59,21 +59,20 @@ export default {
return [
{
title: I18N_ALL_TYPES,
- runnerType: null,
},
...tabs,
];
},
},
methods: {
- onTabSelected({ runnerType }) {
+ onTabSelected(runnerType) {
this.$emit('input', {
...this.value,
runnerType,
pagination: { page: 1 },
});
},
- isTabActive({ runnerType }) {
+ isTabActive(runnerType = null) {
return runnerType === this.value.runnerType;
},
tabBadgeCountVariables(runnerType) {
@@ -102,8 +101,8 @@ export default {
<gl-tab
v-for="tab in tabs"
:key="`${tab.runnerType}`"
- :active="isTabActive(tab)"
- @click="onTabSelected(tab)"
+ :active="isTabActive(tab.runnerType)"
+ @click="onTabSelected(tab.runnerType)"
>
<template #title>
{{ tab.title }}
diff --git a/app/assets/javascripts/ci/runner/components/search_tokens/paused_token_config.js b/app/assets/javascripts/ci/runner/components/search_tokens/paused_token_config.js
index 97ee8ec3eef..71a145dd4a3 100644
--- a/app/assets/javascripts/ci/runner/components/search_tokens/paused_token_config.js
+++ b/app/assets/javascripts/ci/runner/components/search_tokens/paused_token_config.js
@@ -1,5 +1,5 @@
import { __ } from '~/locale';
-import { OPERATOR_IS_ONLY } from '~/vue_shared/components/filtered_search_bar/constants';
+import { OPERATORS_IS } from '~/vue_shared/components/filtered_search_bar/constants';
import BaseToken from '~/vue_shared/components/filtered_search_bar/tokens/base_token.vue';
import { PARAM_KEY_PAUSED, I18N_PAUSED } from '../../constants';
@@ -24,5 +24,5 @@ export const pausedTokenConfig = {
// see: https://gitlab.com/gitlab-org/gitlab-ui/-/issues/1438
title: title.replace(/\s/g, '\u00a0'),
})),
- operators: OPERATOR_IS_ONLY,
+ operators: OPERATORS_IS,
};
diff --git a/app/assets/javascripts/ci/runner/components/search_tokens/status_token_config.js b/app/assets/javascripts/ci/runner/components/search_tokens/status_token_config.js
index 117a630719e..4bc32909777 100644
--- a/app/assets/javascripts/ci/runner/components/search_tokens/status_token_config.js
+++ b/app/assets/javascripts/ci/runner/components/search_tokens/status_token_config.js
@@ -1,5 +1,5 @@
import {
- OPERATOR_IS_ONLY,
+ OPERATORS_IS,
TOKEN_TITLE_STATUS,
} from '~/vue_shared/components/filtered_search_bar/constants';
import BaseToken from '~/vue_shared/components/filtered_search_bar/tokens/base_token.vue';
@@ -38,5 +38,5 @@ export const statusTokenConfig = {
// see: https://gitlab.com/gitlab-org/gitlab-ui/-/issues/1438
title: title.replace(/\s/g, '\u00a0'),
})),
- operators: OPERATOR_IS_ONLY,
+ operators: OPERATORS_IS,
};
diff --git a/app/assets/javascripts/ci/runner/components/search_tokens/tag_token_config.js b/app/assets/javascripts/ci/runner/components/search_tokens/tag_token_config.js
index fdeba714385..369b214f952 100644
--- a/app/assets/javascripts/ci/runner/components/search_tokens/tag_token_config.js
+++ b/app/assets/javascripts/ci/runner/components/search_tokens/tag_token_config.js
@@ -1,5 +1,5 @@
import { s__ } from '~/locale';
-import { OPERATOR_IS_ONLY } from '~/vue_shared/components/filtered_search_bar/constants';
+import { OPERATORS_IS } from '~/vue_shared/components/filtered_search_bar/constants';
import { PARAM_KEY_TAG } from '../../constants';
import TagToken from './tag_token.vue';
@@ -8,5 +8,5 @@ export const tagTokenConfig = {
title: s__('Runners|Tags'),
type: PARAM_KEY_TAG,
token: TagToken,
- operators: OPERATOR_IS_ONLY,
+ operators: OPERATORS_IS,
};
diff --git a/app/assets/javascripts/ci/runner/components/stat/runner_count.vue b/app/assets/javascripts/ci/runner/components/stat/runner_count.vue
index 4ad9259f59d..c33c42f3afe 100644
--- a/app/assets/javascripts/ci/runner/components/stat/runner_count.vue
+++ b/app/assets/javascripts/ci/runner/components/stat/runner_count.vue
@@ -16,13 +16,13 @@ import { INSTANCE_TYPE, GROUP_TYPE } from '../../constants';
* <strong/> tag.
*
* ```vue
- * <runner-count-stat
+ * <runner-count
* #default="{ count }"
* :scope="INSTANCE_TYPE"
* :variables="{ status: 'ONLINE' }"
* >
* <strong>{{ count }}</strong>
- * </runner-count-stat>
+ * </runner-count>
* ```
*
* Use `:skip="true"` to prevent data from being fetched and
diff --git a/app/assets/javascripts/ci/runner/components/stat/runner_stats.vue b/app/assets/javascripts/ci/runner/components/stat/runner_stats.vue
index 3965e5551f1..2e50dc13d2d 100644
--- a/app/assets/javascripts/ci/runner/components/stat/runner_stats.vue
+++ b/app/assets/javascripts/ci/runner/components/stat/runner_stats.vue
@@ -1,5 +1,4 @@
<script>
-import RunnerSingleStat from '~/ci/runner/components/stat/runner_single_stat.vue';
import {
I18N_STATUS_ONLINE,
I18N_STATUS_OFFLINE,
@@ -8,9 +7,19 @@ import {
STATUS_OFFLINE,
STATUS_STALE,
} from '../../constants';
+import RunnerSingleStat from './runner_single_stat.vue';
+import RunnerCount from './runner_count.vue';
+
+/**
+ * Shows general stats about the runners.
+ *
+ * First it checks if there are any runners in this context, and if so,
+ * shows more details for different status.
+ */
export default {
components: {
+ RunnerCount,
RunnerSingleStat,
RunnerUpgradeStatusStats: () =>
import('ee_component/ci/runner/components/stat/runner_upgrade_status_stats.vue'),
@@ -71,19 +80,21 @@ export default {
};
</script>
<template>
- <div class="gl-display-flex gl-flex-wrap gl-py-6">
- <runner-single-stat
- v-for="stat in stats"
- :key="stat.key"
- :scope="scope"
- v-bind="stat.props"
- class="gl-px-5"
- />
+ <runner-count #default="{ count }" :scope="scope" :variables="variables">
+ <div v-if="count" class="gl-display-flex gl-flex-wrap gl-py-6">
+ <runner-single-stat
+ v-for="stat in stats"
+ :key="stat.key"
+ :scope="scope"
+ v-bind="stat.props"
+ class="gl-px-5"
+ />
- <runner-upgrade-status-stats
- class="gl-display-contents"
- :scope="scope"
- :variables="variables"
- />
- </div>
+ <runner-upgrade-status-stats
+ class="gl-display-contents"
+ :scope="scope"
+ :variables="variables"
+ />
+ </div>
+ </runner-count>
</template>
diff --git a/app/assets/javascripts/ci/runner/constants.js b/app/assets/javascripts/ci/runner/constants.js
index dfc5f0c4152..31900a1fe89 100644
--- a/app/assets/javascripts/ci/runner/constants.js
+++ b/app/assets/javascripts/ci/runner/constants.js
@@ -32,6 +32,10 @@ export const I18N_STATUS_NEVER_CONTACTED = s__('Runners|Never contacted');
export const I18N_STATUS_OFFLINE = s__('Runners|Offline');
export const I18N_STATUS_STALE = s__('Runners|Stale');
+// Executor Status
+export const I18N_JOB_STATUS_RUNNING = s__('Runners|Running');
+export const I18N_JOB_STATUS_IDLE = s__('Runners|Idle');
+
// Status help popover
export const I18N_STATUS_POPOVER_TITLE = s__('Runners|Runner statuses');
@@ -82,6 +86,7 @@ export const I18N_DELETE_RUNNER = s__('Runners|Delete runner');
export const I18N_DELETED_TOAST = s__('Runners|Runner %{name} was deleted');
// List
+export const I18N_NO_DESCRIPTION = s__('Runners|No description');
export const I18N_LOCKED_RUNNER_DESCRIPTION = s__(
'Runners|Runner is locked and available for currently assigned projects only. Only administrators can change the assigned projects.',
);
@@ -94,6 +99,7 @@ export const I18N_ADMIN = s__('Runners|Administrator');
// Runner details
export const I18N_DETAILS = s__('Runners|Details');
+export const I18N_JOBS = s__('Runners|Jobs');
export const I18N_ASSIGNED_PROJECTS = s__('Runners|Assigned Projects (%{projectCount})');
export const I18N_FILTER_PROJECTS = s__('Runners|Filter projects');
export const I18N_CLEAR_FILTER_PROJECTS = __('Clear');
@@ -134,6 +140,11 @@ export const STATUS_NEVER_CONTACTED = 'NEVER_CONTACTED';
export const STATUS_OFFLINE = 'OFFLINE';
export const STATUS_STALE = 'STALE';
+// CiRunnerJobExecutionStatus
+
+export const JOB_STATUS_RUNNING = 'RUNNING';
+export const JOB_STATUS_IDLE = 'IDLE';
+
// CiRunnerAccessLevel
export const ACCESS_LEVEL_NOT_PROTECTED = 'NOT_PROTECTED';
diff --git a/app/assets/javascripts/ci/runner/graphql/list/list_item_shared.fragment.graphql b/app/assets/javascripts/ci/runner/graphql/list/list_item_shared.fragment.graphql
index 0dff011daaa..6f72509f599 100644
--- a/app/assets/javascripts/ci/runner/graphql/list/list_item_shared.fragment.graphql
+++ b/app/assets/javascripts/ci/runner/graphql/list/list_item_shared.fragment.graphql
@@ -12,6 +12,7 @@ fragment ListItemShared on CiRunner {
createdAt
contactedAt
status(legacyMode: null)
+ jobExecutionStatus
userPermissions {
updateRunner
deleteRunner
diff --git a/app/assets/javascripts/ci/runner/group_runners/group_runners_app.vue b/app/assets/javascripts/ci/runner/group_runners/group_runners_app.vue
index 91c22923075..57ceaa24b6e 100644
--- a/app/assets/javascripts/ci/runner/group_runners/group_runners_app.vue
+++ b/app/assets/javascripts/ci/runner/group_runners/group_runners_app.vue
@@ -82,6 +82,9 @@ export default {
apollo: {
runners: {
query: groupRunnersQuery,
+ context: {
+ isSingleRequest: true,
+ },
fetchPolicy: fetchPolicies.NETWORK_ONLY,
variables() {
return this.variables;
diff --git a/app/assets/javascripts/ci/runner/runner_search_utils.js b/app/assets/javascripts/ci/runner/runner_search_utils.js
index adc832b0600..3dc99baa329 100644
--- a/app/assets/javascripts/ci/runner/runner_search_utils.js
+++ b/app/assets/javascripts/ci/runner/runner_search_utils.js
@@ -176,6 +176,7 @@ export const fromSearchToUrl = (
[PARAM_KEY_RUNNER_TYPE]: [],
[PARAM_KEY_MEMBERSHIP]: [],
[PARAM_KEY_TAG]: [],
+ [PARAM_KEY_PAUSED]: [],
// Current filters
...filterToQueryObject(processFilters(filters), {
filteredSearchTermKey: PARAM_KEY_SEARCH,
diff --git a/app/assets/javascripts/ci_secure_files/components/secure_files_list.vue b/app/assets/javascripts/ci_secure_files/components/secure_files_list.vue
index 9d8cb40b60a..661389f4059 100644
--- a/app/assets/javascripts/ci_secure_files/components/secure_files_list.vue
+++ b/app/assets/javascripts/ci_secure_files/components/secure_files_list.vue
@@ -13,7 +13,7 @@ import {
} from '@gitlab/ui';
import * as Sentry from '@sentry/browser';
import Api, { DEFAULT_PER_PAGE } from '~/api';
-import httpStatusCodes from '~/lib/utils/http_status';
+import { HTTP_STATUS_PAYLOAD_TOO_LARGE } from '~/lib/utils/http_status';
import { __, s__, sprintf } from '~/locale';
import Tracking from '~/tracking';
import TimeagoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
@@ -145,7 +145,7 @@ export default {
let message = '';
if (error?.response?.data?.message?.name) {
message = this.$options.i18n.uploadErrorMessages.duplicate;
- } else if (error.response.status === httpStatusCodes.PAYLOAD_TOO_LARGE) {
+ } else if (error.response.status === HTTP_STATUS_PAYLOAD_TOO_LARGE) {
message = sprintf(this.$options.i18n.uploadErrorMessages.tooLarge, {
limit: this.fileSizeLimit,
});
diff --git a/app/assets/javascripts/ci_variable_list/components/ci_group_variables.vue b/app/assets/javascripts/ci_variable_list/components/ci_group_variables.vue
index c8f5ac1736d..4466a6a8081 100644
--- a/app/assets/javascripts/ci_variable_list/components/ci_group_variables.vue
+++ b/app/assets/javascripts/ci_variable_list/components/ci_group_variables.vue
@@ -46,6 +46,7 @@ export default {
:id="graphqlId"
:are-scoped-variables-available="areScopedVariablesAvailable"
component-name="GroupVariables"
+ entity="group"
:full-path="groupPath"
:mutation-data="$options.mutationData"
:query-data="$options.queryData"
diff --git a/app/assets/javascripts/ci_variable_list/components/ci_project_variables.vue b/app/assets/javascripts/ci_variable_list/components/ci_project_variables.vue
index 2c4818e20c1..6326940148a 100644
--- a/app/assets/javascripts/ci_variable_list/components/ci_project_variables.vue
+++ b/app/assets/javascripts/ci_variable_list/components/ci_project_variables.vue
@@ -48,6 +48,7 @@ export default {
:id="graphqlId"
:are-scoped-variables-available="true"
component-name="ProjectVariables"
+ entity="project"
:full-path="projectFullPath"
:mutation-data="$options.mutationData"
:query-data="$options.queryData"
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 94f8cb9e906..00177539cdc 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
@@ -29,6 +29,7 @@ import {
ENVIRONMENT_SCOPE_LINK_TITLE,
EVENT_LABEL,
EVENT_ACTION,
+ EXPANDED_VARIABLES_NOTE,
EDIT_VARIABLE_ACTION,
VARIABLE_ACTIONS,
variableOptions,
@@ -46,6 +47,7 @@ export default {
awsTipMessage: AWS_TIP_MESSAGE,
containsVariableReferenceMessage: CONTAINS_VARIABLE_REFERENCE_MESSAGE,
environmentScopeLinkTitle: ENVIRONMENT_SCOPE_LINK_TITLE,
+ expandedVariablesNote: EXPANDED_VARIABLES_NOTE,
components: {
CiEnvironmentsDropdown,
GlAlert,
@@ -127,7 +129,7 @@ export default {
},
containsVariableReference() {
const regex = /\$/;
- return regex.test(this.variable.value);
+ return regex.test(this.variable.value) && this.isExpanded;
},
displayMaskedError() {
return !this.canMask && this.variable.masked;
@@ -135,6 +137,9 @@ export default {
isEditing() {
return this.mode === EDIT_VARIABLE_ACTION;
},
+ isExpanded() {
+ return !this.variable.raw;
+ },
isTipVisible() {
return !this.isTipDismissed && AWS_TOKEN_CONSTANTS.includes(this.variable.key);
},
@@ -208,6 +213,9 @@ export default {
hideModal() {
this.$refs.modal.hide();
},
+ onShow() {
+ this.setVariableProtectedByDefault();
+ },
resetModalHandler() {
this.resetVariableData();
this.resetValidationErrorEvents();
@@ -220,6 +228,9 @@ export default {
setEnvironmentScope(scope) {
this.variable = { ...this.variable, environmentScope: scope };
},
+ setVariableRaw(expanded) {
+ this.variable = { ...this.variable, raw: !expanded };
+ },
setVariableProtected() {
this.variable = { ...this.variable, protected: true };
},
@@ -275,7 +286,7 @@ export default {
static
lazy
@hidden="resetModalHandler"
- @shown="setVariableProtectedByDefault"
+ @shown="onShow"
>
<form>
<gl-form-combobox
@@ -304,6 +315,13 @@ export default {
class="gl-font-monospace!"
spellcheck="false"
/>
+ <p
+ v-if="variable.raw"
+ class="gl-mt-2 gl-mb-0 text-secondary"
+ data-testid="raw-variable-tip"
+ >
+ {{ __('Variable value will be evaluated as raw string.') }}
+ </p>
</gl-form-group>
<div class="gl-display-flex">
@@ -361,7 +379,6 @@ export default {
{{ __('Export variable to pipelines running on protected branches and tags only.') }}
</p>
</gl-form-checkbox>
-
<gl-form-checkbox
ref="masked-ci-variable"
v-model="variable.masked"
@@ -371,7 +388,7 @@ export default {
<gl-link target="_blank" :href="maskedEnvironmentVariablesLink">
<gl-icon name="question" :size="12" />
</gl-link>
- <p class="gl-mt-2 gl-mb-0 text-secondary">
+ <p class="gl-mt-2 text-secondary">
{{ __('Variable will be masked in job logs.') }}
<span
:class="{
@@ -385,6 +402,24 @@ export default {
}}</gl-link>
</p>
</gl-form-checkbox>
+ <gl-form-checkbox
+ ref="expanded-ci-variable"
+ :checked="isExpanded"
+ data-testid="ci-variable-expanded-checkbox"
+ @change="setVariableRaw"
+ >
+ {{ __('Expand variable reference') }}
+ <gl-link target="_blank" :href="containsVariableReferenceLink">
+ <gl-icon name="question" :size="12" />
+ </gl-link>
+ <p class="gl-mt-2 gl-mb-0 gl-text-secondary">
+ <gl-sprintf :message="$options.expandedVariablesNote">
+ <template #code="{ content }">
+ <code>{{ content }}</code>
+ </template>
+ </gl-sprintf>
+ </p>
+ </gl-form-checkbox>
</gl-form-group>
</form>
<gl-collapse :visible="isTipVisible">
diff --git a/app/assets/javascripts/ci_variable_list/components/ci_variable_settings.vue b/app/assets/javascripts/ci_variable_list/components/ci_variable_settings.vue
index 94fd6c3892c..3c6114b38ce 100644
--- a/app/assets/javascripts/ci_variable_list/components/ci_variable_settings.vue
+++ b/app/assets/javascripts/ci_variable_list/components/ci_variable_settings.vue
@@ -14,6 +14,11 @@ export default {
required: false,
default: false,
},
+ entity: {
+ type: String,
+ required: false,
+ default: '',
+ },
environments: {
type: Array,
required: false,
@@ -27,7 +32,11 @@ export default {
isLoading: {
type: Boolean,
required: false,
- default: false,
+ },
+ maxVariableLimit: {
+ type: Number,
+ required: false,
+ default: 0,
},
variables: {
type: Array,
@@ -75,7 +84,9 @@ export default {
<div class="row">
<div class="col-lg-12">
<ci-variable-table
+ :entity="entity"
:is-loading="isLoading"
+ :max-variable-limit="maxVariableLimit"
:variables="variables"
@set-selected-variable="setSelectedVariable"
/>
diff --git a/app/assets/javascripts/ci_variable_list/components/ci_variable_shared.vue b/app/assets/javascripts/ci_variable_list/components/ci_variable_shared.vue
index 7ee250cea98..6e39bda0b07 100644
--- a/app/assets/javascripts/ci_variable_list/components/ci_variable_shared.vue
+++ b/app/assets/javascripts/ci_variable_list/components/ci_variable_shared.vue
@@ -26,6 +26,11 @@ export default {
required: true,
type: String,
},
+ entity: {
+ required: false,
+ type: String,
+ default: '',
+ },
fullPath: {
required: false,
type: String,
@@ -90,6 +95,7 @@ export default {
isInitialLoading: true,
isLoadingMoreItems: false,
loadingCounter: 0,
+ maxVariableLimit: 0,
pageInfo: {},
};
},
@@ -107,6 +113,8 @@ export default {
return this.queryData.ciVariables.lookup(data)?.nodes || [];
},
result({ data }) {
+ this.maxVariableLimit = this.queryData.ciVariables.lookup(data)?.limit || 0;
+
this.pageInfo = this.queryData.ciVariables.lookup(data)?.pageInfo || this.pageInfo;
this.hasNextPage = this.pageInfo?.hasNextPage || false;
@@ -221,9 +229,11 @@ export default {
<template>
<ci-variable-settings
:are-scoped-variables-available="areScopedVariablesAvailable"
+ :entity="entity"
:hide-environment-scope="hideEnvironmentScope"
:is-loading="isLoading"
:variables="ciVariables"
+ :max-variable-limit="maxVariableLimit"
:environments="environments"
@add-variable="addVariable"
@delete-variable="deleteVariable"
diff --git a/app/assets/javascripts/ci_variable_list/components/ci_variable_table.vue b/app/assets/javascripts/ci_variable_list/components/ci_variable_table.vue
index 3cdcb68e919..345a8def49d 100644
--- a/app/assets/javascripts/ci_variable_list/components/ci_variable_table.vue
+++ b/app/assets/javascripts/ci_variable_list/components/ci_variable_table.vue
@@ -1,8 +1,21 @@
<script>
-import { GlButton, GlLoadingIcon, GlModalDirective, GlTable, GlTooltipDirective } from '@gitlab/ui';
-import { s__, __ } from '~/locale';
+import {
+ GlAlert,
+ GlButton,
+ GlLoadingIcon,
+ GlModalDirective,
+ GlTable,
+ GlTooltipDirective,
+} from '@gitlab/ui';
+import { __, s__, sprintf } from '~/locale';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
-import { ADD_CI_VARIABLE_MODAL_ID, variableText } from '../constants';
+import {
+ ADD_CI_VARIABLE_MODAL_ID,
+ DEFAULT_EXCEEDS_VARIABLE_LIMIT_TEXT,
+ EXCEEDS_VARIABLE_LIMIT_TEXT,
+ MAXIMUM_VARIABLE_LIMIT_REACHED,
+ variableText,
+} from '../constants';
import { convertEnvironmentScope } from '../utils';
export default {
@@ -41,6 +54,7 @@ export default {
},
],
components: {
+ GlAlert,
GlButton,
GlLoadingIcon,
GlTable,
@@ -51,10 +65,19 @@ export default {
},
mixins: [glFeatureFlagsMixin()],
props: {
+ entity: {
+ type: String,
+ required: false,
+ default: '',
+ },
isLoading: {
type: Boolean,
required: true,
},
+ maxVariableLimit: {
+ type: Number,
+ required: true,
+ },
variables: {
type: Array,
required: true,
@@ -66,6 +89,23 @@ export default {
};
},
computed: {
+ exceedsVariableLimit() {
+ return this.maxVariableLimit > 0 && this.variables.length >= this.maxVariableLimit;
+ },
+ exceedsVariableLimitText() {
+ if (this.exceedsVariableLimit && this.entity) {
+ return sprintf(EXCEEDS_VARIABLE_LIMIT_TEXT, {
+ entity: this.entity,
+ currentVariableCount: this.variables.length,
+ maxVariableLimit: this.maxVariableLimit,
+ });
+ }
+
+ return DEFAULT_EXCEEDS_VARIABLE_LIMIT_TEXT;
+ },
+ showAlert() {
+ return !this.isLoading && this.exceedsVariableLimit;
+ },
valuesButtonText() {
return this.areValuesHidden ? __('Reveal values') : __('Hide values');
},
@@ -104,17 +144,29 @@ export default {
if (item.masked) {
options.push(s__('CiVariables|Masked'));
}
+ if (!item.raw) {
+ options.push(s__('CiVariables|Expanded'));
+ }
return options.join(', ');
},
},
+ maximumVariableLimitReached: MAXIMUM_VARIABLE_LIMIT_REACHED,
};
</script>
<template>
<div class="ci-variable-table" data-testid="ci-variable-table">
<gl-loading-icon v-if="isLoading" />
+ <gl-alert
+ v-if="showAlert"
+ :dismissible="false"
+ :title="$options.maximumVariableLimitReached"
+ variant="info"
+ >
+ {{ exceedsVariableLimitText }}
+ </gl-alert>
<gl-table
- v-else
+ v-if="!isLoading"
:fields="fields"
:items="variablesWithOptions"
tbody-tr-class="js-ci-variable-row"
@@ -178,7 +230,7 @@ export default {
</div>
</template>
<template #cell(options)="{ item }">
- <span>{{ item.options }}</span>
+ <span data-testid="ci-variable-table-row-options">{{ item.options }}</span>
</template>
<template #cell(environmentScope)="{ item }">
<div
@@ -215,6 +267,14 @@ export default {
</p>
</template>
</gl-table>
+ <gl-alert
+ v-if="showAlert"
+ :dismissible="false"
+ :title="$options.maximumVariableLimitReached"
+ variant="info"
+ >
+ {{ exceedsVariableLimitText }}
+ </gl-alert>
<div class="ci-variable-actions gl-display-flex gl-mt-5">
<gl-button
v-gl-modal-directive="$options.modalId"
@@ -223,6 +283,7 @@ export default {
variant="confirm"
category="primary"
:aria-label="__('Add')"
+ :disabled="exceedsVariableLimit"
@click="setSelectedVariable()"
>{{ __('Add variable') }}</gl-button
>
diff --git a/app/assets/javascripts/ci_variable_list/constants.js b/app/assets/javascripts/ci_variable_list/constants.js
index ccad08ef8b6..828d0724d93 100644
--- a/app/assets/javascripts/ci_variable_list/constants.js
+++ b/app/assets/javascripts/ci_variable_list/constants.js
@@ -1,4 +1,4 @@
-import { __ } from '~/locale';
+import { __, s__ } from '~/locale';
export const ADD_CI_VARIABLE_MODAL_ID = 'add-ci-variable';
@@ -43,6 +43,7 @@ export const defaultVariableState = {
key: '',
masked: false,
protected: false,
+ raw: false,
value: '',
variableType: variableTypes.envType,
};
@@ -69,10 +70,19 @@ 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 = __(
- 'Values that contain the %{codeStart}$%{codeEnd} character can be considered a variable reference and expanded. %{docsLinkStart}Learn more.%{docsLinkEnd}',
+ 'Unselect "Expand variable reference" if you want to use the variable value as a raw string.',
);
export const ENVIRONMENT_SCOPE_LINK_TITLE = __('Learn more');
+export const EXCEEDS_VARIABLE_LIMIT_TEXT = s__(
+ 'CiVariables|This %{entity} has %{currentVariableCount} defined CI/CD variables. The maximum number of variables per %{entity} is %{maxVariableLimit}. To add new variables, you must reduce the number of defined variables.',
+);
+export const DEFAULT_EXCEEDS_VARIABLE_LIMIT_TEXT = s__(
+ 'CiVariables|You have reached the maximum number of variables available. To add new variables, you must reduce the number of defined variables.',
+);
+export const MAXIMUM_VARIABLE_LIMIT_REACHED = s__(
+ 'CiVariables|Maximum number of variables reached.',
+);
export const ADD_VARIABLE_ACTION = 'ADD_VARIABLE';
export const EDIT_VARIABLE_ACTION = 'EDIT_VARIABLE';
@@ -85,6 +95,10 @@ export const ADD_MUTATION_ACTION = 'add';
export const UPDATE_MUTATION_ACTION = 'update';
export const DELETE_MUTATION_ACTION = 'delete';
+export const EXPANDED_VARIABLES_NOTE = __(
+ '%{codeStart}$%{codeEnd} will be treated as the start of a reference to another variable.',
+);
+
export const environmentFetchErrorText = __(
'There was an error fetching the environments information.',
);
diff --git a/app/assets/javascripts/ci_variable_list/graphql/mutations/group_add_variable.mutation.graphql b/app/assets/javascripts/ci_variable_list/graphql/mutations/group_add_variable.mutation.graphql
index c44ee2ecc1d..24388637672 100644
--- a/app/assets/javascripts/ci_variable_list/graphql/mutations/group_add_variable.mutation.graphql
+++ b/app/assets/javascripts/ci_variable_list/graphql/mutations/group_add_variable.mutation.graphql
@@ -16,6 +16,7 @@ mutation addGroupVariable($variable: CiVariable!, $endpoint: String!, $fullPath:
environmentScope
masked
protected
+ raw
}
}
}
diff --git a/app/assets/javascripts/ci_variable_list/graphql/mutations/group_delete_variable.mutation.graphql b/app/assets/javascripts/ci_variable_list/graphql/mutations/group_delete_variable.mutation.graphql
index 53e9b411dd2..f7c8e209ccd 100644
--- a/app/assets/javascripts/ci_variable_list/graphql/mutations/group_delete_variable.mutation.graphql
+++ b/app/assets/javascripts/ci_variable_list/graphql/mutations/group_delete_variable.mutation.graphql
@@ -16,6 +16,7 @@ mutation deleteGroupVariable($variable: CiVariable!, $endpoint: String!, $fullPa
environmentScope
masked
protected
+ raw
}
}
}
diff --git a/app/assets/javascripts/ci_variable_list/graphql/mutations/group_update_variable.mutation.graphql b/app/assets/javascripts/ci_variable_list/graphql/mutations/group_update_variable.mutation.graphql
index 2dddca14bd8..757e61a5cd3 100644
--- a/app/assets/javascripts/ci_variable_list/graphql/mutations/group_update_variable.mutation.graphql
+++ b/app/assets/javascripts/ci_variable_list/graphql/mutations/group_update_variable.mutation.graphql
@@ -16,6 +16,7 @@ mutation updateGroupVariable($variable: CiVariable!, $endpoint: String!, $fullPa
environmentScope
masked
protected
+ raw
}
}
}
diff --git a/app/assets/javascripts/ci_variable_list/graphql/mutations/project_add_variable.mutation.graphql b/app/assets/javascripts/ci_variable_list/graphql/mutations/project_add_variable.mutation.graphql
index 39504770e33..fa315084d86 100644
--- a/app/assets/javascripts/ci_variable_list/graphql/mutations/project_add_variable.mutation.graphql
+++ b/app/assets/javascripts/ci_variable_list/graphql/mutations/project_add_variable.mutation.graphql
@@ -16,6 +16,7 @@ mutation addProjectVariable($variable: CiVariable!, $endpoint: String!, $fullPat
environmentScope
masked
protected
+ raw
}
}
}
diff --git a/app/assets/javascripts/ci_variable_list/graphql/mutations/project_delete_variable.mutation.graphql b/app/assets/javascripts/ci_variable_list/graphql/mutations/project_delete_variable.mutation.graphql
index f55c255e332..c3358cc35b9 100644
--- a/app/assets/javascripts/ci_variable_list/graphql/mutations/project_delete_variable.mutation.graphql
+++ b/app/assets/javascripts/ci_variable_list/graphql/mutations/project_delete_variable.mutation.graphql
@@ -21,6 +21,7 @@ mutation deleteProjectVariable(
environmentScope
masked
protected
+ raw
}
}
}
diff --git a/app/assets/javascripts/ci_variable_list/graphql/mutations/project_update_variable.mutation.graphql b/app/assets/javascripts/ci_variable_list/graphql/mutations/project_update_variable.mutation.graphql
index fc589e8a939..fde92cef4cb 100644
--- a/app/assets/javascripts/ci_variable_list/graphql/mutations/project_update_variable.mutation.graphql
+++ b/app/assets/javascripts/ci_variable_list/graphql/mutations/project_update_variable.mutation.graphql
@@ -21,6 +21,7 @@ mutation updateProjectVariable(
environmentScope
masked
protected
+ raw
}
}
}
diff --git a/app/assets/javascripts/ci_variable_list/graphql/queries/group_variables.query.graphql b/app/assets/javascripts/ci_variable_list/graphql/queries/group_variables.query.graphql
index b5555fe4401..900154cd24d 100644
--- a/app/assets/javascripts/ci_variable_list/graphql/queries/group_variables.query.graphql
+++ b/app/assets/javascripts/ci_variable_list/graphql/queries/group_variables.query.graphql
@@ -5,6 +5,7 @@ query getGroupVariables($after: String, $first: Int = 100, $fullPath: ID!) {
group(fullPath: $fullPath) {
id
ciVariables(after: $after, first: $first) {
+ limit
pageInfo {
...PageInfo
}
@@ -14,6 +15,7 @@ query getGroupVariables($after: String, $first: Int = 100, $fullPath: ID!) {
environmentScope
masked
protected
+ raw
}
}
}
diff --git a/app/assets/javascripts/ci_variable_list/graphql/queries/project_variables.query.graphql b/app/assets/javascripts/ci_variable_list/graphql/queries/project_variables.query.graphql
index 08b5bf7af16..ee75eba7547 100644
--- a/app/assets/javascripts/ci_variable_list/graphql/queries/project_variables.query.graphql
+++ b/app/assets/javascripts/ci_variable_list/graphql/queries/project_variables.query.graphql
@@ -5,6 +5,7 @@ query getProjectVariables($after: String, $first: Int = 100, $fullPath: ID!) {
project(fullPath: $fullPath) {
id
ciVariables(after: $after, first: $first) {
+ limit
pageInfo {
...PageInfo
}
@@ -13,6 +14,7 @@ query getProjectVariables($after: String, $first: Int = 100, $fullPath: ID!) {
environmentScope
masked
protected
+ raw
}
}
}
diff --git a/app/assets/javascripts/ci_variable_list/graphql/queries/variables.query.graphql b/app/assets/javascripts/ci_variable_list/graphql/queries/variables.query.graphql
index 2667d6606fe..9b255c3c182 100644
--- a/app/assets/javascripts/ci_variable_list/graphql/queries/variables.query.graphql
+++ b/app/assets/javascripts/ci_variable_list/graphql/queries/variables.query.graphql
@@ -11,6 +11,7 @@ query getVariables($after: String, $first: Int = 100) {
... on CiInstanceVariable {
masked
protected
+ raw
}
}
}
diff --git a/app/assets/javascripts/clusters_list/clusters_util.js b/app/assets/javascripts/clusters_list/clusters_util.js
index ee36a295513..25a8426500e 100644
--- a/app/assets/javascripts/clusters_list/clusters_util.js
+++ b/app/assets/javascripts/clusters_list/clusters_util.js
@@ -1,10 +1,14 @@
-import { ACTIVE_CONNECTION_TIME } from './constants';
+import { ACTIVE_CONNECTION_TIME, NAME_MAX_LENGTH } from './constants';
+
+function getTruncatedName(name) {
+ return name.substring(0, NAME_MAX_LENGTH);
+}
export function generateAgentRegistrationCommand({ name, token, version, address }) {
return `helm repo add gitlab https://charts.gitlab.io
helm repo update
helm upgrade --install ${name} gitlab/gitlab-agent \\
- --namespace gitlab-agent \\
+ --namespace gitlab-agent-${getTruncatedName(name)} \\
--create-namespace \\
--set image.tag=v${version} \\
--set config.token=${token} \\
diff --git a/app/assets/javascripts/clusters_list/components/agent_token.vue b/app/assets/javascripts/clusters_list/components/agent_token.vue
index 4dd6d84566c..93c37226a09 100644
--- a/app/assets/javascripts/clusters_list/components/agent_token.vue
+++ b/app/assets/javascripts/clusters_list/components/agent_token.vue
@@ -1,22 +1,24 @@
<script>
-import { GlAlert, GlFormInputGroup, GlLink, GlSprintf } from '@gitlab/ui';
+import { GlAlert, GlFormInputGroup, GlLink, GlSprintf, GlIcon } from '@gitlab/ui';
import { helpPagePath } from '~/helpers/help_page_helper';
import ModalCopyButton from '~/vue_shared/components/modal_copy_button.vue';
import CodeBlock from '~/vue_shared/components/code_block.vue';
import { generateAgentRegistrationCommand } from '../clusters_util';
-import { I18N_AGENT_TOKEN } from '../constants';
+import { I18N_AGENT_TOKEN, HELM_VERSION_POLICY_URL } from '../constants';
export default {
i18n: I18N_AGENT_TOKEN,
advancedInstallPath: helpPagePath('user/clusters/agent/install/index', {
anchor: 'advanced-installation-method',
}),
+ HELM_VERSION_POLICY_URL,
components: {
GlAlert,
CodeBlock,
GlFormInputGroup,
GlLink,
GlSprintf,
+ GlIcon,
ModalCopyButton,
},
inject: ['kasAddress', 'kasVersion'],
@@ -77,6 +79,11 @@ export default {
<p>
{{ $options.i18n.basicInstallBody }}
+ <gl-sprintf :message="$options.i18n.helmVersionText">
+ <template #link="{ content }"
+ ><gl-link :href="$options.HELM_VERSION_POLICY_URL" target="_blank"
+ >{{ content }} <gl-icon name="external-link" :size="12" /></gl-link></template
+ ></gl-sprintf>
</p>
<p class="gl-display-flex gl-align-items-flex-start">
diff --git a/app/assets/javascripts/clusters_list/components/available_agents_dropdown.vue b/app/assets/javascripts/clusters_list/components/available_agents_dropdown.vue
index bde76c46b4b..365e0384d87 100644
--- a/app/assets/javascripts/clusters_list/components/available_agents_dropdown.vue
+++ b/app/assets/javascripts/clusters_list/components/available_agents_dropdown.vue
@@ -1,23 +1,13 @@
<script>
-import {
- GlDropdown,
- GlDropdownItem,
- GlDropdownDivider,
- GlDropdownText,
- GlSearchBoxByType,
- GlSprintf,
-} from '@gitlab/ui';
+import { GlCollapsibleListbox, GlButton, GlSprintf } from '@gitlab/ui';
import { I18N_AVAILABLE_AGENTS_DROPDOWN } from '../constants';
export default {
name: 'AvailableAgentsDropdown',
i18n: I18N_AVAILABLE_AGENTS_DROPDOWN,
components: {
- GlDropdown,
- GlDropdownItem,
- GlDropdownDivider,
- GlDropdownText,
- GlSearchBoxByType,
+ GlCollapsibleListbox,
+ GlButton,
GlSprintf,
},
props: {
@@ -46,13 +36,21 @@ export default {
return this.selectedAgent;
},
+ dropdownItems() {
+ return this.availableAgents.map((agent) => {
+ return {
+ value: agent,
+ text: agent,
+ };
+ });
+ },
shouldRenderCreateButton() {
return this.searchTerm && !this.availableAgents.includes(this.searchTerm);
},
filteredResults() {
const lowerCasedSearchTerm = this.searchTerm.toLowerCase();
- return this.availableAgents.filter((resultString) =>
- resultString.toLowerCase().includes(lowerCasedSearchTerm),
+ return this.dropdownItems.filter((item) =>
+ item.value.toLowerCase().includes(lowerCasedSearchTerm),
);
},
},
@@ -60,59 +58,48 @@ export default {
selectAgent(agent) {
this.$emit('agentSelected', agent);
this.selectedAgent = agent;
- this.clearSearch();
- },
- isSelected(agent) {
- return this.selectedAgent === agent;
- },
- clearSearch() {
- this.searchTerm = '';
- },
- focusSearch() {
- this.$refs.searchInput.focusInput();
- },
- handleShow() {
- this.clearSearch();
- this.focusSearch();
+
+ this.$refs.dropdown.closeAndFocus();
},
onKeyEnter() {
if (!this.searchTerm?.length) {
return;
}
- this.$refs.dropdown.hide();
this.selectAgent(this.searchTerm);
},
+ searchAgent(searchQuery) {
+ this.searchTerm = searchQuery;
+ },
},
};
</script>
<template>
- <gl-dropdown ref="dropdown" :text="dropdownText" :loading="isRegistering" @shown="handleShow">
- <template #header>
- <gl-search-box-by-type
- ref="searchInput"
- v-model.trim="searchTerm"
- @keydown.enter.stop.prevent="onKeyEnter"
- />
- </template>
- <gl-dropdown-item
- v-for="agent in filteredResults"
- :key="agent"
- :is-checked="isSelected(agent)"
- is-check-item
- @click="selectAgent(agent)"
+ <div @keydown.enter.stop.prevent="onKeyEnter">
+ <gl-collapsible-listbox
+ ref="dropdown"
+ v-model="selectedAgent"
+ class="gl-w-full"
+ toggle-class="select-agent-dropdown"
+ :items="filteredResults"
+ :toggle-text="dropdownText"
+ :loading="isRegistering"
+ :searchable="true"
+ :no-results-text="$options.i18n.noResults"
+ @search="searchAgent"
+ @select="selectAgent"
>
- {{ agent }}
- </gl-dropdown-item>
- <gl-dropdown-text v-if="!filteredResults.length" ref="noMatchingResults">{{
- $options.i18n.noResults
- }}</gl-dropdown-text>
- <template v-if="shouldRenderCreateButton">
- <gl-dropdown-divider />
- <gl-dropdown-item data-testid="create-config-button" @click="selectAgent(searchTerm)">
- <gl-sprintf :message="$options.i18n.createButton">
- <template #searchTerm>{{ searchTerm }}</template>
- </gl-sprintf>
- </gl-dropdown-item>
- </template>
- </gl-dropdown>
+ <template v-if="shouldRenderCreateButton" #footer>
+ <gl-button
+ category="tertiary"
+ class="gl-justify-content-start! gl-border-t-1! gl-border-t-solid gl-border-t-gray-200 gl-pl-7! gl-rounded-top-left-none! gl-rounded-top-right-none!"
+ :class="{ 'gl-mt-3': !filteredResults.length }"
+ @click="selectAgent(searchTerm)"
+ >
+ <gl-sprintf :message="$options.i18n.createButton">
+ <template #searchTerm>{{ searchTerm }}</template>
+ </gl-sprintf>
+ </gl-button>
+ </template>
+ </gl-collapsible-listbox>
+ </div>
</template>
diff --git a/app/assets/javascripts/clusters_list/constants.js b/app/assets/javascripts/clusters_list/constants.js
index 7bc8a1a7304..615754459d6 100644
--- a/app/assets/javascripts/clusters_list/constants.js
+++ b/app/assets/javascripts/clusters_list/constants.js
@@ -4,6 +4,7 @@ import { helpPagePath } from '~/helpers/help_page_helper';
export const MAX_LIST_COUNT = 25;
export const INSTALL_AGENT_MODAL_ID = 'install-agent';
export const ACTIVE_CONNECTION_TIME = 480000;
+export const NAME_MAX_LENGTH = 50;
export const CLUSTER_ERRORS = {
default: {
@@ -100,6 +101,9 @@ export const I18N_AGENT_TOKEN = {
basicInstallBody: s__(
'ClusterAgents|From a terminal, connect to your cluster and run this command. The token is included in the command.',
),
+ helmVersionText: s__(
+ 'ClusterAgents|Use a Helm version compatible with your Kubernetes version (see %{linkStart}Helm version support policy%{linkEnd}).',
+ ),
advancedInstallTitle: s__('ClusterAgents|Advanced installation methods'),
advancedInstallBody: s__(
@@ -107,6 +111,8 @@ export const I18N_AGENT_TOKEN = {
),
};
+export const HELM_VERSION_POLICY_URL = 'https://helm.sh/docs/topics/version_skew/';
+
export const I18N_AGENT_MODAL = {
registerAgentButton: s__('ClusterAgents|Register'),
close: __('Close'),
diff --git a/app/assets/javascripts/constants.js b/app/assets/javascripts/constants.js
new file mode 100644
index 00000000000..c56d45166a0
--- /dev/null
+++ b/app/assets/javascripts/constants.js
@@ -0,0 +1,3 @@
+import { s__ } from '~/locale';
+
+export const MODIFIER_KEY = window.gl?.client?.isMac ? '⌘' : s__('KeyboardKey|Ctrl+');
diff --git a/app/assets/javascripts/content_editor/components/bubble_menus/code_block_bubble_menu.vue b/app/assets/javascripts/content_editor/components/bubble_menus/code_block_bubble_menu.vue
index a9668ebdb69..98b7203778f 100644
--- a/app/assets/javascripts/content_editor/components/bubble_menus/code_block_bubble_menu.vue
+++ b/app/assets/javascripts/content_editor/components/bubble_menus/code_block_bubble_menu.vue
@@ -166,9 +166,7 @@ export default {
icon="arrow-left"
@click.prevent.stop="showCustomLanguageInput = false"
/>
- <p
- class="gl-text-center gl-new-dropdown-header-top gl-mb-0! gl-border-none! gl-pb-1!"
- >
+ <p class="gl-text-center gl-dropdown-header-top gl-mb-0! gl-border-none! gl-pb-1!">
{{ __('Create custom type') }}
</p>
</div>
diff --git a/app/assets/javascripts/content_editor/components/content_editor.vue b/app/assets/javascripts/content_editor/components/content_editor.vue
index 22381377389..53a37fc0c51 100644
--- a/app/assets/javascripts/content_editor/components/content_editor.vue
+++ b/app/assets/javascripts/content_editor/components/content_editor.vue
@@ -11,7 +11,7 @@ import FormattingBubbleMenu from './bubble_menus/formatting_bubble_menu.vue';
import CodeBlockBubbleMenu from './bubble_menus/code_block_bubble_menu.vue';
import LinkBubbleMenu from './bubble_menus/link_bubble_menu.vue';
import MediaBubbleMenu from './bubble_menus/media_bubble_menu.vue';
-import TopToolbar from './top_toolbar.vue';
+import FormattingToolbar from './formatting_toolbar.vue';
import LoadingIndicator from './loading_indicator.vue';
export default {
@@ -20,7 +20,7 @@ export default {
ContentEditorAlert,
ContentEditorProvider,
TiptapEditorContent,
- TopToolbar,
+ FormattingToolbar,
FormattingBubbleMenu,
CodeBlockBubbleMenu,
LinkBubbleMenu,
@@ -57,6 +57,11 @@ export default {
default: false,
validator: (autofocus) => TIPTAP_AUTOFOCUS_OPTIONS.includes(autofocus),
},
+ useBottomToolbar: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
},
data() {
return {
@@ -163,8 +168,8 @@ export default {
class="md-area"
:class="{ 'is-focused': focused }"
>
- <top-toolbar ref="toolbar" class="gl-mb-4" />
- <div class="gl-relative">
+ <formatting-toolbar v-if="!useBottomToolbar" ref="toolbar" class="gl-border-b" />
+ <div class="gl-relative gl-mt-4">
<formatting-bubble-menu />
<code-block-bubble-menu />
<link-bubble-menu />
@@ -176,6 +181,7 @@ export default {
/>
<loading-indicator v-if="isLoading" />
</div>
+ <formatting-toolbar v-if="useBottomToolbar" ref="toolbar" class="gl-border-t" />
</div>
</div>
</content-editor-provider>
diff --git a/app/assets/javascripts/content_editor/components/top_toolbar.vue b/app/assets/javascripts/content_editor/components/formatting_toolbar.vue
index 460368b6a11..8a25ad3fd96 100644
--- a/app/assets/javascripts/content_editor/components/top_toolbar.vue
+++ b/app/assets/javascripts/content_editor/components/formatting_toolbar.vue
@@ -24,9 +24,7 @@ export default {
};
</script>
<template>
- <div
- class="gl-display-flex gl-flex-wrap gl-pb-3 gl-pt-3 gl-border-b-solid gl-border-b-1 gl-border-b-gray-200"
- >
+ <div class="gl-display-flex gl-flex-wrap gl-pb-3 gl-pt-3">
<toolbar-text-style-dropdown
data-testid="text-styles"
class="gl-mr-3"
diff --git a/app/assets/javascripts/content_editor/components/suggestions_dropdown.vue b/app/assets/javascripts/content_editor/components/suggestions_dropdown.vue
index 001b34a00fa..37e6ef61d50 100644
--- a/app/assets/javascripts/content_editor/components/suggestions_dropdown.vue
+++ b/app/assets/javascripts/content_editor/components/suggestions_dropdown.vue
@@ -210,10 +210,10 @@ export default {
<template>
<ul
:class="{ show: items.length > 0 }"
- class="gl-new-dropdown dropdown-menu gl-relative"
+ class="gl-dropdown dropdown-menu gl-relative"
data-testid="content-editor-suggestions-dropdown"
>
- <div class="gl-new-dropdown-inner gl-overflow-y-auto">
+ <div class="gl-dropdown-inner gl-overflow-y-auto">
<gl-dropdown-item
v-for="(item, index) in items"
ref="dropdownItems"
diff --git a/app/assets/javascripts/content_editor/components/toolbar_more_dropdown.vue b/app/assets/javascripts/content_editor/components/toolbar_more_dropdown.vue
index 6bb122153ef..93b31ea7d20 100644
--- a/app/assets/javascripts/content_editor/components/toolbar_more_dropdown.vue
+++ b/app/assets/javascripts/content_editor/components/toolbar_more_dropdown.vue
@@ -58,6 +58,9 @@ export default {
right
lazy
>
+ <gl-dropdown-item @click="insert('comment')">
+ {{ __('Comment') }}
+ </gl-dropdown-item>
<gl-dropdown-item @click="insert('codeBlock')">
{{ __('Code block') }}
</gl-dropdown-item>
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 27432b1e18b..1d85bfcc965 100644
--- a/app/assets/javascripts/content_editor/extensions/code_block_highlight.js
+++ b/app/assets/javascripts/content_editor/extensions/code_block_highlight.js
@@ -23,6 +23,10 @@ export default CodeBlockLowlight.extend({
// eslint-disable-next-line @gitlab/require-i18n-strings
default: 'code highlight',
},
+ langParams: {
+ default: null,
+ parseHTML: (element) => element.dataset.langParams,
+ },
};
},
addInputRules() {
diff --git a/app/assets/javascripts/content_editor/extensions/comment.js b/app/assets/javascripts/content_editor/extensions/comment.js
new file mode 100644
index 00000000000..8e247e552a3
--- /dev/null
+++ b/app/assets/javascripts/content_editor/extensions/comment.js
@@ -0,0 +1,49 @@
+import { Node, textblockTypeInputRule } from '@tiptap/core';
+
+export const commentInputRegex = /^<!--[\s\n]$/;
+
+export default Node.create({
+ name: 'comment',
+ content: 'text*',
+ marks: '',
+ group: 'block',
+ code: true,
+ isolating: true,
+ defining: true,
+
+ parseHTML() {
+ return [
+ {
+ tag: 'comment',
+ preserveWhitespace: 'full',
+ getContent(element, schema) {
+ const node = schema.node('paragraph', {}, [
+ schema.text(
+ element.textContent.replace(/&#x([0-9A-F]{2,4});/gi, (_, code) =>
+ String.fromCharCode(parseInt(code, 16)),
+ ) || ' ',
+ ),
+ ]);
+ return node.content;
+ },
+ },
+ ];
+ },
+
+ renderHTML() {
+ return [
+ 'pre',
+ { class: 'gl-p-0 gl-border-0 gl-bg-transparent gl-text-gray-300' },
+ ['span', { class: 'content-editor-comment' }, 0],
+ ];
+ },
+
+ addInputRules() {
+ return [
+ textblockTypeInputRule({
+ find: commentInputRegex,
+ type: this.type,
+ }),
+ ];
+ },
+});
diff --git a/app/assets/javascripts/content_editor/extensions/image.js b/app/assets/javascripts/content_editor/extensions/image.js
index 65849ec4d0d..fc4c108b773 100644
--- a/app/assets/javascripts/content_editor/extensions/image.js
+++ b/app/assets/javascripts/content_editor/extensions/image.js
@@ -52,6 +52,22 @@ export default Image.extend({
return img.getAttribute('title');
},
},
+ width: {
+ default: null,
+ parseHTML: (element) => {
+ const img = resolveImageEl(element);
+
+ return img.getAttribute('width');
+ },
+ },
+ height: {
+ default: null,
+ parseHTML: (element) => {
+ const img = resolveImageEl(element);
+
+ return img.getAttribute('height');
+ },
+ },
isReference: {
default: false,
renderHTML: () => '',
@@ -76,6 +92,8 @@ export default Image.extend({
src: HTMLAttributes.src,
alt: HTMLAttributes.alt,
title: HTMLAttributes.title,
+ width: HTMLAttributes.width,
+ height: HTMLAttributes.height,
},
];
},
diff --git a/app/assets/javascripts/content_editor/extensions/reference_label.js b/app/assets/javascripts/content_editor/extensions/reference_label.js
index 716e191c3d5..9dff0b7a689 100644
--- a/app/assets/javascripts/content_editor/extensions/reference_label.js
+++ b/app/assets/javascripts/content_editor/extensions/reference_label.js
@@ -1,5 +1,5 @@
import { VueNodeViewRenderer } from '@tiptap/vue-2';
-import { SCOPED_LABEL_DELIMITER } from '~/vue_shared/components/sidebar/labels_select_widget/constants';
+import { SCOPED_LABEL_DELIMITER } from '~/sidebar/components/labels/labels_select_widget/constants';
import LabelWrapper from '../components/wrappers/label.vue';
import Reference from './reference';
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 ba9ce705c62..61c6be574d0 100644
--- a/app/assets/javascripts/content_editor/services/create_content_editor.js
+++ b/app/assets/javascripts/content_editor/services/create_content_editor.js
@@ -10,6 +10,7 @@ import BulletList from '../extensions/bullet_list';
import Code from '../extensions/code';
import CodeBlockHighlight from '../extensions/code_block_highlight';
import ColorChip from '../extensions/color_chip';
+import Comment from '../extensions/comment';
import DescriptionItem from '../extensions/description_item';
import DescriptionList from '../extensions/description_list';
import Details from '../extensions/details';
@@ -100,6 +101,7 @@ export const createContentEditor = ({
BulletList,
Code,
ColorChip,
+ Comment,
CodeBlockHighlight,
DescriptionItem,
DescriptionList,
diff --git a/app/assets/javascripts/content_editor/services/gl_api_markdown_deserializer.js b/app/assets/javascripts/content_editor/services/gl_api_markdown_deserializer.js
index fa46bd9ff81..796dc06ad93 100644
--- a/app/assets/javascripts/content_editor/services/gl_api_markdown_deserializer.js
+++ b/app/assets/javascripts/content_editor/services/gl_api_markdown_deserializer.js
@@ -1,4 +1,5 @@
import { DOMParser as ProseMirrorDOMParser } from 'prosemirror-model';
+import { replaceCommentsWith } from '~/lib/utils/dom_utils';
export default ({ render }) => {
/**
@@ -22,7 +23,9 @@ export default ({ render }) => {
if (!html) return {};
const parser = new DOMParser();
- const { body } = parser.parseFromString(html, 'text/html');
+ const { body } = parser.parseFromString(`<body>${html}</body>`, 'text/html');
+
+ replaceCommentsWith(body, 'comment');
// append original source as a comment that nodes can access
body.append(document.createComment(markdown));
diff --git a/app/assets/javascripts/content_editor/services/markdown_serializer.js b/app/assets/javascripts/content_editor/services/markdown_serializer.js
index 958c27c281a..4e29f85004b 100644
--- a/app/assets/javascripts/content_editor/services/markdown_serializer.js
+++ b/app/assets/javascripts/content_editor/services/markdown_serializer.js
@@ -12,6 +12,7 @@ import DescriptionItem from '../extensions/description_item';
import DescriptionList from '../extensions/description_list';
import Details from '../extensions/details';
import DetailsContent from '../extensions/details_content';
+import Comment from '../extensions/comment';
import Diagram from '../extensions/diagram';
import Emoji from '../extensions/emoji';
import Figure from '../extensions/figure';
@@ -50,6 +51,7 @@ import Text from '../extensions/text';
import Video from '../extensions/video';
import WordBreak from '../extensions/word_break';
import {
+ renderComment,
renderCodeBlock,
renderHardBreak,
renderTable,
@@ -130,6 +132,7 @@ const defaultSerializerConfig = {
}),
[BulletList.name]: preserveUnchanged(renderBulletList),
[CodeBlockHighlight.name]: preserveUnchanged(renderCodeBlock),
+ [Comment.name]: renderComment,
[Diagram.name]: preserveUnchanged(renderCodeBlock),
[DescriptionList.name]: renderHTMLNode('dl', true),
[DescriptionItem.name]: (state, node, parent, index) => {
diff --git a/app/assets/javascripts/content_editor/services/serialization_helpers.js b/app/assets/javascripts/content_editor/services/serialization_helpers.js
index 5c0cb21075a..131c79357bf 100644
--- a/app/assets/javascripts/content_editor/services/serialization_helpers.js
+++ b/app/assets/javascripts/content_editor/services/serialization_helpers.js
@@ -308,7 +308,7 @@ export function renderHardBreak(state, node, parent, index) {
}
export function renderImage(state, node) {
- const { alt, canonicalSrc, src, title, isReference } = node.attrs;
+ const { alt, canonicalSrc, src, title, width, height, isReference } = node.attrs;
if (isString(src) || isString(canonicalSrc)) {
const quotedTitle = title ? ` ${state.quote(title)}` : '';
@@ -316,7 +316,17 @@ export function renderImage(state, node) {
? `[${canonicalSrc}]`
: `(${state.esc(canonicalSrc || src)}${quotedTitle})`;
- state.write(`![${state.esc(alt || '')}]${sourceExpression}`);
+ const sizeAttributes = [];
+ if (width) {
+ sizeAttributes.push(`width=${JSON.stringify(width)}`);
+ }
+ if (height) {
+ sizeAttributes.push(`height=${JSON.stringify(height)}`);
+ }
+
+ const attributes = sizeAttributes.length ? `{${sizeAttributes.join(' ')}}` : '';
+
+ state.write(`![${state.esc(alt || '')}]${sourceExpression}${attributes}`);
}
}
@@ -324,8 +334,19 @@ export function renderPlayable(state, node) {
renderImage(state, node);
}
+export function renderComment(state, node) {
+ state.text('<!--');
+ state.text(node.textContent);
+ state.text('-->');
+ state.closeBlock(node);
+}
+
export function renderCodeBlock(state, node) {
- state.write(`\`\`\`${node.attrs.language || ''}\n`);
+ state.write(
+ `\`\`\`${
+ (node.attrs.language || '') + (node.attrs.langParams ? `:${node.attrs.langParams}` : '')
+ }\n`,
+ );
state.text(node.textContent, false);
state.ensureNewLine();
state.write('```');
diff --git a/app/assets/javascripts/crm/components/form.vue b/app/assets/javascripts/crm/components/crm_form.vue
index ea6a6892bbd..ea6a6892bbd 100644
--- a/app/assets/javascripts/crm/components/form.vue
+++ b/app/assets/javascripts/crm/components/crm_form.vue
diff --git a/app/assets/javascripts/crm/contacts/components/contact_form_wrapper.vue b/app/assets/javascripts/crm/contacts/components/contact_form_wrapper.vue
index b29089519e2..a851c7a9e85 100644
--- a/app/assets/javascripts/crm/contacts/components/contact_form_wrapper.vue
+++ b/app/assets/javascripts/crm/contacts/components/contact_form_wrapper.vue
@@ -2,7 +2,7 @@
import { s__, __ } from '~/locale';
import { convertToGraphQLId } from '~/graphql_shared/utils';
import { TYPE_CRM_CONTACT, TYPE_GROUP } from '~/graphql_shared/constants';
-import ContactForm from '../../components/form.vue';
+import CrmForm from '../../components/crm_form.vue';
import getGroupOrganizationsQuery from '../../organizations/components/graphql/get_group_organizations.query.graphql';
import getGroupContactsQuery from './graphql/get_group_contacts.query.graphql';
import createContactMutation from './graphql/create_contact.mutation.graphql';
@@ -10,7 +10,7 @@ import updateContactMutation from './graphql/update_contact.mutation.graphql';
export default {
components: {
- ContactForm,
+ CrmForm,
},
inject: ['groupFullPath', 'groupId'],
props: {
@@ -111,7 +111,7 @@ export default {
</script>
<template>
- <contact-form
+ <crm-form
:drawer-open="true"
:get-query="getQuery"
get-query-node-path="group.contacts"
diff --git a/app/assets/javascripts/crm/organizations/components/organization_form_wrapper.vue b/app/assets/javascripts/crm/organizations/components/organization_form_wrapper.vue
index 32900d45f22..01bff4b69d6 100644
--- a/app/assets/javascripts/crm/organizations/components/organization_form_wrapper.vue
+++ b/app/assets/javascripts/crm/organizations/components/organization_form_wrapper.vue
@@ -2,14 +2,14 @@
import { s__, __ } from '~/locale';
import { convertToGraphQLId } from '~/graphql_shared/utils';
import { TYPE_CRM_ORGANIZATION, TYPE_GROUP } from '~/graphql_shared/constants';
-import OrganizationForm from '../../components/form.vue';
+import CrmForm from '../../components/crm_form.vue';
import getGroupOrganizationsQuery from './graphql/get_group_organizations.query.graphql';
import createOrganizationMutation from './graphql/create_organization.mutation.graphql';
import updateOrganizationMutation from './graphql/update_organization.mutation.graphql';
export default {
components: {
- OrganizationForm,
+ CrmForm,
},
inject: ['groupFullPath', 'groupId'],
props: {
@@ -73,7 +73,7 @@ export default {
</script>
<template>
- <organization-form
+ <crm-form
:drawer-open="true"
:get-query="getQuery"
get-query-node-path="group.organizations"
diff --git a/app/assets/javascripts/custom_metrics/components/custom_metrics_form_fields.vue b/app/assets/javascripts/custom_metrics/components/custom_metrics_form_fields.vue
index 411e482b0ce..c6aeb6c726d 100644
--- a/app/assets/javascripts/custom_metrics/components/custom_metrics_form_fields.vue
+++ b/app/assets/javascripts/custom_metrics/components/custom_metrics_form_fields.vue
@@ -185,7 +185,6 @@ export default {
name="prometheus_metric[title]"
class="form-control"
:placeholder="s__('Metrics|e.g. Throughput')"
- data-qa-selector="custom_metric_prometheus_title_field"
required
/>
<span class="form-text text-muted">{{ s__('Metrics|Used as a title for the chart') }}</span>
@@ -209,7 +208,6 @@ export default {
<gl-form-input
id="prometheus_metric_query"
v-model.trim="query"
- data-qa-selector="custom_metric_prometheus_query_field"
name="prometheus_metric[query]"
class="form-control"
:placeholder="s__('Metrics|e.g. rate(http_requests_total[5m])')"
@@ -247,7 +245,6 @@ export default {
<gl-form-input
id="prometheus_metric_y_label"
v-model="yLabel"
- data-qa-selector="custom_metric_prometheus_y_label_field"
name="prometheus_metric[y_label]"
class="form-control"
:placeholder="s__('Metrics|e.g. Requests/second')"
@@ -267,7 +264,6 @@ export default {
<gl-form-input
id="prometheus_metric_unit"
v-model="unit"
- data-qa-selector="custom_metric_prometheus_unit_label_field"
name="prometheus_metric[unit]"
class="form-control"
:placeholder="s__('Metrics|e.g. req/sec')"
@@ -282,7 +278,6 @@ export default {
<gl-form-input
id="prometheus_metric_legend"
v-model="legend"
- data-qa-selector="custom_metric_prometheus_legend_label_field"
name="prometheus_metric[legend]"
class="form-control"
:placeholder="s__('Metrics|e.g. HTTP requests')"
diff --git a/app/assets/javascripts/deploy_tokens/components/new_deploy_token.vue b/app/assets/javascripts/deploy_tokens/components/new_deploy_token.vue
index 81d74c64124..48ab9ce0a3c 100644
--- a/app/assets/javascripts/deploy_tokens/components/new_deploy_token.vue
+++ b/app/assets/javascripts/deploy_tokens/components/new_deploy_token.vue
@@ -13,27 +13,7 @@ import { createAlert, VARIANT_INFO } from '~/flash';
import axios from '~/lib/utils/axios_utils';
import { formatDate } from '~/lib/utils/datetime_utility';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
-import { s__ } from '~/locale';
-
-function defaultData() {
- return {
- expiresAt: null,
- name: '',
- newTokenDetails: null,
- readRepository: false,
- writeRepository: false,
- readRegistry: false,
- writeRegistry: false,
- readPackageRegistry: false,
- writePackageRegistry: false,
- username: '',
- placeholders: {
- link: { link: ['link_start', 'link_end'] },
- i: { i: ['i_start', 'i_end'] },
- code: { code: ['code_start', 'code_end'] },
- },
- };
-}
+import translations from '../deploy_token_translations';
export default {
components: {
@@ -72,45 +52,9 @@ export default {
},
data() {
- return defaultData();
- },
- translations: {
- addTokenButton: s__('DeployTokens|Create deploy token'),
- addTokenExpiryLabel: s__('DeployTokens|Expiration date (optional)'),
- addTokenExpiryDescription: s__(
- 'DeployTokens|Enter an expiration date for your token. Defaults to never expire.',
- ),
- addTokenHeader: s__('DeployTokens|New deploy token'),
- addTokenDescription: s__(
- 'DeployTokens|Create a new deploy token for all projects in this group. %{link_start}What are deploy tokens?%{link_end}',
- ),
- addTokenNameLabel: s__('DeployTokens|Name'),
- addTokenNameDescription: s__('DeployTokens|Enter a unique name for your deploy token.'),
- addTokenScopesLabel: s__('DeployTokens|Scopes (select at least one)'),
- addTokenUsernameDescription: s__(
- 'DeployTokens|Enter a username for your token. Defaults to %{code_start}gitlab+deploy-token-{n}%{code_end}.',
- ),
- addTokenUsernameLabel: s__('DeployTokens|Username (optional)'),
- newTokenCopyMessage: s__('DeployTokens|Copy deploy token'),
- newProjectTokenCreated: s__('DeployTokens|Your new project deploy token has been created.'),
- newGroupTokenCreated: s__('DeployTokens|Your new group deploy token has been created.'),
- newTokenDescription: s__(
- 'DeployTokens|Use this token as a password. Save it. This password can %{i_start}not%{i_end} be recovered.',
- ),
- newTokenMessage: s__('DeployTokens|Your New Deploy Token'),
- newTokenUsernameCopy: s__('DeployTokens|Copy username'),
- newTokenUsernameDescription: s__(
- 'DeployTokens|This username supports access. %{link_start}What kind of access?%{link_end}',
- ),
- readRepositoryHelp: s__('DeployTokens|Allows read-only access to the repository.'),
- readRegistryHelp: s__('DeployTokens|Allows read-only access to registry images.'),
- writeRegistryHelp: s__('DeployTokens|Allows read and write access to registry images.'),
- readPackageRegistryHelp: s__('DeployTokens|Allows read-only access to the package registry.'),
- writePackageRegistryHelp: s__(
- 'DeployTokens|Allows read and write access to the package registry.',
- ),
- createTokenFailedAlert: s__('DeployTokens|Failed to create a new deployment token'),
+ return this.defaultData();
},
+ translations,
computed: {
formattedExpiryDate() {
return this.expiresAt ? formatDate(this.expiresAt, 'yyyy-mm-dd') : '';
@@ -122,20 +66,78 @@ export default {
},
},
methods: {
+ defaultData() {
+ return {
+ expiresAt: null,
+ name: '',
+ newTokenDetails: null,
+ readRepository: false,
+ writeRepository: false,
+ readRegistry: false,
+ writeRegistry: false,
+ readPackageRegistry: false,
+ writePackageRegistry: false,
+ scopes: [
+ {
+ id: 'deploy_token_read_repository',
+ isShown: true,
+ value: false,
+ helpText: this.$options.translations.readRepositoryHelp,
+ scopeName: 'read_repository',
+ },
+ {
+ id: 'deploy_token_read_registry',
+ isShown: this.$props.containerRegistryEnabled,
+ value: false,
+ helpText: this.$options.translations.readRegistryHelp,
+ scopeName: 'read_registry',
+ },
+ {
+ id: 'deploy_token_write_registry',
+ isShown: this.$props.containerRegistryEnabled,
+ value: false,
+ helpText: this.$options.translations.writeRegistryHelp,
+ scopeName: 'write_registry',
+ },
+ {
+ id: 'deploy_token_read_package_registry',
+ isShown: this.$props.packagesRegistryEnabled,
+ value: false,
+ helpText: this.$options.translations.readPackageRegistryHelp,
+ scopeName: 'read_package_registry',
+ },
+ {
+ id: 'deploy_token_write_package_registry',
+ isShown: this.$props.packagesRegistryEnabled,
+ value: false,
+ helpText: this.$options.translations.writePackageRegistryHelp,
+ scopeName: 'write_package_registry',
+ },
+ ],
+ username: '',
+ placeholders: {
+ link: { link: ['link_start', 'link_end'] },
+ i: { i: ['i_start', 'i_end'] },
+ code: { code: ['code_start', 'code_end'] },
+ },
+ };
+ },
createDeployToken() {
+ const scopes = {};
+ this.scopes.forEach((scope) => {
+ scopes[scope.scopeName] = scope.value;
+ });
+ const body = {
+ deploy_token: {
+ expires_at: this.expiresAt,
+ name: this.name,
+ username: this.username,
+ ...scopes,
+ },
+ };
+
return axios
- .post(this.createNewTokenPath, {
- deploy_token: {
- expires_at: this.expiresAt,
- name: this.name,
- read_repository: this.readRepository,
- read_registry: this.readRegistry,
- write_registry: this.writeRegistry,
- read_package_registry: this.readPackageRegistry,
- write_package_registry: this.writePackageRegistry,
- username: this.username,
- },
- })
+ .post(this.createNewTokenPath, body)
.then((response) => {
this.newTokenDetails = response.data;
this.resetData();
@@ -152,7 +154,7 @@ export default {
});
},
resetData() {
- const newData = defaultData();
+ const newData = this.defaultData();
delete newData.newTokenDetails;
Object.keys(newData).forEach((k) => {
this[k] = newData[k];
@@ -269,55 +271,19 @@ export default {
>
<div id="deploy-token-scopes">
<!-- eslint-disable @gitlab/vue-require-i18n-strings -->
- <gl-form-checkbox
- id="deploy_token_read_repository"
- v-model="readRepository"
- name="deploy_token_read_repository"
- data-qa-selector="deploy_token_read_repository_checkbox"
- >
- read_repository
- <template #help>{{ $options.translations.readRepositoryHelp }}</template>
- </gl-form-checkbox>
- <gl-form-checkbox
- v-if="containerRegistryEnabled"
- id="deploy_token_read_registry"
- v-model="readRegistry"
- name="deploy_token_read_registry"
- data-qa-selector="deploy_token_read_registry_checkbox"
- >
- read_registry
- <template #help>{{ $options.translations.readRegistryHelp }}</template>
- </gl-form-checkbox>
- <gl-form-checkbox
- v-if="containerRegistryEnabled"
- id="deploy_token_write_registry"
- v-model="writeRegistry"
- name="deploy_token_write_registry"
- data-qa-selector="deploy_token_write_registry_checkbox"
- >
- write_registry
- <template #help>{{ $options.translations.writeRegistryHelp }}</template>
- </gl-form-checkbox>
- <gl-form-checkbox
- v-if="packagesRegistryEnabled"
- id="deploy_token_read_package_registry"
- v-model="readPackageRegistry"
- name="deploy_token_read_package_registry"
- data-qa-selector="deploy_token_read_package_registry_checkbox"
- >
- read_package_registry
- <template #help>{{ $options.translations.readPackageRegistryHelp }}</template>
- </gl-form-checkbox>
- <gl-form-checkbox
- v-if="packagesRegistryEnabled"
- id="deploy_token_write_package_registry"
- v-model="writePackageRegistry"
- name="deploy_token_write_package_registry"
- data-qa-selector="deploy_token_write_package_registry_checkbox"
- >
- write_package_registry
- <template #help>{{ $options.translations.writePackageRegistryHelp }}</template>
- </gl-form-checkbox>
+ <template v-for="scope in scopes">
+ <gl-form-checkbox
+ v-if="scope.isShown"
+ :id="scope.id"
+ :key="scope.id"
+ v-model="scope.value"
+ :name="scope.id"
+ :data-qa-selector="`${scope.id}_checkbox`"
+ >
+ {{ scope.scopeName }}
+ <template #help>{{ scope.helpText }}</template>
+ </gl-form-checkbox>
+ </template>
<!-- eslint-enable @gitlab/vue-require-i18n-strings -->
</div>
</gl-form-group>
diff --git a/app/assets/javascripts/deploy_tokens/deploy_token_translations.js b/app/assets/javascripts/deploy_tokens/deploy_token_translations.js
new file mode 100644
index 00000000000..3767e9e6170
--- /dev/null
+++ b/app/assets/javascripts/deploy_tokens/deploy_token_translations.js
@@ -0,0 +1,41 @@
+import { s__ } from '~/locale';
+
+const translations = {
+ addTokenButton: s__('DeployTokens|Create deploy token'),
+ addTokenExpiryLabel: s__('DeployTokens|Expiration date (optional)'),
+ addTokenExpiryDescription: s__(
+ 'DeployTokens|Enter an expiration date for your token. Defaults to never expire.',
+ ),
+ addTokenHeader: s__('DeployTokens|New deploy token'),
+ addTokenDescription: s__(
+ 'DeployTokens|Create a new deploy token for all projects in this group. %{link_start}What are deploy tokens?%{link_end}',
+ ),
+ addTokenNameLabel: s__('DeployTokens|Name'),
+ addTokenNameDescription: s__('DeployTokens|Enter a unique name for your deploy token.'),
+ addTokenScopesLabel: s__('DeployTokens|Scopes (select at least one)'),
+ addTokenUsernameDescription: s__(
+ 'DeployTokens|Enter a username for your token. Defaults to %{code_start}gitlab+deploy-token-{n}%{code_end}.',
+ ),
+ addTokenUsernameLabel: s__('DeployTokens|Username (optional)'),
+ newTokenCopyMessage: s__('DeployTokens|Copy deploy token'),
+ newProjectTokenCreated: s__('DeployTokens|Your new project deploy token has been created.'),
+ newGroupTokenCreated: s__('DeployTokens|Your new group deploy token has been created.'),
+ newTokenDescription: s__(
+ 'DeployTokens|Use this token as a password. Save it. This password can %{i_start}not%{i_end} be recovered.',
+ ),
+ newTokenMessage: s__('DeployTokens|Your New Deploy Token'),
+ newTokenUsernameCopy: s__('DeployTokens|Copy username'),
+ newTokenUsernameDescription: s__(
+ 'DeployTokens|This username supports access. %{link_start}What kind of access?%{link_end}',
+ ),
+ readRepositoryHelp: s__('DeployTokens|Allows read-only access to the repository.'),
+ readRegistryHelp: s__('DeployTokens|Allows read-only access to registry images.'),
+ writeRegistryHelp: s__('DeployTokens|Allows read and write access to registry images.'),
+ readPackageRegistryHelp: s__('DeployTokens|Allows read-only access to the package registry.'),
+ writePackageRegistryHelp: s__(
+ 'DeployTokens|Allows read and write access to the package registry.',
+ ),
+ createTokenFailedAlert: s__('DeployTokens|Failed to create a new deployment token'),
+};
+
+export default translations;
diff --git a/app/assets/javascripts/deprecated_jquery_dropdown/render.js b/app/assets/javascripts/deprecated_jquery_dropdown/render.js
index 0f612989bb4..97698d55011 100644
--- a/app/assets/javascripts/deprecated_jquery_dropdown/render.js
+++ b/app/assets/javascripts/deprecated_jquery_dropdown/render.js
@@ -149,7 +149,7 @@ function renderLink(row, data, { options, group, index }) {
}
function getOptionRenderer({ options, instance }) {
- return options.renderRow && ((li, data) => options.renderRow(data, instance));
+ return options.renderRow && ((li, data, params) => options.renderRow(data, instance, params));
}
function getRenderer(data, params) {
diff --git a/app/assets/javascripts/deprecated_notes.js b/app/assets/javascripts/deprecated_notes.js
index 2ac62b9b927..c090a66a69d 100644
--- a/app/assets/javascripts/deprecated_notes.js
+++ b/app/assets/javascripts/deprecated_notes.js
@@ -15,6 +15,7 @@ import Autosize from 'autosize';
import $ from 'jquery';
import { escape, uniqueId } from 'lodash';
import Vue from 'vue';
+import { createAlert, VARIANT_INFO } from '~/flash';
import '~/lib/utils/jquery_at_who';
import AjaxCache from '~/lib/utils/ajax_cache';
import { loadingIconForLegacyJS } from '~/loading_icon_for_legacy_js';
@@ -24,7 +25,6 @@ import * as constants from '~/notes/constants';
import { confirmAction } from '~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal';
import Autosave from './autosave';
import loadAwardsHandler from './awards_handler';
-import createFlash from './flash';
import { defaultAutocompleteConfig } from './gfm_auto_complete';
import GLForm from './gl_form';
import axios from './lib/utils/axios_utils';
@@ -40,6 +40,7 @@ import { localTimeAgo } from './lib/utils/datetime_utility';
import { getLocationHash } from './lib/utils/url_utility';
import { sprintf, s__, __ } from './locale';
import TaskList from './task_list';
+import '~/behaviors/markdown/init_gfm';
window.autosize = Autosize;
@@ -81,7 +82,7 @@ export default class Notes {
this.keydownNoteText = this.keydownNoteText.bind(this);
this.toggleCommitList = this.toggleCommitList.bind(this);
this.postComment = this.postComment.bind(this);
- this.clearFlashWrapper = this.clearFlash.bind(this);
+ this.clearAlertWrapper = this.clearAlert.bind(this);
this.onHashChange = this.onHashChange.bind(this);
this.notes_url = notes_url;
@@ -431,9 +432,9 @@ export default class Notes {
if (noteEntity.commands_changes && Object.keys(noteEntity.commands_changes).length > 0) {
$notesList.find('.system-note.being-posted').remove();
}
- this.addFlash({
+ this.addAlert({
message: noteEntity.errors.commands_only,
- type: 'notice',
+ variant: VARIANT_INFO,
parent: this.parentTimeline.get(0),
});
this.refresh();
@@ -656,7 +657,7 @@ export default class Notes {
} else if ($form.hasClass('js-discussion-note-form')) {
formParentTimeline = $form.closest('.discussion-notes').find('.notes');
}
- return this.addFlash({
+ return this.addAlert({
message: __(
'Your comment could not be submitted! Please check your network connection and try again.',
),
@@ -665,7 +666,7 @@ export default class Notes {
}
updateNoteError() {
- createFlash({
+ createAlert({
message: __(
'Your comment could not be updated! Please check your network connection and try again.',
),
@@ -1338,15 +1339,12 @@ export default class Notes {
});
}
- addFlash(...flashParams) {
- this.flashContainer = createFlash(...flashParams);
+ addAlert(...alertParams) {
+ this.alert = createAlert(...alertParams);
}
- clearFlash() {
- if (this.flashContainer) {
- this.flashContainer.style.display = 'none';
- this.flashContainer = null;
- }
+ clearAlert() {
+ this.alert?.dismiss();
}
cleanForm($form) {
@@ -1535,7 +1533,7 @@ export default class Notes {
* b. Reset comment form to original state.
* b) If request failed
* 1. Remove placeholder element
- * 2. Show error Flash message about failure
+ * 2. Show error alert message about failure
*/
postComment(e) {
e.preventDefault();
@@ -1645,7 +1643,7 @@ export default class Notes {
}
// Clear previous form errors
- this.clearFlashWrapper();
+ this.clearAlertWrapper();
// Check if this was discussion comment
if (isDiscussionForm) {
diff --git a/app/assets/javascripts/design_management/components/design_notes/design_discussion.vue b/app/assets/javascripts/design_management/components/design_notes/design_discussion.vue
index a4430b15752..3091c6703b4 100644
--- a/app/assets/javascripts/design_management/components/design_notes/design_discussion.vue
+++ b/app/assets/javascripts/design_management/components/design_notes/design_discussion.vue
@@ -4,7 +4,7 @@ import { ApolloMutation } from 'vue-apollo';
import { createAlert } from '~/flash';
import { s__ } from '~/locale';
import ReplyPlaceholder from '~/notes/components/discussion_reply_placeholder.vue';
-import { updateGlobalTodoCount } from '~/vue_shared/components/sidebar/todo_toggle/utils';
+import { updateGlobalTodoCount } from '~/sidebar/utils';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
import DesignNotePin from '~/vue_shared/components/design_management/design_note_pin.vue';
import { isLoggedIn } from '~/lib/utils/common_utils';
diff --git a/app/assets/javascripts/design_management/components/design_notes/design_note.vue b/app/assets/javascripts/design_management/components/design_notes/design_note.vue
index e629f74ba02..af4bf7eb14d 100644
--- a/app/assets/javascripts/design_management/components/design_notes/design_note.vue
+++ b/app/assets/javascripts/design_management/components/design_notes/design_note.vue
@@ -1,13 +1,7 @@
<script>
-import {
- GlAvatar,
- GlAvatarLink,
- GlButton,
- GlLink,
- GlSafeHtmlDirective,
- GlTooltipDirective,
-} from '@gitlab/ui';
+import { GlAvatar, GlAvatarLink, GlButton, GlLink, GlTooltipDirective } from '@gitlab/ui';
import { ApolloMutation } from 'vue-apollo';
+import SafeHtml from '~/vue_shared/directives/safe_html';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { __ } from '~/locale';
import TimelineEntryItem from '~/vue_shared/components/notes/timeline_entry_item.vue';
@@ -33,7 +27,7 @@ export default {
},
directives: {
GlTooltip: GlTooltipDirective,
- SafeHtml: GlSafeHtmlDirective,
+ SafeHtml,
},
props: {
note: {
diff --git a/app/assets/javascripts/design_management/components/design_todo_button.vue b/app/assets/javascripts/design_management/components/design_todo_button.vue
index 013dd1d89f3..a1a23d61093 100644
--- a/app/assets/javascripts/design_management/components/design_todo_button.vue
+++ b/app/assets/javascripts/design_management/components/design_todo_button.vue
@@ -1,6 +1,6 @@
<script>
import todoMarkDoneMutation from '~/graphql_shared/mutations/todo_mark_done.mutation.graphql';
-import TodoButton from '~/vue_shared/components/sidebar/todo_toggle/todo_button.vue';
+import TodoButton from '~/sidebar/components/todo_toggle/todo_button.vue';
import createDesignTodoMutation from '../graphql/mutations/create_design_todo.mutation.graphql';
import getDesignQuery from '../graphql/queries/get_design.query.graphql';
import allVersionsMixin from '../mixins/all_versions';
diff --git a/app/assets/javascripts/design_management/components/upload/design_version_dropdown.vue b/app/assets/javascripts/design_management/components/upload/design_version_dropdown.vue
index f10545faea6..c96487d0d08 100644
--- a/app/assets/javascripts/design_management/components/upload/design_version_dropdown.vue
+++ b/app/assets/javascripts/design_management/components/upload/design_version_dropdown.vue
@@ -1,5 +1,5 @@
<script>
-import { GlDropdown, GlDropdownItem, GlSprintf } from '@gitlab/ui';
+import { GlAvatar, GlCollapsibleListbox } from '@gitlab/ui';
import defaultAvatarUrl from 'images/no_avatar.png';
import { __, sprintf } from '~/locale';
import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue';
@@ -8,13 +8,19 @@ import { findVersionId } from '../../utils/design_management_utils';
export default {
components: {
- GlDropdown,
- GlDropdownItem,
- GlSprintf,
+ GlAvatar,
+ GlCollapsibleListbox,
TimeAgo,
},
mixins: [allVersionsMixin],
computed: {
+ allVersionsList() {
+ return this.allVersions.map(({ id, ...item }, index) => ({
+ value: id,
+ index,
+ ...item,
+ }));
+ },
queryVersion() {
return this.$route.query.version;
},
@@ -29,17 +35,11 @@ export default {
// then return the latest version (index 0)
return idx !== -1 ? idx : 0;
},
- currentVersionId() {
- if (this.queryVersion) return this.queryVersion;
-
- const currentVersion = this.allVersions[this.currentVersionIdx];
- return this.findVersionId(currentVersion.id);
- },
dropdownText() {
if (this.isLatestVersion) {
return __('Showing latest version');
}
- // allVersions is sorted in reverse chronological order (latest first)
+ // allVersions is sorted in reverse chronological order (the latest first)
const currentVersionNumber = this.allVersions.length - this.currentVersionIdx;
return sprintf(__('Showing version #%{versionNumber}'), {
@@ -55,47 +55,49 @@ export default {
query: { version: this.findVersionId(versionId) },
});
},
- versionText(versionId) {
- if (this.findVersionId(versionId) === this.latestVersionId) {
- return __('Version %{versionNumber} (latest)');
- }
- return __('Version %{versionNumber}');
+ versionText(item) {
+ const versionNumber = this.allVersions.length - item.index;
+ const message =
+ this.findVersionId(item.value) === this.latestVersionId
+ ? __('Version %{versionNumber} (latest)')
+ : __('Version %{versionNumber}');
+ return sprintf(message, { versionNumber });
},
getAvatarUrl(version) {
return version?.author?.avatarUrl || defaultAvatarUrl;
},
+ getAuthorName(author) {
+ return author?.name;
+ },
},
};
</script>
<template>
- <gl-dropdown :text="dropdownText" size="small">
- <gl-dropdown-item
- v-for="(version, index) in allVersions"
- :key="version.id"
- is-check-item
- is-check-centered
- :is-checked="findVersionId(version.id) === currentVersionId"
- :avatar-url="getAvatarUrl(version)"
- @click="routeToVersion(version.id)"
- >
- <strong>
- <gl-sprintf :message="versionText(version.id)">
- <template #versionNumber>
- {{ allVersions.length - index }}
- </template>
- </gl-sprintf>
- </strong>
-
- <div v-if="version.author" class="gl-text-gray-600 gl-mt-1">
- <div>{{ version.author.name }}</div>
- <time-ago
- v-if="version.createdAt"
- class="text-1"
- :time="version.createdAt"
- tooltip-placement="bottom"
- />
- </div>
- </gl-dropdown-item>
- </gl-dropdown>
+ <gl-collapsible-listbox
+ is-check-centered
+ :items="allVersionsList"
+ :toggle-text="dropdownText"
+ :selected="designsVersion"
+ size="small"
+ @select="routeToVersion"
+ >
+ <template #list-item="{ item }">
+ <span class="gl-display-flex gl-align-items-center">
+ <gl-avatar :alt="getAuthorName(item.author)" :size="32" :src="getAvatarUrl(item)" />
+ <span class="gl-display-flex gl-flex-direction-column">
+ <span class="gl-font-weight-bold">{{ versionText(item) }}</span>
+ <span v-if="item.author" class="gl-text-gray-600 gl-mt-1">
+ <span class="gl-display-block">{{ getAuthorName(item.author) }}</span>
+ <time-ago
+ v-if="item.createdAt"
+ class="text-1"
+ :time="item.createdAt"
+ tooltip-placement="bottom"
+ />
+ </span>
+ </span>
+ </span>
+ </template>
+ </gl-collapsible-listbox>
</template>
diff --git a/app/assets/javascripts/design_management/pages/design/index.vue b/app/assets/javascripts/design_management/pages/design/index.vue
index d4c177e2e5f..f448e2f9e3d 100644
--- a/app/assets/javascripts/design_management/pages/design/index.vue
+++ b/app/assets/javascripts/design_management/pages/design/index.vue
@@ -6,7 +6,7 @@ import { ApolloMutation } from 'vue-apollo';
import { keysFor, ISSUE_CLOSE_DESIGN } from '~/behaviors/shortcuts/keybindings';
import { createAlert } from '~/flash';
import { fetchPolicies } from '~/lib/graphql';
-import { updateGlobalTodoCount } from '~/vue_shared/components/sidebar/todo_toggle/utils';
+import { updateGlobalTodoCount } from '~/sidebar/utils';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import DesignDestroyer from '../../components/design_destroyer.vue';
import DesignReplyForm from '../../components/design_notes/design_reply_form.vue';
diff --git a/app/assets/javascripts/diffs/components/commit_item.vue b/app/assets/javascripts/diffs/components/commit_item.vue
index 5a45797ed98..1857ff557e6 100644
--- a/app/assets/javascripts/diffs/components/commit_item.vue
+++ b/app/assets/javascripts/diffs/components/commit_item.vue
@@ -1,5 +1,6 @@
<script>
-import { GlButtonGroup, GlButton, GlTooltipDirective, GlSafeHtmlDirective } from '@gitlab/ui';
+import { GlButtonGroup, GlButton, GlTooltipDirective } from '@gitlab/ui';
+import SafeHtml from '~/vue_shared/directives/safe_html';
import CommitPipelineStatus from '~/projects/tree/components/commit_pipeline_status_component.vue';
import ModalCopyButton from '~/vue_shared/components/modal_copy_button.vue';
@@ -32,7 +33,7 @@ export default {
},
directives: {
GlTooltip: GlTooltipDirective,
- SafeHtml: GlSafeHtmlDirective,
+ SafeHtml,
},
mixins: [glFeatureFlagsMixin()],
props: {
diff --git a/app/assets/javascripts/diffs/components/diff_code_quality.vue b/app/assets/javascripts/diffs/components/diff_code_quality.vue
index 8498724740f..11aa856619b 100644
--- a/app/assets/javascripts/diffs/components/diff_code_quality.vue
+++ b/app/assets/javascripts/diffs/components/diff_code_quality.vue
@@ -1,8 +1,12 @@
<script>
import { GlButton, GlIcon } from '@gitlab/ui';
-import { SEVERITY_CLASSES, SEVERITY_ICONS } from '~/reports/codequality_report/constants';
+import { SEVERITY_CLASSES, SEVERITY_ICONS } from '~/ci/reports/codequality_report/constants';
+import { NEW_CODE_QUALITY_FINDINGS } from '../i18n';
export default {
+ i18n: {
+ newFindings: NEW_CODE_QUALITY_FINDINGS,
+ },
components: { GlButton, GlIcon },
props: {
codeQuality: {
@@ -22,22 +26,33 @@ export default {
</script>
<template>
- <div data-testid="diff-codequality" class="gl-relative">
- <ul
- class="gl-list-style-none gl-mb-0 gl-p-0 codequality-findings-list gl-border-top-1 gl-border-bottom-1 gl-bg-gray-10"
+ <div
+ data-testid="diff-codequality"
+ class="gl-relative codequality-findings-list gl-border-top-1 gl-border-bottom-1 gl-bg-gray-10 gl-pl-5 gl-pt-4 gl-pb-4"
+ >
+ <h4
+ data-testid="diff-codequality-findings-heading"
+ class="gl-mt-0 gl-mb-0 gl-font-base gl-font-regular"
>
+ {{ $options.i18n.newFindings }}
+ </h4>
+ <ul class="gl-list-style-none gl-mb-0 gl-p-0">
<li
v-for="finding in codeQuality"
:key="finding.description"
- class="gl-pt-1 gl-pb-1 gl-pl-3 gl-border-solid gl-border-bottom-0 gl-border-right-0 gl-border-1 gl-border-gray-100 gl-font-regular"
+ class="gl-pt-1 gl-pb-1 gl-font-regular gl-display-flex"
>
- <gl-icon
- :size="12"
- :name="severityIcon(finding.severity)"
- :class="severityClass(finding.severity)"
- class="codequality-severity-icon"
- />
- {{ finding.description }}
+ <span class="gl-mr-3">
+ <gl-icon
+ :size="12"
+ :name="severityIcon(finding.severity)"
+ :class="severityClass(finding.severity)"
+ class="codequality-severity-icon"
+ />
+ </span>
+ <span>
+ <span class="severity-copy">{{ finding.severity }}</span> - {{ finding.description }}
+ </span>
</li>
</ul>
<gl-button
diff --git a/app/assets/javascripts/diffs/components/diff_discussion_reply.vue b/app/assets/javascripts/diffs/components/diff_discussion_reply.vue
index 3766c125325..8b747aa08dd 100644
--- a/app/assets/javascripts/diffs/components/diff_discussion_reply.vue
+++ b/app/assets/javascripts/diffs/components/diff_discussion_reply.vue
@@ -1,13 +1,18 @@
<script>
+import { GlButton } from '@gitlab/ui';
import { mapGetters } from 'vuex';
-import ReplyPlaceholder from '~/notes/components/discussion_reply_placeholder.vue';
import NoteSignedOutWidget from '~/notes/components/note_signed_out_widget.vue';
+import { START_THREAD } from '../i18n';
+
export default {
name: 'DiffDiscussionReply',
+ i18n: {
+ START_THREAD,
+ },
components: {
+ GlButton,
NoteSignedOutWidget,
- ReplyPlaceholder,
},
props: {
hasForm: {
@@ -34,11 +39,9 @@ export default {
<template v-if="userCanReply">
<slot v-if="hasForm" name="form"></slot>
<template v-else-if="renderReplyPlaceholder">
- <reply-placeholder
- :placeholder-text="__('Start a new discussion…')"
- :label-text="__('New discussion')"
- @focus="$emit('showNewDiscussionForm')"
- />
+ <gl-button @click="$emit('showNewDiscussionForm')">
+ {{ $options.i18n.START_THREAD }}
+ </gl-button>
</template>
</template>
<note-signed-out-widget v-else />
diff --git a/app/assets/javascripts/diffs/components/diff_expansion_cell.vue b/app/assets/javascripts/diffs/components/diff_expansion_cell.vue
index b2098b9e82d..8fcbc4b5cce 100644
--- a/app/assets/javascripts/diffs/components/diff_expansion_cell.vue
+++ b/app/assets/javascripts/diffs/components/diff_expansion_cell.vue
@@ -1,6 +1,7 @@
<script>
-import { GlTooltipDirective, GlSafeHtmlDirective, GlIcon, GlLoadingIcon } from '@gitlab/ui';
+import { GlTooltipDirective, GlIcon, GlLoadingIcon } from '@gitlab/ui';
import { mapActions } from 'vuex';
+import SafeHtml from '~/vue_shared/directives/safe_html';
import { createAlert } from '~/flash';
import { s__, sprintf } from '~/locale';
import { UNFOLD_COUNT, INLINE_DIFF_LINES_KEY } from '../constants';
@@ -21,7 +22,7 @@ export default {
},
directives: {
GlTooltip: GlTooltipDirective,
- SafeHtml: GlSafeHtmlDirective,
+ SafeHtml,
},
props: {
file: {
diff --git a/app/assets/javascripts/diffs/components/diff_file.vue b/app/assets/javascripts/diffs/components/diff_file.vue
index 8f041d1e670..564f776edd2 100644
--- a/app/assets/javascripts/diffs/components/diff_file.vue
+++ b/app/assets/javascripts/diffs/components/diff_file.vue
@@ -1,13 +1,8 @@
<script>
-import {
- GlButton,
- GlLoadingIcon,
- GlSafeHtmlDirective as SafeHtml,
- GlSprintf,
- GlAlert,
-} from '@gitlab/ui';
+import { GlButton, GlLoadingIcon, GlSprintf, GlAlert } from '@gitlab/ui';
import { escape } from 'lodash';
import { mapActions, mapGetters, mapState } from 'vuex';
+import SafeHtml from '~/vue_shared/directives/safe_html';
import { IdState } from 'vendor/vue-virtual-scroller';
import DiffContent from 'jh_else_ce/diffs/components/diff_content.vue';
import { createAlert } from '~/flash';
diff --git a/app/assets/javascripts/diffs/components/diff_file_header.vue b/app/assets/javascripts/diffs/components/diff_file_header.vue
index 91c3df39e32..dff61acdfba 100644
--- a/app/assets/javascripts/diffs/components/diff_file_header.vue
+++ b/app/assets/javascripts/diffs/components/diff_file_header.vue
@@ -1,7 +1,6 @@
<script>
import {
GlTooltipDirective,
- GlSafeHtmlDirective,
GlIcon,
GlBadge,
GlButton,
@@ -14,6 +13,7 @@ import {
} from '@gitlab/ui';
import { escape } from 'lodash';
import { mapActions, mapGetters, mapState } from 'vuex';
+import SafeHtml from '~/vue_shared/directives/safe_html';
import { IdState } from 'vendor/vue-virtual-scroller';
import { scrollToElement } from '~/lib/utils/common_utils';
import { truncateSha } from '~/lib/utils/text_utility';
@@ -44,7 +44,7 @@ export default {
},
directives: {
GlTooltip: GlTooltipDirective,
- SafeHtml: GlSafeHtmlDirective,
+ SafeHtml,
},
mixins: [IdState({ idProp: (vm) => vm.diffFile.file_hash }), glFeatureFlagsMixin()],
i18n: {
diff --git a/app/assets/javascripts/diffs/components/diff_view.vue b/app/assets/javascripts/diffs/components/diff_view.vue
index 5ea118afe78..aa9a17d18e3 100644
--- a/app/assets/javascripts/diffs/components/diff_view.vue
+++ b/app/assets/javascripts/diffs/components/diff_view.vue
@@ -1,5 +1,4 @@
<script>
-import { GlSafeHtmlDirective as SafeHtml } from '@gitlab/ui';
import { mapGetters, mapState, mapActions } from 'vuex';
import { IdState } from 'vendor/vue-virtual-scroller';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
@@ -22,9 +21,6 @@ export default {
DiffCommentCell,
DraftNote,
},
- directives: {
- SafeHtml,
- },
mixins: [
draftCommentsMixin,
IdState({ idProp: (vm) => vm.diffFile.file_hash }),
@@ -307,7 +303,11 @@ export default {
class="diff-td notes-content parallel old"
>
<div v-for="draft in lineDrafts(line, 'left')" :key="draft.id" class="content">
- <draft-note :draft="draft" :line="line.left" />
+ <article class="note-wrapper">
+ <ul class="notes draft-notes">
+ <draft-note :draft="draft" :line="line.left" />
+ </ul>
+ </article>
</div>
</div>
<div
@@ -315,7 +315,11 @@ export default {
class="diff-td notes-content parallel new"
>
<div v-for="draft in lineDrafts(line, 'right')" :key="draft.id" class="content">
- <draft-note :draft="draft" :line="line.right" />
+ <article class="note-wrapper">
+ <ul class="notes draft-notes">
+ <draft-note :draft="draft" :line="line.right" />
+ </ul>
+ </article>
</div>
</div>
</div>
diff --git a/app/assets/javascripts/diffs/i18n.js b/app/assets/javascripts/diffs/i18n.js
index f7f4aad3ad0..0f44eb06cb3 100644
--- a/app/assets/javascripts/diffs/i18n.js
+++ b/app/assets/javascripts/diffs/i18n.js
@@ -19,6 +19,7 @@ export const DIFF_FILE = {
autoCollapsed: __('Files with large changes are collapsed by default.'),
expand: __('Expand file'),
};
+export const START_THREAD = __('Start another thread');
export const SETTINGS_DROPDOWN = {
whitespace: __('Show whitespace changes'),
@@ -49,3 +50,5 @@ export const CONFLICT_TEXT = {
};
export const HIDE_COMMENTS = __('Hide comments');
+
+export const NEW_CODE_QUALITY_FINDINGS = __('New code quality findings');
diff --git a/app/assets/javascripts/diffs/index.js b/app/assets/javascripts/diffs/index.js
index b4ff5e4f250..7da5ef54b80 100644
--- a/app/assets/javascripts/diffs/index.js
+++ b/app/assets/javascripts/diffs/index.js
@@ -1,6 +1,7 @@
import Vue from 'vue';
import { mapActions, mapState, mapGetters } from 'vuex';
import { getCookie, parseBoolean, removeCookie } from '~/lib/utils/common_utils';
+import notesStore from '~/mr_notes/stores';
import eventHub from '../notes/event_hub';
import DiffsApp from './components/app.vue';
@@ -9,7 +10,7 @@ import { TREE_LIST_STORAGE_KEY, DIFF_WHITESPACE_COOKIE_NAME } from './constants'
import { getReviewsForMergeRequest } from './utils/file_reviews';
import { getDerivedMergeRequestInformation } from './utils/merge_request';
-export default function initDiffsApp(store) {
+export default function initDiffsApp(store = notesStore) {
const vm = new Vue({
el: '#js-diffs-app',
name: 'MergeRequestDiffs',
diff --git a/app/assets/javascripts/diffs/store/actions.js b/app/assets/javascripts/diffs/store/actions.js
index c73012527a2..96a73917820 100644
--- a/app/assets/javascripts/diffs/store/actions.js
+++ b/app/assets/javascripts/diffs/store/actions.js
@@ -52,7 +52,7 @@ import { isCollapsed } from '../utils/diff_file';
import { markFileReview, setReviewsForMergeRequest } from '../utils/file_reviews';
import { getDerivedMergeRequestInformation } from '../utils/merge_request';
import { queueRedisHllEvents } from '../utils/queue_events';
-import TreeWorker from '../workers/tree_worker';
+import TreeWorker from '../workers/tree_worker?worker';
import * as types from './mutation_types';
import {
getDiffPositionByLineCode,
@@ -444,20 +444,27 @@ export const scrollToLineIfNeededParallel = (_, line) => {
}
};
-export const loadCollapsedDiff = ({ commit, getters, state }, file) =>
- axios
- .get(file.load_collapsed_diff_url, {
- params: {
- commit_id: getters.commitId,
- w: state.showWhitespace ? '0' : '1',
- },
- })
- .then((res) => {
- commit(types.ADD_COLLAPSED_DIFFS, {
- file,
- data: res.data,
- });
+export const loadCollapsedDiff = ({ commit, getters, state }, file) => {
+ const versionPath = state.mergeRequestDiff?.version_path;
+ const loadParams = {
+ commit_id: getters.commitId,
+ w: state.showWhitespace ? '0' : '1',
+ };
+
+ if (versionPath) {
+ const { diffId, startSha } = getDerivedMergeRequestInformation({ endpoint: versionPath });
+
+ loadParams.diff_id = diffId;
+ loadParams.start_sha = startSha;
+ }
+
+ return axios.get(file.load_collapsed_diff_url, { params: loadParams }).then((res) => {
+ commit(types.ADD_COLLAPSED_DIFFS, {
+ file,
+ data: res.data,
});
+ });
+};
/**
* Toggles the file discussions after user clicked on the toggle discussions button.
diff --git a/app/assets/javascripts/diffs/utils/merge_request.js b/app/assets/javascripts/diffs/utils/merge_request.js
index edb4304f558..43e04a814c5 100644
--- a/app/assets/javascripts/diffs/utils/merge_request.js
+++ b/app/assets/javascripts/diffs/utils/merge_request.js
@@ -1,14 +1,30 @@
const endpointRE = /^(\/?(.+?)\/(.+?)\/-\/merge_requests\/(\d+)).*$/i;
+function getVersionInfo({ endpoint } = {}) {
+ const dummyRoot = 'https://gitlab.com';
+ const endpointUrl = new URL(endpoint, dummyRoot);
+ const params = Object.fromEntries(endpointUrl.searchParams.entries());
+
+ const { start_sha: startSha, diff_id: diffId } = params;
+
+ return {
+ diffId,
+ startSha,
+ };
+}
+
export function getDerivedMergeRequestInformation({ endpoint } = {}) {
let mrPath;
let userOrGroup;
let project;
let id;
+ let diffId;
+ let startSha;
const matches = endpointRE.exec(endpoint);
if (matches) {
[, mrPath, userOrGroup, project, id] = matches;
+ ({ diffId, startSha } = getVersionInfo({ endpoint }));
}
return {
@@ -16,5 +32,7 @@ export function getDerivedMergeRequestInformation({ endpoint } = {}) {
userOrGroup,
project,
id,
+ diffId,
+ startSha,
};
}
diff --git a/app/assets/javascripts/editor/components/source_editor_toolbar.vue b/app/assets/javascripts/editor/components/source_editor_toolbar.vue
index 2c177634bbe..c72145f9d2f 100644
--- a/app/assets/javascripts/editor/components/source_editor_toolbar.vue
+++ b/app/assets/javascripts/editor/components/source_editor_toolbar.vue
@@ -57,13 +57,12 @@ export default {
>
<div v-for="group in $options.groups" :key="group">
<gl-button-group v-if="hasGroupItems(group)">
- <template v-for="item in getGroupItems(group)">
- <source-editor-toolbar-button
- :key="item.id"
- :button="item"
- @click="$emit('click', item)"
- />
- </template>
+ <source-editor-toolbar-button
+ v-for="item in getGroupItems(group)"
+ :key="item.id"
+ :button="item"
+ @click="$emit('click', item)"
+ />
</gl-button-group>
</div>
</section>
diff --git a/app/assets/javascripts/editor/components/source_editor_toolbar_button.vue b/app/assets/javascripts/editor/components/source_editor_toolbar_button.vue
index 6ce48ddf89a..38f586f0773 100644
--- a/app/assets/javascripts/editor/components/source_editor_toolbar_button.vue
+++ b/app/assets/javascripts/editor/components/source_editor_toolbar_button.vue
@@ -31,12 +31,19 @@ export default {
return Object.entries(this.button).length > 0;
},
},
+ mounted() {
+ if (this.button.data) {
+ Object.entries(this.button.data).forEach(([attr, value]) => {
+ this.$el.dataset[attr] = value;
+ });
+ }
+ },
methods: {
- clickHandler() {
+ clickHandler(event) {
if (this.button.onClick) {
- this.button.onClick();
+ this.button.onClick(event);
}
- this.$emit('click');
+ this.$emit('click', event);
},
},
};
@@ -52,7 +59,7 @@ export default {
:icon="icon"
:title="label"
:aria-label="label"
- data-qa-selector="editor_toolbar_button"
- @click="clickHandler"
+ :class="button.class"
+ @click="clickHandler($event)"
/>
</template>
diff --git a/app/assets/javascripts/editor/constants.js b/app/assets/javascripts/editor/constants.js
index 83cfdd25757..d0649ecccba 100644
--- a/app/assets/javascripts/editor/constants.js
+++ b/app/assets/javascripts/editor/constants.js
@@ -1,5 +1,6 @@
+import { MODIFIER_KEY } from '~/constants';
import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
-import { s__, __ } from '~/locale';
+import { s__, __, sprintf } from '~/locale';
export const URI_PREFIX = 'gitlab';
export const CONTENT_UPDATE_DEBOUNCE = DEFAULT_DEBOUNCE_AND_THROTTLE_MS;
@@ -62,3 +63,104 @@ export const EXTENSION_MARKDOWN_PREVIEW_PANEL_WIDTH = 0.5; // 50% of the width
export const EXTENSION_MARKDOWN_PREVIEW_UPDATE_DELAY = 250; // ms
export const EXTENSION_MARKDOWN_PREVIEW_LABEL = __('Preview Markdown');
export const EXTENSION_MARKDOWN_HIDE_PREVIEW_LABEL = __('Hide Live Preview');
+export const EXTENSION_MARKDOWN_BUTTONS = [
+ {
+ id: 'bold',
+ label: sprintf(s__('MarkdownEditor|Add bold text (%{modifierKey}B)'), {
+ modifierKey: MODIFIER_KEY,
+ }),
+ data: {
+ mdTag: '**',
+ mdShortcuts: '["mod+b"]',
+ },
+ },
+ {
+ id: 'italic',
+ label: sprintf(s__('MarkdownEditor|Add italic text (%{modifierKey}I)'), {
+ modifierKey: MODIFIER_KEY,
+ }),
+ data: {
+ mdTag: '_',
+ mdShortcuts: '["mod+i"]',
+ },
+ },
+ {
+ id: 'strikethrough',
+ label: sprintf(s__('MarkdownEditor|Add strikethrough text (%{modifierKey}⇧X)'), {
+ modifierKey: MODIFIER_KEY,
+ }),
+ data: {
+ mdTag: '~~',
+ mdShortcuts: '["mod+shift+x]',
+ },
+ },
+ {
+ id: 'quote',
+ label: __('Insert a quote'),
+ data: {
+ mdTag: '> ',
+ mdPrepend: true,
+ },
+ },
+ {
+ id: 'code',
+ label: __('Insert code'),
+ data: {
+ mdTag: '`',
+ mdBlock: '```',
+ },
+ },
+ {
+ id: 'link',
+ label: sprintf(s__('MarkdownEditor|Add a link (%{modifier_key}K)'), {
+ modifierKey: MODIFIER_KEY,
+ }),
+ data: {
+ mdTag: '[{text}](url)',
+ mdSelect: 'url',
+ mdShortcuts: '["mod+k"]',
+ },
+ },
+ {
+ id: 'list-bulleted',
+ label: __('Add a bullet list'),
+ data: {
+ mdTag: '- ',
+ mdPrepend: true,
+ },
+ },
+ {
+ id: 'list-numbered',
+ label: __('Add a numbered list'),
+ data: {
+ mdTag: '1. ',
+ mdPrepend: true,
+ },
+ },
+ {
+ id: 'list-task',
+ label: __('Add a checklist'),
+ data: {
+ mdTag: '- [ ] ',
+ mdPrepend: true,
+ },
+ },
+ {
+ id: 'details-block',
+ label: __('Add a collapsible section'),
+ data: {
+ mdTag: '<details><summary>Click to expand</summary>\n{text}\n</details>',
+ mdPrepend: true,
+ mdSelect: __('Click to expand'),
+ },
+ },
+ {
+ id: 'table',
+ label: __('Add a table'),
+ data: {
+ /* eslint-disable-next-line @gitlab/require-i18n-strings */
+ mdTag: '| header | header |\n| ------ | ------ |\n| | |\n| | |',
+ mdPrepend: true,
+ },
+ },
+];
diff --git a/app/assets/javascripts/editor/extensions/source_editor_markdown_ext.js b/app/assets/javascripts/editor/extensions/source_editor_markdown_ext.js
index a16fe93026e..6105a577996 100644
--- a/app/assets/javascripts/editor/extensions/source_editor_markdown_ext.js
+++ b/app/assets/javascripts/editor/extensions/source_editor_markdown_ext.js
@@ -1,8 +1,37 @@
+import { insertMarkdownText } from '~/lib/utils/text_markdown';
+import { EDITOR_TOOLBAR_RIGHT_GROUP, EXTENSION_MARKDOWN_BUTTONS } from '../constants';
+
export class EditorMarkdownExtension {
static get extensionName() {
return 'EditorMarkdown';
}
+ onSetup(instance) {
+ this.toolbarButtons = [];
+ if (instance.toolbar) {
+ this.setupToolbar(instance);
+ }
+ }
+ onBeforeUnuse(instance) {
+ const ids = this.toolbarButtons.map((item) => item.id);
+ if (instance.toolbar) {
+ instance.toolbar.removeItems(ids);
+ }
+ }
+
+ setupToolbar(instance) {
+ this.toolbarButtons = EXTENSION_MARKDOWN_BUTTONS.map((btn) => {
+ return {
+ ...btn,
+ icon: btn.id,
+ group: EDITOR_TOOLBAR_RIGHT_GROUP,
+ category: 'tertiary',
+ onClick: (e) => instance.insertMarkdown(e),
+ };
+ });
+ instance.toolbar.addItems(this.toolbarButtons);
+ }
+
// eslint-disable-next-line class-methods-use-this
provides() {
return {
@@ -36,6 +65,25 @@ export class EditorMarkdownExtension {
pos.lineNumber += dy;
instance.setPosition(pos);
},
+ insertMarkdown: (instance, e) => {
+ const {
+ mdTag: tag,
+ mdBlock: blockTag,
+ mdPrepend,
+ mdSelect: select,
+ } = e.currentTarget.dataset;
+
+ insertMarkdownText({
+ tag,
+ blockTag,
+ wrap: !mdPrepend,
+ select,
+ selected: instance.getSelectedText(),
+ text: instance.getValue(),
+ editor: instance,
+ });
+ instance.focus();
+ },
/**
* Adjust existing selection to select text within the original selection.
* - If `selectedText` is not supplied, we fetch selected text with
diff --git a/app/assets/javascripts/editor/extensions/source_editor_markdown_livepreview_ext.js b/app/assets/javascripts/editor/extensions/source_editor_markdown_livepreview_ext.js
index dd4a7a689d7..58ddaa94d5e 100644
--- a/app/assets/javascripts/editor/extensions/source_editor_markdown_livepreview_ext.js
+++ b/app/assets/javascripts/editor/extensions/source_editor_markdown_livepreview_ext.js
@@ -120,6 +120,9 @@ export class EditorMarkdownPreviewExtension {
category: 'primary',
selectedLabel: EXTENSION_MARKDOWN_HIDE_PREVIEW_LABEL,
onClick: () => instance.togglePreview(),
+ data: {
+ qaSelector: 'editor_toolbar_button',
+ },
},
];
instance.toolbar.addItems(this.toolbarButtons);
diff --git a/app/assets/javascripts/editor/schema/ci.json b/app/assets/javascripts/editor/schema/ci.json
index 45f063a2048..d94aa73e43a 100644
--- a/app/assets/javascripts/editor/schema/ci.json
+++ b/app/assets/javascripts/editor/schema/ci.json
@@ -41,6 +41,9 @@
"before_script": {
"$ref": "#/definitions/before_script"
},
+ "hooks": {
+ "$ref": "#/definitions/hooks"
+ },
"cache": {
"$ref": "#/definitions/cache"
},
@@ -202,25 +205,11 @@
"when": {
"markdownDescription": "Configure when artifacts are uploaded depended on job status. [Learn More](https://docs.gitlab.com/ee/ci/yaml/#artifactswhen).",
"default": "on_success",
- "oneOf": [
- {
- "enum": [
- "on_success"
- ],
- "description": "Upload artifacts only when the job succeeds (this is the default)."
- },
- {
- "enum": [
- "on_failure"
- ],
- "description": "Upload artifacts only when the job fails."
- },
- {
- "enum": [
- "always"
- ],
- "description": "Upload artifacts regardless of job status."
- }
+ "type": "string",
+ "enum": [
+ "on_success",
+ "on_failure",
+ "always"
]
},
"expire_in": {
@@ -347,10 +336,10 @@
"include_item": {
"oneOf": [
{
- "description": "Will infer the method based on the value. E.g. `https://...` strings will be of type `include:remote`, and `/templates/...` will be of type `include:local`.",
+ "description": "Will infer the method based on the value. E.g. `https://...` strings will be of type `include:remote`, and `/templates/...` or `templates/...` will be of type `include:local`.",
"type": "string",
"format": "uri-reference",
- "pattern": "^(https?://|/).+\\.ya?ml$"
+ "pattern": "^(https?://|/?.?-?(?!\\w+://)\\w).+\\.ya?ml$"
},
{
"type": "object",
@@ -585,56 +574,98 @@
]
}
},
+ "id_tokens": {
+ "type": "object",
+ "markdownDescription": "Defines JWTs to be injected as environment variables.",
+ "patternProperties": {
+ ".*": {
+ "type": "object",
+ "properties": {
+ "aud": {
+ "oneOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "array",
+ "items": {
+ "type": "string"
+ },
+ "minItems": 1,
+ "uniqueItems": true
+ }
+ ]
+ }
+ },
+ "required": [
+ "aud"
+ ],
+ "additionalProperties": false
+ }
+ }
+ },
"secrets": {
"type": "object",
"markdownDescription": "Defines secrets to be injected as environment variables. [Learn More](https://docs.gitlab.com/ee/ci/yaml/#secrets).",
- "additionalProperties": {
- "type": "object",
- "description": "Environment variable name",
- "properties": {
- "vault": {
- "oneOf": [
- {
- "type": "string",
- "description": "The secret to be fetched from Vault (e.g. 'production/db/password@ops' translates to secret 'ops/data/production/db', field `password`)"
- },
- {
- "type": "object",
- "properties": {
- "engine": {
- "type": "object",
- "properties": {
- "name": {
- "type": "string"
+ "patternProperties": {
+ ".*": {
+ "type": "object",
+ "properties": {
+ "vault": {
+ "oneOf": [
+ {
+ "type": "string",
+ "markdownDescription": "The secret to be fetched from Vault (e.g. 'production/db/password@ops' translates to secret 'ops/data/production/db', field `password`). [Learn More](https://docs.gitlab.com/ee/ci/yaml/#secretsvault)"
+ },
+ {
+ "type": "object",
+ "properties": {
+ "engine": {
+ "type": "object",
+ "properties": {
+ "name": {
+ "type": "string"
+ },
+ "path": {
+ "type": "string"
+ }
},
- "path": {
- "type": "string"
- }
+ "required": [
+ "name",
+ "path"
+ ]
},
- "required": [
- "name",
- "path"
- ]
- },
- "path": {
- "type": "string"
+ "path": {
+ "type": "string"
+ },
+ "field": {
+ "type": "string"
+ }
},
- "field": {
- "type": "string"
- }
- },
- "required": [
- "engine",
- "path",
- "field"
- ]
- }
- ]
- }
- },
- "required": [
- "vault"
- ]
+ "required": [
+ "engine",
+ "path",
+ "field"
+ ],
+ "additionalProperties": false
+ }
+ ]
+ },
+ "file": {
+ "type": "boolean",
+ "default": true,
+ "markdownDescription": "Configures the secret to be stored as either a file or variable type CI/CD variable. [Learn More](https://docs.gitlab.com/ee/ci/yaml/#secretsfile)"
+ },
+ "token": {
+ "type": "string",
+ "description": "Specifies the JWT variable that should be used to authenticate with Hashicorp Vault."
+ }
+ },
+ "required": [
+ "vault"
+ ],
+ "additionalProperties": false
+ }
}
},
"before_script": {
@@ -739,7 +770,17 @@
"type": "object",
"properties": {
"value": {
- "type": "string"
+ "type": "string",
+ "markdownDescription": "Default value of the variable. If used with `options`, `value` must be included in the array. [Learn More](https://docs.gitlab.com/ee/ci/pipelines/index.html#prefill-variables-in-manual-pipelines)"
+ },
+ "options": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ },
+ "minItems": 1,
+ "uniqueItems": true,
+ "markdownDescription": "A list of predefined values that users can select from in the **Run pipeline** page when running a pipeline manually. [Learn More](https://docs.gitlab.com/ee/ci/pipelines/index.html#configure-a-list-of-selectable-values-for-a-prefilled-variable)"
},
"description": {
"type": "string",
@@ -959,6 +1000,7 @@
"default": false
},
"when": {
+ "type": "string",
"markdownDescription": "Defines when to save the cache, based on the status of the job. [Learn More](https://docs.gitlab.com/ee/ci/yaml/#cachewhen).",
"default": "on_success",
"enum": [
@@ -1200,6 +1242,9 @@
"after_script": {
"$ref": "#/definitions/after_script"
},
+ "hooks": {
+ "$ref": "#/definitions/hooks"
+ },
"rules": {
"$ref": "#/definitions/rules"
},
@@ -1209,6 +1254,9 @@
"cache": {
"$ref": "#/definitions/cache"
},
+ "id_tokens": {
+ "$ref": "#/definitions/id_tokens"
+ },
"secrets": {
"$ref": "#/definitions/secrets"
},
@@ -1861,6 +1909,39 @@
}
]
}
+ },
+ "hooks": {
+ "type": "object",
+ "markdownDescription": "Specifies lists of commands to execute on the runner at certain stages of job execution. [Learn More](https://docs.gitlab.com/ee/ci/yaml/#hooks).",
+ "properties": {
+ "pre_get_sources_script": {
+ "markdownDescription": "Specifies a list of commands to execute on the runner before updating the Git repository and any submodules. [Learn More](https://docs.gitlab.com/ee/ci/yaml/#hookspre_get_sources_script).",
+ "oneOf": [
+ {
+ "type": "string",
+ "minLength": 1
+ },
+ {
+ "type": "array",
+ "items": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ }
+ ]
+ },
+ "minItems": 1
+ }
+ ]
+ }
+ },
+ "additionalProperties": false
}
}
} \ No newline at end of file
diff --git a/app/assets/javascripts/environments/components/deploy_board.vue b/app/assets/javascripts/environments/components/deploy_board.vue
index f22a0705b3d..31bc462f0b9 100644
--- a/app/assets/javascripts/environments/components/deploy_board.vue
+++ b/app/assets/javascripts/environments/components/deploy_board.vue
@@ -15,10 +15,10 @@ import {
GlLink,
GlTooltip,
GlTooltipDirective,
- GlSafeHtmlDirective as SafeHtml,
GlSprintf,
} from '@gitlab/ui';
import { isEmpty } from 'lodash';
+import SafeHtml from '~/vue_shared/directives/safe_html';
import { s__, n__ } from '~/locale';
import InstanceComponent from '~/vue_shared/components/deployment_instance.vue';
import { STATUS_MAP, CANARY_STATUS } from '../constants';
diff --git a/app/assets/javascripts/environments/environment_details/constants.js b/app/assets/javascripts/environments/environment_details/constants.js
new file mode 100644
index 00000000000..56c70c354b7
--- /dev/null
+++ b/app/assets/javascripts/environments/environment_details/constants.js
@@ -0,0 +1,47 @@
+import { __ } from '~/locale';
+
+export const ENVIRONMENT_DETAILS_PAGE_SIZE = 20;
+export const ENVIRONMENT_DETAILS_TABLE_FIELDS = [
+ {
+ key: 'status',
+ label: __('Status'),
+ columnClass: 'gl-w-10p',
+ tdClass: 'gl-vertical-align-middle!',
+ },
+ {
+ key: 'id',
+ label: __('ID'),
+ columnClass: 'gl-w-5p',
+ tdClass: 'gl-vertical-align-middle!',
+ },
+ {
+ key: 'triggerer',
+ label: __('Triggerer'),
+ columnClass: 'gl-w-10p',
+ tdClass: 'gl-vertical-align-middle!',
+ },
+ {
+ key: 'commit',
+ label: __('Commit'),
+ columnClass: 'gl-w-20p',
+ tdClass: 'gl-vertical-align-middle!',
+ },
+ {
+ key: 'job',
+ label: __('Job'),
+ columnClass: 'gl-w-20p',
+ tdClass: 'gl-vertical-align-middle!',
+ },
+ {
+ key: 'created',
+ label: __('Created'),
+ columnClass: 'gl-w-10p',
+ tdClass: 'gl-vertical-align-middle! gl-white-space-nowrap',
+ },
+ {
+ key: 'deployed',
+ label: __('Deployed'),
+ columnClass: 'gl-w-10p',
+ tdClass: 'gl-vertical-align-middle! gl-white-space-nowrap',
+ },
+];
diff --git a/app/assets/javascripts/environments/environment_details/index.vue b/app/assets/javascripts/environments/environment_details/index.vue
new file mode 100644
index 00000000000..435d3fd820e
--- /dev/null
+++ b/app/assets/javascripts/environments/environment_details/index.vue
@@ -0,0 +1,118 @@
+<script>
+import {
+ GlTableLite,
+ GlAvatarLink,
+ GlAvatar,
+ GlLink,
+ GlTooltipDirective,
+ GlTruncate,
+ GlBadge,
+ GlLoadingIcon,
+} from '@gitlab/ui';
+import Commit from '~/vue_shared/components/commit.vue';
+import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
+import environmentDetailsQuery from '../graphql/queries/environment_details.query.graphql';
+import { convertToDeploymentTableRow } from '../helpers/deployment_data_transformation_helper';
+import DeploymentStatusBadge from '../components/deployment_status_badge.vue';
+import { ENVIRONMENT_DETAILS_PAGE_SIZE, ENVIRONMENT_DETAILS_TABLE_FIELDS } from './constants';
+
+export default {
+ components: {
+ GlLoadingIcon,
+ GlBadge,
+ DeploymentStatusBadge,
+ TimeAgoTooltip,
+ GlTableLite,
+ GlAvatarLink,
+ GlAvatar,
+ GlLink,
+ GlTruncate,
+ Commit,
+ },
+ directives: {
+ GlTooltip: GlTooltipDirective,
+ },
+ props: {
+ projectFullPath: {
+ type: String,
+ required: true,
+ },
+ environmentName: {
+ type: String,
+ required: true,
+ },
+ },
+ apollo: {
+ project: {
+ query: environmentDetailsQuery,
+ variables() {
+ return {
+ projectFullPath: this.projectFullPath,
+ environmentName: this.environmentName,
+ pageSize: ENVIRONMENT_DETAILS_PAGE_SIZE,
+ };
+ },
+ },
+ },
+ data() {
+ return {
+ project: {
+ loading: true,
+ },
+ loading: 0,
+ tableFields: ENVIRONMENT_DETAILS_TABLE_FIELDS,
+ };
+ },
+ computed: {
+ deployments() {
+ return this.project.environment?.deployments.nodes.map(convertToDeploymentTableRow) || [];
+ },
+ isLoading() {
+ return this.$apollo.queries.project.loading;
+ },
+ },
+};
+</script>
+<template>
+ <div>
+ <gl-loading-icon v-if="isLoading" size="lg" class="mt-3" />
+ <gl-table-lite v-else :items="deployments" :fields="tableFields" fixed stacked="lg">
+ <template #table-colgroup="{ fields }">
+ <col v-for="field in fields" :key="field.key" :class="field.columnClass" />
+ </template>
+ <template #cell(status)="{ item }">
+ <div>
+ <deployment-status-badge :status="item.status" />
+ </div>
+ </template>
+ <template #cell(id)="{ item }">
+ <strong>{{ item.id }}</strong>
+ </template>
+ <template #cell(triggerer)="{ item }">
+ <gl-avatar-link :href="item.triggerer.webUrl">
+ <gl-avatar
+ v-gl-tooltip
+ :title="item.triggerer.name"
+ :src="item.triggerer.avatarUrl"
+ :size="24"
+ />
+ </gl-avatar-link>
+ </template>
+ <template #cell(commit)="{ item }">
+ <commit v-bind="item.commit" />
+ </template>
+ <template #cell(job)="{ item }">
+ <gl-link v-if="item.job" :href="item.job.webPath">
+ <gl-truncate :text="item.job.label" />
+ </gl-link>
+ <gl-badge v-else variant="info">{{ __('API') }}</gl-badge>
+ </template>
+ <template #cell(created)="{ item }">
+ <time-ago-tooltip :time="item.created" />
+ </template>
+ <template #cell(deployed)="{ item }">
+ <time-ago-tooltip :time="item.deployed" />
+ </template>
+ </gl-table-lite>
+ </div>
+</template>
diff --git a/app/assets/javascripts/environments/graphql/queries/environment_details.query.graphql b/app/assets/javascripts/environments/graphql/queries/environment_details.query.graphql
new file mode 100644
index 00000000000..e8f2a2cdf7f
--- /dev/null
+++ b/app/assets/javascripts/environments/graphql/queries/environment_details.query.graphql
@@ -0,0 +1,48 @@
+query getEnvironmentDetails($projectFullPath: ID!, $environmentName: String, $pageSize: Int) {
+ project(fullPath: $projectFullPath) {
+ id
+ name
+ fullPath
+ environment(name: $environmentName) {
+ id
+ name
+ deployments(orderBy: { createdAt: DESC }, first: $pageSize) {
+ nodes {
+ id
+ iid
+ status
+ ref
+ tag
+ job {
+ name
+ id
+ webPath
+ }
+ commit {
+ id
+ shortId
+ message
+ webUrl
+ authorGravatar
+ authorName
+ authorEmail
+ author {
+ id
+ name
+ avatarUrl
+ webUrl
+ }
+ }
+ triggerer {
+ id
+ webUrl
+ name
+ avatarUrl
+ }
+ createdAt
+ finishedAt
+ }
+ }
+ }
+ }
+}
diff --git a/app/assets/javascripts/environments/helpers/deployment_data_transformation_helper.js b/app/assets/javascripts/environments/helpers/deployment_data_transformation_helper.js
new file mode 100644
index 00000000000..bfe92fe3125
--- /dev/null
+++ b/app/assets/javascripts/environments/helpers/deployment_data_transformation_helper.js
@@ -0,0 +1,62 @@
+import { getIdFromGraphQLId } from '~/graphql_shared/utils';
+
+/**
+ * This function transforms Commit object coming from GraphQL to object compatible with app/assets/javascripts/vue_shared/components/commit.vue author object
+ * @param {Object} Commit
+ * @returns {Object}
+ */
+export const getAuthorFromCommit = (commit) => {
+ if (commit.author) {
+ return {
+ username: commit.author.name,
+ path: commit.author.webUrl,
+ avatar_url: commit.author.avatarUrl,
+ };
+ }
+ return {
+ username: commit.authorName,
+ path: `mailto:${commit.authorEmail}`,
+ avatar_url: commit.authorGravatar,
+ };
+};
+
+/**
+ * This function transforms deploymentNode object coming from GraphQL to object compatible with app/assets/javascripts/vue_shared/components/commit.vue
+ * @param {Object} deploymentNode
+ * @returns {Object}
+ */
+export const getCommitFromDeploymentNode = (deploymentNode) => {
+ if (!deploymentNode.commit) {
+ throw new Error("deploymentNode argument doesn't have 'commit' field", deploymentNode);
+ }
+ return {
+ title: deploymentNode.commit.message,
+ commitUrl: deploymentNode.commit.webUrl,
+ shortSha: deploymentNode.commit.shortId,
+ tag: deploymentNode.tag,
+ commitRef: {
+ name: deploymentNode.ref,
+ },
+ author: getAuthorFromCommit(deploymentNode.commit),
+ };
+};
+
+/**
+ * This function transforms deploymentNode object coming from GraphQL to object compatible with app/assets/javascripts/environments/environment_details/page.vue table
+ * @param {Object} deploymentNode
+ * @returns {Object}
+ */
+export const convertToDeploymentTableRow = (deploymentNode) => {
+ return {
+ status: deploymentNode.status.toLowerCase(),
+ id: deploymentNode.iid,
+ triggerer: deploymentNode.triggerer,
+ commit: getCommitFromDeploymentNode(deploymentNode),
+ job: deploymentNode.job && {
+ webPath: deploymentNode.job.webPath,
+ label: `${deploymentNode.job.name} (#${getIdFromGraphQLId(deploymentNode.job.id)})`,
+ },
+ created: deploymentNode.createdAt || '',
+ deployed: deploymentNode.finishedAt || '',
+ };
+};
diff --git a/app/assets/javascripts/environments/mount_show.js b/app/assets/javascripts/environments/mount_show.js
index 6df4fad83f2..ba816599ac2 100644
--- a/app/assets/javascripts/environments/mount_show.js
+++ b/app/assets/javascripts/environments/mount_show.js
@@ -1,6 +1,8 @@
import Vue from 'vue';
+import VueApollo from 'vue-apollo';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import EnvironmentsDetailHeader from './components/environments_detail_header.vue';
+import { apolloProvider } from './graphql/client';
import environmentsMixin from './mixins/environments_mixin';
export const initHeader = () => {
@@ -41,7 +43,33 @@ export const initHeader = () => {
cancelAutoStopPath: dataset.environmentCancelAutoStopPath,
terminalPath: dataset.environmentTerminalPath,
metricsPath: dataset.environmentMetricsPath,
- updatePath: dataset.environmentEditPath,
+ updatePath: dataset.tnvironmentEditPath,
+ },
+ });
+ },
+ });
+};
+
+export const initPage = async () => {
+ if (!gon.features.environmentDetailsVue) {
+ return null;
+ }
+ const EnvironmentsDetailPageModule = await import('./environment_details/index.vue');
+ const EnvironmentsDetailPage = EnvironmentsDetailPageModule.default;
+ const dataElement = document.getElementById('environments-detail-view');
+ const dataSet = convertObjectPropsToCamelCase(JSON.parse(dataElement.dataset.details));
+
+ Vue.use(VueApollo);
+ const el = document.getElementById('environment_details_page');
+ return new Vue({
+ el,
+ apolloProvider: apolloProvider(),
+ provide: {},
+ render(createElement) {
+ return createElement(EnvironmentsDetailPage, {
+ props: {
+ projectFullPath: dataSet.projectFullPath,
+ environmentName: dataSet.name,
},
});
},
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 de4b11699fc..122c7c005e9 100644
--- a/app/assets/javascripts/error_tracking/components/error_tracking_list.vue
+++ b/app/assets/javascripts/error_tracking/components/error_tracking_list.vue
@@ -357,7 +357,7 @@ export default {
>
<span class="d-flex">
<gl-icon
- class="gl-new-dropdown-item-check-icon"
+ class="gl-dropdown-item-check-icon"
:class="{ invisible: !isCurrentStatusFilter(status) }"
name="mobile-issue-close"
/>
@@ -374,7 +374,7 @@ export default {
>
<span class="d-flex">
<gl-icon
- class="gl-new-dropdown-item-check-icon"
+ class="gl-dropdown-item-check-icon"
:class="{ invisible: !isCurrentSortField(field) }"
name="mobile-issue-close"
/>
diff --git a/app/assets/javascripts/error_tracking/components/stacktrace_entry.vue b/app/assets/javascripts/error_tracking/components/stacktrace_entry.vue
index 34d01f21da2..6ddd982ebf1 100644
--- a/app/assets/javascripts/error_tracking/components/stacktrace_entry.vue
+++ b/app/assets/javascripts/error_tracking/components/stacktrace_entry.vue
@@ -1,5 +1,6 @@
<script>
-import { GlTooltip, GlSprintf, GlIcon, GlSafeHtmlDirective as SafeHtml } from '@gitlab/ui';
+import { GlTooltip, GlSprintf, GlIcon } from '@gitlab/ui';
+import SafeHtml from '~/vue_shared/directives/safe_html';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import FileIcon from '~/vue_shared/components/file_icon.vue';
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 f0f42d19ea5..286b214b511 100644
--- a/app/assets/javascripts/feature_flags/components/feature_flags_table.vue
+++ b/app/assets/javascripts/feature_flags/components/feature_flags_table.vue
@@ -4,6 +4,8 @@ import { __, s__, sprintf } from '~/locale';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { labelForStrategy } from '../utils';
+import StrategyLabel from './strategy_label.vue';
+
export default {
i18n: {
deleteLabel: __('Delete'),
@@ -15,6 +17,7 @@ export default {
GlButton,
GlModal,
GlToggle,
+ StrategyLabel,
},
directives: {
GlTooltip: GlTooltipDirective,
@@ -166,14 +169,13 @@ export default {
<div
class="table-mobile-content d-flex flex-wrap justify-content-end justify-content-md-start js-feature-flag-environments"
>
- <gl-badge
+ <strategy-label
v-for="strategy in featureFlag.strategies"
:key="strategy.id"
- data-testid="strategy-badge"
- variant="info"
- class="gl-mr-3 gl-mt-2 gl-white-space-normal gl-text-left gl-px-5"
- >{{ strategyBadgeText(strategy) }}</gl-badge
- >
+ data-testid="strategy-label"
+ class="w-100 gl-mr-3 gl-mt-2 gl-white-space-normal gl-text-left"
+ v-bind="strategyBadgeText(strategy)"
+ />
</div>
</div>
diff --git a/app/assets/javascripts/feature_flags/components/strategies/flexible_rollout.vue b/app/assets/javascripts/feature_flags/components/strategies/flexible_rollout.vue
index 1a470d74b59..0fde87dd0ba 100644
--- a/app/assets/javascripts/feature_flags/components/strategies/flexible_rollout.vue
+++ b/app/assets/javascripts/feature_flags/components/strategies/flexible_rollout.vue
@@ -90,10 +90,10 @@ export default {
:id="inputId"
:value="percentage"
:state="isValid"
- class="rollout-percentage gl-text-right gl-w-9"
type="number"
min="0"
max="100"
+ size="xs"
@input="onPercentageChange"
/>
<span class="ml-1">%</span>
diff --git a/app/assets/javascripts/feature_flags/components/strategies/percent_rollout.vue b/app/assets/javascripts/feature_flags/components/strategies/percent_rollout.vue
index 91e1b85d66e..0acb0d4366c 100644
--- a/app/assets/javascripts/feature_flags/components/strategies/percent_rollout.vue
+++ b/app/assets/javascripts/feature_flags/components/strategies/percent_rollout.vue
@@ -56,10 +56,10 @@ export default {
:id="inputId"
:value="percentage"
:state="isValid"
- class="rollout-percentage gl-text-right gl-w-9"
type="number"
min="0"
max="100"
+ size="xs"
@input="onPercentageChange"
/>
<span class="gl-ml-2">%</span>
diff --git a/app/assets/javascripts/feature_flags/components/strategy_label.vue b/app/assets/javascripts/feature_flags/components/strategy_label.vue
new file mode 100644
index 00000000000..c2d3ec5708f
--- /dev/null
+++ b/app/assets/javascripts/feature_flags/components/strategy_label.vue
@@ -0,0 +1,29 @@
+<script>
+export default {
+ props: {
+ name: {
+ type: String,
+ required: true,
+ },
+ scopes: {
+ type: String,
+ required: false,
+ default: null,
+ },
+ parameters: {
+ type: String,
+ required: false,
+ default: null,
+ },
+ },
+};
+</script>
+<template>
+ <div>
+ <strong class="gl-fw-bold"
+ >{{ name }}<span v-if="parameters"> - {{ parameters }}</span
+ >:</strong
+ >
+ <span v-if="scopes">{{ scopes }}</span>
+ </div>
+</template>
diff --git a/app/assets/javascripts/feature_flags/utils.js b/app/assets/javascripts/feature_flags/utils.js
index e77cb8406cc..47deeab0571 100644
--- a/app/assets/javascripts/feature_flags/utils.js
+++ b/app/assets/javascripts/feature_flags/utils.js
@@ -50,17 +50,11 @@ const scopeName = ({ environment_scope: scope }) =>
export const labelForStrategy = (strategy) => {
const { name, parameters } = badgeTextByType[strategy.name];
+ const scopes = strategy.scopes.map(scopeName).join(', ');
- if (parameters) {
- return sprintf('%{name} - %{parameters}: %{scopes}', {
- name,
- parameters: parameters(strategy),
- scopes: strategy.scopes.map(scopeName).join(', '),
- });
- }
-
- return sprintf('%{name}: %{scopes}', {
+ return {
name,
- scopes: strategy.scopes.map(scopeName).join(', '),
- });
+ parameters: parameters ? parameters(strategy) : null,
+ scopes,
+ };
};
diff --git a/app/assets/javascripts/feature_highlight/feature_highlight_popover.vue b/app/assets/javascripts/feature_highlight/feature_highlight_popover.vue
index 79d7eb94569..1c6e6380e76 100644
--- a/app/assets/javascripts/feature_highlight/feature_highlight_popover.vue
+++ b/app/assets/javascripts/feature_highlight/feature_highlight_popover.vue
@@ -1,12 +1,7 @@
<script>
import clusterPopover from '@gitlab/svgs/dist/illustrations/cluster_popover.svg';
-import {
- GlPopover,
- GlSprintf,
- GlLink,
- GlButton,
- GlSafeHtmlDirective as SafeHtml,
-} from '@gitlab/ui';
+import { GlPopover, GlSprintf, GlLink, GlButton } from '@gitlab/ui';
+import SafeHtml from '~/vue_shared/directives/safe_html';
import { __ } from '~/locale';
import { POPOVER_TARGET_ID } from './constants';
import { dismiss } from './feature_highlight_helper';
diff --git a/app/assets/javascripts/filtered_search/add_extra_tokens_for_merge_requests.js b/app/assets/javascripts/filtered_search/add_extra_tokens_for_merge_requests.js
index d9c627f5c93..397ba879866 100644
--- a/app/assets/javascripts/filtered_search/add_extra_tokens_for_merge_requests.js
+++ b/app/assets/javascripts/filtered_search/add_extra_tokens_for_merge_requests.js
@@ -1,9 +1,16 @@
-import { __, s__ } from '~/locale';
+import { __ } from '~/locale';
+import {
+ TOKEN_TITLE_APPROVED_BY,
+ TOKEN_TITLE_REVIEWER,
+ TOKEN_TYPE_APPROVED_BY,
+ TOKEN_TYPE_REVIEWER,
+ TOKEN_TYPE_TARGET_BRANCH,
+} from '~/vue_shared/components/filtered_search_bar/constants';
export default (IssuableTokenKeys, disableTargetBranchFilter = false) => {
const reviewerToken = {
- formattedKey: s__('SearchToken|Reviewer'),
- key: 'reviewer',
+ formattedKey: TOKEN_TITLE_REVIEWER,
+ key: TOKEN_TYPE_REVIEWER,
type: 'string',
param: 'username',
symbol: '@',
@@ -53,7 +60,7 @@ export default (IssuableTokenKeys, disableTargetBranchFilter = false) => {
if (!disableTargetBranchFilter) {
const targetBranchToken = {
formattedKey: __('Target-Branch'),
- key: 'target-branch',
+ key: TOKEN_TYPE_TARGET_BRANCH,
type: 'string',
param: '',
symbol: '',
@@ -67,8 +74,8 @@ export default (IssuableTokenKeys, disableTargetBranchFilter = false) => {
const approvedBy = {
token: {
- formattedKey: __('Approved-By'),
- key: 'approved-by',
+ formattedKey: TOKEN_TITLE_APPROVED_BY,
+ key: TOKEN_TYPE_APPROVED_BY,
type: 'array',
param: 'usernames[]',
symbol: '@',
@@ -76,8 +83,8 @@ export default (IssuableTokenKeys, disableTargetBranchFilter = false) => {
tag: '@approved-by',
},
tokenAlternative: {
- formattedKey: __('Approved-By'),
- key: 'approved-by',
+ formattedKey: TOKEN_TITLE_APPROVED_BY,
+ key: TOKEN_TYPE_APPROVED_BY,
type: 'string',
param: 'usernames',
symbol: '@',
@@ -85,25 +92,25 @@ export default (IssuableTokenKeys, disableTargetBranchFilter = false) => {
condition: [
{
url: 'approved_by_usernames[]=None',
- tokenKey: 'approved-by',
+ tokenKey: TOKEN_TYPE_APPROVED_BY,
value: __('None'),
operator: '=',
},
{
url: 'not[approved_by_usernames][]=None',
- tokenKey: 'approved-by',
+ tokenKey: TOKEN_TYPE_APPROVED_BY,
value: __('None'),
operator: '!=',
},
{
url: 'approved_by_usernames[]=Any',
- tokenKey: 'approved-by',
+ tokenKey: TOKEN_TYPE_APPROVED_BY,
value: __('Any'),
operator: '=',
},
{
url: 'not[approved_by_usernames][]=Any',
- tokenKey: 'approved-by',
+ tokenKey: TOKEN_TYPE_APPROVED_BY,
value: __('Any'),
operator: '!=',
},
diff --git a/app/assets/javascripts/filtered_search/available_dropdown_mappings.js b/app/assets/javascripts/filtered_search/available_dropdown_mappings.js
index 3913e4e8d81..1f8baa470d8 100644
--- a/app/assets/javascripts/filtered_search/available_dropdown_mappings.js
+++ b/app/assets/javascripts/filtered_search/available_dropdown_mappings.js
@@ -1,5 +1,17 @@
import { sortMilestonesByDueDate } from '~/milestones/utils';
-import { mergeUrlParams } from '../lib/utils/url_utility';
+import { mergeUrlParams } from '~/lib/utils/url_utility';
+import {
+ TOKEN_TYPE_APPROVED_BY,
+ TOKEN_TYPE_ASSIGNEE,
+ TOKEN_TYPE_AUTHOR,
+ TOKEN_TYPE_CONFIDENTIAL,
+ TOKEN_TYPE_LABEL,
+ TOKEN_TYPE_MILESTONE,
+ TOKEN_TYPE_MY_REACTION,
+ TOKEN_TYPE_RELEASE,
+ TOKEN_TYPE_REVIEWER,
+ TOKEN_TYPE_TARGET_BRANCH,
+} from '~/vue_shared/components/filtered_search_bar/constants';
import DropdownEmoji from './dropdown_emoji';
import DropdownHint from './dropdown_hint';
import DropdownNonUser from './dropdown_non_user';
@@ -58,17 +70,17 @@ export default class AvailableDropdownMappings {
getMappings() {
return {
- author: {
+ [TOKEN_TYPE_AUTHOR]: {
reference: null,
gl: DropdownUser,
element: this.container.querySelector('#js-dropdown-author'),
},
- assignee: {
+ [TOKEN_TYPE_ASSIGNEE]: {
reference: null,
gl: DropdownUser,
element: this.container.querySelector('#js-dropdown-assignee'),
},
- reviewer: {
+ [TOKEN_TYPE_REVIEWER]: {
reference: null,
gl: DropdownUser,
element: this.container.querySelector('#js-dropdown-reviewer'),
@@ -78,12 +90,12 @@ export default class AvailableDropdownMappings {
gl: DropdownUser,
element: this.container.getElementById('js-dropdown-attention-requested'),
},
- 'approved-by': {
+ [TOKEN_TYPE_APPROVED_BY]: {
reference: null,
gl: DropdownUser,
element: this.container.querySelector('#js-dropdown-approved-by'),
},
- milestone: {
+ [TOKEN_TYPE_MILESTONE]: {
reference: null,
gl: DropdownNonUser,
extraArguments: {
@@ -93,7 +105,7 @@ export default class AvailableDropdownMappings {
},
element: this.container.querySelector('#js-dropdown-milestone'),
},
- release: {
+ [TOKEN_TYPE_RELEASE]: {
reference: null,
gl: DropdownNonUser,
extraArguments: {
@@ -106,7 +118,7 @@ export default class AvailableDropdownMappings {
},
element: this.container.querySelector('#js-dropdown-release'),
},
- label: {
+ [TOKEN_TYPE_LABEL]: {
reference: null,
gl: DropdownNonUser,
extraArguments: {
@@ -116,7 +128,7 @@ export default class AvailableDropdownMappings {
},
element: this.container.querySelector('#js-dropdown-label'),
},
- 'my-reaction': {
+ [TOKEN_TYPE_MY_REACTION]: {
reference: null,
gl: DropdownEmoji,
element: this.container.querySelector('#js-dropdown-my-reaction'),
@@ -126,12 +138,12 @@ export default class AvailableDropdownMappings {
gl: DropdownNonUser,
element: this.container.querySelector('#js-dropdown-wip'),
},
- confidential: {
+ [TOKEN_TYPE_CONFIDENTIAL]: {
reference: null,
gl: DropdownNonUser,
element: this.container.querySelector('#js-dropdown-confidential'),
},
- 'target-branch': {
+ [TOKEN_TYPE_TARGET_BRANCH]: {
reference: null,
gl: DropdownNonUser,
extraArguments: {
diff --git a/app/assets/javascripts/filtered_search/constants.js b/app/assets/javascripts/filtered_search/constants.js
index e07dccd11e8..b328ae6a872 100644
--- a/app/assets/javascripts/filtered_search/constants.js
+++ b/app/assets/javascripts/filtered_search/constants.js
@@ -1,4 +1,17 @@
-export const USER_TOKEN_TYPES = ['author', 'assignee', 'approved-by', 'reviewer', 'attention'];
+import {
+ TOKEN_TYPE_APPROVED_BY,
+ TOKEN_TYPE_ASSIGNEE,
+ TOKEN_TYPE_AUTHOR,
+ TOKEN_TYPE_REVIEWER,
+} from '~/vue_shared/components/filtered_search_bar/constants';
+
+export const USER_TOKEN_TYPES = [
+ TOKEN_TYPE_AUTHOR,
+ TOKEN_TYPE_ASSIGNEE,
+ TOKEN_TYPE_APPROVED_BY,
+ TOKEN_TYPE_REVIEWER,
+ 'attention',
+];
export const DROPDOWN_TYPE = {
hint: 'hint',
diff --git a/app/assets/javascripts/filtered_search/dropdown_utils.js b/app/assets/javascripts/filtered_search/dropdown_utils.js
index 22e1604871a..38909db0555 100644
--- a/app/assets/javascripts/filtered_search/dropdown_utils.js
+++ b/app/assets/javascripts/filtered_search/dropdown_utils.js
@@ -1,4 +1,5 @@
import { last } from 'lodash';
+import { TOKEN_TYPE_LABEL } from '~/vue_shared/components/filtered_search_bar/constants';
import FilteredSearchContainer from './container';
import FilteredSearchDropdownManager from './filtered_search_dropdown_manager';
import FilteredSearchTokenizer from './filtered_search_tokenizer';
@@ -113,7 +114,7 @@ export default class DropdownUtils {
visualToken &&
visualToken.querySelector('.value') &&
visualToken.querySelector('.value').textContent.trim();
- if (tokenName === 'label' && tokenValue) {
+ if (tokenName === TOKEN_TYPE_LABEL && tokenValue) {
// remove leading symbol and wrapping quotes
tokenValue = tokenValue.replace(/^~("|')?(.*)/, '$2').replace(/("|')$/, '');
}
diff --git a/app/assets/javascripts/filtered_search/filtered_search_manager.js b/app/assets/javascripts/filtered_search/filtered_search_manager.js
index bc0f5398b4c..16c70fdd069 100644
--- a/app/assets/javascripts/filtered_search/filtered_search_manager.js
+++ b/app/assets/javascripts/filtered_search/filtered_search_manager.js
@@ -10,8 +10,12 @@ import {
DOWN_KEY_CODE,
} from '~/lib/utils/keycodes';
import { __ } from '~/locale';
-import { addClassIfElementExists } from '../lib/utils/dom_utils';
-import { visitUrl, getUrlParamsArray, getParameterByName } from '../lib/utils/url_utility';
+import { addClassIfElementExists } from '~/lib/utils/dom_utils';
+import { visitUrl, getUrlParamsArray, getParameterByName } from '~/lib/utils/url_utility';
+import {
+ TOKEN_TYPE_ASSIGNEE,
+ TOKEN_TYPE_AUTHOR,
+} from '~/vue_shared/components/filtered_search_bar/constants';
import FilteredSearchContainer from './container';
import DropdownUtils from './dropdown_utils';
import eventHub from './event_hub';
@@ -675,7 +679,7 @@ export default class FilteredSearchManager {
const id = parseInt(value, 10);
if (usernameParams[id]) {
hasFilteredSearch = true;
- const tokenName = 'assignee';
+ const tokenName = TOKEN_TYPE_ASSIGNEE;
const canEdit = this.canEdit && this.canEdit(tokenName);
const operator = FilteredSearchVisualTokens.getOperatorToken(usernameParams[id]);
const valueToken = FilteredSearchVisualTokens.getValueToken(usernameParams[id]);
@@ -688,7 +692,7 @@ export default class FilteredSearchManager {
const id = parseInt(value, 10);
if (usernameParams[id]) {
hasFilteredSearch = true;
- const tokenName = 'author';
+ const tokenName = TOKEN_TYPE_AUTHOR;
const canEdit = this.canEdit && this.canEdit(tokenName);
const operator = FilteredSearchVisualTokens.getOperatorToken(usernameParams[id]);
const valueToken = FilteredSearchVisualTokens.getValueToken(usernameParams[id]);
diff --git a/app/assets/javascripts/filtered_search/filtered_search_visual_tokens.js b/app/assets/javascripts/filtered_search/filtered_search_visual_tokens.js
index 0c01220a7be..4994559e923 100644
--- a/app/assets/javascripts/filtered_search/filtered_search_visual_tokens.js
+++ b/app/assets/javascripts/filtered_search/filtered_search_visual_tokens.js
@@ -1,5 +1,6 @@
import { spriteIcon } from '~/lib/utils/common_utils';
import { objectToQuery } from '~/lib/utils/url_utility';
+import { FILTERED_SEARCH_TERM } from '~/vue_shared/components/filtered_search_bar/constants';
import FilteredSearchContainer from './container';
import VisualTokenValue from './visual_token_value';
@@ -38,7 +39,7 @@ export default class FilteredSearchVisualTokens {
lastVisualToken,
isLastVisualTokenValid:
lastVisualToken === null ||
- lastVisualToken.className.indexOf('filtered-search-term') !== -1 ||
+ lastVisualToken.className.indexOf(FILTERED_SEARCH_TERM) !== -1 ||
(lastVisualToken &&
lastVisualToken.querySelector('.operator') !== null &&
lastVisualToken.querySelector('.value') !== null),
@@ -113,7 +114,7 @@ export default class FilteredSearchVisualTokens {
} = options;
const li = document.createElement('li');
li.classList.add('js-visual-token');
- li.classList.add(isSearchTerm ? 'filtered-search-term' : 'filtered-search-token');
+ li.classList.add(isSearchTerm ? FILTERED_SEARCH_TERM : 'filtered-search-token');
if (!isSearchTerm) {
li.classList.add(tokenClass);
@@ -239,7 +240,7 @@ export default class FilteredSearchVisualTokens {
static addSearchVisualToken(searchTerm) {
const { lastVisualToken } = FilteredSearchVisualTokens.getLastVisualTokenBeforeInput();
- if (lastVisualToken && lastVisualToken.classList.contains('filtered-search-term')) {
+ if (lastVisualToken && lastVisualToken.classList.contains(FILTERED_SEARCH_TERM)) {
lastVisualToken.querySelector('.name').textContent += ` ${searchTerm}`;
} else {
FilteredSearchVisualTokens.addVisualTokenElement({
diff --git a/app/assets/javascripts/filtered_search/issuable_filtered_search_token_keys.js b/app/assets/javascripts/filtered_search/issuable_filtered_search_token_keys.js
index d6e7887f93f..8aa99ec52f9 100644
--- a/app/assets/javascripts/filtered_search/issuable_filtered_search_token_keys.js
+++ b/app/assets/javascripts/filtered_search/issuable_filtered_search_token_keys.js
@@ -7,13 +7,20 @@ import {
TOKEN_TITLE_MILESTONE,
TOKEN_TITLE_MY_REACTION,
TOKEN_TITLE_RELEASE,
+ TOKEN_TYPE_ASSIGNEE,
+ TOKEN_TYPE_AUTHOR,
+ TOKEN_TYPE_LABEL,
+ TOKEN_TYPE_MILESTONE,
+ TOKEN_TYPE_MY_REACTION,
+ TOKEN_TYPE_RELEASE,
+ TOKEN_TYPE_REVIEWER,
} from '~/vue_shared/components/filtered_search_bar/constants';
import FilteredSearchTokenKeys from './filtered_search_token_keys';
export const tokenKeys = [
{
formattedKey: TOKEN_TITLE_AUTHOR,
- key: 'author',
+ key: TOKEN_TYPE_AUTHOR,
type: 'string',
param: 'username',
symbol: '@',
@@ -22,7 +29,7 @@ export const tokenKeys = [
},
{
formattedKey: TOKEN_TITLE_ASSIGNEE,
- key: 'assignee',
+ key: TOKEN_TYPE_ASSIGNEE,
type: 'string',
param: 'username',
symbol: '@',
@@ -31,7 +38,7 @@ export const tokenKeys = [
},
{
formattedKey: TOKEN_TITLE_MILESTONE,
- key: 'milestone',
+ key: TOKEN_TYPE_MILESTONE,
type: 'string',
param: 'title',
symbol: '%',
@@ -40,7 +47,7 @@ export const tokenKeys = [
},
{
formattedKey: TOKEN_TITLE_RELEASE,
- key: 'release',
+ key: TOKEN_TYPE_RELEASE,
type: 'string',
param: 'tag',
symbol: '',
@@ -49,7 +56,7 @@ export const tokenKeys = [
},
{
formattedKey: TOKEN_TITLE_LABEL,
- key: 'label',
+ key: TOKEN_TYPE_LABEL,
type: 'array',
param: 'name[]',
symbol: '~',
@@ -62,7 +69,7 @@ if (gon.current_user_id) {
// Appending tokenkeys only logged-in
tokenKeys.push({
formattedKey: TOKEN_TITLE_MY_REACTION,
- key: 'my-reaction',
+ key: TOKEN_TYPE_MY_REACTION,
type: 'string',
param: 'emoji',
symbol: '',
@@ -74,7 +81,7 @@ if (gon.current_user_id) {
export const alternativeTokenKeys = [
{
formattedKey: TOKEN_TITLE_LABEL,
- key: 'label',
+ key: TOKEN_TYPE_LABEL,
type: 'string',
param: 'name',
symbol: '~',
@@ -85,77 +92,77 @@ export const conditions = flattenDeep(
[
{
url: 'assignee_id=None',
- tokenKey: 'assignee',
+ tokenKey: TOKEN_TYPE_ASSIGNEE,
value: __('None'),
},
{
url: 'assignee_id=Any',
- tokenKey: 'assignee',
+ tokenKey: TOKEN_TYPE_ASSIGNEE,
value: __('Any'),
},
{
url: 'reviewer_id=None',
- tokenKey: 'reviewer',
+ tokenKey: TOKEN_TYPE_REVIEWER,
value: __('None'),
},
{
url: 'reviewer_id=Any',
- tokenKey: 'reviewer',
+ tokenKey: TOKEN_TYPE_REVIEWER,
value: __('Any'),
},
{
url: 'author_username=support-bot',
- tokenKey: 'author',
+ tokenKey: TOKEN_TYPE_AUTHOR,
value: 'support-bot',
},
{
url: 'milestone_title=None',
- tokenKey: 'milestone',
+ tokenKey: TOKEN_TYPE_MILESTONE,
value: __('None'),
},
{
url: 'milestone_title=Any',
- tokenKey: 'milestone',
+ tokenKey: TOKEN_TYPE_MILESTONE,
value: __('Any'),
},
{
url: 'milestone_title=%23upcoming',
- tokenKey: 'milestone',
+ tokenKey: TOKEN_TYPE_MILESTONE,
value: __('Upcoming'),
},
{
url: 'milestone_title=%23started',
- tokenKey: 'milestone',
+ tokenKey: TOKEN_TYPE_MILESTONE,
value: __('Started'),
},
{
url: 'release_tag=None',
- tokenKey: 'release',
+ tokenKey: TOKEN_TYPE_RELEASE,
value: __('None'),
},
{
url: 'release_tag=Any',
- tokenKey: 'release',
+ tokenKey: TOKEN_TYPE_RELEASE,
value: __('Any'),
},
{
url: 'label_name[]=None',
- tokenKey: 'label',
+ tokenKey: TOKEN_TYPE_LABEL,
value: __('None'),
},
{
url: 'label_name[]=Any',
- tokenKey: 'label',
+ tokenKey: TOKEN_TYPE_LABEL,
value: __('Any'),
},
{
url: 'my_reaction_emoji=None',
- tokenKey: 'my-reaction',
+ tokenKey: TOKEN_TYPE_MY_REACTION,
value: __('None'),
},
{
url: 'my_reaction_emoji=Any',
- tokenKey: 'my-reaction',
+ tokenKey: TOKEN_TYPE_MY_REACTION,
value: __('Any'),
},
].map((condition) => {
diff --git a/app/assets/javascripts/filtered_search/visual_token_value.js b/app/assets/javascripts/filtered_search/visual_token_value.js
index 1ad2006d689..33fda7533e4 100644
--- a/app/assets/javascripts/filtered_search/visual_token_value.js
+++ b/app/assets/javascripts/filtered_search/visual_token_value.js
@@ -8,6 +8,7 @@ import { createAlert } from '~/flash';
import AjaxCache from '~/lib/utils/ajax_cache';
import UsersCache from '~/lib/utils/users_cache';
import { __ } from '~/locale';
+import { TOKEN_TYPE_LABEL } from '~/vue_shared/components/filtered_search_bar/constants';
export default class VisualTokenValue {
constructor(tokenValue, tokenType, tokenOperator) {
@@ -23,7 +24,7 @@ export default class VisualTokenValue {
return;
}
- if (tokenType === 'label') {
+ if (tokenType === TOKEN_TYPE_LABEL) {
this.updateLabelTokenColor(tokenValueContainer);
} else if (USER_TOKEN_TYPES.includes(tokenType)) {
this.updateUserTokenAppearance(tokenValueContainer, tokenValueElement);
diff --git a/app/assets/javascripts/flash.js b/app/assets/javascripts/flash.js
index dc6c4642e94..9e804b60d59 100644
--- a/app/assets/javascripts/flash.js
+++ b/app/assets/javascripts/flash.js
@@ -114,6 +114,7 @@ const addDismissFlashClickListener = (flashEl, fadeTransition) => {
* @param {object} [options.parent] - Reference to parent element under which alert needs to appear. Defaults to `document`.
* @param {Function} [options.onDismiss] - Handler to call when this alert is dismissed.
* @param {string} [options.containerSelector] - Selector for the container of the alert
+ * @param {boolean} [options.preservePrevious] - Set to `true` to preserve previous alerts. Defaults to `false`.
* @param {object} [options.primaryButton] - Object describing primary button of alert
* @param {string} [options.primaryButton.link] - Href of primary button
* @param {string} [options.primaryButton.text] - Text of primary button
@@ -131,6 +132,7 @@ const createAlert = function createAlert({
variant = VARIANT_DANGER,
parent = document,
containerSelector = '.flash-container',
+ preservePrevious = false,
primaryButton = null,
secondaryButton = null,
onDismiss = null,
@@ -143,7 +145,11 @@ const createAlert = function createAlert({
if (!alertContainer) return null;
const el = document.createElement('div');
- alertContainer.appendChild(el);
+ if (preservePrevious) {
+ alertContainer.appendChild(el);
+ } else {
+ alertContainer.replaceChildren(el);
+ }
return new Vue({
el,
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 33ab1d5cd7f..89b6885091c 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,6 +1,7 @@
<script>
-import { GlButton, GlSafeHtmlDirective } from '@gitlab/ui';
+import { GlButton } from '@gitlab/ui';
import { snakeCase } from 'lodash';
+import SafeHtml from '~/vue_shared/directives/safe_html';
import highlight from '~/lib/utils/highlight';
import { truncateNamespace } from '~/lib/utils/text_utility';
import { mapVuexModuleState } from '~/lib/utils/vuex_module_mappers';
@@ -15,7 +16,7 @@ export default {
ProjectAvatar,
},
directives: {
- SafeHtml: GlSafeHtmlDirective,
+ SafeHtml,
},
mixins: [trackingMixin],
inject: ['vuexModule'],
diff --git a/app/assets/javascripts/gfm_auto_complete.js b/app/assets/javascripts/gfm_auto_complete.js
index 49c47e9d778..293cd2df16f 100644
--- a/app/assets/javascripts/gfm_auto_complete.js
+++ b/app/assets/javascripts/gfm_auto_complete.js
@@ -538,7 +538,12 @@ class GfmAutoComplete {
setupLabels($input) {
const instance = this;
const fetchData = this.fetchData.bind(this);
- const LABEL_COMMAND = { LABEL: '/label', UNLABEL: '/unlabel', RELABEL: '/relabel' };
+ const LABEL_COMMAND = {
+ LABEL: '/label',
+ LABELS: '/labels',
+ UNLABEL: '/unlabel',
+ RELABEL: '/relabel',
+ };
let command = '';
$input.atwho({
@@ -570,13 +575,9 @@ class GfmAutoComplete {
matcher(flag, subtext) {
const subtextNodes = subtext.split(/\n+/g).pop().split(GfmAutoComplete.regexSubtext);
- // Check if ~ is followed by '/label', '/relabel' or '/unlabel' commands.
+ // Check if ~ is followed by '/label', '/labels', '/relabel' or '/unlabel' commands.
command = subtextNodes.find((node) => {
- if (
- node === LABEL_COMMAND.LABEL ||
- node === LABEL_COMMAND.RELABEL ||
- node === LABEL_COMMAND.UNLABEL
- ) {
+ if (Object.values(LABEL_COMMAND).includes(node)) {
return node;
}
return null;
@@ -621,7 +622,7 @@ class GfmAutoComplete {
// The `LABEL_COMMAND.RELABEL` is intentionally skipped
// because we want to return all the labels (unfiltered) for that command.
- if (command === LABEL_COMMAND.LABEL) {
+ if (command === LABEL_COMMAND.LABEL || command === LABEL_COMMAND.LABELS) {
// Return labels with set: undefined.
return data.filter((label) => !label.set);
} else if (command === LABEL_COMMAND.UNLABEL) {
@@ -996,7 +997,7 @@ GfmAutoComplete.Issues = {
return value.reference || '${atwho-at}${id}';
},
templateFunction({ id, title, reference }) {
- return `<li><small>${reference || id}</small> ${escape(title)}</li>`;
+ return `<li><small>${escape(reference || id)}</small> ${escape(title)}</li>`;
},
};
// Milestones
diff --git a/app/assets/javascripts/gitlab_pages/components/pages_pipeline_wizard.vue b/app/assets/javascripts/gitlab_pages/components/pages_pipeline_wizard.vue
index f17a05999b0..bf71f682048 100644
--- a/app/assets/javascripts/gitlab_pages/components/pages_pipeline_wizard.vue
+++ b/app/assets/javascripts/gitlab_pages/components/pages_pipeline_wizard.vue
@@ -2,7 +2,7 @@
import { GlLoadingIcon } from '@gitlab/ui';
import { captureException } from '@sentry/browser';
import PipelineWizard from '~/pipeline_wizard/pipeline_wizard.vue';
-import PagesWizardTemplate from '~/pipeline_wizard/templates/pages.yml';
+import PagesWizardTemplate from '~/pipeline_wizard/templates/pages.yml?raw';
import { logError } from '~/lib/logger';
import { s__ } from '~/locale';
import { redirectTo } from '~/lib/utils/url_utility';
diff --git a/app/assets/javascripts/gitlab_version_check/components/security_patch_upgrade_alert.vue b/app/assets/javascripts/gitlab_version_check/components/security_patch_upgrade_alert.vue
new file mode 100644
index 00000000000..89dc68ec73e
--- /dev/null
+++ b/app/assets/javascripts/gitlab_version_check/components/security_patch_upgrade_alert.vue
@@ -0,0 +1,76 @@
+<script>
+import { GlAlert, GlSprintf, GlLink, GlButton } from '@gitlab/ui';
+import { s__ } from '~/locale';
+import Tracking from '~/tracking';
+import { UPGRADE_DOCS_URL, ABOUT_RELEASES_PAGE } from '../constants';
+
+export default {
+ name: 'SecurityPatchUpgradeAlert',
+ i18n: {
+ alertTitle: s__('VersionCheck|Critical security upgrade available'),
+ alertBody: s__(
+ 'VersionCheck|You are currently on version %{currentVersion}. We strongly recommend upgrading your GitLab installation. %{link}',
+ ),
+ learnMore: s__('VersionCheck|Learn more about this critical security release.'),
+ primaryButtonText: s__('VersionCheck|Upgrade now'),
+ },
+ components: {
+ GlAlert,
+ GlSprintf,
+ GlLink,
+ GlButton,
+ },
+ mixins: [Tracking.mixin()],
+ props: {
+ currentVersion: {
+ type: String,
+ required: true,
+ },
+ },
+ mounted() {
+ this.track('render', {
+ label: 'security_patch_upgrade_alert',
+ property: this.currentVersion,
+ });
+ },
+ methods: {
+ trackLearnMoreClick() {
+ this.track('click_link', {
+ label: 'security_patch_upgrade_alert_learn_more',
+ property: this.currentVersion,
+ });
+ },
+ trackUpgradeNowClick() {
+ this.track('click_link', {
+ label: 'security_patch_upgrade_alert_upgrade_now',
+ property: this.currentVersion,
+ });
+ },
+ },
+ UPGRADE_DOCS_URL,
+ ABOUT_RELEASES_PAGE,
+};
+</script>
+
+<template>
+ <gl-alert :title="$options.i18n.alertTitle" variant="danger" :dismissible="false">
+ <gl-sprintf :message="$options.i18n.alertBody">
+ <template #currentVersion>
+ <span class="gl-font-weight-bold">{{ currentVersion }}</span>
+ </template>
+ <template #link>
+ <gl-link :href="$options.ABOUT_RELEASES_PAGE" @click="trackLearnMoreClick">{{
+ $options.i18n.learnMore
+ }}</gl-link>
+ </template>
+ </gl-sprintf>
+ <template #actions>
+ <gl-button
+ :href="$options.UPGRADE_DOCS_URL"
+ variant="confirm"
+ @click="trackUpgradeNowClick"
+ >{{ $options.i18n.primaryButtonText }}</gl-button
+ >
+ </template>
+ </gl-alert>
+</template>
diff --git a/app/assets/javascripts/gitlab_version_check/components/security_patch_upgrade_alert_modal.vue b/app/assets/javascripts/gitlab_version_check/components/security_patch_upgrade_alert_modal.vue
new file mode 100644
index 00000000000..4638ba8a268
--- /dev/null
+++ b/app/assets/javascripts/gitlab_version_check/components/security_patch_upgrade_alert_modal.vue
@@ -0,0 +1,160 @@
+<script>
+import { GlModal, GlSprintf, GlLink, GlButton } from '@gitlab/ui';
+import SafeHtml from '~/vue_shared/directives/safe_html';
+import { glEmojiTag } from '~/emoji';
+import { s__, sprintf } from '~/locale';
+import Tracking from '~/tracking';
+import { getHideAlertModalCookie, setHideAlertModalCookie } from '../utils';
+import {
+ UPGRADE_DOCS_URL,
+ ABOUT_RELEASES_PAGE,
+ ALERT_MODAL_ID,
+ TRACKING_ACTIONS,
+ TRACKING_LABELS,
+} from '../constants';
+
+export default {
+ name: 'SecurityPatchUpgradeAlertModal',
+ i18n: {
+ modalTitle: s__('VersionCheck|Important notice - Critical security release'),
+ modalBodyNoStableVersions: s__(
+ 'VersionCheck|You are currently on version %{currentVersion}! We strongly recommend upgrading your GitLab installation immediately.',
+ ),
+ modalBodyStableVersions: s__(
+ 'VersionCheck|You are currently on version %{currentVersion}! We strongly recommend upgrading your GitLab installation to one of the following versions immediately: %{latestStableVersions}.',
+ ),
+ modalDetails: s__('VersionCheck|%{details}'),
+ learnMore: s__('VersionCheck|Learn more about this critical security release.'),
+ primaryButtonText: s__('VersionCheck|Upgrade now'),
+ secondaryButtonText: s__('VersionCheck|Remind me again in 3 days'),
+ },
+ components: {
+ GlModal,
+ GlSprintf,
+ GlLink,
+ GlButton,
+ },
+ directives: {
+ SafeHtml,
+ },
+ mixins: [Tracking.mixin()],
+ props: {
+ currentVersion: {
+ type: String,
+ required: true,
+ },
+ details: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ latestStableVersions: {
+ type: Array,
+ required: false,
+ default: () => [],
+ },
+ },
+ data() {
+ return {
+ visible: true,
+ };
+ },
+ computed: {
+ alertEmoji() {
+ return glEmojiTag('rotating_light');
+ },
+ modalBody() {
+ if (this.latestStableVersions?.length > 0) {
+ return this.$options.i18n.modalBodyStableVersions;
+ }
+
+ return this.$options.i18n.modalBodyNoStableVersions;
+ },
+ modalDetails() {
+ return sprintf(this.$options.i18n.modalDetails, { details: this.details });
+ },
+ latestStableVersionsStrings() {
+ return this.latestStableVersions?.length > 0 ? this.latestStableVersions.join(', ') : '';
+ },
+ },
+ created() {
+ if (getHideAlertModalCookie(this.currentVersion)) {
+ this.visible = false;
+ return;
+ }
+
+ this.dispatchTrackingEvent(TRACKING_ACTIONS.RENDER, TRACKING_LABELS.MODAL);
+ },
+ methods: {
+ dispatchTrackingEvent(action, label) {
+ this.track(action, {
+ label,
+ property: this.currentVersion,
+ });
+ },
+ trackLearnMoreClick() {
+ this.dispatchTrackingEvent(TRACKING_ACTIONS.CLICK_LINK, TRACKING_LABELS.LEARN_MORE_LINK);
+ },
+ trackRemindMeLaterClick() {
+ this.dispatchTrackingEvent(TRACKING_ACTIONS.CLICK_BUTTON, TRACKING_LABELS.REMIND_ME_BTN);
+ setHideAlertModalCookie(this.currentVersion);
+ this.$refs.alertModal.hide();
+ },
+ trackUpgradeNowClick() {
+ this.dispatchTrackingEvent(TRACKING_ACTIONS.CLICK_LINK, TRACKING_LABELS.UPGRADE_BTN_LINK);
+ setHideAlertModalCookie(this.currentVersion);
+ },
+ trackModalDismissed() {
+ this.dispatchTrackingEvent(TRACKING_ACTIONS.CLICK_BUTTON, TRACKING_LABELS.DISMISS);
+ },
+ },
+ safeHtmlConfig: { ADD_TAGS: ['gl-emoji'] },
+ UPGRADE_DOCS_URL,
+ ABOUT_RELEASES_PAGE,
+ ALERT_MODAL_ID,
+};
+</script>
+
+<template>
+ <gl-modal
+ ref="alertModal"
+ :modal-id="$options.ALERT_MODAL_ID"
+ :visible="visible"
+ @close="trackModalDismissed"
+ >
+ <template #modal-title>
+ <span v-safe-html:[$options.safeHtmlConfig]="alertEmoji"></span>
+ <span data-testid="alert-modal-title">{{ $options.i18n.modalTitle }}</span>
+ </template>
+ <template #default>
+ <div data-testid="alert-modal-body" class="gl-mb-6">
+ <gl-sprintf :message="modalBody">
+ <template #currentVersion>
+ <span class="gl-font-weight-bold">{{ currentVersion }}</span>
+ </template>
+ <template #latestStableVersions>
+ <span class="gl-font-weight-bold">{{ latestStableVersionsStrings }}</span>
+ </template>
+ </gl-sprintf>
+ </div>
+ <div v-if="details" data-testid="alert-modal-details" class="gl-mb-6">
+ {{ modalDetails }}
+ </div>
+ <gl-link :href="$options.ABOUT_RELEASES_PAGE" @click="trackLearnMoreClick">{{
+ $options.i18n.learnMore
+ }}</gl-link>
+ </template>
+ <template #modal-footer>
+ <gl-button data-testid="alert-modal-remind-button" @click="trackRemindMeLaterClick">{{
+ $options.i18n.secondaryButtonText
+ }}</gl-button>
+ <gl-button
+ data-testid="alert-modal-upgrade-button"
+ :href="$options.UPGRADE_DOCS_URL"
+ variant="confirm"
+ @click="trackUpgradeNowClick"
+ >{{ $options.i18n.primaryButtonText }}</gl-button
+ >
+ </template>
+ </gl-modal>
+</template>
diff --git a/app/assets/javascripts/gitlab_version_check/constants.js b/app/assets/javascripts/gitlab_version_check/constants.js
index 259723a4e22..049397148ab 100644
--- a/app/assets/javascripts/gitlab_version_check/constants.js
+++ b/app/assets/javascripts/gitlab_version_check/constants.js
@@ -7,3 +7,25 @@ export const STATUS_TYPES = {
};
export const UPGRADE_DOCS_URL = helpPagePath('update/index');
+
+export const ABOUT_RELEASES_PAGE = 'https://about.gitlab.com/releases/categories/releases/';
+
+export const ALERT_MODAL_ID = 'security-patch-upgrade-alert-modal';
+
+export const COOKIE_EXPIRATION = 3;
+
+export const COOKIE_SUFFIX = '-hide-alert-modal';
+
+export const TRACKING_ACTIONS = {
+ RENDER: 'render',
+ CLICK_LINK: 'click_link',
+ CLICK_BUTTON: 'click_button',
+};
+
+export const TRACKING_LABELS = {
+ MODAL: 'security_patch_upgrade_alert_modal',
+ LEARN_MORE_LINK: 'security_patch_upgrade_alert_modal_learn_more',
+ REMIND_ME_BTN: 'security_patch_upgrade_alert_modal_remind_3_days',
+ UPGRADE_BTN_LINK: 'security_patch_upgrade_alert_modal_upgrade_now',
+ DISMISS: 'security_patch_upgrade_alert_modal_close',
+};
diff --git a/app/assets/javascripts/gitlab_version_check/index.js b/app/assets/javascripts/gitlab_version_check/index.js
index 203ce10ef57..edb7e9abe49 100644
--- a/app/assets/javascripts/gitlab_version_check/index.js
+++ b/app/assets/javascripts/gitlab_version_check/index.js
@@ -1,50 +1,98 @@
import Vue from 'vue';
-import * as Sentry from '@sentry/browser';
-import { parseBoolean } from '~/lib/utils/common_utils';
-import axios from '~/lib/utils/axios_utils';
-import { joinPaths } from '~/lib/utils/url_utility';
+import { parseBoolean, convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import GitlabVersionCheckBadge from './components/gitlab_version_check_badge.vue';
+import SecurityPatchUpgradeAlert from './components/security_patch_upgrade_alert.vue';
+import SecurityPatchUpgradeAlertModal from './components/security_patch_upgrade_alert_modal.vue';
-const mountGitlabVersionCheckBadge = ({ el, status }) => {
- const { size } = el.dataset;
+const mountGitlabVersionCheckBadge = (el) => {
+ const { size, version } = el.dataset;
const actionable = parseBoolean(el.dataset.actionable);
- return new Vue({
- el,
- render(createElement) {
- return createElement(GitlabVersionCheckBadge, {
- props: {
- size,
- actionable,
- status,
- },
- });
- },
- });
+ try {
+ const { severity } = JSON.parse(version);
+
+ // If no severity (status) data don't worry about rendering
+ if (!severity) {
+ return null;
+ }
+
+ return new Vue({
+ el,
+ render(createElement) {
+ return createElement(GitlabVersionCheckBadge, {
+ props: {
+ size,
+ actionable,
+ status: severity,
+ },
+ });
+ },
+ });
+ } catch {
+ return null;
+ }
};
-export default async () => {
- const versionCheckBadges = [...document.querySelectorAll('.js-gitlab-version-check-badge')];
+const mountSecurityPatchUpgradeAlert = (el) => {
+ const { currentVersion } = el.dataset;
- // If there are no version check elements, exit out
- if (versionCheckBadges?.length <= 0) {
+ try {
+ return new Vue({
+ el,
+ render(createElement) {
+ return createElement(SecurityPatchUpgradeAlert, {
+ props: {
+ currentVersion,
+ },
+ });
+ },
+ });
+ } catch {
return null;
}
+};
- const status = await axios
- .get(joinPaths('/', gon.relative_url_root, '/admin/version_check.json'))
- .then((res) => {
- return res.data?.severity;
- })
- .catch((e) => {
- Sentry.captureException(e);
- return null;
+const mountSecurityPatchUpgradeAlertModal = (el) => {
+ const { currentVersion, version } = el.dataset;
+
+ try {
+ const { details, latestStableVersions } = convertObjectPropsToCamelCase(JSON.parse(version));
+
+ return new Vue({
+ el,
+ render(createElement) {
+ return createElement(SecurityPatchUpgradeAlertModal, {
+ props: {
+ currentVersion,
+ details,
+ latestStableVersions,
+ },
+ });
+ },
});
+ } catch {
+ return null;
+ }
+};
+
+export default () => {
+ const renderedApps = [];
- // If we don't have a status there is nothing to render
- if (status) {
- return versionCheckBadges.map((el) => mountGitlabVersionCheckBadge({ el, status }));
+ const securityPatchUpgradeAlert = document.getElementById('js-security-patch-upgrade-alert');
+ const securityPatchUpgradeAlertModal = document.getElementById(
+ 'js-security-patch-upgrade-alert-modal',
+ );
+ const versionCheckBadges = [...document.querySelectorAll('.js-gitlab-version-check-badge')];
+
+ if (securityPatchUpgradeAlert) {
+ renderedApps.push(mountSecurityPatchUpgradeAlert(securityPatchUpgradeAlert));
}
- return null;
+ if (securityPatchUpgradeAlertModal) {
+ renderedApps.push(mountSecurityPatchUpgradeAlertModal(securityPatchUpgradeAlertModal));
+ }
+
+ renderedApps.push(...versionCheckBadges.map((el) => mountGitlabVersionCheckBadge(el)));
+
+ return renderedApps;
};
diff --git a/app/assets/javascripts/gitlab_version_check/utils.js b/app/assets/javascripts/gitlab_version_check/utils.js
new file mode 100644
index 00000000000..d2f4349483c
--- /dev/null
+++ b/app/assets/javascripts/gitlab_version_check/utils.js
@@ -0,0 +1,18 @@
+import { setCookie, getCookie, parseBoolean } from '~/lib/utils/common_utils';
+import { COOKIE_EXPIRATION, COOKIE_SUFFIX } from './constants';
+
+const buildKey = (currentVersion) => {
+ return `${currentVersion}${COOKIE_SUFFIX}`;
+};
+
+export const setHideAlertModalCookie = (currentVersion) => {
+ const key = buildKey(currentVersion);
+
+ setCookie(key, true, { expires: COOKIE_EXPIRATION });
+};
+
+export const getHideAlertModalCookie = (currentVersion) => {
+ const key = buildKey(currentVersion);
+
+ return parseBoolean(getCookie(key));
+};
diff --git a/app/assets/javascripts/graphql_shared/fragments/alert.fragment.graphql b/app/assets/javascripts/graphql_shared/fragments/alert.fragment.graphql
index 64f547f933a..3ecaee435e2 100644
--- a/app/assets/javascripts/graphql_shared/fragments/alert.fragment.graphql
+++ b/app/assets/javascripts/graphql_shared/fragments/alert.fragment.graphql
@@ -1,4 +1,5 @@
fragment AlertListItem on AlertManagementAlert {
+ id
iid
title
severity
diff --git a/app/assets/javascripts/graphql_shared/mutations/alert_status_update.mutation.graphql b/app/assets/javascripts/graphql_shared/mutations/alert_status_update.mutation.graphql
index ba1e607bc10..9ec87ba291d 100644
--- a/app/assets/javascripts/graphql_shared/mutations/alert_status_update.mutation.graphql
+++ b/app/assets/javascripts/graphql_shared/mutations/alert_status_update.mutation.graphql
@@ -4,6 +4,7 @@ mutation updateAlertStatus($projectPath: ID!, $status: AlertManagementStatus!, $
updateAlertStatus(input: { iid: $iid, status: $status, projectPath: $projectPath }) {
errors
alert {
+ id
iid
status
endedAt
diff --git a/app/assets/javascripts/graphql_shared/possible_types.json b/app/assets/javascripts/graphql_shared/possible_types.json
index e8b0174b8f6..5467105ac3c 100644
--- a/app/assets/javascripts/graphql_shared/possible_types.json
+++ b/app/assets/javascripts/graphql_shared/possible_types.json
@@ -7,10 +7,12 @@
"CiGroupVariable",
"CiInstanceVariable",
"CiManualVariable",
- "CiProjectVariable"
+ "CiProjectVariable",
+ "PipelineScheduleVariable"
],
"CommitSignature": [
"GpgSignature",
+ "SshSignature",
"X509Signature"
],
"CurrentUserTodos": [
@@ -144,10 +146,13 @@
"WorkItemWidget": [
"WorkItemWidgetAssignees",
"WorkItemWidgetDescription",
+ "WorkItemWidgetHealthStatus",
"WorkItemWidgetHierarchy",
"WorkItemWidgetIteration",
"WorkItemWidgetLabels",
"WorkItemWidgetMilestone",
+ "WorkItemWidgetNotes",
+ "WorkItemWidgetProgress",
"WorkItemWidgetStartAndDueDate",
"WorkItemWidgetStatus",
"WorkItemWidgetWeight"
diff --git a/app/assets/javascripts/graphql_shared/queries/alert_details.query.graphql b/app/assets/javascripts/graphql_shared/queries/alert_details.query.graphql
index 8debc6113d1..77b95bb8910 100644
--- a/app/assets/javascripts/graphql_shared/queries/alert_details.query.graphql
+++ b/app/assets/javascripts/graphql_shared/queries/alert_details.query.graphql
@@ -5,6 +5,7 @@ query alertDetails($fullPath: ID!, $alertId: String) {
id
alertManagementAlerts(iid: $alertId) {
nodes {
+ id
...AlertDetailItem
}
}
diff --git a/app/assets/javascripts/groups/components/app.vue b/app/assets/javascripts/groups/components/app.vue
index 15f5a3518a5..46d5341ea97 100644
--- a/app/assets/javascripts/groups/components/app.vue
+++ b/app/assets/javascripts/groups/components/app.vue
@@ -1,21 +1,25 @@
<script>
-import { GlLoadingIcon, GlModal } from '@gitlab/ui';
+import { GlLoadingIcon, GlModal, GlEmptyState } from '@gitlab/ui';
import { createAlert } from '~/flash';
import { mergeUrlParams, getParameterByName } from '~/lib/utils/url_utility';
-import { HIDDEN_CLASS } from '~/lib/utils/constants';
import { __, s__, sprintf } from '~/locale';
-import { COMMON_STR, CONTENT_LIST_CLASS } from '../constants';
+import { COMMON_STR } from '../constants';
import eventHub from '../event_hub';
import GroupsComponent from './groups.vue';
-import EmptyState from './empty_state.vue';
export default {
+ i18n: {
+ searchEmptyState: {
+ title: __('No results found'),
+ description: __('Edit your search and try again'),
+ },
+ },
components: {
GroupsComponent,
GlModal,
GlLoadingIcon,
- EmptyState,
+ GlEmptyState,
},
props: {
action: {
@@ -40,20 +44,14 @@ export default {
type: Boolean,
required: true,
},
- renderEmptyState: {
- type: Boolean,
- required: false,
- default: false,
- },
},
data() {
return {
isModalVisible: false,
isLoading: true,
- isSearchEmpty: false,
+ fromSearch: false,
targetGroup: null,
targetParentGroup: null,
- showEmptyState: false,
};
},
computed: {
@@ -79,6 +77,9 @@ export default {
groups() {
return this.store.getGroups();
},
+ hasGroups() {
+ return this.groups && this.groups.length > 0;
+ },
pageInfo() {
return this.store.getPaginationInfo();
},
@@ -231,47 +232,17 @@ export default {
this.targetGroup.isBeingRemoved = false;
});
},
- showLegacyEmptyState() {
- const { containerEl } = this;
-
- if (!containerEl) return;
-
- const contentListEl = containerEl.querySelector(CONTENT_LIST_CLASS);
- const emptyStateEl = containerEl.querySelector('.empty-state');
-
- if (contentListEl) {
- contentListEl.remove();
- }
-
- if (emptyStateEl) {
- emptyStateEl.classList.remove(HIDDEN_CLASS);
- }
- },
updatePagination(headers) {
this.store.setPaginationInfo(headers);
},
updateGroups(groups, fromSearch) {
- const hasGroups = groups && groups.length > 0;
-
- if (this.renderEmptyState) {
- this.isSearchEmpty = fromSearch && !hasGroups;
- } else {
- this.isSearchEmpty = !hasGroups;
- }
+ this.fromSearch = fromSearch;
if (fromSearch) {
this.store.setSearchedGroups(groups);
} else {
this.store.setGroups(groups);
}
-
- if (this.action && !hasGroups && !fromSearch) {
- if (this.renderEmptyState) {
- this.showEmptyState = true;
- } else {
- this.showLegacyEmptyState();
- }
- }
},
},
};
@@ -285,14 +256,16 @@ export default {
size="lg"
class="loading-animation prepend-top-20"
/>
- <groups-component
- v-else
- :groups="groups"
- :search-empty="isSearchEmpty"
- :page-info="pageInfo"
- :action="action"
- />
- <empty-state v-if="showEmptyState" />
+ <template v-else>
+ <groups-component v-if="hasGroups" :groups="groups" :page-info="pageInfo" :action="action" />
+ <gl-empty-state
+ v-else-if="fromSearch"
+ :title="$options.i18n.searchEmptyState.title"
+ :description="$options.i18n.searchEmptyState.description"
+ data-testid="search-empty-state"
+ />
+ <slot v-else name="empty-state"></slot>
+ </template>
<gl-modal
modal-id="leave-group-modal"
:visible="isModalVisible"
diff --git a/app/assets/javascripts/groups/components/empty_states/archived_projects_empty_state.vue b/app/assets/javascripts/groups/components/empty_states/archived_projects_empty_state.vue
new file mode 100644
index 00000000000..535758750f9
--- /dev/null
+++ b/app/assets/javascripts/groups/components/empty_states/archived_projects_empty_state.vue
@@ -0,0 +1,21 @@
+<script>
+import { GlEmptyState } from '@gitlab/ui';
+
+import { s__ } from '~/locale';
+
+export default {
+ components: { GlEmptyState },
+ i18n: {
+ title: s__('GroupsEmptyState|No archived projects.'),
+ },
+ inject: ['newProjectIllustration'],
+};
+</script>
+
+<template>
+ <gl-empty-state
+ :title="$options.i18n.title"
+ :svg-path="newProjectIllustration"
+ :svg-height="100"
+ />
+</template>
diff --git a/app/assets/javascripts/groups/components/empty_states/shared_projects_empty_state.vue b/app/assets/javascripts/groups/components/empty_states/shared_projects_empty_state.vue
new file mode 100644
index 00000000000..7223321bf3e
--- /dev/null
+++ b/app/assets/javascripts/groups/components/empty_states/shared_projects_empty_state.vue
@@ -0,0 +1,21 @@
+<script>
+import { GlEmptyState } from '@gitlab/ui';
+
+import { s__ } from '~/locale';
+
+export default {
+ components: { GlEmptyState },
+ i18n: {
+ title: s__('GroupsEmptyState|No shared projects.'),
+ },
+ inject: ['newProjectIllustration'],
+};
+</script>
+
+<template>
+ <gl-empty-state
+ :title="$options.i18n.title"
+ :svg-path="newProjectIllustration"
+ :svg-height="100"
+ />
+</template>
diff --git a/app/assets/javascripts/groups/components/empty_state.vue b/app/assets/javascripts/groups/components/empty_states/subgroups_and_projects_empty_state.vue
index 4219b52737d..955cb1ca63e 100644
--- a/app/assets/javascripts/groups/components/empty_state.vue
+++ b/app/assets/javascripts/groups/components/empty_states/subgroups_and_projects_empty_state.vue
@@ -83,7 +83,6 @@ export default {
</div>
<gl-empty-state
v-else
- class="gl-mt-5"
:title="$options.i18n.withoutLinks.title"
:svg-path="emptySubgroupIllustration"
:description="$options.i18n.withoutLinks.description"
diff --git a/app/assets/javascripts/groups/components/group_item.vue b/app/assets/javascripts/groups/components/group_item.vue
index 961af800971..d9781ef9c84 100644
--- a/app/assets/javascripts/groups/components/group_item.vue
+++ b/app/assets/javascripts/groups/components/group_item.vue
@@ -9,8 +9,8 @@ import {
GlPopover,
GlLink,
GlTooltipDirective,
- GlSafeHtmlDirective,
} from '@gitlab/ui';
+import SafeHtml from '~/vue_shared/directives/safe_html';
import { visitUrl } from '~/lib/utils/url_utility';
import UserAccessRoleBadge from '~/vue_shared/components/user_access_role_badge.vue';
import { AVATAR_SHAPE_OPTION_RECT } from '~/vue_shared/constants';
@@ -29,7 +29,7 @@ import ItemTypeIcon from './item_type_icon.vue';
export default {
directives: {
GlTooltip: GlTooltipDirective,
- SafeHtml: GlSafeHtmlDirective,
+ SafeHtml,
},
components: {
GlAvatar,
@@ -200,11 +200,9 @@ export default {
class="no-expand gl-mr-3 gl-text-gray-900!"
:itemprop="microdata.nameItemprop"
>
- {{
- // ending bracket must be by closing tag to prevent
- // link hover text-decoration from over-extending
- group.name
- }}
+ <!-- ending bracket must be by closing tag to prevent -->
+ <!-- link hover text-decoration from over-extending -->
+ {{ group.name }}
</a>
<gl-icon
v-gl-tooltip.hover.bottom
diff --git a/app/assets/javascripts/groups/components/group_name_and_path.vue b/app/assets/javascripts/groups/components/group_name_and_path.vue
index 9a1ea2f1812..5f997ecc7ba 100644
--- a/app/assets/javascripts/groups/components/group_name_and_path.vue
+++ b/app/assets/javascripts/groups/components/group_name_and_path.vue
@@ -59,7 +59,7 @@ export default {
learnMore: s__('Groups|Learn more'),
},
inputSize: { md: 'lg' },
- changingGroupPathHelpPagePath: helpPagePath('user/group/index', {
+ changingGroupPathHelpPagePath: helpPagePath('user/group/manage', {
anchor: 'change-a-groups-path',
}),
mattermostDataBindName: 'create_chat_team',
diff --git a/app/assets/javascripts/groups/components/groups.vue b/app/assets/javascripts/groups/components/groups.vue
index 43aa0753082..5075be62214 100644
--- a/app/assets/javascripts/groups/components/groups.vue
+++ b/app/assets/javascripts/groups/components/groups.vue
@@ -1,5 +1,4 @@
<script>
-import { GlEmptyState } from '@gitlab/ui';
import PaginationLinks from '~/vue_shared/components/pagination_links.vue';
import { getParameterByName } from '~/lib/utils/url_utility';
import { __ } from '~/locale';
@@ -12,7 +11,6 @@ export default {
},
components: {
PaginationLinks,
- GlEmptyState,
},
props: {
groups: {
@@ -23,10 +21,6 @@ export default {
type: Object,
required: true,
},
- searchEmpty: {
- type: Boolean,
- required: true,
- },
action: {
type: String,
required: false,
@@ -46,18 +40,11 @@ export default {
<template>
<div class="groups-list-tree-container" data-qa-selector="groups_list_tree_container">
- <gl-empty-state
- v-if="searchEmpty"
- :title="$options.i18n.emptyStateTitle"
- :description="$options.i18n.emptyStateDescription"
+ <group-folder :groups="groups" :action="action" />
+ <pagination-links
+ :change="change"
+ :page-info="pageInfo"
+ class="d-flex justify-content-center gl-mt-3"
/>
- <template v-else>
- <group-folder :groups="groups" :action="action" />
- <pagination-links
- :change="change"
- :page-info="pageInfo"
- class="d-flex justify-content-center gl-mt-3"
- />
- </template>
</div>
</template>
diff --git a/app/assets/javascripts/groups/components/overview_tabs.vue b/app/assets/javascripts/groups/components/overview_tabs.vue
index 46ab30367a0..79a2e11b0bb 100644
--- a/app/assets/javascripts/groups/components/overview_tabs.vue
+++ b/app/assets/javascripts/groups/components/overview_tabs.vue
@@ -13,19 +13,32 @@ import {
} from '../constants';
import eventHub from '../event_hub';
import GroupsApp from './app.vue';
+import SubgroupsAndProjectsEmptyState from './empty_states/subgroups_and_projects_empty_state.vue';
+import SharedProjectsEmptyState from './empty_states/shared_projects_empty_state.vue';
+import ArchivedProjectsEmptyState from './empty_states/archived_projects_empty_state.vue';
const [SORTING_ITEM_NAME] = OVERVIEW_TABS_SORTING_ITEMS;
const MIN_SEARCH_LENGTH = 3;
export default {
- components: { GlTabs, GlTab, GroupsApp, GlSearchBoxByType, GlSorting, GlSortingItem },
+ components: {
+ GlTabs,
+ GlTab,
+ GroupsApp,
+ GlSearchBoxByType,
+ GlSorting,
+ GlSortingItem,
+ SubgroupsAndProjectsEmptyState,
+ SharedProjectsEmptyState,
+ ArchivedProjectsEmptyState,
+ },
inject: ['endpoints', 'initialSort'],
data() {
const tabs = [
{
title: this.$options.i18n[ACTIVE_TAB_SUBGROUPS_AND_PROJECTS],
key: ACTIVE_TAB_SUBGROUPS_AND_PROJECTS,
- renderEmptyState: true,
+ emptyStateComponent: SubgroupsAndProjectsEmptyState,
lazy: this.$route.name !== ACTIVE_TAB_SUBGROUPS_AND_PROJECTS,
service: new GroupsService(this.endpoints[ACTIVE_TAB_SUBGROUPS_AND_PROJECTS]),
store: new GroupsStore({ showSchemaMarkup: true }),
@@ -33,7 +46,7 @@ export default {
{
title: this.$options.i18n[ACTIVE_TAB_SHARED],
key: ACTIVE_TAB_SHARED,
- renderEmptyState: false,
+ emptyStateComponent: SharedProjectsEmptyState,
lazy: this.$route.name !== ACTIVE_TAB_SHARED,
service: new GroupsService(this.endpoints[ACTIVE_TAB_SHARED]),
store: new GroupsStore(),
@@ -41,7 +54,7 @@ export default {
{
title: this.$options.i18n[ACTIVE_TAB_ARCHIVED],
key: ACTIVE_TAB_ARCHIVED,
- renderEmptyState: false,
+ emptyStateComponent: ArchivedProjectsEmptyState,
lazy: this.$route.name !== ACTIVE_TAB_ARCHIVED,
service: new GroupsService(this.endpoints[ACTIVE_TAB_ARCHIVED]),
store: new GroupsStore(),
@@ -158,18 +171,16 @@ export default {
<template>
<gl-tabs content-class="gl-pt-0" :value="activeTabIndex" @input="handleTabInput">
<gl-tab
- v-for="{ key, title, renderEmptyState, lazy, service, store } in tabs"
+ v-for="{ key, title, emptyStateComponent, lazy, service, store } in tabs"
:key="key"
:title="title"
:lazy="lazy"
>
- <groups-app
- :action="key"
- :service="service"
- :store="store"
- :hide-projects="false"
- :render-empty-state="renderEmptyState"
- />
+ <groups-app :action="key" :service="service" :store="store" :hide-projects="false">
+ <template v-if="emptyStateComponent" #empty-state>
+ <component :is="emptyStateComponent" />
+ </template>
+ </groups-app>
</gl-tab>
<template #tabs-end>
<li class="gl-flex-grow-1 gl-align-self-center gl-w-full gl-lg-w-auto gl-py-2">
diff --git a/app/assets/javascripts/groups/components/transfer_group_form.vue b/app/assets/javascripts/groups/components/transfer_group_form.vue
index 15a193f7cb8..3da417ebf0a 100644
--- a/app/assets/javascripts/groups/components/transfer_group_form.vue
+++ b/app/assets/javascripts/groups/components/transfer_group_form.vue
@@ -73,6 +73,7 @@ export default {
:disabled="disableSubmitButton"
:phrase="confirmationPhrase"
:button-text="confirmButtonText"
+ button-qa-selector="transfer_group_button"
@confirm="$emit('confirm')"
/>
</div>
diff --git a/app/assets/javascripts/header.js b/app/assets/javascripts/header.js
index 4d03a523486..f58781fa9ec 100644
--- a/app/assets/javascripts/header.js
+++ b/app/assets/javascripts/header.js
@@ -1,7 +1,9 @@
import Vue from 'vue';
+import NewNavToggle from '~/nav/components/new_nav_toggle.vue';
import { highCountTrim } from '~/lib/utils/text_utility';
import Tracking from '~/tracking';
import Translate from '~/vue_shared/translate';
+import { parseBoolean } from '~/lib/utils/common_utils';
/**
* Updates todo counter when todos are toggled.
@@ -99,6 +101,7 @@ function trackShowUserDropdownLink(trackEvent, elToTrack, el) {
});
});
}
+
export function initNavUserDropdownTracking() {
const el = document.querySelector('.js-nav-user-dropdown');
const buyEl = document.querySelector('.js-buy-pipeline-minutes-link');
@@ -108,5 +111,23 @@ export function initNavUserDropdownTracking() {
}
}
+function initNewNavToggle() {
+ const el = document.querySelector('.js-new-nav-toggle');
+ if (!el) return false;
+
+ return new Vue({
+ el,
+ render(h) {
+ return h(NewNavToggle, {
+ props: {
+ enabled: parseBoolean(el.dataset.enabled),
+ endpoint: el.dataset.endpoint,
+ },
+ });
+ },
+ });
+}
+
requestIdleCallback(initStatusTriggers);
requestIdleCallback(initNavUserDropdownTracking);
+requestIdleCallback(initNewNavToggle);
diff --git a/app/assets/javascripts/header_search/components/app.vue b/app/assets/javascripts/header_search/components/app.vue
index 8fc0ce48e61..bf5daf29b21 100644
--- a/app/assets/javascripts/header_search/components/app.vue
+++ b/app/assets/javascripts/header_search/components/app.vue
@@ -4,7 +4,6 @@ import {
GlOutsideDirective as Outside,
GlIcon,
GlToken,
- GlSafeHtmlDirective as SafeHtml,
GlTooltipDirective,
GlResizeObserverDirective,
} from '@gitlab/ui';
@@ -56,7 +55,7 @@ export default {
false,
),
},
- directives: { SafeHtml, Outside, GlTooltip: GlTooltipDirective, GlResizeObserverDirective },
+ directives: { Outside, GlTooltip: GlTooltipDirective, GlResizeObserverDirective },
components: {
GlSearchBoxByType,
HeaderSearchDefaultItems,
diff --git a/app/assets/javascripts/header_search/components/header_search_autocomplete_items.vue b/app/assets/javascripts/header_search/components/header_search_autocomplete_items.vue
index 025c48f355d..c85fb4f4158 100644
--- a/app/assets/javascripts/header_search/components/header_search_autocomplete_items.vue
+++ b/app/assets/javascripts/header_search/components/header_search_autocomplete_items.vue
@@ -6,9 +6,9 @@ import {
GlAvatar,
GlAlert,
GlLoadingIcon,
- GlSafeHtmlDirective as SafeHtml,
} from '@gitlab/ui';
import { mapState, mapGetters } from 'vuex';
+import SafeHtml from '~/vue_shared/directives/safe_html';
import { s__ } from '~/locale';
import highlight from '~/lib/utils/highlight';
import { AVATAR_SHAPE_OPTION_RECT } from '~/vue_shared/constants';
diff --git a/app/assets/javascripts/header_search/constants.js b/app/assets/javascripts/header_search/constants.js
index 332ccee510f..cda3379309c 100644
--- a/app/assets/javascripts/header_search/constants.js
+++ b/app/assets/javascripts/header_search/constants.js
@@ -26,6 +26,8 @@ export const GROUPS_CATEGORY = s__('GlobalSearch|Groups');
export const PROJECTS_CATEGORY = s__('GlobalSearch|Projects');
+export const USERS_CATEGORY = s__('GlobalSearch|Users');
+
export const ISSUES_CATEGORY = s__('GlobalSearch|Recent issues');
export const MERGE_REQUEST_CATEGORY = s__('GlobalSearch|Recent merge requests');
@@ -68,6 +70,7 @@ export const DROPDOWN_ORDER = [
RECENT_EPICS_CATEGORY,
GROUPS_CATEGORY,
PROJECTS_CATEGORY,
+ USERS_CATEGORY,
IN_THIS_PROJECT_CATEGORY,
SETTINGS_CATEGORY,
HELP_CATEGORY,
diff --git a/app/assets/javascripts/ide/components/commit_sidebar/form.vue b/app/assets/javascripts/ide/components/commit_sidebar/form.vue
index d02dc67d933..ef3da57c240 100644
--- a/app/assets/javascripts/ide/components/commit_sidebar/form.vue
+++ b/app/assets/javascripts/ide/components/commit_sidebar/form.vue
@@ -1,6 +1,7 @@
<script>
-import { GlModal, GlSafeHtmlDirective, GlButton, GlTooltipDirective } from '@gitlab/ui';
+import { GlModal, GlButton, GlTooltipDirective } from '@gitlab/ui';
import { mapState, mapActions, mapGetters } from 'vuex';
+import SafeHtml from '~/vue_shared/directives/safe_html';
import { n__ } from '~/locale';
import { leftSidebarViews, MAX_WINDOW_HEIGHT_COMPACT } from '../../constants';
import { createUnexpectedCommitError } from '../../lib/errors';
@@ -17,7 +18,7 @@ export default {
GlButton,
},
directives: {
- SafeHtml: GlSafeHtmlDirective,
+ SafeHtml,
GlTooltip: GlTooltipDirective,
},
data() {
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 5272c4310d8..dd343bc5f79 100644
--- a/app/assets/javascripts/ide/components/commit_sidebar/success_message.vue
+++ b/app/assets/javascripts/ide/components/commit_sidebar/success_message.vue
@@ -1,6 +1,6 @@
<script>
-import { GlSafeHtmlDirective as SafeHtml } from '@gitlab/ui';
import { mapState } from 'vuex';
+import SafeHtml from '~/vue_shared/directives/safe_html';
export default {
directives: {
diff --git a/app/assets/javascripts/ide/components/error_message.vue b/app/assets/javascripts/ide/components/error_message.vue
index 67eedc6b37f..eba9bbcdf09 100644
--- a/app/assets/javascripts/ide/components/error_message.vue
+++ b/app/assets/javascripts/ide/components/error_message.vue
@@ -1,6 +1,7 @@
<script>
-import { GlAlert, GlLoadingIcon, GlSafeHtmlDirective } from '@gitlab/ui';
+import { GlAlert, GlLoadingIcon } from '@gitlab/ui';
import { mapActions } from 'vuex';
+import SafeHtml from '~/vue_shared/directives/safe_html';
export default {
components: {
@@ -8,7 +9,7 @@ export default {
GlLoadingIcon,
},
directives: {
- SafeHtml: GlSafeHtmlDirective,
+ SafeHtml,
},
props: {
message: {
diff --git a/app/assets/javascripts/ide/components/jobs/detail.vue b/app/assets/javascripts/ide/components/jobs/detail.vue
index 8d6a0b99e0c..9676233a443 100644
--- a/app/assets/javascripts/ide/components/jobs/detail.vue
+++ b/app/assets/javascripts/ide/components/jobs/detail.vue
@@ -1,7 +1,8 @@
<script>
-import { GlTooltipDirective, GlButton, GlIcon, GlSafeHtmlDirective } from '@gitlab/ui';
+import { GlTooltipDirective, GlButton, GlIcon } from '@gitlab/ui';
import { throttle } from 'lodash';
import { mapActions, mapState } from 'vuex';
+import SafeHtml from '~/vue_shared/directives/safe_html';
import { __ } from '~/locale';
import JobDescription from './detail/description.vue';
import ScrollButton from './detail/scroll_button.vue';
@@ -14,7 +15,7 @@ const scrollPositions = {
export default {
directives: {
GlTooltip: GlTooltipDirective,
- SafeHtml: GlSafeHtmlDirective,
+ SafeHtml,
},
components: {
GlButton,
diff --git a/app/assets/javascripts/ide/components/new_dropdown/index.vue b/app/assets/javascripts/ide/components/new_dropdown/index.vue
index 9a529bdcee1..ea1dbee4669 100644
--- a/app/assets/javascripts/ide/components/new_dropdown/index.vue
+++ b/app/assets/javascripts/ide/components/new_dropdown/index.vue
@@ -80,7 +80,7 @@ export default {
@click="createNewItem('blob')"
/>
</li>
- <li><upload :path="path" @create="createTempEntry" /></li>
+ <upload :path="path" @create="createTempEntry" />
<li>
<item-button
:label="__('New directory')"
diff --git a/app/assets/javascripts/ide/components/new_dropdown/upload.vue b/app/assets/javascripts/ide/components/new_dropdown/upload.vue
index 76d8a0aff3d..7c10e055e91 100644
--- a/app/assets/javascripts/ide/components/new_dropdown/upload.vue
+++ b/app/assets/javascripts/ide/components/new_dropdown/upload.vue
@@ -65,7 +65,7 @@ export default {
</script>
<template>
- <div>
+ <li>
<item-button
:class="buttonCssClasses"
:show-label="showLabel"
@@ -84,5 +84,5 @@ export default {
data-qa-selector="file_upload_field"
@change="openFile"
/>
- </div>
+ </li>
</template>
diff --git a/app/assets/javascripts/ide/components/panes/right.vue b/app/assets/javascripts/ide/components/panes/right.vue
index c74a5052573..da2d4fbe7f0 100644
--- a/app/assets/javascripts/ide/components/panes/right.vue
+++ b/app/assets/javascripts/ide/components/panes/right.vue
@@ -7,7 +7,6 @@ import PipelinesList from '../pipelines/list.vue';
import Clientside from '../preview/clientside.vue';
import ResizablePanel from '../resizable_panel.vue';
import TerminalView from '../terminal/view.vue';
-import SwitchEditorsView from '../switch_editors/switch_editors_view.vue';
import CollapsibleSidebar from './collapsible_sidebar.vue';
// Need to add the width of the nav buttons since the resizable container contains those as well
@@ -21,7 +20,7 @@ export default {
},
computed: {
...mapState('terminal', { isTerminalVisible: 'isVisible' }),
- ...mapState(['currentMergeRequestId', 'clientsidePreviewEnabled', 'canUseNewWebIde']),
+ ...mapState(['currentMergeRequestId', 'clientsidePreviewEnabled']),
...mapGetters(['packageJson']),
...mapState('rightPane', ['isOpen']),
showLivePreview() {
@@ -30,12 +29,6 @@ export default {
rightExtensionTabs() {
return [
{
- show: this.canUseNewWebIde,
- title: __('Switch editors'),
- views: [{ component: SwitchEditorsView, ...rightSidebarViews.switchEditors }],
- icon: 'bullhorn',
- },
- {
show: true,
title: __('Pipelines'),
views: [
@@ -60,7 +53,6 @@ export default {
},
},
WIDTH,
- SWITCH_EDITORS_VIEW_NAME: rightSidebarViews.switchEditors.name,
};
</script>
@@ -72,11 +64,6 @@ export default {
:min-size="$options.WIDTH"
:resizable="isOpen"
>
- <collapsible-sidebar
- class="gl-w-full"
- :extension-tabs="rightExtensionTabs"
- :init-open-view="$options.SWITCH_EDITORS_VIEW_NAME"
- side="right"
- />
+ <collapsible-sidebar class="gl-w-full" :extension-tabs="rightExtensionTabs" side="right" />
</resizable-panel>
</template>
diff --git a/app/assets/javascripts/ide/components/pipelines/list.vue b/app/assets/javascripts/ide/components/pipelines/list.vue
index 7f513afe82e..7f662f528d7 100644
--- a/app/assets/javascripts/ide/components/pipelines/list.vue
+++ b/app/assets/javascripts/ide/components/pipelines/list.vue
@@ -1,17 +1,8 @@
<script>
-import {
- GlLoadingIcon,
- GlIcon,
- GlSafeHtmlDirective as SafeHtml,
- GlTabs,
- GlTab,
- GlBadge,
- GlAlert,
-} from '@gitlab/ui';
-import { escape } from 'lodash';
+import { GlLoadingIcon, GlIcon, GlTabs, GlTab, GlBadge, GlAlert } from '@gitlab/ui';
import { mapActions, mapGetters, mapState } from 'vuex';
+import SafeHtml from '~/vue_shared/directives/safe_html';
import IDEServices from '~/ide/services';
-import { sprintf, __ } from '~/locale';
import CiIcon from '~/vue_shared/components/ci_icon.vue';
import JobsList from '../jobs/list.vue';
import EmptyState from './empty_state.vue';
@@ -48,16 +39,6 @@ export default {
'stages',
'isLoadingJobs',
]),
- ciLintText() {
- return sprintf(
- __('You can test your .gitlab-ci.yml in %{linkStart}CI Lint%{linkEnd}.'),
- {
- linkStart: `<a href="${escape(this.currentProject.web_url)}/-/ci/lint">`,
- linkEnd: '</a>',
- },
- false,
- );
- },
showLoadingIcon() {
return this.isLoadingPipeline && !this.hasLoadedPipeline;
},
@@ -101,9 +82,8 @@ export default {
:dismissible="false"
class="gl-mt-5"
>
- <p class="gl-mb-0">{{ __('Found errors in your .gitlab-ci.yml:') }}</p>
+ <p class="gl-mb-0">{{ __('Unable to create pipeline') }}</p>
<p class="gl-mb-0 break-word">{{ latestPipeline.yamlError }}</p>
- <p v-safe-html="ciLintText" class="gl-mb-0"></p>
</gl-alert>
<gl-tabs v-else>
<gl-tab :active="!pipelineFailed">
diff --git a/app/assets/javascripts/ide/components/repo_editor.vue b/app/assets/javascripts/ide/components/repo_editor.vue
index 5f35dbdc5e7..3c9c0b1ade1 100644
--- a/app/assets/javascripts/ide/components/repo_editor.vue
+++ b/app/assets/javascripts/ide/components/repo_editor.vue
@@ -7,6 +7,7 @@ import {
EDITOR_TYPE_CODE,
EDITOR_CODE_INSTANCE_FN,
EDITOR_DIFF_INSTANCE_FN,
+ EXTENSION_CI_SCHEMA_FILE_NAME_MATCH,
} from '~/editor/constants';
import { SourceEditorExtension } from '~/editor/extensions/source_editor_extension_base';
import { EditorWebIdeExtension } from '~/editor/extensions/source_editor_webide_ext';
@@ -26,6 +27,7 @@ import { performanceMarkAndMeasure } from '~/performance/utils';
import ContentViewer from '~/vue_shared/components/content_viewer/content_viewer.vue';
import { viewerInformationForPath } from '~/vue_shared/components/content_viewer/lib/viewer_utils';
import DiffViewer from '~/vue_shared/components/diff_viewer/diff_viewer.vue';
+import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import {
leftSidebarViews,
viewerTypes,
@@ -53,6 +55,7 @@ export default {
DiffViewer,
FileTemplatesBar,
},
+ mixins: [glFeatureFlagMixin()],
props: {
file: {
type: Object,
@@ -145,6 +148,12 @@ export default {
showTabs() {
return !this.shouldHideEditor && this.isEditModeActive && this.previewMode;
},
+ isCiConfigFile() {
+ return (
+ this.file.path === EXTENSION_CI_SCHEMA_FILE_NAME_MATCH &&
+ this.editor?.getEditorType() === EDITOR_TYPE_CODE
+ );
+ },
},
watch: {
'file.name': {
@@ -232,8 +241,6 @@ export default {
return;
}
- this.registerSchemaForFile();
-
Promise.all([this.fetchFileData(), this.fetchEditorconfigRules()])
.then(() => {
this.createEditorInstance();
@@ -357,6 +364,8 @@ export default {
this.model.updateOptions(this.rules);
+ this.registerSchemaForFile();
+
this.model.onChange((model) => {
const { file } = model;
if (!file.active) return;
@@ -446,8 +455,33 @@ export default {
return Promise.resolve();
},
registerSchemaForFile() {
- const schema = this.getJsonSchemaForPath(this.file.path);
- registerSchema(schema);
+ const registerExternalSchema = () => {
+ const schema = this.getJsonSchemaForPath(this.file.path);
+ return registerSchema(schema);
+ };
+ const registerLocalSchema = async () => {
+ if (!this.CiSchemaExtension) {
+ const { CiSchemaExtension } = await import(
+ '~/editor/extensions/source_editor_ci_schema_ext'
+ ).catch((e) =>
+ createAlert({
+ message: e,
+ }),
+ );
+ this.CiSchemaExtension = CiSchemaExtension;
+ }
+ this.editor.use({ definition: this.CiSchemaExtension });
+ this.editor.registerCiSchema();
+ };
+
+ if (this.isCiConfigFile && this.glFeatures.schemaLinting) {
+ registerLocalSchema();
+ } else {
+ if (this.CiSchemaExtension) {
+ this.editor.unuse(this.CiSchemaExtension);
+ }
+ registerExternalSchema();
+ }
},
updateEditor(data) {
// Looks like our model wrapper `.dispose` causes the monaco editor to emit some position changes after
diff --git a/app/assets/javascripts/ide/components/switch_editors/switch_editors_view.vue b/app/assets/javascripts/ide/components/switch_editors/switch_editors_view.vue
deleted file mode 100644
index 00164f65e33..00000000000
--- a/app/assets/javascripts/ide/components/switch_editors/switch_editors_view.vue
+++ /dev/null
@@ -1,103 +0,0 @@
-<script>
-import { GlButton, GlEmptyState, GlLink } from '@gitlab/ui';
-import { mapState } from 'vuex';
-import { createAlert } from '~/flash';
-import { logError } from '~/lib/logger';
-import axios from '~/lib/utils/axios_utils';
-import { confirmAction } from '~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal';
-import { ignoreWhilePending } from '~/lib/utils/ignore_while_pending';
-import { s__, __ } from '~/locale';
-import eventHub from '../../eventhub';
-
-export const MSG_DESCRIPTION = s__('WebIDE|You are invited to experience the new Web IDE.');
-export const MSG_BUTTON_TEXT = s__('WebIDE|Switch to new Web IDE');
-export const MSG_LEARN_MORE = __('Learn more');
-export const MSG_TITLE = s__('WebIDE|Ready for something new?');
-
-export const MSG_CONFIRM = s__(
- 'WebIDE|Are you sure you want to switch editors? You will lose any unsaved changes.',
-);
-export const MSG_ERROR_ALERT = s__(
- 'WebIDE|Something went wrong while updating the user preferences. Please see developer console for details.',
-);
-
-export default {
- components: {
- GlButton,
- GlEmptyState,
- GlLink,
- },
- data() {
- return {
- loading: false,
- };
- },
- computed: {
- ...mapState(['switchEditorSvgPath', 'links', 'userPreferencesPath']),
- },
- methods: {
- async submitSwitch() {
- const confirmed = await confirmAction(MSG_CONFIRM, {
- primaryBtnText: __('Switch editors'),
- cancelBtnText: __('Cancel'),
- });
-
- if (!confirmed) {
- return;
- }
-
- try {
- await axios.put(this.userPreferencesPath, {
- user: { use_legacy_web_ide: false },
- });
- } catch (e) {
- // why: We do not want to translate console logs
- // eslint-disable-next-line @gitlab/require-i18n-strings
- logError('Error while updating user preferences', e);
- createAlert({
- message: MSG_ERROR_ALERT,
- });
- return;
- }
-
- eventHub.$emit('skip-beforeunload');
- window.location.reload();
- },
- // what: ignoreWhilePending prevents double confirmation boxes
- onSwitchClicked: ignoreWhilePending(async function onSwitchClicked() {
- this.loading = true;
-
- try {
- await this.submitSwitch();
- } finally {
- this.loading = false;
- }
- }),
- },
- MSG_TITLE,
- MSG_DESCRIPTION,
- MSG_BUTTON_TEXT,
- MSG_LEARN_MORE,
-};
-</script>
-
-<template>
- <div class="gl-h-full gl-display-flex gl-flex-direction-column gl-justify-content-center">
- <gl-empty-state :svg-path="switchEditorSvgPath" :svg-height="150" :title="$options.MSG_TITLE">
- <template #description>
- <span>{{ $options.MSG_DESCRIPTION }}</span>
- <gl-link :href="links.newWebIDEHelpPagePath">{{ $options.MSG_LEARN_MORE }}</gl-link
- >.
- </template>
- <template #actions>
- <gl-button
- category="primary"
- variant="confirm"
- :loading="loading"
- @click="onSwitchClicked"
- >{{ $options.MSG_BUTTON_TEXT }}</gl-button
- >
- </template>
- </gl-empty-state>
- </div>
-</template>
diff --git a/app/assets/javascripts/ide/components/terminal/empty_state.vue b/app/assets/javascripts/ide/components/terminal/empty_state.vue
index 623ba719b28..fa93f6d42a5 100644
--- a/app/assets/javascripts/ide/components/terminal/empty_state.vue
+++ b/app/assets/javascripts/ide/components/terminal/empty_state.vue
@@ -1,5 +1,6 @@
<script>
-import { GlLoadingIcon, GlButton, GlAlert, GlSafeHtmlDirective } from '@gitlab/ui';
+import { GlLoadingIcon, GlButton, GlAlert } from '@gitlab/ui';
+import SafeHtml from '~/vue_shared/directives/safe_html';
export default {
components: {
@@ -8,7 +9,7 @@ export default {
GlAlert,
},
directives: {
- SafeHtml: GlSafeHtmlDirective,
+ SafeHtml,
},
props: {
isLoading: {
diff --git a/app/assets/javascripts/ide/constants.js b/app/assets/javascripts/ide/constants.js
index c8e737fa6f5..01ce5fa07ee 100644
--- a/app/assets/javascripts/ide/constants.js
+++ b/app/assets/javascripts/ide/constants.js
@@ -61,7 +61,6 @@ export const leftSidebarViews = {
};
export const rightSidebarViews = {
- switchEditors: { name: 'switch-editors', keepAlive: true },
pipelines: { name: 'pipelines-list', keepAlive: true },
jobsDetail: { name: 'jobs-detail', keepAlive: false },
mergeRequestInfo: { name: 'merge-request-info', keepAlive: true },
@@ -119,3 +118,5 @@ export const DEFAULT_BRANCH = 'main';
// Ping Usage Metrics Keys
export const PING_USAGE_PREVIEW_KEY = 'web_ide_clientside_preview';
export const PING_USAGE_PREVIEW_SUCCESS_KEY = 'web_ide_clientside_preview_success';
+
+export const GITLAB_WEB_IDE_FEEDBACK_ISSUE = 'https://gitlab.com/gitlab-org/gitlab/-/issues/377367';
diff --git a/app/assets/javascripts/ide/index.js b/app/assets/javascripts/ide/index.js
index dec282239d9..1347d92b3b7 100644
--- a/app/assets/javascripts/ide/index.js
+++ b/app/assets/javascripts/ide/index.js
@@ -8,7 +8,6 @@ import { parseBoolean } from '../lib/utils/common_utils';
import { resetServiceWorkersPublicPath } from '../lib/utils/webpack';
import ide from './components/ide.vue';
import { createRouter } from './ide_router';
-import { initGitlabWebIDE } from './init_gitlab_web_ide';
import { DEFAULT_THEME } from './lib/themes';
import { createStore } from './stores';
@@ -74,7 +73,6 @@ export const initLegacyWebIDE = (el, options = {}) => {
codesandboxBundlerUrl: el.dataset.codesandboxBundlerUrl,
environmentsGuidanceAlertDismissed: !parseBoolean(el.dataset.enableEnvironmentsGuidance),
previewMarkdownPath: el.dataset.previewMarkdownPath,
- canUseNewWebIde: parseBoolean(el.dataset.canUseNewWebIde),
userPreferencesPath: el.dataset.userPreferencesPath,
});
},
@@ -96,7 +94,7 @@ export const initLegacyWebIDE = (el, options = {}) => {
*
* @param {Objects} options - Extra options for the IDE (Used by EE).
*/
-export function startIde(options) {
+export async function startIde(options) {
const ideElement = document.getElementById('ide');
if (!ideElement) {
@@ -106,6 +104,7 @@ export function startIde(options) {
const useNewWebIde = parseBoolean(ideElement.dataset.useNewWebIde);
if (useNewWebIde) {
+ const { initGitlabWebIDE } = await import('./init_gitlab_web_ide');
initGitlabWebIDE(ideElement);
} else {
resetServiceWorkersPublicPath();
diff --git a/app/assets/javascripts/ide/init_gitlab_web_ide.js b/app/assets/javascripts/ide/init_gitlab_web_ide.js
index 140f2895a29..d3c64754e8a 100644
--- a/app/assets/javascripts/ide/init_gitlab_web_ide.js
+++ b/app/assets/javascripts/ide/init_gitlab_web_ide.js
@@ -1,29 +1,89 @@
-import { cleanTrailingSlash } from './stores/utils';
+import { start } from '@gitlab/web-ide';
+import { __ } from '~/locale';
+import { cleanLeadingSeparator } from '~/lib/utils/url_utility';
+import { confirmAction } from '~/lib/utils/confirm_via_gl_modal/confirm_action';
+import { createAndSubmitForm } from '~/lib/utils/create_and_submit_form';
+import csrf from '~/lib/utils/csrf';
+import { getBaseConfig } from './lib/gitlab_web_ide/get_base_config';
+import { setupRootElement } from './lib/gitlab_web_ide/setup_root_element';
+import { GITLAB_WEB_IDE_FEEDBACK_ISSUE } from './constants';
-export const initGitlabWebIDE = async (el) => {
- const { start } = await import('@gitlab/web-ide');
+const buildRemoteIdeURL = (ideRemotePath, remoteHost, remotePathArg) => {
+ const remotePath = cleanLeadingSeparator(remotePathArg);
- const { gitlab_url: gitlabUrl } = window.gon;
- const baseUrl = new URL(process.env.GITLAB_WEB_IDE_PUBLIC_PATH, window.location.origin);
+ const replacers = {
+ ':remote_host': encodeURIComponent(remoteHost),
+ ':remote_path': encodeURIComponent(remotePath).replaceAll('%2F', '/'),
+ };
- // what: Pull what we need from the element. We will replace it soon.
- const { cspNonce: nonce, branchName: ref, projectPath } = el.dataset;
+ // why: Use the function callback of "replace" so we replace both keys at once
+ return ideRemotePath.replace(/(:remote_host|:remote_path)/g, (key) => {
+ return replacers[key];
+ });
+};
+
+const getMRTargetProject = () => {
+ const url = new URL(window.location.href);
+
+ return url.searchParams.get('target_project') || '';
+};
- // what: Clean up the element, but preserve id.
- // why: This way we don't inherit any `ide-loading` side-effects. This
- // mirrors the behavior of Vue when it mounts to an element.
- const newEl = document.createElement(el.tagName);
- newEl.id = el.id;
- newEl.classList.add('gl--flex-center', 'gl-relative', 'gl-h-full');
+export const initGitlabWebIDE = async (el) => {
+ // what: Pull what we need from the element. We will replace it soon.
+ const {
+ cspNonce: nonce,
+ branchName: ref,
+ projectPath,
+ ideRemotePath,
+ filePath,
+ mergeRequest: mrId,
+ forkInfo: forkInfoJSON,
+ } = el.dataset;
- el.replaceWith(newEl);
+ const rootEl = setupRootElement(el);
+ const forkInfo = forkInfoJSON ? JSON.parse(forkInfoJSON) : null;
- // what: Trigger start on our new mounting element
- await start(newEl, {
- baseUrl: cleanTrailingSlash(baseUrl.href),
+ // See ClientOnlyConfig https://gitlab.com/gitlab-org/gitlab-web-ide/-/blob/main/packages/web-ide-types/src/config.ts#L17
+ start(rootEl, {
+ ...getBaseConfig(),
+ nonce,
+ // Use same headers as defined in axios_utils
+ httpHeaders: {
+ [csrf.headerKey]: csrf.token,
+ 'X-Requested-With': 'XMLHttpRequest',
+ },
projectPath,
- gitlabUrl,
ref,
- nonce,
+ filePath,
+ mrId,
+ mrTargetProject: getMRTargetProject(),
+ // note: At the time of writing this, forkInfo isn't expected by `@gitlab/web-ide`,
+ // but it will be soon.
+ forkInfo,
+ links: {
+ feedbackIssue: GITLAB_WEB_IDE_FEEDBACK_ISSUE,
+ userPreferences: el.dataset.userPreferencesPath,
+ },
+ async handleStartRemote({ remoteHost, remotePath, connectionToken }) {
+ const confirmed = await confirmAction(
+ __('Are you sure you want to leave the Web IDE? All unsaved changes will be lost.'),
+ {
+ primaryBtnText: __('Start remote connection'),
+ cancelBtnText: __('Continue editing'),
+ },
+ );
+
+ if (!confirmed) {
+ return;
+ }
+
+ createAndSubmitForm({
+ url: buildRemoteIdeURL(ideRemotePath, remoteHost, remotePath),
+ data: {
+ connection_token: connectionToken,
+ return_url: window.location.href,
+ },
+ });
+ },
});
};
diff --git a/app/assets/javascripts/ide/lib/diff/controller.js b/app/assets/javascripts/ide/lib/diff/controller.js
index 682914df9ec..7595a1cedf1 100644
--- a/app/assets/javascripts/ide/lib/diff/controller.js
+++ b/app/assets/javascripts/ide/lib/diff/controller.js
@@ -2,7 +2,7 @@ import { throttle } from 'lodash';
import { Range } from 'monaco-editor';
import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
import Disposable from '../common/disposable';
-import DirtyDiffWorker from './diff_worker';
+import DirtyDiffWorker from './diff_worker?worker';
export const getDiffChangeType = (change) => {
if (change.modified) {
diff --git a/app/assets/javascripts/ide/lib/editor_options.js b/app/assets/javascripts/ide/lib/editor_options.js
index 525afcb2083..289027c3054 100644
--- a/app/assets/javascripts/ide/lib/editor_options.js
+++ b/app/assets/javascripts/ide/lib/editor_options.js
@@ -1,3 +1,12 @@
+import { useNewFonts } from '~/lib/utils/common_utils';
+import { getCssVariable } from '~/lib/utils/css_utils';
+
+const fontOptions = {};
+
+if (useNewFonts()) {
+ fontOptions.fontFamily = getCssVariable('--code-editor-font');
+}
+
export const defaultEditorOptions = {
model: null,
readOnly: false,
@@ -9,6 +18,7 @@ export const defaultEditorOptions = {
wordWrap: 'on',
glyphMargin: true,
automaticLayout: true,
+ ...fontOptions,
};
export const defaultDiffOptions = {
@@ -27,7 +37,6 @@ export const defaultDiffEditorOptions = {
};
export const defaultModelOptions = {
- endOfLine: 0,
insertFinalNewline: true,
trimTrailingWhitespace: false,
};
diff --git a/app/assets/javascripts/ide/lib/gitlab_web_ide/get_base_config.js b/app/assets/javascripts/ide/lib/gitlab_web_ide/get_base_config.js
new file mode 100644
index 00000000000..fbd2ce4ce69
--- /dev/null
+++ b/app/assets/javascripts/ide/lib/gitlab_web_ide/get_base_config.js
@@ -0,0 +1,12 @@
+import { cleanEndingSeparator } from '~/lib/utils/url_utility';
+
+const getBaseUrl = () => {
+ const baseUrlObj = new URL(process.env.GITLAB_WEB_IDE_PUBLIC_PATH, window.location.origin);
+
+ return cleanEndingSeparator(baseUrlObj.href);
+};
+
+export const getBaseConfig = () => ({
+ baseUrl: getBaseUrl(),
+ gitlabUrl: window.gon.gitlab_url,
+});
diff --git a/app/assets/javascripts/ide/lib/gitlab_web_ide/index.js b/app/assets/javascripts/ide/lib/gitlab_web_ide/index.js
new file mode 100644
index 00000000000..8311e11672e
--- /dev/null
+++ b/app/assets/javascripts/ide/lib/gitlab_web_ide/index.js
@@ -0,0 +1,2 @@
+export * from './get_base_config';
+export * from './setup_root_element';
diff --git a/app/assets/javascripts/ide/lib/gitlab_web_ide/setup_root_element.js b/app/assets/javascripts/ide/lib/gitlab_web_ide/setup_root_element.js
new file mode 100644
index 00000000000..b0e06c88d26
--- /dev/null
+++ b/app/assets/javascripts/ide/lib/gitlab_web_ide/setup_root_element.js
@@ -0,0 +1,14 @@
+/**
+ * Cleans up the given element and prepares it for mounting to `@gitlab/web-ide`
+ *
+ * @param {Element} root The original root element
+ * @returns {Element} A new element ready to be used by `@gitlab/web-ide`
+ */
+export const setupRootElement = (el) => {
+ const newEl = document.createElement(el.tagName);
+ newEl.id = el.id;
+ newEl.classList.add('gl--flex-center', 'gl-relative', 'gl-h-full');
+ el.replaceWith(newEl);
+
+ return newEl;
+};
diff --git a/app/assets/javascripts/ide/remote/index.js b/app/assets/javascripts/ide/remote/index.js
new file mode 100644
index 00000000000..fb8db20c0c1
--- /dev/null
+++ b/app/assets/javascripts/ide/remote/index.js
@@ -0,0 +1,40 @@
+import { startRemote } from '@gitlab/web-ide';
+import { getBaseConfig, setupRootElement } from '~/ide/lib/gitlab_web_ide';
+import { isSameOriginUrl, joinPaths } from '~/lib/utils/url_utility';
+
+/**
+ * @param {Element} rootEl
+ */
+export const mountRemoteIDE = async (el) => {
+ const {
+ remoteHost: remoteAuthority,
+ remotePath: hostPath,
+ cspNonce,
+ connectionToken,
+ returnUrl,
+ } = el.dataset;
+
+ const rootEl = setupRootElement(el);
+
+ const visitReturnUrl = () => {
+ // security: Only change `href` if of the same origin as current page
+ if (returnUrl && isSameOriginUrl(returnUrl)) {
+ window.location.href = returnUrl;
+ } else {
+ window.location.reload();
+ }
+ };
+
+ startRemote(rootEl, {
+ ...getBaseConfig(),
+ nonce: cspNonce,
+ connectionToken,
+ // remoteAuthority must start with "/"
+ remoteAuthority: joinPaths('/', remoteAuthority),
+ // hostPath must start with "/"
+ hostPath: joinPaths('/', hostPath),
+ // TODO Handle error better
+ handleError: visitReturnUrl,
+ handleClose: visitReturnUrl,
+ });
+};
diff --git a/app/assets/javascripts/ide/services/index.js b/app/assets/javascripts/ide/services/index.js
index 805476c71bc..1f9bc834140 100644
--- a/app/assets/javascripts/ide/services/index.js
+++ b/app/assets/javascripts/ide/services/index.js
@@ -4,7 +4,7 @@ import dismissUserCallout from '~/graphql_shared/mutations/dismiss_user_callout.
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import axios from '~/lib/utils/axios_utils';
import { joinPaths, escapeFileUrl } from '~/lib/utils/url_utility';
-import ciConfig from '~/pipeline_editor/graphql/queries/ci_config.query.graphql';
+import ciConfig from '~/ci/pipeline_editor/graphql/queries/ci_config.query.graphql';
import { query, mutate } from './gql';
export default {
diff --git a/app/assets/javascripts/ide/stores/modules/terminal/actions/session_controls.js b/app/assets/javascripts/ide/stores/modules/terminal/actions/session_controls.js
index 91868132a5a..a510ec0847b 100644
--- a/app/assets/javascripts/ide/stores/modules/terminal/actions/session_controls.js
+++ b/app/assets/javascripts/ide/stores/modules/terminal/actions/session_controls.js
@@ -1,6 +1,6 @@
import { createAlert } from '~/flash';
import axios from '~/lib/utils/axios_utils';
-import httpStatus from '~/lib/utils/http_status';
+import httpStatus, { HTTP_STATUS_UNPROCESSABLE_ENTITY } from '~/lib/utils/http_status';
import * as terminalService from '../../../../services/terminals';
import { STARTING, STOPPING, STOPPED } from '../constants';
import * as messages from '../messages';
@@ -108,7 +108,7 @@ export const restartSession = ({ state, dispatch, rootState }) => {
// We may have removed the build, in this case we'll just create a new session
if (
responseStatus === httpStatus.NOT_FOUND ||
- responseStatus === httpStatus.UNPROCESSABLE_ENTITY
+ responseStatus === HTTP_STATUS_UNPROCESSABLE_ENTITY
) {
dispatch('startSession');
} else {
diff --git a/app/assets/javascripts/ide/stores/modules/terminal/messages.js b/app/assets/javascripts/ide/stores/modules/terminal/messages.js
index ec05ca84754..fa1c7f23677 100644
--- a/app/assets/javascripts/ide/stores/modules/terminal/messages.js
+++ b/app/assets/javascripts/ide/stores/modules/terminal/messages.js
@@ -1,5 +1,5 @@
import { escape } from 'lodash';
-import httpStatus from '~/lib/utils/http_status';
+import httpStatus, { HTTP_STATUS_UNPROCESSABLE_ENTITY } from '~/lib/utils/http_status';
import { __, sprintf } from '~/locale';
export const UNEXPECTED_ERROR_CONFIG = __(
@@ -28,7 +28,7 @@ export const ERROR_PERMISSION = __(
);
export const configCheckError = (status, helpUrl) => {
- if (status === httpStatus.UNPROCESSABLE_ENTITY) {
+ if (status === HTTP_STATUS_UNPROCESSABLE_ENTITY) {
return sprintf(
ERROR_CONFIG,
{
diff --git a/app/assets/javascripts/ide/stores/state.js b/app/assets/javascripts/ide/stores/state.js
index 70efda970bf..b89d9d38a1a 100644
--- a/app/assets/javascripts/ide/stores/state.js
+++ b/app/assets/javascripts/ide/stores/state.js
@@ -34,5 +34,4 @@ export default () => ({
environmentsGuidanceAlertDetected: false,
previewMarkdownPath: '',
userPreferencesPath: '',
- canUseNewWebIde: false,
});
diff --git a/app/assets/javascripts/import_entities/components/group_dropdown.vue b/app/assets/javascripts/import_entities/components/group_dropdown.vue
index 25d4037bbe5..f351a9a392f 100644
--- a/app/assets/javascripts/import_entities/components/group_dropdown.vue
+++ b/app/assets/javascripts/import_entities/components/group_dropdown.vue
@@ -1,5 +1,21 @@
<script>
import { GlDropdown, GlSearchBoxByType } from '@gitlab/ui';
+import { debounce } from 'lodash';
+
+import { s__ } from '~/locale';
+import { createAlert } from '~/flash';
+import searchNamespacesWhereUserCanCreateProjectsQuery from '~/projects/new/queries/search_namespaces_where_user_can_create_projects.query.graphql';
+import { DEBOUNCE_DELAY } from '~/vue_shared/components/filtered_search_bar/constants';
+import { MINIMUM_SEARCH_LENGTH } from '~/graphql_shared/constants';
+import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
+
+const reportNamespaceLoadError = debounce(
+ () =>
+ createAlert({
+ message: s__('ImportProjects|Requesting namespaces failed'),
+ }),
+ DEFAULT_DEBOUNCE_AND_THROTTLE_MS,
+);
export default {
components: {
@@ -7,18 +23,32 @@ export default {
GlSearchBoxByType,
},
inheritAttrs: false,
- props: {
- namespaces: {
- type: Array,
- required: true,
- },
- },
data() {
return { searchTerm: '' };
},
+ apollo: {
+ namespaces: {
+ query: searchNamespacesWhereUserCanCreateProjectsQuery,
+ variables() {
+ return {
+ search: this.searchTerm,
+ };
+ },
+ skip() {
+ const hasNotEnoughSearchCharacters =
+ this.searchTerm.length > 0 && this.searchTerm.length < MINIMUM_SEARCH_LENGTH;
+ return hasNotEnoughSearchCharacters;
+ },
+ update(data) {
+ return data.currentUser.groups.nodes;
+ },
+ error: reportNamespaceLoadError,
+ debounce: DEBOUNCE_DELAY,
+ },
+ },
computed: {
filteredNamespaces() {
- return this.namespaces.filter((ns) =>
+ return (this.namespaces ?? []).filter((ns) =>
ns.fullPath.toLowerCase().includes(this.searchTerm.toLowerCase()),
);
},
diff --git a/app/assets/javascripts/import_entities/components/import_status.vue b/app/assets/javascripts/import_entities/components/import_status.vue
index 5455a034106..bd69165f0ca 100644
--- a/app/assets/javascripts/import_entities/components/import_status.vue
+++ b/app/assets/javascripts/import_entities/components/import_status.vue
@@ -49,7 +49,7 @@ const STATUS_MAP = {
text: __('Timeout'),
variant: 'danger',
},
- [STATUSES.CANCELLED]: {
+ [STATUSES.CANCELED]: {
icon: 'status-stopped',
text: __('Cancelled'),
variant: 'neutral',
diff --git a/app/assets/javascripts/import_entities/constants.js b/app/assets/javascripts/import_entities/constants.js
index c470da21765..48b7febca4b 100644
--- a/app/assets/javascripts/import_entities/constants.js
+++ b/app/assets/javascripts/import_entities/constants.js
@@ -9,6 +9,10 @@ export const STATUSES = {
STARTED: 'started',
NONE: 'none',
SCHEDULING: 'scheduling',
- CANCELLED: 'cancelled',
+ CANCELED: 'canceled',
TIMEOUT: 'timeout',
};
+
+export const PROVIDERS = {
+ GITHUB: 'github',
+};
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 66dff77eef8..6412f26fde7 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
@@ -21,12 +21,13 @@ import { getGroupPathAvailability } from '~/rest_api';
import axios from '~/lib/utils/axios_utils';
import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
import { helpPagePath } from '~/helpers/help_page_helper';
+import searchNamespacesWhereUserCanCreateProjectsQuery from '~/projects/new/queries/search_namespaces_where_user_can_create_projects.query.graphql';
+import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { STATUSES } from '../../constants';
import ImportStatusCell from '../../components/import_status.vue';
import importGroupsMutation from '../graphql/mutations/import_groups.mutation.graphql';
import updateImportStatusMutation from '../graphql/mutations/update_import_status.mutation.graphql';
-import availableNamespacesQuery from '../graphql/queries/available_namespaces.query.graphql';
import bulkImportSourceGroupsQuery from '../graphql/queries/bulk_import_source_groups.query.graphql';
import { NEW_NAME_FIELD, ROOT_NAMESPACE, i18n } from '../constants';
import { StatusPoller } from '../services/status_poller';
@@ -107,7 +108,12 @@ export default {
return { page: this.page, filter: this.filter, perPage: this.perPage };
},
},
- availableNamespaces: availableNamespacesQuery,
+ availableNamespaces: {
+ query: searchNamespacesWhereUserCanCreateProjectsQuery,
+ update(data) {
+ return data.currentUser.groups.nodes;
+ },
+ },
},
fields: [
@@ -158,7 +164,7 @@ export default {
}
return this.groups.map((group) => {
- const importTarget = this.getImportTarget(group);
+ const importTarget = this.importTargets[group.id];
const status = this.getStatus(group);
const flags = {
@@ -250,10 +256,14 @@ export default {
this.page = 1;
},
- groupsTableData() {
+ groups() {
const table = this.getTableRef();
const matches = new Set();
- this.groupsTableData.forEach((g, idx) => {
+ this.groups.forEach((g, idx) => {
+ if (!this.importGroups[g.id]) {
+ this.setDefaultImportTarget(g);
+ }
+
if (this.selectedGroupsIds.includes(g.id)) {
matches.add(g.id);
this.$nextTick(() => {
@@ -421,7 +431,7 @@ export default {
data: { exists },
} = await getGroupPathAvailability(
importTarget.newName,
- importTarget.targetNamespace.id,
+ getIdFromGraphQLId(importTarget.targetNamespace.id),
{
cancelToken: importTarget.cancellationToken?.token,
},
@@ -444,11 +454,7 @@ export default {
importTarget.validationErrors = newValidationErrors;
}, VALIDATION_DEBOUNCE_TIME),
- getImportTarget(group) {
- if (this.importTargets[group.id]) {
- return this.importTargets[group.id];
- }
-
+ setDefaultImportTarget(group) {
// If we've reached this Vue application we have at least one potential import destination
const defaultTargetNamespace =
// first option: namespace id was explicitly provided
@@ -482,9 +488,13 @@ export default {
validationErrors: [],
});
- getGroupPathAvailability(importTarget.newName, importTarget.targetNamespace.id, {
- cancelToken: cancellationToken.token,
- })
+ getGroupPathAvailability(
+ importTarget.newName,
+ getIdFromGraphQLId(importTarget.targetNamespace.id),
+ {
+ cancelToken: cancellationToken.token,
+ },
+ )
.then(({ data: { exists, suggests: suggestions } }) => {
if (!exists) return;
@@ -505,7 +515,6 @@ export default {
.catch(() => {
// empty catch intended
});
- return this.importTargets[group.id];
},
},
@@ -692,7 +701,6 @@ export default {
<template #cell(importTarget)="{ item: group }">
<import-target-cell
:group="group"
- :available-namespaces="availableNamespaces"
:group-path-regex="groupPathRegex"
@update-target-namespace="updateImportTarget(group, { targetNamespace: $event })"
@update-new-name="updateImportTarget(group, { newName: $event })"
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 4fbbd5b239c..04a90d9c20c 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
@@ -22,10 +22,6 @@ export default {
type: Object,
required: true,
},
- availableNamespaces: {
- type: Array,
- required: true,
- },
},
computed: {
@@ -53,7 +49,6 @@ export default {
#default="{ namespaces }"
:text="fullPath"
:disabled="!group.flags.isAvailableForImport"
- :namespaces="availableNamespaces"
toggle-class="gl-rounded-top-right-none! gl-rounded-bottom-right-none!"
class="gl-h-7 gl-flex-grow-1"
data-qa-selector="target_namespace_selector_dropdown"
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 36da996ea17..913a5a659b3 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
@@ -10,7 +10,6 @@ import typeDefs from './typedefs.graphql';
export const clientTypenames = {
BulkImportSourceGroupConnection: 'ClientBulkImportSourceGroupConnection',
BulkImportSourceGroup: 'ClientBulkImportSourceGroup',
- AvailableNamespace: 'ClientAvailableNamespace',
BulkImportPageInfo: 'ClientBulkImportPageInfo',
BulkImportTarget: 'ClientBulkImportTarget',
BulkImportProgress: 'ClientBulkImportProgress',
@@ -110,15 +109,6 @@ export function createResolvers({ endpoints }) {
};
return response;
},
-
- availableNamespaces: () =>
- axios.get(endpoints.availableNamespaces).then(({ data }) =>
- data.map((namespace) => ({
- __typename: clientTypenames.AvailableNamespace,
- id: namespace.id,
- fullPath: namespace.full_path,
- })),
- ),
},
Mutation: {
async updateImportStatus(_, { id, status: newStatus }, { client, getCacheKey }) {
diff --git a/app/assets/javascripts/import_entities/import_groups/graphql/queries/available_namespaces.query.graphql b/app/assets/javascripts/import_entities/import_groups/graphql/queries/available_namespaces.query.graphql
deleted file mode 100644
index b0741dfbe5c..00000000000
--- a/app/assets/javascripts/import_entities/import_groups/graphql/queries/available_namespaces.query.graphql
+++ /dev/null
@@ -1,6 +0,0 @@
-query availableNamespaces {
- availableNamespaces @client {
- id
- fullPath
- }
-}
diff --git a/app/assets/javascripts/import_entities/import_groups/index.js b/app/assets/javascripts/import_entities/import_groups/index.js
index 5d7e7911f5a..494a845b1f9 100644
--- a/app/assets/javascripts/import_entities/import_groups/index.js
+++ b/app/assets/javascripts/import_entities/import_groups/index.js
@@ -12,7 +12,6 @@ export function mountImportGroupsApp(mountElement) {
const {
statusPath,
- availableNamespacesPath,
createBulkImportPath,
jobsPath,
historyPath,
@@ -25,7 +24,6 @@ export function mountImportGroupsApp(mountElement) {
sourceUrl,
endpoints: {
status: statusPath,
- availableNamespaces: availableNamespacesPath,
createBulkImport: createBulkImportPath,
},
}),
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 97a7ed4bf55..63a36f1a79f 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
@@ -37,6 +37,11 @@ export default {
required: false,
default: false,
},
+ cancelable: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
optionalStages: {
type: Array,
required: false,
@@ -58,9 +63,8 @@ export default {
},
computed: {
- ...mapState(['filter', 'repositories', 'namespaces', 'defaultTargetNamespace', 'pageInfo']),
+ ...mapState(['filter', 'repositories', 'defaultTargetNamespace', 'pageInfo', 'isLoadingRepos']),
...mapGetters([
- 'isLoading',
'isImportingAnyRepo',
'importingRepoCount',
'hasImportableRepos',
@@ -98,7 +102,6 @@ export default {
},
mounted() {
- this.fetchNamespaces();
this.fetchJobs();
if (!this.paginatable) {
@@ -115,7 +118,6 @@ export default {
...mapActions([
'fetchRepos',
'fetchJobs',
- 'fetchNamespaces',
'stopJobsPolling',
'clearJobsEtagPoll',
'setFilter',
@@ -196,22 +198,22 @@ export default {
<provider-repo-table-row
:key="repo.importSource.providerLink"
:repo="repo"
- :available-namespaces="namespaces"
:user-namespace="defaultTargetNamespace"
:optional-stages="optionalStagesSelection"
+ :cancelable="cancelable"
/>
</template>
</tbody>
</table>
</div>
<gl-intersection-observer
- v-if="paginatable"
+ v-if="paginatable && pageInfo.hasNextPage"
:key="pagePaginationStateKey"
@appear="fetchRepos"
/>
- <gl-loading-icon v-if="isLoading" class="gl-mt-7" size="lg" />
+ <gl-loading-icon v-if="isLoadingRepos" class="gl-mt-7" size="lg" />
- <div v-if="!isLoading && repositories.length === 0" class="gl-text-center">
+ <div v-if="!isLoadingRepos && repositories.length === 0" class="gl-text-center">
<strong>{{ emptyStateText }}</strong>
</div>
</div>
diff --git a/app/assets/javascripts/import_entities/import_projects/components/provider_repo_table_row.vue b/app/assets/javascripts/import_entities/import_projects/components/provider_repo_table_row.vue
index 458e0fb1cb1..b8faf349375 100644
--- a/app/assets/javascripts/import_entities/import_projects/components/provider_repo_table_row.vue
+++ b/app/assets/javascripts/import_entities/import_projects/components/provider_repo_table_row.vue
@@ -8,13 +8,15 @@ import {
GlDropdownItem,
GlDropdownDivider,
GlDropdownSectionHeader,
+ GlTooltip,
} from '@gitlab/ui';
import { mapState, mapGetters, mapActions } from 'vuex';
import { __ } from '~/locale';
+import { helpPagePath } from '~/helpers/help_page_helper';
import ImportGroupDropdown from '../../components/group_dropdown.vue';
import ImportStatus from '../../components/import_status.vue';
import { STATUSES } from '../../constants';
-import { isProjectImportable, isIncompatible, getImportStatus } from '../utils';
+import { isProjectImportable, isImporting, isIncompatible, getImportStatus } from '../utils';
export default {
name: 'ProviderRepoTableRow',
@@ -29,6 +31,7 @@ export default {
GlIcon,
GlBadge,
GlLink,
+ GlTooltip,
},
props: {
repo: {
@@ -39,14 +42,15 @@ export default {
type: String,
required: true,
},
- availableNamespaces: {
- type: Array,
- required: true,
- },
optionalStages: {
type: Object,
required: true,
},
+ cancelable: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
},
computed: {
@@ -73,6 +77,14 @@ export default {
return getImportStatus(this.repo);
},
+ isImporting() {
+ return isImporting(this.repo);
+ },
+
+ isCancelable() {
+ return this.cancelable && this.isImporting && this.importStatus !== STATUSES.SCHEDULING;
+ },
+
stats() {
return this.repo.importedProject?.stats;
},
@@ -96,7 +108,7 @@ export default {
},
methods: {
- ...mapActions(['fetchImport', 'setImportTarget']),
+ ...mapActions(['fetchImport', 'cancelImport', 'setImportTarget']),
updateImportTarget(changedValues) {
this.setImportTarget({
repoId: this.repo.importSource.id,
@@ -104,6 +116,8 @@ export default {
});
},
},
+
+ helpUrl: helpPagePath('/user/project/import/github.md'),
};
</script>
@@ -127,11 +141,7 @@ export default {
<template v-if="repo.importSource.target">{{ repo.importSource.target }}</template>
<template v-else-if="isImportNotStarted">
<div class="import-entities-target-select gl-display-flex gl-align-items-stretch gl-w-full">
- <import-group-dropdown
- #default="{ namespaces }"
- :text="importTarget.targetNamespace"
- :namespaces="availableNamespaces"
- >
+ <import-group-dropdown #default="{ namespaces }" :text="importTarget.targetNamespace">
<template v-if="namespaces.length">
<gl-dropdown-section-header>{{ __('Groups') }}</gl-dropdown-section-header>
<gl-dropdown-item
@@ -168,6 +178,26 @@ export default {
<import-status :status="importStatus" :stats="stats" />
</td>
<td data-testid="actions" class="gl-vertical-align-top gl-pt-4">
+ <gl-tooltip :target="() => $refs.cancelButton.$el">
+ <div class="gl-text-left">
+ <p class="gl-mb-5 gl-font-weight-bold">{{ s__('ImportProjects|Cancel import') }}</p>
+ {{
+ s__(
+ 'ImportProjects|Imported files will be kept. You can import this repository again later.',
+ )
+ }}
+ <gl-link :href="$options.helpUrl" target="_blank">{{ __('Learn more.') }}</gl-link>
+ </div>
+ </gl-tooltip>
+ <gl-button
+ v-show="isCancelable"
+ ref="cancelButton"
+ variant="danger"
+ category="secondary"
+ icon="cancel"
+ :aria-label="__('Cancel')"
+ @click="cancelImport({ repoId: repo.importSource.id })"
+ />
<gl-button
v-if="isFinished"
class="btn btn-default"
diff --git a/app/assets/javascripts/import_entities/import_projects/index.js b/app/assets/javascripts/import_entities/import_projects/index.js
index df26d6ac4f6..197fb03af2c 100644
--- a/app/assets/javascripts/import_entities/import_projects/index.js
+++ b/app/assets/javascripts/import_entities/import_projects/index.js
@@ -1,10 +1,14 @@
import Vue from 'vue';
+import VueApollo from 'vue-apollo';
import { parseBoolean } from '~/lib/utils/common_utils';
import Translate from '~/vue_shared/translate';
+import createDefaultClient from '~/lib/graphql';
import ImportProjectsTable from './components/import_projects_table.vue';
+
import createStore from './store';
Vue.use(Translate);
+Vue.use(VueApollo);
export function initStoreFromElement(element) {
const {
@@ -15,7 +19,7 @@ export function initStoreFromElement(element) {
reposPath,
jobsPath,
importPath,
- namespacesPath,
+ cancelPath,
defaultTargetNamespace,
paginatable,
} = element.dataset;
@@ -31,7 +35,7 @@ export function initStoreFromElement(element) {
reposPath,
jobsPath,
importPath,
- namespacesPath,
+ cancelPath,
},
hasPagination: parseBoolean(paginatable),
});
@@ -43,9 +47,16 @@ export function initPropsFromElement(element) {
filterable: parseBoolean(element.dataset.filterable),
paginatable: parseBoolean(element.dataset.paginatable),
optionalStages: JSON.parse(element.dataset.optionalStages),
+ cancelable: Boolean(element.dataset.cancelPath),
};
}
+const defaultClient = createDefaultClient();
+
+const apolloProvider = new VueApollo({
+ defaultClient,
+});
+
export default function mountImportProjectsTable(mountElement) {
if (!mountElement) return undefined;
@@ -55,6 +66,7 @@ export default function mountImportProjectsTable(mountElement) {
return new Vue({
el: mountElement,
store,
+ apolloProvider,
render(createElement) {
return createElement(ImportProjectsTable, { props });
},
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 a30c14f9d28..e0db585eb3e 100644
--- a/app/assets/javascripts/import_entities/import_projects/store/actions.js
+++ b/app/assets/javascripts/import_entities/import_projects/store/actions.js
@@ -1,20 +1,22 @@
import Visibility from 'visibilityjs';
+import _ from 'lodash';
import { createAlert } from '~/flash';
import axios from '~/lib/utils/axios_utils';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
-import httpStatusCodes from '~/lib/utils/http_status';
+import { HTTP_STATUS_TOO_MANY_REQUESTS } from '~/lib/utils/http_status';
import Poll from '~/lib/utils/poll';
import { capitalizeFirstCharacter } from '~/lib/utils/text_utility';
import { visitUrl, objectToQuery } from '~/lib/utils/url_utility';
import { s__, sprintf } from '~/locale';
import { isProjectImportable } from '../utils';
+import { PROVIDERS } from '../../constants';
import * as types from './mutation_types';
let eTagPoll;
const hasRedirectInError = (e) => e?.response?.data?.error?.redirect;
const redirectToUrlInError = (e) => visitUrl(e.response.data.error.redirect);
-const tooManyRequests = (e) => e.response.status === httpStatusCodes.TOO_MANY_REQUESTS;
+const tooManyRequests = (e) => e.response.status === HTTP_STATUS_TOO_MANY_REQUESTS;
const pathWithParams = ({ path, ...params }) => {
const filteredParams = Object.fromEntries(
Object.entries(params).filter(([, value]) => value !== ''),
@@ -22,6 +24,24 @@ const pathWithParams = ({ path, ...params }) => {
const queryString = objectToQuery(filteredParams);
return queryString ? `${path}?${queryString}` : path;
};
+const commitPaginationData = ({ state, commit, data }) => {
+ const cursorsGitHubResponse = !_.isEmpty(data.pageInfo || {});
+
+ if (state.provider === PROVIDERS.GITHUB && cursorsGitHubResponse) {
+ commit(types.SET_PAGE_CURSORS, data.pageInfo);
+ } else {
+ const nextPage = state.pageInfo.page + 1;
+ commit(types.SET_PAGE, nextPage);
+ }
+};
+const paginationParams = ({ state }) => {
+ if (state.provider === PROVIDERS.GITHUB && state.pageInfo.endCursor) {
+ return { after: state.pageInfo.endCursor };
+ }
+
+ const nextPage = state.pageInfo.page + 1;
+ return { page: nextPage === 1 ? '' : nextPage.toString() };
+};
const isRequired = () => {
// eslint-disable-next-line @gitlab/require-i18n-strings
@@ -55,7 +75,6 @@ const importAll = ({ state, dispatch }, config = {}) => {
};
const fetchReposFactory = ({ reposPath = isRequired() }) => ({ state, commit }) => {
- const nextPage = state.pageInfo.page + 1;
commit(types.REQUEST_REPOS);
const { provider, filter } = state;
@@ -65,12 +84,13 @@ const fetchReposFactory = ({ reposPath = isRequired() }) => ({ state, commit })
pathWithParams({
path: reposPath,
filter: filter ?? '',
- page: nextPage === 1 ? '' : nextPage.toString(),
+ ...paginationParams({ state }),
}),
)
.then(({ data }) => {
- commit(types.SET_PAGE, nextPage);
- commit(types.RECEIVE_REPOS_SUCCESS, convertObjectPropsToCamelCase(data, { deep: true }));
+ const camelData = convertObjectPropsToCamelCase(data, { deep: true });
+ commitPaginationData({ state, commit, data: camelData });
+ commit(types.RECEIVE_REPOS_SUCCESS, camelData);
})
.catch((e) => {
if (hasRedirectInError(e)) {
@@ -139,6 +159,42 @@ const fetchImportFactory = (importPath = isRequired()) => (
});
};
+export const cancelImportFactory = (cancelImportPath) => ({ state, commit }, { repoId }) => {
+ const existingRepo = state.repositories.find((r) => r.importSource.id === repoId);
+
+ if (!existingRepo?.importedProject) {
+ throw new Error(`Attempting to cancel project which is not started: ${repoId}`);
+ }
+
+ const { id } = existingRepo.importedProject;
+
+ return axios
+ .post(cancelImportPath, {
+ project_id: id,
+ })
+ .then(() => {
+ commit(types.CANCEL_IMPORT_SUCCESS, {
+ repoId,
+ });
+ })
+ .catch((e) => {
+ const serverErrorMessage = e?.response?.data?.errors;
+ const flashMessage = serverErrorMessage
+ ? sprintf(
+ s__('ImportProjects|Cancelling project import failed: %{reason}'),
+ {
+ reason: serverErrorMessage,
+ },
+ false,
+ )
+ : s__('ImportProjects|Cancelling project import failed');
+
+ createAlert({
+ message: flashMessage,
+ });
+ });
+};
+
export const fetchJobsFactory = (jobsPath = isRequired()) => ({ state, commit, dispatch }) => {
if (eTagPoll) {
stopJobsPolling();
@@ -176,22 +232,6 @@ export const fetchJobsFactory = (jobsPath = isRequired()) => ({ state, commit, d
});
};
-const fetchNamespacesFactory = (namespacesPath = isRequired()) => ({ commit }) => {
- commit(types.REQUEST_NAMESPACES);
- axios
- .get(namespacesPath)
- .then(({ data }) =>
- commit(types.RECEIVE_NAMESPACES_SUCCESS, convertObjectPropsToCamelCase(data, { deep: true })),
- )
- .catch(() => {
- createAlert({
- message: s__('ImportProjects|Requesting namespaces failed'),
- });
-
- commit(types.RECEIVE_NAMESPACES_ERROR);
- });
-};
-
const setFilter = ({ commit, dispatch }, filter) => {
commit(types.SET_FILTER, filter);
@@ -207,6 +247,6 @@ export default ({ endpoints = isRequired() }) => ({
importAll,
fetchRepos: fetchReposFactory({ reposPath: endpoints.reposPath }),
fetchImport: fetchImportFactory(endpoints.importPath),
+ cancelImport: cancelImportFactory(endpoints.cancelPath),
fetchJobs: fetchJobsFactory(endpoints.jobsPath),
- fetchNamespaces: fetchNamespacesFactory(endpoints.namespacesPath),
});
diff --git a/app/assets/javascripts/import_entities/import_projects/store/getters.js b/app/assets/javascripts/import_entities/import_projects/store/getters.js
index ef01a67ec94..31ddffd4eb4 100644
--- a/app/assets/javascripts/import_entities/import_projects/store/getters.js
+++ b/app/assets/javascripts/import_entities/import_projects/store/getters.js
@@ -1,7 +1,5 @@
import { isProjectImportable, isIncompatible, isImporting } from '../utils';
-export const isLoading = (state) => state.isLoadingRepos || state.isLoadingNamespaces;
-
export const importingRepoCount = (state) => state.repositories.filter(isImporting).length;
export const isImportingAnyRepo = (state) => state.repositories.some(isImporting);
diff --git a/app/assets/javascripts/import_entities/import_projects/store/mutation_types.js b/app/assets/javascripts/import_entities/import_projects/store/mutation_types.js
index 6adf5e59cff..74832a03ac1 100644
--- a/app/assets/javascripts/import_entities/import_projects/store/mutation_types.js
+++ b/app/assets/javascripts/import_entities/import_projects/store/mutation_types.js
@@ -2,14 +2,12 @@ export const REQUEST_REPOS = 'REQUEST_REPOS';
export const RECEIVE_REPOS_SUCCESS = 'RECEIVE_REPOS_SUCCESS';
export const RECEIVE_REPOS_ERROR = 'RECEIVE_REPOS_ERROR';
-export const REQUEST_NAMESPACES = 'REQUEST_NAMESPACES';
-export const RECEIVE_NAMESPACES_SUCCESS = 'RECEIVE_NAMESPACES_SUCCESS';
-export const RECEIVE_NAMESPACES_ERROR = 'RECEIVE_NAMESPACES_ERROR';
-
export const REQUEST_IMPORT = 'REQUEST_IMPORT';
export const RECEIVE_IMPORT_SUCCESS = 'RECEIVE_IMPORT_SUCCESS';
export const RECEIVE_IMPORT_ERROR = 'RECEIVE_IMPORT_ERROR';
+export const CANCEL_IMPORT_SUCCESS = 'CANCEL_IMPORT_SUCCESS';
+
export const RECEIVE_JOBS_SUCCESS = 'RECEIVE_JOBS_SUCCESS';
export const SET_FILTER = 'SET_FILTER';
@@ -18,4 +16,4 @@ export const SET_IMPORT_TARGET = 'SET_IMPORT_TARGET';
export const SET_PAGE = 'SET_PAGE';
-export const SET_PAGE_INFO = 'SET_PAGE_INFO';
+export const SET_PAGE_CURSORS = 'SET_PAGE_CURSORS';
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 163a19976de..8b2e0364d7a 100644
--- a/app/assets/javascripts/import_entities/import_projects/store/mutations.js
+++ b/app/assets/javascripts/import_entities/import_projects/store/mutations.js
@@ -36,7 +36,12 @@ export default {
[types.SET_FILTER](state, filter) {
state.filter = filter;
state.repositories = [];
- state.pageInfo.page = 0;
+ state.pageInfo = {
+ page: 0,
+ startCursor: null,
+ endCursor: null,
+ hasNextPage: true,
+ };
},
[types.REQUEST_REPOS](state) {
@@ -51,7 +56,9 @@ export default {
// https://gitlab.com/gitlab-org/gitlab/-/issues/27370#note_379034091
const newImportedProjects = processLegacyEntries({
- newRepositories: repositories.importedProjects,
+ newRepositories: repositories.importedProjects.filter(
+ (p) => p.importStatus !== STATUSES.CANCELED,
+ ),
existingRepositories: state.repositories,
factory: makeNewImportedProject,
});
@@ -122,17 +129,9 @@ export default {
});
},
- [types.REQUEST_NAMESPACES](state) {
- state.isLoadingNamespaces = true;
- },
-
- [types.RECEIVE_NAMESPACES_SUCCESS](state, namespaces) {
- state.isLoadingNamespaces = false;
- state.namespaces = namespaces;
- },
-
- [types.RECEIVE_NAMESPACES_ERROR](state) {
- state.isLoadingNamespaces = false;
+ [types.CANCEL_IMPORT_SUCCESS](state, { repoId }) {
+ const existingRepo = state.repositories.find((r) => r.importSource.id === repoId);
+ existingRepo.importedProject.importStatus = STATUSES.CANCELED;
},
[types.SET_IMPORT_TARGET](state, { repoId, importTarget }) {
@@ -151,4 +150,9 @@ export default {
[types.SET_PAGE](state, page) {
state.pageInfo.page = page;
},
+
+ [types.SET_PAGE_CURSORS](state, pageInfo) {
+ const { startCursor, endCursor, hasNextPage } = pageInfo;
+ state.pageInfo = { ...state.pageInfo, startCursor, endCursor, hasNextPage };
+ },
};
diff --git a/app/assets/javascripts/import_entities/import_projects/store/state.js b/app/assets/javascripts/import_entities/import_projects/store/state.js
index ecd93561d52..c384848f0a0 100644
--- a/app/assets/javascripts/import_entities/import_projects/store/state.js
+++ b/app/assets/javascripts/import_entities/import_projects/store/state.js
@@ -1,13 +1,14 @@
export default () => ({
provider: '',
repositories: [],
- namespaces: [],
customImportTargets: {},
isLoadingRepos: false,
- isLoadingNamespaces: false,
ciCdOnly: false,
filter: '',
pageInfo: {
page: 0,
+ startCursor: null,
+ endCursor: null,
+ hasNextPage: true,
},
});
diff --git a/app/assets/javascripts/import_entities/import_projects/utils.js b/app/assets/javascripts/import_entities/import_projects/utils.js
index 38bd529321a..c4c9e544c1e 100644
--- a/app/assets/javascripts/import_entities/import_projects/utils.js
+++ b/app/assets/javascripts/import_entities/import_projects/utils.js
@@ -9,7 +9,10 @@ export function getImportStatus(project) {
}
export function isProjectImportable(project) {
- return !isIncompatible(project) && getImportStatus(project) === STATUSES.NONE;
+ return (
+ !isIncompatible(project) &&
+ [STATUSES.NONE, STATUSES.CANCELED].includes(getImportStatus(project))
+ );
}
export function isImporting(repo) {
diff --git a/app/assets/javascripts/incidents/components/incidents_list.vue b/app/assets/javascripts/incidents/components/incidents_list.vue
index dbd2225167a..14ab7b2dc1e 100644
--- a/app/assets/javascripts/incidents/components/incidents_list.vue
+++ b/app/assets/javascripts/incidents/components/incidents_list.vue
@@ -14,7 +14,7 @@ import {
import { isValidSlaDueAt } from 'ee_else_ce/vue_shared/components/incidents/utils';
import { visitUrl, mergeUrlParams, joinPaths } from '~/lib/utils/url_utility';
import { s__, n__ } from '~/locale';
-import { INCIDENT_SEVERITY } from '~/sidebar/components/severity/constants';
+import { INCIDENT_SEVERITY } from '~/sidebar/constants';
import SeverityToken from '~/sidebar/components/severity/severity.vue';
import Tracking from '~/tracking';
import {
diff --git a/app/assets/javascripts/incidents_settings/incidents_settings_service.js b/app/assets/javascripts/incidents_settings/incidents_settings_service.js
index 93baa54956a..d3850114350 100644
--- a/app/assets/javascripts/incidents_settings/incidents_settings_service.js
+++ b/app/assets/javascripts/incidents_settings/incidents_settings_service.js
@@ -1,4 +1,4 @@
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import axios from '~/lib/utils/axios_utils';
import { refreshCurrentPage } from '~/lib/utils/url_utility';
import { ERROR_MSG } from './constants';
@@ -22,7 +22,7 @@ export default class IncidentsSettingsService {
.catch(({ response }) => {
const message = response?.data?.message || '';
- createFlash({
+ createAlert({
message: `${ERROR_MSG} ${message}`,
});
});
diff --git a/app/assets/javascripts/integrations/edit/components/dynamic_field.vue b/app/assets/javascripts/integrations/edit/components/dynamic_field.vue
index fe687ea9767..904e5639cac 100644
--- a/app/assets/javascripts/integrations/edit/components/dynamic_field.vue
+++ b/app/assets/javascripts/integrations/edit/components/dynamic_field.vue
@@ -1,14 +1,8 @@
<script>
-import {
- GlFormGroup,
- GlFormCheckbox,
- GlFormInput,
- GlFormSelect,
- GlFormTextarea,
- GlSafeHtmlDirective as SafeHtml,
-} from '@gitlab/ui';
+import { GlFormGroup, GlFormCheckbox, GlFormInput, GlFormSelect, GlFormTextarea } from '@gitlab/ui';
import { capitalize, lowerCase, isEmpty } from 'lodash';
import { mapGetters } from 'vuex';
+import SafeHtml from '~/vue_shared/directives/safe_html';
export default {
name: 'DynamicField',
@@ -80,7 +74,7 @@ export default {
};
},
computed: {
- ...mapGetters(['isInheriting']),
+ ...mapGetters(['isInheriting', 'propsSource']),
isCheckbox() {
return this.type === 'checkbox';
},
@@ -122,11 +116,18 @@ export default {
name: this.fieldName,
state: this.valid,
readonly: this.isInheriting,
+ disabled: this.isDisabled,
};
},
valid() {
return !this.required || !isEmpty(this.model) || this.isNonEmptyPassword || !this.isValidated;
},
+ isInheritingOrDisabled() {
+ return this.isInheriting || this.isDisabled;
+ },
+ isDisabled() {
+ return !this.propsSource.editable;
+ },
},
created() {
if (this.isNonEmptyPassword) {
@@ -149,7 +150,7 @@ export default {
<template v-if="isCheckbox">
<input :name="fieldName" type="hidden" :value="model || false" />
- <gl-form-checkbox :id="fieldId" v-model="model" :disabled="isInheriting">
+ <gl-form-checkbox :id="fieldId" v-model="model" :disabled="isInheritingOrDisabled">
{{ checkboxLabel || humanizedTitle }}
<template #help>
<span v-safe-html="help"></span>
@@ -158,7 +159,12 @@ export default {
</template>
<template v-else-if="isSelect">
<input type="hidden" :name="fieldName" :value="model" />
- <gl-form-select :id="fieldId" v-model="model" :options="options" :disabled="isInheriting" />
+ <gl-form-select
+ :id="fieldId"
+ v-model="model"
+ :options="options"
+ :disabled="isInheritingOrDisabled"
+ />
</template>
<gl-form-textarea
v-else-if="isTextarea"
diff --git a/app/assets/javascripts/integrations/edit/components/integration_form.vue b/app/assets/javascripts/integrations/edit/components/integration_form.vue
index 4bf2b8d4468..d86e6326f64 100644
--- a/app/assets/javascripts/integrations/edit/components/integration_form.vue
+++ b/app/assets/javascripts/integrations/edit/components/integration_form.vue
@@ -1,22 +1,15 @@
<script>
-import {
- GlAlert,
- GlBadge,
- GlButton,
- GlModalDirective,
- GlSafeHtmlDirective as SafeHtml,
- GlForm,
-} from '@gitlab/ui';
+import { GlAlert, GlBadge, GlButton, GlForm } from '@gitlab/ui';
import axios from 'axios';
import * as Sentry from '@sentry/browser';
import { mapState, mapActions, mapGetters } from 'vuex';
import { s__ } from '~/locale';
+import SafeHtml from '~/vue_shared/directives/safe_html';
import {
I18N_FETCH_TEST_SETTINGS_DEFAULT_ERROR_MESSAGE,
I18N_DEFAULT_ERROR_MESSAGE,
I18N_SUCCESSFUL_CONNECTION_MESSAGE,
INTEGRATION_FORM_TYPE_SLACK,
- integrationLevels,
integrationFormSectionComponents,
billingPlanNames,
} from '~/integrations/constants';
@@ -25,11 +18,10 @@ import csrf from '~/lib/utils/csrf';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { testIntegrationSettings } from '../api';
import ActiveCheckbox from './active_checkbox.vue';
-import ConfirmationModal from './confirmation_modal.vue';
import DynamicField from './dynamic_field.vue';
import OverrideDropdown from './override_dropdown.vue';
-import ResetConfirmationModal from './reset_confirmation_modal.vue';
import TriggerFields from './trigger_fields.vue';
+import IntegrationFormActions from './integration_form_actions.vue';
export default {
name: 'IntegrationForm',
@@ -38,8 +30,7 @@ export default {
ActiveCheckbox,
TriggerFields,
DynamicField,
- ConfirmationModal,
- ResetConfirmationModal,
+ IntegrationFormActions,
IntegrationSectionConfiguration: () =>
import(
/* webpackChunkName: 'integrationSectionConfiguration' */ '~/integrations/edit/components/sections/configuration.vue'
@@ -66,7 +57,6 @@ export default {
GlForm,
},
directives: {
- GlModal: GlModalDirective,
SafeHtml,
},
mixins: [glFeatureFlagsMixin()],
@@ -78,10 +68,10 @@ export default {
data() {
return {
integrationActive: false,
- isTesting: false,
+ isValidated: false,
isSaving: false,
+ isTesting: false,
isResetting: false,
- isValidated: false,
};
},
computed: {
@@ -90,21 +80,6 @@ export default {
isEditable() {
return this.propsSource.editable;
},
- isInstanceOrGroupLevel() {
- return (
- this.customState.integrationLevel === integrationLevels.INSTANCE ||
- this.customState.integrationLevel === integrationLevels.GROUP
- );
- },
- showResetButton() {
- return this.isInstanceOrGroupLevel && this.propsSource.resetPath;
- },
- showTestButton() {
- return this.propsSource.canTest;
- },
- disableButtons() {
- return Boolean(this.isSaving || this.isResetting || this.isTesting);
- },
hasSections() {
if (this.hasSlackNotificationsDisabled) {
return false;
@@ -134,6 +109,14 @@ export default {
}
return !this.hasSections && this.helpHtml;
},
+ shouldUpgradeSlack() {
+ return (
+ this.isSlackIntegration &&
+ this.glFeatures.integrationSlackAppNotifications &&
+ this.customState.shouldUpgradeSlack &&
+ (this.hasFieldsWithoutSection || this.hasSections)
+ );
+ },
},
methods: {
...mapActions(['setOverride', 'requestJiraIssueTypes']),
@@ -148,7 +131,6 @@ export default {
},
onSaveClick() {
this.isSaving = true;
-
if (this.integrationActive && !this.form().checkValidity()) {
this.isSaving = false;
this.setIsValidated();
@@ -194,7 +176,6 @@ export default {
},
onResetClick() {
this.isResetting = true;
-
return axios
.post(this.propsSource.resetPath)
.then(() => {
@@ -227,7 +208,10 @@ export default {
billingPlanNames,
slackUpgradeInfo: {
title: s__(
- `SlackIntegration|Notifications only work if you're on the latest version of the GitLab for Slack app`,
+ `SlackIntegration|Update to the latest version of GitLab for Slack to get notifications`,
+ ),
+ text: s__(
+ `SlackIntegration|Update to the latest version to receive notifications from GitLab.`,
),
btnText: s__('SlackIntegration|Update to the latest version'),
},
@@ -284,16 +268,18 @@ export default {
</div>
</section>
+ <div v-if="shouldUpgradeSlack" class="gl-border-t">
+ <gl-alert
+ :dismissible="false"
+ :title="$options.slackUpgradeInfo.title"
+ :primary-button-link="customState.upgradeSlackUrl"
+ :primary-button-text="$options.slackUpgradeInfo.btnText"
+ class="gl-mb-8 gl-mt-5"
+ >{{ $options.slackUpgradeInfo.text }}</gl-alert
+ >
+ </div>
+
<template v-if="hasSections">
- <div v-if="customState.shouldUpgradeSlack && isSlackIntegration" class="gl-border-t">
- <gl-alert
- :title="$options.slackUpgradeInfo.title"
- variant="warning"
- :primary-button-link="customState.upgradeSlackUrl"
- :primary-button-text="$options.slackUpgradeInfo.btnText"
- class="gl-mb-8 gl-mt-5"
- />
- </div>
<div
v-for="(section, index) in customState.sections"
:key="section.type"
@@ -344,71 +330,16 @@ export default {
</div>
</section>
- <section v-if="isEditable" :class="!hasSections && 'gl-lg-display-flex gl-justify-content-end'">
- <div :class="!hasSections && 'gl-flex-basis-two-thirds'">
- <div
- class="footer-block row-content-block gl-lg-display-flex gl-justify-content-space-between"
- >
- <div>
- <template v-if="isInstanceOrGroupLevel">
- <gl-button
- v-gl-modal.confirmSaveIntegration
- category="primary"
- variant="confirm"
- :loading="isSaving"
- :disabled="disableButtons"
- data-testid="save-button-instance-group"
- data-qa-selector="save_changes_button"
- >
- {{ __('Save changes') }}
- </gl-button>
- <confirmation-modal @submit="onSaveClick" />
- </template>
- <gl-button
- v-else
- category="primary"
- variant="confirm"
- type="submit"
- :loading="isSaving"
- :disabled="disableButtons"
- data-testid="save-button"
- data-qa-selector="save_changes_button"
- @click.prevent="onSaveClick"
- >
- {{ __('Save changes') }}
- </gl-button>
-
- <gl-button
- v-if="showTestButton"
- category="secondary"
- variant="confirm"
- :loading="isTesting"
- :disabled="disableButtons"
- data-testid="test-button"
- @click.prevent="onTestClick"
- >
- {{ __('Test settings') }}
- </gl-button>
-
- <gl-button :href="propsSource.cancelPath">{{ __('Cancel') }}</gl-button>
- </div>
-
- <template v-if="showResetButton">
- <gl-button
- v-gl-modal.confirmResetIntegration
- category="tertiary"
- variant="danger"
- :loading="isResetting"
- :disabled="disableButtons"
- data-testid="reset-button"
- >
- {{ __('Reset') }}
- </gl-button>
-
- <reset-confirmation-modal @reset="onResetClick" />
- </template>
- </div>
- </div>
- </section>
+ <integration-form-actions
+ v-if="isEditable"
+ :has-sections="hasSections"
+ :class="{ 'gl-lg-display-flex gl-justify-content-end': !hasSections }"
+ :is-saving="isSaving"
+ :is-testing="isTesting"
+ :is-resetting="isResetting"
+ @save="onSaveClick"
+ @test="onTestClick"
+ @reset="onResetClick"
+ />
</gl-form>
</template>
diff --git a/app/assets/javascripts/integrations/edit/components/integration_form_actions.vue b/app/assets/javascripts/integrations/edit/components/integration_form_actions.vue
new file mode 100644
index 00000000000..e5ad5149cf7
--- /dev/null
+++ b/app/assets/javascripts/integrations/edit/components/integration_form_actions.vue
@@ -0,0 +1,143 @@
+<script>
+import { GlButton, GlModalDirective } from '@gitlab/ui';
+import { mapState, mapGetters } from 'vuex';
+import { integrationLevels } from '~/integrations/constants';
+import ConfirmationModal from './confirmation_modal.vue';
+import ResetConfirmationModal from './reset_confirmation_modal.vue';
+
+export default {
+ name: 'IntegrationFormActions',
+ components: {
+ GlButton,
+ ConfirmationModal,
+ ResetConfirmationModal,
+ },
+ directives: {
+ GlModal: GlModalDirective,
+ },
+ props: {
+ hasSections: {
+ type: Boolean,
+ required: true,
+ },
+ isSaving: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ isTesting: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ isResetting: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ },
+ computed: {
+ ...mapGetters(['propsSource']),
+ ...mapState(['customState']),
+ isInstanceOrGroupLevel() {
+ return (
+ this.customState.integrationLevel === integrationLevels.INSTANCE ||
+ this.customState.integrationLevel === integrationLevels.GROUP
+ );
+ },
+ showResetButton() {
+ return this.isInstanceOrGroupLevel && this.propsSource.resetPath;
+ },
+ showTestButton() {
+ return this.propsSource.canTest;
+ },
+ disableButtons() {
+ return Boolean(this.isSaving || this.isResetting || this.isTesting);
+ },
+ },
+ methods: {
+ onSaveClick() {
+ this.$emit('save');
+ },
+ onTestClick() {
+ this.$emit('test');
+ },
+ onResetClick() {
+ this.$emit('reset');
+ },
+ },
+};
+</script>
+<template>
+ <section>
+ <div :class="{ 'gl-flex-basis-two-thirds': !hasSections }">
+ <div
+ class="footer-block row-content-block gl-lg-display-flex gl-justify-content-space-between"
+ >
+ <div>
+ <template v-if="isInstanceOrGroupLevel">
+ <gl-button
+ v-gl-modal.confirmSaveIntegration
+ category="primary"
+ variant="confirm"
+ :loading="isSaving"
+ :disabled="disableButtons"
+ data-testid="save-button"
+ data-qa-selector="save_changes_button"
+ >
+ {{ __('Save changes') }}
+ </gl-button>
+ <confirmation-modal @submit="onSaveClick" />
+ </template>
+ <gl-button
+ v-else
+ category="primary"
+ variant="confirm"
+ type="submit"
+ :loading="isSaving"
+ :disabled="disableButtons"
+ data-testid="save-button"
+ data-qa-selector="save_changes_button"
+ @click.prevent="onSaveClick"
+ >
+ {{ __('Save changes') }}
+ </gl-button>
+
+ <gl-button
+ v-if="showTestButton"
+ category="secondary"
+ variant="confirm"
+ :loading="isTesting"
+ :disabled="disableButtons"
+ data-testid="test-button"
+ @click.prevent="onTestClick"
+ >
+ {{ __('Test settings') }}
+ </gl-button>
+
+ <gl-button
+ :href="propsSource.cancelPath"
+ data-testid="cancel-button"
+ :disabled="disableButtons"
+ >{{ __('Cancel') }}</gl-button
+ >
+ </div>
+
+ <template v-if="showResetButton">
+ <gl-button
+ v-gl-modal.confirmResetIntegration
+ category="tertiary"
+ variant="danger"
+ :loading="isResetting"
+ :disabled="disableButtons"
+ data-testid="reset-button"
+ >
+ {{ __('Reset') }}
+ </gl-button>
+
+ <reset-confirmation-modal @reset="onResetClick" />
+ </template>
+ </div>
+ </div>
+ </section>
+</template>
diff --git a/app/assets/javascripts/integrations/edit/index.js b/app/assets/javascripts/integrations/edit/index.js
index f15ad5e052e..b53bcd50f16 100644
--- a/app/assets/javascripts/integrations/edit/index.js
+++ b/app/assets/javascripts/integrations/edit/index.js
@@ -108,6 +108,7 @@ export default function initIntegrationSettingsForm() {
const initialState = {
defaultState: null,
customState: customSettingsProps,
+ editable: customSettingsProps.editable && !customSettingsProps.shouldUpgradeSlack,
};
if (defaultSettingsEl) {
initialState.defaultState = Object.freeze(parseDatasetToProps(defaultSettingsEl.dataset));
diff --git a/app/assets/javascripts/invite_members/components/import_project_members_modal.vue b/app/assets/javascripts/invite_members/components/import_project_members_modal.vue
index 31b7fd4cc42..b4e9a3a1559 100644
--- a/app/assets/javascripts/invite_members/components/import_project_members_modal.vue
+++ b/app/assets/javascripts/invite_members/components/import_project_members_modal.vue
@@ -5,6 +5,10 @@ import { importProjectMembers } from '~/api/projects_api';
import { BV_SHOW_MODAL } from '~/lib/utils/constants';
import { s__, __, sprintf } from '~/locale';
import eventHub from '../event_hub';
+import {
+ displaySuccessfulInvitationAlert,
+ reloadOnInvitationSuccess,
+} from '../utils/trigger_successful_invite_alert';
import ProjectSelect from './project_select.vue';
export default {
@@ -24,6 +28,11 @@ export default {
type: String,
required: true,
},
+ reloadPageOnSubmit: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
},
data() {
return {
@@ -59,6 +68,10 @@ export default {
},
},
mounted() {
+ if (this.reloadPageOnSubmit) {
+ displaySuccessfulInvitationAlert();
+ }
+
eventHub.$on('openProjectMembersModal', () => {
this.openModal();
});
@@ -74,16 +87,22 @@ export default {
submitImport() {
this.isLoading = true;
return importProjectMembers(this.projectId, this.projectToBeImported.id)
- .then(this.showToastMessage)
+ .then(this.onInviteSuccess)
.catch(this.showErrorAlert)
.finally(() => {
this.isLoading = false;
this.projectToBeImported = {};
});
},
+ onInviteSuccess() {
+ if (this.reloadPageOnSubmit) {
+ reloadOnInvitationSuccess();
+ } else {
+ this.showToastMessage();
+ }
+ },
showToastMessage() {
this.$toast.show(this.$options.i18n.successMessage, this.$options.toastOptions);
-
this.closeModal();
},
showErrorAlert() {
diff --git a/app/assets/javascripts/invite_members/components/invite_group_notification.vue b/app/assets/javascripts/invite_members/components/invite_group_notification.vue
new file mode 100644
index 00000000000..767675cc64c
--- /dev/null
+++ b/app/assets/javascripts/invite_members/components/invite_group_notification.vue
@@ -0,0 +1,37 @@
+<script>
+import { GlAlert, GlSprintf, GlLink } from '@gitlab/ui';
+import { GROUP_MODAL_ALERT_BODY } from '../constants';
+
+const SHARE_GROUP_LINK =
+ 'https://docs.gitlab.com/ee/user/group/manage.html#share-a-group-with-another-group';
+
+export default {
+ SHARE_GROUP_LINK,
+ name: 'InviteGroupNotification',
+ components: { GlAlert, GlSprintf, GlLink },
+ inject: ['freeUsersLimit'],
+ props: {
+ name: {
+ type: String,
+ required: true,
+ },
+ },
+ i18n: {
+ body: GROUP_MODAL_ALERT_BODY,
+ },
+};
+</script>
+
+<template>
+ <gl-alert variant="warning" :dismissible="false">
+ <gl-sprintf :message="$options.i18n.body">
+ <template #link="{ content }">
+ <gl-link :href="$options.SHARE_GROUP_LINK" target="_blank" class="gl-label-link">{{
+ content
+ }}</gl-link>
+ </template>
+
+ <template #count>{{ freeUsersLimit }}</template>
+ </gl-sprintf>
+ </gl-alert>
+</template>
diff --git a/app/assets/javascripts/invite_members/components/invite_groups_modal.vue b/app/assets/javascripts/invite_members/components/invite_groups_modal.vue
index 2ad4bb1a11a..3be3b9df747 100644
--- a/app/assets/javascripts/invite_members/components/invite_groups_modal.vue
+++ b/app/assets/javascripts/invite_members/components/invite_groups_modal.vue
@@ -6,13 +6,19 @@ import InviteModalBase from 'ee_else_ce/invite_members/components/invite_modal_b
import { GROUP_FILTERS, GROUP_MODAL_LABELS } from '../constants';
import eventHub from '../event_hub';
import { getInvalidFeedbackMessage } from '../utils/get_invalid_feedback_message';
+import {
+ displaySuccessfulInvitationAlert,
+ reloadOnInvitationSuccess,
+} from '../utils/trigger_successful_invite_alert';
import GroupSelect from './group_select.vue';
+import InviteGroupNotification from './invite_group_notification.vue';
export default {
name: 'InviteMembersModal',
components: {
GroupSelect,
InviteModalBase,
+ InviteGroupNotification,
},
props: {
id: {
@@ -31,6 +37,10 @@ export default {
type: String,
required: true,
},
+ fullPath: {
+ type: String,
+ required: true,
+ },
accessLevels: {
type: Object,
required: true,
@@ -57,6 +67,15 @@ export default {
type: Array,
required: true,
},
+ freeUserCapEnabled: {
+ type: Boolean,
+ required: true,
+ },
+ reloadPageOnSubmit: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
},
data() {
return {
@@ -85,6 +104,10 @@ export default {
},
},
mounted() {
+ if (this.reloadPageOnSubmit) {
+ displaySuccessfulInvitationAlert();
+ }
+
eventHub.$on('openGroupModal', () => {
this.openModal();
});
@@ -114,7 +137,7 @@ export default {
expires_at: expiresAt,
})
.then(() => {
- this.showSuccessMessage();
+ this.onInviteSuccess();
})
.catch((e) => {
this.showInvalidFeedbackMessage(e);
@@ -128,6 +151,13 @@ export default {
this.isLoading = false;
this.groupToBeSharedWith = {};
},
+ onInviteSuccess() {
+ if (this.reloadPageOnSubmit) {
+ reloadOnInvitationSuccess();
+ } else {
+ this.showSuccessMessage();
+ }
+ },
showSuccessMessage() {
this.$toast.show(this.$options.labels.toastMessageSuccessful, this.toastOptions);
this.closeModal();
@@ -155,9 +185,14 @@ export default {
:root-group-id="rootId"
:invalid-feedback-message="invalidFeedbackMessage"
:is-loading="isLoading"
+ :full-path="fullPath"
@reset="resetFields"
@submit="sendInvite"
>
+ <template #alert>
+ <invite-group-notification v-if="freeUserCapEnabled" :name="name" />
+ </template>
+
<template #select>
<group-select
v-model="groupToBeSharedWith"
diff --git a/app/assets/javascripts/invite_members/components/invite_members_modal.vue b/app/assets/javascripts/invite_members/components/invite_members_modal.vue
index f61e822bf7e..fbb547c28ff 100644
--- a/app/assets/javascripts/invite_members/components/invite_members_modal.vue
+++ b/app/assets/javascripts/invite_members/components/invite_members_modal.vue
@@ -29,6 +29,10 @@ import eventHub from '../event_hub';
import { responseFromSuccess } from '../utils/response_message_parser';
import { memberName } from '../utils/member_utils';
import { getInvalidFeedbackMessage } from '../utils/get_invalid_feedback_message';
+import {
+ displaySuccessfulInvitationAlert,
+ reloadOnInvitationSuccess,
+} from '../utils/trigger_successful_invite_alert';
import ModalConfetti from './confetti.vue';
import MembersTokenSelect from './members_token_select.vue';
import UserLimitNotification from './user_limit_notification.vue';
@@ -98,11 +102,20 @@ export default {
type: Array,
required: true,
},
+ fullPath: {
+ type: String,
+ required: true,
+ },
usersLimitDataset: {
type: Object,
required: false,
default: () => ({}),
},
+ reloadPageOnSubmit: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
},
data() {
return {
@@ -119,7 +132,7 @@ export default {
selectedAccessLevel: undefined,
errorsLimit: 2,
isErrorsSectionExpanded: false,
- emptyInvitesError: false,
+ shouldShowEmptyInvitesAlert: false,
};
},
computed: {
@@ -204,12 +217,15 @@ export default {
count: this.errorsExpanded.length,
});
},
+ formGroupDescription() {
+ return this.invalidFeedbackMessage ? null : this.$options.labels.placeHolder;
+ },
},
watch: {
isEmptyInvites: {
handler(updatedValue) {
// nothing to do if the invites are **still** empty and the emptyInvites were never set from submit
- if (!updatedValue && !this.emptyInvitesError) {
+ if (!updatedValue && !this.shouldShowEmptyInvitesAlert) {
return;
}
@@ -218,6 +234,10 @@ export default {
},
},
mounted() {
+ if (this.reloadPageOnSubmit) {
+ displaySuccessfulInvitationAlert();
+ }
+
eventHub.$on('openModal', (options) => {
this.openModal(options);
if (this.isOnLearnGitlab) {
@@ -258,16 +278,17 @@ export default {
const tracking = new ExperimentTracking(experimentName);
tracking.event(eventName);
},
- showEmptyInvitesError() {
- this.invalidFeedbackMessage = this.$options.labels.emptyInvitesErrorText;
- this.emptyInvitesError = true;
+ showEmptyInvitesAlert() {
+ this.invalidFeedbackMessage = this.$options.labels.placeHolder;
+ this.shouldShowEmptyInvitesAlert = true;
+ this.$refs.alerts.focus();
},
sendInvite({ accessLevel, expiresAt }) {
this.isLoading = true;
this.clearValidation();
if (!this.isEmptyInvites) {
- this.showEmptyInvitesError();
+ this.showEmptyInvitesAlert();
return;
}
@@ -298,7 +319,7 @@ export default {
if (error) {
this.showMemberErrors(message);
} else {
- this.showSuccessMessage();
+ this.onInviteSuccess();
}
})
.catch((e) => this.showInvalidFeedbackMessage(e))
@@ -308,6 +329,7 @@ export default {
},
showMemberErrors(message) {
this.invalidMembers = message;
+ this.$refs.alerts.focus();
},
tokenName(username) {
// initial token creation hits this and nothing is found... so safe navigation
@@ -322,6 +344,7 @@ export default {
resetFields() {
this.clearValidation();
this.isLoading = false;
+ this.shouldShowEmptyInvitesAlert = false;
this.newUsersToInvite = [];
this.selectedTasksToBeDone = [];
[this.selectedTaskProject] = this.projects;
@@ -329,6 +352,13 @@ export default {
changeSelectedTaskProject(project) {
this.selectedTaskProject = project;
},
+ onInviteSuccess() {
+ if (this.reloadPageOnSubmit) {
+ reloadOnInvitationSuccess();
+ } else {
+ this.showSuccessMessage();
+ }
+ },
showSuccessMessage() {
if (this.isOnLearnGitlab) {
eventHub.$emit('showSuccessfulInvitationsAlert');
@@ -347,7 +377,7 @@ export default {
},
clearEmptyInviteError() {
this.invalidFeedbackMessage = '';
- this.emptyInvitesError = false;
+ this.shouldShowEmptyInvitesAlert = false;
},
removeToken(token) {
delete this.invalidMembers[memberName(token)];
@@ -370,12 +400,13 @@ export default {
:help-link="helpLink"
:label-intro-text="labelIntroText"
:label-search-field="$options.labels.searchField"
- :form-group-description="$options.labels.placeHolder"
+ :form-group-description="formGroupDescription"
:invalid-feedback-message="invalidFeedbackMessage"
:is-loading="isLoading"
:new-users-to-invite="newUsersToInvite"
:root-group-id="rootId"
:users-limit-dataset="usersLimitDataset"
+ :full-path="fullPath"
@reset="resetFields"
@submit="sendInvite"
@access-level="onAccessLevelUpdate"
@@ -390,59 +421,77 @@ export default {
</template>
<template #alert>
- <gl-alert
- v-if="hasInvalidMembers"
- variant="danger"
- :dismissible="false"
- :title="memberErrorTitle"
- data-testid="alert-member-error"
- >
- {{ $options.labels.memberErrorListText }}
- <ul class="gl-pl-5 gl-mb-0">
- <li v-for="error in errorsLimited" :key="error.member" data-testid="errors-limited-item">
- <strong>{{ error.displayedMemberName }}:</strong> {{ error.message }}
- </li>
- </ul>
- <template v-if="shouldErrorsSectionExpand">
- <gl-collapse v-model="isErrorsSectionExpanded">
- <ul class="gl-pl-5 gl-mb-0">
- <li
- v-for="error in errorsExpanded"
- :key="error.member"
- data-testid="errors-expanded-item"
- >
- <strong>{{ error.displayedMemberName }}:</strong> {{ error.message }}
- </li>
- </ul>
- </gl-collapse>
- <gl-button
- class="gl-text-decoration-none! gl-shadow-none! gl-mt-3"
- data-testid="accordion-button"
- variant="link"
- @click="toggleErrorExpansion"
- >
- {{ errorCollapseText }}
- <gl-icon
- name="chevron-down"
- class="gl-transition-medium"
- :class="{ 'gl-rotate-180': isErrorsSectionExpanded }"
- />
- </gl-button>
- </template>
- </gl-alert>
- <user-limit-notification
- v-else-if="showUserLimitNotification"
- :limit-variant="limitVariant"
- :users-limit-dataset="usersLimitDataset"
- />
+ <div ref="alerts" tabindex="-1">
+ <gl-alert
+ v-if="shouldShowEmptyInvitesAlert"
+ id="empty-invites-alert"
+ class="gl-mb-4"
+ variant="danger"
+ :dismissible="false"
+ data-testid="empty-invites-alert"
+ >
+ {{ $options.labels.emptyInvitesAlertText }}
+ </gl-alert>
+ <gl-alert
+ v-if="hasInvalidMembers"
+ class="gl-mb-4"
+ variant="danger"
+ :dismissible="false"
+ :title="memberErrorTitle"
+ data-testid="alert-member-error"
+ >
+ {{ $options.labels.memberErrorListText }}
+ <ul class="gl-pl-5 gl-mb-0">
+ <li
+ v-for="error in errorsLimited"
+ :key="error.member"
+ data-testid="errors-limited-item"
+ >
+ <strong>{{ error.displayedMemberName }}:</strong> {{ error.message }}
+ </li>
+ </ul>
+ <template v-if="shouldErrorsSectionExpand">
+ <gl-collapse v-model="isErrorsSectionExpanded">
+ <ul class="gl-pl-5 gl-mb-0">
+ <li
+ v-for="error in errorsExpanded"
+ :key="error.member"
+ data-testid="errors-expanded-item"
+ >
+ <strong>{{ error.displayedMemberName }}:</strong> {{ error.message }}
+ </li>
+ </ul>
+ </gl-collapse>
+ <gl-button
+ class="gl-text-decoration-none! gl-shadow-none! gl-mt-3"
+ data-testid="accordion-button"
+ variant="link"
+ @click="toggleErrorExpansion"
+ >
+ {{ errorCollapseText }}
+ <gl-icon
+ name="chevron-down"
+ class="gl-transition-medium"
+ :class="{ 'gl-rotate-180': isErrorsSectionExpanded }"
+ />
+ </gl-button>
+ </template>
+ </gl-alert>
+ <user-limit-notification
+ v-else-if="showUserLimitNotification"
+ :limit-variant="limitVariant"
+ :users-limit-dataset="usersLimitDataset"
+ />
+ </div>
</template>
- <template #select="{ exceptionState, labelId }">
+ <template #select="{ exceptionState, inputId }">
<members-token-select
v-model="newUsersToInvite"
class="gl-mb-2"
+ aria-labelledby="empty-invites-alert"
+ :input-id="inputId"
:exception-state="exceptionState"
- :aria-labelledby="labelId"
:users-filter="usersFilter"
:filter-id="filterId"
:invalid-members="invalidMembers"
diff --git a/app/assets/javascripts/invite_members/components/invite_modal_base.vue b/app/assets/javascripts/invite_members/components/invite_modal_base.vue
index e3511a49fc5..2cbd681c67d 100644
--- a/app/assets/javascripts/invite_members/components/invite_modal_base.vue
+++ b/app/assets/javascripts/invite_members/components/invite_modal_base.vue
@@ -1,14 +1,5 @@
<script>
-import {
- GlFormGroup,
- GlModal,
- GlDropdown,
- GlDropdownItem,
- GlDatepicker,
- GlLink,
- GlSprintf,
- GlFormInput,
-} from '@gitlab/ui';
+import { GlFormGroup, GlFormSelect, GlModal, GlDatepicker, GlLink, GlSprintf } from '@gitlab/ui';
import Tracking from '~/tracking';
import { sprintf } from '~/locale';
import ContentTransition from '~/vue_shared/components/content_transition.vue';
@@ -37,13 +28,11 @@ const DEFAULT_SLOTS = [
export default {
components: {
GlFormGroup,
+ GlFormSelect,
GlDatepicker,
GlLink,
GlModal,
- GlDropdown,
- GlDropdownItem,
GlSprintf,
- GlFormInput,
ContentTransition,
},
mixins: [Tracking.mixin()],
@@ -141,14 +130,23 @@ export default {
};
},
computed: {
+ accessLevelsOptions() {
+ return Object.entries(this.accessLevels).map(([text, value]) => ({ text, value }));
+ },
introText() {
return sprintf(this.labelIntroText, { name: this.name });
},
exceptionState() {
return this.invalidFeedbackMessage ? false : null;
},
- selectLabelId() {
- return `${this.modalId}_select`;
+ selectId() {
+ return `${this.modalId}_search`;
+ },
+ dropdownId() {
+ return `${this.modalId}_dropdown`;
+ },
+ datepickerId() {
+ return `${this.modalId}_expires_at`;
},
selectedRoleName() {
return Object.keys(this.accessLevels).find(
@@ -218,9 +216,6 @@ export default {
this.$emit('cancel');
},
- changeSelectedItem(item) {
- this.selectedAccessLevel = item;
- },
onSubmit(e) {
// We never want to hide when submitting
e.preventDefault();
@@ -279,64 +274,50 @@ export default {
<slot name="alert"></slot>
<gl-form-group
+ :label="labelSearchField"
+ :label-for="selectId"
:invalid-feedback="invalidFeedbackMessage"
:state="exceptionState"
:description="formGroupDescription"
data-testid="members-form-group"
>
- <label :id="selectLabelId" class="col-form-label">{{ labelSearchField }}</label>
- <slot name="select" v-bind="{ exceptionState, labelId: selectLabelId }"></slot>
+ <slot name="select" v-bind="{ exceptionState, inputId: selectId }"></slot>
</gl-form-group>
- <label class="gl-font-weight-bold">{{ $options.ACCESS_LEVEL }}</label>
- <div class="gl-mt-2 gl-w-half gl-xs-w-full">
- <gl-dropdown
- class="gl-shadow-none gl-w-full"
+ <gl-form-group
+ class="gl-w-half gl-xs-w-full"
+ :label="$options.ACCESS_LEVEL"
+ :label-for="dropdownId"
+ >
+ <template #description>
+ <gl-sprintf :message="$options.READ_MORE_TEXT">
+ <template #link="{ content }">
+ <gl-link :href="helpLink" target="_blank">{{ content }}</gl-link>
+ </template>
+ </gl-sprintf>
+ </template>
+ <gl-form-select
+ :id="dropdownId"
+ v-model="selectedAccessLevel"
data-qa-selector="access_level_dropdown"
- v-bind="$attrs"
- :text="selectedRoleName"
- >
- <template v-for="(key, item) in accessLevels">
- <gl-dropdown-item
- :key="key"
- active-class="is-active"
- is-check-item
- :is-checked="key === selectedAccessLevel"
- @click="changeSelectedItem(key)"
- >
- <div>{{ item }}</div>
- </gl-dropdown-item>
- </template>
- </gl-dropdown>
- </div>
-
- <div class="gl-mt-2 gl-w-half gl-xs-w-full">
- <gl-sprintf :message="$options.READ_MORE_TEXT">
- <template #link="{ content }">
- <gl-link :href="helpLink" target="_blank">{{ content }}</gl-link>
- </template>
- </gl-sprintf>
- </div>
+ :options="accessLevelsOptions"
+ />
+ </gl-form-group>
- <label class="gl-mt-5 gl-display-block" for="expires_at">{{
- $options.ACCESS_EXPIRE_DATE
- }}</label>
- <div class="gl-mt-2 gl-w-half gl-xs-w-full gl-display-inline-block">
+ <gl-form-group
+ class="gl-w-half gl-xs-w-full"
+ :label="$options.ACCESS_EXPIRE_DATE"
+ :label-for="datepickerId"
+ >
<gl-datepicker
v-model="selectedDate"
- class="gl-display-inline!"
+ :input-id="datepickerId"
+ class="gl-display-block!"
:min-date="minDate"
:target="null"
- >
- <template #default="{ formattedDate }">
- <gl-form-input
- class="gl-w-full"
- :value="formattedDate"
- :placeholder="__(`YYYY-MM-DD`)"
- />
- </template>
- </gl-datepicker>
- </div>
+ />
+ </gl-form-group>
+
<slot name="form-after"></slot>
</template>
diff --git a/app/assets/javascripts/invite_members/components/members_token_select.vue b/app/assets/javascripts/invite_members/components/members_token_select.vue
index 2ddb04e1eeb..68602068699 100644
--- a/app/assets/javascripts/invite_members/components/members_token_select.vue
+++ b/app/assets/javascripts/invite_members/components/members_token_select.vue
@@ -49,6 +49,11 @@ export default {
type: Object,
required: true,
},
+ inputId: {
+ type: String,
+ required: false,
+ default: '',
+ },
},
data() {
return {
@@ -84,6 +89,13 @@ export default {
hasInvalidMembers() {
return !isEmpty(this.invalidMembers);
},
+ textInputAttrs() {
+ return {
+ 'data-testid': 'members-token-select-input',
+ 'data-qa-selector': 'members_token_select_input',
+ id: this.inputId,
+ };
+ },
},
watch: {
// We might not really want this to be *reactive* since we want the "class" state to be
@@ -183,10 +195,7 @@ export default {
:hide-dropdown-with-no-items="hideDropdownWithNoItems"
:placeholder="placeholderText"
:aria-labelledby="ariaLabelledby"
- :text-input-attrs="/* eslint-disable @gitlab/vue-no-new-non-primitive-in-template */ {
- 'data-testid': 'members-token-select-input',
- 'data-qa-selector': 'members_token_select_input',
- } /* eslint-enable @gitlab/vue-no-new-non-primitive-in-template */"
+ :text-input-attrs="textInputAttrs"
@blur="handleBlur"
@text-input="handleTextInput"
@input="handleInput"
diff --git a/app/assets/javascripts/invite_members/constants.js b/app/assets/javascripts/invite_members/constants.js
index de7b1019782..a894eb24d38 100644
--- a/app/assets/javascripts/invite_members/constants.js
+++ b/app/assets/javascripts/invite_members/constants.js
@@ -9,6 +9,7 @@ export const INVITE_MEMBERS_FOR_TASK = {
view: 'modal_opened_from_email',
submit: 'submit',
};
+export const TOAST_MESSAGE_LOCALSTORAGE_KEY = 'members_invited_successfully';
export const GROUP_FILTERS = {
ALL: 'all',
@@ -57,6 +58,10 @@ export const GROUP_MODAL_TO_PROJECT_DEFAULT_INTRO_TEXT = s__(
"InviteMembersModal|You're inviting a group to the %{strongStart}%{name}%{strongEnd} project.",
);
+export const GROUP_MODAL_ALERT_BODY = s__(
+ 'InviteMembersModal| Inviting a group %{linkStart}adds its members to your group%{linkEnd}, including members who join after the invite. This might put your group over the free %{count} user limit.',
+);
+
export const GROUP_SEARCH_FIELD = s__('InviteMembersModal|Select a group to invite');
export const GROUP_PLACEHOLDER = s__('InviteMembersModal|Search for a group to invite');
@@ -77,9 +82,7 @@ export const MEMBER_ERROR_LIST_TEXT = s__(
);
export const COLLAPSED_ERRORS = s__('InviteMembersModal|Show more (%{count})');
export const EXPANDED_ERRORS = s__('InviteMembersModal|Show less');
-export const EMPTY_INVITES_ERROR_TEXT = s__(
- 'InviteMembersModal|Please select members or type email addresses to invite',
-);
+export const EMPTY_INVITES_ALERT_TEXT = s__('InviteMembersModal|Please add members to invite');
export const MEMBER_MODAL_LABELS = {
modal: {
@@ -117,7 +120,7 @@ export const MEMBER_MODAL_LABELS = {
memberErrorListText: MEMBER_ERROR_LIST_TEXT,
collapsedErrors: COLLAPSED_ERRORS,
expandedErrors: EXPANDED_ERRORS,
- emptyInvitesErrorText: EMPTY_INVITES_ERROR_TEXT,
+ emptyInvitesAlertText: EMPTY_INVITES_ALERT_TEXT,
};
export const GROUP_MODAL_LABELS = {
diff --git a/app/assets/javascripts/invite_members/init_import_project_members_modal.js b/app/assets/javascripts/invite_members/init_import_project_members_modal.js
index daaa1315884..227d8395250 100644
--- a/app/assets/javascripts/invite_members/init_import_project_members_modal.js
+++ b/app/assets/javascripts/invite_members/init_import_project_members_modal.js
@@ -1,5 +1,6 @@
import Vue from 'vue';
import ImportProjectMembersModal from '~/invite_members/components/import_project_members_modal.vue';
+import { parseBoolean } from '~/lib/utils/common_utils';
export default function initImportProjectMembersModal() {
const el = document.querySelector('.js-import-project-members-modal');
@@ -8,7 +9,7 @@ export default function initImportProjectMembersModal() {
return false;
}
- const { projectId, projectName } = el.dataset;
+ const { projectId, projectName, reloadPageOnSubmit } = el.dataset;
return new Vue({
el,
@@ -17,6 +18,7 @@ export default function initImportProjectMembersModal() {
props: {
projectId,
projectName,
+ reloadPageOnSubmit: parseBoolean(reloadPageOnSubmit),
},
}),
});
diff --git a/app/assets/javascripts/invite_members/init_invite_groups_modal.js b/app/assets/javascripts/invite_members/init_invite_groups_modal.js
index be1576ad0b0..53b756b610f 100644
--- a/app/assets/javascripts/invite_members/init_invite_groups_modal.js
+++ b/app/assets/javascripts/invite_members/init_invite_groups_modal.js
@@ -28,6 +28,9 @@ export default function initInviteGroupsModal() {
return new Vue({
el,
+ provide: {
+ freeUsersLimit: parseInt(el.dataset.freeUsersLimit, 10),
+ },
render: (createElement) =>
createElement(InviteGroupsModal, {
props: {
@@ -38,6 +41,8 @@ export default function initInviteGroupsModal() {
groupSelectFilter: el.dataset.groupsFilter,
groupSelectParentId: parseInt(el.dataset.parentId, 10),
invalidGroups: JSON.parse(el.dataset.invalidGroups || '[]'),
+ freeUserCapEnabled: parseBoolean(el.dataset.freeUserCapEnabled),
+ reloadPageOnSubmit: parseBoolean(el.dataset.reloadPageOnSubmit),
},
}),
});
diff --git a/app/assets/javascripts/invite_members/init_invite_members_modal.js b/app/assets/javascripts/invite_members/init_invite_members_modal.js
index a4be3f205a3..842ab07f368 100644
--- a/app/assets/javascripts/invite_members/init_invite_members_modal.js
+++ b/app/assets/javascripts/invite_members/init_invite_members_modal.js
@@ -41,6 +41,7 @@ export default (function initInviteMembersModal() {
usersLimitDataset: convertObjectPropsToCamelCase(
JSON.parse(el.dataset.usersLimitDataset || '{}'),
),
+ reloadPageOnSubmit: parseBoolean(el.dataset.reloadPageOnSubmit),
},
}),
});
diff --git a/app/assets/javascripts/invite_members/utils/trigger_successful_invite_alert.js b/app/assets/javascripts/invite_members/utils/trigger_successful_invite_alert.js
new file mode 100644
index 00000000000..4d3a7951265
--- /dev/null
+++ b/app/assets/javascripts/invite_members/utils/trigger_successful_invite_alert.js
@@ -0,0 +1,23 @@
+import { createAlert } from '~/flash';
+import AccessorUtilities from '~/lib/utils/accessor';
+
+import { TOAST_MESSAGE_LOCALSTORAGE_KEY, TOAST_MESSAGE_SUCCESSFUL } from '../constants';
+
+export function displaySuccessfulInvitationAlert() {
+ if (!AccessorUtilities.canUseLocalStorage()) {
+ return;
+ }
+
+ const showAlert = Boolean(localStorage.getItem(TOAST_MESSAGE_LOCALSTORAGE_KEY));
+ if (showAlert) {
+ localStorage.removeItem(TOAST_MESSAGE_LOCALSTORAGE_KEY);
+ createAlert({ message: TOAST_MESSAGE_SUCCESSFUL, variant: 'info' });
+ }
+}
+
+export function reloadOnInvitationSuccess() {
+ if (AccessorUtilities.canUseLocalStorage()) {
+ localStorage.setItem(TOAST_MESSAGE_LOCALSTORAGE_KEY, 'true');
+ }
+ window.location.reload();
+}
diff --git a/app/assets/javascripts/issuable/bulk_update_sidebar/constants.js b/app/assets/javascripts/issuable/bulk_update_sidebar/constants.js
deleted file mode 100644
index 68133ceb3c7..00000000000
--- a/app/assets/javascripts/issuable/bulk_update_sidebar/constants.js
+++ /dev/null
@@ -1,23 +0,0 @@
-import { __ } from '~/locale';
-
-export const statusDropdownOptions = [
- {
- text: __('Open'),
- value: 'reopen',
- },
- {
- text: __('Closed'),
- value: 'close',
- },
-];
-
-export const subscriptionsDropdownOptions = [
- {
- text: __('Subscribe'),
- value: 'subscribe',
- },
- {
- text: __('Unsubscribe'),
- value: 'unsubscribe',
- },
-];
diff --git a/app/assets/javascripts/issuable/bulk_update_sidebar/index.js b/app/assets/javascripts/issuable/bulk_update_sidebar/index.js
deleted file mode 100644
index b7cb805ee37..00000000000
--- a/app/assets/javascripts/issuable/bulk_update_sidebar/index.js
+++ /dev/null
@@ -1,75 +0,0 @@
-import Vue from 'vue';
-import VueApollo from 'vue-apollo';
-import { gqlClient } from '../../issues/list/graphql';
-import StatusDropdown from './components/status_dropdown.vue';
-import SubscriptionsDropdown from './components/subscriptions_dropdown.vue';
-import MoveIssuesButton from './components/move_issues_button.vue';
-import issuableBulkUpdateActions from './issuable_bulk_update_actions';
-import IssuableBulkUpdateSidebar from './issuable_bulk_update_sidebar';
-
-export function initBulkUpdateSidebar(prefixId) {
- const el = document.querySelector('.issues-bulk-update');
-
- if (!el) {
- return;
- }
-
- issuableBulkUpdateActions.init({ prefixId });
- new IssuableBulkUpdateSidebar(); // eslint-disable-line no-new
-}
-
-export function initStatusDropdown() {
- const el = document.querySelector('.js-status-dropdown');
-
- if (!el) {
- return null;
- }
-
- return new Vue({
- el,
- name: 'StatusDropdownRoot',
- render: (createElement) => createElement(StatusDropdown),
- });
-}
-
-export function initSubscriptionsDropdown() {
- const el = document.querySelector('.js-subscriptions-dropdown');
-
- if (!el) {
- return null;
- }
-
- return new Vue({
- el,
- name: 'SubscriptionsDropdownRoot',
- render: (createElement) => createElement(SubscriptionsDropdown),
- });
-}
-
-export function initMoveIssuesButton() {
- const el = document.querySelector('.js-move-issues');
-
- if (!el) {
- return null;
- }
-
- const { dataset } = el;
-
- Vue.use(VueApollo);
- const apolloProvider = new VueApollo({
- defaultClient: gqlClient,
- });
-
- return new Vue({
- el,
- name: 'MoveIssuesRoot',
- apolloProvider,
- render: (createElement) =>
- createElement(MoveIssuesButton, {
- props: {
- projectFullPath: dataset.projectFullPath,
- projectsFetchPath: dataset.projectsFetchPath,
- },
- }),
- });
-}
diff --git a/app/assets/javascripts/issuable/components/related_issuable_item.vue b/app/assets/javascripts/issuable/components/related_issuable_item.vue
index 254248ef1d4..fd55f05e955 100644
--- a/app/assets/javascripts/issuable/components/related_issuable_item.vue
+++ b/app/assets/javascripts/issuable/components/related_issuable_item.vue
@@ -1,13 +1,7 @@
<script>
import '~/commons/bootstrap';
-import {
- GlIcon,
- GlLink,
- GlTooltip,
- GlTooltipDirective,
- GlButton,
- GlSafeHtmlDirective as SafeHtml,
-} from '@gitlab/ui';
+import { GlIcon, GlLink, GlTooltip, GlTooltipDirective, GlButton } from '@gitlab/ui';
+import SafeHtml from '~/vue_shared/directives/safe_html';
import IssueDueDate from '~/boards/components/issue_due_date.vue';
import { TYPE_WORK_ITEM } from '~/graphql_shared/constants';
import { convertToGraphQLId } from '~/graphql_shared/utils';
diff --git a/app/assets/javascripts/issuable/index.js b/app/assets/javascripts/issuable/index.js
index 10dbefce503..ed336deb2ed 100644
--- a/app/assets/javascripts/issuable/index.js
+++ b/app/assets/javascripts/issuable/index.js
@@ -1,12 +1,25 @@
import { GlToast } from '@gitlab/ui';
import Vue from 'vue';
-import IssuableContext from '~/issuable/issuable_context';
import { parseBoolean } from '~/lib/utils/common_utils';
import Sidebar from '~/right_sidebar';
import { getSidebarOptions } from '~/sidebar/mount_sidebar';
import CsvImportExportButtons from './components/csv_import_export_buttons.vue';
import IssuableByEmail from './components/issuable_by_email.vue';
import IssuableHeaderWarnings from './components/issuable_header_warnings.vue';
+import issuableBulkUpdateActions from './issuable_bulk_update_actions';
+import IssuableBulkUpdateSidebar from './issuable_bulk_update_sidebar';
+import IssuableContext from './issuable_context';
+
+export function initBulkUpdateSidebar(prefixId) {
+ const el = document.querySelector('.issues-bulk-update');
+
+ if (!el) {
+ return;
+ }
+
+ issuableBulkUpdateActions.init({ prefixId });
+ new IssuableBulkUpdateSidebar(); // eslint-disable-line no-new
+}
export function initCsvImportExportButtons() {
const el = document.querySelector('.js-csv-import-export-buttons');
diff --git a/app/assets/javascripts/issuable/bulk_update_sidebar/issuable_bulk_update_actions.js b/app/assets/javascripts/issuable/issuable_bulk_update_actions.js
index 14824820c0d..c386267501a 100644
--- a/app/assets/javascripts/issuable/bulk_update_sidebar/issuable_bulk_update_actions.js
+++ b/app/assets/javascripts/issuable/issuable_bulk_update_actions.js
@@ -1,6 +1,6 @@
import $ from 'jquery';
import { difference, intersection, union } from 'lodash';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import axios from '~/lib/utils/axios_utils';
import { __ } from '~/locale';
@@ -32,7 +32,7 @@ export default {
onFormSubmitFailure() {
this.form.find('[type="submit"]').enable();
- return createFlash({
+ return createAlert({
message: __('Issue update failed'),
});
},
diff --git a/app/assets/javascripts/issuable/bulk_update_sidebar/issuable_bulk_update_sidebar.js b/app/assets/javascripts/issuable/issuable_bulk_update_sidebar.js
index b46a95c7dfa..095da60a583 100644
--- a/app/assets/javascripts/issuable/bulk_update_sidebar/issuable_bulk_update_sidebar.js
+++ b/app/assets/javascripts/issuable/issuable_bulk_update_sidebar.js
@@ -3,7 +3,12 @@
import $ from 'jquery';
import issuableEventHub from '~/issues/list/eventhub';
import LabelsSelect from '~/labels/labels_select';
-import { mountMilestoneDropdown } from '~/sidebar/mount_sidebar';
+import {
+ mountMilestoneDropdown,
+ mountMoveIssuesButton,
+ mountStatusDropdown,
+ mountSubscriptionsDropdown,
+} from '~/sidebar/mount_sidebar';
import IssuableBulkUpdateActions from './issuable_bulk_update_actions';
const HIDDEN_CLASS = 'hidden';
@@ -56,6 +61,9 @@ export default class IssuableBulkUpdateSidebar {
initDropdowns() {
new LabelsSelect();
mountMilestoneDropdown();
+ mountMoveIssuesButton();
+ mountStatusDropdown();
+ mountSubscriptionsDropdown();
// Checking IS_EE and using ee_else_ce is odd, but we do it here to satisfy
// the import/no-unresolved lint rule when FOSS_ONLY=1, even though at
diff --git a/app/assets/javascripts/issuable/issuable_label_selector.js b/app/assets/javascripts/issuable/issuable_label_selector.js
new file mode 100644
index 00000000000..ad8bbf04d6f
--- /dev/null
+++ b/app/assets/javascripts/issuable/issuable_label_selector.js
@@ -0,0 +1,56 @@
+import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+import createDefaultClient from '~/lib/graphql';
+import {
+ DropdownVariant,
+ LabelType,
+} from '~/sidebar/components/labels/labels_select_widget/constants';
+import { WorkspaceType } from '~/issues/constants';
+import IssuableLabelSelector from '~/vue_shared/issuable/create/components/issuable_label_selector.vue';
+
+Vue.use(VueApollo);
+
+const apolloProvider = new VueApollo({
+ defaultClient: createDefaultClient(),
+});
+
+export default () => {
+ const el = document.querySelector('.js-issuable-form-label-selector');
+
+ if (!el) {
+ return false;
+ }
+
+ const {
+ fieldName,
+ fullPath,
+ initialLabels,
+ issuableType,
+ labelsFilterBasePath,
+ labelsManagePath,
+ } = el.dataset;
+
+ return new Vue({
+ el,
+ apolloProvider,
+ provide: {
+ allowLabelCreate: true,
+ allowLabelEdit: true,
+ allowLabelRemove: true,
+ allowScopedLabels: true,
+ attrWorkspacePath: fullPath,
+ fieldName,
+ fullPath,
+ initialLabels: JSON.parse(initialLabels),
+ issuableType,
+ labelType: LabelType.project,
+ labelsFilterBasePath,
+ labelsManagePath,
+ variant: DropdownVariant.Embedded,
+ workspaceType: WorkspaceType.project,
+ },
+ render(createElement) {
+ return createElement(IssuableLabelSelector);
+ },
+ });
+};
diff --git a/app/assets/javascripts/issues/create_merge_request_dropdown.js b/app/assets/javascripts/issues/create_merge_request_dropdown.js
index 92ff7f21eff..977a505437d 100644
--- a/app/assets/javascripts/issues/create_merge_request_dropdown.js
+++ b/app/assets/javascripts/issues/create_merge_request_dropdown.js
@@ -7,7 +7,7 @@ import {
import confidentialMergeRequestState from '~/confidential_merge_request/state';
import DropLab from '~/filtered_search/droplab/drop_lab_deprecated';
import ISetter from '~/filtered_search/droplab/plugins/input_setter';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import axios from '~/lib/utils/axios_utils';
import { __, sprintf } from '~/locale';
import { mergeUrlParams } from '~/lib/utils/url_utility';
@@ -141,7 +141,7 @@ export default class CreateMergeRequestDropdown {
.catch(() => {
this.unavailable();
this.disable();
- createFlash({
+ createAlert({
message: __('Failed to check related branches.'),
});
});
@@ -162,7 +162,7 @@ export default class CreateMergeRequestDropdown {
}
})
.catch(() =>
- createFlash({
+ createAlert({
message: __('Failed to create a branch for this issue. Please try again.'),
}),
);
@@ -293,7 +293,7 @@ export default class CreateMergeRequestDropdown {
}
this.unavailable();
this.disable();
- createFlash({
+ createAlert({
message: __('Failed to get ref.'),
});
diff --git a/app/assets/javascripts/issues/dashboard/components/issues_dashboard_app.vue b/app/assets/javascripts/issues/dashboard/components/issues_dashboard_app.vue
index 29f6aecca03..b9d876ef72f 100644
--- a/app/assets/javascripts/issues/dashboard/components/issues_dashboard_app.vue
+++ b/app/assets/javascripts/issues/dashboard/components/issues_dashboard_app.vue
@@ -1,13 +1,50 @@
<script>
-import { GlButton, GlEmptyState } from '@gitlab/ui';
+import { GlButton, GlEmptyState, GlTooltipDirective } from '@gitlab/ui';
+import * as Sentry from '@sentry/browser';
+import getIssuesQuery from 'ee_else_ce/issues/dashboard/queries/get_issues.query.graphql';
+import IssueCardStatistics from 'ee_else_ce/issues/list/components/issue_card_statistics.vue';
+import IssueCardTimeInfo from 'ee_else_ce/issues/list/components/issue_card_time_info.vue';
+import { IssuableStatus } from '~/issues/constants';
+import {
+ CREATED_DESC,
+ PAGE_SIZE,
+ PARAM_STATE,
+ UPDATED_DESC,
+ urlSortParams,
+} from '~/issues/list/constants';
+import setSortPreferenceMutation from '~/issues/list/queries/set_sort_preference.mutation.graphql';
+import {
+ convertToApiParams,
+ convertToSearchQuery,
+ convertToUrlParams,
+ getFilterTokens,
+ getInitialPageParams,
+ getSortKey,
+ getSortOptions,
+ isSortKey,
+} from '~/issues/list/utils';
+import axios from '~/lib/utils/axios_utils';
+import { scrollUp } from '~/lib/utils/scroll_utils';
+import { getParameterByName } from '~/lib/utils/url_utility';
import { __ } from '~/locale';
+import {
+ TOKEN_TITLE_ASSIGNEE,
+ TOKEN_TITLE_AUTHOR,
+ TOKEN_TYPE_ASSIGNEE,
+ TOKEN_TYPE_AUTHOR,
+} from '~/vue_shared/components/filtered_search_bar/constants';
import IssuableList from '~/vue_shared/issuable/list/components/issuable_list_root.vue';
import { IssuableListTabs, IssuableStates } from '~/vue_shared/issuable/list/constants';
+const UserToken = () => import('~/vue_shared/components/filtered_search_bar/tokens/user_token.vue');
+
export default {
i18n: {
calendarButtonText: __('Subscribe to calendar'),
+ closed: __('CLOSED'),
+ closedMoved: __('CLOSED (MOVED)'),
emptyStateTitle: __('Please select at least one filter to see results'),
+ errorFetchingIssues: __('An error occurred while loading issues'),
rssButtonText: __('Subscribe to RSS feed'),
searchInputPlaceholder: __('Search or filter results...'),
},
@@ -16,29 +53,237 @@ export default {
GlButton,
GlEmptyState,
IssuableList,
+ IssueCardStatistics,
+ IssueCardTimeInfo,
+ },
+ directives: {
+ GlTooltip: GlTooltipDirective,
},
- inject: ['calendarPath', 'emptyStateSvgPath', 'isSignedIn', 'rssPath'],
+ inject: [
+ 'calendarPath',
+ 'emptyStateSvgPath',
+ 'hasBlockedIssuesFeature',
+ 'hasIssuableHealthStatusFeature',
+ 'hasIssueWeightsFeature',
+ 'hasScopedLabelsFeature',
+ 'initialSort',
+ 'isPublicVisibilityRestricted',
+ 'isSignedIn',
+ 'rssPath',
+ ],
data() {
+ const state = getParameterByName(PARAM_STATE);
+
+ const defaultSortKey = state === IssuableStates.Closed ? UPDATED_DESC : CREATED_DESC;
+ const dashboardSortKey = getSortKey(this.initialSort);
+ const graphQLSortKey =
+ isSortKey(this.initialSort?.toUpperCase()) && this.initialSort.toUpperCase();
+
+ // The initial sort is an old enum value when it is saved on the dashboard issues page.
+ // The initial sort is a GraphQL enum value when it is saved on the Vue issues list page.
+ const sortKey = dashboardSortKey || graphQLSortKey || defaultSortKey;
+
return {
+ filterTokens: getFilterTokens(window.location.search),
issues: [],
- searchTokens: [],
- sortOptions: [],
- state: IssuableStates.Opened,
+ issuesError: null,
+ pageInfo: {},
+ pageParams: getInitialPageParams(),
+ sortKey,
+ state: state || IssuableStates.Opened,
};
},
+ apollo: {
+ issues: {
+ query: getIssuesQuery,
+ variables() {
+ return {
+ hideUsers: this.isPublicVisibilityRestricted && !this.isSignedIn,
+ isSignedIn: this.isSignedIn,
+ search: this.searchQuery,
+ sort: this.sortKey,
+ state: this.state,
+ ...this.pageParams,
+ ...this.apiFilterParams,
+ };
+ },
+ update(data) {
+ return data.issues.nodes ?? [];
+ },
+ result({ data }) {
+ this.pageInfo = data?.issues.pageInfo ?? {};
+ },
+ error(error) {
+ this.issuesError = this.$options.i18n.errorFetchingIssues;
+ Sentry.captureException(error);
+ },
+ debounce: 200,
+ },
+ },
+ computed: {
+ apiFilterParams() {
+ return convertToApiParams(this.filterTokens);
+ },
+ searchQuery() {
+ return convertToSearchQuery(this.filterTokens);
+ },
+ searchTokens() {
+ const preloadedUsers = [];
+
+ if (gon.current_user_id) {
+ preloadedUsers.push({
+ id: gon.current_user_id,
+ name: gon.current_user_fullname,
+ username: gon.current_username,
+ avatar_url: gon.current_user_avatar_url,
+ });
+ }
+
+ const tokens = [
+ {
+ type: TOKEN_TYPE_ASSIGNEE,
+ title: TOKEN_TITLE_ASSIGNEE,
+ icon: 'user',
+ token: UserToken,
+ fetchUsers: this.fetchUsers,
+ preloadedUsers,
+ recentSuggestionsStorageKey: 'dashboard-issues-recent-tokens-assignee',
+ },
+ {
+ type: TOKEN_TYPE_AUTHOR,
+ title: TOKEN_TITLE_AUTHOR,
+ icon: 'pencil',
+ token: UserToken,
+ fetchUsers: this.fetchUsers,
+ defaultUsers: [],
+ preloadedUsers,
+ recentSuggestionsStorageKey: 'dashboard-issues-recent-tokens-author',
+ },
+ ];
+
+ return tokens;
+ },
+ showPaginationControls() {
+ return this.issues.length > 0 && (this.pageInfo.hasNextPage || this.pageInfo.hasPreviousPage);
+ },
+ sortOptions() {
+ return getSortOptions({
+ hasBlockedIssuesFeature: this.hasBlockedIssuesFeature,
+ hasIssuableHealthStatusFeature: this.hasIssuableHealthStatusFeature,
+ hasIssueWeightsFeature: this.hasIssueWeightsFeature,
+ });
+ },
+ urlFilterParams() {
+ return convertToUrlParams(this.filterTokens);
+ },
+ urlParams() {
+ return {
+ search: this.searchQuery,
+ sort: urlSortParams[this.sortKey],
+ state: this.state,
+ ...this.urlFilterParams,
+ };
+ },
+ },
+ methods: {
+ fetchUsers(search) {
+ return axios.get('/-/autocomplete/users.json', { params: { active: true, search } });
+ },
+ getStatus(issue) {
+ if (issue.state === IssuableStatus.Closed && issue.moved) {
+ return this.$options.i18n.closedMoved;
+ }
+ if (issue.state === IssuableStatus.Closed) {
+ return this.$options.i18n.closed;
+ }
+ return undefined;
+ },
+ handleClickTab(state) {
+ if (this.state === state) {
+ return;
+ }
+ this.state = state;
+ this.pageParams = getInitialPageParams();
+ },
+ handleDismissAlert() {
+ this.issuesError = null;
+ },
+ handleFilter(tokens) {
+ this.filterTokens = tokens;
+ this.pageParams = getInitialPageParams();
+ },
+ handleNextPage() {
+ this.pageParams = {
+ afterCursor: this.pageInfo.endCursor,
+ firstPageSize: PAGE_SIZE,
+ };
+ scrollUp();
+ },
+ handlePreviousPage() {
+ this.pageParams = {
+ beforeCursor: this.pageInfo.startCursor,
+ lastPageSize: PAGE_SIZE,
+ };
+ scrollUp();
+ },
+ handleSort(sortKey) {
+ if (this.sortKey === sortKey) {
+ return;
+ }
+
+ this.sortKey = sortKey;
+ this.pageParams = getInitialPageParams();
+
+ if (this.isSignedIn) {
+ this.saveSortPreference(sortKey);
+ }
+ },
+ saveSortPreference(sortKey) {
+ this.$apollo
+ .mutate({
+ mutation: setSortPreferenceMutation,
+ variables: { input: { issuesSort: sortKey } },
+ })
+ .then(({ data }) => {
+ if (data.userPreferencesUpdate.errors.length) {
+ throw new Error(data.userPreferencesUpdate.errors);
+ }
+ })
+ .catch((error) => {
+ Sentry.captureException(error);
+ });
+ },
+ },
};
</script>
<template>
<issuable-list
+ :current-tab="state"
+ :error="issuesError"
+ :has-next-page="pageInfo.hasNextPage"
+ :has-previous-page="pageInfo.hasPreviousPage"
+ :has-scoped-labels-feature="hasScopedLabelsFeature"
+ :initial-filter-value="filterTokens"
+ :initial-sort-by="sortKey"
+ :issuables="issues"
+ :issuables-loading="$apollo.queries.issues.loading"
namespace="dashboard"
recent-searches-storage-key="issues"
:search-input-placeholder="$options.i18n.searchInputPlaceholder"
:search-tokens="searchTokens"
+ :show-pagination-controls="showPaginationControls"
+ show-work-item-type-icon
:sort-options="sortOptions"
- :issuables="issues"
:tabs="$options.IssuableListTabs"
- :current-tab="state"
+ :url-params="urlParams"
+ use-keyset-pagination
+ @click-tab="handleClickTab"
+ @dismiss-alert="handleDismissAlert"
+ @filter="handleFilter"
+ @next-page="handleNextPage"
+ @previous-page="handlePreviousPage"
+ @sort="handleSort"
>
<template #nav-actions>
<gl-button :href="rssPath" icon="rss">
@@ -49,6 +294,18 @@ export default {
</gl-button>
</template>
+ <template #timeframe="{ issuable = {} }">
+ <issue-card-time-info :issue="issuable" />
+ </template>
+
+ <template #status="{ issuable = {} }">
+ {{ getStatus(issuable) }}
+ </template>
+
+ <template #statistics="{ issuable = {} }">
+ <issue-card-statistics :issue="issuable" />
+ </template>
+
<template #empty-state>
<gl-empty-state :svg-path="emptyStateSvgPath" :title="$options.i18n.emptyStateTitle" />
</template>
diff --git a/app/assets/javascripts/issues/dashboard/index.js b/app/assets/javascripts/issues/dashboard/index.js
index a1ae3b93f7d..e3e5cc614cb 100644
--- a/app/assets/javascripts/issues/dashboard/index.js
+++ b/app/assets/javascripts/issues/dashboard/index.js
@@ -1,4 +1,6 @@
import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+import createDefaultClient from '~/lib/graphql';
import { parseBoolean } from '~/lib/utils/common_utils';
import IssuesDashboardApp from './components/issues_dashboard_app.vue';
@@ -9,14 +11,36 @@ export function mountIssuesDashboardApp() {
return null;
}
- const { calendarPath, emptyStateSvgPath, isSignedIn, rssPath } = el.dataset;
+ Vue.use(VueApollo);
+
+ const {
+ calendarPath,
+ emptyStateSvgPath,
+ hasBlockedIssuesFeature,
+ hasIssuableHealthStatusFeature,
+ hasIssueWeightsFeature,
+ hasScopedLabelsFeature,
+ initialSort,
+ isPublicVisibilityRestricted,
+ isSignedIn,
+ rssPath,
+ } = el.dataset;
return new Vue({
el,
name: 'IssuesDashboardRoot',
+ apolloProvider: new VueApollo({
+ defaultClient: createDefaultClient(),
+ }),
provide: {
calendarPath,
emptyStateSvgPath,
+ hasBlockedIssuesFeature: parseBoolean(hasBlockedIssuesFeature),
+ hasIssuableHealthStatusFeature: parseBoolean(hasIssuableHealthStatusFeature),
+ hasIssueWeightsFeature: parseBoolean(hasIssueWeightsFeature),
+ hasScopedLabelsFeature: parseBoolean(hasScopedLabelsFeature),
+ initialSort,
+ isPublicVisibilityRestricted: parseBoolean(isPublicVisibilityRestricted),
isSignedIn: parseBoolean(isSignedIn),
rssPath,
},
diff --git a/app/assets/javascripts/issues/dashboard/queries/get_issues.query.graphql b/app/assets/javascripts/issues/dashboard/queries/get_issues.query.graphql
new file mode 100644
index 00000000000..8ffcb456755
--- /dev/null
+++ b/app/assets/javascripts/issues/dashboard/queries/get_issues.query.graphql
@@ -0,0 +1,36 @@
+#import "~/graphql_shared/fragments/page_info.fragment.graphql"
+#import "~/issues/list/queries/issue.fragment.graphql"
+
+query getDashboardIssues(
+ $hideUsers: Boolean = false
+ $isSignedIn: Boolean = false
+ $search: String
+ $sort: IssueSort
+ $state: IssuableState
+ $assigneeUsernames: [String!]
+ $authorUsername: String
+ $afterCursor: String
+ $beforeCursor: String
+ $firstPageSize: Int
+ $lastPageSize: Int
+) {
+ issues(
+ search: $search
+ sort: $sort
+ state: $state
+ assigneeUsernames: $assigneeUsernames
+ authorUsername: $authorUsername
+ after: $afterCursor
+ before: $beforeCursor
+ first: $firstPageSize
+ last: $lastPageSize
+ ) {
+ nodes {
+ ...IssueFragment
+ reference(full: true)
+ }
+ pageInfo {
+ ...PageInfo
+ }
+ }
+}
diff --git a/app/assets/javascripts/issues/index.js b/app/assets/javascripts/issues/index.js
index a785790169d..e3716d0e111 100644
--- a/app/assets/javascripts/issues/index.js
+++ b/app/assets/javascripts/issues/index.js
@@ -1,5 +1,6 @@
import $ from 'jquery';
import IssuableForm from 'ee_else_ce/issuable/issuable_form';
+import IssuableLabelSelector from '~/issuable/issuable_label_selector';
import ShortcutsIssuable from '~/behaviors/shortcuts/shortcuts_issuable';
import ShortcutsNavigation from '~/behaviors/shortcuts/shortcuts_navigation';
import GLForm from '~/gl_form';
@@ -39,6 +40,7 @@ export function initFilteredSearchServiceDesk() {
export function initForm() {
new GLForm($('.issue-form')); // eslint-disable-line no-new
new IssuableForm($('.issue-form')); // eslint-disable-line no-new
+ IssuableLabelSelector();
new IssuableTemplateSelectors({ warnTemplateOverride: true }); // eslint-disable-line no-new
new LabelsSelect(); // eslint-disable-line no-new
new ShortcutsNavigation(); // eslint-disable-line no-new
diff --git a/app/assets/javascripts/issues/issue.js b/app/assets/javascripts/issues/issue.js
index a9321cf200d..de1c689e590 100644
--- a/app/assets/javascripts/issues/issue.js
+++ b/app/assets/javascripts/issues/issue.js
@@ -1,6 +1,6 @@
import $ from 'jquery';
import { joinPaths } from '~/lib/utils/url_utility';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import { EVENT_ISSUABLE_VUE_APP_CHANGE } from '~/issuable/constants';
import axios from '~/lib/utils/axios_utils';
import { addDelimiter } from '~/lib/utils/text_utility';
@@ -68,7 +68,7 @@ export default class Issue {
this.createMergeRequestDropdown.checkAbilityToCreateBranch();
}
} else {
- createFlash({
+ createAlert({
message: issueFailMessage,
});
}
@@ -105,7 +105,7 @@ export default class Issue {
}
})
.catch(() =>
- createFlash({
+ createAlert({
message: __('Failed to load related branches'),
}),
);
diff --git a/app/assets/javascripts/issues/list/components/empty_state_with_any_issues.vue b/app/assets/javascripts/issues/list/components/empty_state_with_any_issues.vue
new file mode 100644
index 00000000000..8aece24de0c
--- /dev/null
+++ b/app/assets/javascripts/issues/list/components/empty_state_with_any_issues.vue
@@ -0,0 +1,53 @@
+<script>
+import { GlButton, GlEmptyState } from '@gitlab/ui';
+import { i18n } from '../constants';
+
+export default {
+ i18n,
+ components: {
+ GlButton,
+ GlEmptyState,
+ },
+ inject: ['emptyStateSvgPath', 'newIssuePath', 'showNewIssueLink'],
+ props: {
+ hasSearch: {
+ type: Boolean,
+ required: true,
+ },
+ isOpenTab: {
+ type: Boolean,
+ required: true,
+ },
+ },
+};
+</script>
+
+<template>
+ <gl-empty-state
+ v-if="hasSearch"
+ :description="$options.i18n.noSearchResultsDescription"
+ :title="$options.i18n.noSearchResultsTitle"
+ :svg-path="emptyStateSvgPath"
+ >
+ <template #actions>
+ <gl-button v-if="showNewIssueLink" :href="newIssuePath" variant="confirm">
+ {{ $options.i18n.newIssueLabel }}
+ </gl-button>
+ </template>
+ </gl-empty-state>
+
+ <gl-empty-state
+ v-else-if="isOpenTab"
+ :description="$options.i18n.noOpenIssuesDescription"
+ :title="$options.i18n.noOpenIssuesTitle"
+ :svg-path="emptyStateSvgPath"
+ >
+ <template #actions>
+ <gl-button v-if="showNewIssueLink" :href="newIssuePath" variant="confirm">
+ {{ $options.i18n.newIssueLabel }}
+ </gl-button>
+ </template>
+ </gl-empty-state>
+
+ <gl-empty-state v-else :title="$options.i18n.noClosedIssuesTitle" :svg-path="emptyStateSvgPath" />
+</template>
diff --git a/app/assets/javascripts/issues/list/components/empty_state_without_any_issues.vue b/app/assets/javascripts/issues/list/components/empty_state_without_any_issues.vue
new file mode 100644
index 00000000000..5a37751410a
--- /dev/null
+++ b/app/assets/javascripts/issues/list/components/empty_state_without_any_issues.vue
@@ -0,0 +1,110 @@
+<script>
+import { GlButton, GlEmptyState, GlLink, GlSprintf } from '@gitlab/ui';
+import { helpPagePath } from '~/helpers/help_page_helper';
+import CsvImportExportButtons from '~/issuable/components/csv_import_export_buttons.vue';
+import { i18n } from '../constants';
+import NewIssueDropdown from './new_issue_dropdown.vue';
+
+export default {
+ i18n,
+ issuesHelpPagePath: helpPagePath('user/project/issues/index'),
+ components: {
+ CsvImportExportButtons,
+ GlButton,
+ GlEmptyState,
+ GlLink,
+ GlSprintf,
+ NewIssueDropdown,
+ },
+ inject: [
+ 'canCreateProjects',
+ 'emptyStateSvgPath',
+ 'isSignedIn',
+ 'jiraIntegrationPath',
+ 'newIssuePath',
+ 'newProjectPath',
+ 'showNewIssueLink',
+ 'signInPath',
+ ],
+ props: {
+ currentTabCount: {
+ type: Number,
+ required: false,
+ default: undefined,
+ },
+ exportCsvPathWithQuery: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ showCsvButtons: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ showNewIssueDropdown: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ },
+};
+</script>
+
+<template>
+ <div v-if="isSignedIn">
+ <gl-empty-state :title="$options.i18n.noIssuesTitle" :svg-path="emptyStateSvgPath">
+ <template #description>
+ <gl-link :href="$options.issuesHelpPagePath">
+ {{ $options.i18n.noIssuesDescription }}
+ </gl-link>
+ <p v-if="canCreateProjects">
+ <strong>{{ $options.i18n.noGroupIssuesSignedInDescription }}</strong>
+ </p>
+ </template>
+ <template #actions>
+ <gl-button v-if="canCreateProjects" :href="newProjectPath" variant="confirm">
+ {{ $options.i18n.newProjectLabel }}
+ </gl-button>
+ <gl-button v-if="showNewIssueLink" :href="newIssuePath" variant="confirm">
+ {{ $options.i18n.newIssueLabel }}
+ </gl-button>
+ <csv-import-export-buttons
+ v-if="showCsvButtons"
+ class="gl-w-full gl-sm-w-auto gl-sm-mr-3"
+ :export-csv-path="exportCsvPathWithQuery"
+ :issuable-count="currentTabCount"
+ />
+ <new-issue-dropdown v-if="showNewIssueDropdown" class="gl-align-self-center" />
+ </template>
+ </gl-empty-state>
+ <hr />
+ <p class="gl-text-center gl-font-weight-bold gl-mb-0">
+ {{ $options.i18n.jiraIntegrationTitle }}
+ </p>
+ <p class="gl-text-center gl-mb-0">
+ <gl-sprintf :message="$options.i18n.jiraIntegrationMessage">
+ <template #jiraDocsLink="{ content }">
+ <gl-link :href="jiraIntegrationPath">{{ content }}</gl-link>
+ </template>
+ </gl-sprintf>
+ </p>
+ <p class="gl-text-center gl-text-secondary">
+ {{ $options.i18n.jiraIntegrationSecondaryMessage }}
+ </p>
+ </div>
+
+ <gl-empty-state
+ v-else
+ :title="$options.i18n.noIssuesTitle"
+ :svg-path="emptyStateSvgPath"
+ :primary-button-text="$options.i18n.noIssuesSignedOutButtonText"
+ :primary-button-link="signInPath"
+ >
+ <template #description>
+ <gl-link :href="$options.issuesHelpPagePath">
+ {{ $options.i18n.noIssuesDescription }}
+ </gl-link>
+ </template>
+ </gl-empty-state>
+</template>
diff --git a/app/assets/javascripts/issues/list/components/issue_card_statistics.vue b/app/assets/javascripts/issues/list/components/issue_card_statistics.vue
new file mode 100644
index 00000000000..2d00c3e549d
--- /dev/null
+++ b/app/assets/javascripts/issues/list/components/issue_card_statistics.vue
@@ -0,0 +1,56 @@
+<script>
+import { GlIcon, GlTooltipDirective } from '@gitlab/ui';
+import { i18n } from '../constants';
+
+export default {
+ i18n,
+ components: {
+ GlIcon,
+ },
+ directives: {
+ GlTooltip: GlTooltipDirective,
+ },
+ props: {
+ issue: {
+ type: Object,
+ required: true,
+ },
+ },
+};
+</script>
+
+<template>
+ <ul class="gl-display-contents">
+ <li
+ v-if="issue.mergeRequestsCount"
+ v-gl-tooltip
+ class="gl-display-none gl-sm-display-block gl-mr-3"
+ :title="$options.i18n.relatedMergeRequests"
+ data-testid="merge-requests"
+ >
+ <gl-icon name="merge-request" />
+ {{ issue.mergeRequestsCount }}
+ </li>
+ <li
+ v-if="issue.upvotes"
+ v-gl-tooltip
+ class="gl-display-none gl-sm-display-block gl-mr-3"
+ :title="$options.i18n.upvotes"
+ data-testid="issuable-upvotes"
+ >
+ <gl-icon name="thumb-up" />
+ {{ issue.upvotes }}
+ </li>
+ <li
+ v-if="issue.downvotes"
+ v-gl-tooltip
+ class="gl-display-none gl-sm-display-block gl-mr-3"
+ :title="$options.i18n.downvotes"
+ data-testid="issuable-downvotes"
+ >
+ <gl-icon name="thumb-down" />
+ {{ issue.downvotes }}
+ </li>
+ <slot></slot>
+ </ul>
+</template>
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 64de4b1947b..12a83f06453 100644
--- a/app/assets/javascripts/issues/list/components/issues_list_app.vue
+++ b/app/assets/javascripts/issues/list/components/issues_list_app.vue
@@ -1,19 +1,12 @@
<script>
-import {
- GlButton,
- GlEmptyState,
- GlFilteredSearchToken,
- GlIcon,
- GlLink,
- GlSprintf,
- GlTooltipDirective,
-} from '@gitlab/ui';
+import { GlButton, GlFilteredSearchToken, GlTooltipDirective } from '@gitlab/ui';
import * as Sentry from '@sentry/browser';
import fuzzaldrinPlus from 'fuzzaldrin-plus';
+import IssueCardStatistics from 'ee_else_ce/issues/list/components/issue_card_statistics.vue';
import IssueCardTimeInfo from 'ee_else_ce/issues/list/components/issue_card_time_info.vue';
import 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, { FLASH_TYPES } from '~/flash';
+import { createAlert, VARIANT_INFO } from '~/flash';
import { TYPE_USER } from '~/graphql_shared/constants';
import { convertToGraphQLId, getIdFromGraphQLId } from '~/graphql_shared/utils';
import { ITEM_TYPE } from '~/groups/constants';
@@ -24,11 +17,11 @@ import axios from '~/lib/utils/axios_utils';
import { isPositiveInteger } from '~/lib/utils/number_utils';
import { scrollUp } from '~/lib/utils/scroll_utils';
import { getParameterByName, joinPaths } from '~/lib/utils/url_utility';
-import { helpPagePath } from '~/helpers/help_page_helper';
import {
- DEFAULT_NONE_ANY,
FILTERED_SEARCH_TERM,
- OPERATOR_IS_ONLY,
+ OPERATORS_IS,
+ OPERATORS_IS_NOT,
+ OPERATORS_IS_NOT_OR,
TOKEN_TITLE_ASSIGNEE,
TOKEN_TITLE_AUTHOR,
TOKEN_TITLE_CONFIDENTIAL,
@@ -38,9 +31,8 @@ import {
TOKEN_TITLE_MY_REACTION,
TOKEN_TITLE_ORGANIZATION,
TOKEN_TITLE_RELEASE,
+ TOKEN_TITLE_SEARCH_WITHIN,
TOKEN_TITLE_TYPE,
- OPERATOR_IS_NOT_OR,
- OPERATOR_IS_AND_IS_NOT,
TOKEN_TYPE_ASSIGNEE,
TOKEN_TYPE_AUTHOR,
TOKEN_TYPE_CONFIDENTIAL,
@@ -50,6 +42,7 @@ import {
TOKEN_TYPE_MY_REACTION,
TOKEN_TYPE_ORGANIZATION,
TOKEN_TYPE_RELEASE,
+ TOKEN_TYPE_SEARCH_WITHIN,
TOKEN_TYPE_TYPE,
} from '~/vue_shared/components/filtered_search_bar/constants';
import IssuableList from '~/vue_shared/issuable/list/components/issuable_list_root.vue';
@@ -70,11 +63,9 @@ import {
PARAM_SORT,
PARAM_STATE,
RELATIVE_POSITION_ASC,
- TYPE_TOKEN_TASK_OPTION,
UPDATED_DESC,
urlSortParams,
} from '../constants';
-
import eventHub from '../eventhub';
import reorderIssuesMutation from '../queries/reorder_issues.mutation.graphql';
import searchLabelsQuery from '../queries/search_labels.query.graphql';
@@ -91,10 +82,11 @@ import {
getSortOptions,
isSortKey,
} from '../utils';
+import EmptyStateWithAnyIssues from './empty_state_with_any_issues.vue';
+import EmptyStateWithoutAnyIssues from './empty_state_without_any_issues.vue';
import NewIssueDropdown from './new_issue_dropdown.vue';
-const AuthorToken = () =>
- import('~/vue_shared/components/filtered_search_bar/tokens/author_token.vue');
+const UserToken = () => import('~/vue_shared/components/filtered_search_bar/tokens/user_token.vue');
const EmojiToken = () =>
import('~/vue_shared/components/filtered_search_bar/tokens/emoji_token.vue');
const LabelToken = () =>
@@ -113,13 +105,12 @@ export default {
IssuableListTabs,
components: {
CsvImportExportButtons,
+ EmptyStateWithAnyIssues,
+ EmptyStateWithoutAnyIssues,
GlButton,
- GlEmptyState,
- GlIcon,
- GlLink,
- GlSprintf,
IssuableByEmail,
IssuableList,
+ IssueCardStatistics,
IssueCardTimeInfo,
NewIssueDropdown,
},
@@ -131,15 +122,14 @@ export default {
'autocompleteAwardEmojisPath',
'calendarPath',
'canBulkUpdate',
- 'canCreateProjects',
'canReadCrmContact',
'canReadCrmOrganization',
- 'emptyStateSvgPath',
'exportCsvPath',
'fullPath',
'hasAnyIssues',
'hasAnyProjects',
'hasBlockedIssuesFeature',
+ 'hasIssuableHealthStatusFeature',
'hasIssueWeightsFeature',
'hasScopedLabelsFeature',
'initialEmail',
@@ -149,13 +139,10 @@ export default {
'isProject',
'isPublicVisibilityRestricted',
'isSignedIn',
- 'jiraIntegrationPath',
'newIssuePath',
- 'newProjectPath',
'releasesPath',
'rssPath',
'showNewIssueLink',
- 'signInPath',
],
props: {
eeSearchTokens: {
@@ -163,6 +150,21 @@ export default {
required: false,
default: () => [],
},
+ eeTypeTokenOptions: {
+ type: Array,
+ required: false,
+ default: () => [],
+ },
+ eeWorkItemTypes: {
+ type: Array,
+ required: false,
+ default: () => [],
+ },
+ eeIsOkrsEnabled: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
},
data() {
return {
@@ -189,10 +191,7 @@ export default {
return data[this.namespace]?.issues.nodes ?? [];
},
result({ data }) {
- if (!data) {
- return;
- }
- this.pageInfo = data[this.namespace]?.issues.pageInfo ?? {};
+ this.pageInfo = data?.[this.namespace]?.issues.pageInfo ?? {};
this.exportCsvPathWithQuery = this.getExportCsvPathWithQuery();
},
error(error) {
@@ -239,24 +238,27 @@ export default {
state: this.state,
...this.pageParams,
...this.apiFilterParams,
- types: this.apiFilterParams.types || defaultWorkItemTypes,
+ types: this.apiFilterParams.types || this.defaultWorkItemTypes,
};
},
namespace() {
return this.isProject ? ITEM_TYPE.PROJECT : ITEM_TYPE.GROUP;
},
+ defaultWorkItemTypes() {
+ return [...defaultWorkItemTypes, ...this.eeWorkItemTypes];
+ },
typeTokenOptions() {
- return defaultTypeTokenOptions.concat(TYPE_TOKEN_TASK_OPTION);
+ return [...defaultTypeTokenOptions, ...this.eeTypeTokenOptions];
},
hasOrFeature() {
return this.glFeatures.orIssuableQueries;
},
hasSearch() {
- return (
+ return Boolean(
this.searchQuery ||
- Object.keys(this.urlFilterParams).length ||
- this.pageParams.afterCursor ||
- this.pageParams.beforeCursor
+ Object.keys(this.urlFilterParams).length ||
+ this.pageParams.afterCursor ||
+ this.pageParams.beforeCursor,
);
},
isBulkEditButtonDisabled() {
@@ -284,13 +286,13 @@ export default {
return convertToUrlParams(this.filterTokens);
},
searchQuery() {
- return convertToSearchQuery(this.filterTokens) || undefined;
+ return convertToSearchQuery(this.filterTokens);
},
searchTokens() {
- const preloadedAuthors = [];
+ const preloadedUsers = [];
if (gon.current_user_id) {
- preloadedAuthors.push({
+ preloadedUsers.push({
id: convertToGraphQLId(TYPE_USER, gon.current_user_id),
name: gon.current_user_fullname,
username: gon.current_username,
@@ -300,28 +302,41 @@ export default {
const tokens = [
{
+ type: TOKEN_TYPE_SEARCH_WITHIN,
+ title: TOKEN_TITLE_SEARCH_WITHIN,
+ icon: 'search',
+ token: GlFilteredSearchToken,
+ unique: true,
+ operators: OPERATORS_IS,
+ options: [
+ { icon: 'title', value: 'TITLE', title: this.$options.i18n.titles },
+ {
+ icon: 'text-description',
+ value: 'DESCRIPTION',
+ title: this.$options.i18n.descriptions,
+ },
+ ],
+ },
+ {
type: TOKEN_TYPE_AUTHOR,
title: TOKEN_TITLE_AUTHOR,
icon: 'pencil',
- token: AuthorToken,
- dataType: 'user',
- unique: true,
- defaultAuthors: [],
- fetchAuthors: this.fetchUsers,
+ token: UserToken,
+ defaultUsers: [],
+ operators: this.hasOrFeature ? OPERATORS_IS_NOT_OR : OPERATORS_IS_NOT,
+ fetchUsers: this.fetchUsers,
recentSuggestionsStorageKey: `${this.fullPath}-issues-recent-tokens-author`,
- preloadedAuthors,
+ preloadedUsers,
},
{
type: TOKEN_TYPE_ASSIGNEE,
title: TOKEN_TITLE_ASSIGNEE,
icon: 'user',
- token: AuthorToken,
- dataType: 'user',
- defaultAuthors: DEFAULT_NONE_ANY,
- operators: this.hasOrFeature ? OPERATOR_IS_NOT_OR : OPERATOR_IS_AND_IS_NOT,
- fetchAuthors: this.fetchUsers,
+ token: UserToken,
+ operators: this.hasOrFeature ? OPERATORS_IS_NOT_OR : OPERATORS_IS_NOT,
+ fetchUsers: this.fetchUsers,
recentSuggestionsStorageKey: `${this.fullPath}-issues-recent-tokens-assignee`,
- preloadedAuthors,
+ preloadedUsers,
},
{
type: TOKEN_TYPE_MILESTONE,
@@ -337,7 +352,6 @@ export default {
title: TOKEN_TITLE_LABEL,
icon: 'labels',
token: LabelToken,
- defaultLabels: DEFAULT_NONE_ANY,
fetchLabels: this.fetchLabels,
recentSuggestionsStorageKey: `${this.fullPath}-issues-recent-tokens-label`,
},
@@ -378,7 +392,7 @@ export default {
icon: 'eye-slash',
token: GlFilteredSearchToken,
unique: true,
- operators: OPERATOR_IS_ONLY,
+ operators: OPERATORS_IS,
options: [
{ icon: 'eye-slash', value: 'yes', title: this.$options.i18n.confidentialYes },
{ icon: 'eye', value: 'no', title: this.$options.i18n.confidentialNo },
@@ -394,9 +408,8 @@ export default {
token: CrmContactToken,
fullPath: this.fullPath,
isProject: this.isProject,
- defaultContacts: DEFAULT_NONE_ANY,
recentSuggestionsStorageKey: `${this.fullPath}-issues-recent-tokens-crm-contacts`,
- operators: OPERATOR_IS_ONLY,
+ operators: OPERATORS_IS,
unique: true,
});
}
@@ -409,9 +422,8 @@ export default {
token: CrmOrganizationToken,
fullPath: this.fullPath,
isProject: this.isProject,
- defaultOrganizations: DEFAULT_NONE_ANY,
recentSuggestionsStorageKey: `${this.fullPath}-issues-recent-tokens-crm-organizations`,
- operators: OPERATOR_IS_ONLY,
+ operators: OPERATORS_IS,
unique: true,
});
}
@@ -428,11 +440,14 @@ export default {
return this.issues.length > 0 && (this.pageInfo.hasNextPage || this.pageInfo.hasPreviousPage);
},
showPageSizeControls() {
- /** only show page size controls when the tab count is greater than the default/minimum page size control i.e 20 in this case */
return this.currentTabCount > PAGE_SIZE;
},
sortOptions() {
- return getSortOptions(this.hasIssueWeightsFeature, this.hasBlockedIssuesFeature);
+ return getSortOptions({
+ hasBlockedIssuesFeature: this.hasBlockedIssuesFeature,
+ hasIssuableHealthStatusFeature: this.hasIssuableHealthStatusFeature,
+ hasIssueWeightsFeature: this.hasIssueWeightsFeature,
+ });
},
tabCounts() {
const { openedIssues, closedIssues, allIssues } = this.issuesCounts;
@@ -457,10 +472,7 @@ export default {
page_before: this.pageParams.beforeCursor ?? undefined,
};
},
- issuesHelpPagePath() {
- return helpPagePath('user/project/issues/index');
- },
- shouldDisableSomeFilters() {
+ shouldDisableTextSearch() {
return this.isAnonymousSearchDisabled && !this.isSignedIn;
},
},
@@ -482,18 +494,17 @@ export default {
eventHub.$off('issuables:toggleBulkEdit', this.toggleBulkEditSidebar);
},
methods: {
- fetchWithCache(path, cacheName, searchKey, search, wrapData = false) {
+ fetchWithCache(path, cacheName, searchKey, search) {
if (this.cache[cacheName]) {
const data = search
? fuzzaldrinPlus.filter(this.cache[cacheName], search, { key: searchKey })
: this.cache[cacheName].slice(0, MAX_LIST_SIZE);
- return wrapData ? Promise.resolve({ data }) : Promise.resolve(data);
+ return Promise.resolve(data);
}
return axios.get(path).then(({ data }) => {
this.cache[cacheName] = data;
- const result = data.slice(0, MAX_LIST_SIZE);
- return wrapData ? { data: result } : result;
+ return data.slice(0, MAX_LIST_SIZE);
});
},
fetchEmojis(search) {
@@ -554,14 +565,10 @@ export default {
},
async handleBulkUpdateClick() {
if (!this.hasInitBulkEdit) {
- const bulkUpdateSidebar = await import('~/issuable/bulk_update_sidebar');
+ const bulkUpdateSidebar = await import('~/issuable');
bulkUpdateSidebar.initBulkUpdateSidebar('issuable_');
- bulkUpdateSidebar.initStatusDropdown();
- bulkUpdateSidebar.initSubscriptionsDropdown();
- bulkUpdateSidebar.initMoveIssuesButton();
- const usersSelect = await import('~/users_select');
- const UsersSelect = usersSelect.default;
+ const UsersSelect = (await import('~/users_select')).default;
new UsersSelect(); // eslint-disable-line no-new
this.hasInitBulkEdit = true;
@@ -570,19 +577,20 @@ export default {
eventHub.$emit('issuables:enableBulkEdit');
},
handleClickTab(state) {
- if (this.state !== state) {
- this.pageParams = getInitialPageParams(this.pageSize);
+ if (this.state === state) {
+ return;
}
+
this.state = state;
+ this.pageParams = getInitialPageParams(this.pageSize);
this.$router.push({ query: this.urlParams });
},
handleDismissAlert() {
this.issuesError = null;
},
- handleFilter(filter) {
- this.setFilterTokens(filter);
-
+ handleFilter(tokens) {
+ this.setFilterTokens(tokens);
this.pageParams = getInitialPageParams(this.pageSize);
this.$router.push({ query: this.urlParams });
@@ -642,15 +650,17 @@ export default {
});
},
handleSort(sortKey) {
+ if (this.sortKey === sortKey) {
+ return;
+ }
+
if (this.isIssueRepositioningDisabled && sortKey === RELATIVE_POSITION_ASC) {
this.showIssueRepositioningMessage();
return;
}
- if (this.sortKey !== sortKey) {
- this.pageParams = getInitialPageParams(this.pageSize);
- }
this.sortKey = sortKey;
+ this.pageParams = getInitialPageParams(this.pageSize);
if (this.isSignedIn) {
this.saveSortPreference(sortKey);
@@ -673,49 +683,36 @@ export default {
Sentry.captureException(error);
});
},
- setFilterTokens(filtersArg) {
- const filters = this.removeDisabledSearchTerms(filtersArg);
+ setFilterTokens(tokens) {
+ this.filterTokens = this.removeDisabledSearchTerms(tokens);
- this.filterTokens = filters;
-
- // If we filtered something out, let's show a warning message
- if (filters.length < filtersArg.length) {
+ if (this.filterTokens.length < tokens.length) {
this.showAnonymousSearchingMessage();
}
},
removeDisabledSearchTerms(filters) {
- // If we shouldn't disable anything, let's return the same thing
- if (!this.shouldDisableSomeFilters) {
- return filters;
- }
-
- const filtersWithoutSearchTerms = filters.filter(
- (token) => !(token.type === FILTERED_SEARCH_TERM && token.value?.data),
- );
-
- return filtersWithoutSearchTerms;
+ return this.shouldDisableTextSearch
+ ? filters.filter((token) => !(token.type === FILTERED_SEARCH_TERM && token.value?.data))
+ : filters;
},
showAnonymousSearchingMessage() {
- createFlash({
+ createAlert({
message: this.$options.i18n.anonymousSearchingMessage,
- type: FLASH_TYPES.NOTICE,
+ variant: VARIANT_INFO,
});
},
showIssueRepositioningMessage() {
- createFlash({
+ createAlert({
message: this.$options.i18n.issueRepositioningMessage,
- type: FLASH_TYPES.NOTICE,
+ variant: VARIANT_INFO,
});
},
toggleBulkEditSidebar(showBulkEditSidebar) {
this.showBulkEditSidebar = showBulkEditSidebar;
},
handlePageSizeChange(newPageSize) {
- /** make sure the page number is preserved so that the current context is not lost* */
- const lastPageSize = getParameterByName(PARAM_LAST_PAGE_SIZE);
- const pageNumberSize = lastPageSize ? 'lastPageSize' : 'firstPageSize';
- /** depending upon what page or page size we are dynamically set pageParams * */
- this.pageParams[pageNumberSize] = newPageSize;
+ const pageParam = getParameterByName(PARAM_LAST_PAGE_SIZE) ? 'lastPageSize' : 'firstPageSize';
+ this.pageParams[pageParam] = newPageSize;
this.pageSize = newPageSize;
scrollUp();
@@ -724,16 +721,14 @@ export default {
updateData(sortValue) {
const firstPageSize = getParameterByName(PARAM_FIRST_PAGE_SIZE);
const lastPageSize = getParameterByName(PARAM_LAST_PAGE_SIZE);
- const pageAfter = getParameterByName(PARAM_PAGE_AFTER);
- const pageBefore = getParameterByName(PARAM_PAGE_BEFORE);
const state = getParameterByName(PARAM_STATE);
const defaultSortKey = state === IssuableStates.Closed ? UPDATED_DESC : CREATED_DESC;
const dashboardSortKey = getSortKey(sortValue);
const graphQLSortKey = isSortKey(sortValue?.toUpperCase()) && sortValue.toUpperCase();
- // The initial sort is an old enum value when it is saved on the dashboard issues page.
- // The initial sort is a GraphQL enum value when it is saved on the Vue issues list page.
+ // The initial sort is an old enum value when it is saved on the Haml dashboard issues page.
+ // The initial sort is a GraphQL enum value when it is saved on the Vue group/project issues page.
let sortKey = dashboardSortKey || graphQLSortKey || defaultSortKey;
if (this.isIssueRepositioningDisabled && sortKey === RELATIVE_POSITION_ASC) {
@@ -741,15 +736,15 @@ export default {
sortKey = defaultSortKey;
}
- this.exportCsvPathWithQuery = this.getExportCsvPathWithQuery();
this.setFilterTokens(getFilterTokens(window.location.search));
+ this.exportCsvPathWithQuery = this.getExportCsvPathWithQuery();
this.pageParams = getInitialPageParams(
this.pageSize,
isPositiveInteger(firstPageSize) ? parseInt(firstPageSize, 10) : undefined,
isPositiveInteger(lastPageSize) ? parseInt(lastPageSize, 10) : undefined,
- pageAfter,
- pageBefore,
+ getParameterByName(PARAM_PAGE_AFTER),
+ getParameterByName(PARAM_PAGE_BEFORE),
);
this.sortKey = sortKey;
this.state = state || IssuableStates.Opened;
@@ -827,9 +822,14 @@ export default {
>
{{ $options.i18n.editIssues }}
</gl-button>
- <gl-button v-if="showNewIssueLink" :href="newIssuePath" variant="confirm">
+ <gl-button
+ v-if="showNewIssueLink && !eeIsOkrsEnabled"
+ :href="newIssuePath"
+ variant="confirm"
+ >
{{ $options.i18n.newIssueLabel }}
</gl-button>
+ <slot name="new-objective-button"></slot>
<new-issue-dropdown v-if="showNewIssueDropdown" />
</template>
@@ -842,129 +842,25 @@ export default {
</template>
<template #statistics="{ issuable = {} }">
- <li
- v-if="issuable.mergeRequestsCount"
- v-gl-tooltip
- class="gl-display-none gl-sm-display-block"
- :title="$options.i18n.relatedMergeRequests"
- data-testid="merge-requests"
- >
- <gl-icon name="merge-request" />
- {{ issuable.mergeRequestsCount }}
- </li>
- <li
- v-if="issuable.upvotes"
- v-gl-tooltip
- class="issuable-upvotes gl-display-none gl-sm-display-block"
- :title="$options.i18n.upvotes"
- data-testid="issuable-upvotes"
- >
- <gl-icon name="thumb-up" />
- {{ issuable.upvotes }}
- </li>
- <li
- v-if="issuable.downvotes"
- v-gl-tooltip
- class="issuable-downvotes gl-display-none gl-sm-display-block"
- :title="$options.i18n.downvotes"
- data-testid="issuable-downvotes"
- >
- <gl-icon name="thumb-down" />
- {{ issuable.downvotes }}
- </li>
- <slot :issuable="issuable"></slot>
+ <issue-card-statistics :issue="issuable" />
</template>
<template #empty-state>
- <gl-empty-state
- v-if="hasSearch"
- :description="$options.i18n.noSearchResultsDescription"
- :title="$options.i18n.noSearchResultsTitle"
- :svg-path="emptyStateSvgPath"
- >
- <template #actions>
- <gl-button v-if="showNewIssueLink" :href="newIssuePath" variant="confirm">
- {{ $options.i18n.newIssueLabel }}
- </gl-button>
- </template>
- </gl-empty-state>
-
- <gl-empty-state
- v-else-if="isOpenTab"
- :description="$options.i18n.noOpenIssuesDescription"
- :title="$options.i18n.noOpenIssuesTitle"
- :svg-path="emptyStateSvgPath"
- >
- <template #actions>
- <gl-button v-if="showNewIssueLink" :href="newIssuePath" variant="confirm">
- {{ $options.i18n.newIssueLabel }}
- </gl-button>
- </template>
- </gl-empty-state>
-
- <gl-empty-state
- v-else
- :title="$options.i18n.noClosedIssuesTitle"
- :svg-path="emptyStateSvgPath"
- />
+ <empty-state-with-any-issues :has-search="hasSearch" :is-open-tab="isOpenTab" />
+ </template>
+
+ <template #list-body>
+ <slot name="list-body"></slot>
</template>
</issuable-list>
- <template v-else-if="isSignedIn">
- <gl-empty-state :title="$options.i18n.noIssuesSignedInTitle" :svg-path="emptyStateSvgPath">
- <template #description>
- <gl-link :href="issuesHelpPagePath" target="_blank">{{
- $options.i18n.noIssuesSignedInDescription
- }}</gl-link>
- <p v-if="canCreateProjects">
- <strong>{{ $options.i18n.noGroupIssuesSignedInDescription }}</strong>
- </p>
- </template>
- <template #actions>
- <gl-button v-if="canCreateProjects" :href="newProjectPath" variant="confirm">
- {{ $options.i18n.newProjectLabel }}
- </gl-button>
- <gl-button v-if="showNewIssueLink" :href="newIssuePath" variant="confirm">
- {{ $options.i18n.newIssueLabel }}
- </gl-button>
- <csv-import-export-buttons
- v-if="showCsvButtons"
- class="gl-w-full gl-sm-w-auto gl-sm-mr-3"
- :export-csv-path="exportCsvPathWithQuery"
- :issuable-count="currentTabCount"
- />
- <new-issue-dropdown v-if="showNewIssueDropdown" class="gl-align-self-center" />
- </template>
- </gl-empty-state>
- <hr />
- <p class="gl-text-center gl-font-weight-bold gl-mb-0">
- {{ $options.i18n.jiraIntegrationTitle }}
- </p>
- <p class="gl-text-center gl-mb-0">
- <gl-sprintf :message="$options.i18n.jiraIntegrationMessage">
- <template #jiraDocsLink="{ content }">
- <gl-link :href="jiraIntegrationPath">{{ content }}</gl-link>
- </template>
- </gl-sprintf>
- </p>
- <p class="gl-text-center gl-text-gray-500">
- {{ $options.i18n.jiraIntegrationSecondaryMessage }}
- </p>
- </template>
-
- <gl-empty-state
+ <empty-state-without-any-issues
v-else
- :title="$options.i18n.noIssuesSignedOutTitle"
- :svg-path="emptyStateSvgPath"
- :primary-button-text="$options.i18n.noIssuesSignedOutButtonText"
- :primary-button-link="signInPath"
- >
- <template #description>
- <gl-link :href="issuesHelpPagePath" target="_blank">{{
- $options.i18n.noIssuesSignedOutDescription
- }}</gl-link>
- </template>
- </gl-empty-state>
+ :current-tab-count="currentTabCount"
+ :export-csv-path-with-query="exportCsvPathWithQuery"
+ :show-csv-buttons="showCsvButtons"
+ :show-new-issue-dropdown="showNewIssueDropdown"
+ />
<issuable-by-email v-if="showIssuableByEmail" class="gl-text-center gl-pt-5 gl-pb-7" />
</div>
diff --git a/app/assets/javascripts/issues/list/components/new_issue_dropdown.vue b/app/assets/javascripts/issues/list/components/new_issue_dropdown.vue
index 666e80dfd4b..e420c21a11f 100644
--- a/app/assets/javascripts/issues/list/components/new_issue_dropdown.vue
+++ b/app/assets/javascripts/issues/list/components/new_issue_dropdown.vue
@@ -6,7 +6,7 @@ import {
GlLoadingIcon,
GlSearchBoxByType,
} from '@gitlab/ui';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import { DASH_SCOPE, joinPaths } from '~/lib/utils/url_utility';
import { __, sprintf } from '~/locale';
import { DEBOUNCE_DELAY } from '~/vue_shared/components/filtered_search_bar/constants';
@@ -45,7 +45,7 @@ export default {
},
update: ({ group }) => group.projects.nodes ?? [],
error(error) {
- createFlash({
+ createAlert({
message: __('An error occurred while loading projects.'),
captureError: true,
error,
diff --git a/app/assets/javascripts/issues/list/constants.js b/app/assets/javascripts/issues/list/constants.js
index 5ed9ceea856..49a953cad43 100644
--- a/app/assets/javascripts/issues/list/constants.js
+++ b/app/assets/javascripts/issues/list/constants.js
@@ -6,7 +6,7 @@ import {
FILTER_STARTED,
FILTER_UPCOMING,
OPERATOR_IS,
- OPERATOR_IS_NOT,
+ OPERATOR_NOT,
OPERATOR_OR,
TOKEN_TYPE_ASSIGNEE,
TOKEN_TYPE_AUTHOR,
@@ -22,6 +22,7 @@ import {
TOKEN_TYPE_RELEASE,
TOKEN_TYPE_TYPE,
TOKEN_TYPE_WEIGHT,
+ TOKEN_TYPE_SEARCH_WITHIN,
} from '~/vue_shared/components/filtered_search_bar/constants';
import {
WORK_ITEM_TYPE_ENUM_INCIDENT,
@@ -30,6 +31,50 @@ import {
WORK_ITEM_TYPE_ENUM_TASK,
} from '~/work_items/constants';
+export const ISSUE_REFERENCE = /^#\d+$/;
+export const MAX_LIST_SIZE = 10;
+export const PAGE_SIZE = 20;
+export const PARAM_ASSIGNEE_ID = 'assignee_id';
+export const PARAM_FIRST_PAGE_SIZE = 'first_page_size';
+export const PARAM_LAST_PAGE_SIZE = 'last_page_size';
+export const PARAM_PAGE_AFTER = 'page_after';
+export const PARAM_PAGE_BEFORE = 'page_before';
+export const PARAM_SORT = 'sort';
+export const PARAM_STATE = 'state';
+export const RELATIVE_POSITION = 'relative_position';
+
+export const BLOCKING_ISSUES_ASC = 'BLOCKING_ISSUES_ASC';
+export const BLOCKING_ISSUES_DESC = 'BLOCKING_ISSUES_DESC';
+export const CLOSED_AT_ASC = 'CLOSED_AT_ASC';
+export const CLOSED_AT_DESC = 'CLOSED_AT_DESC';
+export const CREATED_ASC = 'CREATED_ASC';
+export const CREATED_DESC = 'CREATED_DESC';
+export const DUE_DATE_ASC = 'DUE_DATE_ASC';
+export const DUE_DATE_DESC = 'DUE_DATE_DESC';
+export const HEALTH_STATUS_ASC = 'HEALTH_STATUS_ASC';
+export const HEALTH_STATUS_DESC = 'HEALTH_STATUS_DESC';
+export const LABEL_PRIORITY_ASC = 'LABEL_PRIORITY_ASC';
+export const LABEL_PRIORITY_DESC = 'LABEL_PRIORITY_DESC';
+export const MILESTONE_DUE_ASC = 'MILESTONE_DUE_ASC';
+export const MILESTONE_DUE_DESC = 'MILESTONE_DUE_DESC';
+export const POPULARITY_ASC = 'POPULARITY_ASC';
+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';
+export const WEIGHT_DESC = 'WEIGHT_DESC';
+
+export const API_PARAM = 'apiParam';
+export const URL_PARAM = 'urlParam';
+export const NORMAL_FILTER = 'normalFilter';
+export const SPECIAL_FILTER = 'specialFilter';
+export const ALTERNATIVE_FILTER = 'alternativeFilter';
+
export const i18n = {
anonymousSearchingMessage: __('You must sign in to search for specific terms.'),
calendarLabel: __('Subscribe to calendar'),
@@ -57,11 +102,9 @@ export const i18n = {
),
noOpenIssuesDescription: __('To keep this project going, create a new issue'),
noOpenIssuesTitle: __('There are no open issues'),
- noIssuesSignedInDescription: __('Learn more about issues.'),
- noIssuesSignedInTitle: __('Use issues to collaborate on ideas, solve problems, and plan work'),
+ noIssuesDescription: __('Learn more about issues.'),
+ noIssuesTitle: __('Use issues to collaborate on ideas, solve problems, and plan work'),
noIssuesSignedOutButtonText: __('Register / Sign In'),
- noIssuesSignedOutDescription: __('Learn more about issues.'),
- noIssuesSignedOutTitle: __('Use issues to collaborate on ideas, solve problems, and plan work'),
noSearchResultsDescription: __('To widen your search, change or remove filters above'),
noSearchResultsTitle: __('Sorry, your filter produced no results'),
relatedMergeRequests: __('Related merge requests'),
@@ -69,45 +112,10 @@ export const i18n = {
rssLabel: __('Subscribe to RSS feed'),
searchPlaceholder: __('Search or filter results...'),
upvotes: __('Upvotes'),
+ titles: __('Titles'),
+ descriptions: __('Descriptions'),
};
-export const ISSUE_REFERENCE = /^#\d+$/;
-export const MAX_LIST_SIZE = 10;
-export const PAGE_SIZE = 20;
-export const PAGE_SIZE_MANUAL = 100;
-export const PARAM_ASSIGNEE_ID = 'assignee_id';
-export const PARAM_FIRST_PAGE_SIZE = 'first_page_size';
-export const PARAM_LAST_PAGE_SIZE = 'last_page_size';
-export const PARAM_PAGE_AFTER = 'page_after';
-export const PARAM_PAGE_BEFORE = 'page_before';
-export const PARAM_SORT = 'sort';
-export const PARAM_STATE = 'state';
-export const RELATIVE_POSITION = 'relative_position';
-
-export const BLOCKING_ISSUES_ASC = 'BLOCKING_ISSUES_ASC';
-export const BLOCKING_ISSUES_DESC = 'BLOCKING_ISSUES_DESC';
-export const CREATED_ASC = 'CREATED_ASC';
-export const CREATED_DESC = 'CREATED_DESC';
-export const DUE_DATE_ASC = 'DUE_DATE_ASC';
-export const DUE_DATE_DESC = 'DUE_DATE_DESC';
-export const LABEL_PRIORITY_ASC = 'LABEL_PRIORITY_ASC';
-export const LABEL_PRIORITY_DESC = 'LABEL_PRIORITY_DESC';
-export const MILESTONE_DUE_ASC = 'MILESTONE_DUE_ASC';
-export const MILESTONE_DUE_DESC = 'MILESTONE_DUE_DESC';
-export const POPULARITY_ASC = 'POPULARITY_ASC';
-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';
-export const WEIGHT_DESC = 'WEIGHT_DESC';
-export const CLOSED_ASC = 'CLOSED_AT_ASC';
-export const CLOSED_DESC = 'CLOSED_AT_DESC';
-
export const urlSortParams = {
[PRIORITY_ASC]: 'priority',
[PRIORITY_DESC]: 'priority_desc',
@@ -115,8 +123,8 @@ export const urlSortParams = {
[CREATED_DESC]: 'created_date',
[UPDATED_ASC]: 'updated_asc',
[UPDATED_DESC]: 'updated_desc',
- [CLOSED_ASC]: 'closed_asc',
- [CLOSED_DESC]: 'closed_desc',
+ [CLOSED_AT_ASC]: 'closed_at',
+ [CLOSED_AT_DESC]: 'closed_at_desc',
[MILESTONE_DUE_ASC]: 'milestone',
[MILESTONE_DUE_DESC]: 'milestone_due_desc',
[DUE_DATE_ASC]: 'due_date',
@@ -126,20 +134,16 @@ export const urlSortParams = {
[LABEL_PRIORITY_ASC]: 'label_priority',
[LABEL_PRIORITY_DESC]: 'label_priority_desc',
[RELATIVE_POSITION_ASC]: RELATIVE_POSITION,
+ [TITLE_ASC]: 'title_asc',
+ [TITLE_DESC]: 'title_desc',
+ [HEALTH_STATUS_ASC]: 'health_status_asc',
+ [HEALTH_STATUS_DESC]: 'health_status_desc',
[WEIGHT_ASC]: 'weight',
[WEIGHT_DESC]: 'weight_desc',
[BLOCKING_ISSUES_ASC]: 'blocking_issues_asc',
[BLOCKING_ISSUES_DESC]: 'blocking_issues_desc',
- [TITLE_ASC]: 'title_asc',
- [TITLE_DESC]: 'title_desc',
};
-export const API_PARAM = 'apiParam';
-export const URL_PARAM = 'urlParam';
-export const NORMAL_FILTER = 'normalFilter';
-export const SPECIAL_FILTER = 'specialFilter';
-export const ALTERNATIVE_FILTER = 'alternativeFilter';
-
export const specialFilterValues = [
FILTER_NONE,
FILTER_ANY,
@@ -148,7 +152,17 @@ export const specialFilterValues = [
FILTER_STARTED,
];
-export const TYPE_TOKEN_TASK_OPTION = { icon: 'issue-type-task', title: 'task', value: 'task' };
+export const TYPE_TOKEN_OBJECTIVE_OPTION = {
+ icon: 'issue-type-objective',
+ title: 'objective',
+ value: 'objective',
+};
+
+export const TYPE_TOKEN_KEY_RESULT_OPTION = {
+ icon: 'issue-type-key-result',
+ title: 'key_result',
+ value: 'key_result',
+};
// This should be consistent with Issue::TYPES_FOR_LIST in the backend
// https://gitlab.com/gitlab-org/gitlab/-/blob/1379c2d7bffe2a8d809f23ac5ef9b4114f789c07/app/models/issue.rb#L48
@@ -163,20 +177,35 @@ export const defaultTypeTokenOptions = [
{ icon: 'issue-type-issue', title: 'issue', value: 'issue' },
{ icon: 'issue-type-incident', title: 'incident', value: 'incident' },
{ icon: 'issue-type-test-case', title: 'test_case', value: 'test_case' },
+ { icon: 'issue-type-task', title: 'task', value: 'task' },
];
export const filters = {
[TOKEN_TYPE_AUTHOR]: {
[API_PARAM]: {
[NORMAL_FILTER]: 'authorUsername',
+ [ALTERNATIVE_FILTER]: 'authorUsernames',
},
[URL_PARAM]: {
[OPERATOR_IS]: {
[NORMAL_FILTER]: 'author_username',
},
- [OPERATOR_IS_NOT]: {
+ [OPERATOR_NOT]: {
[NORMAL_FILTER]: 'not[author_username]',
},
+ [OPERATOR_OR]: {
+ [ALTERNATIVE_FILTER]: 'or[author_username]',
+ },
+ },
+ },
+ [TOKEN_TYPE_SEARCH_WITHIN]: {
+ [API_PARAM]: {
+ [NORMAL_FILTER]: 'in',
+ },
+ [URL_PARAM]: {
+ [OPERATOR_IS]: {
+ [NORMAL_FILTER]: 'in',
+ },
},
},
[TOKEN_TYPE_ASSIGNEE]: {
@@ -190,7 +219,7 @@ export const filters = {
[SPECIAL_FILTER]: 'assignee_id',
[ALTERNATIVE_FILTER]: 'assignee_username',
},
- [OPERATOR_IS_NOT]: {
+ [OPERATOR_NOT]: {
[NORMAL_FILTER]: 'not[assignee_username][]',
},
[OPERATOR_OR]: {
@@ -208,7 +237,7 @@ export const filters = {
[NORMAL_FILTER]: 'milestone_title',
[SPECIAL_FILTER]: 'milestone_title',
},
- [OPERATOR_IS_NOT]: {
+ [OPERATOR_NOT]: {
[NORMAL_FILTER]: 'not[milestone_title]',
[SPECIAL_FILTER]: 'not[milestone_title]',
},
@@ -225,7 +254,7 @@ export const filters = {
[SPECIAL_FILTER]: 'label_name[]',
[ALTERNATIVE_FILTER]: 'label_name',
},
- [OPERATOR_IS_NOT]: {
+ [OPERATOR_NOT]: {
[NORMAL_FILTER]: 'not[label_name][]',
},
},
@@ -238,7 +267,7 @@ export const filters = {
[OPERATOR_IS]: {
[NORMAL_FILTER]: 'type[]',
},
- [OPERATOR_IS_NOT]: {
+ [OPERATOR_NOT]: {
[NORMAL_FILTER]: 'not[type][]',
},
},
@@ -253,7 +282,7 @@ export const filters = {
[NORMAL_FILTER]: 'release_tag',
[SPECIAL_FILTER]: 'release_tag',
},
- [OPERATOR_IS_NOT]: {
+ [OPERATOR_NOT]: {
[NORMAL_FILTER]: 'not[release_tag]',
},
},
@@ -268,7 +297,7 @@ export const filters = {
[NORMAL_FILTER]: 'my_reaction_emoji',
[SPECIAL_FILTER]: 'my_reaction_emoji',
},
- [OPERATOR_IS_NOT]: {
+ [OPERATOR_NOT]: {
[NORMAL_FILTER]: 'not[my_reaction_emoji]',
},
},
@@ -293,7 +322,7 @@ export const filters = {
[NORMAL_FILTER]: 'iteration_id',
[SPECIAL_FILTER]: 'iteration_id',
},
- [OPERATOR_IS_NOT]: {
+ [OPERATOR_NOT]: {
[NORMAL_FILTER]: 'not[iteration_id]',
[SPECIAL_FILTER]: 'not[iteration_id]',
},
@@ -309,7 +338,7 @@ export const filters = {
[NORMAL_FILTER]: 'epic_id',
[SPECIAL_FILTER]: 'epic_id',
},
- [OPERATOR_IS_NOT]: {
+ [OPERATOR_NOT]: {
[NORMAL_FILTER]: 'not[epic_id]',
},
},
@@ -324,7 +353,7 @@ export const filters = {
[NORMAL_FILTER]: 'weight',
[SPECIAL_FILTER]: 'weight',
},
- [OPERATOR_IS_NOT]: {
+ [OPERATOR_NOT]: {
[NORMAL_FILTER]: 'not[weight]',
},
},
diff --git a/app/assets/javascripts/issues/list/index.js b/app/assets/javascripts/issues/list/index.js
index 5e04dd1971c..7b68b7432c9 100644
--- a/app/assets/javascripts/issues/list/index.js
+++ b/app/assets/javascripts/issues/list/index.js
@@ -2,16 +2,15 @@ import Vue from 'vue';
import VueApollo from 'vue-apollo';
import VueRouter from 'vue-router';
import IssuesListApp from 'ee_else_ce/issues/list/components/issues_list_app.vue';
-import createDefaultClient from '~/lib/graphql';
import { parseBoolean } from '~/lib/utils/common_utils';
-import JiraIssuesImportStatusRoot from './components/jira_issues_import_status_app.vue';
+import JiraIssuesImportStatusApp from './components/jira_issues_import_status_app.vue';
import { gqlClient } from './graphql';
export function mountJiraIssuesListApp() {
- const el = document.querySelector('.js-jira-issues-import-status');
+ const el = document.querySelector('.js-jira-issues-import-status-root');
if (!el) {
- return false;
+ return null;
}
const { issuesPath, projectPath } = el.dataset;
@@ -19,21 +18,19 @@ export function mountJiraIssuesListApp() {
const isJiraConfigured = parseBoolean(el.dataset.isJiraConfigured);
if (!isJiraConfigured || !canEdit) {
- return false;
+ return null;
}
Vue.use(VueApollo);
- const defaultClient = createDefaultClient();
- const apolloProvider = new VueApollo({
- defaultClient,
- });
return new Vue({
el,
name: 'JiraIssuesImportStatusRoot',
- apolloProvider,
+ apolloProvider: new VueApollo({
+ defaultClient: gqlClient,
+ }),
render(createComponent) {
- return createComponent(JiraIssuesImportStatusRoot, {
+ return createComponent(JiraIssuesImportStatusApp, {
props: {
canEdit,
isJiraConfigured,
@@ -46,10 +43,10 @@ export function mountJiraIssuesListApp() {
}
export function mountIssuesListApp() {
- const el = document.querySelector('.js-issues-list');
+ const el = document.querySelector('.js-issues-list-root');
if (!el) {
- return false;
+ return null;
}
Vue.use(VueApollo);
@@ -77,6 +74,7 @@ export function mountIssuesListApp() {
hasIssueWeightsFeature,
hasIterationsFeature,
hasScopedLabelsFeature,
+ hasOkrsFeature,
importCsvIssuesPath,
initialEmail,
initialSort,
@@ -127,6 +125,7 @@ export function mountIssuesListApp() {
hasIssueWeightsFeature: parseBoolean(hasIssueWeightsFeature),
hasIterationsFeature: parseBoolean(hasIterationsFeature),
hasScopedLabelsFeature: parseBoolean(hasScopedLabelsFeature),
+ hasOkrsFeature: parseBoolean(hasOkrsFeature),
initialSort,
isAnonymousSearchDisabled: parseBoolean(isAnonymousSearchDisabled),
isIssueRepositioningDisabled: parseBoolean(isIssueRepositioningDisabled),
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 b447289b425..ee97fb6edca 100644
--- a/app/assets/javascripts/issues/list/queries/get_issues.query.graphql
+++ b/app/assets/javascripts/issues/list/queries/get_issues.query.graphql
@@ -10,6 +10,7 @@ query getIssues(
$search: String
$sort: IssueSort
$state: IssuableState
+ $in: [IssuableSearchableField!]
$assigneeId: String
$assigneeUsernames: [String!]
$authorUsername: String
@@ -38,6 +39,7 @@ query getIssues(
search: $search
sort: $sort
state: $state
+ in: $in
assigneeId: $assigneeId
assigneeUsernames: $assigneeUsernames
authorUsername: $authorUsername
@@ -72,6 +74,7 @@ query getIssues(
search: $search
sort: $sort
state: $state
+ in: $in
assigneeId: $assigneeId
assigneeUsernames: $assigneeUsernames
authorUsername: $authorUsername
diff --git a/app/assets/javascripts/issues/list/utils.js b/app/assets/javascripts/issues/list/utils.js
index 2f9ab9d62ee..b566e08731c 100644
--- a/app/assets/javascripts/issues/list/utils.js
+++ b/app/assets/javascripts/issues/list/utils.js
@@ -4,9 +4,10 @@ import { getParameterByName } from '~/lib/utils/url_utility';
import { __ } from '~/locale';
import {
FILTERED_SEARCH_TERM,
- OPERATOR_IS_NOT,
+ OPERATOR_NOT,
OPERATOR_OR,
TOKEN_TYPE_ASSIGNEE,
+ TOKEN_TYPE_AUTHOR,
TOKEN_TYPE_CONFIDENTIAL,
TOKEN_TYPE_ITERATION,
TOKEN_TYPE_MILESTONE,
@@ -14,14 +15,19 @@ import {
TOKEN_TYPE_TYPE,
} from '~/vue_shared/components/filtered_search_bar/constants';
import {
+ ALTERNATIVE_FILTER,
API_PARAM,
BLOCKING_ISSUES_ASC,
BLOCKING_ISSUES_DESC,
+ CLOSED_AT_ASC,
+ CLOSED_AT_DESC,
CREATED_ASC,
CREATED_DESC,
DUE_DATE_ASC,
DUE_DATE_DESC,
filters,
+ HEALTH_STATUS_ASC,
+ HEALTH_STATUS_DESC,
LABEL_PRIORITY_ASC,
LABEL_PRIORITY_DESC,
MILESTONE_DUE_ASC,
@@ -44,8 +50,6 @@ import {
urlSortParams,
WEIGHT_ASC,
WEIGHT_DESC,
- CLOSED_ASC,
- CLOSED_DESC,
} from './constants';
export const getInitialPageParams = (
@@ -66,7 +70,11 @@ export const getSortKey = (sort) =>
export const isSortKey = (sort) => Object.keys(urlSortParams).includes(sort);
-export const getSortOptions = (hasIssueWeightsFeature, hasBlockedIssuesFeature) => {
+export const getSortOptions = ({
+ hasBlockedIssuesFeature,
+ hasIssuableHealthStatusFeature,
+ hasIssueWeightsFeature,
+}) => {
const sortOptions = [
{
id: 1,
@@ -96,8 +104,8 @@ export const getSortOptions = (hasIssueWeightsFeature, hasBlockedIssuesFeature)
id: 4,
title: __('Closed date'),
sortDirection: {
- ascending: CLOSED_ASC,
- descending: CLOSED_DESC,
+ ascending: CLOSED_AT_ASC,
+ descending: CLOSED_AT_DESC,
},
},
{
@@ -150,6 +158,17 @@ export const getSortOptions = (hasIssueWeightsFeature, hasBlockedIssuesFeature)
},
];
+ if (hasIssuableHealthStatusFeature) {
+ sortOptions.push({
+ id: sortOptions.length + 1,
+ title: __('Health'),
+ sortDirection: {
+ ascending: HEALTH_STATUS_ASC,
+ descending: HEALTH_STATUS_DESC,
+ },
+ });
+ }
+
if (hasIssueWeightsFeature) {
sortOptions.push({
id: sortOptions.length + 1,
@@ -223,13 +242,24 @@ export const getFilterTokens = (locationSearch) => {
return tokens.length ? tokens : [createTerm()];
};
-const getFilterType = (data, tokenType = '') => {
+const isSpecialFilter = (type, data) => {
const isAssigneeIdParam =
- tokenType === TOKEN_TYPE_ASSIGNEE &&
+ type === TOKEN_TYPE_ASSIGNEE &&
isPositiveInteger(data) &&
getParameterByName(PARAM_ASSIGNEE_ID) === data;
+ return specialFilterValues.includes(data) || isAssigneeIdParam;
+};
+
+const getFilterType = ({ type, value: { data, operator } }) => {
+ const isUnionedAuthor = type === TOKEN_TYPE_AUTHOR && operator === OPERATOR_OR;
- return specialFilterValues.includes(data) || isAssigneeIdParam ? SPECIAL_FILTER : NORMAL_FILTER;
+ if (isUnionedAuthor) {
+ return ALTERNATIVE_FILTER;
+ }
+ if (isSpecialFilter(type, data)) {
+ return SPECIAL_FILTER;
+ }
+ return NORMAL_FILTER;
};
const wildcardTokens = [TOKEN_TYPE_ITERATION, TOKEN_TYPE_MILESTONE, TOKEN_TYPE_RELEASE];
@@ -258,10 +288,10 @@ export const convertToApiParams = (filterTokens) => {
filterTokens
.filter((token) => token.type !== FILTERED_SEARCH_TERM)
.forEach((token) => {
- const filterType = getFilterType(token.value.data, token.type);
- const field = filters[token.type][API_PARAM][filterType];
+ const filterType = getFilterType(token);
+ const apiField = filters[token.type][API_PARAM][filterType];
let obj;
- if (token.value.operator === OPERATOR_IS_NOT) {
+ if (token.value.operator === OPERATOR_NOT) {
obj = not;
} else if (token.value.operator === OPERATOR_OR) {
obj = or;
@@ -270,7 +300,7 @@ export const convertToApiParams = (filterTokens) => {
}
const data = formatData(token);
Object.assign(obj, {
- [field]: obj[field] ? [obj[field], data].flat() : data,
+ [apiField]: obj[apiField] ? [obj[apiField], data].flat() : data,
});
});
@@ -289,10 +319,10 @@ export const convertToUrlParams = (filterTokens) =>
filterTokens
.filter((token) => token.type !== FILTERED_SEARCH_TERM)
.reduce((acc, token) => {
- const filterType = getFilterType(token.value.data, token.type);
- const param = filters[token.type][URL_PARAM][token.value.operator]?.[filterType];
+ const filterType = getFilterType(token);
+ const urlParam = filters[token.type][URL_PARAM][token.value.operator]?.[filterType];
return Object.assign(acc, {
- [param]: acc[param] ? [acc[param], token.value.data].flat() : token.value.data,
+ [urlParam]: acc[urlParam] ? [acc[urlParam], token.value.data].flat() : token.value.data,
});
}, {});
@@ -300,4 +330,4 @@ export const convertToSearchQuery = (filterTokens) =>
filterTokens
.filter((token) => token.type === FILTERED_SEARCH_TERM && token.value.data)
.map((token) => token.value.data)
- .join(' ');
+ .join(' ') || undefined;
diff --git a/app/assets/javascripts/issues/manual_ordering.js b/app/assets/javascripts/issues/manual_ordering.js
index bc1cffef943..1bb53dfd50d 100644
--- a/app/assets/javascripts/issues/manual_ordering.js
+++ b/app/assets/javascripts/issues/manual_ordering.js
@@ -1,5 +1,5 @@
import Sortable from 'sortablejs';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import axios from '~/lib/utils/axios_utils';
import { s__ } from '~/locale';
import { getSortableDefaultOptions, sortableStart } from '~/sortable/utils';
@@ -11,7 +11,7 @@ const updateIssue = (url, { move_before_id, move_after_id }) =>
move_after_id,
})
.catch(() => {
- createFlash({
+ createAlert({
message: s__("ManualOrdering|Couldn't save the order of the issues"),
});
});
diff --git a/app/assets/javascripts/issues/related_merge_requests/store/actions.js b/app/assets/javascripts/issues/related_merge_requests/store/actions.js
index 94abb50de89..4c81f1d9bc1 100644
--- a/app/assets/javascripts/issues/related_merge_requests/store/actions.js
+++ b/app/assets/javascripts/issues/related_merge_requests/store/actions.js
@@ -1,4 +1,4 @@
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import axios from '~/lib/utils/axios_utils';
import { normalizeHeaders } from '~/lib/utils/common_utils';
import { __ } from '~/locale';
@@ -29,7 +29,7 @@ export const fetchMergeRequests = ({ state, dispatch }) => {
})
.catch(() => {
dispatch('receiveDataError');
- createFlash({
+ createAlert({
message: __('Something went wrong while fetching related merge requests.'),
});
});
diff --git a/app/assets/javascripts/issues/show/components/app.vue b/app/assets/javascripts/issues/show/components/app.vue
index 0daf77e03dc..e5428f87095 100644
--- a/app/assets/javascripts/issues/show/components/app.vue
+++ b/app/assets/javascripts/issues/show/components/app.vue
@@ -1,7 +1,7 @@
<script>
import { GlIcon, GlBadge, GlIntersectionObserver, GlTooltipDirective } from '@gitlab/ui';
import Visibility from 'visibilityjs';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import {
IssuableStatus,
IssuableStatusText,
@@ -327,7 +327,7 @@ export default {
this.store.updateState(data);
})
.catch(() => {
- createFlash({
+ createAlert({
message: this.defaultErrorMessage,
});
});
@@ -362,7 +362,7 @@ export default {
this.updateAndShowForm(res.data);
})
.catch(() => {
- createFlash({
+ createAlert({
message: this.defaultErrorMessage,
});
this.updateAndShowForm();
@@ -429,7 +429,7 @@ export default {
errMsg += `. ${message}`;
}
- this.flashContainer = createFlash({
+ this.flashContainer = createAlert({
message: errMsg,
});
})
diff --git a/app/assets/javascripts/issues/show/components/description.vue b/app/assets/javascripts/issues/show/components/description.vue
index 5c2a154362f..78e729b97da 100644
--- a/app/assets/javascripts/issues/show/components/description.vue
+++ b/app/assets/javascripts/issues/show/components/description.vue
@@ -1,11 +1,12 @@
<script>
-import { GlSafeHtmlDirective as SafeHtml, GlToast, GlTooltip, GlModalDirective } from '@gitlab/ui';
+import { GlToast, GlTooltip, GlModalDirective } from '@gitlab/ui';
import $ from 'jquery';
import Sortable from 'sortablejs';
import Vue from 'vue';
+import SafeHtml from '~/vue_shared/directives/safe_html';
import { getIdFromGraphQLId, convertToGraphQLId } from '~/graphql_shared/utils';
import { TYPE_WORK_ITEM } from '~/graphql_shared/constants';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import { IssuableType } from '~/issues/constants';
import { isMetaKey } from '~/lib/utils/common_utils';
import { isPositiveInteger } from '~/lib/utils/number_utils';
@@ -27,6 +28,7 @@ import {
TASK_TYPE_NAME,
WIDGET_TYPE_DESCRIPTION,
} from '~/work_items/constants';
+import { renderGFM } from '~/behaviors/markdown/render_gfm';
import animateMixin from '../mixins/animate';
import { convertDescriptionWithNewSort } from '../utils';
@@ -165,7 +167,7 @@ export default {
this.renderGFM();
this.updateTaskStatusText();
- if (this.workItemId) {
+ if (this.workItemId && this.workItemsEnabled) {
const taskLink = this.$el.querySelector(
`.gfm-issue[data-issue="${getIdFromGraphQLId(this.workItemId)}"]`,
);
@@ -177,7 +179,7 @@ export default {
},
methods: {
renderGFM() {
- $(this.$refs['gfm-content']).renderGFM();
+ renderGFM(this.$refs['gfm-content']);
if (this.canUpdate) {
// eslint-disable-next-line no-new
@@ -283,7 +285,7 @@ export default {
},
taskListUpdateError() {
- createFlash({
+ createAlert({
message: sprintf(
__(
'Someone edited this %{issueType} at the same time you did. The description has been updated and you will need to make your changes again.',
@@ -467,7 +469,7 @@ export default {
this.workItemId = newWorkItem.id;
this.openWorkItemDetailModal(el);
} catch (error) {
- createFlash({
+ createAlert({
message: sprintfWorkItem(I18N_WORK_ITEM_ERROR_CREATING, workItemTypes.TASK),
error,
captureError: true,
diff --git a/app/assets/javascripts/issues/show/components/fields/description.vue b/app/assets/javascripts/issues/show/components/fields/description.vue
index 180dea77003..04c5007dbec 100644
--- a/app/assets/javascripts/issues/show/components/fields/description.vue
+++ b/app/assets/javascripts/issues/show/components/fields/description.vue
@@ -67,6 +67,7 @@ export default {
:quick-actions-docs-path="quickActionsDocsPath"
:enable-autocomplete="enableAutocomplete"
supports-quick-actions
+ use-bottom-toolbar
autofocus
@input="$emit('input', $event)"
@keydown.meta.enter="updateIssuable"
diff --git a/app/assets/javascripts/issues/show/components/form.vue b/app/assets/javascripts/issues/show/components/form.vue
index 0c6b61fb893..b56c91d7983 100644
--- a/app/assets/javascripts/issues/show/components/form.vue
+++ b/app/assets/javascripts/issues/show/components/form.vue
@@ -164,7 +164,7 @@ export default {
<template>
<form data-testid="issuable-form">
- <locked-warning v-if="showLockedWarning" />
+ <locked-warning v-if="showLockedWarning" :issuable-type="issuableType" />
<gl-alert
v-if="showOutdatedDescriptionWarning"
class="gl-mb-5"
diff --git a/app/assets/javascripts/issues/show/components/header_actions.vue b/app/assets/javascripts/issues/show/components/header_actions.vue
index c01de63ced9..983e2e6530e 100644
--- a/app/assets/javascripts/issues/show/components/header_actions.vue
+++ b/app/assets/javascripts/issues/show/components/header_actions.vue
@@ -10,7 +10,7 @@ import {
GlTooltipDirective,
} from '@gitlab/ui';
import { mapActions, mapGetters, mapState } from 'vuex';
-import createFlash, { FLASH_TYPES } from '~/flash';
+import { createAlert, VARIANT_SUCCESS } from '~/flash';
import { EVENT_ISSUABLE_VUE_APP_CHANGE } from '~/issuable/constants';
import { IssuableStatus, IssueType } from '~/issues/constants';
import { ISSUE_STATE_EVENT_CLOSE, ISSUE_STATE_EVENT_REOPEN } from '~/issues/show/constants';
@@ -40,6 +40,7 @@ export default {
promoteSuccessMessage: __(
'The issue was successfully promoted to an epic. Redirecting to epic...',
),
+ reportAbuse: __('Report abuse to administrator'),
},
components: {
DeleteIssueModal,
@@ -191,7 +192,7 @@ export default {
// Dispatch event which updates open/close state, shared among the issue show page
document.dispatchEvent(new CustomEvent(EVENT_ISSUABLE_VUE_APP_CHANGE, payload));
})
- .catch(() => createFlash({ message: __('Error occurred while updating the issue status') }))
+ .catch(() => createAlert({ message: __('Error occurred while updating the issue status') }))
.finally(() => {
this.toggleStateButtonLoading(false);
});
@@ -214,14 +215,14 @@ export default {
throw new Error();
}
- createFlash({
+ createAlert({
message: this.$options.i18n.promoteSuccessMessage,
- type: FLASH_TYPES.SUCCESS,
+ variant: VARIANT_SUCCESS,
});
visitUrl(data.promoteToEpic.epic.webPath);
})
- .catch(() => createFlash({ message: this.$options.i18n.promoteErrorMessage }))
+ .catch(() => createAlert({ message: this.$options.i18n.promoteErrorMessage }))
.finally(() => {
this.toggleStateButtonLoading(false);
});
@@ -255,7 +256,7 @@ export default {
{{ __('Promote to epic') }}
</gl-dropdown-item>
<gl-dropdown-item v-if="!isIssueAuthor" :href="reportAbusePath">
- {{ __('Report abuse') }}
+ {{ $options.i18n.reportAbuse }}
</gl-dropdown-item>
<gl-dropdown-item
v-if="canReportSpam"
@@ -314,7 +315,7 @@ export default {
{{ __('Promote to epic') }}
</gl-dropdown-item>
<gl-dropdown-item v-if="!isIssueAuthor" :href="reportAbusePath">
- {{ __('Report abuse') }}
+ {{ $options.i18n.reportAbuse }}
</gl-dropdown-item>
<gl-dropdown-item
v-if="canReportSpam"
diff --git a/app/assets/javascripts/issues/show/components/incidents/constants.js b/app/assets/javascripts/issues/show/components/incidents/constants.js
index db846009409..22db19610c1 100644
--- a/app/assets/javascripts/issues/show/components/incidents/constants.js
+++ b/app/assets/javascripts/issues/show/components/incidents/constants.js
@@ -14,6 +14,7 @@ export const timelineFormI18n = Object.freeze({
areaPlaceholder: s__('Incident|Timeline text...'),
save: __('Save'),
cancel: __('Cancel'),
+ delete: __('Delete'),
description: __('Description'),
hint: __('You can enter up to 280 characters'),
textRemaining: (count) => n__('%d character remaining', '%d characters remaining', count),
diff --git a/app/assets/javascripts/issues/show/components/incidents/edit_timeline_event.vue b/app/assets/javascripts/issues/show/components/incidents/edit_timeline_event.vue
index 60fa8cb949b..8cdd62ca9ef 100644
--- a/app/assets/javascripts/issues/show/components/incidents/edit_timeline_event.vue
+++ b/app/assets/javascripts/issues/show/components/incidents/edit_timeline_event.vue
@@ -40,8 +40,10 @@ export default {
:is-event-processed="editTimelineEventActive"
:previous-occurred-at="event.occurredAt"
:previous-note="event.note"
+ show-delete
@save-event="saveEvent"
@cancel="$emit('hide-edit')"
+ @delete="$emit('delete')"
/>
</div>
</template>
diff --git a/app/assets/javascripts/issues/show/components/incidents/graphql/queries/create_timeline_event.mutation.graphql b/app/assets/javascripts/issues/show/components/incidents/graphql/queries/create_timeline_event.mutation.graphql
index f1fc27dcb2a..4a8786b04b1 100644
--- a/app/assets/javascripts/issues/show/components/incidents/graphql/queries/create_timeline_event.mutation.graphql
+++ b/app/assets/javascripts/issues/show/components/incidents/graphql/queries/create_timeline_event.mutation.graphql
@@ -7,6 +7,12 @@ mutation CreateTimelineEvent($input: TimelineEventCreateInput!) {
action
occurredAt
createdAt
+ timelineEventTags {
+ nodes {
+ id
+ name
+ }
+ }
}
errors
}
diff --git a/app/assets/javascripts/issues/show/components/incidents/graphql/queries/get_alert.graphql b/app/assets/javascripts/issues/show/components/incidents/graphql/queries/get_alert.graphql
index d88633f2ae9..e057267b006 100644
--- a/app/assets/javascripts/issues/show/components/incidents/graphql/queries/get_alert.graphql
+++ b/app/assets/javascripts/issues/show/components/incidents/graphql/queries/get_alert.graphql
@@ -4,6 +4,7 @@ query getAlert($iid: String!, $fullPath: ID!) {
issue(iid: $iid) {
id
alertManagementAlert {
+ id
iid
title
detailsUrl
diff --git a/app/assets/javascripts/issues/show/components/incidents/graphql/queries/get_timeline_events.query.graphql b/app/assets/javascripts/issues/show/components/incidents/graphql/queries/get_timeline_events.query.graphql
index bc4e8414bfc..baeb81745ab 100644
--- a/app/assets/javascripts/issues/show/components/incidents/graphql/queries/get_timeline_events.query.graphql
+++ b/app/assets/javascripts/issues/show/components/incidents/graphql/queries/get_timeline_events.query.graphql
@@ -9,6 +9,12 @@ query GetTimelineEvents($fullPath: ID!, $incidentId: IssueID!) {
action
occurredAt
createdAt
+ timelineEventTags {
+ nodes {
+ id
+ name
+ }
+ }
}
}
}
diff --git a/app/assets/javascripts/issues/show/components/incidents/incident_tabs.vue b/app/assets/javascripts/issues/show/components/incidents/incident_tabs.vue
index 5725d0f8d6a..53956fcb4b2 100644
--- a/app/assets/javascripts/issues/show/components/incidents/incident_tabs.vue
+++ b/app/assets/javascripts/issues/show/components/incidents/incident_tabs.vue
@@ -1,16 +1,29 @@
<script>
import { GlTab, GlTabs } from '@gitlab/ui';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import { trackIncidentDetailsViewsOptions } from '~/incidents/constants';
import { s__ } from '~/locale';
import Tracking from '~/tracking';
import AlertDetailsTable from '~/vue_shared/components/alert_details_table.vue';
-import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import DescriptionComponent from '../description.vue';
import getAlert from './graphql/queries/get_alert.graphql';
import HighlightBar from './highlight_bar.vue';
import TimelineTab from './timeline_events_tab.vue';
+export const incidentTabsI18n = Object.freeze({
+ summaryTitle: s__('Incident|Summary'),
+ metricsTitle: s__('Incident|Metrics'),
+ alertsTitle: s__('Incident|Alert details'),
+ timelineTitle: s__('Incident|Timeline'),
+});
+
+export const TAB_NAMES = Object.freeze({
+ SUMMARY: '',
+ ALERTS: 'alerts',
+ METRICS: 'metrics',
+ TIMELINE: 'timeline',
+});
+
export default {
components: {
AlertDetailsTable,
@@ -22,8 +35,8 @@ export default {
IncidentMetricTab: () =>
import('ee_component/issues/show/components/incidents/incident_metric_tab.vue'),
},
- mixins: [glFeatureFlagsMixin()],
- inject: ['fullPath', 'iid'],
+ inject: ['fullPath', 'iid', 'uploadMetricsFeatureAvailable'],
+ i18n: incidentTabsI18n,
apollo: {
alert: {
query: getAlert,
@@ -37,7 +50,7 @@ export default {
return data?.project?.issue?.alertManagementAlert;
},
error() {
- createFlash({
+ createAlert({
message: s__('Incident|There was an issue loading alert data. Please try again.'),
});
},
@@ -46,12 +59,44 @@ export default {
data() {
return {
alert: null,
+ activeTabIndex: 0,
};
},
computed: {
loading() {
return this.$apollo.queries.alert.loading;
},
+ tabMapping() {
+ const availableTabs = [TAB_NAMES.SUMMARY];
+
+ if (this.uploadMetricsFeatureAvailable) {
+ availableTabs.push(TAB_NAMES.METRICS);
+ }
+ if (this.alert) {
+ availableTabs.push(TAB_NAMES.ALERTS);
+ }
+
+ availableTabs.push(TAB_NAMES.TIMELINE);
+
+ const tabNamesToIndex = {};
+ const tabIndexToName = {};
+
+ availableTabs.forEach((item, index) => {
+ tabNamesToIndex[item] = index;
+ tabIndexToName[index] = item;
+ });
+
+ return { tabNamesToIndex, tabIndexToName };
+ },
+ currentTabIndex: {
+ get() {
+ return this.activeTabIndex;
+ },
+ set(index) {
+ this.handleTabChange(index);
+ this.activeTabIndex = index;
+ },
+ },
},
mounted() {
this.trackPageViews();
@@ -91,25 +136,33 @@ export default {
<template>
<div>
<gl-tabs
+ v-model="currentTabIndex"
content-class="gl-reset-line-height"
class="gl-mt-n3"
data-testid="incident-tabs"
- @input="handleTabChange"
>
- <gl-tab :title="s__('Incident|Summary')">
+ <gl-tab :title="$options.i18n.summaryTitle" data-testid="summary-tab">
<highlight-bar :alert="alert" />
<description-component v-bind="$attrs" v-on="$listeners" />
</gl-tab>
- <incident-metric-tab />
+ <gl-tab
+ v-if="uploadMetricsFeatureAvailable"
+ :title="$options.i18n.metricsTitle"
+ data-testid="metrics-tab"
+ >
+ <incident-metric-tab />
+ </gl-tab>
<gl-tab
v-if="alert"
class="alert-management-details"
- :title="s__('Incident|Alert details')"
+ :title="$options.i18n.alertsTitle"
data-testid="alert-details-tab"
>
<alert-details-table :alert="alert" :loading="loading" />
</gl-tab>
- <timeline-tab />
+ <gl-tab :title="$options.i18n.timelineTitle" data-testid="timeline-tab">
+ <timeline-tab />
+ </gl-tab>
</gl-tabs>
</div>
</template>
diff --git a/app/assets/javascripts/issues/show/components/incidents/timeline_events_form.vue b/app/assets/javascripts/issues/show/components/incidents/timeline_events_form.vue
index 72dfccca467..f1a3aebc990 100644
--- a/app/assets/javascripts/issues/show/components/incidents/timeline_events_form.vue
+++ b/app/assets/javascripts/issues/show/components/incidents/timeline_events_form.vue
@@ -1,7 +1,6 @@
<script>
import { GlDatepicker, GlFormInput, GlFormGroup, GlButton } from '@gitlab/ui';
import MarkdownField from '~/vue_shared/components/markdown/field.vue';
-import autofocusonshow from '~/vue_shared/directives/autofocusonshow';
import { MAX_TEXT_LENGTH, timelineFormI18n } from './constants';
import { getUtcShiftedDate } from './utils';
@@ -27,15 +26,17 @@ export default {
},
i18n: timelineFormI18n,
MAX_TEXT_LENGTH,
- directives: {
- autofocusonshow,
- },
props: {
showSaveAndAdd: {
type: Boolean,
required: false,
default: false,
},
+ showDelete: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
isEventProcessed: {
type: Boolean,
required: true,
@@ -97,7 +98,7 @@ export default {
this.timelineText = '';
},
focusDate() {
- this.$refs.datepicker.$el.querySelector('input').focus();
+ this.$refs.datepicker.$el.querySelector('input')?.focus();
},
handleSave(addAnotherEvent) {
const event = {
@@ -185,32 +186,42 @@ export default {
</gl-form-group>
</div>
<gl-form-group class="gl-mb-0">
- <gl-button
- variant="confirm"
- category="primary"
- class="gl-mr-3"
- data-testid="save-button"
- :disabled="!isTimelineTextValid"
- :loading="isEventProcessed"
- @click="handleSave(false)"
- >
- {{ $options.i18n.save }}
- </gl-button>
- <gl-button
- v-if="showSaveAndAdd"
- variant="confirm"
- category="secondary"
- class="gl-mr-3 gl-ml-n2"
- data-testid="save-and-add-button"
- :disabled="!isTimelineTextValid"
- :loading="isEventProcessed"
- @click="handleSave(true)"
- >
- {{ $options.i18n.saveAndAdd }}
- </gl-button>
- <gl-button class="gl-ml-n2" :disabled="isEventProcessed" @click="$emit('cancel')">
- {{ $options.i18n.cancel }}
- </gl-button>
+ <div class="gl-display-flex">
+ <gl-button
+ variant="confirm"
+ category="primary"
+ class="gl-mr-3"
+ data-testid="save-button"
+ :disabled="!isTimelineTextValid"
+ :loading="isEventProcessed"
+ @click="handleSave(false)"
+ >
+ {{ $options.i18n.save }}
+ </gl-button>
+ <gl-button
+ v-if="showSaveAndAdd"
+ variant="confirm"
+ category="secondary"
+ class="gl-mr-3 gl-ml-n2"
+ data-testid="save-and-add-button"
+ :disabled="!isTimelineTextValid"
+ :loading="isEventProcessed"
+ @click="handleSave(true)"
+ >
+ {{ $options.i18n.saveAndAdd }}
+ </gl-button>
+ <gl-button class="gl-ml-n2" :disabled="isEventProcessed" @click="$emit('cancel')">
+ {{ $options.i18n.cancel }}
+ </gl-button>
+ <gl-button
+ v-if="showDelete"
+ class="gl-ml-auto btn-danger"
+ :disabled="isEventProcessed"
+ @click="$emit('delete')"
+ >
+ {{ $options.i18n.delete }}
+ </gl-button>
+ </div>
<div class="timeline-event-bottom-border"></div>
</gl-form-group>
</form>
diff --git a/app/assets/javascripts/issues/show/components/incidents/timeline_events_item.vue b/app/assets/javascripts/issues/show/components/incidents/timeline_events_item.vue
index cbf3c387fa3..90ee4351e39 100644
--- a/app/assets/javascripts/issues/show/components/incidents/timeline_events_item.vue
+++ b/app/assets/javascripts/issues/show/components/incidents/timeline_events_item.vue
@@ -1,5 +1,6 @@
<script>
-import { GlDropdown, GlDropdownItem, GlIcon, GlSafeHtmlDirective, GlSprintf } from '@gitlab/ui';
+import { GlDropdown, GlDropdownItem, GlIcon, GlSprintf, GlBadge } from '@gitlab/ui';
+import SafeHtml from '~/vue_shared/directives/safe_html';
import { formatDate } from '~/lib/utils/datetime_utility';
import { timelineItemI18n } from './constants';
import { getEventIcon } from './utils';
@@ -12,9 +13,10 @@ export default {
GlDropdownItem,
GlIcon,
GlSprintf,
+ GlBadge,
},
directives: {
- SafeHtml: GlSafeHtmlDirective,
+ SafeHtml,
},
inject: ['canUpdateTimelineEvent'],
props: {
@@ -30,6 +32,11 @@ export default {
type: String,
required: true,
},
+ eventTag: {
+ type: String,
+ required: false,
+ default: null,
+ },
},
computed: {
time() {
@@ -42,41 +49,41 @@ export default {
};
</script>
<template>
- <div class="gl-display-flex gl-align-items-start">
+ <div class="timeline-event gl-display-grid">
<div
class="gl-display-flex gl-align-items-center gl-justify-content-center gl-bg-white gl-text-gray-200 gl-border-gray-100 gl-border-1 gl-border-solid gl-rounded-full gl-mt-2 gl-mr-3 gl-w-8 gl-h-8 gl-p-3 gl-z-index-1"
>
<gl-icon :name="getEventIcon(action)" class="note-icon" />
</div>
- <div
- class="timeline-event-note timeline-event-border gl-w-full gl-display-flex gl-flex-direction-row"
- data-testid="event-text-container"
- >
- <div>
+ <div class="timeline-event-note timeline-event-border" data-testid="event-text-container">
+ <div class="gl-display-flex gl-align-items-center gl-mb-3">
<strong class="gl-font-lg" data-testid="event-time">
<gl-sprintf :message="$options.i18n.timeUTC">
<template #time>{{ time }}</template>
</gl-sprintf>
</strong>
- <div v-safe-html="noteHtml"></div>
+ <gl-badge v-if="eventTag" variant="muted" icon="tag" class="gl-ml-3">
+ {{ eventTag }}
+ </gl-badge>
</div>
- <gl-dropdown
- v-if="canUpdateTimelineEvent"
- right
- class="event-note-actions gl-ml-auto gl-align-self-start"
- icon="ellipsis_v"
- text-sr-only
- :text="$options.i18n.moreActions"
- category="tertiary"
- no-caret
- >
- <gl-dropdown-item @click="$emit('edit')">
- {{ $options.i18n.edit }}
- </gl-dropdown-item>
- <gl-dropdown-item @click="$emit('delete')">
- {{ $options.i18n.delete }}
- </gl-dropdown-item>
- </gl-dropdown>
+ <div v-safe-html="noteHtml" class="md"></div>
</div>
+ <gl-dropdown
+ v-if="canUpdateTimelineEvent"
+ right
+ class="event-note-actions gl-ml-auto gl-align-self-start"
+ icon="ellipsis_v"
+ text-sr-only
+ :text="$options.i18n.moreActions"
+ category="tertiary"
+ no-caret
+ >
+ <gl-dropdown-item @click="$emit('edit')">
+ {{ $options.i18n.edit }}
+ </gl-dropdown-item>
+ <gl-dropdown-item @click="$emit('delete')">
+ {{ $options.i18n.delete }}
+ </gl-dropdown-item>
+ </gl-dropdown>
</div>
</template>
diff --git a/app/assets/javascripts/issues/show/components/incidents/timeline_events_list.vue b/app/assets/javascripts/issues/show/components/incidents/timeline_events_list.vue
index 321b7ccc14a..c6b93201c97 100644
--- a/app/assets/javascripts/issues/show/components/incidents/timeline_events_list.vue
+++ b/app/assets/javascripts/issues/show/components/incidents/timeline_events_list.vue
@@ -50,6 +50,9 @@ export default {
},
},
methods: {
+ getFirstTag(eventTag) {
+ return eventTag.nodes?.[0]?.name;
+ },
handleEditSelection(event) {
this.eventToEdit = event.id;
this.$emit('hide-new-incident-timeline-event-form');
@@ -153,6 +156,7 @@ export default {
:edit-timeline-event-active="editTimelineEventActive"
@handle-save-edit="handleSaveEdit"
@hide-edit="hideEdit()"
+ @delete="handleDelete(event)"
/>
<incident-timeline-event-item
v-else
@@ -160,6 +164,7 @@ export default {
:action="event.action"
:occurred-at="event.occurredAt"
:note-html="event.noteHtml"
+ :event-tag="getFirstTag(event.timelineEventTags)"
@delete="handleDelete(event)"
@edit="handleEditSelection(event)"
/>
diff --git a/app/assets/javascripts/issues/show/components/incidents/timeline_events_tab.vue b/app/assets/javascripts/issues/show/components/incidents/timeline_events_tab.vue
index 5f70d9acac9..c8237766505 100644
--- a/app/assets/javascripts/issues/show/components/incidents/timeline_events_tab.vue
+++ b/app/assets/javascripts/issues/show/components/incidents/timeline_events_tab.vue
@@ -1,5 +1,5 @@
<script>
-import { GlButton, GlEmptyState, GlLoadingIcon, GlTab } from '@gitlab/ui';
+import { GlButton, GlEmptyState, GlLoadingIcon } from '@gitlab/ui';
import { convertToGraphQLId } from '~/graphql_shared/utils';
import { TYPE_ISSUE } from '~/graphql_shared/constants';
import { fetchPolicies } from '~/lib/graphql';
@@ -15,7 +15,6 @@ export default {
GlButton,
GlEmptyState,
GlLoadingIcon,
- GlTab,
CreateTimelineEvent,
IncidentTimelineEventsList,
},
@@ -77,7 +76,7 @@ export default {
</script>
<template>
- <gl-tab :title="$options.i18n.title">
+ <div>
<gl-loading-icon v-if="timelineEventLoading" size="lg" color="dark" class="gl-mt-5" />
<gl-empty-state
v-else-if="showEmptyState"
@@ -106,5 +105,5 @@ export default {
>
{{ $options.i18n.addEventButton }}
</gl-button>
- </gl-tab>
+ </div>
</template>
diff --git a/app/assets/javascripts/issues/show/components/locked_warning.vue b/app/assets/javascripts/issues/show/components/locked_warning.vue
index 12feacb027b..4414e693ed0 100644
--- a/app/assets/javascripts/issues/show/components/locked_warning.vue
+++ b/app/assets/javascripts/issues/show/components/locked_warning.vue
@@ -1,29 +1,44 @@
<script>
import { GlSprintf, GlLink, GlAlert } from '@gitlab/ui';
-import { __ } from '~/locale';
+import { __, sprintf } from '~/locale';
+import { IssuableType } from '~/issues/constants';
-const alertMessage = __(
- 'Someone edited the issue at the same time you did. Please check out %{linkStart}the issue%{linkEnd} and make sure your changes will not unintentionally remove theirs.',
-);
+export const i18n = Object.freeze({
+ alertMessage: __(
+ "Someone edited the %{issuableType} at the same time you did. Review %{linkStart}the %{issuableType}%{linkEnd} and make sure you don't unintentionally overwrite their changes.",
+ ),
+});
export default {
- alertMessage,
components: {
GlSprintf,
GlLink,
GlAlert,
},
+ props: {
+ issuableType: {
+ type: String,
+ required: true,
+ validator(value) {
+ return Object.values(IssuableType).includes(value);
+ },
+ },
+ },
computed: {
currentPath() {
return window.location.pathname;
},
+ alertMessage() {
+ return sprintf(this.$options.i18n.alertMessage, { issuableType: this.issuableType });
+ },
},
+ i18n,
};
</script>
<template>
<gl-alert variant="danger" class="gl-mb-5" :dismissible="false">
- <gl-sprintf :message="$options.alertMessage">
+ <gl-sprintf :message="alertMessage">
<template #link="{ content }">
<gl-link :href="currentPath" target="_blank" rel="nofollow">
{{ content }}
diff --git a/app/assets/javascripts/issues/show/components/title.vue b/app/assets/javascripts/issues/show/components/title.vue
index 307d9f9f69a..6978f730e1d 100644
--- a/app/assets/javascripts/issues/show/components/title.vue
+++ b/app/assets/javascripts/issues/show/components/title.vue
@@ -1,5 +1,6 @@
<script>
-import { GlButton, GlTooltipDirective, GlSafeHtmlDirective as SafeHtml } from '@gitlab/ui';
+import { GlButton, GlTooltipDirective } from '@gitlab/ui';
+import SafeHtml from '~/vue_shared/directives/safe_html';
import { __ } from '~/locale';
import eventHub from '../event_hub';
import animateMixin from '../mixins/animate';
diff --git a/app/assets/javascripts/jira_connect/branches/components/source_branch_dropdown.vue b/app/assets/javascripts/jira_connect/branches/components/source_branch_dropdown.vue
index 0e2d8821f36..dac807dceb0 100644
--- a/app/assets/javascripts/jira_connect/branches/components/source_branch_dropdown.vue
+++ b/app/assets/javascripts/jira_connect/branches/components/source_branch_dropdown.vue
@@ -1,5 +1,6 @@
<script>
-import { GlDropdown, GlDropdownItem, GlSearchBoxByType, GlLoadingIcon } from '@gitlab/ui';
+import { GlCollapsibleListbox } from '@gitlab/ui';
+import { debounce } from 'lodash';
import { __ } from '~/locale';
import { BRANCHES_PER_PAGE } from '../constants';
import getProjectQuery from '../graphql/queries/get_project.query.graphql';
@@ -7,10 +8,7 @@ import getProjectQuery from '../graphql/queries/get_project.query.graphql';
export default {
BRANCHES_PER_PAGE,
components: {
- GlDropdown,
- GlDropdownItem,
- GlSearchBoxByType,
- GlLoadingIcon,
+ GlCollapsibleListbox,
},
props: {
selectedProject: {
@@ -26,7 +24,6 @@ export default {
},
data() {
return {
- sourceBranchSearchQuery: '',
initialSourceBranchNamesLoading: false,
sourceBranchNamesLoading: false,
sourceBranchNames: [],
@@ -59,6 +56,9 @@ export default {
onSourceBranchSelect(branchName) {
this.$emit('change', branchName);
},
+ onSearch: debounce(function debouncedSearch(branchSearchQuery) {
+ this.onSourceBranchSearchQuery(branchSearchQuery);
+ }, 250),
onSourceBranchSearchQuery(branchSearchQuery) {
this.branchSearchQuery = branchSearchQuery;
this.fetchSourceBranchNames({
@@ -83,7 +83,10 @@ export default {
});
const { branchNames, rootRef } = data?.project.repository || {};
- this.sourceBranchNames = branchNames || [];
+ this.sourceBranchNames =
+ branchNames.map((value) => {
+ return { text: value, value };
+ }) || [];
// Use root ref as the default selection
if (rootRef && !this.hasSelectedSourceBranch) {
@@ -102,33 +105,15 @@ export default {
</script>
<template>
- <gl-dropdown
- :text="branchDropdownText"
- :loading="initialSourceBranchNamesLoading"
- :disabled="!hasSelectedProject"
+ <gl-collapsible-listbox
:class="{ 'gl-font-monospace': hasSelectedSourceBranch }"
- >
- <template #header>
- <gl-search-box-by-type
- :debounce="250"
- :value="sourceBranchSearchQuery"
- @input="onSourceBranchSearchQuery"
- />
- </template>
-
- <gl-loading-icon v-show="sourceBranchNamesLoading" />
- <template v-if="!sourceBranchNamesLoading">
- <gl-dropdown-item
- v-for="branchName in sourceBranchNames"
- v-show="!sourceBranchNamesLoading"
- :key="branchName"
- :is-checked="branchName === selectedBranchName"
- is-check-item
- class="gl-font-monospace"
- @click="onSourceBranchSelect(branchName)"
- >
- {{ branchName }}
- </gl-dropdown-item>
- </template>
- </gl-dropdown>
+ :disabled="!hasSelectedProject"
+ :items="sourceBranchNames"
+ :loading="initialSourceBranchNamesLoading"
+ :searchable="true"
+ :searching="sourceBranchNamesLoading"
+ :toggle-text="branchDropdownText"
+ @search="onSearch"
+ @select="onSourceBranchSelect"
+ />
</template>
diff --git a/app/assets/javascripts/jira_connect/subscriptions/constants.js b/app/assets/javascripts/jira_connect/subscriptions/constants.js
index fc365746b54..01bc5dfc66b 100644
--- a/app/assets/javascripts/jira_connect/subscriptions/constants.js
+++ b/app/assets/javascripts/jira_connect/subscriptions/constants.js
@@ -38,7 +38,7 @@ export const INTEGRATIONS_DOC_LINK = helpPagePath('integration/jira/development_
anchor: 'use-the-integration',
});
export const OAUTH_SELF_MANAGED_DOC_LINK = helpPagePath('integration/jira/connect-app', {
- anchor: 'install-the-gitlabcom-for-jira-cloud-app-for-self-managed-instances',
+ anchor: 'connect-the-gitlabcom-for-jira-cloud-app-for-self-managed-instances',
});
export const GITLAB_COM_BASE_PATH = 'https://gitlab.com';
diff --git a/app/assets/javascripts/jira_connect/subscriptions/pages/sign_in/sign_in_gitlab_multiversion/index.vue b/app/assets/javascripts/jira_connect/subscriptions/pages/sign_in/sign_in_gitlab_multiversion/index.vue
index 5ff75e19425..7c6ff002014 100644
--- a/app/assets/javascripts/jira_connect/subscriptions/pages/sign_in/sign_in_gitlab_multiversion/index.vue
+++ b/app/assets/javascripts/jira_connect/subscriptions/pages/sign_in/sign_in_gitlab_multiversion/index.vue
@@ -5,10 +5,14 @@ import { s__ } from '~/locale';
import { reloadPage, persistBaseUrl, retrieveBaseUrl } from '~/jira_connect/subscriptions/utils';
import { updateInstallation, setApiBaseURL } from '~/jira_connect/subscriptions/api';
-import { I18N_UPDATE_INSTALLATION_ERROR_MESSAGE } from '~/jira_connect/subscriptions/constants';
+import {
+ GITLAB_COM_BASE_PATH,
+ I18N_UPDATE_INSTALLATION_ERROR_MESSAGE,
+} from '~/jira_connect/subscriptions/constants';
import { SET_ALERT } from '~/jira_connect/subscriptions/store/mutation_types';
import SignInOauthButton from '../../../components/sign_in_oauth_button.vue';
+import SetupInstructions from './setup_instructions.vue';
import VersionSelectForm from './version_select_form.vue';
export default {
@@ -16,12 +20,14 @@ export default {
components: {
GlButton,
SignInOauthButton,
+ SetupInstructions,
VersionSelectForm,
},
data() {
return {
gitlabBasePath: null,
loadingVersionSelect: false,
+ showSetupInstructions: false,
};
},
computed: {
@@ -37,6 +43,9 @@ export default {
mounted() {
this.gitlabBasePath = retrieveBaseUrl();
setApiBaseURL(this.gitlabBasePath);
+ if (this.gitlabBasePath !== GITLAB_COM_BASE_PATH) {
+ this.showSetupInstructions = true;
+ }
},
methods: {
...mapMutations({
@@ -61,6 +70,9 @@ export default {
this.loadingVersionSelect = false;
});
},
+ onSetupNext() {
+ this.showSetupInstructions = false;
+ },
onSignInError() {
this.$emit('error');
},
@@ -88,19 +100,23 @@ export default {
@submit="onVersionSelect"
/>
- <div v-else class="gl-text-center">
- <sign-in-oauth-button
- class="gl-mb-5"
- :gitlab-base-path="gitlabBasePath"
- @sign-in="$emit('sign-in-oauth', $event)"
- @error="onSignInError"
- />
+ <template v-else>
+ <setup-instructions v-if="showSetupInstructions" @next="onSetupNext" />
+
+ <div v-else class="gl-text-center">
+ <sign-in-oauth-button
+ class="gl-mb-5"
+ :gitlab-base-path="gitlabBasePath"
+ @sign-in="$emit('sign-in-oauth', $event)"
+ @error="onSignInError"
+ />
- <div>
- <gl-button category="tertiary" variant="confirm" @click="resetGitlabBasePath">
- {{ $options.i18n.changeVersionButtonText }}
- </gl-button>
+ <div>
+ <gl-button category="tertiary" variant="confirm" @click="resetGitlabBasePath">
+ {{ $options.i18n.changeVersionButtonText }}
+ </gl-button>
+ </div>
</div>
- </div>
+ </template>
</div>
</template>
diff --git a/app/assets/javascripts/jira_connect/subscriptions/pages/sign_in/sign_in_gitlab_multiversion/setup_instructions.vue b/app/assets/javascripts/jira_connect/subscriptions/pages/sign_in/sign_in_gitlab_multiversion/setup_instructions.vue
new file mode 100644
index 00000000000..00fa739b518
--- /dev/null
+++ b/app/assets/javascripts/jira_connect/subscriptions/pages/sign_in/sign_in_gitlab_multiversion/setup_instructions.vue
@@ -0,0 +1,35 @@
+<script>
+import { GlButton, GlLink } from '@gitlab/ui';
+import { OAUTH_SELF_MANAGED_DOC_LINK } from '~/jira_connect/subscriptions/constants';
+
+export default {
+ components: {
+ GlButton,
+ GlLink,
+ },
+ OAUTH_SELF_MANAGED_DOC_LINK,
+};
+</script>
+
+<template>
+ <div class="gl-max-w-62 gl-mx-auto gl-mt-7">
+ <h3>{{ s__('JiraService|Continue setup in GitLab') }}</h3>
+ <p>
+ {{
+ s__(
+ 'JiraService|In order to complete the set up, you’ll need to complete a few steps in GitLab.',
+ )
+ }}
+ <gl-link
+ class="gl-reset-font-size!"
+ :href="$options.OAUTH_SELF_MANAGED_DOC_LINK"
+ target="_blank"
+ >{{ __('Learn more') }}</gl-link
+ >
+ </p>
+
+ <gl-button variant="confirm" @click="$emit('next')">
+ {{ __('Next') }}
+ </gl-button>
+ </div>
+</template>
diff --git a/app/assets/javascripts/jira_connect/subscriptions/pages/sign_in/sign_in_gitlab_multiversion/version_select_form.vue b/app/assets/javascripts/jira_connect/subscriptions/pages/sign_in/sign_in_gitlab_multiversion/version_select_form.vue
index 6b32225ed11..37a65946b3f 100644
--- a/app/assets/javascripts/jira_connect/subscriptions/pages/sign_in/sign_in_gitlab_multiversion/version_select_form.vue
+++ b/app/assets/javascripts/jira_connect/subscriptions/pages/sign_in/sign_in_gitlab_multiversion/version_select_form.vue
@@ -55,7 +55,6 @@ export default {
},
radioOptions: RADIO_OPTIONS,
i18n: {
- title: s__('JiraService|Welcome to GitLab for Jira'),
saasRadioLabel: __('GitLab.com (SaaS)'),
saasRadioHelp: __('Most common'),
selfManagedRadioLabel: __('GitLab (self-managed)'),
diff --git a/app/assets/javascripts/jobs/components/filtered_search/jobs_filtered_search.vue b/app/assets/javascripts/jobs/components/filtered_search/jobs_filtered_search.vue
index e498a735898..67cdca6aa0a 100644
--- a/app/assets/javascripts/jobs/components/filtered_search/jobs_filtered_search.vue
+++ b/app/assets/javascripts/jobs/components/filtered_search/jobs_filtered_search.vue
@@ -1,13 +1,13 @@
<script>
import { GlFilteredSearch } from '@gitlab/ui';
-import { s__ } from '~/locale';
-import { OPERATOR_IS_ONLY } from '~/vue_shared/components/filtered_search_bar/constants';
+import {
+ OPERATORS_IS,
+ TOKEN_TITLE_STATUS,
+ TOKEN_TYPE_STATUS,
+} from '~/vue_shared/components/filtered_search_bar/constants';
import JobStatusToken from './tokens/job_status_token.vue';
export default {
- tokenTypes: {
- status: 'status',
- },
components: {
GlFilteredSearch,
},
@@ -22,12 +22,12 @@ export default {
tokens() {
return [
{
- type: this.$options.tokenTypes.status,
+ type: TOKEN_TYPE_STATUS,
icon: 'status',
- title: s__('Jobs|Status'),
+ title: TOKEN_TITLE_STATUS,
unique: true,
token: JobStatusToken,
- operators: OPERATOR_IS_ONLY,
+ operators: OPERATORS_IS,
},
];
},
@@ -35,7 +35,7 @@ export default {
if (this.queryString?.statuses) {
return [
{
- type: 'status',
+ type: TOKEN_TYPE_STATUS,
value: {
data: this.queryString?.statuses,
operator: '=',
diff --git a/app/assets/javascripts/jobs/components/job/empty_state.vue b/app/assets/javascripts/jobs/components/job/empty_state.vue
index 65b9600e664..d0a39025807 100644
--- a/app/assets/javascripts/jobs/components/job/empty_state.vue
+++ b/app/assets/javascripts/jobs/components/job/empty_state.vue
@@ -1,16 +1,12 @@
<script>
import { GlLink } from '@gitlab/ui';
-import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
-import LegacyManualVariablesForm from '~/jobs/components/job/legacy_manual_variables_form.vue';
import ManualVariablesForm from '~/jobs/components/job/manual_variables_form.vue';
export default {
components: {
GlLink,
- LegacyManualVariablesForm,
ManualVariablesForm,
},
- mixins: [glFeatureFlagsMixin()],
props: {
illustrationPath: {
type: String,
@@ -20,6 +16,14 @@ export default {
type: String,
required: true,
},
+ isRetryable: {
+ type: Boolean,
+ required: true,
+ },
+ jobId: {
+ type: Number,
+ required: true,
+ },
title: {
type: String,
required: true,
@@ -54,9 +58,6 @@ export default {
},
},
computed: {
- isGraphQL() {
- return this.glFeatures?.graphqlJobApp;
- },
shouldRenderManualVariables() {
return this.playable && !this.scheduled;
},
@@ -77,14 +78,14 @@ export default {
<p v-if="content" data-testid="job-empty-state-content">{{ content }}</p>
</div>
- <template v-if="isGraphQL">
- <manual-variables-form v-if="shouldRenderManualVariables" :action="action" />
- </template>
- <template v-else>
- <legacy-manual-variables-form v-if="shouldRenderManualVariables" :action="action" />
- </template>
- <div class="text-content">
- <div v-if="action && !shouldRenderManualVariables" class="text-center">
+ <manual-variables-form
+ v-if="shouldRenderManualVariables"
+ :is-retryable="isRetryable"
+ :job-id="jobId"
+ @hideManualVariablesForm="$emit('hideManualVariablesForm')"
+ />
+ <div v-if="action && !shouldRenderManualVariables" class="text-content">
+ <div class="text-center">
<gl-link
:href="action.path"
:data-method="action.method"
diff --git a/app/assets/javascripts/jobs/components/job/graphql/mutations/job_retry_with_variables.mutation.graphql b/app/assets/javascripts/jobs/components/job/graphql/mutations/job_retry_with_variables.mutation.graphql
new file mode 100644
index 00000000000..2b79892a072
--- /dev/null
+++ b/app/assets/javascripts/jobs/components/job/graphql/mutations/job_retry_with_variables.mutation.graphql
@@ -0,0 +1,16 @@
+mutation retryJobWithVariables($id: CiBuildID!, $variables: [CiVariableInput!]) {
+ jobRetry(input: { id: $id, variables: $variables }) {
+ job {
+ id
+ manualVariables {
+ nodes {
+ id
+ key
+ value
+ }
+ }
+ webPath
+ }
+ errors
+ }
+}
diff --git a/app/assets/javascripts/jobs/components/job/graphql/queries/get_job.query.graphql b/app/assets/javascripts/jobs/components/job/graphql/queries/get_job.query.graphql
new file mode 100644
index 00000000000..aaf1dec8e0f
--- /dev/null
+++ b/app/assets/javascripts/jobs/components/job/graphql/queries/get_job.query.graphql
@@ -0,0 +1,17 @@
+query getJob($fullPath: ID!, $id: JobID!) {
+ project(fullPath: $fullPath) {
+ id
+ job(id: $id) {
+ id
+ manualJob
+ manualVariables {
+ nodes {
+ id
+ key
+ value
+ }
+ }
+ name
+ }
+ }
+}
diff --git a/app/assets/javascripts/jobs/components/job/job_app.vue b/app/assets/javascripts/jobs/components/job/job_app.vue
index 81b65d175a7..c6d900ef13e 100644
--- a/app/assets/javascripts/jobs/components/job/job_app.vue
+++ b/app/assets/javascripts/jobs/components/job/job_app.vue
@@ -1,8 +1,9 @@
<script>
-import { GlLoadingIcon, GlIcon, GlSafeHtmlDirective as SafeHtml, GlAlert } from '@gitlab/ui';
+import { GlLoadingIcon, GlIcon, GlAlert } from '@gitlab/ui';
import { GlBreakpointInstance as bp } from '@gitlab/ui/dist/utils';
import { throttle, isEmpty } from 'lodash';
import { mapGetters, mapState, mapActions } from 'vuex';
+import SafeHtml from '~/vue_shared/directives/safe_html';
import { isScrolledToBottom } from '~/lib/utils/scroll_utils';
import { __, sprintf } from '~/locale';
import CiHeader from '~/vue_shared/components/header_ci_component.vue';
@@ -71,6 +72,7 @@ export default {
data() {
return {
searchResults: [],
+ showUpdateVariablesState: false,
};
},
computed: {
@@ -121,6 +123,10 @@ export default {
return this.shouldRenderCalloutMessage && !this.hasUnmetPrerequisitesFailure;
},
+ isJobRetryable() {
+ return Boolean(this.job.retry_path);
+ },
+
itemName() {
return sprintf(__('Job %{jobName}'), { jobName: this.job.name });
},
@@ -168,10 +174,16 @@ export default {
'toggleScrollButtons',
'toggleScrollAnimation',
]),
+ onHideManualVariablesForm() {
+ this.showUpdateVariablesState = false;
+ },
onResize() {
this.updateSidebar();
this.updateScroll();
},
+ onUpdateVariables() {
+ this.showUpdateVariablesState = true;
+ },
updateSidebar() {
const breakpoint = bp.getBreakpointSize();
if (breakpoint === 'xs' || breakpoint === 'sm') {
@@ -271,14 +283,12 @@ export default {
</div>
<!-- job log -->
<div
- v-if="hasJobLog"
+ v-if="hasJobLog && !showUpdateVariablesState"
class="build-log-container gl-relative"
:class="{ 'gl-mt-3': !job.archived }"
>
<log-top-bar
:class="{
- 'sidebar-expanded': isSidebarOpen,
- 'sidebar-collapsed': !isSidebarOpen,
'has-archived-block': job.archived,
}"
:size="jobLogSize"
@@ -299,14 +309,17 @@ export default {
<!-- empty state -->
<empty-state
- v-if="!hasJobLog"
+ v-if="!hasJobLog || showUpdateVariablesState"
:illustration-path="emptyStateIllustration.image"
:illustration-size-class="emptyStateIllustration.size"
+ :is-retryable="isJobRetryable"
+ :job-id="job.id"
:title="emptyStateTitle"
:content="emptyStateIllustration.content"
:action="emptyStateAction"
:playable="job.playable"
:scheduled="job.scheduled"
+ @hideManualVariablesForm="onHideManualVariablesForm()"
/>
<!-- EO empty state -->
@@ -320,9 +333,9 @@ export default {
'right-sidebar-expanded': isSidebarOpen,
'right-sidebar-collapsed': !isSidebarOpen,
}"
- :erase-path="job.erase_path"
:artifact-help-url="artifactHelpUrl"
data-testid="job-sidebar"
+ @updateVariables="onUpdateVariables()"
/>
</div>
</template>
diff --git a/app/assets/javascripts/jobs/components/job/legacy_manual_variables_form.vue b/app/assets/javascripts/jobs/components/job/legacy_manual_variables_form.vue
deleted file mode 100644
index 1898e02c94e..00000000000
--- a/app/assets/javascripts/jobs/components/job/legacy_manual_variables_form.vue
+++ /dev/null
@@ -1,192 +0,0 @@
-<script>
-import {
- GlFormInputGroup,
- GlInputGroupText,
- GlFormInput,
- GlButton,
- GlLink,
- GlSprintf,
-} from '@gitlab/ui';
-import { uniqueId } from 'lodash';
-import { mapActions } from 'vuex';
-import { helpPagePath } from '~/helpers/help_page_helper';
-import { s__ } from '~/locale';
-
-export default {
- name: 'ManualVariablesForm',
- components: {
- GlFormInputGroup,
- GlInputGroupText,
- GlFormInput,
- GlButton,
- GlLink,
- GlSprintf,
- },
- props: {
- action: {
- type: Object,
- required: false,
- default: null,
- validator(value) {
- return (
- value === null ||
- (Object.prototype.hasOwnProperty.call(value, 'path') &&
- Object.prototype.hasOwnProperty.call(value, 'method') &&
- Object.prototype.hasOwnProperty.call(value, 'button_title'))
- );
- },
- },
- },
- inputTypes: {
- key: 'key',
- value: 'value',
- },
- i18n: {
- header: s__('CiVariables|Variables'),
- keyLabel: s__('CiVariables|Key'),
- valueLabel: s__('CiVariables|Value'),
- keyPlaceholder: s__('CiVariables|Input variable key'),
- valuePlaceholder: s__('CiVariables|Input variable value'),
- formHelpText: s__(
- 'CiVariables|Specify variable values to be used in this run. The values specified in %{linkStart}CI/CD settings%{linkEnd} will be used as default',
- ),
- },
- data() {
- return {
- variables: [
- {
- key: '',
- secretValue: '',
- id: uniqueId(),
- },
- ],
- triggerBtnDisabled: false,
- };
- },
- computed: {
- variableSettings() {
- return helpPagePath('ci/variables/index', { anchor: 'add-a-cicd-variable-to-a-project' });
- },
- preparedVariables() {
- // we need to ensure no empty variables are passed to the API
- // and secretValue should be snake_case when passed to the API
- return this.variables
- .filter((variable) => variable.key !== '')
- .map(({ key, secretValue }) => ({ key, secret_value: secretValue }));
- },
- },
- methods: {
- ...mapActions(['triggerManualJob']),
- addEmptyVariable() {
- const lastVar = this.variables[this.variables.length - 1];
-
- if (lastVar.key === '') {
- return;
- }
-
- this.variables.push({
- key: '',
- secret_value: '',
- id: uniqueId(),
- });
- },
- canRemove(index) {
- return index < this.variables.length - 1;
- },
- deleteVariable(id) {
- this.variables.splice(
- this.variables.findIndex((el) => el.id === id),
- 1,
- );
- },
- inputRef(type, id) {
- return `${this.$options.inputTypes[type]}-${id}`;
- },
- trigger() {
- this.triggerBtnDisabled = true;
-
- this.triggerManualJob(this.preparedVariables);
- },
- },
-};
-</script>
-<template>
- <div class="row gl-justify-content-center">
- <div class="col-10" data-testid="manual-vars-form">
- <label>{{ $options.i18n.header }}</label>
-
- <div
- v-for="(variable, index) in variables"
- :key="variable.id"
- class="gl-display-flex gl-align-items-center gl-mb-4"
- data-testid="ci-variable-row"
- >
- <gl-form-input-group class="gl-mr-4 gl-flex-grow-1">
- <template #prepend>
- <gl-input-group-text>
- {{ $options.i18n.keyLabel }}
- </gl-input-group-text>
- </template>
- <gl-form-input
- :ref="inputRef('key', variable.id)"
- v-model="variable.key"
- :placeholder="$options.i18n.keyPlaceholder"
- data-testid="ci-variable-key"
- @change="addEmptyVariable"
- />
- </gl-form-input-group>
-
- <gl-form-input-group class="gl-flex-grow-2">
- <template #prepend>
- <gl-input-group-text>
- {{ $options.i18n.valueLabel }}
- </gl-input-group-text>
- </template>
- <gl-form-input
- :ref="inputRef('value', variable.id)"
- v-model="variable.secretValue"
- :placeholder="$options.i18n.valuePlaceholder"
- data-testid="ci-variable-value"
- />
- </gl-form-input-group>
-
- <gl-button
- v-if="canRemove(index)"
- class="gl-flex-grow-0 gl-flex-basis-0"
- category="tertiary"
- variant="danger"
- icon="clear"
- :aria-label="__('Delete variable')"
- data-testid="delete-variable-btn"
- @click="deleteVariable(variable.id)"
- />
-
- <!-- delete variable button placeholder to not break flex layout -->
- <div v-else class="gl-w-7 gl-mr-3" data-testid="delete-variable-btn-placeholder"></div>
- </div>
-
- <div class="gl-text-center gl-mt-5">
- <gl-sprintf :message="$options.i18n.formHelpText">
- <template #link="{ content }">
- <gl-link :href="variableSettings" target="_blank">
- {{ content }}
- </gl-link>
- </template>
- </gl-sprintf>
- </div>
- <div class="gl-display-flex gl-justify-content-center gl-mt-5">
- <gl-button
- class="gl-mt-5"
- variant="confirm"
- category="primary"
- :aria-label="__('Trigger manual job')"
- :disabled="triggerBtnDisabled"
- data-testid="trigger-manual-job-btn"
- @click="trigger"
- >
- {{ action.button_title }}
- </gl-button>
- </div>
- </div>
- </div>
-</template>
diff --git a/app/assets/javascripts/jobs/components/job/manual_variables_form.vue b/app/assets/javascripts/jobs/components/job/manual_variables_form.vue
index 2f97301979c..d7bbd6daed2 100644
--- a/app/assets/javascripts/jobs/components/job/manual_variables_form.vue
+++ b/app/assets/javascripts/jobs/components/job/manual_variables_form.vue
@@ -5,15 +5,24 @@ import {
GlFormInput,
GlButton,
GlLink,
+ GlLoadingIcon,
GlSprintf,
+ GlTooltipDirective,
} from '@gitlab/ui';
-import { uniqueId } from 'lodash';
+import { cloneDeep, uniqueId } from 'lodash';
import { mapActions } from 'vuex';
+import { fetchPolicies } from '~/lib/graphql';
+import { createAlert } from '~/flash';
+import { convertToGraphQLId } from '~/graphql_shared/utils';
+import { JOB_GRAPHQL_ERRORS, GRAPHQL_ID_TYPES } from '~/jobs/constants';
import { helpPagePath } from '~/helpers/help_page_helper';
+import { redirectTo } from '~/lib/utils/url_utility';
import { s__ } from '~/locale';
+import GetJob from './graphql/queries/get_job.query.graphql';
+import retryJobWithVariablesMutation from './graphql/mutations/job_retry_with_variables.mutation.graphql';
// This component is a port of ~/jobs/components/job/legacy_manual_variables_form.vue
-// It is meant to fetch the job information via GraphQL instead of REST API.
+// It is meant to fetch/update the job information via GraphQL instead of REST API.
export default {
name: 'ManualVariablesForm',
@@ -23,59 +32,93 @@ export default {
GlFormInput,
GlButton,
GlLink,
+ GlLoadingIcon,
GlSprintf,
},
- props: {
- action: {
- type: Object,
- required: false,
- default: null,
- validator(value) {
- return (
- value === null ||
- (Object.prototype.hasOwnProperty.call(value, 'path') &&
- Object.prototype.hasOwnProperty.call(value, 'method') &&
- Object.prototype.hasOwnProperty.call(value, 'button_title'))
- );
+ directives: {
+ GlTooltip: GlTooltipDirective,
+ },
+ inject: ['projectPath'],
+ apollo: {
+ variables: {
+ query: GetJob,
+ variables() {
+ return {
+ fullPath: this.projectPath,
+ id: convertToGraphQLId(GRAPHQL_ID_TYPES.commitStatus, this.jobId),
+ };
+ },
+ fetchPolicy: fetchPolicies.CACHE_AND_NETWORK,
+ update(data) {
+ const jobVariables = cloneDeep(data?.project?.job?.manualVariables?.nodes);
+ return [...jobVariables.reverse(), ...this.variables];
+ },
+ error() {
+ createAlert({ message: JOB_GRAPHQL_ERRORS.jobQueryErrorText });
},
},
},
+ props: {
+ isRetryable: {
+ type: Boolean,
+ required: true,
+ },
+ jobId: {
+ type: Number,
+ required: true,
+ },
+ },
inputTypes: {
key: 'key',
value: 'value',
},
i18n: {
+ clearInputs: s__('CiVariables|Clear inputs'),
+ formHelpText: s__(
+ 'CiVariables|Specify variable values to be used in this run. The values specified in %{linkStart}CI/CD settings%{linkEnd} will be used as default',
+ ),
header: s__('CiVariables|Variables'),
keyLabel: s__('CiVariables|Key'),
- valueLabel: s__('CiVariables|Value'),
keyPlaceholder: s__('CiVariables|Input variable key'),
+ runAgainButtonText: s__('CiVariables|Run job again'),
+ triggerButtonText: s__('CiVariables|Trigger this manual action'),
+ valueLabel: s__('CiVariables|Value'),
valuePlaceholder: s__('CiVariables|Input variable value'),
- formHelpText: s__(
- 'CiVariables|Specify variable values to be used in this run. The values specified in %{linkStart}CI/CD settings%{linkEnd} will be used as default',
- ),
+ },
+ variableValueKeys: {
+ rest: 'secret_value',
+ gql: 'value',
},
data() {
return {
+ job: {},
variables: [
{
- key: '',
- secretValue: '',
id: uniqueId(),
+ key: '',
+ value: '',
},
],
+ runAgainBtnDisabled: false,
triggerBtnDisabled: false,
};
},
computed: {
- variableSettings() {
- return helpPagePath('ci/variables/index', { anchor: 'add-a-cicd-variable-to-a-project' });
- },
preparedVariables() {
- // we need to ensure no empty variables are passed to the API
- // and secretValue should be snake_case when passed to the API
+ // filtering out 'id' along with empty variables to send only key, value in the mutation.
+ // This will be removed in: https://gitlab.com/gitlab-org/gitlab/-/issues/377268
+
return this.variables
.filter((variable) => variable.key !== '')
- .map(({ key, secretValue }) => ({ key, secret_value: secretValue }));
+ .map(({ key, value }) => ({ key, [this.valueKey]: value }));
+ },
+ valueKey() {
+ return this.isRetryable
+ ? this.$options.variableValueKeys.gql
+ : this.$options.variableValueKeys.rest;
+ },
+ variableSettings() {
+ return helpPagePath('ci/variables/index', { anchor: 'add-a-cicd-variable-to-a-project' });
},
},
methods: {
@@ -88,9 +131,9 @@ export default {
}
this.variables.push({
- key: '',
- secret_value: '',
id: uniqueId(),
+ key: '',
+ value: '',
});
},
canRemove(index) {
@@ -105,7 +148,34 @@ export default {
inputRef(type, id) {
return `${this.$options.inputTypes[type]}-${id}`;
},
- trigger() {
+ navigateToRetriedJob(retryPath) {
+ redirectTo(retryPath);
+ },
+ async retryJob() {
+ try {
+ const { data } = await this.$apollo.mutate({
+ mutation: retryJobWithVariablesMutation,
+ variables: {
+ id: convertToGraphQLId(GRAPHQL_ID_TYPES.ciBuild, this.jobId),
+ // we need to ensure no empty variables are passed to the API
+ variables: this.preparedVariables,
+ },
+ });
+ if (data.jobRetry?.errors?.length) {
+ createAlert({ message: data.jobRetry.errors[0] });
+ } else {
+ this.navigateToRetriedJob(data.jobRetry?.job?.webPath);
+ }
+ } catch (error) {
+ createAlert({ message: JOB_GRAPHQL_ERRORS.retryMutationErrorText });
+ }
+ },
+ runAgain() {
+ this.runAgainBtnDisabled = true;
+
+ this.retryJob();
+ },
+ triggerJob() {
this.triggerBtnDisabled = true;
this.triggerManualJob(this.preparedVariables);
@@ -114,7 +184,8 @@ export default {
};
</script>
<template>
- <div class="row gl-justify-content-center">
+ <gl-loading-icon v-if="$apollo.queries.variables.loading" class="gl-mt-9" size="lg" />
+ <div v-else class="row gl-justify-content-center">
<div class="col-10" data-testid="manual-vars-form">
<label>{{ $options.i18n.header }}</label>
@@ -147,7 +218,7 @@ export default {
</template>
<gl-form-input
:ref="inputRef('value', variable.id)"
- v-model="variable.secretValue"
+ v-model="variable.value"
:placeholder="$options.i18n.valuePlaceholder"
data-testid="ci-variable-value"
/>
@@ -155,11 +226,13 @@ export default {
<gl-button
v-if="canRemove(index)"
+ v-gl-tooltip
+ :aria-label="$options.i18n.clearInputs"
+ :title="$options.i18n.clearInputs"
class="gl-flex-grow-0 gl-flex-basis-0"
category="tertiary"
variant="danger"
icon="clear"
- :aria-label="__('Delete variable')"
data-testid="delete-variable-btn"
@click="deleteVariable(variable.id)"
/>
@@ -177,7 +250,27 @@ export default {
</template>
</gl-sprintf>
</div>
- <div class="gl-display-flex gl-justify-content-center gl-mt-5">
+ <div v-if="isRetryable" class="gl-display-flex gl-justify-content-center gl-mt-5">
+ <gl-button
+ class="gl-mt-5"
+ :aria-label="__('Cancel')"
+ data-testid="cancel-btn"
+ @click="$emit('hideManualVariablesForm')"
+ >{{ __('Cancel') }}</gl-button
+ >
+ <gl-button
+ class="gl-mt-5"
+ variant="confirm"
+ category="primary"
+ :aria-label="__('Run manual job again')"
+ :disabled="runAgainBtnDisabled"
+ data-testid="run-manual-job-btn"
+ @click="runAgain"
+ >
+ {{ $options.i18n.runAgainButtonText }}
+ </gl-button>
+ </div>
+ <div v-else class="gl-display-flex gl-justify-content-center gl-mt-5">
<gl-button
class="gl-mt-5"
variant="confirm"
@@ -185,9 +278,9 @@ export default {
:aria-label="__('Trigger manual job')"
:disabled="triggerBtnDisabled"
data-testid="trigger-manual-job-btn"
- @click="trigger"
+ @click="triggerJob"
>
- {{ action.button_title }}
+ {{ $options.i18n.triggerButtonText }}
</gl-button>
</div>
</div>
diff --git a/app/assets/javascripts/jobs/components/job/sidebar/job_sidebar_retry_button.vue b/app/assets/javascripts/jobs/components/job/sidebar/job_sidebar_retry_button.vue
index dd620977f0c..7183a8b5d03 100644
--- a/app/assets/javascripts/jobs/components/job/sidebar/job_sidebar_retry_button.vue
+++ b/app/assets/javascripts/jobs/components/job/sidebar/job_sidebar_retry_button.vue
@@ -1,15 +1,17 @@
<script>
-import { GlButton, GlModalDirective } from '@gitlab/ui';
+import { GlButton, GlDropdown, GlDropdownItem, GlModalDirective } from '@gitlab/ui';
import { mapGetters } from 'vuex';
import { JOB_SIDEBAR_COPY } from '~/jobs/constants';
export default {
name: 'JobSidebarRetryButton',
i18n: {
- retryLabel: JOB_SIDEBAR_COPY.retry,
+ ...JOB_SIDEBAR_COPY,
},
components: {
GlButton,
+ GlDropdown,
+ GlDropdownItem,
},
directives: {
GlModal: GlModalDirective,
@@ -23,6 +25,10 @@ export default {
type: String,
required: true,
},
+ isManualJob: {
+ type: Boolean,
+ required: true,
+ },
},
computed: {
...mapGetters(['hasForwardDeploymentFailure']),
@@ -33,17 +39,30 @@ export default {
<gl-button
v-if="hasForwardDeploymentFailure"
v-gl-modal="modalId"
- :aria-label="$options.i18n.retryLabel"
+ :aria-label="$options.i18n.retryJobLabel"
category="primary"
variant="confirm"
icon="retry"
data-testid="retry-job-button"
/>
-
+ <gl-dropdown
+ v-else-if="isManualJob"
+ icon="retry"
+ category="primary"
+ :right="true"
+ variant="confirm"
+ >
+ <gl-dropdown-item :href="href" data-method="post">
+ {{ $options.i18n.runAgainJobButtonLabel }}
+ </gl-dropdown-item>
+ <gl-dropdown-item @click="$emit('updateVariablesClicked')">
+ {{ $options.i18n.updateVariables }}
+ </gl-dropdown-item>
+ </gl-dropdown>
<gl-button
v-else
:href="href"
- :aria-label="$options.i18n.retryLabel"
+ :aria-label="$options.i18n.retryJobLabel"
category="primary"
variant="confirm"
icon="retry"
diff --git a/app/assets/javascripts/jobs/components/job/sidebar/legacy_sidebar_header.vue b/app/assets/javascripts/jobs/components/job/sidebar/legacy_sidebar_header.vue
deleted file mode 100644
index 64b497c3550..00000000000
--- a/app/assets/javascripts/jobs/components/job/sidebar/legacy_sidebar_header.vue
+++ /dev/null
@@ -1,104 +0,0 @@
-<script>
-import { GlButton, GlTooltipDirective } from '@gitlab/ui';
-import { mapActions } from 'vuex';
-import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate/tooltip_on_truncate.vue';
-import { JOB_SIDEBAR_COPY, forwardDeploymentFailureModalId } from '~/jobs/constants';
-import JobSidebarRetryButton from './job_sidebar_retry_button.vue';
-
-export default {
- name: 'LegacySidebarHeader',
- i18n: {
- ...JOB_SIDEBAR_COPY,
- },
- forwardDeploymentFailureModalId,
- directives: {
- GlTooltip: GlTooltipDirective,
- },
- components: {
- GlButton,
- JobSidebarRetryButton,
- TooltipOnTruncate,
- },
- props: {
- job: {
- type: Object,
- required: true,
- default: () => ({}),
- },
- erasePath: {
- type: String,
- required: false,
- default: null,
- },
- },
- computed: {
- retryButtonCategory() {
- return this.job.status && this.job.recoverable ? 'primary' : 'secondary';
- },
- buttonTitle() {
- return this.job.status && this.job.status.text === 'passed'
- ? this.$options.i18n.runAgainJobButtonLabel
- : this.$options.i18n.retryJobButtonLabel;
- },
- },
- methods: {
- ...mapActions(['toggleSidebar']),
- },
-};
-</script>
-
-<template>
- <div class="gl-py-5 gl-display-flex gl-align-items-center">
- <tooltip-on-truncate :title="job.name" truncate-target="child"
- ><h4 class="gl-my-0 gl-mr-3 gl-text-truncate">
- {{ job.name }}
- </h4>
- </tooltip-on-truncate>
- <div class="gl-flex-grow-1 gl-flex-shrink-0 gl-text-right">
- <gl-button
- v-if="erasePath"
- v-gl-tooltip.left
- :title="$options.i18n.eraseLogButtonLabel"
- :aria-label="$options.i18n.eraseLogButtonLabel"
- :href="erasePath"
- :data-confirm="$options.i18n.eraseLogConfirmText"
- class="gl-mr-2"
- data-testid="job-log-erase-link"
- data-confirm-btn-variant="danger"
- data-method="post"
- icon="remove"
- />
- <job-sidebar-retry-button
- v-if="job.retry_path"
- v-gl-tooltip.left
- :title="buttonTitle"
- :aria-label="buttonTitle"
- :category="retryButtonCategory"
- :href="job.retry_path"
- :modal-id="$options.forwardDeploymentFailureModalId"
- variant="confirm"
- data-qa-selector="retry_button"
- data-testid="retry-button"
- />
- <gl-button
- v-if="job.cancel_path"
- v-gl-tooltip.left
- :title="$options.i18n.cancelJobButtonLabel"
- :aria-label="$options.i18n.cancelJobButtonLabel"
- :href="job.cancel_path"
- variant="danger"
- icon="cancel"
- data-method="post"
- data-testid="cancel-button"
- rel="nofollow"
- />
- <gl-button
- :aria-label="$options.i18n.toggleSidebar"
- category="tertiary"
- class="gl-md-display-none gl-ml-2"
- icon="chevron-double-lg-right"
- @click="toggleSidebar"
- />
- </div>
- </div>
-</template>
diff --git a/app/assets/javascripts/jobs/components/job/sidebar/sidebar.vue b/app/assets/javascripts/jobs/components/job/sidebar/sidebar.vue
index aac6a0ad6d3..69271cc9022 100644
--- a/app/assets/javascripts/jobs/components/job/sidebar/sidebar.vue
+++ b/app/assets/javascripts/jobs/components/job/sidebar/sidebar.vue
@@ -2,14 +2,12 @@
import { GlButton, GlIcon } from '@gitlab/ui';
import { isEmpty } from 'lodash';
import { mapActions, mapGetters, mapState } from 'vuex';
-import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { JOB_SIDEBAR_COPY, forwardDeploymentFailureModalId } from '~/jobs/constants';
+import ArtifactsBlock from './artifacts_block.vue';
import CommitBlock from './commit_block.vue';
import JobsContainer from './jobs_container.vue';
import JobRetryForwardDeploymentModal from './job_retry_forward_deployment_modal.vue';
import JobSidebarDetailsContainer from './sidebar_job_details_container.vue';
-import ArtifactsBlock from './artifacts_block.vue';
-import LegacySidebarHeader from './legacy_sidebar_header.vue';
import SidebarHeader from './sidebar_header.vue';
import StagesDropdown from './stages_dropdown.vue';
import TriggerBlock from './trigger_block.vue';
@@ -29,23 +27,16 @@ export default {
JobsContainer,
JobRetryForwardDeploymentModal,
JobSidebarDetailsContainer,
- LegacySidebarHeader,
SidebarHeader,
StagesDropdown,
TriggerBlock,
},
- mixins: [glFeatureFlagsMixin()],
props: {
artifactHelpUrl: {
type: String,
required: false,
default: '',
},
- erasePath: {
- type: String,
- required: false,
- default: null,
- },
},
computed: {
...mapGetters(['hasForwardDeploymentFailure']),
@@ -57,9 +48,6 @@ export default {
hasTriggers() {
return !isEmpty(this.job.trigger);
},
- isGraphQL() {
- return this.glFeatures?.graphqlJobApp;
- },
commit() {
return this.job?.pipeline?.commit || {};
},
@@ -89,8 +77,11 @@ export default {
<aside class="right-sidebar build-sidebar" data-offset-top="101" data-spy="affix">
<div class="sidebar-container">
<div class="blocks-container">
- <sidebar-header v-if="isGraphQL" :erase-path="erasePath" :job="job" />
- <legacy-sidebar-header v-else :erase-path="erasePath" :job="job" />
+ <sidebar-header
+ :rest-job="job"
+ :job-id="job.id"
+ @updateVariables="$emit('updateVariables')"
+ />
<div
v-if="job.terminal_path || job.new_issue_path"
class="gl-py-5"
diff --git a/app/assets/javascripts/jobs/components/job/sidebar/sidebar_header.vue b/app/assets/javascripts/jobs/components/job/sidebar/sidebar_header.vue
index 523710598bf..40aec0b0536 100644
--- a/app/assets/javascripts/jobs/components/job/sidebar/sidebar_header.vue
+++ b/app/assets/javascripts/jobs/components/job/sidebar/sidebar_header.vue
@@ -1,13 +1,19 @@
<script>
import { GlButton, GlTooltipDirective } from '@gitlab/ui';
import { mapActions } from 'vuex';
+import { createAlert } from '~/flash';
+import { convertToGraphQLId } from '~/graphql_shared/utils';
import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate/tooltip_on_truncate.vue';
-import { JOB_SIDEBAR_COPY, forwardDeploymentFailureModalId } from '~/jobs/constants';
+import {
+ JOB_GRAPHQL_ERRORS,
+ GRAPHQL_ID_TYPES,
+ JOB_SIDEBAR_COPY,
+ forwardDeploymentFailureModalId,
+ PASSED_STATUS,
+} from '~/jobs/constants';
+import GetJob from '../graphql/queries/get_job.query.graphql';
import JobSidebarRetryButton from './job_sidebar_retry_button.vue';
-// This component is a port of ~/jobs/components/job/sidebar/legacy_sidebar_header.vue
-// It is meant to fetch the job information via GraphQL instead of REST API.
-
export default {
name: 'SidebarHeader',
i18n: {
@@ -22,21 +28,58 @@ export default {
JobSidebarRetryButton,
TooltipOnTruncate,
},
- props: {
+ inject: ['projectPath'],
+ apollo: {
job: {
+ query: GetJob,
+ variables() {
+ return {
+ fullPath: this.projectPath,
+ id: convertToGraphQLId(GRAPHQL_ID_TYPES.commitStatus, this.jobId),
+ };
+ },
+ update(data) {
+ const { name, manualJob } = data?.project?.job || {};
+ return {
+ name,
+ manualJob,
+ };
+ },
+ error() {
+ createAlert({ message: JOB_GRAPHQL_ERRORS.jobQueryErrorText });
+ },
+ },
+ },
+ props: {
+ jobId: {
+ type: Number,
+ required: true,
+ },
+ restJob: {
type: Object,
required: true,
default: () => ({}),
},
- erasePath: {
- type: String,
- required: false,
- default: null,
- },
+ },
+ data() {
+ return {
+ job: {},
+ };
},
computed: {
+ buttonTitle() {
+ return this.restJob.status?.text === PASSED_STATUS
+ ? this.$options.i18n.runAgainJobButtonLabel
+ : this.$options.i18n.retryJobLabel;
+ },
+ canShowJobRetryButton() {
+ return this.restJob.retry_path && !this.$apollo.queries.job.loading;
+ },
+ isManualJob() {
+ return this.job?.manualJob;
+ },
retryButtonCategory() {
- return this.job.status && this.job.recoverable ? 'primary' : 'secondary';
+ return this.restJob.status && this.restJob.recoverable ? 'primary' : 'secondary';
},
},
methods: {
@@ -48,17 +91,15 @@ export default {
<template>
<div class="gl-py-5 gl-display-flex gl-align-items-center">
<tooltip-on-truncate :title="job.name" truncate-target="child"
- ><h4 class="gl-my-0 gl-mr-3 gl-text-truncate">
- {{ job.name }}
- </h4>
+ ><h4 class="gl-my-0 gl-mr-3 gl-text-truncate" data-testid="job-name">{{ job.name }}</h4>
</tooltip-on-truncate>
<div class="gl-flex-grow-1 gl-flex-shrink-0 gl-text-right">
<gl-button
- v-if="erasePath"
+ v-if="restJob.erase_path"
v-gl-tooltip.left
:title="$options.i18n.eraseLogButtonLabel"
:aria-label="$options.i18n.eraseLogButtonLabel"
- :href="erasePath"
+ :href="restJob.erase_path"
:data-confirm="$options.i18n.eraseLogConfirmText"
class="gl-mr-2"
data-testid="job-log-erase-link"
@@ -67,23 +108,25 @@ export default {
icon="remove"
/>
<job-sidebar-retry-button
- v-if="job.retry_path"
+ v-if="canShowJobRetryButton"
v-gl-tooltip.left
- :title="$options.i18n.retryJobButtonLabel"
- :aria-label="$options.i18n.retryJobButtonLabel"
+ :title="buttonTitle"
+ :aria-label="buttonTitle"
+ :is-manual-job="isManualJob"
:category="retryButtonCategory"
- :href="job.retry_path"
+ :href="restJob.retry_path"
:modal-id="$options.forwardDeploymentFailureModalId"
variant="confirm"
data-qa-selector="retry_button"
data-testid="retry-button"
+ @updateVariablesClicked="$emit('updateVariables')"
/>
<gl-button
- v-if="job.cancel_path"
+ v-if="restJob.cancel_path"
v-gl-tooltip.left
:title="$options.i18n.cancelJobButtonLabel"
:aria-label="$options.i18n.cancelJobButtonLabel"
- :href="job.cancel_path"
+ :href="restJob.cancel_path"
variant="danger"
icon="cancel"
data-method="post"
diff --git a/app/assets/javascripts/jobs/components/job/sidebar/sidebar_job_details_container.vue b/app/assets/javascripts/jobs/components/job/sidebar/sidebar_job_details_container.vue
index 3b1509e5be5..8300a22cb67 100644
--- a/app/assets/javascripts/jobs/components/job/sidebar/sidebar_job_details_container.vue
+++ b/app/assets/javascripts/jobs/components/job/sidebar/sidebar_job_details_container.vue
@@ -1,6 +1,7 @@
<script>
import { mapState } from 'vuex';
import { GlBadge } from '@gitlab/ui';
+import { helpPagePath } from '~/helpers/help_page_helper';
import { timeIntervalInWords } from '~/lib/utils/datetime_utility';
import { __, sprintf } from '~/locale';
import timeagoMixin from '~/vue_shared/mixins/timeago';
@@ -79,7 +80,9 @@ export default {
TAGS: __('Tags:'),
TIMEOUT: __('Timeout'),
},
- RUNNER_HELP_URL: 'https://docs.gitlab.com/runner/register/index.html',
+ TIMEOUT_HELP_URL: helpPagePath('/ci/pipelines/settings.md', {
+ anchor: 'set-a-limit-for-how-long-jobs-can-run',
+ }),
};
</script>
@@ -96,7 +99,7 @@ export default {
<detail-row v-if="job.queued_duration" :value="queuedDuration" :title="$options.i18n.QUEUED" />
<detail-row
v-if="hasTimeout"
- :help-url="$options.RUNNER_HELP_URL"
+ :help-url="$options.TIMEOUT_HELP_URL"
:value="timeout"
data-testid="job-timeout"
:title="$options.i18n.TIMEOUT"
diff --git a/app/assets/javascripts/jobs/components/job/sidebar/trigger_block.vue b/app/assets/javascripts/jobs/components/job/sidebar/trigger_block.vue
index 1afc1c9a595..c9172fe0322 100644
--- a/app/assets/javascripts/jobs/components/job/sidebar/trigger_block.vue
+++ b/app/assets/javascripts/jobs/components/job/sidebar/trigger_block.vue
@@ -2,9 +2,7 @@
import { GlButton, GlTableLite } from '@gitlab/ui';
import { __ } from '~/locale';
-const DEFAULT_TD_CLASSES = 'gl-w-half gl-font-sm! gl-border-gray-200!';
-const DEFAULT_TH_CLASSES =
- 'gl-bg-transparent! gl-border-b-solid! gl-border-b-gray-200! gl-border-b-1!';
+const DEFAULT_TD_CLASSES = 'gl-font-sm!';
export default {
fields: [
@@ -13,14 +11,12 @@ export default {
label: __('Key'),
tdAttr: { 'data-testid': 'trigger-build-key' },
tdClass: DEFAULT_TD_CLASSES,
- thClass: DEFAULT_TH_CLASSES,
},
{
key: 'value',
label: __('Value'),
tdAttr: { 'data-testid': 'trigger-build-value' },
tdClass: DEFAULT_TD_CLASSES,
- thClass: DEFAULT_TH_CLASSES,
},
],
components: {
diff --git a/app/assets/javascripts/jobs/constants.js b/app/assets/javascripts/jobs/constants.js
index e9475994e8b..405aea11181 100644
--- a/app/assets/javascripts/jobs/constants.js
+++ b/app/assets/javascripts/jobs/constants.js
@@ -5,6 +5,11 @@ const moreInfo = __('More information');
export const forwardDeploymentFailureModalId = 'forward-deployment-failure';
+export const GRAPHQL_ID_TYPES = {
+ commitStatus: 'CommitStatus',
+ ciBuild: 'Ci::Build',
+};
+
export const JOB_SIDEBAR_COPY = {
cancel,
cancelJobButtonLabel: s__('Job|Cancel'),
@@ -12,10 +17,15 @@ export const JOB_SIDEBAR_COPY = {
eraseLogButtonLabel: s__('Job|Erase job log and artifacts'),
eraseLogConfirmText: s__('Job|Are you sure you want to erase this job log and artifacts?'),
newIssue: __('New issue'),
- retry: __('Retry'),
- retryJobButtonLabel: s__('Job|Retry'),
+ retryJobLabel: s__('Job|Retry'),
toggleSidebar: __('Toggle Sidebar'),
runAgainJobButtonLabel: s__('Job|Run again'),
+ updateVariables: s__('Job|Update CI/CD variables'),
+};
+
+export const JOB_GRAPHQL_ERRORS = {
+ retryMutationErrorText: __('There was an error running the job. Please try again.'),
+ jobQueryErrorText: __('There was an error fetching the job.'),
};
export const JOB_RETRY_FORWARD_DEPLOYMENT_MODAL = {
@@ -31,3 +41,4 @@ export const JOB_RETRY_FORWARD_DEPLOYMENT_MODAL = {
};
export const SUCCESS_STATUS = 'SUCCESS';
+export const PASSED_STATUS = 'passed';
diff --git a/app/assets/javascripts/jobs/index.js b/app/assets/javascripts/jobs/index.js
index 9dd47f4046c..44bb1ffb1bc 100644
--- a/app/assets/javascripts/jobs/index.js
+++ b/app/assets/javascripts/jobs/index.js
@@ -1,10 +1,17 @@
import { GlToast } from '@gitlab/ui';
import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+import createDefaultClient from '~/lib/graphql';
import JobApp from './components/job/job_app.vue';
import createStore from './store';
+Vue.use(VueApollo);
Vue.use(GlToast);
+const apolloProvider = new VueApollo({
+ defaultClient: createDefaultClient(),
+});
+
const initializeJobPage = (element) => {
const store = createStore();
@@ -26,11 +33,13 @@ const initializeJobPage = (element) => {
return new Vue({
el: element,
+ apolloProvider,
store,
components: {
JobApp,
},
provide: {
+ projectPath,
retryOutdatedJobDocsUrl,
},
render(createElement) {
diff --git a/app/assets/javascripts/labels/labels_select.js b/app/assets/javascripts/labels/labels_select.js
index 65dda804a20..515b0a79a03 100644
--- a/app/assets/javascripts/labels/labels_select.js
+++ b/app/assets/javascripts/labels/labels_select.js
@@ -4,7 +4,7 @@
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 IssuableBulkUpdateActions from '~/issuable/issuable_bulk_update_actions';
import { isScopedLabel } from '~/lib/utils/common_utils';
import { createAlert } from '~/flash';
import axios from '~/lib/utils/axios_utils';
diff --git a/app/assets/javascripts/language_switcher/components/app.vue b/app/assets/javascripts/language_switcher/components/app.vue
new file mode 100644
index 00000000000..71babe6c614
--- /dev/null
+++ b/app/assets/javascripts/language_switcher/components/app.vue
@@ -0,0 +1,49 @@
+<script>
+import { GlCollapsibleListbox } from '@gitlab/ui';
+import { setCookie } from '~/lib/utils/common_utils';
+import { PREFERRED_LANGUAGE_COOKIE_KEY } from '../constants';
+
+export default {
+ components: {
+ GlCollapsibleListbox,
+ },
+ inject: {
+ locales: {
+ default: [],
+ },
+ preferredLocale: {
+ default: {},
+ },
+ },
+ data() {
+ return {
+ selected: this.preferredLocale.value,
+ };
+ },
+ methods: {
+ onLanguageSelected(code) {
+ setCookie(PREFERRED_LANGUAGE_COOKIE_KEY, code);
+ window.location.reload();
+ },
+ },
+};
+</script>
+<template>
+ <gl-collapsible-listbox
+ v-model="selected"
+ :toggle-text="preferredLocale.text"
+ :items="locales"
+ category="tertiary"
+ right
+ icon="earth"
+ size="small"
+ toggle-class="py-0 gl-h-6"
+ @select="onLanguageSelected"
+ >
+ <template #list-item="{ item: locale }">
+ <span :data-testid="`language_switcher_lang_${locale.value}`">
+ {{ locale.text }}
+ </span>
+ </template>
+ </gl-collapsible-listbox>
+</template>
diff --git a/app/assets/javascripts/language_switcher/constants.js b/app/assets/javascripts/language_switcher/constants.js
new file mode 100644
index 00000000000..b5c0613ac01
--- /dev/null
+++ b/app/assets/javascripts/language_switcher/constants.js
@@ -0,0 +1 @@
+export const PREFERRED_LANGUAGE_COOKIE_KEY = 'preferred_language';
diff --git a/app/assets/javascripts/language_switcher/index.js b/app/assets/javascripts/language_switcher/index.js
new file mode 100644
index 00000000000..b224e2510bb
--- /dev/null
+++ b/app/assets/javascripts/language_switcher/index.js
@@ -0,0 +1,23 @@
+import Vue from 'vue';
+import { getCookie } from '~/lib/utils/common_utils';
+import LanguageSwitcher from './components/app.vue';
+import { PREFERRED_LANGUAGE_COOKIE_KEY } from './constants';
+
+export const initLanguageSwitcher = () => {
+ const el = document.querySelector('.js-language-switcher');
+ if (!el) return false;
+ const locales = JSON.parse(el.dataset.locales);
+ const preferredLangCode = getCookie(PREFERRED_LANGUAGE_COOKIE_KEY);
+ const preferredLocale = locales.find((locale) => locale.value === preferredLangCode);
+
+ return new Vue({
+ el,
+ provide: {
+ locales,
+ preferredLocale,
+ },
+ render(createElement) {
+ return createElement(LanguageSwitcher);
+ },
+ });
+};
diff --git a/app/assets/javascripts/lib/dompurify.js b/app/assets/javascripts/lib/dompurify.js
index 27760e483aa..5372f6555d2 100644
--- a/app/assets/javascripts/lib/dompurify.js
+++ b/app/assets/javascripts/lib/dompurify.js
@@ -18,7 +18,7 @@ export const defaultConfig = {
'data-disable',
'data-turbo',
],
- FORBID_TAGS: ['style', 'mstyle'],
+ FORBID_TAGS: ['style', 'mstyle', 'form'],
ALLOW_UNKNOWN_PROTOCOLS: true,
};
diff --git a/app/assets/javascripts/lib/utils/common_utils.js b/app/assets/javascripts/lib/utils/common_utils.js
index beced4f9144..4ce63d518a6 100644
--- a/app/assets/javascripts/lib/utils/common_utils.js
+++ b/app/assets/javascripts/lib/utils/common_utils.js
@@ -4,9 +4,9 @@
import { GlBreakpointInstance as breakpointInstance } from '@gitlab/ui/dist/utils';
import $ from 'jquery';
-import { isFunction, defer } from 'lodash';
+import { isFunction, defer, escape } from 'lodash';
import Cookies from '~/lib/utils/cookies';
-import { SCOPED_LABEL_DELIMITER } from '~/vue_shared/components/sidebar/labels_select_widget/constants';
+import { SCOPED_LABEL_DELIMITER } from '~/sidebar/components/labels/labels_select_widget/constants';
import { convertToCamelCase, convertToSnakeCase } from './text_utility';
import { isObject } from './type_utility';
import { getLocationHash } from './url_utility';
@@ -28,16 +28,12 @@ export const checkPageAndAction = (page, action) => {
export const isInIncidentPage = () => checkPageAndAction('incidents', 'show');
export const isInIssuePage = () => checkPageAndAction('issues', 'show');
export const isInDesignPage = () => checkPageAndAction('issues', 'designs');
-export const isInMRPage = () => checkPageAndAction('merge_requests', 'show');
+export const isInMRPage = () =>
+ checkPageAndAction('merge_requests', 'show') || checkPageAndAction('merge_requests', 'diffs');
export const isInEpicPage = () => checkPageAndAction('epics', 'show');
export const getDashPath = (path = window.location.pathname) => path.split('/-/')[1] || null;
-export const getCspNonceValue = () => {
- const metaTag = document.querySelector('meta[name=csp-nonce]');
- return metaTag && metaTag.content;
-};
-
export const rstrip = (val) => {
if (val) {
return val.replace(/\s+$/, '');
@@ -469,7 +465,7 @@ export const backOff = (fn, timeout = 60000) => {
export const spriteIcon = (icon, className = '') => {
const classAttribute = className.length > 0 ? `class="${className}"` : '';
- return `<svg ${classAttribute}><use xlink:href="${gon.sprite_icons}#${icon}" /></svg>`;
+ return `<svg ${classAttribute}><use xlink:href="${gon.sprite_icons}#${escape(icon)}" /></svg>`;
};
/**
@@ -715,3 +711,16 @@ export const getFirstPropertyValue = (data) => {
return data[key];
};
+
+// TODO: remove when FF `new_fonts` is removed https://gitlab.com/gitlab-org/gitlab/-/issues/379147
+/**
+ * This method checks the FF `new_fonts`
+ * as well as a query parameter `new_fonts`.
+ * If either of them is enabled, new fonts will be applied.
+ *
+ * @returns Boolean Whether to apply new fonts
+ */
+export const useNewFonts = () => {
+ const hasQueryParam = new URLSearchParams(window.location.search).has('new_fonts');
+ return window?.gon.features?.newFonts || hasQueryParam;
+};
diff --git a/app/assets/javascripts/lib/utils/confirm_via_gl_modal/confirm_modal.vue b/app/assets/javascripts/lib/utils/confirm_via_gl_modal/confirm_modal.vue
index 3788d8ab20c..ea91ccec546 100644
--- a/app/assets/javascripts/lib/utils/confirm_via_gl_modal/confirm_modal.vue
+++ b/app/assets/javascripts/lib/utils/confirm_via_gl_modal/confirm_modal.vue
@@ -1,10 +1,11 @@
<script>
-import { GlModal, GlSafeHtmlDirective } from '@gitlab/ui';
+import { GlModal } from '@gitlab/ui';
+import SafeHtml from '~/vue_shared/directives/safe_html';
import { __ } from '~/locale';
export default {
directives: {
- SafeHtml: GlSafeHtmlDirective,
+ SafeHtml,
},
components: {
GlModal,
diff --git a/app/assets/javascripts/lib/utils/constants.js b/app/assets/javascripts/lib/utils/constants.js
index 379c57f3945..2c8953237cf 100644
--- a/app/assets/javascripts/lib/utils/constants.js
+++ b/app/assets/javascripts/lib/utils/constants.js
@@ -1,6 +1,5 @@
export const BYTES_IN_KIB = 1024;
export const DEFAULT_DEBOUNCE_AND_THROTTLE_MS = 250;
-export const HIDDEN_CLASS = 'hidden';
export const THOUSAND = 1000;
export const TRUNCATE_WIDTH_DEFAULT_WIDTH = 80;
export const TRUNCATE_WIDTH_DEFAULT_FONT_SIZE = 12;
diff --git a/app/assets/javascripts/lib/utils/create_and_submit_form.js b/app/assets/javascripts/lib/utils/create_and_submit_form.js
new file mode 100644
index 00000000000..fce4f898f2f
--- /dev/null
+++ b/app/assets/javascripts/lib/utils/create_and_submit_form.js
@@ -0,0 +1,26 @@
+import csrf from '~/lib/utils/csrf';
+
+export const createAndSubmitForm = ({ url, data }) => {
+ const form = document.createElement('form');
+
+ form.action = url;
+ // For now we only support 'post'.
+ // `form.method` doesn't support other methods so we would need to
+ // use a hidden `_method` input, which is out of scope for now.
+ form.method = 'post';
+ form.style.display = 'none';
+
+ Object.entries(data)
+ .concat([['authenticity_token', csrf.token]])
+ .forEach(([key, value]) => {
+ const input = document.createElement('input');
+ input.type = 'hidden';
+ input.name = key;
+ input.value = value;
+
+ form.appendChild(input);
+ });
+
+ document.body.appendChild(form);
+ form.submit();
+};
diff --git a/app/assets/javascripts/lib/utils/dom_utils.js b/app/assets/javascripts/lib/utils/dom_utils.js
index cafee641174..317c401e404 100644
--- a/app/assets/javascripts/lib/utils/dom_utils.js
+++ b/app/assets/javascripts/lib/utils/dom_utils.js
@@ -118,3 +118,24 @@ export const getContentWrapperHeight = (contentWrapperClass) => {
const wrapperEl = document.querySelector(contentWrapperClass);
return wrapperEl ? `${wrapperEl.offsetTop}px` : '';
};
+
+/**
+ * Replaces comment nodes in a DOM tree with a different element
+ * containing the text of the comment.
+ *
+ * @param {*} el
+ * @param {*} tagName
+ */
+export const replaceCommentsWith = (el, tagName) => {
+ const iterator = document.createNodeIterator(el, NodeFilter.SHOW_COMMENT);
+ let commentNode = iterator.nextNode();
+
+ while (commentNode) {
+ const newNode = document.createElement(tagName);
+ newNode.textContent = commentNode.textContent;
+
+ commentNode.parentNode.replaceChild(newNode, commentNode);
+
+ commentNode = iterator.nextNode();
+ }
+};
diff --git a/app/assets/javascripts/lib/utils/http_status.js b/app/assets/javascripts/lib/utils/http_status.js
index c5190592bb6..ec0d8d433a5 100644
--- a/app/assets/javascripts/lib/utils/http_status.js
+++ b/app/assets/javascripts/lib/utils/http_status.js
@@ -1,45 +1,43 @@
-/**
- * exports HTTP status codes
- */
+export const HTTP_STATUS_ABORTED = 0;
+export const HTTP_STATUS_CREATED = 201;
+export const HTTP_STATUS_ACCEPTED = 202;
+export const HTTP_STATUS_NON_AUTHORITATIVE_INFORMATION = 203;
+export const HTTP_STATUS_NO_CONTENT = 204;
+export const HTTP_STATUS_RESET_CONTENT = 205;
+export const HTTP_STATUS_PARTIAL_CONTENT = 206;
+export const HTTP_STATUS_MULTI_STATUS = 207;
+export const HTTP_STATUS_ALREADY_REPORTED = 208;
+export const HTTP_STATUS_IM_USED = 226;
+export const HTTP_STATUS_METHOD_NOT_ALLOWED = 405;
+export const HTTP_STATUS_CONFLICT = 409;
+export const HTTP_STATUS_GONE = 410;
+export const HTTP_STATUS_PAYLOAD_TOO_LARGE = 413;
+export const HTTP_STATUS_UNPROCESSABLE_ENTITY = 422;
+export const HTTP_STATUS_TOO_MANY_REQUESTS = 429;
+// TODO move the rest of the status codes to primitive constants
+// https://docs.gitlab.com/ee/development/fe_guide/style/javascript.html#export-constants-as-primitives
const httpStatusCodes = {
- ABORTED: 0,
OK: 200,
- CREATED: 201,
- ACCEPTED: 202,
- NON_AUTHORITATIVE_INFORMATION: 203,
- NO_CONTENT: 204,
- RESET_CONTENT: 205,
- PARTIAL_CONTENT: 206,
- MULTI_STATUS: 207,
- ALREADY_REPORTED: 208,
- IM_USED: 226,
- MULTIPLE_CHOICES: 300,
BAD_REQUEST: 400,
UNAUTHORIZED: 401,
FORBIDDEN: 403,
NOT_FOUND: 404,
- METHOD_NOT_ALLOWED: 405,
- CONFLICT: 409,
- GONE: 410,
- PAYLOAD_TOO_LARGE: 413,
- UNPROCESSABLE_ENTITY: 422,
- TOO_MANY_REQUESTS: 429,
INTERNAL_SERVER_ERROR: 500,
SERVICE_UNAVAILABLE: 503,
};
export const successCodes = [
httpStatusCodes.OK,
- httpStatusCodes.CREATED,
- httpStatusCodes.ACCEPTED,
- httpStatusCodes.NON_AUTHORITATIVE_INFORMATION,
- httpStatusCodes.NO_CONTENT,
- httpStatusCodes.RESET_CONTENT,
- httpStatusCodes.PARTIAL_CONTENT,
- httpStatusCodes.MULTI_STATUS,
- httpStatusCodes.ALREADY_REPORTED,
- httpStatusCodes.IM_USED,
+ HTTP_STATUS_CREATED,
+ HTTP_STATUS_ACCEPTED,
+ HTTP_STATUS_NON_AUTHORITATIVE_INFORMATION,
+ HTTP_STATUS_NO_CONTENT,
+ HTTP_STATUS_RESET_CONTENT,
+ HTTP_STATUS_PARTIAL_CONTENT,
+ HTTP_STATUS_MULTI_STATUS,
+ HTTP_STATUS_ALREADY_REPORTED,
+ HTTP_STATUS_IM_USED,
];
export default httpStatusCodes;
diff --git a/app/assets/javascripts/lib/utils/poll.js b/app/assets/javascripts/lib/utils/poll.js
index 71782c9a4ce..73add1e37ee 100644
--- a/app/assets/javascripts/lib/utils/poll.js
+++ b/app/assets/javascripts/lib/utils/poll.js
@@ -1,5 +1,5 @@
import { normalizeHeaders } from './common_utils';
-import httpStatusCodes, { successCodes } from './http_status';
+import { HTTP_STATUS_ABORTED, successCodes } from './http_status';
/**
* Polling utility for handling realtime updates.
@@ -108,7 +108,7 @@ export default class Poll {
})
.catch((error) => {
notificationCallback(false);
- if (error.status === httpStatusCodes.ABORTED) {
+ if (error.status === HTTP_STATUS_ABORTED) {
return;
}
errorCallback(error);
diff --git a/app/assets/javascripts/lib/utils/url_utility.js b/app/assets/javascripts/lib/utils/url_utility.js
index b1a0baf8150..f33484f4192 100644
--- a/app/assets/javascripts/lib/utils/url_utility.js
+++ b/app/assets/javascripts/lib/utils/url_utility.js
@@ -86,7 +86,7 @@ export function cleanLeadingSeparator(path) {
return path.replace(PATH_SEPARATOR_LEADING_REGEX, '');
}
-function cleanEndingSeparator(path) {
+export function cleanEndingSeparator(path) {
return path.replace(PATH_SEPARATOR_ENDING_REGEX, '');
}
diff --git a/app/assets/javascripts/listbox/index.js b/app/assets/javascripts/listbox/index.js
index 7eacbf7fcdd..7e8fc4b637b 100644
--- a/app/assets/javascripts/listbox/index.js
+++ b/app/assets/javascripts/listbox/index.js
@@ -1,4 +1,4 @@
-import { GlListbox } from '@gitlab/ui';
+import { GlCollapsibleListbox } from '@gitlab/ui';
import Vue from 'vue';
import { parseBoolean } from '~/lib/utils/common_utils';
@@ -31,7 +31,7 @@ export function initListbox(el, { onChange } = {}) {
},
},
render(h) {
- return h(GlListbox, {
+ return h(GlCollapsibleListbox, {
props: {
items,
right,
diff --git a/app/assets/javascripts/listbox/redirect_behavior.js b/app/assets/javascripts/listbox/redirect_behavior.js
index 7e0ea2c4dfd..38d9d84f889 100644
--- a/app/assets/javascripts/listbox/redirect_behavior.js
+++ b/app/assets/javascripts/listbox/redirect_behavior.js
@@ -2,7 +2,7 @@ import { initListbox } from '~/listbox';
import { redirectTo } from '~/lib/utils/url_utility';
/**
- * Instantiates GlListbox components with redirect behavior for tags created
+ * Instantiates GlCollapsibleListbox components with redirect behavior for tags created
* with the `gl_redirect_listbox_tag` HAML helper.
*
* NOTE: Do not import this script explicitly. Using `gl_redirect_listbox_tag`
diff --git a/app/assets/javascripts/main.js b/app/assets/javascripts/main.js
index 8e4ebd510aa..df3b55ed2ad 100644
--- a/app/assets/javascripts/main.js
+++ b/app/assets/javascripts/main.js
@@ -37,6 +37,7 @@ import initBroadcastNotifications from './broadcast_notification';
import { initTopNav } from './nav';
import { initCopyCodeButton } from './behaviors/copy_code';
import initHeaderSearch from './header_search/init';
+import initGitlabVersionCheck from './gitlab_version_check';
import 'ee_else_ce/main_ee';
import 'jh_else_ce/main_jh';
@@ -100,21 +101,7 @@ function deferredInitialisation() {
initDefaultTrackers();
initFeatureHighlight();
initCopyCodeButton();
-
- const helpToggle = document.querySelector('.header-help-dropdown-toggle');
- if (helpToggle) {
- helpToggle.addEventListener(
- 'click',
- () => {
- import(/* webpackChunkName: 'versionCheck' */ './gitlab_version_check')
- .then(({ default: initGitlabVersionCheck }) => {
- initGitlabVersionCheck();
- })
- .catch(() => {});
- },
- { once: true },
- );
- }
+ initGitlabVersionCheck();
addSelectOnFocusBehaviour('.js-select-on-focus');
diff --git a/app/assets/javascripts/members/components/avatars/user_avatar.vue b/app/assets/javascripts/members/components/avatars/user_avatar.vue
index ec59f0f681c..4260ee14a14 100644
--- a/app/assets/javascripts/members/components/avatars/user_avatar.vue
+++ b/app/assets/javascripts/members/components/avatars/user_avatar.vue
@@ -1,10 +1,6 @@
<script>
-import {
- GlAvatarLink,
- GlAvatarLabeled,
- GlBadge,
- GlSafeHtmlDirective as SafeHtml,
-} from '@gitlab/ui';
+import { GlAvatarLink, GlAvatarLabeled, GlBadge } from '@gitlab/ui';
+import SafeHtml from '~/vue_shared/directives/safe_html';
import { generateBadges } from 'ee_else_ce/members/utils';
import { glEmojiTag } from '~/emoji';
import { __ } from '~/locale';
diff --git a/app/assets/javascripts/members/components/filter_sort/members_filtered_search_bar.vue b/app/assets/javascripts/members/components/filter_sort/members_filtered_search_bar.vue
index cb7b963b698..76b286f94ad 100644
--- a/app/assets/javascripts/members/components/filter_sort/members_filtered_search_bar.vue
+++ b/app/assets/javascripts/members/components/filter_sort/members_filtered_search_bar.vue
@@ -7,11 +7,11 @@ import {
redirectTo,
} from '~/lib/utils/url_utility';
import {
- SEARCH_TOKEN_TYPE,
SORT_QUERY_PARAM_NAME,
ACTIVE_TAB_QUERY_PARAM_NAME,
AVAILABLE_FILTERED_SEARCH_TOKENS,
} from 'ee_else_ce/members/constants';
+import { FILTERED_SEARCH_TERM } from '~/vue_shared/components/filtered_search_bar/constants';
import FilteredSearchBar from '~/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue';
export default {
@@ -65,7 +65,7 @@ export default {
if (query[this.filteredSearchBar.searchParam]) {
tokens.push({
- type: SEARCH_TOKEN_TYPE,
+ type: FILTERED_SEARCH_TERM,
value: {
data: query[this.filteredSearchBar.searchParam],
},
@@ -83,7 +83,7 @@ export default {
return accumulator;
}
- if (type === SEARCH_TOKEN_TYPE) {
+ if (type === FILTERED_SEARCH_TERM) {
if (value.data !== '') {
const { searchParam } = this.filteredSearchBar;
const { [searchParam]: searchParamValue } = accumulator;
diff --git a/app/assets/javascripts/members/constants.js b/app/assets/javascripts/members/constants.js
index 3135ec602be..dab544c7cbc 100644
--- a/app/assets/javascripts/members/constants.js
+++ b/app/assets/javascripts/members/constants.js
@@ -1,7 +1,7 @@
import { GlFilteredSearchToken } from '@gitlab/ui';
import { __, s__ } from '~/locale';
-import { OPERATOR_IS_ONLY } from '~/vue_shared/components/filtered_search_bar/constants';
+import { OPERATORS_IS } from '~/vue_shared/components/filtered_search_bar/constants';
// Overridden in EE
export const EE_APP_OPTIONS = {};
@@ -117,7 +117,7 @@ export const FILTERED_SEARCH_TOKEN_TWO_FACTOR = {
title: s__('Members|2FA'),
token: GlFilteredSearchToken,
unique: true,
- operators: OPERATOR_IS_ONLY,
+ operators: OPERATORS_IS,
options: [
{ value: 'enabled', title: s__('Members|Enabled') },
{ value: 'disabled', title: s__('Members|Disabled') },
@@ -131,7 +131,7 @@ export const FILTERED_SEARCH_TOKEN_WITH_INHERITED_PERMISSIONS = {
title: s__('Members|Membership'),
token: GlFilteredSearchToken,
unique: true,
- operators: OPERATOR_IS_ONLY,
+ operators: OPERATORS_IS,
options: [
{ value: 'exclude', title: s__('Members|Direct') },
{ value: 'only', title: s__('Members|Inherited') },
@@ -187,8 +187,6 @@ export const LEAVE_MODAL_ID = 'member-leave-modal';
export const REMOVE_GROUP_LINK_MODAL_ID = 'remove-group-link-modal-id';
-export const SEARCH_TOKEN_TYPE = 'filtered-search-term';
-
export const SORT_QUERY_PARAM_NAME = 'sort';
export const ACTIVE_TAB_QUERY_PARAM_NAME = 'tab';
diff --git a/app/assets/javascripts/merge_conflicts/components/inline_conflict_lines.vue b/app/assets/javascripts/merge_conflicts/components/inline_conflict_lines.vue
index 87eeb272659..6c431dc8af3 100644
--- a/app/assets/javascripts/merge_conflicts/components/inline_conflict_lines.vue
+++ b/app/assets/javascripts/merge_conflicts/components/inline_conflict_lines.vue
@@ -1,6 +1,6 @@
<script>
-import { GlSafeHtmlDirective as SafeHtml } from '@gitlab/ui';
import { mapActions } from 'vuex';
+import SafeHtml from '~/vue_shared/directives/safe_html';
import syntaxHighlight from '~/syntax_highlight';
import { SYNTAX_HIGHLIGHT_CLASS } from '../constants';
import utilsMixin from '../mixins/line_conflict_utils';
diff --git a/app/assets/javascripts/merge_conflicts/components/parallel_conflict_lines.vue b/app/assets/javascripts/merge_conflicts/components/parallel_conflict_lines.vue
index 2c59e7bfa2f..f8a097a3a0f 100644
--- a/app/assets/javascripts/merge_conflicts/components/parallel_conflict_lines.vue
+++ b/app/assets/javascripts/merge_conflicts/components/parallel_conflict_lines.vue
@@ -1,6 +1,6 @@
<script>
-import { GlSafeHtmlDirective as SafeHtml } from '@gitlab/ui';
import { mapActions } from 'vuex';
+import SafeHtml from '~/vue_shared/directives/safe_html';
import syntaxHighlight from '~/syntax_highlight';
import { SYNTAX_HIGHLIGHT_CLASS } from '../constants';
import utilsMixin from '../mixins/line_conflict_utils';
diff --git a/app/assets/javascripts/merge_request.js b/app/assets/javascripts/merge_request.js
index 57b5e9809d2..80eb94a5364 100644
--- a/app/assets/javascripts/merge_request.js
+++ b/app/assets/javascripts/merge_request.js
@@ -94,7 +94,11 @@ MergeRequest.prototype.initMRBtnListeners = function () {
.put(draftToggle.href, null, { params: { format: 'json' } })
.then(({ data }) => {
draftToggle.removeAttribute('disabled');
- eventHub.$emit('MRWidgetUpdateRequested');
+
+ if (!window.gon?.features?.realtimeMrStatusChange) {
+ eventHub.$emit('MRWidgetUpdateRequested');
+ }
+
MergeRequest.toggleDraftStatus(data.title, wipEvent === 'ready');
})
.catch(() => {
@@ -173,7 +177,7 @@ MergeRequest.toggleDraftStatus = function (title, isReady) {
);
draftToggle.setAttribute('href', url);
- draftToggle.querySelector('.gl-new-dropdown-item-text-wrapper').textContent = isReady
+ draftToggle.querySelector('.gl-dropdown-item-text-wrapper').textContent = isReady
? __('Mark as draft')
: __('Mark as ready');
});
diff --git a/app/assets/javascripts/merge_request_tabs.js b/app/assets/javascripts/merge_request_tabs.js
index 0ddf5def8ee..5a1410ceeba 100644
--- a/app/assets/javascripts/merge_request_tabs.js
+++ b/app/assets/javascripts/merge_request_tabs.js
@@ -5,6 +5,7 @@ import { createAlert } from '~/flash';
import { getCookie, isMetaClick, parseBoolean, scrollToElement } from '~/lib/utils/common_utils';
import { parseUrlPathname } from '~/lib/utils/url_utility';
import createEventHub from '~/helpers/event_hub_factory';
+import { renderGFM } from '~/behaviors/markdown/render_gfm';
import BlobForkSuggestion from './blob/blob_fork_suggestion';
import Diff from './diff';
import { initDiffStatsDropdown } from './init_diff_stats_dropdown';
@@ -161,6 +162,23 @@ function toggleLoader(state) {
$('.mr-loading-status .loading').toggleClass('hide', !state);
}
+function getActionFromHref(href) {
+ let action = new URL(href).pathname.match(/\/(commits|diffs|pipelines).*$/);
+
+ if (action) {
+ action = action[0].replace(/(^\/|\.html)/g, '');
+ } else {
+ action = 'show';
+ }
+
+ return action;
+}
+
+const pageBundles = {
+ show: () => import(/* webpackPrefetch: true */ '~/mr_notes/init_notes'),
+ diffs: () => import(/* webpackPrefetch: true */ '~/diffs'),
+};
+
export default class MergeRequestTabs {
constructor({ action, setUrl, stubLocation } = {}) {
this.mergeRequestTabs = document.querySelector('.merge-request-tabs-container');
@@ -186,10 +204,10 @@ export default class MergeRequestTabs {
this.currentTab = null;
this.diffsLoaded = false;
- this.pipelinesLoaded = false;
this.commitsLoaded = false;
this.fixedLayoutPref = null;
this.eventHub = createEventHub();
+ this.loadedPages = { [action]: true };
this.setUrl = setUrl !== undefined ? setUrl : true;
this.setCurrentAction = this.setCurrentAction.bind(this);
@@ -206,12 +224,11 @@ export default class MergeRequestTabs {
bindEvents() {
$('.merge-request-tabs a[data-toggle="tabvue"]').on('click', this.clickTab);
- window.addEventListener('popstate', (event) => {
- if (event.state && event.state.action) {
- this.tabShown(event.state.action, event.target.location);
- this.currentAction = event.state.action;
- this.eventHub.$emit('MergeRequestTabChange', this.getCurrentAction());
- }
+ window.addEventListener('popstate', () => {
+ const action = getActionFromHref(location.href);
+
+ this.tabShown(action, location.href);
+ this.eventHub.$emit('MergeRequestTabChange', action);
});
}
@@ -252,17 +269,18 @@ export default class MergeRequestTabs {
} else if (action) {
const href = e.currentTarget.getAttribute('href');
this.tabShown(action, href);
-
- if (this.setUrl) {
- this.setCurrentAction(action);
- }
}
}
}
tabShown(action, href, shouldScroll = true) {
+ toggleLoader(false);
+
if (action !== this.currentTab && this.mergeRequestTabs) {
this.currentTab = action;
+ if (this.setUrl) {
+ this.setCurrentAction(action);
+ }
if (this.mergeRequestTabPanesAll) {
this.mergeRequestTabPanesAll.forEach((el) => {
@@ -282,6 +300,20 @@ export default class MergeRequestTabs {
const tab = this.mergeRequestTabs.querySelector(`.${action}-tab`);
if (tab) tab.classList.add('active');
+ if (!this.loadedPages[action] && action in pageBundles) {
+ toggleLoader(true);
+ pageBundles[action]()
+ .then(({ default: init }) => {
+ toggleLoader(false);
+ init();
+ this.loadedPages[action] = true;
+ })
+ .catch(() => {
+ toggleLoader(false);
+ createAlert({ message: __('MergeRequest|Failed to load the page') });
+ });
+ }
+
if (window.gon?.features?.movedMrSidebar) {
this.expandSidebar?.forEach((el) =>
el.classList.toggle('gl-display-none!', action !== 'show'),
@@ -334,7 +366,7 @@ export default class MergeRequestTabs {
this.commitPipelinesTable = destroyPipelines(this.commitPipelinesTable);
}
- $('.detail-page-description').renderGFM();
+ renderGFM(document.querySelector('.detail-page-description'));
if (shouldScroll) this.recallScroll(action);
} else if (action === this.currentAction) {
@@ -398,7 +430,7 @@ export default class MergeRequestTabs {
// Ensure parameters and hash come along for the ride
newState += location.search + location.hash;
- if (window.history.state && window.history.state.url && window.location.pathname !== newState) {
+ if (window.location.pathname !== newState) {
window.history.pushState(
{
url: newState,
@@ -477,8 +509,6 @@ export default class MergeRequestTabs {
return;
}
- toggleLoader(true);
-
loadDiffs({
// We extract pathname for the current Changes tab anchor href
// some pages like MergeRequestsController#new has query parameters on that anchor
@@ -496,9 +526,6 @@ export default class MergeRequestTabs {
createAlert({
message: __('An error occurred while fetching this tab.'),
});
- })
- .finally(() => {
- toggleLoader(false);
});
}
diff --git a/app/assets/javascripts/merge_requests/components/sticky_header.vue b/app/assets/javascripts/merge_requests/components/sticky_header.vue
index b7629ba001f..4a675cf7563 100644
--- a/app/assets/javascripts/merge_requests/components/sticky_header.vue
+++ b/app/assets/javascripts/merge_requests/components/sticky_header.vue
@@ -1,12 +1,7 @@
<script>
-import {
- GlIntersectionObserver,
- GlLink,
- GlSprintf,
- GlBadge,
- GlSafeHtmlDirective,
-} from '@gitlab/ui';
+import { GlIntersectionObserver, GlLink, GlSprintf, GlBadge } from '@gitlab/ui';
import { mapGetters, mapState } from 'vuex';
+import SafeHtml from '~/vue_shared/directives/safe_html';
import { TYPE_MERGE_REQUEST } from '~/graphql_shared/constants';
import { convertToGraphQLId } from '~/graphql_shared/utils';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
@@ -28,7 +23,7 @@ export default {
ClipboardButton,
},
directives: {
- SafeHtml: GlSafeHtmlDirective,
+ SafeHtml,
},
mixins: [glFeatureFlagsMixin()],
inject: {
diff --git a/app/assets/javascripts/merge_requests/components/target_project_dropdown.vue b/app/assets/javascripts/merge_requests/components/target_project_dropdown.vue
new file mode 100644
index 00000000000..cd2e25793f4
--- /dev/null
+++ b/app/assets/javascripts/merge_requests/components/target_project_dropdown.vue
@@ -0,0 +1,87 @@
+<script>
+import { GlListbox } from '@gitlab/ui';
+import { debounce } from 'lodash';
+import { createAlert } from '~/flash';
+import { __ } from '~/locale';
+import axios from '~/lib/utils/axios_utils';
+
+export default {
+ components: {
+ GlListbox,
+ },
+ inject: {
+ targetProjectsPath: {
+ type: String,
+ required: true,
+ },
+ currentProject: {
+ type: Object,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ currentProject: this.currentProject,
+ selected: this.currentProject.value,
+ isLoading: false,
+ projects: [],
+ };
+ },
+ methods: {
+ async fetchProjects(search = '') {
+ this.isLoading = true;
+
+ try {
+ const { data } = await axios.get(this.targetProjectsPath, {
+ params: { search },
+ });
+
+ this.projects = data.map((p) => ({
+ value: `${p.id}`,
+ text: p.full_path.replace(/^\//, ''),
+ refsUrl: p.refs_url,
+ }));
+ this.isLoading = false;
+ } catch {
+ createAlert({
+ message: __('Error fetching target projects. Please try again.'),
+ primaryButton: { text: __('Try again'), clickHandler: () => this.fetchProjects(search) },
+ });
+ }
+ },
+ searchProjects: debounce(function searchProjects(search) {
+ this.fetchProjects(search);
+ }, 500),
+ selectProject(projectId) {
+ this.currentProject = this.projects.find((p) => p.value === projectId);
+
+ this.$emit('project-selected', this.currentProject.refsUrl);
+ },
+ },
+};
+</script>
+
+<template>
+ <div>
+ <input
+ id="merge_request_target_project_id"
+ type="hidden"
+ :value="currentProject.value"
+ name="merge_request[target_project_id]"
+ data-testid="target-project-input"
+ />
+ <gl-listbox
+ v-model="selected"
+ :items="projects"
+ :toggle-text="currentProject.text"
+ :header-text="__('Select target project')"
+ :searching="isLoading"
+ searchable
+ class="gl-w-full dropdown-target-project"
+ toggle-class="gl-align-items-flex-start! gl-justify-content-start! mr-compare-dropdown js-target-project"
+ @shown="fetchProjects"
+ @search="searchProjects"
+ @select="selectProject"
+ />
+ </div>
+</template>
diff --git a/app/assets/javascripts/ml/experiment_tracking/components/experiment.vue b/app/assets/javascripts/ml/experiment_tracking/components/experiment.vue
deleted file mode 100644
index 73cdfbc44b0..00000000000
--- a/app/assets/javascripts/ml/experiment_tracking/components/experiment.vue
+++ /dev/null
@@ -1,36 +0,0 @@
-<script>
-import { GlTable } from '@gitlab/ui';
-import IncubationAlert from './incubation_alert.vue';
-
-export default {
- name: 'ShowMlExperiment',
- components: {
- GlTable,
- IncubationAlert,
- },
- inject: ['candidates', 'metricNames', 'paramNames'],
- computed: {
- fields() {
- return [...this.paramNames, ...this.metricNames];
- },
- },
-};
-</script>
-
-<template>
- <div>
- <incubation-alert />
-
- <h3>
- {{ __('Experiment Candidates') }}
- </h3>
-
- <gl-table
- :fields="fields"
- :items="candidates"
- :empty-text="__('This Experiment has no logged Candidates')"
- show-empty
- class="gl-mt-0!"
- />
- </div>
-</template>
diff --git a/app/assets/javascripts/ml/experiment_tracking/components/incubation_alert.vue b/app/assets/javascripts/ml/experiment_tracking/components/incubation_alert.vue
index 51c1e935677..42f6394ed68 100644
--- a/app/assets/javascripts/ml/experiment_tracking/components/incubation_alert.vue
+++ b/app/assets/javascripts/ml/experiment_tracking/components/incubation_alert.vue
@@ -8,8 +8,8 @@ export default {
contentLabel: __(
'GitLab incubates features to explore new use cases. These features are updated regularly, and support is limited',
),
- learnMoreLabel: __('Learn More'),
- feedbackLabel: __('Feedback and Updates'),
+ learnMoreLabel: __('Learn more'),
+ feedbackLabel: __('Feedback'),
},
name: 'MlopsIncubationAlert',
components: { GlAlert, GlLink },
@@ -37,7 +37,7 @@ export default {
:title="$options.i18n.titleLabel"
variant="warning"
:primary-button-text="$options.i18n.feedbackLabel"
- primary-button-link="https://gitlab.com/groups/gitlab-org/-/epics/8560"
+ primary-button-link="https://gitlab.com/gitlab-org/gitlab/-/issues/381660"
@dismiss="dismissAlert"
>
{{ $options.i18n.contentLabel }}
diff --git a/app/assets/javascripts/ml/experiment_tracking/components/ml_candidate.vue b/app/assets/javascripts/ml/experiment_tracking/components/ml_candidate.vue
new file mode 100644
index 00000000000..5f54f24e24c
--- /dev/null
+++ b/app/assets/javascripts/ml/experiment_tracking/components/ml_candidate.vue
@@ -0,0 +1,94 @@
+<script>
+import { GlLink } from '@gitlab/ui';
+import { __ } from '~/locale';
+import IncubationAlert from './incubation_alert.vue';
+
+export default {
+ name: 'MlCandidate',
+ components: {
+ IncubationAlert,
+ GlLink,
+ },
+ inject: ['candidate'],
+ i18n: {
+ titleLabel: __('Model candidate details'),
+ infoLabel: __('Info'),
+ idLabel: __('ID'),
+ statusLabel: __('Status'),
+ experimentLabel: __('Experiment'),
+ artifactsLabel: __('Artifacts'),
+ parametersLabel: __('Parameters'),
+ metricsLabel: __('Metrics'),
+ },
+};
+</script>
+
+<template>
+ <div>
+ <incubation-alert />
+
+ <h3>
+ {{ $options.i18n.titleLabel }}
+ </h3>
+
+ <table class="candidate-details">
+ <tbody>
+ <tr class="divider"></tr>
+
+ <tr>
+ <td class="gl-text-secondary gl-font-weight-bold">{{ $options.i18n.infoLabel }}</td>
+ <td class="gl-font-weight-bold">{{ $options.i18n.idLabel }}</td>
+ <td>{{ candidate.info.iid }}</td>
+ </tr>
+
+ <tr>
+ <td></td>
+ <td class="gl-font-weight-bold">{{ $options.i18n.statusLabel }}</td>
+ <td>{{ candidate.info.status }}</td>
+ </tr>
+
+ <tr>
+ <td></td>
+ <td class="gl-font-weight-bold">{{ $options.i18n.experimentLabel }}</td>
+ <td>
+ <gl-link :href="candidate.info.path_to_experiment">{{
+ candidate.info.experiment_name
+ }}</gl-link>
+ </td>
+ </tr>
+
+ <tr v-if="candidate.info.path_to_artifact">
+ <td></td>
+ <td class="gl-font-weight-bold">{{ $options.i18n.artifactsLabel }}</td>
+ <td>
+ <gl-link :href="candidate.info.path_to_artifact">{{
+ $options.i18n.artifactsLabel
+ }}</gl-link>
+ </td>
+ </tr>
+
+ <tr class="divider"></tr>
+
+ <tr v-for="(param, index) in candidate.params" :key="param.name">
+ <td v-if="index == 0" class="gl-text-secondary gl-font-weight-bold">
+ {{ $options.i18n.parametersLabel }}
+ </td>
+ <td v-else></td>
+ <td class="gl-font-weight-bold">{{ param.name }}</td>
+ <td>{{ param.value }}</td>
+ </tr>
+
+ <tr class="divider"></tr>
+
+ <tr v-for="(metric, index) in candidate.metrics" :key="metric.name">
+ <td v-if="index == 0" class="gl-text-secondary gl-font-weight-bold">
+ {{ $options.i18n.metricsLabel }}
+ </td>
+ <td v-else></td>
+ <td class="gl-font-weight-bold">{{ metric.name }}</td>
+ <td>{{ metric.value }}</td>
+ </tr>
+ </tbody>
+ </table>
+ </div>
+</template>
diff --git a/app/assets/javascripts/ml/experiment_tracking/components/ml_experiment.vue b/app/assets/javascripts/ml/experiment_tracking/components/ml_experiment.vue
new file mode 100644
index 00000000000..f8e269d3b57
--- /dev/null
+++ b/app/assets/javascripts/ml/experiment_tracking/components/ml_experiment.vue
@@ -0,0 +1,59 @@
+<script>
+import { GlTable, GlLink } from '@gitlab/ui';
+import { __ } from '~/locale';
+import IncubationAlert from './incubation_alert.vue';
+
+export default {
+ name: 'MlExperiment',
+ components: {
+ GlTable,
+ GlLink,
+ IncubationAlert,
+ },
+ inject: ['candidates', 'metricNames', 'paramNames'],
+ computed: {
+ fields() {
+ return [
+ ...this.paramNames,
+ ...this.metricNames,
+ { key: 'details', label: '' },
+ { key: 'artifact', label: '' },
+ ];
+ },
+ },
+ i18n: {
+ titleLabel: __('Experiment candidates'),
+ emptyStateLabel: __('This experiment has no logged candidates'),
+ artifactsLabel: __('Artifacts'),
+ detailsLabel: __('Details'),
+ },
+};
+</script>
+
+<template>
+ <div>
+ <incubation-alert />
+
+ <h3>
+ {{ $options.i18n.titleLabel }}
+ </h3>
+
+ <gl-table
+ :fields="fields"
+ :items="candidates"
+ :empty-text="$options.i18n.emptyStateLabel"
+ show-empty
+ class="gl-mt-0!"
+ >
+ <template #cell(artifact)="data">
+ <gl-link v-if="data.value" :href="data.value" target="_blank">{{
+ $options.i18n.artifactsLabel
+ }}</gl-link>
+ </template>
+
+ <template #cell(details)="data">
+ <gl-link :href="data.value">{{ $options.i18n.detailsLabel }}</gl-link>
+ </template>
+ </gl-table>
+ </div>
+</template>
diff --git a/app/assets/javascripts/monitoring/components/charts/empty_chart.vue b/app/assets/javascripts/monitoring/components/charts/empty_chart.vue
index ae079da0b0b..da4c92df711 100644
--- a/app/assets/javascripts/monitoring/components/charts/empty_chart.vue
+++ b/app/assets/javascripts/monitoring/components/charts/empty_chart.vue
@@ -1,11 +1,11 @@
<script>
import chartEmptyStateIllustration from '@gitlab/svgs/dist/illustrations/chart-empty-state.svg';
-import { GlSafeHtmlDirective } from '@gitlab/ui';
+import SafeHtml from '~/vue_shared/directives/safe_html';
import { chartHeight } from '../../constants';
export default {
directives: {
- SafeHtml: GlSafeHtmlDirective,
+ SafeHtml,
},
data() {
return {
diff --git a/app/assets/javascripts/monitoring/components/dashboard.vue b/app/assets/javascripts/monitoring/components/dashboard.vue
index b6ad2d21757..2c185794d17 100644
--- a/app/assets/javascripts/monitoring/components/dashboard.vue
+++ b/app/assets/javascripts/monitoring/components/dashboard.vue
@@ -391,11 +391,7 @@ export default {
};
</script>
<template>
- <div
- class="prometheus-graphs"
- data-qa-selector="prometheus_graphs_content"
- data-testid="prometheus-graphs"
- >
+ <div class="prometheus-graphs" data-testid="prometheus-graphs">
<div>
<gl-alert
v-if="!isDeprecationNoticeDismissed"
diff --git a/app/assets/javascripts/monitoring/components/dashboard_actions_menu.vue b/app/assets/javascripts/monitoring/components/dashboard_actions_menu.vue
index 7f8fb3c223d..d67154b7697 100644
--- a/app/assets/javascripts/monitoring/components/dashboard_actions_menu.vue
+++ b/app/assets/javascripts/monitoring/components/dashboard_actions_menu.vue
@@ -146,7 +146,6 @@ export default {
<gl-dropdown
v-gl-tooltip
data-testid="actions-menu"
- data-qa-selector="actions_menu_dropdown"
right
no-caret
toggle-class="gl-px-3!"
@@ -223,7 +222,6 @@ export default {
<gl-dropdown-item
v-if="isMenuItemEnabled.editDashboard"
:href="selectedDashboard ? selectedDashboard.project_blob_path : null"
- data-qa-selector="edit_dashboard_button_enabled"
data-testid="edit-dashboard-item-enabled"
>
{{ $options.i18n.editDashboard }}
diff --git a/app/assets/javascripts/monitoring/components/dashboard_header.vue b/app/assets/javascripts/monitoring/components/dashboard_header.vue
index 90d2498ac19..7bb0d3874d1 100644
--- a/app/assets/javascripts/monitoring/components/dashboard_header.vue
+++ b/app/assets/javascripts/monitoring/components/dashboard_header.vue
@@ -173,7 +173,6 @@ export default {
<div class="gl-mb-3 gl-mr-3 gl-display-flex gl-sm-display-block">
<dashboards-dropdown
id="monitor-dashboards-dropdown"
- data-qa-selector="dashboards_filter_dropdown"
class="flex-grow-1"
toggle-class="dropdown-menu-toggle"
:default-branch="defaultBranch"
@@ -188,7 +187,6 @@ export default {
id="monitor-environments-dropdown"
ref="monitorEnvironmentsDropdown"
class="flex-grow-1"
- data-qa-selector="environments_dropdown"
data-testid="environments-dropdown"
toggle-class="dropdown-menu-toggle"
menu-class="monitor-environment-dropdown-menu"
@@ -225,7 +223,6 @@ export default {
<date-time-picker
ref="dateTimePicker"
class="flex-grow-1 show-last-dropdown"
- data-qa-selector="range_picker_dropdown"
:value="selectedTimeRange"
:options="$options.timeRanges"
:utc="displayUtc"
diff --git a/app/assets/javascripts/monitoring/components/dashboard_panel.vue b/app/assets/javascripts/monitoring/components/dashboard_panel.vue
index 7e7dcef7639..9ad6da35d6b 100644
--- a/app/assets/javascripts/monitoring/components/dashboard_panel.vue
+++ b/app/assets/javascripts/monitoring/components/dashboard_panel.vue
@@ -292,11 +292,7 @@ export default {
<div v-if="graphDataIsLoading" class="mx-1 mt-1">
<gl-loading-icon size="sm" />
</div>
- <div
- v-if="isContextualMenuShown"
- ref="contextualMenu"
- data-qa-selector="prometheus_graph_widgets"
- >
+ <div v-if="isContextualMenuShown" ref="contextualMenu">
<div data-testid="dropdown-wrapper" class="d-flex align-items-center">
<!--
This component should be replaced with a variant developed
@@ -310,7 +306,6 @@ export default {
:text-sr-only="true"
toggle-class="gl-px-3!"
no-caret
- data-qa-selector="prometheus_widgets_dropdown"
right
:title="__('More actions')"
>
@@ -339,7 +334,6 @@ export default {
ref="copyChartLink"
v-track-event="generateLinkToChartOptions(clipboardText)"
:data-clipboard-text="clipboardText"
- data-qa-selector="generate_chart_link_menu_item"
@click="showToast(clipboardText)"
>
{{ __('Copy link to chart') }}
diff --git a/app/assets/javascripts/monitoring/components/dashboard_panel_builder.vue b/app/assets/javascripts/monitoring/components/dashboard_panel_builder.vue
index 8efea2bfc3e..e8a9c24f5c2 100644
--- a/app/assets/javascripts/monitoring/components/dashboard_panel_builder.vue
+++ b/app/assets/javascripts/monitoring/components/dashboard_panel_builder.vue
@@ -100,7 +100,7 @@ export default {
<gl-form-textarea
id="panel-yml-input"
v-model="yml"
- class="gl-h-200! gl-font-monospace! gl-font-size-monospace!"
+ class="gl-h-200! gl-font-monospace!"
/>
</gl-form-group>
<div class="gl-text-right">
diff --git a/app/assets/javascripts/monitoring/components/duplicate_dashboard_form.vue b/app/assets/javascripts/monitoring/components/duplicate_dashboard_form.vue
index a63008aa382..9ad14b3d52e 100644
--- a/app/assets/javascripts/monitoring/components/duplicate_dashboard_form.vue
+++ b/app/assets/javascripts/monitoring/components/duplicate_dashboard_form.vue
@@ -104,13 +104,7 @@ export default {
label-size="sm"
label-for="fileName"
>
- <gl-form-input
- id="fileName"
- ref="fileName"
- v-model="form.fileName"
- data-qa-selector="duplicate_dashboard_filename_field"
- :required="true"
- />
+ <gl-form-input id="fileName" ref="fileName" v-model="form.fileName" :required="true" />
</gl-form-group>
<gl-form-group :label="__('Branch')" label-size="sm" label-for="branch">
<gl-form-radio-group
diff --git a/app/assets/javascripts/monitoring/components/group_empty_state.vue b/app/assets/javascripts/monitoring/components/group_empty_state.vue
index 0365fc66331..a67770b93be 100644
--- a/app/assets/javascripts/monitoring/components/group_empty_state.vue
+++ b/app/assets/javascripts/monitoring/components/group_empty_state.vue
@@ -1,5 +1,6 @@
<script>
-import { GlEmptyState, GlSafeHtmlDirective as SafeHtml } from '@gitlab/ui';
+import { GlEmptyState } from '@gitlab/ui';
+import SafeHtml from '~/vue_shared/directives/safe_html';
import { __, sprintf } from '~/locale';
import { metricStates } from '../constants';
diff --git a/app/assets/javascripts/monitoring/components/refresh_button.vue b/app/assets/javascripts/monitoring/components/refresh_button.vue
index 544fe10f26e..55c602db33d 100644
--- a/app/assets/javascripts/monitoring/components/refresh_button.vue
+++ b/app/assets/javascripts/monitoring/components/refresh_button.vue
@@ -11,8 +11,6 @@ import Visibility from 'visibilityjs';
import { mapActions } from 'vuex';
import { n__, __, s__ } from '~/locale';
-import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
-
const makeInterval = (length = 0, unit = 's') => {
const shortLabel = `${length}${unit}`;
switch (unit) {
@@ -58,7 +56,6 @@ export default {
directives: {
GlTooltip: GlTooltipDirective,
},
- mixins: [glFeatureFlagsMixin()],
data() {
return {
refreshInterval: null,
@@ -66,12 +63,6 @@ export default {
};
},
computed: {
- disableMetricDashboardRefreshRate() {
- // Can refresh rates impact performance?
- // Add "negative" feature flag called `disable_metric_dashboard_refresh_rate`
- // See more at: https://gitlab.com/gitlab-org/gitlab/-/issues/229831
- return this.glFeatures.disableMetricDashboardRefreshRate;
- },
dropdownText() {
return this.refreshInterval?.shortLabel ?? __('Off');
},
@@ -156,12 +147,7 @@ export default {
icon="retry"
@click="refresh"
/>
- <gl-dropdown
- v-if="!disableMetricDashboardRefreshRate"
- v-gl-tooltip
- :title="s__('Metrics|Set refresh rate')"
- :text="dropdownText"
- >
+ <gl-dropdown v-gl-tooltip :title="s__('Metrics|Set refresh rate')" :text="dropdownText">
<gl-dropdown-item
is-check-item
:is-checked="refreshInterval === null"
diff --git a/app/assets/javascripts/monitoring/components/variables_section.vue b/app/assets/javascripts/monitoring/components/variables_section.vue
index 493d37ce263..971f188e9f3 100644
--- a/app/assets/javascripts/monitoring/components/variables_section.vue
+++ b/app/assets/javascripts/monitoring/components/variables_section.vue
@@ -37,11 +37,7 @@ export default {
};
</script>
<template>
- <div
- ref="variablesSection"
- class="d-sm-flex flex-sm-wrap pt-2 pr-1 pb-0 pl-2 variables-section"
- data-qa-selector="variables_content"
- >
+ <div ref="variablesSection" class="d-sm-flex flex-sm-wrap pt-2 pr-1 pb-0 pl-2 variables-section">
<div v-for="variable in variables" :key="variable.name" class="mb-1 pr-2 d-flex d-sm-block">
<component
:is="variableField(variable.type)"
@@ -50,7 +46,6 @@ export default {
:value="variable.value"
:name="variable.name"
:options="variable.options"
- data-qa-selector="variable_item"
@input="refreshDashboard(variable, $event)"
/>
</div>
diff --git a/app/assets/javascripts/monitoring/csv_export.js b/app/assets/javascripts/monitoring/csv_export.js
index eaeed4a54d4..7e15b659767 100644
--- a/app/assets/javascripts/monitoring/csv_export.js
+++ b/app/assets/javascripts/monitoring/csv_export.js
@@ -110,7 +110,7 @@ const csvData = (metricHeaders, metricValues) => {
// "If double-quotes are used to enclose fields, then a double-quote
// appearing inside a field must be escaped by preceding it with
// another double quote."
- // https://tools.ietf.org/html/rfc4180#page-2
+ // https://www.rfc-editor.org/rfc/rfc4180#page-2
const headers = metricHeaders.map((header) => `"${header.replace(/"/g, '""')}"`);
return {
diff --git a/app/assets/javascripts/monitoring/requests/index.js b/app/assets/javascripts/monitoring/requests/index.js
index 26fedb9c81c..8b65eec051f 100644
--- a/app/assets/javascripts/monitoring/requests/index.js
+++ b/app/assets/javascripts/monitoring/requests/index.js
@@ -1,13 +1,16 @@
import axios from '~/lib/utils/axios_utils';
import { backOff } from '~/lib/utils/common_utils';
-import statusCodes from '~/lib/utils/http_status';
+import statusCodes, {
+ HTTP_STATUS_NO_CONTENT,
+ HTTP_STATUS_UNPROCESSABLE_ENTITY,
+} from '~/lib/utils/http_status';
import { PROMETHEUS_TIMEOUT } from '../constants';
const cancellableBackOffRequest = (makeRequestCallback) =>
backOff((next, stop) => {
makeRequestCallback()
.then((resp) => {
- if (resp.status === statusCodes.NO_CONTENT) {
+ if (resp.status === HTTP_STATUS_NO_CONTENT) {
next();
} else {
stop(resp);
@@ -34,7 +37,7 @@ export const getPrometheusQueryData = (prometheusEndpoint, params, opts) =>
const { response = {} } = error;
if (
response.status === statusCodes.BAD_REQUEST ||
- response.status === statusCodes.UNPROCESSABLE_ENTITY ||
+ response.status === HTTP_STATUS_UNPROCESSABLE_ENTITY ||
response.status === statusCodes.SERVICE_UNAVAILABLE
) {
const { data } = response;
diff --git a/app/assets/javascripts/monitoring/utils.js b/app/assets/javascripts/monitoring/utils.js
index fd8749625da..0d849e1a2d8 100644
--- a/app/assets/javascripts/monitoring/utils.js
+++ b/app/assets/javascripts/monitoring/utils.js
@@ -39,7 +39,6 @@ export const stateAndPropsFromDataset = (dataset = {}) => {
// HTML attributes are always strings, parse other types.
dataProps.hasMetrics = parseBoolean(dataProps.hasMetrics);
dataProps.customMetricsAvailable = parseBoolean(dataProps.customMetricsAvailable);
- dataProps.prometheusAlertsAvailable = parseBoolean(dataProps.prometheusAlertsAvailable);
return {
initState: {
diff --git a/app/assets/javascripts/mr_notes/discussion_counter.js b/app/assets/javascripts/mr_notes/discussion_counter.js
new file mode 100644
index 00000000000..0bb63a7c0f9
--- /dev/null
+++ b/app/assets/javascripts/mr_notes/discussion_counter.js
@@ -0,0 +1,28 @@
+import Vue from 'vue';
+import DiscussionCounter from '~/notes/components/discussion_counter.vue';
+import store from '~/mr_notes/stores';
+
+export function initDiscussionCounter() {
+ const el = document.getElementById('js-vue-discussion-counter');
+
+ if (el) {
+ const { blocksMerge } = el.dataset;
+
+ // eslint-disable-next-line no-new
+ new Vue({
+ el,
+ name: 'DiscussionCounter',
+ components: {
+ DiscussionCounter,
+ },
+ store,
+ render(createElement) {
+ return createElement('discussion-counter', {
+ props: {
+ blocksMerge: blocksMerge === 'true',
+ },
+ });
+ },
+ });
+ }
+}
diff --git a/app/assets/javascripts/mr_notes/index.js b/app/assets/javascripts/mr_notes/index.js
index c32a1f4c2ac..a202923bd21 100644
--- a/app/assets/javascripts/mr_notes/index.js
+++ b/app/assets/javascripts/mr_notes/index.js
@@ -1,12 +1,8 @@
-import Vue from 'vue';
-import store from '~/mr_notes/stores';
import initCherryPickCommitModal from '~/projects/commit/init_cherry_pick_commit_modal';
import initRevertCommitModal from '~/projects/commit/init_revert_commit_modal';
-import initDiffsApp from '../diffs';
-import { resetServiceWorkersPublicPath } from '../lib/utils/webpack';
+import { initMrStateLazyLoad } from '~/mr_notes/init';
import MergeRequest from '../merge_request';
-import DiscussionCounter from '../notes/components/discussion_counter.vue';
-import initNotesApp from './init_notes';
+import { resetServiceWorkersPublicPath } from '../lib/utils/webpack';
export default function initMrNotes() {
resetServiceWorkersPublicPath();
@@ -17,36 +13,10 @@ export default function initMrNotes() {
action: mrShowNode.dataset.mrAction,
});
- initDiffsApp(store);
- initNotesApp();
+ initMrStateLazyLoad();
document.addEventListener('merged:UpdateActions', () => {
initRevertCommitModal('i_code_review_post_merge_submit_revert_modal');
initCherryPickCommitModal('i_code_review_post_merge_submit_cherry_pick_modal');
});
-
- requestIdleCallback(() => {
- const el = document.getElementById('js-vue-discussion-counter');
-
- if (el) {
- const { blocksMerge } = el.dataset;
-
- // eslint-disable-next-line no-new
- new Vue({
- el,
- name: 'DiscussionCounter',
- components: {
- DiscussionCounter,
- },
- store,
- render(createElement) {
- return createElement('discussion-counter', {
- props: {
- blocksMerge: blocksMerge === 'true',
- },
- });
- },
- });
- }
- });
}
diff --git a/app/assets/javascripts/mr_notes/init.js b/app/assets/javascripts/mr_notes/init.js
new file mode 100644
index 00000000000..aab3c41b4cf
--- /dev/null
+++ b/app/assets/javascripts/mr_notes/init.js
@@ -0,0 +1,52 @@
+import { parseBoolean } from '~/lib/utils/common_utils';
+import store from '~/mr_notes/stores';
+import { getLocationHash } from '~/lib/utils/url_utility';
+import eventHub from '~/notes/event_hub';
+import { initReviewBar } from '~/batch_comments';
+import { initDiscussionCounter } from '~/mr_notes/discussion_counter';
+import { initOverviewTabCounter } from '~/mr_notes/init_count';
+
+function setupMrNotesState(notesDataset) {
+ const noteableData = JSON.parse(notesDataset.noteableData);
+ noteableData.noteableType = notesDataset.noteableType;
+ noteableData.targetType = notesDataset.targetType;
+ noteableData.discussion_locked = parseBoolean(notesDataset.isLocked);
+ const notesData = JSON.parse(notesDataset.notesData);
+ const currentUserData = JSON.parse(notesDataset.currentUserData);
+ const endpoints = { metadata: notesDataset.endpointMetadata };
+
+ store.dispatch('setNotesData', notesData);
+ store.dispatch('setNoteableData', noteableData);
+ store.dispatch('setUserData', currentUserData);
+ store.dispatch('setTargetNoteHash', getLocationHash());
+ store.dispatch('setEndpoints', endpoints);
+ eventHub.$once('fetchNotesData', () => store.dispatch('fetchNotes'));
+}
+
+export function initMrStateLazyLoad() {
+ store.dispatch('setActiveTab', window.mrTabs.getCurrentAction());
+ window.mrTabs.eventHub.$on('MergeRequestTabChange', (value) =>
+ store.dispatch('setActiveTab', value),
+ );
+
+ const discussionsEl = document.getElementById('js-vue-mr-discussions');
+ const notesDataset = discussionsEl.dataset;
+ let stop = () => {};
+ stop = store.watch(
+ (state) => state.page.activeTab,
+ (activeTab) => {
+ // prevent loading MR state on commits and pipelines pages
+ // this is due to them having a shared controller with the Overview page
+ if (['diffs', 'show'].includes(activeTab)) {
+ setupMrNotesState(notesDataset);
+ requestIdleCallback(() => {
+ initReviewBar();
+ initOverviewTabCounter();
+ initDiscussionCounter();
+ });
+ stop();
+ }
+ },
+ { immediate: true },
+ );
+}
diff --git a/app/assets/javascripts/mr_notes/init_count.js b/app/assets/javascripts/mr_notes/init_count.js
new file mode 100644
index 00000000000..3e924ebd9d5
--- /dev/null
+++ b/app/assets/javascripts/mr_notes/init_count.js
@@ -0,0 +1,13 @@
+import store from '~/mr_notes/stores';
+
+export function initOverviewTabCounter() {
+ const discussionsCount = document.querySelector('.js-discussions-count');
+ store.watch(
+ (state, getters) => getters.discussionTabCounter,
+ (val) => {
+ if (typeof val !== 'undefined') {
+ discussionsCount.textContent = val;
+ }
+ },
+ );
+}
diff --git a/app/assets/javascripts/mr_notes/init_notes.js b/app/assets/javascripts/mr_notes/init_notes.js
index 3a67e7925c3..e10605609b0 100644
--- a/app/assets/javascripts/mr_notes/init_notes.js
+++ b/app/assets/javascripts/mr_notes/init_notes.js
@@ -1,8 +1,8 @@
-import $ from 'jquery';
import Vue from 'vue';
import { mapActions, mapState, mapGetters } from 'vuex';
import { parseBoolean } from '~/lib/utils/common_utils';
import store from '~/mr_notes/stores';
+import notesEventHub from '~/notes/event_hub';
import discussionNavigator from '../notes/components/discussion_navigator.vue';
import NotesApp from '../notes/components/notes_app.vue';
import { getNotesFilterData } from '../notes/utils/get_notes_filter_data';
@@ -36,13 +36,12 @@ export default () => {
endpoints: {
metadata: notesDataset.endpointMetadata,
},
- currentUserData: JSON.parse(notesDataset.currentUserData),
notesData: JSON.parse(notesDataset.notesData),
helpPagePath: notesDataset.helpPagePath,
};
},
computed: {
- ...mapGetters(['discussionTabCounter']),
+ ...mapGetters(['isNotesFetched']),
...mapState({
activeTab: (state) => state.page.activeTab,
}),
@@ -51,15 +50,6 @@ export default () => {
},
},
watch: {
- discussionTabCounter() {
- if (window.gon?.features?.paginatedMrDiscussions) {
- if (this.$store.state.notes.doneFetchingBatchDiscussions) {
- this.updateDiscussionTabCounter();
- }
- } else {
- this.updateDiscussionTabCounter();
- }
- },
isShowTabActive: {
handler(newVal) {
if (newVal) {
@@ -70,25 +60,16 @@ export default () => {
},
},
created() {
- this.setActiveTab(window.mrTabs.getCurrentAction());
this.setEndpoints(this.endpoints);
+ if (!this.isNotesFetched) {
+ notesEventHub.$emit('fetchNotesData');
+ }
+
this.fetchMrMetadata();
},
- mounted() {
- this.notesCountBadge = $('.issuable-details').find('.notes-tab .badge');
- $(document).on('visibilitychange', this.updateDiscussionTabCounter);
- window.mrTabs.eventHub.$on('MergeRequestTabChange', this.setActiveTab);
- },
- beforeDestroy() {
- $(document).off('visibilitychange', this.updateDiscussionTabCounter);
- window.mrTabs.eventHub.$off('MergeRequestTabChange', this.setActiveTab);
- },
methods: {
- ...mapActions(['setActiveTab', 'setEndpoints', 'fetchMrMetadata']),
- updateDiscussionTabCounter() {
- this.notesCountBadge.text(this.discussionTabCounter);
- },
+ ...mapActions(['setEndpoints', 'fetchMrMetadata']),
},
render(createElement) {
// NOTE: Even though `discussionNavigator` is added to the `notes-app`,
diff --git a/app/assets/javascripts/nav/components/new_nav_toggle.vue b/app/assets/javascripts/nav/components/new_nav_toggle.vue
new file mode 100644
index 00000000000..ef59140115d
--- /dev/null
+++ b/app/assets/javascripts/nav/components/new_nav_toggle.vue
@@ -0,0 +1,71 @@
+<script>
+import { GlBadge, GlToggle } from '@gitlab/ui';
+import axios from '~/lib/utils/axios_utils';
+import { createAlert } from '~/flash';
+import { s__ } from '~/locale';
+
+export default {
+ i18n: {
+ badgeLabel: s__('NorthstarNavigation|Alpha'),
+ sectionTitle: s__('NorthstarNavigation|Navigation redesign'),
+ toggleMenuItemLabel: s__('NorthstarNavigation|New navigation'),
+ toggleLabel: s__('NorthstarNavigation|Toggle new navigation'),
+ updateError: s__(
+ 'NorthstarNavigation|Could not update the new navigation preference. Please try again later.',
+ ),
+ },
+ components: {
+ GlBadge,
+ GlToggle,
+ },
+ props: {
+ enabled: {
+ type: Boolean,
+ required: true,
+ },
+ endpoint: {
+ type: String,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ isEnabled: this.enabled,
+ };
+ },
+ methods: {
+ async toggleNav() {
+ try {
+ await axios.put(this.endpoint, { user: { use_new_navigation: !this.enabled } });
+ window.location.reload();
+ } catch (error) {
+ createAlert({
+ message: this.$options.i18n.updateError,
+ error,
+ });
+ }
+ },
+ },
+};
+</script>
+
+<template>
+ <li>
+ <div
+ class="gl-px-4 gl-py-2 gl-display-flex gl-justify-content-space-between gl-align-items-center"
+ >
+ <b>{{ $options.i18n.sectionTitle }}</b>
+ <gl-badge>{{ $options.i18n.badgeLabel }}</gl-badge>
+ </div>
+
+ <div class="menu-item gl-display-flex! gl-justify-content-space-between gl-align-items-center">
+ {{ $options.i18n.toggleMenuItemLabel }}
+ <gl-toggle
+ v-model="isEnabled"
+ :label="$options.i18n.toggleLabel"
+ label-position="hidden"
+ @change="toggleNav"
+ />
+ </div>
+ </li>
+</template>
diff --git a/app/assets/javascripts/new_branch_form.js b/app/assets/javascripts/new_branch_form.js
index ef36e58374c..a7c2e572037 100644
--- a/app/assets/javascripts/new_branch_form.js
+++ b/app/assets/javascripts/new_branch_form.js
@@ -1,15 +1,9 @@
/* eslint-disable func-names, no-return-assign, @gitlab/require-i18n-strings */
-
-import $ from 'jquery';
-import RefSelectDropdown from './ref_select_dropdown';
-
export default class NewBranchForm {
- constructor(form, availableRefs) {
+ constructor(form) {
this.validate = this.validate.bind(this);
this.branchNameError = form.querySelector('.js-branch-name-error');
this.name = form.querySelector('.js-branch-name');
- this.ref = form.querySelector('#ref');
- new RefSelectDropdown($('.js-branch-select'), availableRefs); // eslint-disable-line no-new
this.setupRestrictions();
this.addBinding();
this.init();
diff --git a/app/assets/javascripts/notebook/cells/markdown.vue b/app/assets/javascripts/notebook/cells/markdown.vue
index 9aa6abd9d8c..2caa93c3c93 100644
--- a/app/assets/javascripts/notebook/cells/markdown.vue
+++ b/app/assets/javascripts/notebook/cells/markdown.vue
@@ -1,7 +1,7 @@
<script>
import katex from 'katex';
import { marked } from 'marked';
-import { GlSafeHtmlDirective as SafeHtml } from '@gitlab/ui';
+import SafeHtml from '~/vue_shared/directives/safe_html';
import { sanitize } from '~/lib/dompurify';
import { hasContent, markdownConfig } from '~/lib/utils/text_utility';
import Prompt from './prompt.vue';
diff --git a/app/assets/javascripts/notebook/cells/output/html.vue b/app/assets/javascripts/notebook/cells/output/html.vue
index 5437a607e8a..74a5dd3806d 100644
--- a/app/assets/javascripts/notebook/cells/output/html.vue
+++ b/app/assets/javascripts/notebook/cells/output/html.vue
@@ -1,14 +1,10 @@
<script>
-import { GlSafeHtmlDirective } from '@gitlab/ui';
import Prompt from '../prompt.vue';
export default {
components: {
Prompt,
},
- directives: {
- SafeHtml: GlSafeHtmlDirective,
- },
props: {
count: {
type: Number,
@@ -28,12 +24,6 @@ export default {
return this.index === 0;
},
},
- safeHtmlConfig: {
- ADD_TAGS: ['use'], // to support icon SVGs
- FORBID_TAGS: ['style'],
- FORBID_ATTR: ['style'],
- ALLOW_DATA_ATTR: false,
- },
};
</script>
diff --git a/app/assets/javascripts/notebook/cells/output/latex.vue b/app/assets/javascripts/notebook/cells/output/latex.vue
index d0ed963b55d..55f97fee3dc 100644
--- a/app/assets/javascripts/notebook/cells/output/latex.vue
+++ b/app/assets/javascripts/notebook/cells/output/latex.vue
@@ -1,5 +1,5 @@
<script>
-import { GlSafeHtmlDirective as SafeHtml } from '@gitlab/ui';
+import SafeHtml from '~/vue_shared/directives/safe_html';
import 'mathjax/es5/tex-svg';
import Prompt from '../prompt.vue';
diff --git a/app/assets/javascripts/notebook/cells/output/markdown.vue b/app/assets/javascripts/notebook/cells/output/markdown.vue
index 5da057dee72..ad74e28ac74 100644
--- a/app/assets/javascripts/notebook/cells/output/markdown.vue
+++ b/app/assets/javascripts/notebook/cells/output/markdown.vue
@@ -1,5 +1,4 @@
<script>
-import { GlSafeHtmlDirective as SafeHtml } from '@gitlab/ui';
import Prompt from '../prompt.vue';
import Markdown from '../markdown.vue';
@@ -9,9 +8,6 @@ export default {
Prompt,
Markdown,
},
- directives: {
- SafeHtml,
- },
props: {
count: {
type: Number,
diff --git a/app/assets/javascripts/notes/components/comment_form.vue b/app/assets/javascripts/notes/components/comment_form.vue
index 0d7ff022f8f..2ccb9a0b514 100644
--- a/app/assets/javascripts/notes/components/comment_form.vue
+++ b/app/assets/javascripts/notes/components/comment_form.vue
@@ -7,7 +7,7 @@ import Autosave from '~/autosave';
import { refreshUserMergeRequestCounts } from '~/commons/nav/user_merge_requests';
import { createAlert } from '~/flash';
import { badgeState } from '~/issuable/components/status_box.vue';
-import httpStatusCodes from '~/lib/utils/http_status';
+import { HTTP_STATUS_UNPROCESSABLE_ENTITY } from '~/lib/utils/http_status';
import {
capitalizeFirstCharacter,
convertToCamelCase,
@@ -28,8 +28,6 @@ import CommentTypeDropdown from './comment_type_dropdown.vue';
import DiscussionLockedWidget from './discussion_locked_widget.vue';
import NoteSignedOutWidget from './note_signed_out_widget.vue';
-const { UNPROCESSABLE_ENTITY } = httpStatusCodes;
-
export default {
name: 'CommentForm',
i18n: COMMENT_FORM,
@@ -198,7 +196,7 @@ export default {
'toggleIssueLocalState',
]),
handleSaveError({ data, status }) {
- if (status === UNPROCESSABLE_ENTITY && data.errors?.commands_only?.length) {
+ if (status === HTTP_STATUS_UNPROCESSABLE_ENTITY && data.errors?.commands_only?.length) {
this.errors = data.errors.commands_only;
} else {
this.errors = [this.$options.i18n.GENERIC_UNSUBMITTABLE_NETWORK];
diff --git a/app/assets/javascripts/notes/components/diff_discussion_header.vue b/app/assets/javascripts/notes/components/diff_discussion_header.vue
index cf6474270a2..f949142d90a 100644
--- a/app/assets/javascripts/notes/components/diff_discussion_header.vue
+++ b/app/assets/javascripts/notes/components/diff_discussion_header.vue
@@ -1,7 +1,8 @@
<script>
-import { GlSafeHtmlDirective as SafeHtml, GlAvatar, GlAvatarLink } from '@gitlab/ui';
+import { GlAvatar, GlAvatarLink } from '@gitlab/ui';
import { escape } from 'lodash';
import { mapActions } from 'vuex';
+import SafeHtml from '~/vue_shared/directives/safe_html';
import { truncateSha } from '~/lib/utils/text_utility';
import { s__, __, sprintf } from '~/locale';
import NoteEditedText from './note_edited_text.vue';
diff --git a/app/assets/javascripts/notes/components/diff_with_note.vue b/app/assets/javascripts/notes/components/diff_with_note.vue
index 3bdf8349a12..aabdc1c99b6 100644
--- a/app/assets/javascripts/notes/components/diff_with_note.vue
+++ b/app/assets/javascripts/notes/components/diff_with_note.vue
@@ -1,6 +1,7 @@
<script>
-import { GlSkeletonLoader, GlSafeHtmlDirective as SafeHtml } from '@gitlab/ui';
+import { GlSkeletonLoader } from '@gitlab/ui';
import { mapState, mapActions } from 'vuex';
+import SafeHtml from '~/vue_shared/directives/safe_html';
import DiffFileHeader from '~/diffs/components/diff_file_header.vue';
import ImageDiffOverlay from '~/diffs/components/image_diff_overlay.vue';
import { getDiffMode } from '~/diffs/store/utils';
diff --git a/app/assets/javascripts/notes/components/note_actions.vue b/app/assets/javascripts/notes/components/note_actions.vue
index 930876e90b1..c15c11ed9db 100644
--- a/app/assets/javascripts/notes/components/note_actions.vue
+++ b/app/assets/javascripts/notes/components/note_actions.vue
@@ -19,6 +19,7 @@ export default {
editCommentLabel: __('Edit comment'),
deleteCommentLabel: __('Delete comment'),
moreActionsLabel: __('More actions'),
+ reportAbuse: __('Report abuse to administrator'),
},
name: 'NoteActions',
components: {
@@ -362,7 +363,7 @@ export default {
<!-- eslint-enable @gitlab/vue-no-data-toggle -->
<ul class="dropdown-menu more-actions-dropdown dropdown-open-left">
<gl-dropdown-item v-if="canReportAsAbuse" :href="reportAbusePath">
- {{ __('Report abuse to admin') }}
+ {{ $options.i18n.reportAbuse }}
</gl-dropdown-item>
<gl-dropdown-item
v-if="noteUrl"
diff --git a/app/assets/javascripts/notes/components/note_body.vue b/app/assets/javascripts/notes/components/note_body.vue
index 82c125b79ce..20cf21cd1b6 100644
--- a/app/assets/javascripts/notes/components/note_body.vue
+++ b/app/assets/javascripts/notes/components/note_body.vue
@@ -1,12 +1,10 @@
<script>
-import $ from 'jquery';
-import { GlSafeHtmlDirective } from '@gitlab/ui';
import { escape } from 'lodash';
import { mapActions, mapGetters, mapState } from 'vuex';
-
+import SafeHtml from '~/vue_shared/directives/safe_html';
import { __ } from '~/locale';
-import '~/behaviors/markdown/render_gfm';
import Suggestions from '~/vue_shared/components/markdown/suggestions.vue';
+import { renderGFM } from '~/behaviors/markdown/render_gfm';
import autosave from '../mixins/autosave';
import NoteAttachment from './note_attachment.vue';
import NoteAwardsList from './note_awards_list.vue';
@@ -22,7 +20,7 @@ export default {
Suggestions,
},
directives: {
- SafeHtml: GlSafeHtmlDirective,
+ SafeHtml,
},
mixins: [autosave],
props: {
@@ -122,7 +120,7 @@ export default {
'removeSuggestionInfoFromBatch',
]),
renderGFM() {
- $(this.$refs['note-body']).renderGFM();
+ renderGFM(this.$refs['note-body']);
},
handleFormUpdate(noteText, parentElement, callback, resolveDiscussion) {
this.$emit('handleFormUpdate', { noteText, parentElement, callback, resolveDiscussion });
diff --git a/app/assets/javascripts/notes/components/note_header.vue b/app/assets/javascripts/notes/components/note_header.vue
index 63c7010983e..36f7d720e48 100644
--- a/app/assets/javascripts/notes/components/note_header.vue
+++ b/app/assets/javascripts/notes/components/note_header.vue
@@ -1,17 +1,10 @@
<script>
-import {
- GlIcon,
- GlBadge,
- GlLoadingIcon,
- GlTooltipDirective,
- GlSafeHtmlDirective as SafeHtml,
-} from '@gitlab/ui';
+import { GlIcon, GlBadge, GlLoadingIcon, GlTooltipDirective } from '@gitlab/ui';
import { mapActions } from 'vuex';
import { __, s__ } from '~/locale';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
export default {
- safeHtmlConfig: { ADD_TAGS: ['gl-emoji'] },
components: {
TimeAgoTooltip,
GitlabTeamMemberBadge: () =>
@@ -21,7 +14,6 @@ export default {
GlLoadingIcon,
},
directives: {
- SafeHtml,
GlTooltip: GlTooltipDirective,
},
props: {
diff --git a/app/assets/javascripts/notes/components/note_signed_out_widget.vue b/app/assets/javascripts/notes/components/note_signed_out_widget.vue
index 593933016e1..94636b3e47b 100644
--- a/app/assets/javascripts/notes/components/note_signed_out_widget.vue
+++ b/app/assets/javascripts/notes/components/note_signed_out_widget.vue
@@ -1,6 +1,6 @@
<script>
-import { GlSafeHtmlDirective as SafeHtml } from '@gitlab/ui';
import { mapGetters } from 'vuex';
+import SafeHtml from '~/vue_shared/directives/safe_html';
import { __, sprintf } from '~/locale';
export default {
diff --git a/app/assets/javascripts/notes/components/noteable_discussion.vue b/app/assets/javascripts/notes/components/noteable_discussion.vue
index b668d6ec182..ff801cdccea 100644
--- a/app/assets/javascripts/notes/components/noteable_discussion.vue
+++ b/app/assets/javascripts/notes/components/noteable_discussion.vue
@@ -235,7 +235,7 @@ export default {
this.saveNote(replyData)
.then((res) => {
- if (res.hasFlash !== true) {
+ if (res.hasAlert !== true) {
this.isReplying = false;
clearDraft(this.autosaveKey);
}
@@ -307,7 +307,7 @@ export default {
:draft="draftForDiscussion(discussion.reply_id)"
:line="line"
/>
- <div
+ <li
v-else-if="canShowReplyActions && showReplies"
:class="{ 'is-replying': isReplying }"
class="discussion-reply-holder gl-border-t-0! clearfix"
@@ -334,7 +334,7 @@ export default {
@cancelForm="cancelReplyForm"
/>
<note-signed-out-widget v-if="!isLoggedIn" />
- </div>
+ </li>
</template>
</discussion-notes>
</component>
diff --git a/app/assets/javascripts/notes/components/noteable_note.vue b/app/assets/javascripts/notes/components/noteable_note.vue
index 8ce0c2f8648..826e7e5a3d0 100644
--- a/app/assets/javascripts/notes/components/noteable_note.vue
+++ b/app/assets/javascripts/notes/components/noteable_note.vue
@@ -1,16 +1,18 @@
<script>
-import { GlSprintf, GlSafeHtmlDirective as SafeHtml, GlAvatarLink, GlAvatar } from '@gitlab/ui';
+import { GlSprintf, GlAvatarLink, GlAvatar } from '@gitlab/ui';
import $ from 'jquery';
import { escape, isEmpty } from 'lodash';
import { mapGetters, mapActions } from 'vuex';
+import SafeHtml from '~/vue_shared/directives/safe_html';
import { confirmAction } from '~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal';
import { INLINE_DIFF_LINES_KEY } from '~/diffs/constants';
import { createAlert } from '~/flash';
-import httpStatusCodes from '~/lib/utils/http_status';
+import { HTTP_STATUS_GONE } from '~/lib/utils/http_status';
import { ignoreWhilePending } from '~/lib/utils/ignore_while_pending';
import { truncateSha } from '~/lib/utils/text_utility';
import TimelineEntryItem from '~/vue_shared/components/notes/timeline_entry_item.vue';
import { __, s__, sprintf } from '~/locale';
+import { renderGFM } from '~/behaviors/markdown/render_gfm';
import eventHub from '../event_hub';
import noteable from '../mixins/noteable';
import resolvable from '../mixins/resolvable';
@@ -286,7 +288,7 @@ export default {
this.isEditing = false;
this.isRequesting = false;
this.oldContent = null;
- $(this.$refs.noteBody.$el).renderGFM();
+ renderGFM(this.$refs.noteBody.$el);
this.$refs.noteBody.resetAutoSave();
this.$emit('updateSuccess');
},
@@ -336,7 +338,7 @@ export default {
callback();
})
.catch((response) => {
- if (response.status === httpStatusCodes.GONE) {
+ if (response.status === HTTP_STATUS_GONE) {
this.removeNote(this.note);
this.updateSuccess();
callback();
@@ -515,6 +517,9 @@ export default {
@handleFormUpdate="formUpdateHandler"
@cancelForm="formCancelHandler"
/>
+ <div class="timeline-discussion-body-footer">
+ <slot name="after-note-body"></slot>
+ </div>
</div>
</div>
</timeline-entry-item>
diff --git a/app/assets/javascripts/notes/components/notes_app.vue b/app/assets/javascripts/notes/components/notes_app.vue
index 7bb1a1a1bfe..fcf37217902 100644
--- a/app/assets/javascripts/notes/components/notes_app.vue
+++ b/app/assets/javascripts/notes/components/notes_app.vue
@@ -1,13 +1,11 @@
<script>
import { mapGetters, mapActions } from 'vuex';
import highlightCurrentUser from '~/behaviors/markdown/highlight_current_user';
-import { createAlert } from '~/flash';
-import { __ } from '~/locale';
import TimelineEntryItem from '~/vue_shared/components/notes/timeline_entry_item.vue';
import OrderedLayout from '~/vue_shared/components/ordered_layout.vue';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import DraftNote from '~/batch_comments/components/draft_note.vue';
-import { getLocationHash, doesHashExistInUrl } from '~/lib/utils/url_utility';
+import { getLocationHash } from '~/lib/utils/url_utility';
import PlaceholderNote from '~/vue_shared/components/notes/placeholder_note.vue';
import PlaceholderSystemNote from '~/vue_shared/components/notes/placeholder_system_note.vue';
import SkeletonLoadingContainer from '~/vue_shared/components/notes/skeleton_note.vue';
@@ -57,11 +55,6 @@ export default {
default: undefined,
required: false,
},
- userData: {
- type: Object,
- required: false,
- default: () => ({}),
- },
shouldShow: {
type: Boolean,
required: false,
@@ -90,16 +83,12 @@ export default {
'commentsDisabled',
'getNoteableData',
'userCanReply',
- 'discussionTabCounter',
'sortDirection',
'timelineEnabled',
]),
sortDirDesc() {
return this.sortDirection === constants.DESC;
},
- discussionTabCounterText() {
- return this.isLoading ? '' : this.discussionTabCounter;
- },
noteableType() {
return this.noteableData.noteableType;
},
@@ -147,11 +136,6 @@ export default {
this.renderSkeleton = !this.shouldShow;
});
},
- discussionTabCounterText(val) {
- if (this.discussionsCount) {
- this.discussionsCount.textContent = val;
- }
- },
isAppReady: {
handler(isReady) {
if (!isReady) return;
@@ -162,20 +146,7 @@ export default {
immediate: true,
},
},
- created() {
- this.discussionsCount = document.querySelector('.js-discussions-count');
-
- this.setNotesData(this.notesData);
- this.setNoteableData(this.noteableData);
- this.setUserData(this.userData);
- this.setTargetNoteHash(getLocationHash());
- eventHub.$once('fetchNotesData', this.fetchNotes);
- },
mounted() {
- if (this.shouldShow) {
- this.fetchNotes();
- }
-
const { parentElement } = this.$el;
if (parentElement && parentElement.classList.contains('js-vue-notes-event')) {
parentElement.addEventListener('toggleAward', (event) => {
@@ -200,23 +171,16 @@ export default {
},
methods: {
...mapActions([
- 'setFetchingState',
- 'setLoadingState',
- 'fetchDiscussions',
- 'poll',
'toggleAward',
- 'setNotesData',
- 'setNoteableData',
- 'setUserData',
'setLastFetchedAt',
'setTargetNoteHash',
'toggleDiscussion',
- 'setNotesFetchedState',
'expandDiscussion',
'startTaskList',
'convertToDiscussion',
'stopPolling',
'setConfidentiality',
+ 'fetchNotes',
]),
discussionIsIndividualNoteAndNotConverted(discussion) {
return discussion.individual_note && !this.convertedDisscussionIds.includes(discussion.id);
@@ -228,37 +192,6 @@ export default {
this.setTargetNoteHash(getLocationHash());
}
},
- fetchNotes() {
- if (this.isFetching) return null;
-
- this.setFetchingState(true);
-
- return this.fetchDiscussions(this.getFetchDiscussionsConfig())
- .then(this.initPolling)
- .then(() => {
- this.setLoadingState(false);
- this.setNotesFetchedState(true);
- eventHub.$emit('fetchedNotesData');
- this.setFetchingState(false);
- })
- .catch(() => {
- this.setLoadingState(false);
- this.setNotesFetchedState(true);
- createAlert({
- message: __('Something went wrong while fetching comments. Please try again.'),
- });
- });
- },
- initPolling() {
- if (this.isPollingInitialized) {
- return;
- }
-
- this.setLastFetchedAt(this.getNotesDataByProp('lastFetchedAt'));
-
- this.poll();
- this.isPollingInitialized = true;
- },
checkLocationHash() {
const hash = getLocationHash();
const noteId = hash && hash.replace(/^note_/, '');
@@ -278,24 +211,6 @@ export default {
.then(this.$nextTick)
.then(() => eventHub.$emit('startReplying', discussionId));
},
- getFetchDiscussionsConfig() {
- const defaultConfig = { path: this.getNotesDataByProp('discussionsPath') };
-
- const currentFilter =
- this.getNotesDataByProp('notesFilter') || constants.DISCUSSION_FILTERS_DEFAULT_VALUE;
-
- if (
- doesHashExistInUrl(constants.NOTE_UNDERSCORE) &&
- currentFilter !== constants.DISCUSSION_FILTERS_DEFAULT_VALUE
- ) {
- return {
- ...defaultConfig,
- filter: constants.DISCUSSION_FILTERS_DEFAULT_VALUE,
- persistFilter: false,
- };
- }
- return defaultConfig;
- },
},
systemNote: constants.SYSTEM_NOTE,
};
diff --git a/app/assets/javascripts/notes/index.js b/app/assets/javascripts/notes/index.js
index defcb0533b7..95263e666b2 100644
--- a/app/assets/javascripts/notes/index.js
+++ b/app/assets/javascripts/notes/index.js
@@ -1,5 +1,6 @@
import Vue from 'vue';
import { parseBoolean } from '~/lib/utils/common_utils';
+import { getLocationHash } from '~/lib/utils/url_utility';
import NotesApp from './components/notes_app.vue';
import { store } from './stores';
import { getNotesFilterData } from './utils/get_notes_filter_data';
@@ -13,6 +14,34 @@ export default () => {
const notesFilterProps = getNotesFilterData(el);
const showTimelineViewToggle = parseBoolean(el.dataset.showTimelineViewToggle);
+ const notesDataset = el.dataset;
+ const parsedUserData = JSON.parse(notesDataset.currentUserData);
+ const noteableData = JSON.parse(notesDataset.noteableData);
+ let currentUserData = {};
+
+ noteableData.noteableType = notesDataset.noteableType;
+ noteableData.targetType = notesDataset.targetType;
+ noteableData.discussion_locked = parseBoolean(noteableData.discussion_locked);
+
+ if (parsedUserData) {
+ currentUserData = {
+ id: parsedUserData.id,
+ name: parsedUserData.name,
+ username: parsedUserData.username,
+ avatar_url: parsedUserData.avatar_path || parsedUserData.avatar_url,
+ path: parsedUserData.path,
+ can_add_timeline_events: parseBoolean(notesDataset.canAddTimelineEvents),
+ };
+ }
+
+ const notesData = JSON.parse(notesDataset.notesData);
+
+ store.dispatch('setNotesData', notesData);
+ store.dispatch('setNoteableData', noteableData);
+ store.dispatch('setUserData', currentUserData);
+ store.dispatch('setTargetNoteHash', getLocationHash());
+ store.dispatch('fetchNotes');
+
// eslint-disable-next-line no-new
new Vue({
el,
@@ -25,30 +54,6 @@ export default () => {
showTimelineViewToggle,
},
data() {
- const notesDataset = el.dataset;
- const parsedUserData = JSON.parse(notesDataset.currentUserData);
- const noteableData = JSON.parse(notesDataset.noteableData);
- let currentUserData = {};
-
- noteableData.noteableType = notesDataset.noteableType;
- noteableData.targetType = notesDataset.targetType;
- if (noteableData.discussion_locked === null) {
- // discussion_locked has never been set for this issuable.
- // set to `false` for safety.
- noteableData.discussion_locked = false;
- }
-
- if (parsedUserData) {
- currentUserData = {
- id: parsedUserData.id,
- name: parsedUserData.name,
- username: parsedUserData.username,
- avatar_url: parsedUserData.avatar_path || parsedUserData.avatar_url,
- path: parsedUserData.path,
- can_add_timeline_events: parseBoolean(notesDataset.canAddTimelineEvents),
- };
- }
-
return {
noteableData,
currentUserData,
diff --git a/app/assets/javascripts/notes/stores/actions.js b/app/assets/javascripts/notes/stores/actions.js
index fcef26d720c..d290a8ccb84 100644
--- a/app/assets/javascripts/notes/stores/actions.js
+++ b/app/assets/javascripts/notes/stores/actions.js
@@ -2,14 +2,14 @@ import $ from 'jquery';
import Visibility from 'visibilityjs';
import Vue from 'vue';
import Api from '~/api';
-import createFlash from '~/flash';
+import { createAlert, VARIANT_INFO } from '~/flash';
import { EVENT_ISSUABLE_VUE_APP_CHANGE } from '~/issuable/constants';
import axios from '~/lib/utils/axios_utils';
import { __, sprintf } from '~/locale';
import toast from '~/vue_shared/plugins/global_toast';
import { confidentialWidget } from '~/sidebar/components/confidential/sidebar_confidentiality_widget.vue';
-import updateIssueLockMutation from '~/sidebar/components/lock/mutations/update_issue_lock.mutation.graphql';
-import updateMergeRequestLockMutation from '~/sidebar/components/lock/mutations/update_merge_request_lock.mutation.graphql';
+import updateIssueLockMutation from '~/sidebar/queries/update_issue_lock.mutation.graphql';
+import updateMergeRequestLockMutation from '~/sidebar/queries/update_merge_request_lock.mutation.graphql';
import loadAwardsHandler from '~/awards_handler';
import { isInViewport, scrollToElement, isInMRPage } from '~/lib/utils/common_utils';
import Poll from '~/lib/utils/poll';
@@ -114,6 +114,39 @@ export const fetchDiscussions = (
});
};
+export const fetchNotes = ({ dispatch, getters }) => {
+ if (getters.isFetching) return null;
+
+ dispatch('setFetchingState', true);
+
+ return dispatch('fetchDiscussions', getters.getFetchDiscussionsConfig)
+ .then(() => dispatch('initPolling'))
+ .then(() => {
+ dispatch('setLoadingState', false);
+ dispatch('setNotesFetchedState', true);
+ notesEventHub.$emit('fetchedNotesData');
+ dispatch('setFetchingState', false);
+ })
+ .catch(() => {
+ dispatch('setLoadingState', false);
+ dispatch('setNotesFetchedState', true);
+ createAlert({
+ message: __('Something went wrong while fetching comments. Please try again.'),
+ });
+ });
+};
+
+export const initPolling = ({ state, dispatch, getters, commit }) => {
+ if (state.isPollingInitialized) {
+ return;
+ }
+
+ dispatch('setLastFetchedAt', getters.getNotesDataByProp('lastFetchedAt'));
+
+ dispatch('poll');
+ commit(types.SET_IS_POLLING_INITIALIZED, true);
+};
+
export const fetchDiscussionsBatch = ({ commit, dispatch }, { path, config, cursor, perPage }) => {
const params = { ...config?.params, per_page: perPage };
@@ -270,7 +303,7 @@ export const promoteCommentToTimelineEvent = (
errorObj = error;
}
- createFlash({
+ createAlert({
message,
captureError,
error: errorObj,
@@ -465,9 +498,9 @@ export const saveNote = ({ commit, dispatch }, noteData) => {
$('.js-gfm-input').trigger('clear-commands-cache.atwho');
- createFlash({
+ createAlert({
message: message || __('Commands applied'),
- type: 'notice',
+ variant: VARIANT_INFO,
parent: noteData.flashContainer,
});
}
@@ -490,7 +523,7 @@ export const saveNote = ({ commit, dispatch }, noteData) => {
awardsHandler.scrollToAwards();
})
.catch(() => {
- createFlash({
+ createAlert({
message: __('Something went wrong while adding your award. Please try again.'),
parent: noteData.flashContainer,
});
@@ -529,11 +562,11 @@ export const saveNote = ({ commit, dispatch }, noteData) => {
const errorMsg = sprintf(__('Your comment could not be submitted because %{error}'), {
error: base[0].toLowerCase(),
});
- createFlash({
+ createAlert({
message: errorMsg,
parent: noteData.flashContainer,
});
- return { ...data, hasFlash: true };
+ return { ...data, hasAlert: true };
}
}
@@ -580,7 +613,7 @@ const getFetchDataParams = (state) => {
export const poll = ({ commit, state, getters, dispatch }) => {
const notePollOccurrenceTracking = create();
- let flashContainer;
+ let alert;
notePollOccurrenceTracking.handle(1, () => {
// Since polling halts internally after 1 failure, we manually try one more time
@@ -588,7 +621,7 @@ export const poll = ({ commit, state, getters, dispatch }) => {
});
notePollOccurrenceTracking.handle(2, () => {
// On the second failure in a row, show the alert and try one more time (hoping to succeed and clear the error)
- flashContainer = createFlash({
+ alert = createAlert({
message: __('Something went wrong while fetching latest comments.'),
});
setTimeout(() => eTagPoll.restart(), NOTES_POLLING_INTERVAL);
@@ -608,7 +641,7 @@ export const poll = ({ commit, state, getters, dispatch }) => {
if (notePollOccurrenceTracking.count) {
notePollOccurrenceTracking.reset();
}
- flashContainer?.close();
+ alert?.dismiss();
},
errorCallback: () => notePollOccurrenceTracking.occur(),
});
@@ -681,7 +714,7 @@ export const filterDiscussion = ({ commit, dispatch }, { path, filter, persistFi
.catch(() => {
dispatch('setLoadingState', false);
dispatch('setNotesFetchedState', true);
- createFlash({
+ createAlert({
message: __('Something went wrong while fetching comments. Please try again.'),
});
});
@@ -726,7 +759,7 @@ export const submitSuggestion = (
const flashMessage = errorMessage || defaultMessage;
- createFlash({
+ createAlert({
message: flashMessage,
parent: flashContainer,
});
@@ -762,7 +795,7 @@ export const submitSuggestionBatch = ({ commit, dispatch, state }, { message, fl
const flashMessage = errorMessage || defaultMessage;
- createFlash({
+ createAlert({
message: flashMessage,
parent: flashContainer,
});
@@ -804,7 +837,7 @@ export const fetchDescriptionVersion = ({ dispatch }, { endpoint, startingVersio
})
.catch((error) => {
dispatch('receiveDescriptionVersionError', error);
- createFlash({
+ createAlert({
message: __('Something went wrong while fetching description changes. Please try again.'),
});
});
@@ -838,7 +871,7 @@ export const softDeleteDescriptionVersion = (
})
.catch((error) => {
dispatch('receiveDeleteDescriptionVersionError', error);
- createFlash({
+ createAlert({
message: __('Something went wrong while deleting description changes. Please try again.'),
});
diff --git a/app/assets/javascripts/notes/stores/getters.js b/app/assets/javascripts/notes/stores/getters.js
index 5ad7a811726..f6373f24b74 100644
--- a/app/assets/javascripts/notes/stores/getters.js
+++ b/app/assets/javascripts/notes/stores/getters.js
@@ -2,6 +2,7 @@ import { flattenDeep, clone } from 'lodash';
import { match } from '~/diffs/utils/diff_file';
import { badgeState } from '~/issuable/components/status_box.vue';
import { isInMRPage } from '~/lib/utils/common_utils';
+import { doesHashExistInUrl } from '~/lib/utils/url_utility';
import * as constants from '../constants';
import { collapseSystemNotes } from './collapse_utils';
@@ -314,3 +315,22 @@ export const getSuggestionsFilePaths = (state) => () =>
return acc;
}, []);
+
+export const getFetchDiscussionsConfig = (state, getters) => {
+ const defaultConfig = { path: getters.getNotesDataByProp('discussionsPath') };
+
+ const currentFilter =
+ getters.getNotesDataByProp('notesFilter') || constants.DISCUSSION_FILTERS_DEFAULT_VALUE;
+
+ if (
+ doesHashExistInUrl(constants.NOTE_UNDERSCORE) &&
+ currentFilter !== constants.DISCUSSION_FILTERS_DEFAULT_VALUE
+ ) {
+ return {
+ ...defaultConfig,
+ filter: constants.DISCUSSION_FILTERS_DEFAULT_VALUE,
+ persistFilter: false,
+ };
+ }
+ return defaultConfig;
+};
diff --git a/app/assets/javascripts/notes/stores/modules/index.js b/app/assets/javascripts/notes/stores/modules/index.js
index 7ba1f470b05..81c4c42a49a 100644
--- a/app/assets/javascripts/notes/stores/modules/index.js
+++ b/app/assets/javascripts/notes/stores/modules/index.js
@@ -50,6 +50,7 @@ export default () => ({
descriptionVersions: {},
isTimelineEnabled: false,
isFetching: false,
+ isPollingInitialized: false,
},
actions,
getters,
diff --git a/app/assets/javascripts/notes/stores/mutation_types.js b/app/assets/javascripts/notes/stores/mutation_types.js
index 42df6bc0980..bc1d5b5bba4 100644
--- a/app/assets/javascripts/notes/stores/mutation_types.js
+++ b/app/assets/javascripts/notes/stores/mutation_types.js
@@ -27,6 +27,7 @@ export const CLEAR_SUGGESTION_BATCH = 'CLEAR_SUGGESTION_BATCH';
export const CONVERT_TO_DISCUSSION = 'CONVERT_TO_DISCUSSION';
export const REMOVE_CONVERTED_DISCUSSION = 'REMOVE_CONVERTED_DISCUSSION';
export const UPDATE_ASSIGNEES = 'UPDATE_ASSIGNEES';
+export const SET_IS_POLLING_INITIALIZED = 'SET_IS_POLLING_INITIALIZED';
// DISCUSSION
export const COLLAPSE_DISCUSSION = 'COLLAPSE_DISCUSSION';
diff --git a/app/assets/javascripts/notes/stores/mutations.js b/app/assets/javascripts/notes/stores/mutations.js
index 83c15c12eac..5d532b68f1b 100644
--- a/app/assets/javascripts/notes/stores/mutations.js
+++ b/app/assets/javascripts/notes/stores/mutations.js
@@ -428,4 +428,7 @@ export default {
[types.SET_PROMOTE_COMMENT_TO_TIMELINE_PROGRESS](state, value) {
state.isPromoteCommentToTimelineEventInProgress = value;
},
+ [types.SET_IS_POLLING_INITIALIZED](state, value) {
+ state.isPollingInitialized = value;
+ },
};
diff --git a/app/assets/javascripts/observability/components/observability_app.vue b/app/assets/javascripts/observability/components/observability_app.vue
index 4f5e27be46f..33d23ea043b 100644
--- a/app/assets/javascripts/observability/components/observability_app.vue
+++ b/app/assets/javascripts/observability/components/observability_app.vue
@@ -1,21 +1,69 @@
<script>
+import { darkModeEnabled } from '~/lib/utils/color_utils';
+import { setUrlParams } from '~/lib/utils/url_utility';
+
+import { MESSAGE_EVENT_TYPE, OBSERVABILITY_ROUTES, SKELETON_VARIANT } from '../constants';
+import ObservabilitySkeleton from './skeleton/index.vue';
+
export default {
+ components: {
+ ObservabilitySkeleton,
+ },
props: {
observabilityIframeSrc: {
type: String,
required: true,
},
},
+ computed: {
+ iframeSrcWithParams() {
+ return setUrlParams(
+ { theme: darkModeEnabled() ? 'dark' : 'light', username: gon?.current_username },
+ this.observabilityIframeSrc,
+ );
+ },
+ getSkeletonVariant() {
+ switch (this.$route.path) {
+ case OBSERVABILITY_ROUTES.DASHBOARDS:
+ return SKELETON_VARIANT.DASHBOARDS;
+ case OBSERVABILITY_ROUTES.EXPLORE:
+ return SKELETON_VARIANT.EXPLORE;
+ case OBSERVABILITY_ROUTES.MANAGE:
+ return SKELETON_VARIANT.MANAGE;
+ default:
+ return SKELETON_VARIANT.DASHBOARDS;
+ }
+ },
+ },
mounted() {
window.addEventListener('message', this.messageHandler);
},
+ destroyed() {
+ window.removeEventListener('message', this.messageHandler);
+ },
methods: {
messageHandler(e) {
const isExpectedOrigin = e.origin === new URL(this.observabilityIframeSrc)?.origin;
+ if (!isExpectedOrigin) return;
- const isNewObservabilityPath = this.$route?.query?.observability_path !== e.data?.url;
+ const {
+ data: { type, payload },
+ } = e;
+ switch (type) {
+ case MESSAGE_EVENT_TYPE.GOUI_LOADED:
+ this.$refs.iframeSkeleton.handleSkeleton();
+ break;
+ case MESSAGE_EVENT_TYPE.GOUI_ROUTE_UPDATE:
+ this.routeUpdateHandler(payload);
+ break;
+ default:
+ break;
+ }
+ },
+ routeUpdateHandler(payload) {
+ const isNewObservabilityPath = this.$route?.query?.observability_path !== payload?.url;
- const shouldNotHandleMessage = !isExpectedOrigin || !e.data.url || !isNewObservabilityPath;
+ const shouldNotHandleMessage = !payload.url || !isNewObservabilityPath;
if (shouldNotHandleMessage) {
return;
@@ -24,7 +72,7 @@ export default {
// this will update the `observability_path` query param on each route change inside Observability UI
this.$router.replace({
name: this.$route.pathname,
- query: { ...this.$route.query, observability_path: e.data.url },
+ query: { ...this.$route.query, observability_path: payload.url },
});
},
},
@@ -32,11 +80,14 @@ export default {
</script>
<template>
- <iframe
- id="observability-ui-iframe"
- data-testid="observability-ui-iframe"
- frameborder="0"
- height="100%"
- :src="observabilityIframeSrc"
- ></iframe>
+ <observability-skeleton ref="iframeSkeleton" :variant="getSkeletonVariant">
+ <iframe
+ id="observability-ui-iframe"
+ data-testid="observability-ui-iframe"
+ frameborder="0"
+ height="100%"
+ :src="iframeSrcWithParams"
+ sandbox="allow-same-origin allow-forms allow-scripts"
+ ></iframe>
+ </observability-skeleton>
</template>
diff --git a/app/assets/javascripts/observability/components/skeleton/dashboards.vue b/app/assets/javascripts/observability/components/skeleton/dashboards.vue
new file mode 100644
index 00000000000..8b106407953
--- /dev/null
+++ b/app/assets/javascripts/observability/components/skeleton/dashboards.vue
@@ -0,0 +1,29 @@
+<script>
+import { GlSkeletonLoader } from '@gitlab/ui';
+
+export default {
+ components: {
+ GlSkeletonLoader,
+ },
+};
+</script>
+<template>
+ <gl-skeleton-loader :height="200">
+ <!-- Top left -->
+ <rect y="2" width="10" height="8" />
+ <rect y="2" x="15" width="15" height="8" />
+ <rect y="2" x="35" width="15" height="8" />
+
+ <!-- Top right -->
+ <rect y="2" x="354" width="10" height="8" />
+ <rect y="2" x="366" width="10" height="8" />
+ <rect y="2" x="378" width="10" height="8" />
+ <rect y="2" x="390" width="10" height="8" />
+
+ <!-- Middle header -->
+ <rect y="15" width="400" height="30" rx="2" ry="2" />
+
+ <!-- Dashboard container -->
+ <rect y="50" width="200" height="100" rx="2" ry="2" />
+ </gl-skeleton-loader>
+</template>
diff --git a/app/assets/javascripts/observability/components/skeleton/explore.vue b/app/assets/javascripts/observability/components/skeleton/explore.vue
new file mode 100644
index 00000000000..1fcbd4fb1cb
--- /dev/null
+++ b/app/assets/javascripts/observability/components/skeleton/explore.vue
@@ -0,0 +1,27 @@
+<script>
+import { GlSkeletonLoader } from '@gitlab/ui';
+
+export default {
+ components: {
+ GlSkeletonLoader,
+ },
+};
+</script>
+<template>
+ <gl-skeleton-loader :height="200">
+ <!-- Top left -->
+ <circle y="2" cx="6" cy="6" r="4" />
+ <rect y="2" x="15" width="15" height="8" />
+ <rect y="2" x="35" width="40" height="8" />
+
+ <!-- Top right -->
+
+ <rect y="2" x="263" width="13" height="8" />
+ <rect y="2" x="278" width="8" height="8" />
+ <rect y="2" x="288" width="50" height="8" />
+ <rect y="2" x="340" width="18" height="8" />
+ <rect y="2" x="360" width="30" height="8" />
+
+ <rect y="15" width="400" height="30" rx="2" ry="2" />
+ </gl-skeleton-loader>
+</template>
diff --git a/app/assets/javascripts/observability/components/skeleton/index.vue b/app/assets/javascripts/observability/components/skeleton/index.vue
new file mode 100644
index 00000000000..1e2671c8166
--- /dev/null
+++ b/app/assets/javascripts/observability/components/skeleton/index.vue
@@ -0,0 +1,89 @@
+<script>
+import { GlSkeletonLoader } from '@gitlab/ui';
+import { SKELETON_VARIANT } from '../../constants';
+import DashboardsSkeleton from './dashboards.vue';
+import ExploreSkeleton from './explore.vue';
+import ManageSkeleton from './manage.vue';
+
+export default {
+ SKELETON_VARIANT,
+ components: {
+ GlSkeletonLoader,
+ DashboardsSkeleton,
+ ExploreSkeleton,
+ ManageSkeleton,
+ },
+ props: {
+ variant: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ },
+ data() {
+ return {
+ loading: null,
+ timerId: null,
+ };
+ },
+ mounted() {
+ this.timerId = setTimeout(() => {
+ /**
+ * If observability UI is not loaded then this.loading would be null
+ * we will show skeleton in that case
+ */
+ if (this.loading !== false) {
+ this.showSkeleton();
+ }
+ }, 500);
+ },
+ methods: {
+ handleSkeleton() {
+ if (this.loading === null) {
+ /**
+ * If observability UI content loads with in 500ms
+ * do not show skeleton.
+ */
+ clearTimeout(this.timerId);
+ return;
+ }
+
+ /**
+ * If observability UI content loads after 500ms
+ * wait for 400ms to hide skeleton.
+ * This is mostly to avoid the flashing effect If content loads imediately after skeleton
+ */
+ setTimeout(this.hideSkeleton, 400);
+ },
+ hideSkeleton() {
+ this.loading = false;
+ },
+ showSkeleton() {
+ this.loading = true;
+ },
+ },
+};
+</script>
+<template>
+ <div class="gl-flex-grow-1 gl-display-flex gl-flex-direction-column gl-flex-align-items-stretch">
+ <div v-show="loading" class="gl-px-5">
+ <dashboards-skeleton v-if="variant === $options.SKELETON_VARIANT.DASHBOARDS" />
+ <explore-skeleton v-else-if="variant === $options.SKELETON_VARIANT.EXPLORE" />
+ <manage-skeleton v-else-if="variant === $options.SKELETON_VARIANT.MANAGE" />
+
+ <gl-skeleton-loader v-else>
+ <rect y="2" width="10" height="8" />
+ <rect y="2" x="15" width="15" height="8" />
+ <rect y="2" x="35" width="15" height="8" />
+ <rect y="15" width="400" height="30" />
+ </gl-skeleton-loader>
+ </div>
+
+ <div
+ v-show="!loading"
+ class="gl-flex-grow-1 gl-display-flex gl-flex-direction-column gl-flex-align-items-stretch"
+ >
+ <slot></slot>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/observability/components/skeleton/manage.vue b/app/assets/javascripts/observability/components/skeleton/manage.vue
new file mode 100644
index 00000000000..4b029120328
--- /dev/null
+++ b/app/assets/javascripts/observability/components/skeleton/manage.vue
@@ -0,0 +1,25 @@
+<script>
+import { GlSkeletonLoader } from '@gitlab/ui';
+
+export default {
+ components: {
+ GlSkeletonLoader,
+ },
+};
+</script>
+<template>
+ <gl-skeleton-loader :height="200">
+ <!-- Top header-->
+ <rect y="2" width="400" height="30" />
+
+ <rect y="35" x="65" width="80" height="8" />
+ <rect y="35" x="205" width="30" height="8" />
+ <rect y="35" x="240" width="25" height="8" />
+ <rect y="35" x="270" width="20" height="8" />
+
+ <rect y="55" x="65" width="100" height="8" />
+ <rect y="55" x="225" width="65" height="8" />
+
+ <rect y="65" x="65" width="225" height="200" rx="2" ry="2" />
+ </gl-skeleton-loader>
+</template>
diff --git a/app/assets/javascripts/observability/constants.js b/app/assets/javascripts/observability/constants.js
new file mode 100644
index 00000000000..74dd543e285
--- /dev/null
+++ b/app/assets/javascripts/observability/constants.js
@@ -0,0 +1,16 @@
+export const MESSAGE_EVENT_TYPE = Object.freeze({
+ GOUI_LOADED: 'GOUI_LOADED',
+ GOUI_ROUTE_UPDATE: 'GOUI_ROUTE_UPDATE',
+});
+
+export const OBSERVABILITY_ROUTES = Object.freeze({
+ DASHBOARDS: '/groups/gitlab-org/-/observability/dashboards',
+ EXPLORE: '/groups/gitlab-org/-/observability/explore',
+ MANAGE: '/groups/gitlab-org/-/observability/manage',
+});
+
+export const SKELETON_VARIANT = Object.freeze({
+ DASHBOARDS: 'dashboards',
+ EXPLORE: 'explore',
+ MANAGE: 'manage',
+});
diff --git a/app/assets/javascripts/packages_and_registries/container_registry/explorer/components/details_page/delete_alert.vue b/app/assets/javascripts/packages_and_registries/container_registry/explorer/components/details_page/delete_alert.vue
index 1b7d5af6134..56d2ff86fb7 100644
--- a/app/assets/javascripts/packages_and_registries/container_registry/explorer/components/details_page/delete_alert.vue
+++ b/app/assets/javascripts/packages_and_registries/container_registry/explorer/components/details_page/delete_alert.vue
@@ -1,11 +1,7 @@
<script>
import { GlSprintf, GlAlert, GlLink } from '@gitlab/ui';
-import {
- ALERT_MESSAGES,
- ADMIN_GARBAGE_COLLECTION_TIP,
- ALERT_DANGER_IMPORTING,
-} from '../../constants/index';
+import { ALERT_MESSAGES, ADMIN_GARBAGE_COLLECTION_TIP } from '../../constants/index';
export default {
components: {
@@ -27,7 +23,6 @@ export default {
},
},
garbageCollectionHelpPagePath: { type: String, required: false, default: '' },
- containerRegistryImportingHelpPagePath: { type: String, required: false, default: '' },
isAdmin: {
type: Boolean,
default: false,
@@ -53,11 +48,6 @@ export default {
}
return config;
},
- alertHref() {
- return this.deleteAlertType === ALERT_DANGER_IMPORTING
- ? this.containerRegistryImportingHelpPagePath
- : this.garbageCollectionHelpPagePath;
- },
},
};
</script>
@@ -71,7 +61,7 @@ export default {
>
<gl-sprintf :message="deleteAlertConfig.message">
<template #docLink="{ content }">
- <gl-link :href="alertHref" target="_blank">
+ <gl-link :href="garbageCollectionHelpPagePath" target="_blank">
{{ content }}
</gl-link>
</template>
diff --git a/app/assets/javascripts/packages_and_registries/container_registry/explorer/components/details_page/tags_list.vue b/app/assets/javascripts/packages_and_registries/container_registry/explorer/components/details_page/tags_list.vue
index 597df2b9bc3..c10d8be69a0 100644
--- a/app/assets/javascripts/packages_and_registries/container_registry/explorer/components/details_page/tags_list.vue
+++ b/app/assets/javascripts/packages_and_registries/container_registry/explorer/components/details_page/tags_list.vue
@@ -6,8 +6,8 @@ import { joinPaths } from '~/lib/utils/url_utility';
import RegistryList from '~/packages_and_registries/shared/components/registry_list.vue';
import PersistedSearch from '~/packages_and_registries/shared/components/persisted_search.vue';
-import { FILTERED_SEARCH_TERM } from '~/packages_and_registries/shared/constants';
import TagsLoader from '~/packages_and_registries/shared/components/tags_loader.vue';
+import { FILTERED_SEARCH_TERM } from '~/vue_shared/components/filtered_search_bar/constants';
import {
REMOVE_TAGS_BUTTON_TITLE,
TAGS_LIST_TITLE,
diff --git a/app/assets/javascripts/packages_and_registries/container_registry/explorer/constants/details.js b/app/assets/javascripts/packages_and_registries/container_registry/explorer/constants/details.js
index 98c24350f09..7bb69363743 100644
--- a/app/assets/javascripts/packages_and_registries/container_registry/explorer/constants/details.js
+++ b/app/assets/javascripts/packages_and_registries/container_registry/explorer/constants/details.js
@@ -93,10 +93,6 @@ export const DETAILS_DELETE_IMAGE_ERROR_MESSAGE = s__(
'ContainerRegistry|Something went wrong while scheduling the image for deletion.',
);
-export const DETAILS_IMPORTING_ERROR_MESSAGE = s__(
- 'ContainerRegistry|Tags temporarily cannot be marked for deletion. Please try again in a few minutes. %{docLinkStart}More details%{docLinkEnd}.',
-);
-
export const DELETE_IMAGE_CONFIRMATION_TITLE = s__('ContainerRegistry|Delete image repository?');
export const DELETE_IMAGE_CONFIRMATION_TEXT = s__(
'ContainerRegistry|Deleting the image repository will delete all images and tags inside. This action cannot be undone. Please type the following to confirm: %{code}',
@@ -137,7 +133,6 @@ export const ALERT_DANGER_TAG = 'danger_tag';
export const ALERT_SUCCESS_TAGS = 'success_tags';
export const ALERT_DANGER_TAGS = 'danger_tags';
export const ALERT_DANGER_IMAGE = 'danger_image';
-export const ALERT_DANGER_IMPORTING = 'danger_importing';
export const DELETE_SCHEDULED = 'DELETE_SCHEDULED';
export const DELETE_FAILED = 'DELETE_FAILED';
@@ -148,7 +143,6 @@ export const ALERT_MESSAGES = {
[ALERT_SUCCESS_TAGS]: DELETE_TAGS_SUCCESS_MESSAGE,
[ALERT_DANGER_TAGS]: DELETE_TAGS_ERROR_MESSAGE,
[ALERT_DANGER_IMAGE]: DETAILS_DELETE_IMAGE_ERROR_MESSAGE,
- [ALERT_DANGER_IMPORTING]: DETAILS_IMPORTING_ERROR_MESSAGE,
};
export const UNFINISHED_STATUS = 'UNFINISHED';
diff --git a/app/assets/javascripts/packages_and_registries/container_registry/explorer/pages/details.vue b/app/assets/javascripts/packages_and_registries/container_registry/explorer/pages/details.vue
index b339c8c8371..83c0d2cdfca 100644
--- a/app/assets/javascripts/packages_and_registries/container_registry/explorer/pages/details.vue
+++ b/app/assets/javascripts/packages_and_registries/container_registry/explorer/pages/details.vue
@@ -20,7 +20,6 @@ import {
ALERT_SUCCESS_TAGS,
ALERT_DANGER_TAGS,
ALERT_DANGER_IMAGE,
- ALERT_DANGER_IMPORTING,
FETCH_IMAGES_LIST_ERROR_MESSAGE,
UNFINISHED_STATUS,
MISSING_OR_DELETED_IMAGE_BREADCRUMB,
@@ -33,8 +32,6 @@ import getContainerRepositoryDetailsQuery from '../graphql/queries/get_container
import getContainerRepositoryTagsQuery from '../graphql/queries/get_container_repository_tags.query.graphql';
import getContainerRepositoriesDetails from '../graphql/queries/get_container_repositories_details.query.graphql';
-const REPOSITORY_IMPORTING_ERROR_MESSAGE = 'repository importing';
-
export default {
name: 'RegistryDetailsPage',
components: {
@@ -157,17 +154,12 @@ export default {
});
if (data?.destroyContainerRepositoryTags?.errors[0]) {
- throw new Error(data.destroyContainerRepositoryTags.errors[0]);
+ throw new Error();
}
this.deleteAlertType =
itemsToBeDeleted.length === 0 ? ALERT_SUCCESS_TAG : ALERT_SUCCESS_TAGS;
} catch (e) {
- if (e.message === REPOSITORY_IMPORTING_ERROR_MESSAGE) {
- this.deleteAlertType = ALERT_DANGER_IMPORTING;
- } else {
- this.deleteAlertType =
- itemsToBeDeleted.length === 0 ? ALERT_DANGER_TAG : ALERT_DANGER_TAGS;
- }
+ this.deleteAlertType = itemsToBeDeleted.length === 0 ? ALERT_DANGER_TAG : ALERT_DANGER_TAGS;
}
this.mutationLoading = false;
@@ -203,7 +195,6 @@ export default {
<delete-alert
v-model="deleteAlertType"
:garbage-collection-help-page-path="config.garbageCollectionHelpPagePath"
- :container-registry-importing-help-page-path="config.containerRegistryImportingHelpPagePath"
:is-admin="config.isAdmin"
class="gl-my-2"
/>
diff --git a/app/assets/javascripts/packages_and_registries/container_registry/explorer/pages/list.vue b/app/assets/javascripts/packages_and_registries/container_registry/explorer/pages/list.vue
index 794be8d5195..8a038d7c974 100644
--- a/app/assets/javascripts/packages_and_registries/container_registry/explorer/pages/list.vue
+++ b/app/assets/javascripts/packages_and_registries/container_registry/explorer/pages/list.vue
@@ -11,9 +11,9 @@ import {
import { get } from 'lodash';
import getContainerRepositoriesQuery from 'shared_queries/container_registry/get_container_repositories.query.graphql';
import { createAlert } from '~/flash';
-import { FILTERED_SEARCH_TERM } from '~/packages_and_registries/shared/constants';
import Tracking from '~/tracking';
import PersistedSearch from '~/packages_and_registries/shared/components/persisted_search.vue';
+import { FILTERED_SEARCH_TERM } from '~/vue_shared/components/filtered_search_bar/constants';
import DeleteImage from '../components/delete_image.vue';
import RegistryHeader from '../components/list_page/registry_header.vue';
diff --git a/app/assets/javascripts/packages_and_registries/harbor_registry/pages/details.vue b/app/assets/javascripts/packages_and_registries/harbor_registry/pages/details.vue
index c6ab746b9f4..bafcd78ad5d 100644
--- a/app/assets/javascripts/packages_and_registries/harbor_registry/pages/details.vue
+++ b/app/assets/javascripts/packages_and_registries/harbor_registry/pages/details.vue
@@ -8,7 +8,7 @@ import {
TOKEN_TYPE_TAG_NAME,
TAG_LABEL,
} from '~/packages_and_registries/harbor_registry/constants/index';
-import { OPERATOR_IS_ONLY } from '~/vue_shared/components/filtered_search_bar/constants';
+import { OPERATORS_IS } from '~/vue_shared/components/filtered_search_bar/constants';
import { createAlert } from '~/flash';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import TagsLoader from '~/packages_and_registries/shared/components/tags_loader.vue';
@@ -39,7 +39,7 @@ export default {
title: TAG_LABEL,
unique: true,
token: GlFilteredSearchToken,
- operators: OPERATOR_IS_ONLY,
+ operators: OPERATORS_IS,
},
],
data() {
diff --git a/app/assets/javascripts/packages_and_registries/harbor_registry/utils.js b/app/assets/javascripts/packages_and_registries/harbor_registry/utils.js
index 13df303cffe..2ae5957343b 100644
--- a/app/assets/javascripts/packages_and_registries/harbor_registry/utils.js
+++ b/app/assets/javascripts/packages_and_registries/harbor_registry/utils.js
@@ -3,8 +3,8 @@ import {
SORT_FIELD_MAPPING,
TOKEN_TYPE_TAG_NAME,
} from '~/packages_and_registries/harbor_registry/constants';
-import { FILTERED_SEARCH_TERM } from '~/packages_and_registries/shared/constants';
import { normalizeHeaders, parseIntPagination } from '~/lib/utils/common_utils';
+import { FILTERED_SEARCH_TERM } from '~/vue_shared/components/filtered_search_bar/constants';
export const extractSortingDetail = (parsedSorting = '') => {
const [orderBy, sortOrder] = parsedSorting.split('_');
diff --git a/app/assets/javascripts/packages_and_registries/infrastructure_registry/list/components/packages_list_app.vue b/app/assets/javascripts/packages_and_registries/infrastructure_registry/list/components/packages_list_app.vue
index 2adf6187c4b..0aeeb2c3d15 100644
--- a/app/assets/javascripts/packages_and_registries/infrastructure_registry/list/components/packages_list_app.vue
+++ b/app/assets/javascripts/packages_and_registries/infrastructure_registry/list/components/packages_list_app.vue
@@ -4,16 +4,14 @@ import { mapActions, mapState } from 'vuex';
import { createAlert, VARIANT_INFO } from '~/flash';
import { historyReplaceState } from '~/lib/utils/common_utils';
import { s__ } from '~/locale';
-import {
- SHOW_DELETE_SUCCESS_ALERT,
- FILTERED_SEARCH_TERM,
-} from '~/packages_and_registries/shared/constants';
+import { SHOW_DELETE_SUCCESS_ALERT } from '~/packages_and_registries/shared/constants';
import { getQueryParams, extractFilterAndSorting } from '~/packages_and_registries/shared/utils';
import InfrastructureTitle from '~/packages_and_registries/infrastructure_registry/list/components/infrastructure_title.vue';
import InfrastructureSearch from '~/packages_and_registries/infrastructure_registry/list/components/infrastructure_search.vue';
import PackageList from '~/packages_and_registries/infrastructure_registry/list/components/packages_list.vue';
import { DELETE_PACKAGE_SUCCESS_MESSAGE } from '~/packages_and_registries/infrastructure_registry/list/constants';
+import { FILTERED_SEARCH_TERM } from '~/vue_shared/components/filtered_search_bar/constants';
export default {
components: {
diff --git a/app/assets/javascripts/packages_and_registries/infrastructure_registry/list/stores/actions.js b/app/assets/javascripts/packages_and_registries/infrastructure_registry/list/stores/actions.js
index 37b51797490..7a452abdc26 100644
--- a/app/assets/javascripts/packages_and_registries/infrastructure_registry/list/stores/actions.js
+++ b/app/assets/javascripts/packages_and_registries/infrastructure_registry/list/stores/actions.js
@@ -2,6 +2,7 @@ import Api from '~/api';
import { createAlert, VARIANT_SUCCESS } from '~/flash';
import axios from '~/lib/utils/axios_utils';
import { DELETE_PACKAGE_ERROR_MESSAGE } from '~/packages_and_registries/shared/constants';
+import { FILTERED_SEARCH_TERM } from '~/vue_shared/components/filtered_search_bar/constants';
import {
FETCH_PACKAGES_LIST_ERROR_MESSAGE,
DELETE_PACKAGE_SUCCESS_MESSAGE,
@@ -31,7 +32,7 @@ export const requestPackagesList = ({ dispatch, state }, params = {}) => {
const type = state.config.forceTerraform
? TERRAFORM_SEARCH_TYPE
: state.filter.find((f) => f.type === 'type');
- const name = state.filter.find((f) => f.type === 'filtered-search-term');
+ const name = state.filter.find((f) => f.type === FILTERED_SEARCH_TERM);
const packageFilters = { package_type: type?.value?.data, package_name: name?.value?.data };
const apiMethod = state.config.isGroupPage ? 'groupPackages' : 'projectPackages';
diff --git a/app/assets/javascripts/packages_and_registries/package_registry/components/list/package_list_row.vue b/app/assets/javascripts/packages_and_registries/package_registry/components/list/package_list_row.vue
index 4553dd3421b..7ad1ebac11e 100644
--- a/app/assets/javascripts/packages_and_registries/package_registry/components/list/package_list_row.vue
+++ b/app/assets/javascripts/packages_and_registries/package_registry/components/list/package_list_row.vue
@@ -54,6 +54,9 @@ export default {
},
},
computed: {
+ containsWebPathLink() {
+ return Boolean(this.packageEntity?._links?.webPath);
+ },
packageType() {
return getPackageTypeLabel(this.packageEntity.packageType);
},
@@ -109,6 +112,7 @@ export default {
<template #left-primary>
<div class="gl-display-flex gl-align-items-center gl-mr-3 gl-min-w-0">
<router-link
+ v-if="containsWebPathLink"
:class="errorPackageStyle"
class="gl-text-body gl-min-w-0"
data-testid="details-link"
@@ -118,6 +122,7 @@ export default {
>
<gl-truncate :text="packageEntity.name" />
</router-link>
+ <gl-truncate v-else :text="packageEntity.name" />
<package-tags
v-if="showTags"
diff --git a/app/assets/javascripts/packages_and_registries/package_registry/components/list/package_search.vue b/app/assets/javascripts/packages_and_registries/package_registry/components/list/package_search.vue
index d28847c7900..0cf49b25bf2 100644
--- a/app/assets/javascripts/packages_and_registries/package_registry/components/list/package_search.vue
+++ b/app/assets/javascripts/packages_and_registries/package_registry/components/list/package_search.vue
@@ -1,14 +1,14 @@
<script>
-import { s__ } from '~/locale';
import { sortableFields } from '~/packages_and_registries/package_registry/utils';
-import { OPERATOR_IS_ONLY } from '~/vue_shared/components/filtered_search_bar/constants';
+import {
+ FILTERED_SEARCH_TERM,
+ OPERATORS_IS,
+ TOKEN_TITLE_TYPE,
+ TOKEN_TYPE_TYPE,
+} from '~/vue_shared/components/filtered_search_bar/constants';
import RegistrySearch from '~/vue_shared/components/registry/registry_search.vue';
import UrlSync from '~/vue_shared/components/url_sync.vue';
import { getQueryParams, extractFilterAndSorting } from '~/packages_and_registries/shared/utils';
-import {
- FILTERED_SEARCH_TERM,
- FILTERED_SEARCH_TYPE,
-} from '~/packages_and_registries/shared/constants';
import { LIST_KEY_CREATED_AT } from '~/packages_and_registries/package_registry/constants';
import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue';
import PackageTypeToken from './tokens/package_type_token.vue';
@@ -16,12 +16,12 @@ import PackageTypeToken from './tokens/package_type_token.vue';
export default {
tokens: [
{
- type: 'type',
+ type: TOKEN_TYPE_TYPE,
icon: 'package',
- title: s__('PackageRegistry|Type'),
+ title: TOKEN_TITLE_TYPE,
unique: true,
token: PackageTypeToken,
- operators: OPERATOR_IS_ONLY,
+ operators: OPERATORS_IS,
},
],
components: { RegistrySearch, UrlSync, LocalStorageSync },
@@ -51,7 +51,7 @@ export default {
};
return this.filters.reduce((acc, filter) => {
- if (filter.type === FILTERED_SEARCH_TYPE && filter.value?.data) {
+ if (filter.type === TOKEN_TYPE_TYPE && filter.value?.data) {
return {
...acc,
packageType: filter.value.data.toUpperCase(),
diff --git a/app/assets/javascripts/packages_and_registries/package_registry/graphql/fragments/package_data.fragment.graphql b/app/assets/javascripts/packages_and_registries/package_registry/graphql/fragments/package_data.fragment.graphql
index b5695a01376..2d405f3e9cc 100644
--- a/app/assets/javascripts/packages_and_registries/package_registry/graphql/fragments/package_data.fragment.graphql
+++ b/app/assets/javascripts/packages_and_registries/package_registry/graphql/fragments/package_data.fragment.graphql
@@ -29,4 +29,7 @@ fragment PackageData on Package {
fullPath
webUrl
}
+ _links {
+ webPath
+ }
}
diff --git a/app/assets/javascripts/packages_and_registries/package_registry/graphql/queries/get_package_details.query.graphql b/app/assets/javascripts/packages_and_registries/package_registry/graphql/queries/get_package_details.query.graphql
index 51e0ab5aba8..9153906a38c 100644
--- a/app/assets/javascripts/packages_and_registries/package_registry/graphql/queries/get_package_details.query.graphql
+++ b/app/assets/javascripts/packages_and_registries/package_registry/graphql/queries/get_package_details.query.graphql
@@ -62,6 +62,7 @@ query getPackageDetails(
}
}
versions(after: $after, before: $before, first: $first, last: $last) {
+ count
nodes {
id
name
diff --git a/app/assets/javascripts/packages_and_registries/package_registry/pages/details.vue b/app/assets/javascripts/packages_and_registries/package_registry/pages/details.vue
index c59dcaee411..03352f01aca 100644
--- a/app/assets/javascripts/packages_and_registries/package_registry/pages/details.vue
+++ b/app/assets/javascripts/packages_and_registries/package_registry/pages/details.vue
@@ -304,7 +304,7 @@ export default {
deleteFileModalContent: s__(
`PackageRegistry|You are about to delete %{filename}. This is a destructive action that may render your package unusable. Are you sure?`,
),
- otherVersionsTabTitle: __('Other versions'),
+ otherVersionsTabTitle: s__('PackageRegistry|Other versions'),
},
modal: {
packageDeletePrimaryAction: {
@@ -380,7 +380,9 @@ export default {
<gl-tab v-if="showDependencies">
<template #title>
<span>{{ __('Dependencies') }}</span>
- <gl-badge size="sm">{{ packageDependencies.length }}</gl-badge>
+ <gl-badge size="sm" data-testid="dependencies-badge">{{
+ packageDependencies.length
+ }}</gl-badge>
</template>
<template v-if="packageDependencies.length > 0">
@@ -392,7 +394,14 @@ export default {
</p>
</gl-tab>
- <gl-tab :title="$options.i18n.otherVersionsTabTitle" title-item-class="js-versions-tab" lazy>
+ <gl-tab title-item-class="js-versions-tab" lazy>
+ <template #title>
+ <span>{{ $options.i18n.otherVersionsTabTitle }}</span>
+ <gl-badge size="sm" class="gl-tab-counter-badge" data-testid="other-versions-badge">{{
+ packageEntity.versions.count
+ }}</gl-badge>
+ </template>
+
<package-versions-list
:is-loading="isLoading"
:page-info="versionPageInfo"
diff --git a/app/assets/javascripts/packages_and_registries/shared/components/package_path.vue b/app/assets/javascripts/packages_and_registries/shared/components/package_path.vue
index 6fb001e5e92..0a94f67ea5e 100644
--- a/app/assets/javascripts/packages_and_registries/shared/components/package_path.vue
+++ b/app/assets/javascripts/packages_and_registries/shared/components/package_path.vue
@@ -47,7 +47,7 @@ export default {
</script>
<template>
- <div data-qa-selector="package-path" class="gl-display-flex gl-align-items-center">
+ <div data-qa-selector="package_path" class="gl-display-flex gl-align-items-center">
<gl-icon data-testid="base-icon" name="project" class="gl-mx-3 gl-min-w-0" />
<gl-link
diff --git a/app/assets/javascripts/packages_and_registries/shared/constants/package_registry.js b/app/assets/javascripts/packages_and_registries/shared/constants/package_registry.js
index f3ce967b756..fe6e06ad830 100644
--- a/app/assets/javascripts/packages_and_registries/shared/constants/package_registry.js
+++ b/app/assets/javascripts/packages_and_registries/shared/constants/package_registry.js
@@ -1,7 +1,5 @@
import { s__ } from '~/locale';
-export const FILTERED_SEARCH_TERM = 'filtered-search-term';
-export const FILTERED_SEARCH_TYPE = 'type';
export const HISTORY_PIPELINES_LIMIT = 5;
export const DELETE_PACKAGE_TRACKING_ACTION = 'delete_package';
diff --git a/app/assets/javascripts/packages_and_registries/shared/utils.js b/app/assets/javascripts/packages_and_registries/shared/utils.js
index 7e963cd0b08..76623377d90 100644
--- a/app/assets/javascripts/packages_and_registries/shared/utils.js
+++ b/app/assets/javascripts/packages_and_registries/shared/utils.js
@@ -1,6 +1,6 @@
import Vue from 'vue';
import { queryToObject } from '~/lib/utils/url_utility';
-import { FILTERED_SEARCH_TERM } from './constants';
+import { FILTERED_SEARCH_TERM } from '~/vue_shared/components/filtered_search_bar/constants';
export const getQueryParams = (query) =>
queryToObject(query, { gatherArrays: true, legacySpacesDecode: true });
diff --git a/app/assets/javascripts/pages/admin/application_settings/general/components/signup_form.vue b/app/assets/javascripts/pages/admin/application_settings/general/components/signup_form.vue
index b68148e5461..96477b9f476 100644
--- a/app/assets/javascripts/pages/admin/application_settings/general/components/signup_form.vue
+++ b/app/assets/javascripts/pages/admin/application_settings/general/components/signup_form.vue
@@ -43,7 +43,6 @@ export default {
'settingsPath',
'signupEnabled',
'requireAdminApprovalAfterUserSignup',
- 'sendUserConfirmationEmail',
'emailConfirmationSetting',
'minimumPasswordLength',
'minimumPasswordLengthMin',
@@ -68,7 +67,6 @@ export default {
form: {
signupEnabled: this.signupEnabled,
requireAdminApproval: this.requireAdminApprovalAfterUserSignup,
- sendConfirmationEmail: this.sendUserConfirmationEmail,
emailConfirmationSetting: this.emailConfirmationSetting,
minimumPasswordLength: this.minimumPasswordLength,
minimumPasswordLengthMin: this.minimumPasswordLengthMin,
@@ -204,7 +202,6 @@ export default {
buttonText: s__('ApplicationSettings|Save changes'),
signupEnabledLabel: s__('ApplicationSettings|Sign-up enabled'),
requireAdminApprovalLabel: s__('ApplicationSettings|Require admin approval for new sign-ups'),
- sendConfirmationEmailLabel: s__('ApplicationSettings|Send confirmation email on sign-up'),
emailConfirmationSettingsLabel: s__('ApplicationSettings|Email confirmation settings'),
emailConfirmationSettingsOffLabel: s__('ApplicationSettings|Off'),
emailConfirmationSettingsOffHelpText: s__(
@@ -284,13 +281,6 @@ export default {
data-testid="require-admin-approval-checkbox"
/>
- <signup-checkbox
- v-model="form.sendConfirmationEmail"
- class="gl-mb-5"
- name="application_setting[send_user_confirmation_email]"
- :label="$options.i18n.sendConfirmationEmailLabel"
- />
-
<gl-form-group :label="$options.i18n.emailConfirmationSettingsLabel">
<gl-form-radio-group
v-model="form.emailConfirmationSetting"
diff --git a/app/assets/javascripts/pages/admin/application_settings/signup_restrictions.js b/app/assets/javascripts/pages/admin/application_settings/signup_restrictions.js
index 0d5c55cb87b..395d8a38bf7 100644
--- a/app/assets/javascripts/pages/admin/application_settings/signup_restrictions.js
+++ b/app/assets/javascripts/pages/admin/application_settings/signup_restrictions.js
@@ -14,7 +14,6 @@ export default function initSignupRestrictions(elementSelector = '#js-signup-for
booleanAttributes: [
'signupEnabled',
'requireAdminApprovalAfterUserSignup',
- 'sendUserConfirmationEmail',
'domainDenylistEnabled',
'denylistTypeRawSelected',
'emailRestrictionsEnabled',
diff --git a/app/assets/javascripts/pages/admin/broadcast_messages/edit/index.js b/app/assets/javascripts/pages/admin/broadcast_messages/edit/index.js
new file mode 100644
index 00000000000..25036984082
--- /dev/null
+++ b/app/assets/javascripts/pages/admin/broadcast_messages/edit/index.js
@@ -0,0 +1,8 @@
+import initEditBroadcastMessage from '~/admin/broadcast_messages/edit';
+import initBroadcastMessagesForm from '../broadcast_message';
+
+if (gon.features.vueBroadcastMessages) {
+ initEditBroadcastMessage();
+} else {
+ initBroadcastMessagesForm();
+}
diff --git a/app/assets/javascripts/pages/admin/broadcast_messages/index.js b/app/assets/javascripts/pages/admin/broadcast_messages/index/index.js
index ffd976be8c6..1f37df2b340 100644
--- a/app/assets/javascripts/pages/admin/broadcast_messages/index.js
+++ b/app/assets/javascripts/pages/admin/broadcast_messages/index/index.js
@@ -1,6 +1,6 @@
import initBroadcastMessages from '~/admin/broadcast_messages';
import initDeprecatedRemoveRowBehavior from '~/behaviors/deprecated_remove_row_behavior';
-import initBroadcastMessagesForm from './broadcast_message';
+import initBroadcastMessagesForm from '../broadcast_message';
if (gon.features.vueBroadcastMessages) {
initBroadcastMessages();
diff --git a/app/assets/javascripts/pages/admin/dashboard/index.js b/app/assets/javascripts/pages/admin/dashboard/index.js
deleted file mode 100644
index b63e612be47..00000000000
--- a/app/assets/javascripts/pages/admin/dashboard/index.js
+++ /dev/null
@@ -1,3 +0,0 @@
-import initGitlabVersionCheck from '~/gitlab_version_check';
-
-initGitlabVersionCheck();
diff --git a/app/assets/javascripts/pages/admin/projects/index/components/delete_project_modal.vue b/app/assets/javascripts/pages/admin/projects/index/components/delete_project_modal.vue
index b06c804f3ca..48241a213ef 100644
--- a/app/assets/javascripts/pages/admin/projects/index/components/delete_project_modal.vue
+++ b/app/assets/javascripts/pages/admin/projects/index/components/delete_project_modal.vue
@@ -1,6 +1,7 @@
<script>
-import { GlSafeHtmlDirective as SafeHtml, GlModal } from '@gitlab/ui';
+import { GlModal } from '@gitlab/ui';
import { escape } from 'lodash';
+import SafeHtml from '~/vue_shared/directives/safe_html';
import { __, s__, sprintf } from '~/locale';
export default {
diff --git a/app/assets/javascripts/pages/dashboard/todos/index/todos.js b/app/assets/javascripts/pages/dashboard/todos/index/todos.js
index 2a7619da8cc..c5d62ae5daf 100644
--- a/app/assets/javascripts/pages/dashboard/todos/index/todos.js
+++ b/app/assets/javascripts/pages/dashboard/todos/index/todos.js
@@ -6,9 +6,7 @@ import { getProjects } from '~/api/projects_api';
import initDeprecatedJQueryDropdown from '~/deprecated_jquery_dropdown';
import { createAlert } from '~/flash';
import axios from '~/lib/utils/axios_utils';
-import { isMetaClick } from '~/lib/utils/common_utils';
import { addDelimiter } from '~/lib/utils/text_utility';
-import { visitUrl } from '~/lib/utils/url_utility';
import { __ } from '~/locale';
import UsersSelect from '~/users_select';
@@ -34,10 +32,6 @@ export default class Todos {
document.querySelectorAll('.js-todos-mark-all, .js-todos-undo-all').forEach((el) => {
el.removeEventListener('click', this.updateallStateClickedWrapper);
});
- document.querySelectorAll('.todo').forEach((el) => {
- el.removeEventListener('click', this.goToTodoUrl);
- el.removeEventListener('auxclick', this.goToTodoUrl);
- });
}
bindEvents() {
@@ -50,10 +44,6 @@ export default class Todos {
document.querySelectorAll('.js-todos-mark-all, .js-todos-undo-all').forEach((el) => {
el.addEventListener('click', this.updateAllStateClickedWrapper);
});
- document.querySelectorAll('.todo').forEach((el) => {
- el.addEventListener('click', this.goToTodoUrl);
- el.addEventListener('auxclick', this.goToTodoUrl);
- });
}
initFilters() {
@@ -106,19 +96,22 @@ export default class Todos {
e.stopPropagation();
e.preventDefault();
- const { target } = e;
- target.setAttribute('disabled', true);
- target.classList.add('disabled');
+ let { currentTarget } = e;
+ if (currentTarget.tagName === 'svg' || currentTarget.tagName === 'use') {
+ currentTarget = currentTarget.closest('a');
+ }
+ currentTarget.setAttribute('disabled', true);
+ currentTarget.classList.add('disabled');
- target.querySelector('.gl-spinner-container').classList.add('gl-mr-2');
+ currentTarget.querySelector('.js-todo-button-icon').classList.add('hidden');
- axios[target.dataset.method](target.dataset.href)
+ axios[currentTarget.dataset.method](currentTarget.href)
.then(({ data }) => {
- this.updateRowState(target);
+ this.updateRowState(currentTarget);
this.updateBadges(data);
})
.catch(() => {
- this.updateRowState(target, true);
+ this.updateRowState(currentTarget, true);
return createAlert({
message: __('Error updating status of to-do item.'),
});
@@ -134,7 +127,7 @@ export default class Todos {
target.removeAttribute('disabled');
target.classList.remove('disabled');
- target.querySelector('.gl-spinner-container').classList.remove('gl-mr-2');
+ target.querySelector('.js-todo-button-icon').classList.remove('hidden');
if (isInactive === true) {
restoreBtn.classList.add('hidden');
@@ -209,25 +202,4 @@ export default class Todos {
data.done_count,
);
}
-
- goToTodoUrl(e) {
- const todoLink = this.dataset.url;
-
- if (!todoLink || e.target.closest('a')) {
- return;
- }
-
- e.stopPropagation();
- e.preventDefault();
-
- const isPrimaryClick = e.button === 0;
-
- if (isMetaClick(e)) {
- const windowTarget = '_blank';
-
- window.open(todoLink, windowTarget);
- } else if (isPrimaryClick) {
- visitUrl(todoLink);
- }
- }
}
diff --git a/app/assets/javascripts/pages/groups/merge_requests/index.js b/app/assets/javascripts/pages/groups/merge_requests/index.js
index 377ba0f13a9..bf0147ca885 100644
--- a/app/assets/javascripts/pages/groups/merge_requests/index.js
+++ b/app/assets/javascripts/pages/groups/merge_requests/index.js
@@ -1,11 +1,7 @@
import addExtraTokensForMergeRequests from 'ee_else_ce/filtered_search/add_extra_tokens_for_merge_requests';
import IssuableFilteredSearchTokenKeys from '~/filtered_search/issuable_filtered_search_token_keys';
-import {
- initBulkUpdateSidebar,
- initStatusDropdown,
- initSubscriptionsDropdown,
-} from '~/issuable/bulk_update_sidebar';
import { FILTERED_SEARCH } from '~/filtered_search/constants';
+import { initBulkUpdateSidebar } from '~/issuable';
import initFilteredSearch from '~/pages/search/init_filtered_search';
import projectSelect from '~/project_select';
@@ -13,8 +9,6 @@ const ISSUABLE_BULK_UPDATE_PREFIX = 'merge_request_';
addExtraTokensForMergeRequests(IssuableFilteredSearchTokenKeys);
initBulkUpdateSidebar(ISSUABLE_BULK_UPDATE_PREFIX);
-initStatusDropdown();
-initSubscriptionsDropdown();
initFilteredSearch({
page: FILTERED_SEARCH.MERGE_REQUESTS,
diff --git a/app/assets/javascripts/pages/help/index/index.js b/app/assets/javascripts/pages/help/index/index.js
index a8e67c57307..da748223440 100644
--- a/app/assets/javascripts/pages/help/index/index.js
+++ b/app/assets/javascripts/pages/help/index/index.js
@@ -1,5 +1,3 @@
import docs from '~/docs/docs_bundle';
-import initGitlabVersionCheck from '~/gitlab_version_check';
docs();
-initGitlabVersionCheck();
diff --git a/app/assets/javascripts/pages/import/fogbugz/new_user_map/components/user_select.vue b/app/assets/javascripts/pages/import/fogbugz/new_user_map/components/user_select.vue
index 20ce296bbec..912b84dbae6 100644
--- a/app/assets/javascripts/pages/import/fogbugz/new_user_map/components/user_select.vue
+++ b/app/assets/javascripts/pages/import/fogbugz/new_user_map/components/user_select.vue
@@ -1,5 +1,5 @@
<script>
-import { GlAvatarLabeled, GlListbox } from '@gitlab/ui';
+import { GlAvatarLabeled, GlCollapsibleListbox } from '@gitlab/ui';
import { __ } from '~/locale';
import searchUsersQuery from '~/graphql_shared/queries/users_search_all.query.graphql';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
@@ -11,7 +11,7 @@ const USERS_PER_PAGE = 20;
export default {
components: {
GlAvatarLabeled,
- GlListbox,
+ GlCollapsibleListbox,
},
props: {
name: {
@@ -70,7 +70,7 @@ export default {
</script>
<template>
<div>
- <gl-listbox
+ <gl-collapsible-listbox
ref="listbox"
v-model="user"
:items="users"
@@ -89,7 +89,7 @@ export default {
:sub-label="item.username"
/>
</template>
- </gl-listbox>
+ </gl-collapsible-listbox>
<input type="hidden" :name="name" :value="userId" />
</div>
</template>
diff --git a/app/assets/javascripts/pages/import/gitlab_projects/new/index.js b/app/assets/javascripts/pages/import/gitlab_projects/new/index.js
index 870c14f99ae..d0560af5b3f 100644
--- a/app/assets/javascripts/pages/import/gitlab_projects/new/index.js
+++ b/app/assets/javascripts/pages/import/gitlab_projects/new/index.js
@@ -1,3 +1,5 @@
import initGitLabImportProject from '~/projects/project_import_gitlab_project';
+import { initNewProjectUrlSelect } from '~/projects/new';
+initNewProjectUrlSelect();
initGitLabImportProject();
diff --git a/app/assets/javascripts/pages/import/manifest/new/index.js b/app/assets/javascripts/pages/import/manifest/new/index.js
new file mode 100644
index 00000000000..0bb70a7364e
--- /dev/null
+++ b/app/assets/javascripts/pages/import/manifest/new/index.js
@@ -0,0 +1,3 @@
+import { initNewProjectUrlSelect } from '~/projects/new';
+
+initNewProjectUrlSelect();
diff --git a/app/assets/javascripts/pages/import/phabricator/new/index.js b/app/assets/javascripts/pages/import/phabricator/new/index.js
new file mode 100644
index 00000000000..0bb70a7364e
--- /dev/null
+++ b/app/assets/javascripts/pages/import/phabricator/new/index.js
@@ -0,0 +1,3 @@
+import { initNewProjectUrlSelect } from '~/projects/new';
+
+initNewProjectUrlSelect();
diff --git a/app/assets/javascripts/pages/projects/branches/new/index.js b/app/assets/javascripts/pages/projects/branches/new/index.js
index dbae89b5ade..f2b03468b0b 100644
--- a/app/assets/javascripts/pages/projects/branches/new/index.js
+++ b/app/assets/javascripts/pages/projects/branches/new/index.js
@@ -1,7 +1,6 @@
import NewBranchForm from '~/new_branch_form';
+import initNewBranchRefSelector from '~/branches/init_new_branch_ref_selector';
+initNewBranchRefSelector();
// eslint-disable-next-line no-new
-new NewBranchForm(
- document.querySelector('.js-create-branch-form'),
- JSON.parse(document.getElementById('availableRefs').innerHTML),
-);
+new NewBranchForm(document.querySelector('.js-create-branch-form'));
diff --git a/app/assets/javascripts/pages/projects/ci/lints/show/index.js b/app/assets/javascripts/pages/projects/ci/lints/show/index.js
index 6e1cdf557b5..caac76fc6d7 100644
--- a/app/assets/javascripts/pages/projects/ci/lints/show/index.js
+++ b/app/assets/javascripts/pages/projects/ci/lints/show/index.js
@@ -1,3 +1,3 @@
-import initCiLint from '~/ci_lint';
+import initCiLint from '~/ci/ci_lint';
initCiLint();
diff --git a/app/assets/javascripts/pages/projects/ci/pipeline_editor/show/index.js b/app/assets/javascripts/pages/projects/ci/pipeline_editor/show/index.js
index 67d32648ce8..7e91f23dd7f 100644
--- a/app/assets/javascripts/pages/projects/ci/pipeline_editor/show/index.js
+++ b/app/assets/javascripts/pages/projects/ci/pipeline_editor/show/index.js
@@ -1,3 +1,3 @@
-import { initPipelineEditor } from '~/pipeline_editor';
+import { initPipelineEditor } from '~/ci/pipeline_editor';
initPipelineEditor();
diff --git a/app/assets/javascripts/pages/projects/commits/show/index.js b/app/assets/javascripts/pages/projects/commits/show/index.js
index ee74628a994..f5ecf9be591 100644
--- a/app/assets/javascripts/pages/projects/commits/show/index.js
+++ b/app/assets/javascripts/pages/projects/commits/show/index.js
@@ -1,9 +1,10 @@
import ShortcutsNavigation from '~/behaviors/shortcuts/shortcuts_navigation';
import CommitsList from '~/commits';
import GpgBadges from '~/gpg_badges';
-import mountCommits from '~/projects/commits';
+import { mountCommits, initCommitsRefSwitcher } from '~/projects/commits';
new CommitsList(document.querySelector('.js-project-commits-show').dataset.commitsLimit); // eslint-disable-line no-new
new ShortcutsNavigation(); // eslint-disable-line no-new
GpgBadges.fetch();
mountCommits(document.getElementById('js-author-dropdown'));
+initCommitsRefSwitcher();
diff --git a/app/assets/javascripts/pages/projects/cycle_analytics/show/index.js b/app/assets/javascripts/pages/projects/cycle_analytics/show/index.js
index bef21ef8fdf..05a1bbc69ed 100644
--- a/app/assets/javascripts/pages/projects/cycle_analytics/show/index.js
+++ b/app/assets/javascripts/pages/projects/cycle_analytics/show/index.js
@@ -1,3 +1,3 @@
-import initCycleAnalytics from '~/cycle_analytics';
+import initCycleAnalytics from '~/analytics/cycle_analytics';
initCycleAnalytics();
diff --git a/app/assets/javascripts/pages/projects/environments/show/index.js b/app/assets/javascripts/pages/projects/environments/show/index.js
index 53e48ad8d86..1ce8899ac63 100644
--- a/app/assets/javascripts/pages/projects/environments/show/index.js
+++ b/app/assets/javascripts/pages/projects/environments/show/index.js
@@ -1,5 +1,6 @@
import initConfirmRollBackModal from '~/environments/init_confirm_rollback_modal';
-import { initHeader } from '~/environments/mount_show';
+import { initHeader, initPage } from '~/environments/mount_show';
initHeader();
+initPage();
initConfirmRollBackModal();
diff --git a/app/assets/javascripts/pages/projects/forks/new/components/fork_form.vue b/app/assets/javascripts/pages/projects/forks/new/components/fork_form.vue
index 30cefa3d717..91650003d4a 100644
--- a/app/assets/javascripts/pages/projects/forks/new/components/fork_form.vue
+++ b/app/assets/javascripts/pages/projects/forks/new/components/fork_form.vue
@@ -23,6 +23,7 @@ import {
VISIBILITY_LEVEL_INTERNAL_STRING,
VISIBILITY_LEVEL_PUBLIC_STRING,
VISIBILITY_LEVELS_STRING_TO_INTEGER,
+ VISIBILITY_LEVELS_INTEGER_TO_STRING,
} from '~/visibility_level/constants';
import ProjectNamespace from './project_namespace.vue';
@@ -105,39 +106,8 @@ export default {
};
},
computed: {
- projectVisibilityLevel() {
- return VISIBILITY_LEVELS_STRING_TO_INTEGER[this.projectVisibility];
- },
- namespaceVisibilityLevel() {
- const visibility =
- this.form.fields.namespace.value?.visibility || VISIBILITY_LEVEL_PUBLIC_STRING;
- return VISIBILITY_LEVELS_STRING_TO_INTEGER[visibility];
- },
- visibilityLevelCap() {
- return Math.min(this.projectVisibilityLevel, this.namespaceVisibilityLevel);
- },
- restrictedVisibilityLevelsSet() {
- return new Set(this.restrictedVisibilityLevels);
- },
allowedVisibilityLevels() {
- const allowedLevels = Object.entries(VISIBILITY_LEVELS_STRING_TO_INTEGER).reduce(
- (levels, [levelName, levelValue]) => {
- if (
- !this.restrictedVisibilityLevelsSet.has(levelValue) &&
- levelValue <= this.visibilityLevelCap
- ) {
- levels.push(levelName);
- }
- return levels;
- },
- [],
- );
-
- if (!allowedLevels.length) {
- return [VISIBILITY_LEVEL_PRIVATE_STRING];
- }
-
- return allowedLevels;
+ return this.getAllowedVisibilityLevels();
},
visibilityLevels() {
return [
@@ -178,13 +148,60 @@ export default {
return !this.allowedVisibilityLevels.includes(visibility);
},
getInitialVisibilityValue() {
- return this.restrictedVisibilityLevels.length !== 0 ? null : this.projectVisibility;
+ return this.getMaximumAllowedVisibilityLevel(this.projectVisibility);
},
setNamespace(namespace) {
- this.form.fields.visibility.value =
- this.restrictedVisibilityLevels.length !== 0 ? null : VISIBILITY_LEVEL_PRIVATE_STRING;
this.form.fields.namespace.value = namespace;
this.form.fields.namespace.state = true;
+ this.form.fields.visibility.value = this.getMaximumAllowedVisibilityLevel(
+ this.form.fields.visibility.value,
+ );
+ },
+ getProjectVisibilityLevel() {
+ return VISIBILITY_LEVELS_STRING_TO_INTEGER[this.projectVisibility];
+ },
+ getNamespaceVisibilityLevel() {
+ const visibility =
+ this.form?.fields?.namespace?.value?.visibility || VISIBILITY_LEVEL_PUBLIC_STRING;
+ return VISIBILITY_LEVELS_STRING_TO_INTEGER[visibility];
+ },
+ getVisibilityLevelCap() {
+ return Math.min(this.getProjectVisibilityLevel(), this.getNamespaceVisibilityLevel());
+ },
+ getRestrictedVisibilityLevelsSet() {
+ return new Set(this.restrictedVisibilityLevels);
+ },
+ getAllowedVisibilityLevels() {
+ const allowedLevels = Object.entries(VISIBILITY_LEVELS_STRING_TO_INTEGER).reduce(
+ (levels, [levelName, levelValue]) => {
+ if (
+ !this.getRestrictedVisibilityLevelsSet().has(levelValue) &&
+ levelValue <= this.getVisibilityLevelCap()
+ ) {
+ levels.push(levelName);
+ }
+ return levels;
+ },
+ [],
+ );
+
+ if (!allowedLevels.length) {
+ return [VISIBILITY_LEVEL_PRIVATE_STRING];
+ }
+
+ return allowedLevels;
+ },
+ getMaximumAllowedVisibilityLevel(visibility) {
+ const allowedVisibilities = this.getAllowedVisibilityLevels().map(
+ (s) => VISIBILITY_LEVELS_STRING_TO_INTEGER[s],
+ );
+ const current = VISIBILITY_LEVELS_STRING_TO_INTEGER[visibility];
+ const lower = allowedVisibilities.filter((l) => l <= current);
+ if (lower.length) {
+ return VISIBILITY_LEVELS_INTEGER_TO_STRING[Math.max(...lower)];
+ }
+ const higher = allowedVisibilities.filter((l) => l >= current);
+ return VISIBILITY_LEVELS_INTEGER_TO_STRING[Math.min(...higher)];
},
async onSubmit() {
this.form.showValidation = true;
diff --git a/app/assets/javascripts/pages/projects/graphs/components/code_coverage.vue b/app/assets/javascripts/pages/projects/graphs/components/code_coverage.vue
index 08d24344ffc..10bfcdc2294 100644
--- a/app/assets/javascripts/pages/projects/graphs/components/code_coverage.vue
+++ b/app/assets/javascripts/pages/projects/graphs/components/code_coverage.vue
@@ -1,5 +1,5 @@
<script>
-import { GlAlert, GlButton, GlDropdown, GlDropdownItem, GlSprintf } from '@gitlab/ui';
+import { GlAlert, GlButton, GlListbox, GlSprintf } from '@gitlab/ui';
import { GlAreaChart } from '@gitlab/ui/dist/charts';
import { get } from 'lodash';
import { formatDate } from '~/lib/utils/datetime_utility';
@@ -12,8 +12,7 @@ export default {
GlAlert,
GlAreaChart,
GlButton,
- GlDropdown,
- GlDropdownItem,
+ GlListbox,
GlSprintf,
},
props: {
@@ -96,6 +95,14 @@ export default {
formattedData() {
return this.sortedData.map((value) => [value.date, value.coverage]);
},
+ mappedCoverages() {
+ return this.dailyCoverageData?.map((item, index) => ({
+ // A numerical index makes an item into a group header, so
+ // convert these to strings to get non-header GlListbox items
+ value: index.toString(),
+ text: item.group_name,
+ }));
+ },
chartData() {
return [
{
@@ -175,18 +182,13 @@ export default {
{{ __('It seems that there is currently no available data for code coverage') }}
</span>
</gl-alert>
- <gl-dropdown v-if="canShowData" :text="selectedDailyCoverageName">
- <gl-dropdown-item
- v-for="({ group_name }, index) in dailyCoverageData"
- :key="index"
- :value="group_name"
- is-check-item
- :is-checked="index === selectedCoverageIndex"
- @click="setSelectedCoverage(index)"
- >
- {{ group_name }}
- </gl-dropdown-item>
- </gl-dropdown>
+ <gl-listbox
+ v-if="canShowData"
+ :items="mappedCoverages"
+ :selected="selectedCoverageIndex.toString()"
+ :toggle-text="selectedDailyCoverageName"
+ @select="setSelectedCoverage"
+ />
</div>
<gl-area-chart
v-if="!isLoading"
diff --git a/app/assets/javascripts/pages/projects/merge_requests/creations/new/compare.js b/app/assets/javascripts/pages/projects/merge_requests/creations/new/compare.js
index 2d26d3922bf..653f903c6d1 100644
--- a/app/assets/javascripts/pages/projects/merge_requests/creations/new/compare.js
+++ b/app/assets/javascripts/pages/projects/merge_requests/creations/new/compare.js
@@ -26,7 +26,10 @@ const updateCommitList = (url, $emptyState, $loadingIndicator, $commitList, para
export default (mrNewCompareNode) => {
const { sourceBranchUrl, targetBranchUrl } = mrNewCompareNode.dataset;
- initTargetProjectDropdown();
+
+ if (!window.gon?.features?.mrCompareDropdowns) {
+ initTargetProjectDropdown();
+ }
const updateSourceBranchCommitList = () =>
updateCommitList(
diff --git a/app/assets/javascripts/pages/projects/merge_requests/creations/new/index.js b/app/assets/javascripts/pages/projects/merge_requests/creations/new/index.js
index 9aecd154483..b3868653d6a 100644
--- a/app/assets/javascripts/pages/projects/merge_requests/creations/new/index.js
+++ b/app/assets/javascripts/pages/projects/merge_requests/creations/new/index.js
@@ -1,10 +1,37 @@
+import $ from 'jquery';
+import Vue from 'vue';
import initPipelines from '~/commit/pipelines/pipelines_bundle';
import MergeRequest from '~/merge_request';
+import TargetProjectDropdown from '~/merge_requests/components/target_project_dropdown.vue';
import initCompare from './compare';
const mrNewCompareNode = document.querySelector('.js-merge-request-new-compare');
if (mrNewCompareNode) {
initCompare(mrNewCompareNode);
+
+ const el = document.getElementById('js-target-project-dropdown');
+ const { targetProjectsPath, currentProject } = el.dataset;
+
+ // eslint-disable-next-line no-new
+ new Vue({
+ el,
+ name: 'TargetProjectDropdown',
+ provide: {
+ targetProjectsPath,
+ currentProject: JSON.parse(currentProject),
+ },
+ render(h) {
+ return h(TargetProjectDropdown, {
+ on: {
+ 'project-selected': function projectSelectedFunction(refsUrl) {
+ const $targetBranchDropdown = $('.js-target-branch');
+ $targetBranchDropdown.data('refsUrl', refsUrl);
+ $targetBranchDropdown.data('deprecatedJQueryDropdown').clearMenu();
+ },
+ },
+ });
+ },
+ });
} else {
const mrNewSubmitNode = document.querySelector('.js-merge-request-new-submit');
// eslint-disable-next-line no-new
diff --git a/app/assets/javascripts/pages/projects/merge_requests/diffs/index.js b/app/assets/javascripts/pages/projects/merge_requests/diffs/index.js
new file mode 100644
index 00000000000..77294c0fb9e
--- /dev/null
+++ b/app/assets/javascripts/pages/projects/merge_requests/diffs/index.js
@@ -0,0 +1,5 @@
+import initDiffsApp from '~/diffs';
+import { initMrPage } from '../page';
+
+initMrPage();
+initDiffsApp();
diff --git a/app/assets/javascripts/pages/projects/merge_requests/index/index.js b/app/assets/javascripts/pages/projects/merge_requests/index/index.js
index 2399aafc9b5..b3a09cc0be3 100644
--- a/app/assets/javascripts/pages/projects/merge_requests/index/index.js
+++ b/app/assets/javascripts/pages/projects/merge_requests/index/index.js
@@ -1,20 +1,13 @@
import addExtraTokensForMergeRequests from 'ee_else_ce/filtered_search/add_extra_tokens_for_merge_requests';
import ShortcutsNavigation from '~/behaviors/shortcuts/shortcuts_navigation';
import IssuableFilteredSearchTokenKeys from '~/filtered_search/issuable_filtered_search_token_keys';
-import { initCsvImportExportButtons, initIssuableByEmail } from '~/issuable';
-import {
- initBulkUpdateSidebar,
- initStatusDropdown,
- initSubscriptionsDropdown,
-} from '~/issuable/bulk_update_sidebar';
import { FILTERED_SEARCH } from '~/filtered_search/constants';
+import { initBulkUpdateSidebar, initCsvImportExportButtons, initIssuableByEmail } from '~/issuable';
import { ISSUABLE_INDEX } from '~/issuable/constants';
import initFilteredSearch from '~/pages/search/init_filtered_search';
import UsersSelect from '~/users_select';
initBulkUpdateSidebar(ISSUABLE_INDEX.MERGE_REQUEST);
-initStatusDropdown();
-initSubscriptionsDropdown();
addExtraTokensForMergeRequests(IssuableFilteredSearchTokenKeys);
IssuableFilteredSearchTokenKeys.removeTokensForKeys('iteration');
diff --git a/app/assets/javascripts/pages/projects/merge_requests/init_merge_request.js b/app/assets/javascripts/pages/projects/merge_requests/init_merge_request.js
index 42fa306d226..a4e3ddfc506 100644
--- a/app/assets/javascripts/pages/projects/merge_requests/init_merge_request.js
+++ b/app/assets/javascripts/pages/projects/merge_requests/init_merge_request.js
@@ -2,6 +2,7 @@
import $ from 'jquery';
import IssuableForm from 'ee_else_ce/issuable/issuable_form';
+import IssuableLabelSelector from '~/issuable/issuable_label_selector';
import ShortcutsNavigation from '~/behaviors/shortcuts/shortcuts_navigation';
import Diff from '~/diff';
import GLForm from '~/gl_form';
@@ -14,6 +15,7 @@ export default () => {
new ShortcutsNavigation();
new GLForm($('.merge-request-form'));
new IssuableForm($('.merge-request-form'));
+ IssuableLabelSelector();
new LabelsSelect();
new IssuableTemplateSelectors({
warnTemplateOverride: true,
diff --git a/app/assets/javascripts/pages/projects/merge_requests/page.js b/app/assets/javascripts/pages/projects/merge_requests/page.js
new file mode 100644
index 00000000000..a8699b350f8
--- /dev/null
+++ b/app/assets/javascripts/pages/projects/merge_requests/page.js
@@ -0,0 +1,45 @@
+import Vue from 'vue';
+import StickyHeader from '~/merge_requests/components/sticky_header.vue';
+import { initIssuableHeaderWarnings } from '~/issuable';
+import initMrNotes from '~/mr_notes';
+import store from '~/mr_notes/stores';
+import initSidebarBundle from '~/sidebar/sidebar_bundle';
+import { apolloProvider } from '~/graphql_shared/issuable_client';
+import { parseBoolean } from '~/lib/utils/common_utils';
+import initShow from './init_merge_request_show';
+import getStateQuery from './queries/get_state.query.graphql';
+
+export function initMrPage() {
+ initMrNotes();
+ initShow();
+}
+
+requestIdleCallback(() => {
+ initSidebarBundle(store);
+ initIssuableHeaderWarnings(store);
+
+ const el = document.getElementById('js-merge-sticky-header');
+
+ if (el) {
+ const { data } = el.dataset;
+ const { iid, projectPath, title, tabs, isFluidLayout } = JSON.parse(data);
+
+ // eslint-disable-next-line no-new
+ new Vue({
+ el,
+ store,
+ apolloProvider,
+ provide: {
+ query: getStateQuery,
+ iid,
+ projectPath,
+ title,
+ tabs,
+ isFluidLayout: parseBoolean(isFluidLayout),
+ },
+ render(h) {
+ return h(StickyHeader);
+ },
+ });
+ }
+});
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 cc5c393ff8c..568bf19b55e 100644
--- a/app/assets/javascripts/pages/projects/merge_requests/show/index.js
+++ b/app/assets/javascripts/pages/projects/merge_requests/show/index.js
@@ -1,45 +1,5 @@
-import Vue from 'vue';
-import StickyHeader from '~/merge_requests/components/sticky_header.vue';
-import { initReviewBar } from '~/batch_comments';
-import { initIssuableHeaderWarnings } from '~/issuable';
-import initMrNotes from '~/mr_notes';
-import store from '~/mr_notes/stores';
-import initSidebarBundle from '~/sidebar/sidebar_bundle';
-import { apolloProvider } from '~/graphql_shared/issuable_client';
-import { parseBoolean } from '~/lib/utils/common_utils';
-import initShow from '../init_merge_request_show';
-import getStateQuery from '../queries/get_state.query.graphql';
+import initNotesApp from '~/mr_notes/init_notes';
+import { initMrPage } from '../page';
-initMrNotes();
-initShow();
-
-requestIdleCallback(() => {
- initSidebarBundle(store);
- initReviewBar();
- initIssuableHeaderWarnings(store);
-
- const el = document.getElementById('js-merge-sticky-header');
-
- if (el) {
- const { data } = el.dataset;
- const { iid, projectPath, title, tabs, isFluidLayout } = JSON.parse(data);
-
- // eslint-disable-next-line no-new
- new Vue({
- el,
- store,
- apolloProvider,
- provide: {
- query: getStateQuery,
- iid,
- projectPath,
- title,
- tabs,
- isFluidLayout: parseBoolean(isFluidLayout),
- },
- render(h) {
- return h(StickyHeader);
- },
- });
- }
-});
+initMrPage();
+initNotesApp();
diff --git a/app/assets/javascripts/pages/projects/ml/candidates/show/index.js b/app/assets/javascripts/pages/projects/ml/candidates/show/index.js
new file mode 100644
index 00000000000..c1acef5ac13
--- /dev/null
+++ b/app/assets/javascripts/pages/projects/ml/candidates/show/index.js
@@ -0,0 +1,27 @@
+import Vue from 'vue';
+import MlCandidate from '~/ml/experiment_tracking/components/ml_candidate.vue';
+
+const initShowCandidate = () => {
+ const element = document.querySelector('#js-show-ml-candidate');
+ if (!element) {
+ return;
+ }
+
+ const container = document.createElement('div');
+ element.appendChild(container);
+
+ const candidate = JSON.parse(element.dataset.candidate);
+
+ // eslint-disable-next-line no-new
+ new Vue({
+ el: container,
+ provide: {
+ candidate,
+ },
+ render(h) {
+ return h(MlCandidate);
+ },
+ });
+};
+
+initShowCandidate();
diff --git a/app/assets/javascripts/pages/projects/ml/experiments/show/index.js b/app/assets/javascripts/pages/projects/ml/experiments/show/index.js
index 0a9d9f4c987..97e436920c7 100644
--- a/app/assets/javascripts/pages/projects/ml/experiments/show/index.js
+++ b/app/assets/javascripts/pages/projects/ml/experiments/show/index.js
@@ -1,5 +1,5 @@
import Vue from 'vue';
-import ShowExperiment from '~/ml/experiment_tracking/components/experiment.vue';
+import MlExperiment from '~/ml/experiment_tracking/components/ml_experiment.vue';
const initShowExperiment = () => {
const element = document.querySelector('#js-show-ml-experiment');
@@ -23,7 +23,7 @@ const initShowExperiment = () => {
paramNames,
},
render(h) {
- return h(ShowExperiment);
+ return h(MlExperiment);
},
});
};
diff --git a/app/assets/javascripts/pages/projects/new/index.js b/app/assets/javascripts/pages/projects/new/index.js
index 50733d8a145..d022428df98 100644
--- a/app/assets/javascripts/pages/projects/new/index.js
+++ b/app/assets/javascripts/pages/projects/new/index.js
@@ -4,10 +4,8 @@ import {
initDeploymentTargetSelect,
} from '~/projects/new';
import initProjectVisibilitySelector from '~/projects/project_visibility';
-import initProjectNew from '~/projects/project_new';
initProjectVisibilitySelector();
-initProjectNew.bindEvents();
initNewProjectCreation();
initNewProjectUrlSelect();
initDeploymentTargetSelect();
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 85443843684..fd8b1a6290f 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
@@ -39,6 +39,11 @@ export default {
required: false,
default: '',
},
+ sendNativeErrors: {
+ type: Boolean,
+ required: false,
+ default: true,
+ },
},
data() {
return {
@@ -114,9 +119,11 @@ export default {
cronInterval() {
// updates field validation state when model changes, as
// glFieldError only updates on input.
- this.$nextTick(() => {
- gl.pipelineScheduleFieldErrors.updateFormValidityState();
- });
+ if (this.sendNativeErrors) {
+ this.$nextTick(() => {
+ gl.pipelineScheduleFieldErrors.updateFormValidityState();
+ });
+ }
},
radioValue: {
immediate: true,
diff --git a/app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/timezone_dropdown.js b/app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/timezone_dropdown.js
deleted file mode 100644
index bc467952551..00000000000
--- a/app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/timezone_dropdown.js
+++ /dev/null
@@ -1,82 +0,0 @@
-import initDeprecatedJQueryDropdown from '~/deprecated_jquery_dropdown';
-import { formatTimezone } from '~/lib/utils/datetime_utility';
-
-const defaultTimezone = { identifier: 'Etc/UTC', name: 'UTC', offset: 0 };
-const defaults = {
- $inputEl: null,
- $dropdownEl: null,
- onSelectTimezone: null,
- displayFormat: (item) => item.name,
-};
-
-export const formatUtcOffset = (offset) => {
- const parsed = parseInt(offset, 10);
- if (Number.isNaN(parsed) || parsed === 0) {
- return `0`;
- }
- const prefix = offset > 0 ? '+' : '-';
- return `${prefix} ${Math.abs(offset / 3600)}`;
-};
-
-export const findTimezoneByIdentifier = (tzList = [], identifier = null) => {
- if (tzList && tzList.length && identifier && identifier.length) {
- return tzList.find((tz) => tz.identifier === identifier) || null;
- }
- return null;
-};
-
-export default class TimezoneDropdown {
- constructor({
- $dropdownEl,
- $inputEl,
- onSelectTimezone,
- displayFormat,
- allowEmpty = false,
- } = defaults) {
- this.$dropdown = $dropdownEl;
- this.$dropdownToggle = this.$dropdown.find('.dropdown-toggle-text');
- this.$input = $inputEl;
- this.timezoneData = this.$dropdown.data('data') || [];
-
- this.onSelectTimezone = onSelectTimezone;
- this.displayFormat = displayFormat || defaults.displayFormat;
- this.allowEmpty = allowEmpty;
-
- this.initDropdown();
- }
-
- initDropdown() {
- initDeprecatedJQueryDropdown(this.$dropdown, {
- data: this.timezoneData,
- filterable: true,
- selectable: true,
- toggleLabel: this.displayFormat,
- search: {
- fields: ['name'],
- },
- clicked: (cfg) => this.handleDropdownChange(cfg),
- text: (item) => formatTimezone(item),
- });
-
- const initialTimezone = findTimezoneByIdentifier(this.timezoneData, this.$input.val());
-
- if (initialTimezone !== null) {
- this.setDropdownValue(initialTimezone);
- } else if (!this.allowEmpty) {
- this.setDropdownValue(defaultTimezone);
- }
- }
-
- setDropdownValue(timezone) {
- this.$dropdownToggle.text(this.displayFormat(timezone));
- this.$input.val(timezone.identifier);
- }
-
- handleDropdownChange({ selectedObj, e }) {
- e.preventDefault();
- this.$input.val(selectedObj.identifier);
- if (this.onSelectTimezone) {
- this.onSelectTimezone({ selectedObj, e });
- }
- }
-}
diff --git a/app/assets/javascripts/pages/projects/project.js b/app/assets/javascripts/pages/projects/project.js
index d177c67f133..4c9eb830ff6 100644
--- a/app/assets/javascripts/pages/projects/project.js
+++ b/app/assets/javascripts/pages/projects/project.js
@@ -11,10 +11,14 @@ import { mergeUrlParams } from '~/lib/utils/url_utility';
import { __ } from '~/locale';
import projectSelect from '~/project_select';
+const BRANCH_REF_TYPE = 'heads';
+const TAG_REF_TYPE = 'tags';
+const BRANCH_GROUP_NAME = __('Branches');
+const TAG_GROUP_NAME = __('Tags');
+
export default class Project {
constructor() {
initClonePanel();
-
// Ref switcher
if (document.querySelector('.js-project-refs-dropdown')) {
Project.initRefSwitcher();
@@ -62,6 +66,7 @@ export default class Project {
return $('.js-project-refs-dropdown').each(function () {
const $dropdown = $(this);
const selected = $dropdown.data('selected');
+ const refType = $dropdown.data('refType');
const fieldName = $dropdown.data('fieldName');
const shouldVisit = Boolean($dropdown.data('visit'));
const $form = $dropdown.closest('form');
@@ -91,18 +96,32 @@ export default class Project {
filterByText: true,
inputFieldName: $dropdown.data('inputFieldName'),
fieldName,
- renderRow(ref) {
+ renderRow(ref, _, params) {
const li = refListItem.cloneNode(false);
const link = refLink.cloneNode(false);
if (ref === selected) {
- link.className = 'is-active';
+ // Check group and current ref type to avoid adding a class when tags and branches share the same name
+ if (
+ (refType === BRANCH_REF_TYPE && params.group === BRANCH_GROUP_NAME) ||
+ (refType === TAG_REF_TYPE && params.group === TAG_GROUP_NAME) ||
+ !refType
+ ) {
+ link.className = 'is-active';
+ }
}
+
link.textContent = ref;
link.dataset.ref = ref;
if (ref.length > 0 && shouldVisit) {
- link.href = mergeUrlParams({ [fieldName]: ref }, linkTarget);
+ const urlParams = { [fieldName]: ref };
+ if (params.group === BRANCH_GROUP_NAME) {
+ urlParams.ref_type = BRANCH_REF_TYPE;
+ } else {
+ urlParams.ref_type = TAG_REF_TYPE;
+ }
+ link.href = mergeUrlParams(urlParams, linkTarget);
}
li.appendChild(link);
diff --git a/app/assets/javascripts/pages/projects/settings/merge_requests/index.js b/app/assets/javascripts/pages/projects/settings/merge_requests/index.js
index 739e666644c..0f7ede8ed42 100644
--- a/app/assets/javascripts/pages/projects/settings/merge_requests/index.js
+++ b/app/assets/javascripts/pages/projects/settings/merge_requests/index.js
@@ -1,9 +1,6 @@
import groupsSelect from '~/groups_select';
import UserCallout from '~/user_callout';
-import UsersSelect from '~/users_select';
-// eslint-disable-next-line no-new
-new UsersSelect();
groupsSelect();
// eslint-disable-next-line no-new
diff --git a/app/assets/javascripts/pages/projects/shared/permissions/components/settings_panel.vue b/app/assets/javascripts/pages/projects/shared/permissions/components/settings_panel.vue
index c37b4cc643a..5fa3288bbef 100644
--- a/app/assets/javascripts/pages/projects/shared/permissions/components/settings_panel.vue
+++ b/app/assets/javascripts/pages/projects/shared/permissions/components/settings_panel.vue
@@ -23,6 +23,12 @@ import ProjectSettingRow from './project_setting_row.vue';
const FEATURE_ACCESS_LEVEL_ANONYMOUS = [30, s__('ProjectSettings|Everyone')];
+const PACKAGE_REGISTRY_ACCESS_LEVEL_DEFAULT_BY_PROJECT_VISIBILITY = {
+ [VISIBILITY_LEVEL_PRIVATE_INTEGER]: featureAccessLevel.PROJECT_MEMBERS,
+ [VISIBILITY_LEVEL_INTERNAL_INTEGER]: featureAccessLevel.EVERYONE,
+ [VISIBILITY_LEVEL_PUBLIC_INTEGER]: FEATURE_ACCESS_LEVEL_ANONYMOUS[0],
+};
+
export default {
i18n: {
...CVE_ID_REQUEST_BUTTON_I18N,
@@ -32,7 +38,6 @@ export default {
issuesLabel: s__('ProjectSettings|Issues'),
lfsLabel: s__('ProjectSettings|Git Large File Storage (LFS)'),
mergeRequestsLabel: s__('ProjectSettings|Merge requests'),
- operationsLabel: s__('ProjectSettings|Operations'),
environmentsLabel: s__('ProjectSettings|Environments'),
environmentsHelpText: s__(
'ProjectSettings|Every project can make deployments to environments either via CI/CD or API calls. Non-project members have read-only access.',
@@ -47,11 +52,15 @@ export default {
packagesHelpText: s__(
'ProjectSettings|Every project can have its own space to store its packages. Note: The Package Registry is always visible when a project is public.',
),
- packageRegistryHelpText: s__(
- 'ProjectSettings|Every project can have its own space to store its packages.',
+ packageRegistryHelpText: s__('ProjectSettings|Publish, store, and view packages in a project.'),
+ packageRegistryForEveryoneHelpText: s__(
+ 'ProjectSettings|Anyone can pull packages with a package manager API.',
),
packagesLabel: s__('ProjectSettings|Packages'),
packageRegistryLabel: s__('ProjectSettings|Package registry'),
+ packageRegistryForEveryoneLabel: s__(
+ 'ProjectSettings|Allow anyone to pull from Package Registry',
+ ),
pagesLabel: s__('ProjectSettings|Pages'),
ciCdLabel: __('CI/CD'),
repositoryLabel: s__('ProjectSettings|Repository'),
@@ -249,7 +258,6 @@ export default {
analyticsAccessLevel: featureAccessLevel.EVERYONE,
requirementsAccessLevel: featureAccessLevel.EVERYONE,
securityAndComplianceAccessLevel: featureAccessLevel.PROJECT_MEMBERS,
- operationsAccessLevel: featureAccessLevel.EVERYONE,
environmentsAccessLevel: featureAccessLevel.EVERYONE,
featureFlagsAccessLevel: featureAccessLevel.PROJECT_MEMBERS,
infrastructureAccessLevel: featureAccessLevel.PROJECT_MEMBERS,
@@ -287,18 +295,6 @@ export default {
);
},
- packageRegistryFeatureAccessLevelOptions() {
- const options = [FEATURE_ACCESS_LEVEL_ANONYMOUS];
-
- if (this.visibilityLevel === VISIBILITY_LEVEL_PRIVATE_INTEGER) {
- options.unshift(featureAccessLevelMembers);
- } else if (this.visibilityLevel === VISIBILITY_LEVEL_INTERNAL_INTEGER) {
- options.unshift(featureAccessLevelEveryone);
- }
-
- return options;
- },
-
pagesFeatureAccessLevelOptions() {
const options = [featureAccessLevelMembers];
@@ -318,10 +314,6 @@ export default {
return options;
},
- operationsEnabled() {
- return this.operationsAccessLevel > featureAccessLevel.NOT_ENABLED;
- },
-
environmentsEnabled() {
return this.environmentsAccessLevel > featureAccessLevel.NOT_ENABLED;
},
@@ -351,7 +343,7 @@ export default {
}
return s__(
- 'ProjectSettings|View and edit files in this project. Non-project members have only read access.',
+ 'ProjectSettings|View and edit files in this project. When set to **Everyone With Access** non-project members have only read access.',
);
},
cveIdRequestIsDisabled() {
@@ -366,16 +358,17 @@ export default {
packageRegistryAccessLevelEnabled() {
return this.glFeatures.packageRegistryAccessLevel;
},
- splitOperationsEnabled() {
- return this.glFeatures.splitOperationsVisibilityPermissions;
+ packageRegistryEnabled() {
+ return this.packageRegistryAccessLevel > featureAccessLevel.NOT_ENABLED;
+ },
+ packageRegistryApiForEveryoneEnabled() {
+ return this.packageRegistryAccessLevel === FEATURE_ACCESS_LEVEL_ANONYMOUS[0];
+ },
+ packageRegistryApiForEveryoneEnabledShown() {
+ return this.visibilityLevel !== VISIBILITY_LEVEL_PUBLIC_INTEGER;
},
monitorOperationsFeatureAccessLevelOptions() {
- if (this.splitOperationsEnabled) {
- return this.featureAccessLevelOptions.filter(([value]) => value <= this.monitorAccessLevel);
- }
- return this.featureAccessLevelOptions.filter(
- ([value]) => value <= this.operationsAccessLevel,
- );
+ return this.featureAccessLevelOptions.filter(([value]) => value <= this.monitorAccessLevel);
},
},
@@ -429,10 +422,6 @@ export default {
featureAccessLevel.PROJECT_MEMBERS,
this.securityAndComplianceAccessLevel,
);
- this.operationsAccessLevel = Math.min(
- featureAccessLevel.PROJECT_MEMBERS,
- this.operationsAccessLevel,
- );
this.environmentsAccessLevel = Math.min(
featureAccessLevel.PROJECT_MEMBERS,
this.environmentsAccessLevel,
@@ -474,9 +463,8 @@ export default {
this.packageRegistryAccessLevelEnabled &&
this.packageRegistryAccessLevel === featureAccessLevel.PROJECT_MEMBERS
) {
- this.packageRegistryAccessLevel = Math.min(
- ...this.packageRegistryFeatureAccessLevelOptions.map((option) => option[0]),
- );
+ this.packageRegistryAccessLevel =
+ PACKAGE_REGISTRY_ACCESS_LEVEL_DEFAULT_BY_PROJECT_VISIBILITY[value];
}
if (this.buildsAccessLevel > featureAccessLevel.NOT_ENABLED)
this.buildsAccessLevel = featureAccessLevel.EVERYONE;
@@ -492,8 +480,6 @@ export default {
this.metricsDashboardAccessLevel = featureAccessLevel.EVERYONE;
if (this.requirementsAccessLevel === featureAccessLevel.PROJECT_MEMBERS)
this.requirementsAccessLevel = featureAccessLevel.EVERYONE;
- if (this.operationsAccessLevel === featureAccessLevel.PROJECT_MEMBERS)
- this.operationsAccessLevel = featureAccessLevel.EVERYONE;
if (this.environmentsAccessLevel === featureAccessLevel.PROJECT_MEMBERS)
this.environmentsAccessLevel = featureAccessLevel.EVERYONE;
if (this.monitorAccessLevel === featureAccessLevel.PROJECT_MEMBERS)
@@ -532,10 +518,6 @@ export default {
toggleHiddenClassBySelector('.merge-requests-feature', false);
},
- operationsAccessLevel(value, oldValue) {
- this.updateSubFeatureAccessLevel(value, oldValue);
- },
-
monitorAccessLevel(value, oldValue) {
this.updateSubFeatureAccessLevel(value, oldValue);
},
@@ -561,6 +543,22 @@ export default {
visibilityAllowed(option) {
return this.allowedVisibilityOptions.includes(option);
},
+ onPackageRegistryEnabledToggle(value) {
+ this.packageRegistryAccessLevel = value
+ ? this.packageRegistryAccessLevelDefault()
+ : featureAccessLevel.NOT_ENABLED;
+ },
+ onPackageRegistryApiForEveryoneEnabledToggle(value) {
+ this.packageRegistryAccessLevel = value
+ ? FEATURE_ACCESS_LEVEL_ANONYMOUS[0]
+ : this.packageRegistryAccessLevelDefault();
+ },
+ packageRegistryAccessLevelDefault() {
+ return (
+ PACKAGE_REGISTRY_ACCESS_LEVEL_DEFAULT_BY_PROJECT_VISIBILITY[this.visibilityLevel] ??
+ featureAccessLevel.NOT_ENABLED
+ );
+ },
},
};
</script>
@@ -897,10 +895,36 @@ export default {
:help-text="$options.i18n.packageRegistryHelpText"
data-testid="package-registry-access-level"
>
- <project-feature-setting
- v-model="packageRegistryAccessLevel"
+ <gl-toggle
+ class="gl-my-2"
+ :value="packageRegistryEnabled"
:label="$options.i18n.packageRegistryLabel"
- :options="packageRegistryFeatureAccessLevelOptions"
+ label-position="hidden"
+ name="package_registry_enabled"
+ @change="onPackageRegistryEnabledToggle"
+ />
+ <div
+ v-if="packageRegistryApiForEveryoneEnabledShown"
+ class="project-feature-setting-group gl-pl-7 gl-sm-pl-5 gl-my-3"
+ >
+ <project-setting-row
+ :label="$options.i18n.packageRegistryForEveryoneLabel"
+ :help-text="$options.i18n.packageRegistryForEveryoneHelpText"
+ >
+ <gl-toggle
+ class="gl-my-2"
+ :value="packageRegistryApiForEveryoneEnabled"
+ :disabled="!packageRegistryEnabled"
+ :label="$options.i18n.packageRegistryForEveryoneLabel"
+ label-position="hidden"
+ name="package_registry_api_for_everyone_enabled"
+ @change="onPackageRegistryApiForEveryoneEnabledToggle"
+ />
+ </project-setting-row>
+ </div>
+ <input
+ :value="packageRegistryAccessLevel"
+ type="hidden"
name="project[project_feature_attributes][package_registry_access_level]"
/>
</project-setting-row>
@@ -923,11 +947,10 @@ export default {
/>
</project-setting-row>
<project-setting-row
- v-if="splitOperationsEnabled"
ref="monitor-settings"
:label="$options.i18n.monitorLabel"
:help-text="
- s__('ProjectSettings|Configure your project resources and monitor their health.')
+ s__('ProjectSettings|Monitor the health of your project and respond to incidents.')
"
>
<project-feature-setting
@@ -937,21 +960,6 @@ export default {
name="project[project_feature_attributes][monitor_access_level]"
/>
</project-setting-row>
- <project-setting-row
- v-else
- ref="operations-settings"
- :label="$options.i18n.operationsLabel"
- :help-text="
- s__('ProjectSettings|Configure your project resources and monitor their health.')
- "
- >
- <project-feature-setting
- v-model="operationsAccessLevel"
- :label="$options.i18n.operationsLabel"
- :options="featureAccessLevelOptions"
- name="project[project_feature_attributes][operations_access_level]"
- />
- </project-setting-row>
<div class="project-feature-setting-group gl-pl-7 gl-sm-pl-5">
<project-setting-row
ref="metrics-visibility-settings"
@@ -966,47 +974,45 @@ export default {
/>
</project-setting-row>
</div>
- <template v-if="splitOperationsEnabled">
- <project-setting-row
- ref="environments-settings"
+ <project-setting-row
+ ref="environments-settings"
+ :label="$options.i18n.environmentsLabel"
+ :help-text="$options.i18n.environmentsHelpText"
+ :help-path="environmentsHelpPath"
+ >
+ <project-feature-setting
+ v-model="environmentsAccessLevel"
:label="$options.i18n.environmentsLabel"
- :help-text="$options.i18n.environmentsHelpText"
- :help-path="environmentsHelpPath"
- >
- <project-feature-setting
- v-model="environmentsAccessLevel"
- :label="$options.i18n.environmentsLabel"
- :options="featureAccessLevelOptions"
- name="project[project_feature_attributes][environments_access_level]"
- />
- </project-setting-row>
- <project-setting-row
- ref="feature-flags-settings"
+ :options="featureAccessLevelOptions"
+ name="project[project_feature_attributes][environments_access_level]"
+ />
+ </project-setting-row>
+ <project-setting-row
+ ref="feature-flags-settings"
+ :label="$options.i18n.featureFlagsLabel"
+ :help-text="$options.i18n.featureFlagsHelpText"
+ :help-path="featureFlagsHelpPath"
+ >
+ <project-feature-setting
+ v-model="featureFlagsAccessLevel"
:label="$options.i18n.featureFlagsLabel"
- :help-text="$options.i18n.featureFlagsHelpText"
- :help-path="featureFlagsHelpPath"
- >
- <project-feature-setting
- v-model="featureFlagsAccessLevel"
- :label="$options.i18n.featureFlagsLabel"
- :options="featureAccessLevelOptions"
- name="project[project_feature_attributes][feature_flags_access_level]"
- />
- </project-setting-row>
- <project-setting-row
- ref="infrastructure-settings"
+ :options="featureAccessLevelOptions"
+ name="project[project_feature_attributes][feature_flags_access_level]"
+ />
+ </project-setting-row>
+ <project-setting-row
+ ref="infrastructure-settings"
+ :label="$options.i18n.infrastructureLabel"
+ :help-text="$options.i18n.infrastructureHelpText"
+ :help-path="infrastructureHelpPath"
+ >
+ <project-feature-setting
+ v-model="infrastructureAccessLevel"
:label="$options.i18n.infrastructureLabel"
- :help-text="$options.i18n.infrastructureHelpText"
- :help-path="infrastructureHelpPath"
- >
- <project-feature-setting
- v-model="infrastructureAccessLevel"
- :label="$options.i18n.infrastructureLabel"
- :options="featureAccessLevelOptions"
- name="project[project_feature_attributes][infrastructure_access_level]"
- />
- </project-setting-row>
- </template>
+ :options="featureAccessLevelOptions"
+ name="project[project_feature_attributes][infrastructure_access_level]"
+ />
+ </project-setting-row>
<project-setting-row
ref="releases-settings"
:label="$options.i18n.releasesLabel"
diff --git a/app/assets/javascripts/pages/projects/shared/web_ide_link/index.js b/app/assets/javascripts/pages/projects/shared/web_ide_link/index.js
index 5f08943d211..84ff802c268 100644
--- a/app/assets/javascripts/pages/projects/shared/web_ide_link/index.js
+++ b/app/assets/javascripts/pages/projects/shared/web_ide_link/index.js
@@ -1,7 +1,15 @@
import Vue from 'vue';
+import VueApollo from 'vue-apollo';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import { joinPaths, webIDEUrl } from '~/lib/utils/url_utility';
import WebIdeButton from '~/vue_shared/components/web_ide_link.vue';
+import createDefaultClient from '~/lib/graphql';
+
+Vue.use(VueApollo);
+
+const apolloProvider = new VueApollo({
+ defaultClient: createDefaultClient(),
+});
export default ({ el, router }) => {
if (!el) return;
@@ -9,15 +17,18 @@ export default ({ el, router }) => {
const { projectPath, ref, isBlob, webIdeUrl, ...options } = convertObjectPropsToCamelCase(
JSON.parse(el.dataset.options),
);
+ const { webIdePromoPopoverImg } = el.dataset;
// eslint-disable-next-line no-new
new Vue({
el,
router,
+ apolloProvider,
render(h) {
return h(WebIdeButton, {
props: {
isBlob,
+ webIdePromoPopoverImg,
webIdeUrl: isBlob
? webIdeUrl
: webIDEUrl(
diff --git a/app/assets/javascripts/pages/projects/tags/new/index.js b/app/assets/javascripts/pages/projects/tags/new/index.js
index 9ef1017f9f2..eb1f705eab9 100644
--- a/app/assets/javascripts/pages/projects/tags/new/index.js
+++ b/app/assets/javascripts/pages/projects/tags/new/index.js
@@ -1,8 +1,8 @@
import $ from 'jquery';
import GLForm from '~/gl_form';
-import RefSelectDropdown from '~/ref_select_dropdown';
import ZenMode from '~/zen_mode';
+import initNewTagRefSelector from '~/tags/init_new_tag_ref_selector';
+initNewTagRefSelector();
new ZenMode(); // eslint-disable-line no-new
new GLForm($('.tag-form')); // eslint-disable-line no-new
-new RefSelectDropdown($('.js-branch-select')); // eslint-disable-line no-new
diff --git a/app/assets/javascripts/pages/registrations/new/index.js b/app/assets/javascripts/pages/registrations/new/index.js
index 897acf9b02c..eaafc0235a8 100644
--- a/app/assets/javascripts/pages/registrations/new/index.js
+++ b/app/assets/javascripts/pages/registrations/new/index.js
@@ -4,6 +4,7 @@ import NoEmojiValidator from '~/emoji/no_emoji_validator';
import LengthValidator from '~/pages/sessions/new/length_validator';
import UsernameValidator from '~/pages/sessions/new/username_validator';
import EmailFormatValidator from '~/pages/sessions/new/email_format_validator';
+import { initLanguageSwitcher } from '~/language_switcher';
import Tracking from '~/tracking';
new UsernameValidator(); // eslint-disable-line no-new
@@ -19,3 +20,5 @@ trackNewRegistrations();
Tracking.enableFormTracking({
forms: { allow: ['new_user'] },
});
+
+initLanguageSwitcher();
diff --git a/app/assets/javascripts/pages/sessions/new/index.js b/app/assets/javascripts/pages/sessions/new/index.js
index b62417cf595..a84ed5f01ad 100644
--- a/app/assets/javascripts/pages/sessions/new/index.js
+++ b/app/assets/javascripts/pages/sessions/new/index.js
@@ -1,6 +1,7 @@
import $ from 'jquery';
import initVueAlerts from '~/vue_alerts';
import NoEmojiValidator from '~/emoji/no_emoji_validator';
+import { initLanguageSwitcher } from '~/language_switcher';
import LengthValidator from './length_validator';
import OAuthRememberMe from './oauth_remember_me';
import preserveUrlFragment from './preserve_url_fragment';
@@ -20,3 +21,4 @@ new OAuthRememberMe({
// redirected to sign-in after attempting to access a protected URL that included a fragment.
preserveUrlFragment(window.location.hash);
initVueAlerts();
+initLanguageSwitcher();
diff --git a/app/assets/javascripts/pages/shared/wikis/components/wiki_content.vue b/app/assets/javascripts/pages/shared/wikis/components/wiki_content.vue
index b72579276e8..b19809aff53 100644
--- a/app/assets/javascripts/pages/shared/wikis/components/wiki_content.vue
+++ b/app/assets/javascripts/pages/shared/wikis/components/wiki_content.vue
@@ -1,10 +1,11 @@
<script>
-import { GlSkeletonLoader, GlSafeHtmlDirective, GlAlert } from '@gitlab/ui';
+import { GlSkeletonLoader, GlAlert } from '@gitlab/ui';
+import SafeHtml from '~/vue_shared/directives/safe_html';
import { createAlert } from '~/flash';
import { __ } from '~/locale';
import axios from '~/lib/utils/axios_utils';
import { handleLocationHash } from '~/lib/utils/common_utils';
-import { renderGFM } from '../render_gfm_facade';
+import { renderGFM } from '~/behaviors/markdown/render_gfm';
export default {
components: {
@@ -12,7 +13,7 @@ export default {
GlAlert,
},
directives: {
- SafeHtml: GlSafeHtmlDirective,
+ SafeHtml,
},
props: {
getWikiContentUrl: {
@@ -86,9 +87,9 @@ export default {
<div
v-else-if="!loadingContentFailed && !isLoadingContent"
ref="content"
+ v-safe-html="content"
data-qa-selector="wiki_page_content"
data-testid="wiki-page-content"
class="js-wiki-page-content md"
- v-html="content /* eslint-disable-line vue/no-v-html */"
></div>
</template>
diff --git a/app/assets/javascripts/pages/shared/wikis/render_gfm_facade.js b/app/assets/javascripts/pages/shared/wikis/render_gfm_facade.js
deleted file mode 100644
index 90cc2983153..00000000000
--- a/app/assets/javascripts/pages/shared/wikis/render_gfm_facade.js
+++ /dev/null
@@ -1,5 +0,0 @@
-import $ from 'jquery';
-
-export const renderGFM = (el) => {
- return $(el).renderGFM();
-};
diff --git a/app/assets/javascripts/pages/web_ide/remote_ide/index.js b/app/assets/javascripts/pages/web_ide/remote_ide/index.js
new file mode 100644
index 00000000000..463798e85b9
--- /dev/null
+++ b/app/assets/javascripts/pages/web_ide/remote_ide/index.js
@@ -0,0 +1,3 @@
+import { mountRemoteIDE } from '~/ide/remote';
+
+mountRemoteIDE(document.getElementById('ide'));
diff --git a/app/assets/javascripts/performance_bar/components/detailed_metric.vue b/app/assets/javascripts/performance_bar/components/detailed_metric.vue
index 0640faae8b7..ea8005e8dfb 100644
--- a/app/assets/javascripts/performance_bar/components/detailed_metric.vue
+++ b/app/assets/javascripts/performance_bar/components/detailed_metric.vue
@@ -1,5 +1,5 @@
<script>
-import { GlButton, GlDropdown, GlDropdownItem, GlModal, GlModalDirective } from '@gitlab/ui';
+import { GlButton, GlModal, GlModalDirective, GlCollapsibleListbox } from '@gitlab/ui';
import { __, s__ } from '~/locale';
import { sortOrders, sortOrderOptions } from '../constants';
@@ -9,9 +9,8 @@ export default {
components: {
RequestWarning,
GlButton,
- GlDropdown,
- GlDropdownItem,
GlModal,
+ GlCollapsibleListbox,
},
directives: {
'gl-modal': GlModalDirective,
@@ -119,9 +118,6 @@ export default {
itemHasOpenedBacktrace(toggledIndex) {
return this.openedBacktraces.find((openedIndex) => openedIndex === toggledIndex) >= 0;
},
- changeSortOrder(order) {
- this.sortOrder = order;
- },
sortDetailByDuration(a, b) {
return a.duration < b.duration ? 1 : -1;
},
@@ -157,19 +153,14 @@ export default {
</div>
</div>
</div>
- <gl-dropdown
+ <gl-collapsible-listbox
v-if="displaySortOrder"
- :text="$options.sortOrderOptions[sortOrder]"
+ v-model="sortOrder"
+ :toggle-text="$options.sortOrderOptions[sortOrder].text"
+ :items="Object.values($options.sortOrderOptions)"
right
data-testid="performance-bar-sort-order"
- >
- <gl-dropdown-item
- v-for="option in Object.keys($options.sortOrderOptions)"
- :key="option"
- @click="changeSortOrder(option)"
- >{{ $options.sortOrderOptions[option] }}</gl-dropdown-item
- >
- </gl-dropdown>
+ />
</div>
<hr />
<table class="table gl-table">
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 a5fa85f1ed5..dbca8bc9be7 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>
-import { GlSafeHtmlDirective } from '@gitlab/ui';
+import SafeHtml from '~/vue_shared/directives/safe_html';
import { glEmojiTag } from '~/emoji';
import { mergeUrlParams } from '~/lib/utils/url_utility';
@@ -15,7 +15,7 @@ export default {
RequestSelector,
},
directives: {
- SafeHtml: GlSafeHtmlDirective,
+ SafeHtml,
},
props: {
store: {
diff --git a/app/assets/javascripts/performance_bar/components/request_warning.vue b/app/assets/javascripts/performance_bar/components/request_warning.vue
index 3ebd222029b..91e905d62e6 100644
--- a/app/assets/javascripts/performance_bar/components/request_warning.vue
+++ b/app/assets/javascripts/performance_bar/components/request_warning.vue
@@ -1,5 +1,6 @@
<script>
-import { GlPopover, GlSafeHtmlDirective } from '@gitlab/ui';
+import { GlPopover } from '@gitlab/ui';
+import SafeHtml from '~/vue_shared/directives/safe_html';
import { glEmojiTag } from '~/emoji';
export default {
@@ -7,7 +8,7 @@ export default {
GlPopover,
},
directives: {
- SafeHtml: GlSafeHtmlDirective,
+ SafeHtml,
},
props: {
htmlId: {
diff --git a/app/assets/javascripts/performance_bar/constants.js b/app/assets/javascripts/performance_bar/constants.js
index 09745797424..6f4ddd5c242 100644
--- a/app/assets/javascripts/performance_bar/constants.js
+++ b/app/assets/javascripts/performance_bar/constants.js
@@ -6,6 +6,12 @@ export const sortOrders = {
};
export const sortOrderOptions = {
- [sortOrders.DURATION]: s__('PerformanceBar|Sort by duration'),
- [sortOrders.CHRONOLOGICAL]: s__('PerformanceBar|Sort chronologically'),
+ [sortOrders.DURATION]: {
+ value: sortOrders.DURATION,
+ text: s__('PerformanceBar|Sort by duration'),
+ },
+ [sortOrders.CHRONOLOGICAL]: {
+ value: sortOrders.CHRONOLOGICAL,
+ text: s__('PerformanceBar|Sort chronologically'),
+ },
};
diff --git a/app/assets/javascripts/pipeline_new/components/legacy_pipeline_new_form.vue b/app/assets/javascripts/pipeline_new/components/legacy_pipeline_new_form.vue
deleted file mode 100644
index cd7cb7f8393..00000000000
--- a/app/assets/javascripts/pipeline_new/components/legacy_pipeline_new_form.vue
+++ /dev/null
@@ -1,490 +0,0 @@
-<script>
-import {
- GlAlert,
- GlIcon,
- GlButton,
- GlDropdown,
- GlDropdownItem,
- GlForm,
- GlFormGroup,
- GlFormInput,
- GlFormTextarea,
- GlLink,
- GlSprintf,
- GlLoadingIcon,
- GlSafeHtmlDirective as SafeHtml,
-} from '@gitlab/ui';
-import * as Sentry from '@sentry/browser';
-import { uniqueId } from 'lodash';
-import Vue from 'vue';
-import axios from '~/lib/utils/axios_utils';
-import { backOff } from '~/lib/utils/common_utils';
-import httpStatusCodes from '~/lib/utils/http_status';
-import { redirectTo } from '~/lib/utils/url_utility';
-import { s__, __, n__ } from '~/locale';
-import {
- VARIABLE_TYPE,
- FILE_TYPE,
- CONFIG_VARIABLES_TIMEOUT,
- CC_VALIDATION_REQUIRED_ERROR,
-} from '../constants';
-import filterVariables from '../utils/filter_variables';
-import RefsDropdown from './refs_dropdown.vue';
-
-const i18n = {
- variablesDescription: s__(
- 'Pipeline|Specify variable values to be used in this run. The values specified in %{linkStart}CI/CD settings%{linkEnd} will be used by default.',
- ),
- defaultError: __('Something went wrong on our end. Please try again.'),
- refsLoadingErrorTitle: s__('Pipeline|Branches or tags could not be loaded.'),
- submitErrorTitle: s__('Pipeline|Pipeline cannot be run.'),
- warningTitle: __('The form contains the following warning:'),
- maxWarningsSummary: __('%{total} warnings found: showing first %{warningsDisplayed}'),
- removeVariableLabel: s__('CiVariables|Remove variable'),
-};
-
-export default {
- typeOptions: {
- [VARIABLE_TYPE]: __('Variable'),
- [FILE_TYPE]: __('File'),
- },
- i18n,
- formElementClasses: 'gl-mr-3 gl-mb-3 gl-flex-basis-quarter gl-flex-shrink-0 gl-flex-grow-0',
- // this height value is used inline on the textarea to match the input field height
- // it's used to prevent the overwrite if 'gl-h-7' or 'gl-h-7!' were used
- textAreaStyle: { height: '32px' },
- components: {
- GlAlert,
- GlIcon,
- GlButton,
- GlDropdown,
- GlDropdownItem,
- GlForm,
- GlFormGroup,
- GlFormInput,
- GlFormTextarea,
- GlLink,
- GlSprintf,
- GlLoadingIcon,
- RefsDropdown,
- CcValidationRequiredAlert: () =>
- import('ee_component/billings/components/cc_validation_required_alert.vue'),
- },
- directives: { SafeHtml },
- props: {
- pipelinesPath: {
- type: String,
- required: true,
- },
- configVariablesPath: {
- type: String,
- required: true,
- },
- defaultBranch: {
- type: String,
- required: true,
- },
- projectId: {
- type: String,
- required: true,
- },
- settingsLink: {
- type: String,
- required: true,
- },
- fileParams: {
- type: Object,
- required: false,
- default: () => ({}),
- },
- refParam: {
- type: String,
- required: false,
- default: '',
- },
- variableParams: {
- type: Object,
- required: false,
- default: () => ({}),
- },
- maxWarnings: {
- type: Number,
- required: true,
- },
- },
- data() {
- return {
- refValue: {
- shortName: this.refParam,
- },
- form: {},
- errorTitle: null,
- error: null,
- warnings: [],
- totalWarnings: 0,
- isWarningDismissed: false,
- isLoading: false,
- submitted: false,
- ccAlertDismissed: false,
- };
- },
- computed: {
- overMaxWarningsLimit() {
- return this.totalWarnings > this.maxWarnings;
- },
- warningsSummary() {
- return n__('%d warning found:', '%d warnings found:', this.warnings.length);
- },
- summaryMessage() {
- return this.overMaxWarningsLimit ? i18n.maxWarningsSummary : this.warningsSummary;
- },
- shouldShowWarning() {
- return this.warnings.length > 0 && !this.isWarningDismissed;
- },
- refShortName() {
- return this.refValue.shortName;
- },
- refFullName() {
- return this.refValue.fullName;
- },
- variables() {
- return this.form[this.refFullName]?.variables ?? [];
- },
- descriptions() {
- return this.form[this.refFullName]?.descriptions ?? {};
- },
- ccRequiredError() {
- return this.error === CC_VALIDATION_REQUIRED_ERROR && !this.ccAlertDismissed;
- },
- },
- watch: {
- refValue() {
- this.loadConfigVariablesForm();
- },
- },
- created() {
- // this is needed until we add support for ref type in url query strings
- // ensure default branch is called with full ref on load
- // https://gitlab.com/gitlab-org/gitlab/-/issues/287815
- if (this.refValue.shortName === this.defaultBranch) {
- this.refValue.fullName = `refs/heads/${this.refValue.shortName}`;
- }
-
- this.loadConfigVariablesForm();
- },
- methods: {
- addEmptyVariable(refValue) {
- const { variables } = this.form[refValue];
-
- const lastVar = variables[variables.length - 1];
- if (lastVar?.key === '' && lastVar?.value === '') {
- return;
- }
-
- variables.push({
- uniqueId: uniqueId(`var-${refValue}`),
- variable_type: VARIABLE_TYPE,
- key: '',
- value: '',
- });
- },
- setVariable(refValue, type, key, value) {
- const { variables } = this.form[refValue];
-
- const variable = variables.find((v) => v.key === key);
- if (variable) {
- variable.type = type;
- variable.value = value;
- } else {
- variables.push({
- uniqueId: uniqueId(`var-${refValue}`),
- key,
- value,
- variable_type: type,
- });
- }
- },
- setVariableType(key, type) {
- const { variables } = this.form[this.refFullName];
- const variable = variables.find((v) => v.key === key);
- variable.variable_type = type;
- },
- setVariableParams(refValue, type, paramsObj) {
- Object.entries(paramsObj).forEach(([key, value]) => {
- this.setVariable(refValue, type, key, value);
- });
- },
- removeVariable(index) {
- this.variables.splice(index, 1);
- },
- canRemove(index) {
- return index < this.variables.length - 1;
- },
- loadConfigVariablesForm() {
- // Skip when variables already cached in `form`
- if (this.form[this.refFullName]) {
- return;
- }
-
- this.fetchConfigVariables(this.refFullName || this.refShortName)
- .then(({ descriptions, params }) => {
- Vue.set(this.form, this.refFullName, {
- variables: [],
- descriptions,
- });
-
- // Add default variables from yml
- this.setVariableParams(this.refFullName, VARIABLE_TYPE, params);
- })
- .catch(() => {
- Vue.set(this.form, this.refFullName, {
- variables: [],
- descriptions: {},
- });
- })
- .finally(() => {
- // Add/update variables, e.g. from query string
- if (this.variableParams) {
- this.setVariableParams(this.refFullName, VARIABLE_TYPE, this.variableParams);
- }
- if (this.fileParams) {
- this.setVariableParams(this.refFullName, FILE_TYPE, this.fileParams);
- }
-
- // Adds empty var at the end of the form
- this.addEmptyVariable(this.refFullName);
- });
- },
- fetchConfigVariables(refValue) {
- this.isLoading = true;
-
- return backOff((next, stop) => {
- axios
- .get(this.configVariablesPath, {
- params: {
- sha: refValue,
- },
- })
- .then(({ data, status }) => {
- if (status === httpStatusCodes.NO_CONTENT) {
- next();
- } else {
- this.isLoading = false;
- stop(data);
- }
- })
- .catch((error) => {
- stop(error);
- });
- }, CONFIG_VARIABLES_TIMEOUT)
- .then((data) => {
- const params = {};
- const descriptions = {};
-
- Object.entries(data).forEach(([key, { value, description }]) => {
- if (description) {
- params[key] = value;
- descriptions[key] = description;
- }
- });
-
- return { params, descriptions };
- })
- .catch((error) => {
- this.isLoading = false;
-
- Sentry.captureException(error);
-
- return { params: {}, descriptions: {} };
- });
- },
- createPipeline() {
- this.submitted = true;
- this.ccAlertDismissed = false;
-
- return axios
- .post(this.pipelinesPath, {
- // send shortName as fall back for query params
- // https://gitlab.com/gitlab-org/gitlab/-/issues/287815
- ref: this.refValue.fullName || this.refShortName,
- variables_attributes: filterVariables(this.variables),
- })
- .then(({ data }) => {
- redirectTo(`${this.pipelinesPath}/${data.id}`);
- })
- .catch((err) => {
- // always re-enable submit button
- this.submitted = false;
-
- const {
- errors = [],
- warnings = [],
- total_warnings: totalWarnings = 0,
- } = err.response.data;
- const [error] = errors;
-
- this.reportError({
- title: i18n.submitErrorTitle,
- error,
- warnings,
- totalWarnings,
- });
- });
- },
- onRefsLoadingError(error) {
- this.reportError({ title: i18n.refsLoadingErrorTitle });
-
- Sentry.captureException(error);
- },
- reportError({ title = null, error = i18n.defaultError, warnings = [], totalWarnings = 0 }) {
- this.errorTitle = title;
- this.error = error;
- 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" @dismiss="dismissError" />
- <gl-alert
- v-else-if="error"
- :title="errorTitle"
- :dismissible="false"
- variant="danger"
- class="gl-mb-4"
- data-testid="run-pipeline-error-alert"
- >
- <span v-safe-html="error"></span>
- </gl-alert>
- <gl-alert
- v-if="shouldShowWarning"
- :title="$options.i18n.warningTitle"
- variant="warning"
- class="gl-mb-4"
- data-testid="run-pipeline-warning-alert"
- @dismiss="isWarningDismissed = true"
- >
- <details>
- <summary>
- <gl-sprintf :message="summaryMessage">
- <template #total>
- {{ totalWarnings }}
- </template>
- <template #warningsDisplayed>
- {{ maxWarnings }}
- </template>
- </gl-sprintf>
- </summary>
- <p
- v-for="(warning, index) in warnings"
- :key="`warning-${index}`"
- data-testid="run-pipeline-warning"
- >
- {{ warning }}
- </p>
- </details>
- </gl-alert>
- <gl-form-group :label="s__('Pipeline|Run for branch name or tag')">
- <refs-dropdown v-model="refValue" @loadingError="onRefsLoadingError" />
- </gl-form-group>
-
- <gl-loading-icon v-if="isLoading" class="gl-mb-5" size="lg" />
-
- <gl-form-group v-else :label="s__('Pipeline|Variables')">
- <div
- v-for="(variable, index) in variables"
- :key="variable.uniqueId"
- class="gl-mb-3 gl-pb-2"
- data-testid="ci-variable-row"
- data-qa-selector="ci_variable_row_container"
- >
- <div
- class="gl-display-flex gl-align-items-stretch gl-flex-direction-column gl-md-flex-direction-row"
- >
- <gl-dropdown
- :text="$options.typeOptions[variable.variable_type]"
- :class="$options.formElementClasses"
- data-testid="pipeline-form-ci-variable-type"
- >
- <gl-dropdown-item
- v-for="type in Object.keys($options.typeOptions)"
- :key="type"
- @click="setVariableType(variable.key, type)"
- >
- {{ $options.typeOptions[type] }}
- </gl-dropdown-item>
- </gl-dropdown>
- <gl-form-input
- v-model="variable.key"
- :placeholder="s__('CiVariables|Input variable key')"
- :class="$options.formElementClasses"
- data-testid="pipeline-form-ci-variable-key"
- data-qa-selector="ci_variable_key_field"
- @change="addEmptyVariable(refFullName)"
- />
- <gl-form-textarea
- v-model="variable.value"
- :placeholder="s__('CiVariables|Input variable value')"
- class="gl-mb-3"
- :style="$options.textAreaStyle"
- :no-resize="false"
- data-testid="pipeline-form-ci-variable-value"
- data-qa-selector="ci_variable_value_field"
- />
-
- <template v-if="variables.length > 1">
- <gl-button
- v-if="canRemove(index)"
- class="gl-md-ml-3 gl-mb-3"
- data-testid="remove-ci-variable-row"
- variant="danger"
- category="secondary"
- :aria-label="$options.i18n.removeVariableLabel"
- @click="removeVariable(index)"
- >
- <gl-icon class="gl-mr-0! gl-display-none gl-md-display-block" name="clear" />
- <span class="gl-md-display-none">{{ $options.i18n.removeVariableLabel }}</span>
- </gl-button>
- <gl-button
- v-else
- class="gl-md-ml-3 gl-mb-3 gl-display-none gl-md-display-block gl-visibility-hidden"
- icon="clear"
- :aria-label="$options.i18n.removeVariableLabel"
- />
- </template>
- </div>
- <div v-if="descriptions[variable.key]" class="gl-text-gray-500 gl-mb-3">
- {{ descriptions[variable.key] }}
- </div>
- </div>
-
- <template #description
- ><gl-sprintf :message="$options.i18n.variablesDescription">
- <template #link="{ content }">
- <gl-link :href="settingsLink">{{ content }}</gl-link>
- </template>
- </gl-sprintf></template
- >
- </gl-form-group>
- <div class="gl-pt-5 gl-display-flex">
- <gl-button
- type="submit"
- category="primary"
- variant="confirm"
- class="js-no-auto-disable gl-mr-3"
- data-qa-selector="run_pipeline_button"
- data-testid="run_pipeline_button"
- :disabled="submitted"
- >{{ s__('Pipeline|Run pipeline') }}</gl-button
- >
- <gl-button :href="pipelinesPath">{{ __('Cancel') }}</gl-button>
- </div>
- </gl-form>
-</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 a9af1181027..5692627abef 100644
--- a/app/assets/javascripts/pipeline_new/components/pipeline_new_form.vue
+++ b/app/assets/javascripts/pipeline_new/components/pipeline_new_form.vue
@@ -12,11 +12,11 @@ import {
GlLink,
GlSprintf,
GlLoadingIcon,
- GlSafeHtmlDirective as SafeHtml,
} from '@gitlab/ui';
import * as Sentry from '@sentry/browser';
import { uniqueId } from 'lodash';
import Vue from 'vue';
+import SafeHtml from '~/vue_shared/directives/safe_html';
import { redirectTo } from '~/lib/utils/url_utility';
import { s__, __, n__ } from '~/locale';
import { VARIABLE_TYPE, FILE_TYPE, CC_VALIDATION_REQUIRED_ERROR } from '../constants';
@@ -400,11 +400,13 @@ export default {
:class="$options.formElementClasses"
class="gl-flex-grow-1 gl-mr-0!"
data-testid="pipeline-form-ci-variable-value-dropdown"
+ data-qa-selector="ci_variable_value_dropdown"
>
<gl-dropdown-item
v-for="value in predefinedValueOptions[variable.key]"
:key="value"
data-testid="pipeline-form-ci-variable-value-dropdown-items"
+ data-qa-selector="ci_variable_value_dropdown_item"
@click="setVariableAttribute(variable.key, 'value', value)"
>
{{ value }}
diff --git a/app/assets/javascripts/pipeline_new/index.js b/app/assets/javascripts/pipeline_new/index.js
index 60b4c93d1d5..71c76aeab36 100644
--- a/app/assets/javascripts/pipeline_new/index.js
+++ b/app/assets/javascripts/pipeline_new/index.js
@@ -1,53 +1,9 @@
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import createDefaultClient from '~/lib/graphql';
-import LegacyPipelineNewForm from './components/legacy_pipeline_new_form.vue';
import PipelineNewForm from './components/pipeline_new_form.vue';
import { resolvers } from './graphql/resolvers';
-const mountLegacyPipelineNewForm = (el) => {
- const {
- // provide/inject
- projectRefsEndpoint,
-
- // props
- configVariablesPath,
- defaultBranch,
- fileParam,
- maxWarnings,
- pipelinesPath,
- projectId,
- refParam,
- settingsLink,
- varParam,
- } = el.dataset;
-
- const variableParams = JSON.parse(varParam);
- const fileParams = JSON.parse(fileParam);
-
- return new Vue({
- el,
- provide: {
- projectRefsEndpoint,
- },
- render(createElement) {
- return createElement(LegacyPipelineNewForm, {
- props: {
- configVariablesPath,
- defaultBranch,
- fileParams,
- maxWarnings: Number(maxWarnings),
- pipelinesPath,
- projectId,
- refParam,
- settingsLink,
- variableParams,
- },
- });
- },
- });
-};
-
const mountPipelineNewForm = (el) => {
const {
// provide/inject
@@ -101,9 +57,5 @@ const mountPipelineNewForm = (el) => {
export default () => {
const el = document.getElementById('js-new-pipeline');
- if (gon.features?.runPipelineGraphql) {
- mountPipelineNewForm(el);
- } else {
- mountLegacyPipelineNewForm(el);
- }
+ mountPipelineNewForm(el);
};
diff --git a/app/assets/javascripts/pipeline_wizard/components/step_nav.vue b/app/assets/javascripts/pipeline_wizard/components/step_nav.vue
index 8f9198855c6..e3d825bbcc7 100644
--- a/app/assets/javascripts/pipeline_wizard/components/step_nav.vue
+++ b/app/assets/javascripts/pipeline_wizard/components/step_nav.vue
@@ -27,12 +27,13 @@ export default {
</script>
<template>
- <div>
+ <div class="gl-display-flex">
<slot name="before"></slot>
<gl-button
v-if="showBackButton"
category="secondary"
data-testid="back-button"
+ class="gl-mr-3"
@click="$emit('back')"
>
{{ __('Back') }}
diff --git a/app/assets/javascripts/pipelines/components/graph/graph_component_wrapper.vue b/app/assets/javascripts/pipelines/components/graph/graph_component_wrapper.vue
index f822e2c0874..4d7596e6e16 100644
--- a/app/assets/javascripts/pipelines/components/graph/graph_component_wrapper.vue
+++ b/app/assets/javascripts/pipelines/components/graph/graph_component_wrapper.vue
@@ -148,12 +148,13 @@ export default {
reportMessageToSentry(
this.$options.name,
- `| type: ${LOAD_FAILURE} , info: ${serializeLoadErrors(err)}`,
+ `| type: ${LOAD_FAILURE} , info: ${JSON.stringify(err)}`,
{
+ graphViewType: this.graphViewType,
+ graphqlResourceEtag: this.graphqlResourceEtag,
+ metricsPath: this.metricsPath,
projectPath: this.pipelineProjectPath,
pipelineIid: this.pipelineIid,
- pipelineStages: this.pipeline?.stages?.length || 0,
- nbOfDownstreams: this.pipeline?.downstream?.length || 0,
},
);
},
diff --git a/app/assets/javascripts/pipelines/components/graph/job_item.vue b/app/assets/javascripts/pipelines/components/graph/job_item.vue
index 377f21b299f..4f2be27486c 100644
--- a/app/assets/javascripts/pipelines/components/graph/job_item.vue
+++ b/app/assets/javascripts/pipelines/components/graph/job_item.vue
@@ -252,7 +252,7 @@ export default {
@click="jobItemClick"
@mouseout="hideTooltips"
>
- <div class="ci-job-name-component gl-display-flex gl-align-items-center">
+ <div class="gl-display-flex gl-align-items-center gl-flex-grow-1">
<ci-icon :size="24" :status="job.status" class="gl-line-height-0" />
<div class="gl-pl-3 gl-pr-3 gl-display-flex gl-flex-direction-column gl-pipeline-job-width">
<div class="gl-text-truncate gl-pr-9 gl-line-height-normal">{{ job.name }}</div>
diff --git a/app/assets/javascripts/pipelines/components/jobs/failed_jobs_table.vue b/app/assets/javascripts/pipelines/components/jobs/failed_jobs_table.vue
index 18607bfae1c..c56537f4039 100644
--- a/app/assets/javascripts/pipelines/components/jobs/failed_jobs_table.vue
+++ b/app/assets/javascripts/pipelines/components/jobs/failed_jobs_table.vue
@@ -1,5 +1,6 @@
<script>
-import { GlButton, GlLink, GlSafeHtmlDirective, GlTableLite } from '@gitlab/ui';
+import { GlButton, GlLink, GlTableLite } from '@gitlab/ui';
+import SafeHtml from '~/vue_shared/directives/safe_html';
import { __, s__ } from '~/locale';
import { createAlert } from '~/flash';
import { redirectTo } from '~/lib/utils/url_utility';
@@ -17,7 +18,7 @@ export default {
GlTableLite,
},
directives: {
- SafeHtml: GlSafeHtmlDirective,
+ SafeHtml,
},
props: {
failedJobs: {
diff --git a/app/assets/javascripts/pipelines/components/jobs_shared/action_component.vue b/app/assets/javascripts/pipelines/components/jobs_shared/action_component.vue
index 7ee5ec48f44..387b01aee7e 100644
--- a/app/assets/javascripts/pipelines/components/jobs_shared/action_component.vue
+++ b/app/assets/javascripts/pipelines/components/jobs_shared/action_component.vue
@@ -70,7 +70,6 @@ export default {
axios
.post(`${this.link}.json`)
.then(() => {
- this.isDisabled = false;
this.isLoading = false;
this.$emit('pipelineActionRequestComplete');
diff --git a/app/assets/javascripts/pipelines/components/jobs_shared/job_name_component.vue b/app/assets/javascripts/pipelines/components/jobs_shared/job_name_component.vue
index f4fc6893520..1c7f5a7476d 100644
--- a/app/assets/javascripts/pipelines/components/jobs_shared/job_name_component.vue
+++ b/app/assets/javascripts/pipelines/components/jobs_shared/job_name_component.vue
@@ -29,7 +29,7 @@ export default {
};
</script>
<template>
- <span class="ci-job-name-component mw-100 gl-display-flex gl-align-items-center">
+ <span class="mw-100 gl-display-flex gl-align-items-center gl-flex-grow-1">
<ci-icon :size="iconSize" :status="status" class="gl-line-height-0" />
<span class="gl-text-truncate mw-70p gl-pl-3 gl-display-inline-block">
{{ name }}
diff --git a/app/assets/javascripts/pipelines/components/pipeline_mini_graph/job_item.vue b/app/assets/javascripts/pipelines/components/pipeline_mini_graph/job_item.vue
index 211c5f117c7..51b46f25048 100644
--- a/app/assets/javascripts/pipelines/components/pipeline_mini_graph/job_item.vue
+++ b/app/assets/javascripts/pipelines/components/pipeline_mini_graph/job_item.vue
@@ -137,9 +137,6 @@ export default {
hideTooltips() {
this.$root.$emit(BV_HIDE_TOOLTIP);
},
- pipelineActionRequestComplete() {
- this.$emit('pipelineActionRequestComplete');
- },
},
};
</script>
@@ -163,7 +160,7 @@ export default {
@click.stop="hideTooltips"
@mouseout="hideTooltips"
>
- <job-name-component :name="job.name" :status="job.status" :icon-size="24" />
+ <job-name-component :name="job.name" :status="job.status" />
</gl-link>
<div
@@ -175,7 +172,7 @@ export default {
data-testid="job-without-link"
@mouseout="hideTooltips"
>
- <job-name-component :name="job.name" :status="job.status" :icon-size="24" />
+ <job-name-component :name="job.name" :status="job.status" />
</div>
<action-component
@@ -184,7 +181,6 @@ export default {
:link="status.action.path"
:action-icon="status.action.icon"
data-qa-selector="action_button"
- @pipelineActionRequestComplete="pipelineActionRequestComplete"
/>
</div>
</template>
diff --git a/app/assets/javascripts/pipelines/components/pipeline_mini_graph/pipeline_mini_graph.vue b/app/assets/javascripts/pipelines/components/pipeline_mini_graph/pipeline_mini_graph.vue
index 993fa121d89..827adf9f7f7 100644
--- a/app/assets/javascripts/pipelines/components/pipeline_mini_graph/pipeline_mini_graph.vue
+++ b/app/assets/javascripts/pipelines/components/pipeline_mini_graph/pipeline_mini_graph.vue
@@ -35,11 +35,6 @@ export default {
required: true,
default: () => [],
},
- stagesClass: {
- type: [Array, Object, String],
- required: false,
- default: '',
- },
updateDropdown: {
type: Boolean,
required: false,
@@ -56,15 +51,10 @@ export default {
return Boolean(this.downstreamPipelines.length);
},
},
- methods: {
- onPipelineActionRequestComplete() {
- this.$emit('pipelineActionRequestComplete');
- },
- },
};
</script>
<template>
- <div class="stage-cell" data-testid="pipeline-mini-graph">
+ <div data-testid="pipeline-mini-graph">
<linked-pipelines-mini-list
v-if="upstreamPipeline"
:triggered-by="/* eslint-disable @gitlab/vue-no-new-non-primitive-in-template */ [
@@ -82,9 +72,7 @@ export default {
:is-merge-train="isMergeTrain"
:stages="stages"
:update-dropdown="updateDropdown"
- :stages-class="stagesClass"
data-testid="pipeline-stages"
- @pipelineActionRequestComplete="onPipelineActionRequestComplete"
@miniGraphStageClick="$emit('miniGraphStageClick')"
/>
<gl-icon
diff --git a/app/assets/javascripts/pipelines/components/pipeline_mini_graph/pipeline_stage.vue b/app/assets/javascripts/pipelines/components/pipeline_mini_graph/pipeline_stage.vue
index ba150919e58..ec42b738e03 100644
--- a/app/assets/javascripts/pipelines/components/pipeline_mini_graph/pipeline_stage.vue
+++ b/app/assets/javascripts/pipelines/components/pipeline_mini_graph/pipeline_stage.vue
@@ -100,13 +100,6 @@ export default {
});
});
},
- pipelineActionRequestComplete() {
- // close the dropdown in MR widget
- this.$refs.dropdown.hide();
-
- // warn the pipelines table to update
- this.$emit('pipelineActionRequestComplete');
- },
stageAriaLabel(title) {
return sprintf(__('View Stage: %{title}'), { title });
},
@@ -149,7 +142,7 @@ export default {
class="js-builds-dropdown-list scrollable-menu"
data-testid="mini-pipeline-graph-dropdown-menu-list"
>
- <div class="gl--flex-center gl-border-b gl-font-weight-bold gl-pb-3">
+ <div class="gl--flex-center gl-border-b gl-font-weight-bold gl-mb-3 gl-pb-3">
<span class="gl-mr-1">{{ $options.i18n.stage }}</span>
<span data-testid="pipeline-stage-dropdown-menu-title">{{ stageName }}</span>
</div>
@@ -158,11 +151,10 @@ export default {
:dropdown-length="dropdownContent.length"
:job="job"
css-class-job-name="mini-pipeline-graph-dropdown-item"
- @pipelineActionRequestComplete="pipelineActionRequestComplete"
/>
</li>
<template v-if="isMergeTrain">
- <li class="gl-new-dropdown-divider" role="presentation">
+ <li class="gl-dropdown-divider" role="presentation">
<hr role="separator" aria-orientation="horizontal" class="dropdown-divider" />
</li>
<li>
diff --git a/app/assets/javascripts/pipelines/components/pipeline_mini_graph/pipeline_stages.vue b/app/assets/javascripts/pipelines/components/pipeline_mini_graph/pipeline_stages.vue
index e965dc5e6b0..ba549d9b423 100644
--- a/app/assets/javascripts/pipelines/components/pipeline_mini_graph/pipeline_stages.vue
+++ b/app/assets/javascripts/pipelines/components/pipeline_mini_graph/pipeline_stages.vue
@@ -17,22 +17,12 @@ export default {
required: false,
default: false,
},
- stagesClass: {
- type: [Array, Object, String],
- required: false,
- default: '',
- },
isMergeTrain: {
type: Boolean,
required: false,
default: false,
},
},
- methods: {
- onPipelineActionRequestComplete() {
- this.$emit('pipelineActionRequestComplete');
- },
- },
};
</script>
<template>
@@ -40,14 +30,12 @@ export default {
<div
v-for="stage in stages"
:key="stage.name"
- :class="stagesClass"
- class="dropdown gl-display-inline-block gl-mr-2 gl-my-2 gl-vertical-align-middle stage-container"
+ class="pipeline-mini-graph-stage-container dropdown gl-display-inline-block gl-mr-2 gl-my-2 gl-vertical-align-middle"
>
<pipeline-stage
:stage="stage"
:update-dropdown="updateDropdown"
:is-merge-train="isMergeTrain"
- @pipelineActionRequestComplete="onPipelineActionRequestComplete"
@miniGraphStageClick="$emit('miniGraphStageClick')"
/>
</div>
diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/empty_state/pipelines_ci_templates.vue b/app/assets/javascripts/pipelines/components/pipelines_list/empty_state/pipelines_ci_templates.vue
index 3eafb36bd1d..03a2eac89e4 100644
--- a/app/assets/javascripts/pipelines/components/pipelines_list/empty_state/pipelines_ci_templates.vue
+++ b/app/assets/javascripts/pipelines/components/pipelines_list/empty_state/pipelines_ci_templates.vue
@@ -8,7 +8,7 @@ import {
RUNNERS_DOCUMENTATION_LINK_CLICKED_EVENT,
RUNNERS_SETTINGS_BUTTON_CLICKED_EVENT,
I18N,
-} from '~/pipeline_editor/constants';
+} from '~/ci/pipeline_editor/constants';
import Tracking from '~/tracking';
import { helpPagePath } from '~/helpers/help_page_helper';
import { isExperimentVariant } from '~/experimentation/utils';
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 af089aebbbe..7dc1e60610e 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
@@ -3,7 +3,7 @@ import { GlFilteredSearch } from '@gitlab/ui';
import { map } from 'lodash';
import { s__ } from '~/locale';
import Tracking from '~/tracking';
-import { OPERATOR_IS_ONLY } from '~/vue_shared/components/filtered_search_bar/constants';
+import { OPERATORS_IS } from '~/vue_shared/components/filtered_search_bar/constants';
import { TRACKING_CATEGORIES } from '../../constants';
import PipelineBranchNameToken from './tokens/pipeline_branch_name_token.vue';
import PipelineSourceToken from './tokens/pipeline_source_token.vue';
@@ -54,7 +54,7 @@ export default {
title: s__('Pipeline|Trigger author'),
unique: true,
token: PipelineTriggerAuthorToken,
- operators: OPERATOR_IS_ONLY,
+ operators: OPERATORS_IS,
projectId: this.projectId,
},
{
@@ -63,7 +63,7 @@ export default {
title: s__('Pipeline|Branch name'),
unique: true,
token: PipelineBranchNameToken,
- operators: OPERATOR_IS_ONLY,
+ operators: OPERATORS_IS,
projectId: this.projectId,
defaultBranchName: this.defaultBranchName,
disabled: this.selectedTypes.includes(this.$options.tagType),
@@ -74,7 +74,7 @@ export default {
title: s__('Pipeline|Tag name'),
unique: true,
token: PipelineTagNameToken,
- operators: OPERATOR_IS_ONLY,
+ operators: OPERATORS_IS,
projectId: this.projectId,
disabled: this.selectedTypes.includes(this.$options.branchType),
},
@@ -84,7 +84,7 @@ export default {
title: s__('Pipeline|Status'),
unique: true,
token: PipelineStatusToken,
- operators: OPERATOR_IS_ONLY,
+ operators: OPERATORS_IS,
},
{
type: this.$options.sourceType,
@@ -92,7 +92,7 @@ export default {
title: s__('Pipeline|Source'),
unique: true,
token: PipelineSourceToken,
- operators: OPERATOR_IS_ONLY,
+ operators: OPERATORS_IS,
},
];
},
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 f6e46c090d3..346f5735576 100644
--- a/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_table.vue
+++ b/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_table.vue
@@ -124,9 +124,6 @@ export default {
eventHub.$emit('postAction', this.endpoint);
this.cancelingPipeline = this.pipelineId;
},
- onPipelineActionRequestComplete() {
- eventHub.$emit('refreshPipelinesTable');
- },
trackPipelineMiniGraph() {
this.track('click_minigraph', { label: TRACKING_CATEGORIES.table });
},
@@ -179,7 +176,6 @@ export default {
:stages="item.details.stages"
:update-dropdown="updateGraphDropdown"
:upstream-pipeline="item.triggered_by"
- @pipelineActionRequestComplete="onPipelineActionRequestComplete"
@miniGraphStageClick="trackPipelineMiniGraph"
/>
</template>
diff --git a/app/assets/javascripts/pipelines/components/test_reports/test_reports.vue b/app/assets/javascripts/pipelines/components/test_reports/test_reports.vue
index e5666f7a658..3f2c013d44a 100644
--- a/app/assets/javascripts/pipelines/components/test_reports/test_reports.vue
+++ b/app/assets/javascripts/pipelines/components/test_reports/test_reports.vue
@@ -1,8 +1,6 @@
<script>
import { GlLoadingIcon } from '@gitlab/ui';
import { mapActions, mapGetters, mapState } from 'vuex';
-import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
-import createTestReportsStore from '../../stores/test_reports';
import EmptyState from './empty_state.vue';
import TestSuiteTable from './test_suite_table.vue';
import TestSummary from './test_summary.vue';
@@ -17,7 +15,6 @@ export default {
TestSummary,
TestSummaryTable,
},
- mixins: [glFeatureFlagMixin()],
inject: ['blobPath', 'summaryEndpoint', 'suiteEndpoint'],
computed: {
...mapState('testReports', ['isLoading', 'selectedSuiteIndex', 'testReports']),
@@ -31,17 +28,6 @@ export default {
},
},
created() {
- if (!this.glFeatures.pipelineTabsVue) {
- this.$store.registerModule(
- 'testReports',
- createTestReportsStore({
- blobPath: this.blobPath,
- summaryEndpoint: this.summaryEndpoint,
- suiteEndpoint: this.suiteEndpoint,
- }),
- );
- }
-
this.fetchSummary();
},
methods: {
diff --git a/app/assets/javascripts/pipelines/mixins/pipelines_mixin.js b/app/assets/javascripts/pipelines/mixins/pipelines_mixin.js
index 9602ca1ba88..07551c2342f 100644
--- a/app/assets/javascripts/pipelines/mixins/pipelines_mixin.js
+++ b/app/assets/javascripts/pipelines/mixins/pipelines_mixin.js
@@ -55,7 +55,6 @@ export default {
eventHub.$on('retryPipeline', this.postAction);
eventHub.$on('clickedDropdown', this.updateTable);
eventHub.$on('updateTable', this.updateTable);
- eventHub.$on('refreshPipelinesTable', this.fetchPipelines);
eventHub.$on('runMergeRequestPipeline', this.runMergeRequestPipeline);
},
beforeDestroy() {
@@ -63,7 +62,6 @@ export default {
eventHub.$off('retryPipeline', this.postAction);
eventHub.$off('clickedDropdown', this.updateTable);
eventHub.$off('updateTable', this.updateTable);
- eventHub.$off('refreshPipelinesTable', this.fetchPipelines);
eventHub.$off('runMergeRequestPipeline', this.runMergeRequestPipeline);
},
destroyed() {
diff --git a/app/assets/javascripts/pipelines/pipeline_details_bundle.js b/app/assets/javascripts/pipelines/pipeline_details_bundle.js
index 1bbdd3625be..f00378733fc 100644
--- a/app/assets/javascripts/pipelines/pipeline_details_bundle.js
+++ b/app/assets/javascripts/pipelines/pipeline_details_bundle.js
@@ -1,36 +1,33 @@
import VueRouter from 'vue-router';
import { createAlert } from '~/flash';
-import { __, s__ } from '~/locale';
-import createDagApp from './pipeline_details_dag';
-import { createPipelinesDetailApp } from './pipeline_details_graph';
+import { __ } from '~/locale';
import { createPipelineHeaderApp } from './pipeline_details_header';
-import { createPipelineJobsApp } from './pipeline_details_jobs';
-import { createPipelineFailedJobsApp } from './pipeline_details_failed_jobs';
import { apolloProvider } from './pipeline_shared_client';
-import { createTestDetails } from './pipeline_test_details';
const SELECTORS = {
- PIPELINE_DETAILS: '.js-pipeline-details-vue',
- PIPELINE_GRAPH: '#js-pipeline-graph-vue',
PIPELINE_HEADER: '#js-pipeline-header-vue',
PIPELINE_TABS: '#js-pipeline-tabs',
- PIPELINE_TESTS: '#js-pipeline-tests-detail',
- PIPELINE_JOBS: '#js-pipeline-jobs-vue',
- PIPELINE_FAILED_JOBS: '#js-pipeline-failed-jobs-vue',
};
export default async function initPipelineDetailsBundle() {
- const { dataset } = document.querySelector(SELECTORS.PIPELINE_DETAILS);
+ const { dataset: headerDataset } = document.querySelector(SELECTORS.PIPELINE_HEADER);
try {
- createPipelineHeaderApp(SELECTORS.PIPELINE_HEADER, apolloProvider, dataset.graphqlResourceEtag);
+ createPipelineHeaderApp(
+ SELECTORS.PIPELINE_HEADER,
+ apolloProvider,
+ headerDataset.graphqlResourceEtag,
+ );
} catch {
createAlert({
message: __('An error occurred while loading a section of this page.'),
});
}
- if (gon.features?.pipelineTabsVue) {
+ const tabsEl = document.querySelector(SELECTORS.PIPELINE_TABS);
+
+ if (tabsEl) {
+ const { dataset } = tabsEl;
const { createAppOptions } = await import('ee_else_ce/pipelines/pipeline_tabs');
const { createPipelineTabs } = await import('./pipeline_tabs');
const { routes } = await import('ee_else_ce/pipelines/routes');
@@ -49,45 +46,5 @@ export default async function initPipelineDetailsBundle() {
message: __('An error occurred while loading a section of this page.'),
});
}
- } else {
- try {
- createPipelinesDetailApp(SELECTORS.PIPELINE_GRAPH, apolloProvider, dataset);
- } catch {
- createAlert({
- message: __('An error occurred while loading the pipeline.'),
- });
- }
-
- try {
- createDagApp(apolloProvider);
- } catch {
- createAlert({
- message: __('An error occurred while loading the Needs tab.'),
- });
- }
-
- try {
- createTestDetails(SELECTORS.PIPELINE_TESTS);
- } catch {
- createAlert({
- message: __('An error occurred while loading the Test Reports tab.'),
- });
- }
-
- try {
- createPipelineJobsApp(SELECTORS.PIPELINE_JOBS);
- } catch {
- createAlert({
- message: __('An error occurred while loading the Jobs tab.'),
- });
- }
-
- try {
- createPipelineFailedJobsApp(SELECTORS.PIPELINE_FAILED_JOBS);
- } catch {
- createAlert({
- message: s__('Jobs|An error occurred while loading the Failed Jobs tab.'),
- });
- }
}
}
diff --git a/app/assets/javascripts/pipelines/pipeline_details_dag.js b/app/assets/javascripts/pipelines/pipeline_details_dag.js
deleted file mode 100644
index b2cb0457c4d..00000000000
--- a/app/assets/javascripts/pipelines/pipeline_details_dag.js
+++ /dev/null
@@ -1,42 +0,0 @@
-import Vue from 'vue';
-import VueApollo from 'vue-apollo';
-import Dag from './components/dag/dag.vue';
-
-Vue.use(VueApollo);
-
-const createDagApp = (apolloProvider) => {
- const el = document.querySelector('#js-pipeline-dag-vue');
-
- if (!el) {
- return;
- }
-
- const {
- aboutDagDocPath,
- dagDocPath,
- emptySvgPath,
- pipelineProjectPath,
- pipelineIid,
- } = el.dataset;
-
- // eslint-disable-next-line no-new
- new Vue({
- el,
- components: {
- Dag,
- },
- apolloProvider,
- provide: {
- aboutDagDocPath,
- dagDocPath,
- emptySvgPath,
- pipelineProjectPath,
- pipelineIid,
- },
- render(createElement) {
- return createElement('dag', {});
- },
- });
-};
-
-export default createDagApp;
diff --git a/app/assets/javascripts/pipelines/pipeline_details_failed_jobs.js b/app/assets/javascripts/pipelines/pipeline_details_failed_jobs.js
deleted file mode 100644
index 7bf3b64bf47..00000000000
--- a/app/assets/javascripts/pipelines/pipeline_details_failed_jobs.js
+++ /dev/null
@@ -1,36 +0,0 @@
-import Vue from 'vue';
-import VueApollo from 'vue-apollo';
-import createDefaultClient from '~/lib/graphql';
-import FailedJobsApp from './components/jobs/failed_jobs_app.vue';
-
-Vue.use(VueApollo);
-
-const apolloProvider = new VueApollo({
- defaultClient: createDefaultClient(),
-});
-
-export const createPipelineFailedJobsApp = (selector) => {
- const containerEl = document.querySelector(selector);
-
- if (!containerEl) {
- return false;
- }
-
- const { fullPath, pipelineIid, failedJobsSummaryData } = containerEl.dataset;
-
- return new Vue({
- el: containerEl,
- apolloProvider,
- provide: {
- fullPath,
- pipelineIid,
- },
- render(createElement) {
- return createElement(FailedJobsApp, {
- props: {
- failedJobsSummary: JSON.parse(failedJobsSummaryData),
- },
- });
- },
- });
-};
diff --git a/app/assets/javascripts/pipelines/pipeline_details_graph.js b/app/assets/javascripts/pipelines/pipeline_details_graph.js
deleted file mode 100644
index 9dd5cd7b281..00000000000
--- a/app/assets/javascripts/pipelines/pipeline_details_graph.js
+++ /dev/null
@@ -1,35 +0,0 @@
-import Vue from 'vue';
-import VueApollo from 'vue-apollo';
-import PipelineGraphWrapper from './components/graph/graph_component_wrapper.vue';
-import { reportToSentry } from './utils';
-
-Vue.use(VueApollo);
-
-const createPipelinesDetailApp = (
- selector,
- apolloProvider,
- { pipelineProjectPath, pipelineIid, metricsPath, graphqlResourceEtag } = {},
-) => {
- // eslint-disable-next-line no-new
- new Vue({
- el: selector,
- components: {
- PipelineGraphWrapper,
- },
- apolloProvider,
- provide: {
- metricsPath,
- pipelineProjectPath,
- pipelineIid,
- graphqlResourceEtag,
- },
- errorCaptured(err, _vm, info) {
- reportToSentry('pipeline_details_graph', `error: ${err}, info: ${info}`);
- },
- render(createElement) {
- return createElement(PipelineGraphWrapper);
- },
- });
-};
-
-export { createPipelinesDetailApp };
diff --git a/app/assets/javascripts/pipelines/pipeline_details_jobs.js b/app/assets/javascripts/pipelines/pipeline_details_jobs.js
deleted file mode 100644
index a1294a484f0..00000000000
--- a/app/assets/javascripts/pipelines/pipeline_details_jobs.js
+++ /dev/null
@@ -1,34 +0,0 @@
-import { GlToast } from '@gitlab/ui';
-import Vue from 'vue';
-import VueApollo from 'vue-apollo';
-import createDefaultClient from '~/lib/graphql';
-import JobsApp from './components/jobs/jobs_app.vue';
-
-Vue.use(VueApollo);
-Vue.use(GlToast);
-
-const apolloProvider = new VueApollo({
- defaultClient: createDefaultClient(),
-});
-
-export const createPipelineJobsApp = (selector) => {
- const containerEl = document.querySelector(selector);
-
- if (!containerEl) {
- return false;
- }
-
- const { fullPath, pipelineIid } = containerEl.dataset;
-
- return new Vue({
- el: containerEl,
- apolloProvider,
- provide: {
- fullPath,
- pipelineIid,
- },
- render(createElement) {
- return createElement(JobsApp);
- },
- });
-};
diff --git a/app/assets/javascripts/pipelines/pipeline_test_details.js b/app/assets/javascripts/pipelines/pipeline_test_details.js
deleted file mode 100644
index fe4ca8e9529..00000000000
--- a/app/assets/javascripts/pipelines/pipeline_test_details.js
+++ /dev/null
@@ -1,40 +0,0 @@
-import Vue from 'vue';
-import Vuex from 'vuex';
-import { parseBoolean } from '~/lib/utils/common_utils';
-import Translate from '~/vue_shared/translate';
-import TestReports from './components/test_reports/test_reports.vue';
-
-Vue.use(Vuex);
-Vue.use(Translate);
-
-export const createTestDetails = (selector) => {
- const el = document.querySelector(selector);
- const {
- blobPath,
- emptyStateImagePath,
- hasTestReport,
- summaryEndpoint,
- suiteEndpoint,
- artifactsExpiredImagePath,
- } = el?.dataset || {};
-
- // eslint-disable-next-line no-new
- new Vue({
- el,
- components: {
- TestReports,
- },
- provide: {
- emptyStateImagePath,
- artifactsExpiredImagePath,
- hasTestReport: parseBoolean(hasTestReport),
- blobPath,
- summaryEndpoint,
- suiteEndpoint,
- },
- store: new Vuex.Store(),
- render(createElement) {
- return createElement('test-reports');
- },
- });
-};
diff --git a/app/assets/javascripts/popovers/components/popovers.vue b/app/assets/javascripts/popovers/components/popovers.vue
index a758503b56b..7ec54231e65 100644
--- a/app/assets/javascripts/popovers/components/popovers.vue
+++ b/app/assets/javascripts/popovers/components/popovers.vue
@@ -1,5 +1,6 @@
<script>
-import { GlPopover, GlSafeHtmlDirective } from '@gitlab/ui';
+import { GlPopover } from '@gitlab/ui';
+import SafeHtml from '~/vue_shared/directives/safe_html';
const newPopover = (element) => {
const { content, html, placement, title, triggers = 'focus' } = element.dataset;
@@ -19,7 +20,7 @@ export default {
GlPopover,
},
directives: {
- SafeHtml: GlSafeHtmlDirective,
+ SafeHtml,
},
data() {
return {
diff --git a/app/assets/javascripts/profile/account/components/update_username.vue b/app/assets/javascripts/profile/account/components/update_username.vue
index b038b78088f..51e62984715 100644
--- a/app/assets/javascripts/profile/account/components/update_username.vue
+++ b/app/assets/javascripts/profile/account/components/update_username.vue
@@ -1,6 +1,7 @@
<script>
-import { GlSafeHtmlDirective as SafeHtml, GlButton, GlModal, GlModalDirective } from '@gitlab/ui';
+import { GlButton, GlModal, GlModalDirective } from '@gitlab/ui';
import { escape } from 'lodash';
+import SafeHtml from '~/vue_shared/directives/safe_html';
import { createAlert, VARIANT_INFO } from '~/flash';
import axios from '~/lib/utils/axios_utils';
import { __, s__, sprintf } from '~/locale';
diff --git a/app/assets/javascripts/projects/commit/components/branches_dropdown.vue b/app/assets/javascripts/projects/commit/components/branches_dropdown.vue
index 52da8aaba4d..a037e721677 100644
--- a/app/assets/javascripts/projects/commit/components/branches_dropdown.vue
+++ b/app/assets/javascripts/projects/commit/components/branches_dropdown.vue
@@ -28,6 +28,11 @@ export default {
required: false,
default: '',
},
+ blanked: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
},
i18n: {
noResultsMessage: I18N_NO_RESULTS_MESSAGE,
@@ -36,7 +41,7 @@ export default {
},
data() {
return {
- searchTerm: this.value,
+ searchTerm: this.blanked ? '' : this.value,
};
},
computed: {
diff --git a/app/assets/javascripts/projects/commit/components/form_modal.vue b/app/assets/javascripts/projects/commit/components/form_modal.vue
index d9aaa574fec..1febe8ceaab 100644
--- a/app/assets/javascripts/projects/commit/components/form_modal.vue
+++ b/app/assets/javascripts/projects/commit/components/form_modal.vue
@@ -41,6 +41,11 @@ export default {
required: false,
default: false,
},
+ isRevert: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
primaryActionEventName: {
type: String,
required: false,
@@ -150,7 +155,12 @@ export default {
>
<input id="start_branch" type="hidden" name="start_branch" :value="branch" />
- <branches-dropdown class="gl-w-half" :value="branch" @selectBranch="setBranch" />
+ <branches-dropdown
+ class="gl-w-half"
+ :value="branch"
+ :blanked="isRevert"
+ @selectBranch="setBranch"
+ />
</gl-form-group>
<gl-form-checkbox
diff --git a/app/assets/javascripts/projects/commit/init_revert_commit_modal.js b/app/assets/javascripts/projects/commit/init_revert_commit_modal.js
index 849b2f4858c..41be71932e5 100644
--- a/app/assets/javascripts/projects/commit/init_revert_commit_modal.js
+++ b/app/assets/javascripts/projects/commit/init_revert_commit_modal.js
@@ -49,6 +49,7 @@ export default function initInviteMembersModal(primaryActionEventName) {
i18n: { ...I18N_REVERT_MODAL, ...I18N_MODAL },
openModal: OPEN_REVERT_MODAL,
modalId: REVERT_MODAL_ID,
+ isRevert: true,
primaryActionEventName,
},
}),
diff --git a/app/assets/javascripts/projects/commits/index.js b/app/assets/javascripts/projects/commits/index.js
index 03b94fde0f3..53169f689c9 100644
--- a/app/assets/javascripts/projects/commits/index.js
+++ b/app/assets/javascripts/projects/commits/index.js
@@ -1,11 +1,13 @@
import Vue from 'vue';
import Vuex from 'vuex';
+import { visitUrl } from '~/lib/utils/url_utility';
+import RefSelector from '~/ref/components/ref_selector.vue';
import AuthorSelectApp from './components/author_select.vue';
import store from './store';
Vue.use(Vuex);
-export default (el) => {
+export const mountCommits = (el) => {
if (!el) {
return null;
}
@@ -24,3 +26,30 @@ export default (el) => {
},
});
};
+
+export const initCommitsRefSwitcher = () => {
+ const el = document.getElementById('js-project-commits-ref-switcher');
+ const COMMITS_PATH_REGEX = /^(.*?)\/-\/commits/g;
+
+ if (!el) return false;
+
+ const { projectId, ref, commitsPath } = el.dataset;
+ const commitsPathPrefix = commitsPath.match(COMMITS_PATH_REGEX)?.[0];
+
+ return new Vue({
+ el,
+ render(createElement) {
+ return createElement(RefSelector, {
+ props: {
+ projectId,
+ value: ref,
+ },
+ on: {
+ input(selected) {
+ visitUrl(`${commitsPathPrefix}/${selected}`);
+ },
+ },
+ });
+ },
+ });
+};
diff --git a/app/assets/javascripts/projects/compare/components/repo_dropdown.vue b/app/assets/javascripts/projects/compare/components/repo_dropdown.vue
index ba1e00a2b36..c00e75db722 100644
--- a/app/assets/javascripts/projects/compare/components/repo_dropdown.vue
+++ b/app/assets/javascripts/projects/compare/components/repo_dropdown.vue
@@ -57,7 +57,7 @@ export default {
<gl-dropdown
:text="selectedProject.name"
:header-text="s__(`CompareRevisions|Select target project`)"
- class="gl-w-full gl-font-monospace gl-sm-pr-3"
+ class="gl-w-full gl-font-monospace"
toggle-class="gl-min-w-0"
:disabled="disableRepoDropdown"
>
diff --git a/app/assets/javascripts/projects/compare/components/revision_card.vue b/app/assets/javascripts/projects/compare/components/revision_card.vue
index d6ada24604d..162aca44f9d 100644
--- a/app/assets/javascripts/projects/compare/components/revision_card.vue
+++ b/app/assets/javascripts/projects/compare/components/revision_card.vue
@@ -43,7 +43,7 @@ export default {
<h2 class="gl-font-size-h2">
{{ s__(`CompareRevisions|${revisionText}`) }}
</h2>
- <div class="gl-sm-display-flex gl-align-items-center">
+ <div class="gl-sm-display-flex gl-align-items-center gl-gap-3">
<repo-dropdown
class="gl-sm-w-half"
:params-name="paramsName"
diff --git a/app/assets/javascripts/projects/default_project_templates.js b/app/assets/javascripts/projects/default_project_templates.js
index 3671b24b502..a44855c14d5 100644
--- a/app/assets/javascripts/projects/default_project_templates.js
+++ b/app/assets/javascripts/projects/default_project_templates.js
@@ -113,4 +113,12 @@ export default {
text: s__('ProjectTemplates|Jsonnet for Dynamic Child Pipelines'),
icon: '.template-option .icon-gitlab_logo',
},
+ bridgetown: {
+ text: s__('ProjectTemplates|Pages/Bridgetown'),
+ icon: '.template-option .icon-gitlab_logo',
+ },
+ typo3_distribution: {
+ text: s__('ProjectTemplates|TYPO3 Distribution'),
+ icon: '.template-option .icon-typo3',
+ },
};
diff --git a/app/assets/javascripts/projects/new/components/app.vue b/app/assets/javascripts/projects/new/components/app.vue
index 59ca393fe92..3100029eb31 100644
--- a/app/assets/javascripts/projects/new/components/app.vue
+++ b/app/assets/javascripts/projects/new/components/app.vue
@@ -3,7 +3,7 @@ import createFromTemplateIllustration from '@gitlab/svgs/dist/illustrations/proj
import blankProjectIllustration from '@gitlab/svgs/dist/illustrations/project-create-new-sm.svg';
import importProjectIllustration from '@gitlab/svgs/dist/illustrations/project-import-sm.svg';
import ciCdProjectIllustration from '@gitlab/svgs/dist/illustrations/project-run-CICD-pipelines-sm.svg';
-import { GlSafeHtmlDirective as SafeHtml } from '@gitlab/ui';
+import SafeHtml from '~/vue_shared/directives/safe_html';
import { s__ } from '~/locale';
import NewNamespacePage from '~/vue_shared/new_namespace/new_namespace_page.vue';
import NewProjectPushTipPopover from './new_project_push_tip_popover.vue';
diff --git a/app/assets/javascripts/projects/new/components/new_project_url_select.vue b/app/assets/javascripts/projects/new/components/new_project_url_select.vue
index eccfb3d844c..d6d88b5b297 100644
--- a/app/assets/javascripts/projects/new/components/new_project_url_select.vue
+++ b/app/assets/javascripts/projects/new/components/new_project_url_select.vue
@@ -46,7 +46,15 @@ export default {
debounce: DEBOUNCE_DELAY,
},
},
- inject: ['namespaceFullPath', 'namespaceId', 'rootUrl', 'trackLabel', 'userNamespaceId'],
+ inject: [
+ 'namespaceFullPath',
+ 'namespaceId',
+ 'rootUrl',
+ 'trackLabel',
+ 'userNamespaceId',
+ 'inputName',
+ 'inputId',
+ ],
data() {
return {
currentUser: {},
@@ -124,6 +132,11 @@ export default {
}
: this.$options.emptyNameSpace;
},
+ trackDropdownShow() {
+ if (this.trackLabel) {
+ this.track('activate_form_input', { label: this.trackLabel, property: 'project_path' });
+ }
+ },
},
emptyNameSpace: {
id: undefined,
@@ -145,7 +158,7 @@ export default {
class="js-group-namespace-dropdown gl-flex-grow-1"
:toggle-class="`gl-rounded-top-right-base! gl-rounded-bottom-right-base! gl-w-20 ${dropdownPlaceholderClass}`"
data-qa-selector="select_namespace_dropdown"
- @show="track('activate_form_input', { label: trackLabel, property: 'project_path' })"
+ @show="trackDropdownShow"
@shown="handleDropdownShown"
>
<template #button-text>
@@ -173,7 +186,7 @@ export default {
{{ group.fullPath }}
</gl-dropdown-item>
</template>
- <template v-if="hasNamespaceMatches">
+ <template v-if="hasNamespaceMatches && userNamespaceId">
<gl-dropdown-section-header>{{ __('Users') }}</gl-dropdown-section-header>
<gl-dropdown-item @click="handleDropdownItemClick(userNamespace)">
{{ userNamespace.fullPath }}
@@ -186,9 +199,9 @@ export default {
<input type="hidden" name="project[selected_namespace_id]" :value="selectedNamespace.id" />
<input
- id="project_namespace_id"
+ :id="inputId"
type="hidden"
- name="project[namespace_id]"
+ :name="inputName"
:value="selectedNamespace.id || userNamespaceId"
/>
</gl-button-group>
diff --git a/app/assets/javascripts/projects/new/constants.js b/app/assets/javascripts/projects/new/constants.js
index e52a84dc07e..7b6b2cfc7ca 100644
--- a/app/assets/javascripts/projects/new/constants.js
+++ b/app/assets/javascripts/projects/new/constants.js
@@ -12,6 +12,8 @@ export const DEPLOYMENT_TARGET_SELECTIONS = [
s__('DeploymentTarget|Registry (package or container)'),
s__('DeploymentTarget|Infrastructure provider (Terraform, Cloudformation, and so on)'),
s__('DeploymentTarget|Serverless backend (Lambda, Cloud functions)'),
+ s__('DeploymentTarget|Edge Computing (e.g. Cloudflare Workers)'),
+ s__('DeploymentTarget|Web Deployment Platform (Netlify, Vercel, Gatsby)'),
s__('DeploymentTarget|GitLab Pages'),
s__('DeploymentTarget|Other hosting service'),
s__('DeploymentTarget|No deployment planned'),
diff --git a/app/assets/javascripts/projects/new/index.js b/app/assets/javascripts/projects/new/index.js
index a72172a4f5e..910244c657b 100644
--- a/app/assets/javascripts/projects/new/index.js
+++ b/app/assets/javascripts/projects/new/index.js
@@ -59,6 +59,8 @@ export function initNewProjectUrlSelect() {
rootUrl: el.dataset.rootUrl,
trackLabel: el.dataset.trackLabel,
userNamespaceId: el.dataset.userNamespaceId,
+ inputId: el.dataset.inputId,
+ inputName: el.dataset.inputName,
},
render: (createElement) => createElement(NewProjectUrlSelect),
}),
diff --git a/app/assets/javascripts/projects/project_name_rules.js b/app/assets/javascripts/projects/project_name_rules.js
new file mode 100644
index 00000000000..eeef1fb5afc
--- /dev/null
+++ b/app/assets/javascripts/projects/project_name_rules.js
@@ -0,0 +1,28 @@
+import { __ } from '~/locale';
+
+const rulesReg = [
+ {
+ reg: /^[a-zA-Z0-9\u{00A9}-\u{1f9ff}_]/u,
+ msg: __("Name must start with a letter, digit, emoji, or '_'"),
+ },
+ {
+ reg: /^[a-zA-Z0-9\p{Pd}\u{002B}\u{00A9}-\u{1f9ff}_. ]+$/u,
+ msg: __("Name can contain only letters, digits, emojis, '_', '.', '+', dashes, or spaces"),
+ },
+];
+
+/**
+ *
+ * @param {string} text
+ * @returns {string} msg
+ */
+function checkRules(text) {
+ for (const item of rulesReg) {
+ if (!item.reg.test(text)) {
+ return item.msg;
+ }
+ }
+ return '';
+}
+
+export { checkRules };
diff --git a/app/assets/javascripts/projects/project_new.js b/app/assets/javascripts/projects/project_new.js
index 424ea3b61c5..d71e80dffcf 100644
--- a/app/assets/javascripts/projects/project_new.js
+++ b/app/assets/javascripts/projects/project_new.js
@@ -12,6 +12,7 @@ import {
slugify,
convertUnicodeToAscii,
} from '../lib/utils/text_utility';
+import { checkRules } from './project_name_rules';
let hasUserDefinedProjectPath = false;
let hasUserDefinedProjectName = false;
@@ -87,10 +88,23 @@ const validateGroupNamespaceDropdown = (e) => {
}
};
+const checkProjectName = (projectNameInput) => {
+ const msg = checkRules(projectNameInput.value);
+ const projectNameError = document.querySelector('#project_name_error');
+ if (!projectNameError) return;
+ if (msg) {
+ projectNameError.innerText = msg;
+ projectNameError.classList.remove('hidden');
+ } else {
+ projectNameError.classList.add('hidden');
+ }
+};
+
const setProjectNamePathHandlers = ($projectNameInput, $projectPathInput) => {
const specialRepo = document.querySelector('.js-user-readme-repo');
const projectNameInputListener = () => {
onProjectNameChange($projectNameInput, $projectPathInput);
+ checkProjectName($projectNameInput);
hasUserDefinedProjectName = $projectNameInput.value.trim().length > 0;
hasUserDefinedProjectPath = $projectPathInput.value.trim().length > 0;
};
diff --git a/app/assets/javascripts/projects/settings/access_dropdown.js b/app/assets/javascripts/projects/settings/access_dropdown.js
index 335545c802a..dcf7415a444 100644
--- a/app/assets/javascripts/projects/settings/access_dropdown.js
+++ b/app/assets/javascripts/projects/settings/access_dropdown.js
@@ -580,7 +580,7 @@ export default class AccessDropdown {
return `
<li>
<a href="#" class="${isActiveClass} item-${role.type}" data-role-id="${role.id}">
- ${role.text}
+ ${escape(role.text)}
</a>
</li>
`;
diff --git a/app/assets/javascripts/projects/settings/branch_rules/components/view/constants.js b/app/assets/javascripts/projects/settings/branch_rules/components/view/constants.js
index 6da058ebc9c..61c37a2348a 100644
--- a/app/assets/javascripts/projects/settings/branch_rules/components/view/constants.js
+++ b/app/assets/javascripts/projects/settings/branch_rules/components/view/constants.js
@@ -6,6 +6,7 @@ export const I18N = {
branchNameOrPattern: s__('BranchRules|Branch name or pattern'),
branch: s__('BranchRules|Target Branch'),
allBranches: s__('BranchRules|All branches'),
+ matchingBranchesLinkTitle: s__('BranchRules|%{total} matching %{subject}'),
protectBranchTitle: s__('BranchRules|Protect branch'),
protectBranchDescription: s__(
'BranchRules|Keep stable branches secure and force developers to use merge requests. %{linkStart}What are protected branches?%{linkEnd}',
diff --git a/app/assets/javascripts/projects/settings/branch_rules/components/view/index.vue b/app/assets/javascripts/projects/settings/branch_rules/components/view/index.vue
index eb11e17dd1b..626ed67c466 100644
--- a/app/assets/javascripts/projects/settings/branch_rules/components/view/index.vue
+++ b/app/assets/javascripts/projects/settings/branch_rules/components/view/index.vue
@@ -1,9 +1,10 @@
<script>
import { GlSprintf, GlLink, GlLoadingIcon } from '@gitlab/ui';
-import { sprintf } from '~/locale';
-import { getParameterByName } from '~/lib/utils/url_utility';
+import { sprintf, n__ } from '~/locale';
+import { getParameterByName, mergeUrlParams } from '~/lib/utils/url_utility';
import { helpPagePath } from '~/helpers/help_page_helper';
import branchRulesQuery from '../../queries/branch_rules_details.query.graphql';
+import { getAccessLevels } from '../../../utils';
import Protection from './protection.vue';
import {
I18N,
@@ -41,6 +42,9 @@ export default {
statusChecksPath: {
default: '',
},
+ branchesPath: {
+ default: '',
+ },
},
apollo: {
project: {
@@ -55,6 +59,7 @@ export default {
this.branchProtection = branchRule?.branchProtection;
this.approvalRules = branchRule?.approvalRules;
this.statusChecks = branchRule?.externalStatusChecks?.nodes || [];
+ this.matchingBranchesCount = branchRule?.matchingBranchesCount;
},
},
},
@@ -64,6 +69,7 @@ export default {
branchProtection: {},
approvalRules: {},
statusChecks: [],
+ matchingBranchesCount: null,
};
},
computed: {
@@ -115,28 +121,20 @@ export default {
? this.$options.i18n.targetBranch
: this.$options.i18n.branchNameOrPattern;
},
+ matchingBranchesLinkHref() {
+ return mergeUrlParams({ state: 'all', search: this.branch }, this.branchesPath);
+ },
+ matchingBranchesLinkTitle() {
+ const total = this.matchingBranchesCount;
+ const subject = n__('branch', 'branches', total);
+ return sprintf(this.$options.i18n.matchingBranchesLinkTitle, { total, subject });
+ },
approvals() {
return this.approvalRules?.nodes || [];
},
},
methods: {
- getAccessLevels(accessLevels = {}) {
- const total = accessLevels.edges?.length;
- const accessLevelTypes = { total, users: [], groups: [], roles: [] };
-
- accessLevels.edges?.forEach(({ node }) => {
- if (node.user) {
- const src = node.user.avatarUrl;
- accessLevelTypes.users.push({ src, ...node.user });
- } else if (node.group) {
- accessLevelTypes.groups.push(node);
- } else {
- accessLevelTypes.roles.push(node);
- }
- });
-
- return accessLevelTypes;
- },
+ getAccessLevels,
},
};
</script>
@@ -161,6 +159,10 @@ export default {
</div>
<code v-else class="gl-mt-2" data-testid="branch">{{ branch }}</code>
+ <p v-if="matchingBranchesCount" class="gl-mt-3">
+ <gl-link :href="matchingBranchesLinkHref">{{ matchingBranchesLinkTitle }}</gl-link>
+ </p>
+
<h4 class="gl-mb-1 gl-mt-5">{{ $options.i18n.protectBranchTitle }}</h4>
<gl-sprintf :message="$options.i18n.protectBranchDescription">
<template #link="{ content }">
diff --git a/app/assets/javascripts/projects/settings/branch_rules/mount_branch_rules.js b/app/assets/javascripts/projects/settings/branch_rules/mount_branch_rules.js
index 89cfb1e1c8e..7639acc1181 100644
--- a/app/assets/javascripts/projects/settings/branch_rules/mount_branch_rules.js
+++ b/app/assets/javascripts/projects/settings/branch_rules/mount_branch_rules.js
@@ -14,7 +14,13 @@ export default function mountBranchRules(el) {
defaultClient: createDefaultClient(),
});
- const { projectPath, protectedBranchesPath, approvalRulesPath, statusChecksPath } = el.dataset;
+ const {
+ projectPath,
+ protectedBranchesPath,
+ approvalRulesPath,
+ statusChecksPath,
+ branchesPath,
+ } = el.dataset;
return new Vue({
el,
@@ -24,6 +30,7 @@ export default function mountBranchRules(el) {
protectedBranchesPath,
approvalRulesPath,
statusChecksPath,
+ branchesPath,
},
render(h) {
return h(View);
diff --git a/app/assets/javascripts/projects/settings/branch_rules/queries/branch_rules_details.query.graphql b/app/assets/javascripts/projects/settings/branch_rules/queries/branch_rules_details.query.graphql
index aa1e4923aa8..a832e59aa67 100644
--- a/app/assets/javascripts/projects/settings/branch_rules/queries/branch_rules_details.query.graphql
+++ b/app/assets/javascripts/projects/settings/branch_rules/queries/branch_rules_details.query.graphql
@@ -68,6 +68,7 @@ query getBranchRulesDetails($projectPath: ID!) {
externalUrl
}
}
+ matchingBranchesCount
}
}
}
diff --git a/app/assets/javascripts/projects/settings/repository/branch_rules/app.vue b/app/assets/javascripts/projects/settings/repository/branch_rules/app.vue
index a9eb2a53fbf..9b669024a8b 100644
--- a/app/assets/javascripts/projects/settings/repository/branch_rules/app.vue
+++ b/app/assets/javascripts/projects/settings/repository/branch_rules/app.vue
@@ -1,7 +1,7 @@
<script>
import { s__ } from '~/locale';
import { createAlert } from '~/flash';
-import branchRulesQuery from './graphql/queries/branch_rules.query.graphql';
+import branchRulesQuery from 'ee_else_ce/projects/settings/repository/branch_rules/graphql/queries/branch_rules.query.graphql';
import BranchRule from './components/branch_rule.vue';
export const i18n = {
@@ -51,13 +51,14 @@ export default {
<template>
<div class="settings-content">
<branch-rule
- v-for="rule in branchRules"
- :key="rule.name"
+ v-for="(rule, index) in branchRules"
+ :key="`${rule.name}-${index}`"
:name="rule.name"
:is-default="rule.isDefault"
:branch-protection="rule.branchProtection"
- :status-checks-total="rule.externalStatusChecks.nodes.length"
- :approval-rules-total="rule.approvalRules.nodes.length"
+ :status-checks-total="rule.externalStatusChecks ? rule.externalStatusChecks.nodes.length : 0"
+ :approval-rules-total="rule.approvalRules ? rule.approvalRules.nodes.length : 0"
+ :matching-branches-count="rule.matchingBranchesCount"
/>
<span v-if="!branchRules.length" data-testid="empty">{{ $options.i18n.emptyState }}</span>
diff --git a/app/assets/javascripts/projects/settings/repository/branch_rules/components/branch_rule.vue b/app/assets/javascripts/projects/settings/repository/branch_rules/components/branch_rule.vue
index 78c824c66d1..41947834bdb 100644
--- a/app/assets/javascripts/projects/settings/repository/branch_rules/components/branch_rule.vue
+++ b/app/assets/javascripts/projects/settings/repository/branch_rules/components/branch_rule.vue
@@ -1,6 +1,7 @@
<script>
import { GlBadge, GlButton } from '@gitlab/ui';
import { s__, sprintf, n__ } from '~/locale';
+import { getAccessLevels } from '../../../utils';
export const i18n = {
defaultLabel: s__('BranchRules|default'),
@@ -9,6 +10,9 @@ export const i18n = {
codeOwnerApprovalRequired: s__('BranchRules|Requires CODEOWNERS approval'),
statusChecks: s__('BranchRules|%{total} status %{subject}'),
approvalRules: s__('BranchRules|%{total} approval %{subject}'),
+ matchingBranches: s__('BranchRules|%{total} matching %{subject}'),
+ pushAccessLevels: s__('BranchRules|Allowed to merge'),
+ mergeAccessLevels: s__('BranchRules|Allowed to push'),
};
export default {
@@ -48,8 +52,16 @@ export default {
required: false,
default: 0,
},
+ matchingBranchesCount: {
+ type: Number,
+ required: false,
+ default: 0,
+ },
},
computed: {
+ isWildcard() {
+ return this.name.includes('*');
+ },
hasApprovalDetails() {
return this.approvalDetails.length;
},
@@ -68,8 +80,31 @@ export default {
subject: n__('rule', 'rules', this.approvalRulesTotal),
});
},
+ matchingBranchesText() {
+ return sprintf(this.$options.i18n.matchingBranches, {
+ total: this.matchingBranchesCount,
+ subject: n__('branch', 'branches', this.matchingBranchesCount),
+ });
+ },
+ mergeAccessLevels() {
+ const { mergeAccessLevels } = this.branchProtection || {};
+ return this.getAccessLevels(mergeAccessLevels);
+ },
+ pushAccessLevels() {
+ const { pushAccessLevels } = this.branchProtection || {};
+ return this.getAccessLevels(pushAccessLevels);
+ },
+ pushAccessLevelsText() {
+ return this.getAccessLevelsText(this.$options.i18n.pushAccessLevels, this.pushAccessLevels);
+ },
+ mergeAccessLevelsText() {
+ return this.getAccessLevelsText(this.$options.i18n.mergeAccessLevels, this.mergeAccessLevels);
+ },
approvalDetails() {
const approvalDetails = [];
+ if (this.isWildcard) {
+ approvalDetails.push(this.matchingBranchesText);
+ }
if (this.branchProtection.allowForcePush) {
approvalDetails.push(this.$options.i18n.allowForcePush);
}
@@ -82,9 +117,31 @@ export default {
if (this.approvalRulesTotal) {
approvalDetails.push(this.approvalRulesText);
}
+ if (this.mergeAccessLevels.total > 0) {
+ approvalDetails.push(this.mergeAccessLevelsText);
+ }
+ if (this.pushAccessLevels.total > 0) {
+ approvalDetails.push(this.pushAccessLevelsText);
+ }
return approvalDetails;
},
},
+ methods: {
+ getAccessLevels,
+ getAccessLevelsText(beginString = '', accessLevels) {
+ const textParts = [];
+ if (accessLevels.roles.length) {
+ textParts.push(n__('1 role', '%d roles', accessLevels.roles.length));
+ }
+ if (accessLevels.groups.length) {
+ textParts.push(n__('1 group', '%d groups', accessLevels.groups.length));
+ }
+ if (accessLevels.users.length) {
+ textParts.push(n__('1 user', '%d users', accessLevels.users.length));
+ }
+ return `${beginString}: ${textParts.join(', ')}`;
+ },
+ },
};
</script>
diff --git a/app/assets/javascripts/projects/settings/repository/branch_rules/graphql/queries/branch_rules.query.graphql b/app/assets/javascripts/projects/settings/repository/branch_rules/graphql/queries/branch_rules.query.graphql
index 49e089e7805..a8cdda5505f 100644
--- a/app/assets/javascripts/projects/settings/repository/branch_rules/graphql/queries/branch_rules.query.graphql
+++ b/app/assets/javascripts/projects/settings/repository/branch_rules/graphql/queries/branch_rules.query.graphql
@@ -5,18 +5,24 @@ query getBranchRules($projectPath: ID!) {
nodes {
name
isDefault
+ matchingBranchesCount
branchProtection {
allowForcePush
- codeOwnerApprovalRequired
- }
- externalStatusChecks {
- nodes {
- id
+ mergeAccessLevels {
+ edges {
+ node {
+ accessLevel
+ accessLevelDescription
+ }
+ }
}
- }
- approvalRules {
- nodes {
- id
+ pushAccessLevels {
+ edges {
+ node {
+ accessLevel
+ accessLevelDescription
+ }
+ }
}
}
}
diff --git a/app/assets/javascripts/projects/settings/utils.js b/app/assets/javascripts/projects/settings/utils.js
new file mode 100644
index 00000000000..7bcfde39178
--- /dev/null
+++ b/app/assets/javascripts/projects/settings/utils.js
@@ -0,0 +1,17 @@
+export const getAccessLevels = (accessLevels = {}) => {
+ const total = accessLevels.edges?.length;
+ const accessLevelTypes = { total, users: [], groups: [], roles: [] };
+
+ accessLevels.edges?.forEach(({ node }) => {
+ if (node.user) {
+ const src = node.user.avatarUrl;
+ accessLevelTypes.users.push({ src, ...node.user });
+ } else if (node.group) {
+ accessLevelTypes.groups.push(node);
+ } else {
+ accessLevelTypes.roles.push(node);
+ }
+ });
+
+ return accessLevelTypes;
+};
diff --git a/app/assets/javascripts/projects/settings_service_desk/components/service_desk_root.vue b/app/assets/javascripts/projects/settings_service_desk/components/service_desk_root.vue
index 71ff3e892b1..b79b3fa4573 100644
--- a/app/assets/javascripts/projects/settings_service_desk/components/service_desk_root.vue
+++ b/app/assets/javascripts/projects/settings_service_desk/components/service_desk_root.vue
@@ -1,5 +1,6 @@
<script>
-import { GlAlert, GlSprintf, GlLink, GlSafeHtmlDirective } from '@gitlab/ui';
+import { GlAlert, GlSprintf, GlLink } from '@gitlab/ui';
+import SafeHtml from '~/vue_shared/directives/safe_html';
import axios from '~/lib/utils/axios_utils';
import { helpPagePath } from '~/helpers/help_page_helper';
import { __, sprintf } from '~/locale';
@@ -16,7 +17,7 @@ export default {
ServiceDeskSetting,
},
directives: {
- SafeHtml: GlSafeHtmlDirective,
+ SafeHtml,
},
inject: {
initialIsEnabled: {
diff --git a/app/assets/javascripts/protected_tags/protected_tag_create.js b/app/assets/javascripts/protected_tags/protected_tag_create.js
index e3f427b8408..75fd11cd074 100644
--- a/app/assets/javascripts/protected_tags/protected_tag_create.js
+++ b/app/assets/javascripts/protected_tags/protected_tag_create.js
@@ -42,7 +42,7 @@ export default class ProtectedTagCreate {
const $allowedToCreateInput = this.$form.find('#create_access_levels_attributes');
this.$form
- .find('input[type="submit"]')
+ .find('button[type="submit"]')
.prop('disabled', !($tagInput.val() && $allowedToCreateInput.length));
}
diff --git a/app/assets/javascripts/releases/components/app_index.vue b/app/assets/javascripts/releases/components/app_index.vue
index 6dc8240e680..1b360b79b0c 100644
--- a/app/assets/javascripts/releases/components/app_index.vue
+++ b/app/assets/javascripts/releases/components/app_index.vue
@@ -263,6 +263,7 @@ export default {
v-for="(release, index) in releases"
:key="getReleaseKey(release, index)"
:release="release"
+ :sort="sort"
:class="{ 'linked-card': releases.length > 1 && index !== releases.length - 1 }"
/>
diff --git a/app/assets/javascripts/releases/components/release_block.vue b/app/assets/javascripts/releases/components/release_block.vue
index b2bd405574f..49c349e7a7b 100644
--- a/app/assets/javascripts/releases/components/release_block.vue
+++ b/app/assets/javascripts/releases/components/release_block.vue
@@ -1,12 +1,12 @@
<script>
-import { GlSafeHtmlDirective as SafeHtml } from '@gitlab/ui';
-import $ from 'jquery';
import { isEmpty } from 'lodash';
+import SafeHtml from '~/vue_shared/directives/safe_html';
import { scrollToElement } from '~/lib/utils/common_utils';
import { slugify } from '~/lib/utils/text_utility';
import { getLocationHash } from '~/lib/utils/url_utility';
+import { CREATED_ASC } from '~/releases/constants';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
-import '~/behaviors/markdown/render_gfm';
+import { renderGFM } from '~/behaviors/markdown/render_gfm';
import EvidenceBlock from './evidence_block.vue';
import ReleaseBlockAssets from './release_block_assets.vue';
import ReleaseBlockFooter from './release_block_footer.vue';
@@ -32,6 +32,11 @@ export default {
required: true,
default: () => ({}),
},
+ sort: {
+ type: String,
+ required: false,
+ default: CREATED_ASC,
+ },
},
data() {
return {
@@ -80,7 +85,7 @@ export default {
},
methods: {
renderGFM() {
- $(this.$refs['gfm-content']).renderGFM();
+ renderGFM(this.$refs['gfm-content']);
},
},
safeHtmlConfig: { ADD_TAGS: ['gl-emoji'] },
@@ -119,6 +124,8 @@ export default {
:tag-path="release.tagPath"
:author="release.author"
:released-at="release.releasedAt"
+ :created-at="release.createdAt"
+ :sort="sort"
/>
</div>
</template>
diff --git a/app/assets/javascripts/releases/components/release_block_footer.vue b/app/assets/javascripts/releases/components/release_block_footer.vue
index 3881c83b5c2..85fb7d02a37 100644
--- a/app/assets/javascripts/releases/components/release_block_footer.vue
+++ b/app/assets/javascripts/releases/components/release_block_footer.vue
@@ -3,6 +3,7 @@ import { GlTooltipDirective, GlLink, GlIcon } from '@gitlab/ui';
import { __, sprintf } from '~/locale';
import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue';
import timeagoMixin from '~/vue_shared/mixins/timeago';
+import { RELEASED_AT_ASC, RELEASED_AT_DESC } from '~/releases/constants';
export default {
name: 'ReleaseBlockFooter',
@@ -46,10 +47,26 @@ export default {
required: false,
default: null,
},
+ createdAt: {
+ type: Date,
+ required: false,
+ default: null,
+ },
+ sort: {
+ type: String,
+ required: false,
+ default: '',
+ },
},
computed: {
- releasedAtTimeAgo() {
- return this.timeFormatted(this.releasedAt);
+ isSortedByReleaseDate() {
+ return this.sort === RELEASED_AT_ASC || this.sort === RELEASED_AT_DESC;
+ },
+ timeAt() {
+ return this.isSortedByReleaseDate ? this.releasedAt : this.createdAt;
+ },
+ atTimeAgo() {
+ return this.timeFormatted(this.timeAt);
},
userImageAltDescription() {
return this.author && this.author.username
@@ -58,7 +75,10 @@ export default {
},
createdTime() {
const now = new Date();
- const isFuture = now < new Date(this.releasedAt);
+ const isFuture = now < new Date(this.timeAt);
+ if (this.isSortedByReleaseDate) {
+ return isFuture ? __('Will be released') : __('Released');
+ }
return isFuture ? __('Will be created') : __('Created');
},
},
@@ -93,17 +113,17 @@ export default {
</div>
<div
- v-if="releasedAt || author"
+ v-if="timeAt || author"
class="gl-float-left gl-display-flex gl-align-items-center js-author-date-info"
>
<span class="gl-text-secondary">{{ createdTime }}&nbsp;</span>
- <template v-if="releasedAt">
+ <template v-if="timeAt">
<span
v-gl-tooltip.bottom
- :title="tooltipTitle(releasedAt)"
+ :title="tooltipTitle(timeAt)"
class="gl-text-secondary gl-flex-shrink-0"
>
- {{ releasedAtTimeAgo }}&nbsp;
+ {{ atTimeAgo }}&nbsp;
</span>
</template>
diff --git a/app/assets/javascripts/releases/graphql/fragments/release_for_editing.fragment.graphql b/app/assets/javascripts/releases/graphql/fragments/release_for_editing.fragment.graphql
index 3ad66afa259..177dff1823e 100644
--- a/app/assets/javascripts/releases/graphql/fragments/release_for_editing.fragment.graphql
+++ b/app/assets/javascripts/releases/graphql/fragments/release_for_editing.fragment.graphql
@@ -4,6 +4,7 @@ fragment ReleaseForEditing on Release {
tagName
description
releasedAt
+ createdAt
tagPath
assets {
links {
diff --git a/app/assets/javascripts/releases/util.js b/app/assets/javascripts/releases/util.js
index a1027ef08d7..10d7887c0b1 100644
--- a/app/assets/javascripts/releases/util.js
+++ b/app/assets/javascripts/releases/util.js
@@ -15,7 +15,8 @@ const convertScalarProperties = (graphQLRelease) =>
'historicalRelease',
]);
-const convertDateProperties = ({ releasedAt }) => ({
+const convertDateProperties = ({ createdAt, releasedAt }) => ({
+ createdAt: new Date(createdAt),
releasedAt: new Date(releasedAt),
});
diff --git a/app/assets/javascripts/repository/components/last_commit.vue b/app/assets/javascripts/repository/components/last_commit.vue
index 05d64077866..4d3c1521559 100644
--- a/app/assets/javascripts/repository/components/last_commit.vue
+++ b/app/assets/javascripts/repository/components/last_commit.vue
@@ -1,12 +1,6 @@
<script>
-import {
- GlTooltipDirective,
- GlLink,
- GlButton,
- GlButtonGroup,
- GlLoadingIcon,
- GlSafeHtmlDirective,
-} from '@gitlab/ui';
+import { GlTooltipDirective, GlLink, GlButton, GlButtonGroup, GlLoadingIcon } from '@gitlab/ui';
+import SafeHtml from '~/vue_shared/directives/safe_html';
import defaultAvatarUrl from 'images/no_avatar.png';
import pathLastCommitQuery from 'shared_queries/repository/path_last_commit.query.graphql';
import { sprintf, s__ } from '~/locale';
@@ -32,7 +26,7 @@ export default {
},
directives: {
GlTooltip: GlTooltipDirective,
- SafeHtml: GlSafeHtmlDirective,
+ SafeHtml,
},
mixins: [getRefMixin],
apollo: {
diff --git a/app/assets/javascripts/repository/components/preview/index.vue b/app/assets/javascripts/repository/components/preview/index.vue
index 4935b8029f9..8feac6b8e35 100644
--- a/app/assets/javascripts/repository/components/preview/index.vue
+++ b/app/assets/javascripts/repository/components/preview/index.vue
@@ -1,8 +1,8 @@
<script>
-import { GlIcon, GlLink, GlLoadingIcon, GlSafeHtmlDirective as SafeHtml } from '@gitlab/ui';
-import $ from 'jquery';
-import '~/behaviors/markdown/render_gfm';
+import { GlIcon, GlLink, GlLoadingIcon } from '@gitlab/ui';
+import SafeHtml from '~/vue_shared/directives/safe_html';
import { handleLocationHash } from '~/lib/utils/common_utils';
+import { renderGFM } from '~/behaviors/markdown/render_gfm';
import readmeQuery from '../../queries/readme.query.graphql';
export default {
@@ -42,7 +42,7 @@ export default {
if (newVal) {
this.$nextTick(() => {
handleLocationHash();
- $(this.$refs.readme).renderGFM();
+ renderGFM(this.$refs.readme);
});
}
},
diff --git a/app/assets/javascripts/repository/components/table/index.vue b/app/assets/javascripts/repository/components/table/index.vue
index 99eb167172b..46d546c2ee4 100644
--- a/app/assets/javascripts/repository/components/table/index.vue
+++ b/app/assets/javascripts/repository/components/table/index.vue
@@ -1,6 +1,5 @@
<script>
import { GlSkeletonLoader, GlButton } from '@gitlab/ui';
-import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { sprintf, __ } from '~/locale';
import { joinPaths } from '~/lib/utils/url_utility';
import getRefMixin from '../../mixins/get_ref';
@@ -17,7 +16,7 @@ export default {
ParentRow,
GlButton,
},
- mixins: [getRefMixin, glFeatureFlagMixin()],
+ mixins: [getRefMixin],
apollo: {
projectPath: {
query: projectPathQuery,
@@ -93,9 +92,6 @@ export default {
},
generateRowNumber(path, id, index) {
const key = `${path}-${id}-${index}`;
- if (!this.glFeatures.lazyLoadCommits) {
- return 0;
- }
if (!this.rowNumbers[key] && this.rowNumbers[key] !== 0) {
this.$options.totalRowsLoaded += 1;
@@ -105,10 +101,6 @@ export default {
return this.rowNumbers[key];
},
getCommit(fileName) {
- if (!this.glFeatures.lazyLoadCommits) {
- return {};
- }
-
return this.commits.find(
(commitEntry) => commitEntry.filePath === joinPaths(this.path, fileName),
);
diff --git a/app/assets/javascripts/repository/components/table/row.vue b/app/assets/javascripts/repository/components/table/row.vue
index f3c5ace75fc..27ac11f3c58 100644
--- a/app/assets/javascripts/repository/components/table/row.vue
+++ b/app/assets/javascripts/repository/components/table/row.vue
@@ -7,10 +7,10 @@ import {
GlLoadingIcon,
GlIcon,
GlHoverLoadDirective,
- GlSafeHtmlDirective,
GlIntersectionObserver,
} from '@gitlab/ui';
import { escapeRegExp } from 'lodash';
+import SafeHtml from '~/vue_shared/directives/safe_html';
import paginatedTreeQuery from 'shared_queries/repository/paginated_tree.query.graphql';
import { escapeFileUrl } from '~/lib/utils/url_utility';
import { TREE_PAGE_SIZE, ROW_APPEAR_DELAY } from '~/repository/constants';
@@ -19,7 +19,6 @@ import TimeagoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import blobInfoQuery from 'shared_queries/repository/blob_info.query.graphql';
import getRefMixin from '../../mixins/get_ref';
-import commitQuery from '../../queries/commit.query.graphql';
export default {
components: {
@@ -35,23 +34,7 @@ export default {
directives: {
GlTooltip: GlTooltipDirective,
GlHoverLoad: GlHoverLoadDirective,
- SafeHtml: GlSafeHtmlDirective,
- },
- apollo: {
- commit: {
- query: commitQuery,
- variables() {
- return {
- fileName: this.name,
- path: this.currentPath,
- projectPath: this.projectPath,
- maxOffset: this.totalEntries,
- };
- },
- skip() {
- return this.glFeatures.lazyLoadCommits;
- },
- },
+ SafeHtml,
},
mixins: [getRefMixin, glFeatureFlagMixin()],
props: {
@@ -125,14 +108,13 @@ export default {
},
data() {
return {
- commit: null,
hasRowAppeared: false,
delayedRowAppear: null,
};
},
computed: {
commitData() {
- return this.glFeatures.lazyLoadCommits ? this.commitInfo : this.commit;
+ return this.commitInfo;
},
routerLinkTo() {
const blobRouteConfig = { path: `/-/blob/${this.escapedRef}/${escapeFileUrl(this.path)}` };
@@ -200,12 +182,10 @@ export default {
return;
}
- if (this.glFeatures.lazyLoadCommits) {
- this.delayedRowAppear = setTimeout(
- () => this.$emit('row-appear', this.rowNumber),
- ROW_APPEAR_DELAY,
- );
- }
+ this.delayedRowAppear = setTimeout(
+ () => this.$emit('row-appear', this.rowNumber),
+ ROW_APPEAR_DELAY,
+ );
},
rowDisappeared() {
clearTimeout(this.delayedRowAppear);
diff --git a/app/assets/javascripts/repository/components/tree_content.vue b/app/assets/javascripts/repository/components/tree_content.vue
index 8a45a351c35..4a8f83458f4 100644
--- a/app/assets/javascripts/repository/components/tree_content.vue
+++ b/app/assets/javascripts/repository/components/tree_content.vue
@@ -157,7 +157,7 @@ export default {
.find(({ hasNextPage }) => hasNextPage);
},
handleRowAppear(rowNumber) {
- if (!this.glFeatures.lazyLoadCommits || isRequested(rowNumber)) {
+ if (isRequested(rowNumber)) {
return;
}
diff --git a/app/assets/javascripts/repository/constants.js b/app/assets/javascripts/repository/constants.js
index 3a6d7d2f779..e194bddcc56 100644
--- a/app/assets/javascripts/repository/constants.js
+++ b/app/assets/javascripts/repository/constants.js
@@ -99,5 +99,4 @@ export const LEGACY_FILE_TYPES = [
'requirements_txt',
'cargo_toml',
'go_mod',
- 'go_sum',
];
diff --git a/app/assets/javascripts/repository/index.js b/app/assets/javascripts/repository/index.js
index 1d295e18332..e9214e3acff 100644
--- a/app/assets/javascripts/repository/index.js
+++ b/app/assets/javascripts/repository/index.js
@@ -2,11 +2,12 @@ import { GlButton } from '@gitlab/ui';
import Vue from 'vue';
import Vuex from 'vuex';
import { parseBoolean } from '~/lib/utils/common_utils';
-import { escapeFileUrl } from '~/lib/utils/url_utility';
+import { escapeFileUrl, visitUrl } from '~/lib/utils/url_utility';
import { __ } from '~/locale';
import initWebIdeLink from '~/pages/projects/shared/web_ide_link';
import PerformancePlugin from '~/performance/vue_performance_plugin';
import createStore from '~/code_navigation/store';
+import RefSelector from '~/ref/components/ref_selector.vue';
import App from './components/app.vue';
import Breadcrumbs from './components/breadcrumbs.vue';
import DirectoryDownloadLinks from './components/directory_download_links.vue';
@@ -20,6 +21,7 @@ import refsQuery from './queries/ref.query.graphql';
import createRouter from './router';
import { updateFormAction } from './utils/dom';
import { setTitle } from './utils/title';
+import { generateRefDestinationPath } from './utils/ref_switcher_utils';
Vue.use(Vuex);
Vue.use(PerformancePlugin, {
@@ -89,9 +91,34 @@ export default function setupVueRepositoryList() {
},
});
- initLastCommitApp();
+ const initRefSwitcher = () => {
+ const refSwitcherEl = document.getElementById('js-tree-ref-switcher');
+
+ if (!refSwitcherEl) return false;
+
+ const { projectId, projectRootPath } = refSwitcherEl.dataset;
+
+ return new Vue({
+ el: refSwitcherEl,
+ render(createElement) {
+ return createElement(RefSelector, {
+ props: {
+ projectId,
+ value: ref,
+ },
+ on: {
+ input(selectedRef) {
+ visitUrl(generateRefDestinationPath(projectRootPath, selectedRef));
+ },
+ },
+ });
+ },
+ });
+ };
+ initLastCommitApp();
initBlobControlsApp();
+ initRefSwitcher();
router.afterEach(({ params: { path } }) => {
setTitle(path, ref, fullName);
diff --git a/app/assets/javascripts/repository/queries/commit.query.graphql b/app/assets/javascripts/repository/queries/commit.query.graphql
deleted file mode 100644
index 1a01462bd19..00000000000
--- a/app/assets/javascripts/repository/queries/commit.query.graphql
+++ /dev/null
@@ -1,7 +0,0 @@
-#import "ee_else_ce/repository/queries/commit.fragment.graphql"
-
-query getCommit($fileName: String!, $path: String!, $maxOffset: Number!) {
- commit(path: $path, fileName: $fileName, maxOffset: $maxOffset) @client {
- ...TreeEntryCommit
- }
-}
diff --git a/app/assets/javascripts/repository/utils/ref_switcher_utils.js b/app/assets/javascripts/repository/utils/ref_switcher_utils.js
new file mode 100644
index 00000000000..8ff52104c93
--- /dev/null
+++ b/app/assets/javascripts/repository/utils/ref_switcher_utils.js
@@ -0,0 +1,30 @@
+import { joinPaths } from '~/lib/utils/url_utility';
+
+/**
+ * Matches the namespace and target directory/blob in a path
+ * Example: /root/Flight/-/blob/fix/main/test/spec/utils_spec.js
+ * Group 1: /-/blob
+ * Group 2: blob
+ * Group 3: main/test/spec/utils_spec.js
+ */
+const NAMESPACE_TARGET_REGEX = /(\/-\/(blob|tree))\/.*?\/(.*)/;
+
+/**
+ * Generates a ref destination path based on the selected ref and current path.
+ * A user could either be in the project root, a directory on the blob view.
+ * @param {string} projectRootPath - The root path for a project.
+ * @param {string} selectedRef - The selected ref from the ref dropdown.
+ */
+export function generateRefDestinationPath(projectRootPath, selectedRef) {
+ const currentPath = window.location.pathname;
+ let namespace = '/-/tree';
+ let target;
+ const match = NAMESPACE_TARGET_REGEX.exec(currentPath);
+ if (match) {
+ [, namespace, , target] = match;
+ }
+
+ const destinationPath = joinPaths(projectRootPath, namespace, selectedRef, target);
+
+ return `${destinationPath}${window.location.hash}`;
+}
diff --git a/app/assets/javascripts/search/sidebar/components/confidentiality_filter.vue b/app/assets/javascripts/search/sidebar/components/confidentiality_filter.vue
index 38dccb9675d..4ddf695f61a 100644
--- a/app/assets/javascripts/search/sidebar/components/confidentiality_filter.vue
+++ b/app/assets/javascripts/search/sidebar/components/confidentiality_filter.vue
@@ -1,5 +1,5 @@
<script>
-import { mapState } from 'vuex';
+import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { confidentialFilterData } from '../constants/confidential_filter_data';
import RadioFilter from './radio_filter.vue';
@@ -8,10 +8,10 @@ export default {
components: {
RadioFilter,
},
+ mixins: [glFeatureFlagsMixin()],
computed: {
- ...mapState(['query']),
- showDropdown() {
- return Object.values(confidentialFilterData.scopes).includes(this.query.scope);
+ ffBasedXPadding() {
+ return this.glFeatures.searchPageVerticalNav ? 'gl-px-5' : 'gl-px-0';
},
},
confidentialFilterData,
@@ -19,8 +19,8 @@ export default {
</script>
<template>
- <div v-if="showDropdown">
- <radio-filter :filter-data="$options.confidentialFilterData" />
+ <div>
+ <radio-filter :class="ffBasedXPadding" :filter-data="$options.confidentialFilterData" />
<hr class="gl-my-5 gl-border-gray-100" />
</div>
</template>
diff --git a/app/assets/javascripts/search/sidebar/components/results_filters.vue b/app/assets/javascripts/search/sidebar/components/results_filters.vue
index 5b53f94bb53..9b993ab9a86 100644
--- a/app/assets/javascripts/search/sidebar/components/results_filters.vue
+++ b/app/assets/javascripts/search/sidebar/components/results_filters.vue
@@ -2,6 +2,8 @@
import { GlButton, GlLink } from '@gitlab/ui';
import { mapActions, mapState } from 'vuex';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
+import { confidentialFilterData } from '../constants/confidential_filter_data';
+import { stateFilterData } from '../constants/state_filter_data';
import ConfidentialityFilter from './confidentiality_filter.vue';
import StatusFilter from './status_filter.vue';
@@ -22,6 +24,15 @@ export default {
searchPageVerticalNavFeatureFlag() {
return this.glFeatures.searchPageVerticalNav;
},
+ showConfidentialityFilter() {
+ return Object.values(confidentialFilterData.scopes).includes(this.urlQuery.scope);
+ },
+ showStatusFilter() {
+ return Object.values(stateFilterData.scopes).includes(this.urlQuery.scope);
+ },
+ ffBasedXPadding() {
+ return this.glFeatures.searchPageVerticalNav ? 'gl-px-5' : 'gl-px-0';
+ },
},
methods: {
...mapActions(['applyQuery', 'resetQuery']),
@@ -30,14 +41,14 @@ export default {
</script>
<template>
- <form
- :class="searchPageVerticalNavFeatureFlag ? 'gl-px-5' : 'gl-px-0'"
- @submit.prevent="applyQuery"
- >
- <hr v-if="searchPageVerticalNavFeatureFlag" class="gl-my-5 gl-border-gray-100" />
- <status-filter />
- <confidentiality-filter />
- <div class="gl-display-flex gl-align-items-center gl-mt-4">
+ <form class="gl-pt-5 gl-md-pt-0" @submit.prevent="applyQuery">
+ <hr
+ v-if="searchPageVerticalNavFeatureFlag"
+ class="gl-my-5 gl-border-gray-100 gl-display-none gl-md-display-block"
+ />
+ <status-filter v-if="showStatusFilter" />
+ <confidentiality-filter v-if="showConfidentialityFilter" />
+ <div class="gl-display-flex gl-align-items-center gl-mt-4" :class="ffBasedXPadding">
<gl-button category="primary" variant="confirm" type="submit" :disabled="!sidebarDirty">
{{ __('Apply') }}
</gl-button>
diff --git a/app/assets/javascripts/search/sidebar/components/scope_navigation.vue b/app/assets/javascripts/search/sidebar/components/scope_navigation.vue
index f5e1525090e..7a03306e2f9 100644
--- a/app/assets/javascripts/search/sidebar/components/scope_navigation.vue
+++ b/app/assets/javascripts/search/sidebar/components/scope_navigation.vue
@@ -1,15 +1,23 @@
<script>
-import { GlNav, GlNavItem } from '@gitlab/ui';
+import { GlNav, GlNavItem, GlIcon } from '@gitlab/ui';
import { mapActions, mapState } from 'vuex';
-import { formatNumber } from '~/locale';
+import { formatNumber, s__ } from '~/locale';
import Tracking from '~/tracking';
-import { NAV_LINK_DEFAULT_CLASSES, NUMBER_FORMATING_OPTIONS } from '../constants';
+import {
+ NAV_LINK_DEFAULT_CLASSES,
+ NUMBER_FORMATING_OPTIONS,
+ NAV_LINK_COUNT_DEFAULT_CLASSES,
+} from '../constants';
export default {
name: 'ScopeNavigation',
+ i18n: {
+ countOverLimitLabel: s__('GlobalSearch|Result count is over limit.'),
+ },
components: {
GlNav,
GlNavItem,
+ GlIcon,
},
mixins: [Tracking.mixin()],
computed: {
@@ -20,9 +28,6 @@ export default {
},
methods: {
...mapActions(['fetchSidebarCount']),
- activeClasses(currentScope) {
- return currentScope === this.urlQuery.scope ? 'gl-font-weight-bold' : '';
- },
showFormatedCount(count) {
if (!count) {
return '0';
@@ -30,17 +35,27 @@ export default {
const countNumber = parseInt(count.replace(/,/g, ''), 10);
return formatNumber(countNumber, NUMBER_FORMATING_OPTIONS);
},
+ isCountOverLimit(count) {
+ return count.includes('+');
+ },
handleClick(scope) {
this.track('click_menu_item', { label: `vertical_navigation_${scope}` });
},
- linkClasses(scope) {
+ linkClasses(isHighlighted) {
+ return [...this.$options.NAV_LINK_DEFAULT_CLASSES, { 'gl-font-weight-bold': isHighlighted }];
+ },
+ countClasses(isHighlighted) {
return [
- { 'gl-font-weight-bold': scope === this.urlQuery.scope },
- ...this.$options.NAV_LINK_DEFAULT_CLASSES,
+ ...this.$options.NAV_LINK_COUNT_DEFAULT_CLASSES,
+ isHighlighted ? 'gl-text-gray-900' : 'gl-text-gray-500',
];
},
+ isActive(scope, index) {
+ return this.urlQuery.scope ? this.urlQuery.scope === scope : index === 0;
+ },
},
NAV_LINK_DEFAULT_CLASSES,
+ NAV_LINK_COUNT_DEFAULT_CLASSES,
};
</script>
@@ -50,14 +65,20 @@ export default {
<gl-nav-item
v-for="(item, scope, index) in navigation"
:key="scope"
- :link-classes="linkClasses(scope)"
+ :link-classes="linkClasses(isActive(scope, index))"
class="gl-mb-1"
:href="item.link"
- :active="urlQuery.scope ? urlQuery.scope === scope : index === 0"
+ :active="isActive(scope, index)"
@click="handleClick(scope)"
><span>{{ item.label }}</span
- ><span v-if="item.count" class="gl-font-sm gl-font-weight-normal">
- {{ showFormatedCount(item.count) }}
+ ><span v-if="item.count" :class="countClasses(isActive(scope, index))">
+ {{ showFormatedCount(item.count)
+ }}<gl-icon
+ v-if="isCountOverLimit(item.count)"
+ name="plus"
+ :aria-label="$options.i18n.countOverLimitLabel"
+ :size="8"
+ />
</span>
</gl-nav-item>
</gl-nav>
diff --git a/app/assets/javascripts/search/sidebar/components/status_filter.vue b/app/assets/javascripts/search/sidebar/components/status_filter.vue
index 5cec2090906..eaf7d95822a 100644
--- a/app/assets/javascripts/search/sidebar/components/status_filter.vue
+++ b/app/assets/javascripts/search/sidebar/components/status_filter.vue
@@ -1,5 +1,5 @@
<script>
-import { mapState } from 'vuex';
+import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { stateFilterData } from '../constants/state_filter_data';
import RadioFilter from './radio_filter.vue';
@@ -8,10 +8,10 @@ export default {
components: {
RadioFilter,
},
+ mixins: [glFeatureFlagsMixin()],
computed: {
- ...mapState(['query']),
- showDropdown() {
- return Object.values(stateFilterData.scopes).includes(this.query.scope);
+ ffBasedXPadding() {
+ return this.glFeatures.searchPageVerticalNav ? 'gl-px-5' : 'gl-px-0';
},
},
stateFilterData,
@@ -19,8 +19,8 @@ export default {
</script>
<template>
- <div v-if="showDropdown">
- <radio-filter :filter-data="$options.stateFilterData" />
+ <div>
+ <radio-filter :class="ffBasedXPadding" :filter-data="$options.stateFilterData" />
<hr class="gl-my-5 gl-border-gray-100" />
</div>
</template>
diff --git a/app/assets/javascripts/search/sidebar/constants/index.js b/app/assets/javascripts/search/sidebar/constants/index.js
index 3621138afe4..a9c031f91a4 100644
--- a/app/assets/javascripts/search/sidebar/constants/index.js
+++ b/app/assets/javascripts/search/sidebar/constants/index.js
@@ -9,3 +9,5 @@ export const NAV_LINK_DEFAULT_CLASSES = [
'gl-justify-content-space-between',
'gl-text-gray-900',
];
+
+export const NAV_LINK_COUNT_DEFAULT_CLASSES = ['gl-font-sm', 'gl-font-weight-normal'];
diff --git a/app/assets/javascripts/search/topbar/components/app.vue b/app/assets/javascripts/search/topbar/components/app.vue
index d0fcbb0d83b..0629bea3239 100644
--- a/app/assets/javascripts/search/topbar/components/app.vue
+++ b/app/assets/javascripts/search/topbar/components/app.vue
@@ -1,9 +1,11 @@
<script>
-import { GlSearchBoxByClick } from '@gitlab/ui';
+import { GlSearchBoxByClick, GlButton } from '@gitlab/ui';
import { mapState, mapActions } from 'vuex';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { s__ } from '~/locale';
import { parseBoolean } from '~/lib/utils/common_utils';
+import MarkdownDrawer from '~/vue_shared/components/markdown_drawer/markdown_drawer.vue';
+import { SYNTAX_OPTIONS_DOCUMENT } from '../constants';
import GroupFilter from './group_filter.vue';
import ProjectFilter from './project_filter.vue';
@@ -12,24 +14,45 @@ export default {
i18n: {
searchPlaceholder: s__(`GlobalSearch|Search for projects, issues, etc.`),
searchLabel: s__(`GlobalSearch|What are you searching for?`),
+ documentFetchErrorMessage: s__(
+ 'GlobalSearch|There was an error fetching the "Syntax Options" document.',
+ ),
+ searchFieldLabel: s__('GlobalSearch|What are you searching for?'),
+ syntaxOptionsLabel: s__('GlobalSearch|Syntax options'),
+ groupFieldLabel: s__('GlobalSearch|Group'),
+ projectFieldLabel: s__('GlobalSearch|Project'),
+ searchButtonLabel: s__('GlobalSearch|Search'),
+ closeButtonLabel: s__('GlobalSearch|Close'),
},
components: {
+ GlButton,
GlSearchBoxByClick,
GroupFilter,
ProjectFilter,
+ MarkdownDrawer,
},
mixins: [glFeatureFlagsMixin()],
props: {
- groupInitialData: {
+ groupInitialJson: {
type: Object,
required: false,
default: () => ({}),
},
- projectInitialData: {
+ projectInitialJson: {
type: Object,
required: false,
default: () => ({}),
},
+ elasticsearchEnabled: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ defaultBranchName: {
+ type: String,
+ required: false,
+ default: '',
+ },
},
computed: {
...mapState(['query']),
@@ -44,16 +67,26 @@ export default {
showFilters() {
return !parseBoolean(this.query.snippets);
},
+ showSyntaxOptions() {
+ return this.elasticsearchEnabled && this.isDefaultBranch;
+ },
hasVerticalNav() {
return this.glFeatures.searchPageVerticalNav;
},
+ isDefaultBranch() {
+ return !this.query.repository_ref || this.query.repository_ref === this.defaultBranchName;
+ },
},
created() {
this.preloadStoredFrequentItems();
},
methods: {
...mapActions(['applyQuery', 'setQuery', 'preloadStoredFrequentItems']),
+ onToggleDrawer() {
+ this.$refs.markdownDrawer.toggleDrawer();
+ },
},
+ SYNTAX_OPTIONS_DOCUMENT,
};
</script>
@@ -61,7 +94,25 @@ export default {
<section class="search-page-form gl-lg-display-flex gl-flex-direction-column">
<div class="gl-lg-display-flex gl-flex-direction-row gl-align-items-flex-end">
<div class="gl-flex-grow-1 gl-mb-4 gl-lg-mb-0 gl-lg-mr-2">
- <label>{{ $options.i18n.searchLabel }}</label>
+ <div
+ class="gl-sm-display-flex gl-flex-direction-row gl-justify-content-space-between gl-mb-4 gl-md-mb-0"
+ >
+ <label>{{ $options.i18n.searchLabel }}</label>
+ <template v-if="showSyntaxOptions">
+ <gl-button
+ category="tertiary"
+ variant="link"
+ size="small"
+ button-text-classes="gl-font-sm!"
+ @click="onToggleDrawer"
+ >{{ $options.i18n.syntaxOptionsLabel }}
+ </gl-button>
+ <markdown-drawer
+ ref="markdownDrawer"
+ :document-path="$options.SYNTAX_OPTIONS_DOCUMENT"
+ />
+ </template>
+ </div>
<gl-search-box-by-click
id="dashboard_search"
v-model="search"
@@ -70,13 +121,13 @@ export default {
@submit="applyQuery"
/>
</div>
- <div v-if="showFilters" class="gl-mb-4 gl-lg-mb-0 gl-lg-mx-2">
- <label class="gl-display-block">{{ __('Group') }}</label>
- <group-filter :initial-data="groupInitialData" />
+ <div v-if="showFilters" class="gl-mb-4 gl-lg-mb-0 gl-lg-mx-3">
+ <label class="gl-display-block">{{ $options.i18n.groupFieldLabel }}</label>
+ <group-filter :initial-data="groupInitialJson" />
</div>
- <div v-if="showFilters" class="gl-mb-4 gl-lg-mb-0 gl-lg-mx-2">
- <label class="gl-display-block">{{ __('Project') }}</label>
- <project-filter :initial-data="projectInitialData" />
+ <div v-if="showFilters" class="gl-mb-4 gl-lg-mb-0 gl-lg-ml-3">
+ <label class="gl-display-block">{{ $options.i18n.projectFieldLabel }}</label>
+ <project-filter :initial-data="projectInitialJson" />
</div>
</div>
<hr v-if="hasVerticalNav" class="gl-mt-5 gl-mb-0 gl-border-gray-100" />
diff --git a/app/assets/javascripts/search/topbar/components/searchable_dropdown_item.vue b/app/assets/javascripts/search/topbar/components/searchable_dropdown_item.vue
index 70156142365..c1e33df3c42 100644
--- a/app/assets/javascripts/search/topbar/components/searchable_dropdown_item.vue
+++ b/app/assets/javascripts/search/topbar/components/searchable_dropdown_item.vue
@@ -1,5 +1,6 @@
<script>
-import { GlDropdownItem, GlAvatar, GlSafeHtmlDirective as SafeHtml } from '@gitlab/ui';
+import { GlDropdownItem, GlAvatar } from '@gitlab/ui';
+import SafeHtml from '~/vue_shared/directives/safe_html';
import highlight from '~/lib/utils/highlight';
import { truncateNamespace } from '~/lib/utils/text_utility';
import { AVATAR_SHAPE_OPTION_RECT } from '~/vue_shared/constants';
diff --git a/app/assets/javascripts/search/topbar/constants.js b/app/assets/javascripts/search/topbar/constants.js
index dc040fdef34..121c15199dd 100644
--- a/app/assets/javascripts/search/topbar/constants.js
+++ b/app/assets/javascripts/search/topbar/constants.js
@@ -19,3 +19,5 @@ export const PROJECT_DATA = {
name: 'name',
fullName: 'name_with_namespace',
};
+
+export const SYNTAX_OPTIONS_DOCUMENT = 'drawers/user/search/advanced_search.md';
diff --git a/app/assets/javascripts/search/topbar/index.js b/app/assets/javascripts/search/topbar/index.js
index 87316e10e8d..d6e16085c28 100644
--- a/app/assets/javascripts/search/topbar/index.js
+++ b/app/assets/javascripts/search/topbar/index.js
@@ -11,10 +11,18 @@ export const initTopbar = (store) => {
return false;
}
- let { groupInitialData, projectInitialData } = el.dataset;
+ const {
+ groupInitialJson,
+ projectInitialJson,
+ elasticsearchEnabled,
+ defaultBranchName,
+ } = el.dataset;
- groupInitialData = JSON.parse(groupInitialData);
- projectInitialData = JSON.parse(projectInitialData);
+ const groupInitialJsonParsed = JSON.parse(groupInitialJson);
+ const projectInitialJsonParsed = JSON.parse(projectInitialJson);
+ const elasticsearchEnabledParsed = elasticsearchEnabled
+ ? JSON.parse(elasticsearchEnabled)
+ : false;
return new Vue({
el,
@@ -22,8 +30,10 @@ export const initTopbar = (store) => {
render(createElement) {
return createElement(GlobalSearchTopbar, {
props: {
- groupInitialData,
- projectInitialData,
+ groupInitialJson: groupInitialJsonParsed,
+ projectInitialJson: projectInitialJsonParsed,
+ elasticsearchEnabled: elasticsearchEnabledParsed,
+ defaultBranchName,
},
});
},
diff --git a/app/assets/javascripts/security_configuration/components/training_provider_list.vue b/app/assets/javascripts/security_configuration/components/training_provider_list.vue
index 0bcb2bb6720..6dae8e50908 100644
--- a/app/assets/javascripts/security_configuration/components/training_provider_list.vue
+++ b/app/assets/javascripts/security_configuration/components/training_provider_list.vue
@@ -8,9 +8,9 @@ import {
GlLink,
GlSkeletonLoader,
GlIcon,
- GlSafeHtmlDirective,
} from '@gitlab/ui';
import * as Sentry from '@sentry/browser';
+import SafeHtml from '~/vue_shared/directives/safe_html';
import Tracking from '~/tracking';
import { __, s__ } from '~/locale';
import {
@@ -54,7 +54,7 @@ export default {
},
directives: {
GlTooltip: GlTooltipDirective,
- SafeHtml: GlSafeHtmlDirective,
+ SafeHtml,
},
mixins: [Tracking.mixin()],
inject: ['projectFullPath'],
diff --git a/app/assets/javascripts/self_monitor/components/self_monitor_form.vue b/app/assets/javascripts/self_monitor/components/self_monitor_form.vue
index ffba3aac681..d9e969e2278 100644
--- a/app/assets/javascripts/self_monitor/components/self_monitor_form.vue
+++ b/app/assets/javascripts/self_monitor/components/self_monitor_form.vue
@@ -1,15 +1,8 @@
<script>
-import {
- GlFormGroup,
- GlButton,
- GlModal,
- GlToast,
- GlToggle,
- GlLink,
- GlSafeHtmlDirective,
-} from '@gitlab/ui';
+import { GlFormGroup, GlButton, GlModal, GlToast, GlToggle, GlLink } from '@gitlab/ui';
import Vue from 'vue';
import { mapState, mapActions } from 'vuex';
+import SafeHtml from '~/vue_shared/directives/safe_html';
import { helpPagePath } from '~/helpers/help_page_helper';
import { BV_SHOW_MODAL, BV_HIDE_MODAL } from '~/lib/utils/constants';
import { visitUrl, getBaseURL } from '~/lib/utils/url_utility';
@@ -26,7 +19,7 @@ export default {
GlLink,
},
directives: {
- SafeHtml: GlSafeHtmlDirective,
+ SafeHtml,
},
formLabels: {
createProject: __('Self-monitoring'),
diff --git a/app/assets/javascripts/self_monitor/store/actions.js b/app/assets/javascripts/self_monitor/store/actions.js
index 5b9e994290c..7198dbe8b04 100644
--- a/app/assets/javascripts/self_monitor/store/actions.js
+++ b/app/assets/javascripts/self_monitor/store/actions.js
@@ -1,6 +1,6 @@
import axios from '~/lib/utils/axios_utils';
import { backOff } from '~/lib/utils/common_utils';
-import statusCodes from '~/lib/utils/http_status';
+import statusCodes, { HTTP_STATUS_ACCEPTED } from '~/lib/utils/http_status';
import { __, s__ } from '~/locale';
import * as types from './mutation_types';
@@ -10,7 +10,7 @@ function backOffRequest(makeRequestCallback) {
return backOff((next, stop) => {
makeRequestCallback()
.then((resp) => {
- if (resp.status === statusCodes.ACCEPTED) {
+ if (resp.status === HTTP_STATUS_ACCEPTED) {
next();
} else {
stop(resp);
@@ -31,7 +31,7 @@ export const requestCreateProject = ({ dispatch, state, commit }) => {
axios
.post(state.createProjectEndpoint)
.then((resp) => {
- if (resp.status === statusCodes.ACCEPTED) {
+ if (resp.status === HTTP_STATUS_ACCEPTED) {
dispatch('requestCreateProjectStatus', resp.data.job_id);
}
})
@@ -83,7 +83,7 @@ export const requestDeleteProject = ({ dispatch, state, commit }) => {
axios
.delete(state.deleteProjectEndpoint)
.then((resp) => {
- if (resp.status === statusCodes.ACCEPTED) {
+ if (resp.status === HTTP_STATUS_ACCEPTED) {
dispatch('requestDeleteProjectStatus', resp.data.job_id);
}
})
diff --git a/app/assets/javascripts/sentry/constants.js b/app/assets/javascripts/sentry/constants.js
index fd96da5faf6..5531c4f56db 100644
--- a/app/assets/javascripts/sentry/constants.js
+++ b/app/assets/javascripts/sentry/constants.js
@@ -1,5 +1,6 @@
import { __ } from '~/locale';
+// TODO: Remove in favor of https://gitlab.com/gitlab-org/gitlab/issues/35144
export const IGNORE_ERRORS = [
// Random plugins/extensions
'top.GLOBALS',
diff --git a/app/assets/javascripts/sentry/index.js b/app/assets/javascripts/sentry/index.js
index 176745b4177..5539a061726 100644
--- a/app/assets/javascripts/sentry/index.js
+++ b/app/assets/javascripts/sentry/index.js
@@ -1,26 +1,34 @@
import '../webpack';
+import * as Sentry from 'sentrybrowser7';
import SentryConfig from './sentry_config';
const index = function index() {
+ // Configuration for newer versions of Sentry SDK (v7)
SentryConfig.init({
dsn: gon.sentry_dsn,
+ environment: gon.sentry_environment,
currentUserId: gon.current_user_id,
- whitelistUrls:
+ allowUrls:
process.env.NODE_ENV === 'production'
? [gon.gitlab_url]
: [gon.gitlab_url, 'webpack-internal://'],
- environment: gon.sentry_environment,
release: gon.revision,
tags: {
revision: gon.revision,
feature_category: gon.feature_category,
},
});
-
- return SentryConfig;
};
index();
+// The _Sentry object is globally exported so it can be used by
+// ./sentry_browser_wrapper.js
+// This hack allows us to load a single version of `@sentry/browser`
+// in the browser, see app/views/layouts/_head.html.haml to find how it is imported.
+
+// eslint-disable-next-line no-underscore-dangle
+window._Sentry = Sentry;
+
export default index;
diff --git a/app/assets/javascripts/sentry/legacy_index.js b/app/assets/javascripts/sentry/legacy_index.js
new file mode 100644
index 00000000000..604b982e128
--- /dev/null
+++ b/app/assets/javascripts/sentry/legacy_index.js
@@ -0,0 +1,34 @@
+import '../webpack';
+
+import * as Sentry5 from 'sentrybrowser5';
+import LegacySentryConfig from './legacy_sentry_config';
+
+const index = function index() {
+ // Configuration for legacy versions of Sentry SDK (v5)
+ LegacySentryConfig.init({
+ dsn: gon.sentry_dsn,
+ currentUserId: gon.current_user_id,
+ whitelistUrls:
+ process.env.NODE_ENV === 'production'
+ ? [gon.gitlab_url]
+ : [gon.gitlab_url, 'webpack-internal://'],
+ environment: gon.sentry_environment,
+ release: gon.revision,
+ tags: {
+ revision: gon.revision,
+ feature_category: gon.feature_category,
+ },
+ });
+};
+
+index();
+
+// The _Sentry object is globally exported so it can be used by
+// ./sentry_browser_wrapper.js
+// This hack allows us to load a single version of `@sentry/browser`
+// in the browser, see app/views/layouts/_head.html.haml to find how it is imported.
+
+// eslint-disable-next-line no-underscore-dangle
+window._Sentry = Sentry5;
+
+export default index;
diff --git a/app/assets/javascripts/sentry/legacy_sentry_config.js b/app/assets/javascripts/sentry/legacy_sentry_config.js
new file mode 100644
index 00000000000..50a943886db
--- /dev/null
+++ b/app/assets/javascripts/sentry/legacy_sentry_config.js
@@ -0,0 +1,64 @@
+import * as Sentry5 from 'sentrybrowser5';
+import $ from 'jquery';
+import { __ } from '~/locale';
+import { IGNORE_ERRORS, DENY_URLS, SAMPLE_RATE } from './constants';
+
+const SentryConfig = {
+ IGNORE_ERRORS,
+ BLACKLIST_URLS: DENY_URLS,
+ SAMPLE_RATE,
+ init(options = {}) {
+ this.options = options;
+
+ this.configure();
+ this.bindSentryErrors();
+ if (this.options.currentUserId) this.setUser();
+ },
+
+ configure() {
+ const { dsn, release, tags, whitelistUrls, environment } = this.options;
+
+ Sentry5.init({
+ dsn,
+ release,
+ whitelistUrls,
+ environment,
+ ignoreErrors: this.IGNORE_ERRORS, // TODO: Remove in favor of https://gitlab.com/gitlab-org/gitlab/issues/35144
+ blacklistUrls: this.BLACKLIST_URLS,
+ sampleRate: SAMPLE_RATE,
+ });
+
+ Sentry5.setTags(tags);
+ },
+
+ setUser() {
+ Sentry5.setUser({
+ id: this.options.currentUserId,
+ });
+ },
+
+ bindSentryErrors() {
+ $(document).on('ajaxError.sentry', this.handleSentryErrors);
+ },
+
+ handleSentryErrors(event, req, config, err) {
+ const error = err || req.statusText;
+ const { responseText = __('Unknown response text') } = req;
+ const { type, url, data } = config;
+ const { status } = req;
+
+ Sentry5.captureMessage(error, {
+ extra: {
+ type,
+ url,
+ data,
+ status,
+ response: responseText,
+ error,
+ event,
+ },
+ });
+ },
+};
+
+export default SentryConfig;
diff --git a/app/assets/javascripts/sentry/sentry_browser_wrapper.js b/app/assets/javascripts/sentry/sentry_browser_wrapper.js
new file mode 100644
index 00000000000..0382827f82c
--- /dev/null
+++ b/app/assets/javascripts/sentry/sentry_browser_wrapper.js
@@ -0,0 +1,27 @@
+// The _Sentry object is globally exported so it can be used here
+// This hack allows us to load a single version of `@sentry/browser`
+// in the browser (or none). See app/views/layouts/_head.html.haml
+// to find how it is imported.
+
+// This module wraps methods used by our production code.
+// Each export is names as we cannot export the entire namespace from *.
+export const captureException = (...args) => {
+ // eslint-disable-next-line no-underscore-dangle
+ const Sentry = window._Sentry;
+
+ Sentry?.captureException(...args);
+};
+
+export const captureMessage = (...args) => {
+ // eslint-disable-next-line no-underscore-dangle
+ const Sentry = window._Sentry;
+
+ Sentry?.captureMessage(...args);
+};
+
+export const withScope = (...args) => {
+ // eslint-disable-next-line no-underscore-dangle
+ const Sentry = window._Sentry;
+
+ Sentry?.withScope(...args);
+};
diff --git a/app/assets/javascripts/sentry/sentry_config.js b/app/assets/javascripts/sentry/sentry_config.js
index 4c5b8dbad5a..ed8a55b7d44 100644
--- a/app/assets/javascripts/sentry/sentry_config.js
+++ b/app/assets/javascripts/sentry/sentry_config.js
@@ -1,30 +1,24 @@
-import * as Sentry from '@sentry/browser';
-import $ from 'jquery';
-import { __ } from '~/locale';
+import * as Sentry from 'sentrybrowser7';
import { IGNORE_ERRORS, DENY_URLS, SAMPLE_RATE } from './constants';
const SentryConfig = {
- IGNORE_ERRORS,
- BLACKLIST_URLS: DENY_URLS,
- SAMPLE_RATE,
init(options = {}) {
this.options = options;
this.configure();
- this.bindSentryErrors();
if (this.options.currentUserId) this.setUser();
},
configure() {
- const { dsn, release, tags, whitelistUrls, environment } = this.options;
+ const { dsn, release, tags, allowUrls, environment } = this.options;
Sentry.init({
dsn,
release,
- whitelistUrls,
+ allowUrls,
environment,
- ignoreErrors: this.IGNORE_ERRORS, // TODO: Remove in favor of https://gitlab.com/gitlab-org/gitlab/issues/35144
- blacklistUrls: this.BLACKLIST_URLS,
+ ignoreErrors: IGNORE_ERRORS,
+ denyUrls: DENY_URLS,
sampleRate: SAMPLE_RATE,
});
@@ -36,29 +30,6 @@ const SentryConfig = {
id: this.options.currentUserId,
});
},
-
- bindSentryErrors() {
- $(document).on('ajaxError.sentry', this.handleSentryErrors);
- },
-
- handleSentryErrors(event, req, config, err) {
- const error = err || req.statusText;
- const { responseText = __('Unknown response text') } = req;
- const { type, url, data } = config;
- const { status } = req;
-
- Sentry.captureMessage(error, {
- extra: {
- type,
- url,
- data,
- status,
- response: responseText,
- error,
- event,
- },
- });
- },
};
export default SentryConfig;
diff --git a/app/assets/javascripts/set_status_modal/set_status_form.vue b/app/assets/javascripts/set_status_modal/set_status_form.vue
index 86049a2b781..dd27a12cbee 100644
--- a/app/assets/javascripts/set_status_modal/set_status_form.vue
+++ b/app/assets/javascripts/set_status_modal/set_status_form.vue
@@ -10,9 +10,9 @@ import {
GlDropdownItem,
GlSprintf,
GlFormGroup,
- GlSafeHtmlDirective,
} from '@gitlab/ui';
import $ from 'jquery';
+import SafeHtml from '~/vue_shared/directives/safe_html';
import GfmAutoComplete from 'ee_else_ce/gfm_auto_complete';
import * as Emoji from '~/emoji';
import { s__ } from '~/locale';
@@ -33,7 +33,7 @@ export default {
},
directives: {
GlTooltip: GlTooltipDirective,
- SafeHtml: GlSafeHtmlDirective,
+ SafeHtml,
},
props: {
defaultEmoji: {
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 80158c55dbc..5becc03646e 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,5 @@
<script>
-import { GlToast, GlTooltipDirective, GlSafeHtmlDirective, GlModal } from '@gitlab/ui';
+import { GlToast, GlTooltipDirective, GlModal } from '@gitlab/ui';
import Vue from 'vue';
import { createAlert } from '~/flash';
import { BV_SHOW_MODAL, BV_HIDE_MODAL } from '~/lib/utils/constants';
@@ -19,7 +19,6 @@ export default {
},
directives: {
GlTooltip: GlTooltipDirective,
- SafeHtml: GlSafeHtmlDirective,
},
mixins: [glFeatureFlagsMixin()],
props: {
@@ -110,7 +109,6 @@ export default {
this.availability = value;
},
},
- safeHtmlConfig: { ADD_TAGS: ['gl-emoji'] },
actionPrimary: { text: s__('SetStatusModal|Set status') },
actionSecondary: { text: s__('SetStatusModal|Remove status') },
};
diff --git a/app/assets/javascripts/sidebar/components/assignees/assignees_realtime.vue b/app/assets/javascripts/sidebar/components/assignees/assignees_realtime.vue
index 78d12ac113b..93fcf2cf1c9 100644
--- a/app/assets/javascripts/sidebar/components/assignees/assignees_realtime.vue
+++ b/app/assets/javascripts/sidebar/components/assignees/assignees_realtime.vue
@@ -1,7 +1,7 @@
<script>
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { IssuableType } from '~/issues/constants';
-import { assigneesQueries } from '~/sidebar/constants';
+import { assigneesQueries } from '../../constants';
export default {
subscription: null,
diff --git a/app/assets/javascripts/sidebar/components/assignees/issuable_assignees.vue b/app/assets/javascripts/sidebar/components/assignees/issuable_assignees.vue
index 4408ebb881b..fd51cd5bb16 100644
--- a/app/assets/javascripts/sidebar/components/assignees/issuable_assignees.vue
+++ b/app/assets/javascripts/sidebar/components/assignees/issuable_assignees.vue
@@ -1,7 +1,7 @@
<script>
import { GlButton } from '@gitlab/ui';
import { n__ } from '~/locale';
-import UncollapsedAssigneeList from '~/sidebar/components/assignees/uncollapsed_assignee_list.vue';
+import UncollapsedAssigneeList from './uncollapsed_assignee_list.vue';
export default {
components: {
diff --git a/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.vue b/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.vue
index 15fd365b4da..7979f450fdd 100644
--- a/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.vue
+++ b/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.vue
@@ -2,9 +2,9 @@
import { refreshUserMergeRequestCounts } from '~/commons/nav/user_merge_requests';
import { createAlert } from '~/flash';
import { __ } from '~/locale';
-import eventHub from '~/sidebar/event_hub';
-import Store from '~/sidebar/stores/sidebar_store';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
+import eventHub from '../../event_hub';
+import Store from '../../stores/sidebar_store';
import AssigneeTitle from './assignee_title.vue';
import Assignees from './assignees.vue';
import AssigneesRealtime from './assignees_realtime.vue';
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 395dcf73693..d6c679f2f07 100644
--- a/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees_widget.vue
+++ b/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees_widget.vue
@@ -4,12 +4,12 @@ import Vue from 'vue';
import { createAlert } from '~/flash';
import { IssuableType } from '~/issues/constants';
import { __, n__ } from '~/locale';
-import SidebarAssigneesRealtime from '~/sidebar/components/assignees/assignees_realtime.vue';
-import IssuableAssignees from '~/sidebar/components/assignees/issuable_assignees.vue';
-import SidebarEditableItem from '~/sidebar/components/sidebar_editable_item.vue';
-import { assigneesQueries } from '~/sidebar/constants';
import UserSelect from '~/vue_shared/components/user_select/user_select.vue';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
+import { assigneesQueries } from '../../constants';
+import SidebarEditableItem from '../sidebar_editable_item.vue';
+import SidebarAssigneesRealtime from './assignees_realtime.vue';
+import IssuableAssignees from './issuable_assignees.vue';
import SidebarInviteMembers from './sidebar_invite_members.vue';
export const assigneesWidget = Vue.observable({
diff --git a/app/assets/javascripts/sidebar/components/confidential/sidebar_confidentiality_form.vue b/app/assets/javascripts/sidebar/components/confidential/sidebar_confidentiality_form.vue
index 3532b75b6e7..dbedfe57325 100644
--- a/app/assets/javascripts/sidebar/components/confidential/sidebar_confidentiality_form.vue
+++ b/app/assets/javascripts/sidebar/components/confidential/sidebar_confidentiality_form.vue
@@ -3,7 +3,7 @@ import { GlSprintf, GlButton } from '@gitlab/ui';
import { createAlert } from '~/flash';
import { IssuableType } from '~/issues/constants';
import { __, sprintf } from '~/locale';
-import { confidentialityQueries } from '~/sidebar/constants';
+import { confidentialityQueries } from '../../constants';
export default {
i18n: {
diff --git a/app/assets/javascripts/sidebar/components/confidential/sidebar_confidentiality_widget.vue b/app/assets/javascripts/sidebar/components/confidential/sidebar_confidentiality_widget.vue
index f3bd58c11d4..c2f239b56c7 100644
--- a/app/assets/javascripts/sidebar/components/confidential/sidebar_confidentiality_widget.vue
+++ b/app/assets/javascripts/sidebar/components/confidential/sidebar_confidentiality_widget.vue
@@ -3,8 +3,8 @@ import produce from 'immer';
import Vue from 'vue';
import { createAlert } from '~/flash';
import { __, sprintf } from '~/locale';
-import SidebarEditableItem from '~/sidebar/components/sidebar_editable_item.vue';
-import { confidentialityQueries, Tracking } from '~/sidebar/constants';
+import { confidentialityQueries, Tracking } from '../../constants';
+import SidebarEditableItem from '../sidebar_editable_item.vue';
import SidebarConfidentialityContent from './sidebar_confidentiality_content.vue';
import SidebarConfidentialityForm from './sidebar_confidentiality_form.vue';
diff --git a/app/assets/javascripts/sidebar/components/copy_email_to_clipboard.vue b/app/assets/javascripts/sidebar/components/copy/copy_email_to_clipboard.vue
index fd652583f76..96ecdc84ef5 100644
--- a/app/assets/javascripts/sidebar/components/copy_email_to_clipboard.vue
+++ b/app/assets/javascripts/sidebar/components/copy/copy_email_to_clipboard.vue
@@ -1,5 +1,5 @@
<script>
-import CopyableField from '~/vue_shared/components/sidebar/copyable_field.vue';
+import CopyableField from './copyable_field.vue';
export default {
components: {
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/copyable_field.vue b/app/assets/javascripts/sidebar/components/copy/copyable_field.vue
index 6538de085b0..6538de085b0 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/copyable_field.vue
+++ b/app/assets/javascripts/sidebar/components/copy/copyable_field.vue
diff --git a/app/assets/javascripts/sidebar/components/reference/sidebar_reference_widget.vue b/app/assets/javascripts/sidebar/components/copy/sidebar_reference_widget.vue
index d07c6e0cbd2..3287539e502 100644
--- a/app/assets/javascripts/sidebar/components/reference/sidebar_reference_widget.vue
+++ b/app/assets/javascripts/sidebar/components/copy/sidebar_reference_widget.vue
@@ -1,7 +1,7 @@
<script>
import { __ } from '~/locale';
-import { referenceQueries } from '~/sidebar/constants';
-import CopyableField from '~/vue_shared/components/sidebar/copyable_field.vue';
+import { referenceQueries } from '../../constants';
+import CopyableField from './copyable_field.vue';
export default {
components: {
diff --git a/app/assets/javascripts/sidebar/components/crm_contacts/crm_contacts.vue b/app/assets/javascripts/sidebar/components/crm_contacts/crm_contacts.vue
index 81090bfa062..0660e4f58e4 100644
--- a/app/assets/javascripts/sidebar/components/crm_contacts/crm_contacts.vue
+++ b/app/assets/javascripts/sidebar/components/crm_contacts/crm_contacts.vue
@@ -4,8 +4,8 @@ import { __, n__, sprintf } from '~/locale';
import { createAlert } from '~/flash';
import { convertToGraphQLId, getIdFromGraphQLId } from '~/graphql_shared/utils';
import { TYPE_ISSUE } from '~/graphql_shared/constants';
-import getIssueCrmContactsQuery from './queries/get_issue_crm_contacts.query.graphql';
-import issueCrmContactsSubscription from './queries/issue_crm_contacts.subscription.graphql';
+import getIssueCrmContactsQuery from '../../queries/get_issue_crm_contacts.query.graphql';
+import issueCrmContactsSubscription from '../../queries/issue_crm_contacts.subscription.graphql';
export default {
components: {
diff --git a/app/assets/javascripts/sidebar/components/date/sidebar_date_widget.vue b/app/assets/javascripts/sidebar/components/date/sidebar_date_widget.vue
index c262d65f6ce..eb48732f558 100644
--- a/app/assets/javascripts/sidebar/components/date/sidebar_date_widget.vue
+++ b/app/assets/javascripts/sidebar/components/date/sidebar_date_widget.vue
@@ -4,14 +4,8 @@ import { createAlert } from '~/flash';
import { IssuableType } from '~/issues/constants';
import { dateInWords, formatDate, parsePikadayDate } from '~/lib/utils/datetime_utility';
import { __, sprintf } from '~/locale';
-import SidebarEditableItem from '~/sidebar/components/sidebar_editable_item.vue';
-import {
- dateFields,
- dateTypes,
- dueDateQueries,
- startDateQueries,
- Tracking,
-} from '~/sidebar/constants';
+import { dateFields, dateTypes, dueDateQueries, startDateQueries, Tracking } from '../../constants';
+import SidebarEditableItem from '../sidebar_editable_item.vue';
import SidebarFormattedDate from './sidebar_formatted_date.vue';
import SidebarInheritDate from './sidebar_inherit_date.vue';
diff --git a/app/assets/javascripts/sidebar/components/incidents/constants.js b/app/assets/javascripts/sidebar/components/incidents/constants.js
deleted file mode 100644
index cd05a6099fd..00000000000
--- a/app/assets/javascripts/sidebar/components/incidents/constants.js
+++ /dev/null
@@ -1,25 +0,0 @@
-import { s__ } from '~/locale';
-
-export const STATUS_TRIGGERED = 'TRIGGERED';
-export const STATUS_ACKNOWLEDGED = 'ACKNOWLEDGED';
-export const STATUS_RESOLVED = 'RESOLVED';
-
-export const STATUS_TRIGGERED_LABEL = s__('IncidentManagement|Triggered');
-export const STATUS_ACKNOWLEDGED_LABEL = s__('IncidentManagement|Acknowledged');
-export const STATUS_RESOLVED_LABEL = s__('IncidentManagement|Resolved');
-
-export const STATUS_LABELS = {
- [STATUS_TRIGGERED]: STATUS_TRIGGERED_LABEL,
- [STATUS_ACKNOWLEDGED]: STATUS_ACKNOWLEDGED_LABEL,
- [STATUS_RESOLVED]: STATUS_RESOLVED_LABEL,
-};
-
-export const i18n = {
- fetchError: s__(
- 'IncidentManagement|An error occurred while fetching the incident status. Please reload the page.',
- ),
- title: s__('IncidentManagement|Status'),
- updateError: s__(
- 'IncidentManagement|An error occurred while updating the incident status. Please reload the page and try again.',
- ),
-};
diff --git a/app/assets/javascripts/sidebar/components/incidents/escalation_status.vue b/app/assets/javascripts/sidebar/components/incidents/escalation_status.vue
index 9c41db98c63..72a572087c7 100644
--- a/app/assets/javascripts/sidebar/components/incidents/escalation_status.vue
+++ b/app/assets/javascripts/sidebar/components/incidents/escalation_status.vue
@@ -1,7 +1,12 @@
<script>
import { GlDropdown, GlDropdownItem } from '@gitlab/ui';
-import { i18n, STATUS_ACKNOWLEDGED, STATUS_TRIGGERED, STATUS_RESOLVED } from './constants';
-import { getStatusLabel } from './utils';
+import {
+ INCIDENTS_I18N as i18n,
+ STATUS_ACKNOWLEDGED,
+ STATUS_TRIGGERED,
+ STATUS_RESOLVED,
+} from '../../constants';
+import { getStatusLabel } from '../../utils';
const STATUS_LIST = [STATUS_TRIGGERED, STATUS_ACKNOWLEDGED, STATUS_RESOLVED];
diff --git a/app/assets/javascripts/sidebar/components/incidents/sidebar_escalation_status.vue b/app/assets/javascripts/sidebar/components/incidents/sidebar_escalation_status.vue
index 67ae1e6fcab..f7daad63f45 100644
--- a/app/assets/javascripts/sidebar/components/incidents/sidebar_escalation_status.vue
+++ b/app/assets/javascripts/sidebar/components/incidents/sidebar_escalation_status.vue
@@ -1,12 +1,15 @@
<script>
import { GlIcon, GlTooltipDirective } from '@gitlab/ui';
-import { escalationStatusQuery, escalationStatusMutation } from '~/sidebar/constants';
import { createAlert } from '~/flash';
import { logError } from '~/lib/logger';
import EscalationStatus from 'ee_else_ce/sidebar/components/incidents/escalation_status.vue';
+import {
+ escalationStatusQuery,
+ escalationStatusMutation,
+ INCIDENTS_I18N as i18n,
+} from '../../constants';
+import { getStatusLabel } from '../../utils';
import SidebarEditableItem from '../sidebar_editable_item.vue';
-import { i18n } from './constants';
-import { getStatusLabel } from './utils';
export default {
i18n,
diff --git a/app/assets/javascripts/sidebar/components/incidents/utils.js b/app/assets/javascripts/sidebar/components/incidents/utils.js
deleted file mode 100644
index 59bf1ea466c..00000000000
--- a/app/assets/javascripts/sidebar/components/incidents/utils.js
+++ /dev/null
@@ -1,5 +0,0 @@
-import { s__ } from '~/locale';
-
-import { STATUS_LABELS } from './constants';
-
-export const getStatusLabel = (status) => STATUS_LABELS[status] ?? s__('IncidentManagement|None');
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/constants.js b/app/assets/javascripts/sidebar/components/labels/labels_select_vue/constants.js
index 00c54313292..00c54313292 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/constants.js
+++ b/app/assets/javascripts/sidebar/components/labels/labels_select_vue/constants.js
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_button.vue b/app/assets/javascripts/sidebar/components/labels/labels_select_vue/dropdown_button.vue
index 9388ef4ba45..864d9b308e7 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_button.vue
+++ b/app/assets/javascripts/sidebar/components/labels/labels_select_vue/dropdown_button.vue
@@ -4,7 +4,7 @@ import { mapActions, mapGetters } from 'vuex';
// @deprecated This component should only be used when there is no GraphQL API.
// In most cases you should use
-// `app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget` instead.
+// `app/assets/javascripts/sidebar/components/labels/labels_select_widget` instead.
export default {
components: {
GlButton,
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_contents.vue b/app/assets/javascripts/sidebar/components/labels/labels_select_vue/dropdown_contents.vue
index 1064cbc26e3..89a976d45fa 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_contents.vue
+++ b/app/assets/javascripts/sidebar/components/labels/labels_select_vue/dropdown_contents.vue
@@ -6,7 +6,7 @@ import DropdownContentsLabelsView from './dropdown_contents_labels_view.vue';
// @deprecated This component should only be used when there is no GraphQL API.
// In most cases you should use
-// `app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents.vue` instead.
+// `app/assets/javascripts/sidebar/components/labels/labels_select_widget/dropdown_contents.vue` instead.
export default {
components: {
DropdownContentsLabelsView,
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_create_view.vue b/app/assets/javascripts/sidebar/components/labels/labels_select_vue/dropdown_contents_create_view.vue
index 3ff3755de46..b8afa67a947 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_create_view.vue
+++ b/app/assets/javascripts/sidebar/components/labels/labels_select_vue/dropdown_contents_create_view.vue
@@ -4,7 +4,7 @@ import { mapState, mapActions } from 'vuex';
// @deprecated This component should only be used when there is no GraphQL API.
// In most cases you should use
-// `app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_create_view.vue` instead.
+// `app/assets/javascripts/sidebar/components/labels/labels_select_widget/dropdown_contents_create_view.vue` instead.
export default {
components: {
GlButton,
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_labels_view.vue b/app/assets/javascripts/sidebar/components/labels/labels_select_vue/dropdown_contents_labels_view.vue
index e235bfde394..ee6b531c1ca 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_labels_view.vue
+++ b/app/assets/javascripts/sidebar/components/labels/labels_select_vue/dropdown_contents_labels_view.vue
@@ -15,7 +15,7 @@ import LabelItem from './label_item.vue';
// @deprecated This component should only be used when there is no GraphQL API.
// In most cases you should use
-// `app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_labels_view.vue` instead.
+// `app/assets/javascripts/sidebar/components/labels/labels_select_widget/dropdown_contents_labels_view.vue` instead.
export default {
components: {
GlIntersectionObserver,
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_title.vue b/app/assets/javascripts/sidebar/components/labels/labels_select_vue/dropdown_title.vue
index e4325492334..1e9edd222c5 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_title.vue
+++ b/app/assets/javascripts/sidebar/components/labels/labels_select_vue/dropdown_title.vue
@@ -4,7 +4,7 @@ import { mapState, mapActions } from 'vuex';
// @deprecated This component should only be used when there is no GraphQL API.
// In most cases you should use
-// `app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_header.vue` instead.
+// `app/assets/javascripts/sidebar/components/labels/labels_select_widget/dropdown_header.vue` instead.
export default {
components: {
GlButton,
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_value.vue b/app/assets/javascripts/sidebar/components/labels/labels_select_vue/dropdown_value.vue
index e59d150dd43..583f060be8a 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_value.vue
+++ b/app/assets/javascripts/sidebar/components/labels/labels_select_vue/dropdown_value.vue
@@ -7,7 +7,7 @@ import { isScopedLabel } from '~/lib/utils/common_utils';
// @deprecated This component should only be used when there is no GraphQL API.
// In most cases you should use
-// `app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_value.vue` instead.
+// `app/assets/javascripts/sidebar/components/labels/labels_select_widget/dropdown_value.vue` instead.
export default {
components: {
GlLabel,
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_value_collapsed.vue b/app/assets/javascripts/sidebar/components/labels/labels_select_vue/dropdown_value_collapsed.vue
index 5966c78aa51..e84da6ee12b 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_value_collapsed.vue
+++ b/app/assets/javascripts/sidebar/components/labels/labels_select_vue/dropdown_value_collapsed.vue
@@ -4,7 +4,7 @@ import { s__, sprintf } from '~/locale';
// @deprecated This component should only be used when there is no GraphQL API.
// In most cases you should use
-// `app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget` instead.
+// `app/assets/javascripts/sidebar/components/labels/labels_select_widget` instead.
export default {
directives: {
GlTooltip: GlTooltipDirective,
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/label_item.vue b/app/assets/javascripts/sidebar/components/labels/labels_select_vue/label_item.vue
index 154e3013acd..135fa9f6228 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/label_item.vue
+++ b/app/assets/javascripts/sidebar/components/labels/labels_select_vue/label_item.vue
@@ -4,7 +4,7 @@ import { __ } from '~/locale';
// @deprecated This component should only be used when there is no GraphQL API.
// In most cases you should use
-// `app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/label_item.vue` instead.
+// `app/assets/javascripts/sidebar/components/labels/labels_select_widget/label_item.vue` instead.
export default {
functional: true,
props: {
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/labels_select_root.vue b/app/assets/javascripts/sidebar/components/labels/labels_select_vue/labels_select_root.vue
index e6c29e24f0c..2a78db352d7 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/labels_select_root.vue
+++ b/app/assets/javascripts/sidebar/components/labels/labels_select_vue/labels_select_root.vue
@@ -17,7 +17,7 @@ Vue.use(Vuex);
// @deprecated This component should only be used when there is no GraphQL API.
// In most cases you should use
-// `app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/labels_select_root.vue` instead.
+// `app/assets/javascripts/sidebar/components/labels/labels_select_widget/labels_select_root.vue` instead.
export default {
store: new Vuex.Store(labelsSelectModule()),
components: {
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/store/actions.js b/app/assets/javascripts/sidebar/components/labels/labels_select_vue/store/actions.js
index 2dab97826b9..2dab97826b9 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/store/actions.js
+++ b/app/assets/javascripts/sidebar/components/labels/labels_select_vue/store/actions.js
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/store/getters.js b/app/assets/javascripts/sidebar/components/labels/labels_select_vue/store/getters.js
index ef3eedd9bb2..ef3eedd9bb2 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/store/getters.js
+++ b/app/assets/javascripts/sidebar/components/labels/labels_select_vue/store/getters.js
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/store/index.js b/app/assets/javascripts/sidebar/components/labels/labels_select_vue/store/index.js
index 5f61cb732c8..5f61cb732c8 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/store/index.js
+++ b/app/assets/javascripts/sidebar/components/labels/labels_select_vue/store/index.js
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/store/mutation_types.js b/app/assets/javascripts/sidebar/components/labels/labels_select_vue/store/mutation_types.js
index f26e36031f4..f26e36031f4 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/store/mutation_types.js
+++ b/app/assets/javascripts/sidebar/components/labels/labels_select_vue/store/mutation_types.js
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/store/mutations.js b/app/assets/javascripts/sidebar/components/labels/labels_select_vue/store/mutations.js
index c85d9befcbb..c85d9befcbb 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/store/mutations.js
+++ b/app/assets/javascripts/sidebar/components/labels/labels_select_vue/store/mutations.js
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/store/state.js b/app/assets/javascripts/sidebar/components/labels/labels_select_vue/store/state.js
index 0185d5f88e1..0185d5f88e1 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/store/state.js
+++ b/app/assets/javascripts/sidebar/components/labels/labels_select_vue/store/state.js
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/constants.js b/app/assets/javascripts/sidebar/components/labels/labels_select_widget/constants.js
index cd671b4d8f5..cd671b4d8f5 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/constants.js
+++ b/app/assets/javascripts/sidebar/components/labels/labels_select_widget/constants.js
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents.vue b/app/assets/javascripts/sidebar/components/labels/labels_select_widget/dropdown_contents.vue
index 27186281c42..83df9056af2 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents.vue
+++ b/app/assets/javascripts/sidebar/components/labels/labels_select_widget/dropdown_contents.vue
@@ -110,6 +110,9 @@ export default {
isStandalone() {
return isDropdownVariantStandalone(this.variant);
},
+ isSidebar() {
+ return isDropdownVariantSidebar(this.variant);
+ },
},
watch: {
localSelectedLabels: {
@@ -129,7 +132,7 @@ export default {
}
},
selectedLabels(newVal) {
- if (!this.isDirty) {
+ if (!this.isDirty || !this.isSidebar) {
this.localSelectedLabels = newVal;
}
},
@@ -159,7 +162,7 @@ export default {
},
handleDropdownHide() {
this.$emit('closeDropdown');
- if (!isDropdownVariantSidebar(this.variant)) {
+ if (!this.isSidebar) {
this.setLabels();
}
},
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_create_view.vue b/app/assets/javascripts/sidebar/components/labels/labels_select_widget/dropdown_contents_create_view.vue
index ce93ad216ec..aa1184ed314 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_create_view.vue
+++ b/app/assets/javascripts/sidebar/components/labels/labels_select_widget/dropdown_contents_create_view.vue
@@ -10,7 +10,7 @@ import {
import produce from 'immer';
import { createAlert } from '~/flash';
import { __ } from '~/locale';
-import { workspaceLabelsQueries } from '~/sidebar/constants';
+import { workspaceLabelsQueries } from '../../../constants';
import createLabelMutation from './graphql/create_label.mutation.graphql';
import { LabelType } from './constants';
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_labels_view.vue b/app/assets/javascripts/sidebar/components/labels/labels_select_widget/dropdown_contents_labels_view.vue
index 1d854505d11..c1939dc7785 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_labels_view.vue
+++ b/app/assets/javascripts/sidebar/components/labels/labels_select_widget/dropdown_contents_labels_view.vue
@@ -4,7 +4,7 @@ import fuzzaldrinPlus from 'fuzzaldrin-plus';
import { createAlert } from '~/flash';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { __ } from '~/locale';
-import { workspaceLabelsQueries } from '~/sidebar/constants';
+import { workspaceLabelsQueries } from '../../../constants';
import LabelItem from './label_item.vue';
export default {
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_footer.vue b/app/assets/javascripts/sidebar/components/labels/labels_select_widget/dropdown_footer.vue
index e67e704ffb8..e67e704ffb8 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_footer.vue
+++ b/app/assets/javascripts/sidebar/components/labels/labels_select_widget/dropdown_footer.vue
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_header.vue b/app/assets/javascripts/sidebar/components/labels/labels_select_widget/dropdown_header.vue
index 154a8e866d0..154a8e866d0 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_header.vue
+++ b/app/assets/javascripts/sidebar/components/labels/labels_select_widget/dropdown_header.vue
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_value.vue b/app/assets/javascripts/sidebar/components/labels/labels_select_widget/dropdown_value.vue
index 57e3ee4aaa5..57e3ee4aaa5 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_value.vue
+++ b/app/assets/javascripts/sidebar/components/labels/labels_select_widget/dropdown_value.vue
diff --git a/app/assets/javascripts/sidebar/components/labels/labels_select_widget/embedded_labels_list.vue b/app/assets/javascripts/sidebar/components/labels/labels_select_widget/embedded_labels_list.vue
new file mode 100644
index 00000000000..3a93fc7f3b2
--- /dev/null
+++ b/app/assets/javascripts/sidebar/components/labels/labels_select_widget/embedded_labels_list.vue
@@ -0,0 +1,73 @@
+<script>
+import { GlLabel } from '@gitlab/ui';
+import { sortBy } from 'lodash';
+import { isScopedLabel } from '~/lib/utils/common_utils';
+
+export default {
+ components: {
+ GlLabel,
+ },
+ inject: ['allowScopedLabels'],
+ props: {
+ disabled: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ selectedLabels: {
+ type: Array,
+ required: true,
+ },
+ allowLabelRemove: {
+ type: Boolean,
+ required: true,
+ },
+ labelsFilterBasePath: {
+ type: String,
+ required: true,
+ },
+ labelsFilterParam: {
+ type: String,
+ required: true,
+ },
+ },
+ computed: {
+ sortedSelectedLabels() {
+ return sortBy(this.selectedLabels, (label) => isScopedLabel(label));
+ },
+ },
+ methods: {
+ buildFilterUrl({ title }) {
+ const { labelsFilterBasePath: basePath, labelsFilterParam: filterParam } = this;
+
+ return `${basePath}?${filterParam}[]=${encodeURIComponent(title)}`;
+ },
+ showScopedLabel(label) {
+ return this.allowScopedLabels && isScopedLabel(label);
+ },
+ removeLabel(labelId) {
+ this.$emit('onLabelRemove', labelId);
+ },
+ },
+};
+</script>
+
+<template>
+ <div>
+ <gl-label
+ v-for="label in sortedSelectedLabels"
+ :key="label.id"
+ class="gl-mr-2 gl-mb-2"
+ :data-qa-label-name="label.title"
+ :title="label.title"
+ :description="label.description"
+ :background-color="label.color"
+ :target="buildFilterUrl(label)"
+ :scoped="showScopedLabel(label)"
+ :show-close-button="allowLabelRemove"
+ :disabled="disabled"
+ tooltip-placement="top"
+ @close="removeLabel(label.id)"
+ />
+ </div>
+</template>
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/graphql/create_label.mutation.graphql b/app/assets/javascripts/sidebar/components/labels/labels_select_widget/graphql/create_label.mutation.graphql
index a9c791091fc..a9c791091fc 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/graphql/create_label.mutation.graphql
+++ b/app/assets/javascripts/sidebar/components/labels/labels_select_widget/graphql/create_label.mutation.graphql
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/graphql/epic_labels.query.graphql b/app/assets/javascripts/sidebar/components/labels/labels_select_widget/graphql/epic_labels.query.graphql
index c442c17eb88..c442c17eb88 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/graphql/epic_labels.query.graphql
+++ b/app/assets/javascripts/sidebar/components/labels/labels_select_widget/graphql/epic_labels.query.graphql
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/graphql/epic_update_labels.mutation.graphql b/app/assets/javascripts/sidebar/components/labels/labels_select_widget/graphql/epic_update_labels.mutation.graphql
index cb054e2968f..cb054e2968f 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/graphql/epic_update_labels.mutation.graphql
+++ b/app/assets/javascripts/sidebar/components/labels/labels_select_widget/graphql/epic_update_labels.mutation.graphql
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/graphql/group_labels.query.graphql b/app/assets/javascripts/sidebar/components/labels/labels_select_widget/graphql/group_labels.query.graphql
index ce1a69f84c0..ce1a69f84c0 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/graphql/group_labels.query.graphql
+++ b/app/assets/javascripts/sidebar/components/labels/labels_select_widget/graphql/group_labels.query.graphql
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/graphql/issue_labels.query.graphql b/app/assets/javascripts/sidebar/components/labels/labels_select_widget/graphql/issue_labels.query.graphql
index 2904857270e..2904857270e 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/graphql/issue_labels.query.graphql
+++ b/app/assets/javascripts/sidebar/components/labels/labels_select_widget/graphql/issue_labels.query.graphql
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/graphql/merge_request_labels.query.graphql b/app/assets/javascripts/sidebar/components/labels/labels_select_widget/graphql/merge_request_labels.query.graphql
index e0cdfd91658..e0cdfd91658 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/graphql/merge_request_labels.query.graphql
+++ b/app/assets/javascripts/sidebar/components/labels/labels_select_widget/graphql/merge_request_labels.query.graphql
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/graphql/project_labels.query.graphql b/app/assets/javascripts/sidebar/components/labels/labels_select_widget/graphql/project_labels.query.graphql
index a7c24620aad..a7c24620aad 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/graphql/project_labels.query.graphql
+++ b/app/assets/javascripts/sidebar/components/labels/labels_select_widget/graphql/project_labels.query.graphql
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/label_item.vue b/app/assets/javascripts/sidebar/components/labels/labels_select_widget/label_item.vue
index 314ffbaf84c..314ffbaf84c 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/label_item.vue
+++ b/app/assets/javascripts/sidebar/components/labels/labels_select_widget/label_item.vue
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/labels_select_root.vue b/app/assets/javascripts/sidebar/components/labels/labels_select_widget/labels_select_root.vue
index 2c27a69d587..b7b4bbac661 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/labels_select_root.vue
+++ b/app/assets/javascripts/sidebar/components/labels/labels_select_widget/labels_select_root.vue
@@ -7,11 +7,12 @@ import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { IssuableType } from '~/issues/constants';
import { __ } from '~/locale';
-import SidebarEditableItem from '~/sidebar/components/sidebar_editable_item.vue';
-import { issuableLabelsQueries } from '~/sidebar/constants';
+import { issuableLabelsQueries } from '../../../constants';
+import SidebarEditableItem from '../../sidebar_editable_item.vue';
import { DEBOUNCE_DROPDOWN_DELAY, DropdownVariant } from './constants';
import DropdownContents from './dropdown_contents.vue';
import DropdownValue from './dropdown_value.vue';
+import EmbeddedLabelsList from './embedded_labels_list.vue';
import {
isDropdownVariantSidebar,
isDropdownVariantStandalone,
@@ -22,6 +23,7 @@ export default {
components: {
DropdownValue,
DropdownContents,
+ EmbeddedLabelsList,
SidebarEditableItem,
},
mixins: [glFeatureFlagsMixin()],
@@ -50,6 +52,11 @@ export default {
required: false,
default: false,
},
+ showEmbeddedLabelsList: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
variant: {
type: String,
required: false,
@@ -106,6 +113,11 @@ export default {
type: String,
required: true,
},
+ selectedLabels: {
+ type: Array,
+ required: false,
+ default: () => [],
+ },
},
data() {
return {
@@ -124,11 +136,21 @@ export default {
return this.issuableLabels.map((label) => label.id);
},
issuableLabels() {
- return this.issuable?.labels.nodes || [];
+ if (this.iid !== '') {
+ return this.issuable?.labels.nodes || [];
+ }
+
+ return this.selectedLabels || [];
},
issuableId() {
return this.issuable?.id;
},
+ isRealtimeEnabled() {
+ return this.glFeatures.realtimeLabels;
+ },
+ isLabelListEnabled() {
+ return this.showEmbeddedLabelsList && isDropdownVariantEmbedded(this.variant);
+ },
},
apollo: {
issuable: {
@@ -311,7 +333,10 @@ export default {
}
},
handleLabelRemove(labelId) {
- this.updateSelectedLabels(this.getRemoveVariables(labelId));
+ if (this.iid !== '') {
+ this.updateSelectedLabels(this.getRemoveVariables(labelId));
+ }
+
this.$emit('onLabelRemove', labelId);
},
isDropdownVariantSidebar,
@@ -385,22 +410,32 @@ export default {
</template>
</sidebar-editable-item>
</template>
- <dropdown-contents
- v-else
- ref="dropdownContents"
- :allow-multiselect="allowMultiselect"
- :dropdown-button-text="dropdownButtonText"
- :labels-list-title="labelsListTitle"
- :footer-create-label-title="footerCreateLabelTitle"
- :footer-manage-label-title="footerManageLabelTitle"
- :labels-create-title="labelsCreateTitle"
- :selected-labels="issuableLabels"
- :variant="variant"
- :full-path="fullPath"
- :workspace-type="workspaceType"
- :attr-workspace-path="attrWorkspacePath"
- :label-create-type="labelCreateType"
- @setLabels="handleDropdownClose"
- />
+ <template v-else>
+ <dropdown-contents
+ ref="dropdownContents"
+ :allow-multiselect="allowMultiselect"
+ :dropdown-button-text="dropdownButtonText"
+ :labels-list-title="labelsListTitle"
+ :footer-create-label-title="footerCreateLabelTitle"
+ :footer-manage-label-title="footerManageLabelTitle"
+ :labels-create-title="labelsCreateTitle"
+ :selected-labels="issuableLabels"
+ :variant="variant"
+ :full-path="fullPath"
+ :workspace-type="workspaceType"
+ :attr-workspace-path="attrWorkspacePath"
+ :label-create-type="labelCreateType"
+ @setLabels="handleDropdownClose"
+ />
+ <embedded-labels-list
+ v-if="isLabelListEnabled"
+ :disabled="labelsSelectInProgress"
+ :selected-labels="issuableLabels"
+ :allow-label-remove="allowLabelRemove"
+ :labels-filter-base-path="labelsFilterBasePath"
+ :labels-filter-param="labelsFilterParam"
+ @onLabelRemove="handleLabelRemove"
+ />
+ </template>
</div>
</template>
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/utils.js b/app/assets/javascripts/sidebar/components/labels/labels_select_widget/utils.js
index b5cd946a189..b5cd946a189 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/utils.js
+++ b/app/assets/javascripts/sidebar/components/labels/labels_select_widget/utils.js
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 d32d8a7b044..cdce6617591 100644
--- a/app/assets/javascripts/sidebar/components/lock/issuable_lock_form.vue
+++ b/app/assets/javascripts/sidebar/components/lock/issuable_lock_form.vue
@@ -4,8 +4,8 @@ import { mapGetters, mapActions } from 'vuex';
import { __, sprintf } from '~/locale';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { createAlert } from '~/flash';
-import eventHub from '~/sidebar/event_hub';
import toast from '~/vue_shared/plugins/global_toast';
+import eventHub from '../../event_hub';
import EditForm from './edit_form.vue';
export default {
@@ -111,9 +111,9 @@ export default {
</script>
<template>
- <li v-if="isMergeRequest" class="gl-new-dropdown-item">
+ <li v-if="isMergeRequest" class="gl-dropdown-item">
<button type="button" class="dropdown-item" @click="toggleLocked">
- <span class="gl-new-dropdown-item-text-wrapper">
+ <span class="gl-dropdown-item-text-wrapper">
<template v-if="isLocked">
{{ __('Unlock merge request') }}
</template>
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/issuable_move_dropdown.vue b/app/assets/javascripts/sidebar/components/move/issuable_move_dropdown.vue
index 02323e5a0c6..02323e5a0c6 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/issuable_move_dropdown.vue
+++ b/app/assets/javascripts/sidebar/components/move/issuable_move_dropdown.vue
diff --git a/app/assets/javascripts/issuable/bulk_update_sidebar/components/move_issues_button.vue b/app/assets/javascripts/sidebar/components/move/move_issues_button.vue
index 6e287ac3bb7..ab4ac9500ad 100644
--- a/app/assets/javascripts/issuable/bulk_update_sidebar/components/move_issues_button.vue
+++ b/app/assets/javascripts/sidebar/components/move/move_issues_button.vue
@@ -1,7 +1,6 @@
<script>
import { GlAlert } from '@gitlab/ui';
-import IssuableMoveDropdown from '~/vue_shared/components/sidebar/issuable_move_dropdown.vue';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import { logError } from '~/lib/logger';
import { s__ } from '~/locale';
import {
@@ -13,7 +12,8 @@ import {
import issuableEventHub from '~/issues/list/eventhub';
import getIssuesQuery from 'ee_else_ce/issues/list/queries/get_issues.query.graphql';
import getIssuesCountQuery from 'ee_else_ce/issues/list/queries/get_issues_counts.query.graphql';
-import moveIssueMutation from './graphql/mutations/move_issue.mutation.graphql';
+import moveIssueMutation from '../../queries/move_issue.mutation.graphql';
+import IssuableMoveDropdown from './issuable_move_dropdown.vue';
export default {
name: 'MoveIssuesButton',
@@ -130,7 +130,7 @@ export default {
this.moveInProgress = false;
issuableEventHub.$emit('issuables:bulkMoveEnded');
- createFlash({
+ createAlert({
message: s__(`Issues|There was an error while moving the issues.`),
});
});
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 46a04725a49..b0556e22a8d 100644
--- a/app/assets/javascripts/sidebar/components/participants/sidebar_participants_widget.vue
+++ b/app/assets/javascripts/sidebar/components/participants/sidebar_participants_widget.vue
@@ -1,6 +1,6 @@
<script>
import { __ } from '~/locale';
-import { participantsQueries } from '~/sidebar/constants';
+import { participantsQueries } from '../../constants';
import Participants from './participants.vue';
export default {
diff --git a/app/assets/javascripts/sidebar/components/reviewers/reviewers.vue b/app/assets/javascripts/sidebar/components/reviewers/reviewers.vue
index 5e1172ad835..7af8dcb4e3e 100644
--- a/app/assets/javascripts/sidebar/components/reviewers/reviewers.vue
+++ b/app/assets/javascripts/sidebar/components/reviewers/reviewers.vue
@@ -58,11 +58,21 @@ export default {
<collapsed-reviewer-list :users="sortedReviewers" :issuable-type="issuableType" />
<div class="value hide-collapsed">
- <template v-if="hasNoUsers">
- <span class="no-value">
- {{ __('None') }}
- </span>
- </template>
+ <span v-if="hasNoUsers" class="no-value" data-testid="no-value">
+ {{ __('None') }}
+ <template v-if="editable">
+ -
+ <button
+ type="button"
+ class="gl-button btn-link gl-reset-color!"
+ data-testid="assign-yourself"
+ data-qa-selector="assign_yourself_button"
+ @click="assignSelf"
+ >
+ {{ __('assign yourself') }}
+ </button>
+ </template>
+ </span>
<uncollapsed-reviewer-list
v-else
diff --git a/app/assets/javascripts/sidebar/components/reviewers/sidebar_reviewers.vue b/app/assets/javascripts/sidebar/components/reviewers/sidebar_reviewers.vue
index 5f1350690eb..faa36f3d8d2 100644
--- a/app/assets/javascripts/sidebar/components/reviewers/sidebar_reviewers.vue
+++ b/app/assets/javascripts/sidebar/components/reviewers/sidebar_reviewers.vue
@@ -5,12 +5,12 @@ import Vue from 'vue';
import { refreshUserMergeRequestCounts } from '~/commons/nav/user_merge_requests';
import { createAlert } from '~/flash';
import { __ } from '~/locale';
-import eventHub from '~/sidebar/event_hub';
-import Store from '~/sidebar/stores/sidebar_store';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
-import getMergeRequestReviewersQuery from '~/vue_shared/components/sidebar/queries/get_merge_request_reviewers.query.graphql';
-import mergeRequestReviewersUpdatedSubscription from '~/vue_shared/components/sidebar/queries/merge_request_reviewers.subscription.graphql';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
+import eventHub from '../../event_hub';
+import getMergeRequestReviewersQuery from '../../queries/get_merge_request_reviewers.query.graphql';
+import mergeRequestReviewersUpdatedSubscription from '../../queries/merge_request_reviewers.subscription.graphql';
+import Store from '../../stores/sidebar_store';
import ReviewerTitle from './reviewer_title.vue';
import Reviewers from './reviewers.vue';
@@ -143,6 +143,13 @@ export default {
eventHub.$off('sidebar.saveReviewers', this.saveReviewers);
},
methods: {
+ reviewBySelf() {
+ // Notify gl dropdown that we are now assigning to current user
+ this.$el.parentElement.dispatchEvent(new Event('assignYourself'));
+
+ this.mediator.addSelfReview();
+ this.saveReviewers();
+ },
saveReviewers() {
this.loading = true;
@@ -181,6 +188,7 @@ export default {
:editable="canUpdate"
:issuable-type="issuableType"
@request-review="requestReview"
+ @assign-self="reviewBySelf"
/>
</div>
</template>
diff --git a/app/assets/javascripts/sidebar/components/severity/constants.js b/app/assets/javascripts/sidebar/components/severity/constants.js
deleted file mode 100644
index 4f58ff38121..00000000000
--- a/app/assets/javascripts/sidebar/components/severity/constants.js
+++ /dev/null
@@ -1,41 +0,0 @@
-import { __, s__ } from '~/locale';
-
-export const INCIDENT_SEVERITY = {
- CRITICAL: {
- value: 'CRITICAL',
- icon: 'critical',
- label: s__('IncidentManagement|Critical - S1'),
- },
- HIGH: {
- value: 'HIGH',
- icon: 'high',
- label: s__('IncidentManagement|High - S2'),
- },
- MEDIUM: {
- value: 'MEDIUM',
- icon: 'medium',
- label: s__('IncidentManagement|Medium - S3'),
- },
- LOW: {
- value: 'LOW',
- icon: 'low',
- label: s__('IncidentManagement|Low - S4'),
- },
- UNKNOWN: {
- value: 'UNKNOWN',
- icon: 'unknown',
- label: s__('IncidentManagement|Unknown'),
- },
-};
-
-export const ISSUABLE_TYPES = {
- INCIDENT: 'incident',
-};
-
-export const I18N = {
- UPDATE_SEVERITY_ERROR: s__('SeverityWidget|There was an error while updating severity.'),
- TRY_AGAIN: __('Please try again'),
- EDIT: __('Edit'),
- SEVERITY: s__('SeverityWidget|Severity'),
- SEVERITY_VALUE: s__('SeverityWidget|Severity: %{severity}'),
-};
diff --git a/app/assets/javascripts/sidebar/components/severity/sidebar_severity.vue b/app/assets/javascripts/sidebar/components/severity/sidebar_severity.vue
index f02e0c783e1..5b624c17b0c 100644
--- a/app/assets/javascripts/sidebar/components/severity/sidebar_severity.vue
+++ b/app/assets/javascripts/sidebar/components/severity/sidebar_severity.vue
@@ -8,8 +8,8 @@ import {
GlButton,
} from '@gitlab/ui';
import { createAlert } from '~/flash';
-import { INCIDENT_SEVERITY, ISSUABLE_TYPES, I18N } from './constants';
-import updateIssuableSeverity from './graphql/mutations/update_issuable_severity.mutation.graphql';
+import updateIssuableSeverity from '../../queries/update_issuable_severity.mutation.graphql';
+import { INCIDENT_SEVERITY, ISSUABLE_TYPES, SEVERITY_I18N as I18N } from '../../constants';
import SeverityToken from './severity.vue';
export default {
diff --git a/app/assets/javascripts/sidebar/components/sidebar_dropdown_widget.vue b/app/assets/javascripts/sidebar/components/sidebar_dropdown_widget.vue
index a685929cdea..35667495ace 100644
--- a/app/assets/javascripts/sidebar/components/sidebar_dropdown_widget.vue
+++ b/app/assets/javascripts/sidebar/components/sidebar_dropdown_widget.vue
@@ -6,7 +6,6 @@ import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { IssuableType } from '~/issues/constants';
import { timeFor } from '~/lib/utils/datetime_utility';
import { __ } from '~/locale';
-import SidebarEditableItem from '~/sidebar/components/sidebar_editable_item.vue';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import {
dropdowni18nText,
@@ -17,6 +16,7 @@ import {
Tracking,
} from 'ee_else_ce/sidebar/constants';
import SidebarDropdown from './sidebar_dropdown.vue';
+import SidebarEditableItem from './sidebar_editable_item.vue';
export default {
i18n: {
diff --git a/app/assets/javascripts/issuable/bulk_update_sidebar/components/status_dropdown.vue b/app/assets/javascripts/sidebar/components/status/status_dropdown.vue
index ba94932289e..7763ec00091 100644
--- a/app/assets/javascripts/issuable/bulk_update_sidebar/components/status_dropdown.vue
+++ b/app/assets/javascripts/sidebar/components/status/status_dropdown.vue
@@ -1,7 +1,7 @@
<script>
import { GlDropdown, GlDropdownItem } from '@gitlab/ui';
import { __ } from '~/locale';
-import { statusDropdownOptions } from '../constants';
+import { statusDropdownOptions } from '../../constants';
export default {
components: {
diff --git a/app/assets/javascripts/sidebar/components/subscriptions/sidebar_subscriptions_widget.vue b/app/assets/javascripts/sidebar/components/subscriptions/sidebar_subscriptions_widget.vue
index 99e7c825b72..0fba1cb5e4e 100644
--- a/app/assets/javascripts/sidebar/components/subscriptions/sidebar_subscriptions_widget.vue
+++ b/app/assets/javascripts/sidebar/components/subscriptions/sidebar_subscriptions_widget.vue
@@ -4,10 +4,10 @@ import { createAlert } from '~/flash';
import { IssuableType } from '~/issues/constants';
import { isLoggedIn } from '~/lib/utils/common_utils';
import { __, sprintf } from '~/locale';
-import SidebarEditableItem from '~/sidebar/components/sidebar_editable_item.vue';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import toast from '~/vue_shared/plugins/global_toast';
-import { subscribedQueries, Tracking } from '~/sidebar/constants';
+import { subscribedQueries, Tracking } from '../../constants';
+import SidebarEditableItem from '../sidebar_editable_item.vue';
const ICON_ON = 'notifications';
const ICON_OFF = 'notifications-off';
@@ -182,7 +182,7 @@ export default {
</script>
<template>
- <gl-dropdown-form v-if="isMergeRequest" class="gl-new-dropdown-item">
+ <gl-dropdown-form v-if="isMergeRequest" class="gl-dropdown-item">
<div class="gl-px-5 gl-pb-2 gl-pt-1">
<gl-toggle
:value="subscribed"
diff --git a/app/assets/javascripts/issuable/bulk_update_sidebar/components/subscriptions_dropdown.vue b/app/assets/javascripts/sidebar/components/subscriptions/subscriptions_dropdown.vue
index 8774b065c22..4c3ba76d12d 100644
--- a/app/assets/javascripts/issuable/bulk_update_sidebar/components/subscriptions_dropdown.vue
+++ b/app/assets/javascripts/sidebar/components/subscriptions/subscriptions_dropdown.vue
@@ -1,7 +1,7 @@
<script>
import { GlDropdown, GlDropdownItem } from '@gitlab/ui';
import { __ } from '~/locale';
-import { subscriptionsDropdownOptions } from '../constants';
+import { subscriptionsDropdownOptions } from '../../constants';
export default {
subscriptionsDropdownOptions,
diff --git a/app/assets/javascripts/sidebar/components/time_tracking/constants.js b/app/assets/javascripts/sidebar/components/time_tracking/constants.js
new file mode 100644
index 00000000000..56e986e3b27
--- /dev/null
+++ b/app/assets/javascripts/sidebar/components/time_tracking/constants.js
@@ -0,0 +1 @@
+export const CREATE_TIMELOG_MODAL_ID = 'create-timelog-modal';
diff --git a/app/assets/javascripts/sidebar/components/time_tracking/create_timelog_form.vue b/app/assets/javascripts/sidebar/components/time_tracking/create_timelog_form.vue
new file mode 100644
index 00000000000..ec8e1ee9952
--- /dev/null
+++ b/app/assets/javascripts/sidebar/components/time_tracking/create_timelog_form.vue
@@ -0,0 +1,227 @@
+<script>
+import {
+ GlFormGroup,
+ GlFormInput,
+ GlDatepicker,
+ GlFormTextarea,
+ GlModal,
+ GlAlert,
+ GlLink,
+ GlSprintf,
+} from '@gitlab/ui';
+import { convertToGraphQLId } from '~/graphql_shared/utils';
+import { formatDate } from '~/lib/utils/datetime_utility';
+import { TYPE_ISSUE, TYPE_MERGE_REQUEST } from '~/graphql_shared/constants';
+import { joinPaths } from '~/lib/utils/url_utility';
+import { s__ } from '~/locale';
+import createTimelogMutation from '../../queries/create_timelog.mutation.graphql';
+import { CREATE_TIMELOG_MODAL_ID } from './constants';
+
+export default {
+ components: {
+ GlDatepicker,
+ GlFormGroup,
+ GlFormInput,
+ GlFormTextarea,
+ GlModal,
+ GlAlert,
+ GlLink,
+ GlSprintf,
+ },
+ inject: ['issuableType'],
+ props: {
+ issuableId: {
+ type: String,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ timeSpent: '',
+ spentAt: null,
+ summary: '',
+ isLoading: false,
+ saveError: '',
+ };
+ },
+ computed: {
+ submitDisabled() {
+ return this.isLoading || this.timeSpent.length === 0;
+ },
+ primaryProps() {
+ return {
+ text: s__('CreateTimelogForm|Save'),
+ attributes: [
+ {
+ variant: 'confirm',
+ disabled: this.submitDisabled,
+ loading: this.isLoading,
+ },
+ ],
+ };
+ },
+ cancelProps() {
+ return {
+ text: s__('CreateTimelogForm|Cancel'),
+ };
+ },
+ timeTrackingDocsPath() {
+ return joinPaths(gon.relative_url_root || '', '/help/user/project/time_tracking.md');
+ },
+ issuableTypeName() {
+ return this.isIssue()
+ ? s__('CreateTimelogForm|issue')
+ : s__('CreateTimelogForm|merge request');
+ },
+ },
+ methods: {
+ resetModal() {
+ this.isLoading = false;
+ this.timeSpent = '';
+ this.spentAt = null;
+ this.summary = '';
+ this.saveError = '';
+ },
+ close() {
+ this.resetModal();
+ this.$refs.modal.close();
+ },
+ registerTimeSpent(event) {
+ event.preventDefault();
+
+ if (this.timeSpent.length === 0) {
+ return;
+ }
+
+ this.isLoading = true;
+ this.saveError = '';
+
+ this.$apollo
+ .mutate({
+ mutation: createTimelogMutation,
+ variables: {
+ input: {
+ timeSpent: this.timeSpent,
+ spentAt: this.spentAt
+ ? formatDate(this.spentAt, 'isoDateTime')
+ : formatDate(Date.now(), 'isoDateTime'),
+ summary: this.summary,
+ issuableId: this.getIssuableId(),
+ },
+ },
+ })
+ .then(({ data }) => {
+ if (data.timelogCreate?.errors.length) {
+ this.saveError = data.timelogCreate.errors[0].message || data.timelogCreate.errors[0];
+ } else {
+ this.close();
+ }
+ })
+ .catch((error) => {
+ this.saveError =
+ error?.message ||
+ s__('CreateTimelogForm|An error occurred while saving the time entry.');
+ })
+ .finally(() => {
+ this.isLoading = false;
+ });
+ },
+ isIssue() {
+ return this.issuableType === 'issue';
+ },
+ getGraphQLEntityType() {
+ return this.isIssue() ? TYPE_ISSUE : TYPE_MERGE_REQUEST;
+ },
+ updateSpentAtDate(val) {
+ this.spentAt = val;
+ },
+ getIssuableId() {
+ return convertToGraphQLId(this.getGraphQLEntityType(), this.issuableId);
+ },
+ },
+ CREATE_TIMELOG_MODAL_ID,
+};
+</script>
+
+<template>
+ <gl-modal
+ ref="modal"
+ :title="s__('CreateTimelogForm|Add time entry')"
+ :modal-id="$options.CREATE_TIMELOG_MODAL_ID"
+ size="sm"
+ data-testid="create-timelog-modal"
+ :action-primary="primaryProps"
+ :action-cancel="cancelProps"
+ @primary="registerTimeSpent"
+ @cancel="close"
+ @close="close"
+ @hide="close"
+ >
+ <p data-testid="timetracking-docs-link">
+ <gl-sprintf
+ :message="
+ s__(
+ 'CreateTimelogForm|Track time spent on this %{issuableTypeNameStart}%{issuableTypeNameEnd}. %{timeTrackingDocsLinkStart}%{timeTrackingDocsLinkEnd}',
+ )
+ "
+ >
+ <template #issuableTypeName>{{ issuableTypeName }}</template>
+ <template #timeTrackingDocsLink>
+ <gl-link :href="timeTrackingDocsPath" target="_blank">{{
+ s__('CreateTimelogForm|How do I track and estimate time?')
+ }}</gl-link>
+ </template>
+ </gl-sprintf>
+ </p>
+ <form
+ class="gl-display-flex gl-flex-direction-column js-quick-submit"
+ @submit.prevent="registerTimeSpent"
+ >
+ <div class="gl-display-flex gl-gap-3">
+ <gl-form-group
+ key="time-spent"
+ label-for="time-spent"
+ :label="s__(`CreateTimelogForm|Time spent`)"
+ :description="s__(`CreateTimelogForm|Example: 1h 30m`)"
+ >
+ <gl-form-input
+ id="time-spent"
+ ref="timeSpent"
+ v-model="timeSpent"
+ class="gl-form-input-sm"
+ autocomplete="off"
+ />
+ </gl-form-group>
+ <gl-form-group
+ key="spent-at"
+ optional
+ label-for="spent-at"
+ :label="s__(`CreateTimelogForm|Spent at`)"
+ >
+ <gl-datepicker
+ :target="null"
+ :value="spentAt"
+ show-clear-button
+ autocomplete="off"
+ size="small"
+ @input="updateSpentAtDate"
+ @clear="updateSpentAtDate(null)"
+ />
+ </gl-form-group>
+ </div>
+ <gl-form-group
+ :label="s__('CreateTimelogForm|Summary')"
+ optional
+ label-for="summary"
+ class="gl-mb-0"
+ >
+ <gl-form-textarea id="summary" v-model="summary" rows="3" :no-resize="true" />
+ </gl-form-group>
+ <gl-alert v-if="saveError" variant="danger" class="gl-mt-5" :dismissible="false">
+ {{ saveError }}
+ </gl-alert>
+ <!-- This is needed to have the quick-submit behaviour (with Ctrl + Enter or Cmd + Enter) -->
+ <input type="submit" hidden />
+ </form>
+ </gl-modal>
+</template>
diff --git a/app/assets/javascripts/sidebar/components/time_tracking/help_state.vue b/app/assets/javascripts/sidebar/components/time_tracking/help_state.vue
index 91c15061fb9..6cd9596e43f 100644
--- a/app/assets/javascripts/sidebar/components/time_tracking/help_state.vue
+++ b/app/assets/javascripts/sidebar/components/time_tracking/help_state.vue
@@ -1,5 +1,6 @@
<script>
-import { GlButton, GlSafeHtmlDirective } from '@gitlab/ui';
+import { GlButton } from '@gitlab/ui';
+import SafeHtml from '~/vue_shared/directives/safe_html';
import { joinPaths } from '~/lib/utils/url_utility';
import { sprintf, s__ } from '~/locale';
@@ -9,7 +10,7 @@ export default {
GlButton,
},
directives: {
- SafeHtml: GlSafeHtmlDirective,
+ SafeHtml,
},
computed: {
href() {
diff --git a/app/assets/javascripts/sidebar/components/time_tracking/report.vue b/app/assets/javascripts/sidebar/components/time_tracking/report.vue
index 124464088cf..6f4ced06ddf 100644
--- a/app/assets/javascripts/sidebar/components/time_tracking/report.vue
+++ b/app/assets/javascripts/sidebar/components/time_tracking/report.vue
@@ -5,8 +5,8 @@ import { TYPE_ISSUE, TYPE_MERGE_REQUEST } from '~/graphql_shared/constants';
import { convertToGraphQLId } from '~/graphql_shared/utils';
import { formatDate, parseSeconds, stringifyTime } from '~/lib/utils/datetime_utility';
import { __, s__ } from '~/locale';
-import { timelogQueries } from '~/sidebar/constants';
-import deleteTimelogMutation from './graphql/mutations/delete_timelog.mutation.graphql';
+import { timelogQueries } from '../../constants';
+import deleteTimelogMutation from '../../queries/delete_timelog.mutation.graphql';
const TIME_DATE_FORMAT = 'mmmm d, yyyy, HH:MM ("UTC:" o)';
diff --git a/app/assets/javascripts/sidebar/components/time_tracking/sidebar_time_tracking.vue b/app/assets/javascripts/sidebar/components/time_tracking/sidebar_time_tracking.vue
index 62b05421884..06adc048942 100644
--- a/app/assets/javascripts/sidebar/components/time_tracking/sidebar_time_tracking.vue
+++ b/app/assets/javascripts/sidebar/components/time_tracking/sidebar_time_tracking.vue
@@ -30,6 +30,11 @@ export default {
required: false,
default: false,
},
+ canAddTimeEntries: {
+ type: Boolean,
+ required: false,
+ default: true,
+ },
},
mounted() {
this.listenForQuickActions();
@@ -67,6 +72,7 @@ export default {
:issuable-id="issuableId"
:issuable-iid="issuableIid"
:limit-to-hours="limitToHours"
+ :can-add-time-entries="canAddTimeEntries"
/>
</div>
</template>
diff --git a/app/assets/javascripts/sidebar/components/time_tracking/time_tracker.vue b/app/assets/javascripts/sidebar/components/time_tracking/time_tracker.vue
index 13981c477c6..b32836dc87d 100644
--- a/app/assets/javascripts/sidebar/components/time_tracking/time_tracker.vue
+++ b/app/assets/javascripts/sidebar/components/time_tracking/time_tracker.vue
@@ -9,15 +9,17 @@ import {
GlTooltipDirective,
} from '@gitlab/ui';
import { IssuableType } from '~/issues/constants';
+import { BV_SHOW_MODAL } from '~/lib/utils/constants';
import { s__, __ } from '~/locale';
-import { HOW_TO_TRACK_TIME, timeTrackingQueries } from '~/sidebar/constants';
+import { HOW_TO_TRACK_TIME, timeTrackingQueries } from '../../constants';
import eventHub from '../../event_hub';
import TimeTrackingCollapsedState from './collapsed_state.vue';
import TimeTrackingComparisonPane from './comparison_pane.vue';
-import TimeTrackingHelpState from './help_state.vue';
import TimeTrackingReport from './report.vue';
import TimeTrackingSpentOnlyPane from './spent_only_pane.vue';
+import { CREATE_TIMELOG_MODAL_ID } from './constants';
+import CreateTimelogForm from './create_timelog_form.vue';
export default {
name: 'IssuableTimeTracker',
@@ -34,8 +36,8 @@ export default {
TimeTrackingCollapsedState,
TimeTrackingSpentOnlyPane,
TimeTrackingComparisonPane,
- TimeTrackingHelpState,
TimeTrackingReport,
+ CreateTimelogForm,
},
directives: {
GlModal: GlModalDirective,
@@ -87,6 +89,11 @@ export default {
default: true,
required: false,
},
+ canAddTimeEntries: {
+ type: Boolean,
+ required: false,
+ default: true,
+ },
},
data() {
return {
@@ -192,12 +199,12 @@ export default {
eventHub.$on('timeTracker:refresh', this.refresh);
},
methods: {
- toggleHelpState(show) {
- this.showHelp = show;
- },
refresh() {
this.$apollo.queries.issuableTimeTracking.refetch();
},
+ openRegisterTimeSpentModal() {
+ this.$root.$emit(BV_SHOW_MODAL, CREATE_TIMELOG_MODAL_ID);
+ },
},
};
</script>
@@ -215,24 +222,21 @@ export default {
:time-estimate-human-readable="humanTimeEstimate"
/>
<div
- class="hide-collapsed gl-line-height-20 gl-text-gray-900 gl-display-flex gl-align-items-center gl-font-weight-bold gl-mr-3"
+ class="hide-collapsed gl-line-height-20 gl-text-gray-900 gl-display-flex gl-align-items-center gl-font-weight-bold"
>
{{ __('Time tracking') }}
<gl-loading-icon v-if="isTimeTrackingInfoLoading" size="sm" class="gl-ml-2" inline />
<gl-button
- :data-testid="showHelpState ? 'closeHelpButton' : 'helpButton'"
+ v-if="canAddTimeEntries"
+ v-gl-tooltip.left
category="tertiary"
size="small"
- variant="link"
class="gl-ml-auto"
- @click="toggleHelpState(!showHelpState)"
+ data-testid="add-time-entry-button"
+ :title="__('Add time entry')"
+ @click="openRegisterTimeSpentModal()"
>
- <gl-icon
- v-gl-tooltip.left
- :title="timeTrackingIconTitle"
- :name="timeTrackingIconName"
- class="gl-text-gray-900!"
- />
+ <gl-icon name="plus" class="gl-text-gray-900!" />
</gl-button>
</div>
<div v-if="!isTimeTrackingInfoLoading" class="hide-collapsed">
@@ -272,9 +276,7 @@ export default {
<time-tracking-report :limit-to-hours="limitToHours" :issuable-id="issuableId" />
</gl-modal>
</template>
- <transition name="help-state-toggle">
- <time-tracking-help-state v-if="showHelpState" />
- </transition>
+ <create-timelog-form :issuable-id="issuableId" />
</div>
</div>
</template>
diff --git a/app/assets/javascripts/sidebar/components/todo_toggle/sidebar_todo_widget.vue b/app/assets/javascripts/sidebar/components/todo_toggle/sidebar_todo_widget.vue
index 5da2d65723a..b86ff279fd8 100644
--- a/app/assets/javascripts/sidebar/components/todo_toggle/sidebar_todo_widget.vue
+++ b/app/assets/javascripts/sidebar/components/todo_toggle/sidebar_todo_widget.vue
@@ -3,11 +3,11 @@ import { GlButton, GlIcon, GlTooltipDirective } from '@gitlab/ui';
import { produce } from 'immer';
import { createAlert } from '~/flash';
import { __, sprintf } from '~/locale';
-import { todoQueries, TodoMutationTypes, todoMutations } from '~/sidebar/constants';
-import { todoLabel } from '~/vue_shared/components/sidebar/todo_toggle//utils';
-import TodoButton from '~/vue_shared/components/sidebar/todo_toggle/todo_button.vue';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import Tracking from '~/tracking';
+import { todoQueries, TodoMutationTypes, todoMutations } from '../../constants';
+import { todoLabel } from '../../utils';
+import TodoButton from './todo_button.vue';
const trackingMixin = Tracking.mixin();
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/todo_toggle/todo_button.vue b/app/assets/javascripts/sidebar/components/todo_toggle/todo_button.vue
index cdc7422c7df..b49b8fc389b 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/todo_toggle/todo_button.vue
+++ b/app/assets/javascripts/sidebar/components/todo_toggle/todo_button.vue
@@ -1,6 +1,6 @@
<script>
import { GlButton } from '@gitlab/ui';
-import { todoLabel, updateGlobalTodoCount } from './utils';
+import { todoLabel, updateGlobalTodoCount } from '../../utils';
export default {
components: {
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/toggle_sidebar.vue b/app/assets/javascripts/sidebar/components/toggle/toggle_sidebar.vue
index 6dacf4e10d3..6dacf4e10d3 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/toggle_sidebar.vue
+++ b/app/assets/javascripts/sidebar/components/toggle/toggle_sidebar.vue
diff --git a/app/assets/javascripts/sidebar/constants.js b/app/assets/javascripts/sidebar/constants.js
index 67b9b540e91..825a89daf58 100644
--- a/app/assets/javascripts/sidebar/constants.js
+++ b/app/assets/javascripts/sidebar/constants.js
@@ -4,55 +4,55 @@ import updateIssueLabelsMutation from '~/boards/graphql/issue_set_labels.mutatio
import userSearchQuery from '~/graphql_shared/queries/users_search.query.graphql';
import userSearchWithMRPermissionsQuery from '~/graphql_shared/queries/users_search_with_mr_permissions.graphql';
import { IssuableType, WorkspaceType } from '~/issues/constants';
-import epicConfidentialQuery from '~/sidebar/queries/epic_confidential.query.graphql';
-import epicDueDateQuery from '~/sidebar/queries/epic_due_date.query.graphql';
-import epicParticipantsQuery from '~/sidebar/queries/epic_participants.query.graphql';
-import epicReferenceQuery from '~/sidebar/queries/epic_reference.query.graphql';
-import epicStartDateQuery from '~/sidebar/queries/epic_start_date.query.graphql';
-import epicSubscribedQuery from '~/sidebar/queries/epic_subscribed.query.graphql';
-import epicTodoQuery from '~/sidebar/queries/epic_todo.query.graphql';
-import issuableAssigneesSubscription from '~/sidebar/queries/issuable_assignees.subscription.graphql';
-import issueConfidentialQuery from '~/sidebar/queries/issue_confidential.query.graphql';
-import issueDueDateQuery from '~/sidebar/queries/issue_due_date.query.graphql';
-import issueReferenceQuery from '~/sidebar/queries/issue_reference.query.graphql';
-import issueSubscribedQuery from '~/sidebar/queries/issue_subscribed.query.graphql';
-import issueTimeTrackingQuery from '~/sidebar/queries/issue_time_tracking.query.graphql';
-import issueTodoQuery from '~/sidebar/queries/issue_todo.query.graphql';
-import mergeRequestMilestone from '~/sidebar/queries/merge_request_milestone.query.graphql';
-import mergeRequestReferenceQuery from '~/sidebar/queries/merge_request_reference.query.graphql';
-import mergeRequestSubscribed from '~/sidebar/queries/merge_request_subscribed.query.graphql';
-import mergeRequestTimeTrackingQuery from '~/sidebar/queries/merge_request_time_tracking.query.graphql';
-import mergeRequestTodoQuery from '~/sidebar/queries/merge_request_todo.query.graphql';
-import todoCreateMutation from '~/sidebar/queries/todo_create.mutation.graphql';
-import todoMarkDoneMutation from '~/sidebar/queries/todo_mark_done.mutation.graphql';
-import updateEpicConfidentialMutation from '~/sidebar/queries/update_epic_confidential.mutation.graphql';
-import updateEpicDueDateMutation from '~/sidebar/queries/update_epic_due_date.mutation.graphql';
-import updateEpicStartDateMutation from '~/sidebar/queries/update_epic_start_date.mutation.graphql';
-import updateEpicSubscriptionMutation from '~/sidebar/queries/update_epic_subscription.mutation.graphql';
-import updateIssueConfidentialMutation from '~/sidebar/queries/update_issue_confidential.mutation.graphql';
-import updateIssueDueDateMutation from '~/sidebar/queries/update_issue_due_date.mutation.graphql';
-import updateIssueSubscriptionMutation from '~/sidebar/queries/update_issue_subscription.mutation.graphql';
-import mergeRequestMilestoneMutation from '~/sidebar/queries/update_merge_request_milestone.mutation.graphql';
-import updateMergeRequestLabelsMutation from '~/sidebar/queries/update_merge_request_labels.mutation.graphql';
-import updateMergeRequestSubscriptionMutation from '~/sidebar/queries/update_merge_request_subscription.mutation.graphql';
import updateAlertAssigneesMutation from '~/vue_shared/alert_details/graphql/mutations/alert_set_assignees.mutation.graphql';
-import epicLabelsQuery from '~/vue_shared/components/sidebar/labels_select_widget/graphql/epic_labels.query.graphql';
-import updateEpicLabelsMutation from '~/vue_shared/components/sidebar/labels_select_widget/graphql/epic_update_labels.mutation.graphql';
-import groupLabelsQuery from '~/vue_shared/components/sidebar/labels_select_widget/graphql/group_labels.query.graphql';
-import issueLabelsQuery from '~/vue_shared/components/sidebar/labels_select_widget/graphql/issue_labels.query.graphql';
-import mergeRequestLabelsQuery from '~/vue_shared/components/sidebar/labels_select_widget/graphql/merge_request_labels.query.graphql';
-import projectLabelsQuery from '~/vue_shared/components/sidebar/labels_select_widget/graphql/project_labels.query.graphql';
-import getAlertAssignees from '~/vue_shared/components/sidebar/queries/get_alert_assignees.query.graphql';
-import getIssueAssignees from '~/vue_shared/components/sidebar/queries/get_issue_assignees.query.graphql';
-import issueParticipantsQuery from '~/vue_shared/components/sidebar/queries/get_issue_participants.query.graphql';
-import getIssueTimelogsQuery from '~/vue_shared/components/sidebar/queries/get_issue_timelogs.query.graphql';
-import getMergeRequestAssignees from '~/vue_shared/components/sidebar/queries/get_mr_assignees.query.graphql';
-import getMergeRequestParticipants from '~/vue_shared/components/sidebar/queries/get_mr_participants.query.graphql';
-import getMrTimelogsQuery from '~/vue_shared/components/sidebar/queries/get_mr_timelogs.query.graphql';
-import updateIssueAssigneesMutation from '~/vue_shared/components/sidebar/queries/update_issue_assignees.mutation.graphql';
-import updateMergeRequestAssigneesMutation from '~/vue_shared/components/sidebar/queries/update_mr_assignees.mutation.graphql';
-import getEscalationStatusQuery from '~/sidebar/queries/escalation_status.query.graphql';
-import updateEscalationStatusMutation from '~/sidebar/queries/update_escalation_status.mutation.graphql';
+import epicLabelsQuery from './components/labels/labels_select_widget/graphql/epic_labels.query.graphql';
+import updateEpicLabelsMutation from './components/labels/labels_select_widget/graphql/epic_update_labels.mutation.graphql';
+import groupLabelsQuery from './components/labels/labels_select_widget/graphql/group_labels.query.graphql';
+import issueLabelsQuery from './components/labels/labels_select_widget/graphql/issue_labels.query.graphql';
+import mergeRequestLabelsQuery from './components/labels/labels_select_widget/graphql/merge_request_labels.query.graphql';
+import projectLabelsQuery from './components/labels/labels_select_widget/graphql/project_labels.query.graphql';
+import epicConfidentialQuery from './queries/epic_confidential.query.graphql';
+import epicDueDateQuery from './queries/epic_due_date.query.graphql';
+import epicParticipantsQuery from './queries/epic_participants.query.graphql';
+import epicReferenceQuery from './queries/epic_reference.query.graphql';
+import epicStartDateQuery from './queries/epic_start_date.query.graphql';
+import epicSubscribedQuery from './queries/epic_subscribed.query.graphql';
+import epicTodoQuery from './queries/epic_todo.query.graphql';
+import issuableAssigneesSubscription from './queries/issuable_assignees.subscription.graphql';
+import issueConfidentialQuery from './queries/issue_confidential.query.graphql';
+import issueDueDateQuery from './queries/issue_due_date.query.graphql';
+import issueReferenceQuery from './queries/issue_reference.query.graphql';
+import issueSubscribedQuery from './queries/issue_subscribed.query.graphql';
+import issueTimeTrackingQuery from './queries/issue_time_tracking.query.graphql';
+import issueTodoQuery from './queries/issue_todo.query.graphql';
+import mergeRequestMilestone from './queries/merge_request_milestone.query.graphql';
+import mergeRequestReferenceQuery from './queries/merge_request_reference.query.graphql';
+import mergeRequestSubscribed from './queries/merge_request_subscribed.query.graphql';
+import mergeRequestTimeTrackingQuery from './queries/merge_request_time_tracking.query.graphql';
+import mergeRequestTodoQuery from './queries/merge_request_todo.query.graphql';
+import todoCreateMutation from './queries/todo_create.mutation.graphql';
+import todoMarkDoneMutation from './queries/todo_mark_done.mutation.graphql';
+import updateEpicConfidentialMutation from './queries/update_epic_confidential.mutation.graphql';
+import updateEpicDueDateMutation from './queries/update_epic_due_date.mutation.graphql';
+import updateEpicStartDateMutation from './queries/update_epic_start_date.mutation.graphql';
+import updateEpicSubscriptionMutation from './queries/update_epic_subscription.mutation.graphql';
+import updateIssueConfidentialMutation from './queries/update_issue_confidential.mutation.graphql';
+import updateIssueDueDateMutation from './queries/update_issue_due_date.mutation.graphql';
+import updateIssueSubscriptionMutation from './queries/update_issue_subscription.mutation.graphql';
+import mergeRequestMilestoneMutation from './queries/update_merge_request_milestone.mutation.graphql';
+import updateMergeRequestLabelsMutation from './queries/update_merge_request_labels.mutation.graphql';
+import updateMergeRequestSubscriptionMutation from './queries/update_merge_request_subscription.mutation.graphql';
+import getAlertAssignees from './queries/get_alert_assignees.query.graphql';
+import getIssueAssignees from './queries/get_issue_assignees.query.graphql';
+import issueParticipantsQuery from './queries/get_issue_participants.query.graphql';
+import getIssueTimelogsQuery from './queries/get_issue_timelogs.query.graphql';
+import getMergeRequestAssignees from './queries/get_mr_assignees.query.graphql';
+import getMergeRequestParticipants from './queries/get_mr_participants.query.graphql';
+import getMrTimelogsQuery from './queries/get_mr_timelogs.query.graphql';
+import updateIssueAssigneesMutation from './queries/update_issue_assignees.mutation.graphql';
+import updateMergeRequestAssigneesMutation from './queries/update_mr_assignees.mutation.graphql';
+import getEscalationStatusQuery from './queries/escalation_status.query.graphql';
+import updateEscalationStatusMutation from './queries/update_escalation_status.mutation.graphql';
import groupMilestonesQuery from './queries/group_milestones.query.graphql';
import projectIssueMilestoneMutation from './queries/project_issue_milestone.mutation.graphql';
import projectIssueMilestoneQuery from './queries/project_issue_milestone.query.graphql';
@@ -350,3 +350,94 @@ export const escalationStatusQuery = getEscalationStatusQuery;
export const escalationStatusMutation = updateEscalationStatusMutation;
export const HOW_TO_TRACK_TIME = __('How to track time');
+
+export const statusDropdownOptions = [
+ {
+ text: __('Open'),
+ value: 'reopen',
+ },
+ {
+ text: __('Closed'),
+ value: 'close',
+ },
+];
+
+export const subscriptionsDropdownOptions = [
+ {
+ text: __('Subscribe'),
+ value: 'subscribe',
+ },
+ {
+ text: __('Unsubscribe'),
+ value: 'unsubscribe',
+ },
+];
+
+export const INCIDENT_SEVERITY = {
+ CRITICAL: {
+ value: 'CRITICAL',
+ icon: 'critical',
+ label: s__('IncidentManagement|Critical - S1'),
+ },
+ HIGH: {
+ value: 'HIGH',
+ icon: 'high',
+ label: s__('IncidentManagement|High - S2'),
+ },
+ MEDIUM: {
+ value: 'MEDIUM',
+ icon: 'medium',
+ label: s__('IncidentManagement|Medium - S3'),
+ },
+ LOW: {
+ value: 'LOW',
+ icon: 'low',
+ label: s__('IncidentManagement|Low - S4'),
+ },
+ UNKNOWN: {
+ value: 'UNKNOWN',
+ icon: 'unknown',
+ label: s__('IncidentManagement|Unknown'),
+ },
+};
+
+export const ISSUABLE_TYPES = {
+ INCIDENT: 'incident',
+};
+
+export const MILESTONE_STATE = {
+ ACTIVE: 'active',
+ CLOSED: 'closed',
+};
+
+export const SEVERITY_I18N = {
+ UPDATE_SEVERITY_ERROR: s__('SeverityWidget|There was an error while updating severity.'),
+ TRY_AGAIN: __('Please try again'),
+ EDIT: __('Edit'),
+ SEVERITY: s__('SeverityWidget|Severity'),
+ SEVERITY_VALUE: s__('SeverityWidget|Severity: %{severity}'),
+};
+
+export const STATUS_TRIGGERED = 'TRIGGERED';
+export const STATUS_ACKNOWLEDGED = 'ACKNOWLEDGED';
+export const STATUS_RESOLVED = 'RESOLVED';
+
+export const STATUS_TRIGGERED_LABEL = s__('IncidentManagement|Triggered');
+export const STATUS_ACKNOWLEDGED_LABEL = s__('IncidentManagement|Acknowledged');
+export const STATUS_RESOLVED_LABEL = s__('IncidentManagement|Resolved');
+
+export const STATUS_LABELS = {
+ [STATUS_TRIGGERED]: STATUS_TRIGGERED_LABEL,
+ [STATUS_ACKNOWLEDGED]: STATUS_ACKNOWLEDGED_LABEL,
+ [STATUS_RESOLVED]: STATUS_RESOLVED_LABEL,
+};
+
+export const INCIDENTS_I18N = {
+ fetchError: s__(
+ 'IncidentManagement|An error occurred while fetching the incident status. Please reload the page.',
+ ),
+ title: s__('IncidentManagement|Status'),
+ updateError: s__(
+ 'IncidentManagement|An error occurred while updating the incident status. Please reload the page and try again.',
+ ),
+};
diff --git a/app/assets/javascripts/sidebar/mount_milestone_sidebar.js b/app/assets/javascripts/sidebar/mount_milestone_sidebar.js
index afce59d304f..b908cf0cd9e 100644
--- a/app/assets/javascripts/sidebar/mount_milestone_sidebar.js
+++ b/app/assets/javascripts/sidebar/mount_milestone_sidebar.js
@@ -39,6 +39,7 @@ export default class SidebarMilestone {
humanTimeEstimate,
humanTotalTimeSpent: humanTimeSpent,
},
+ canAddTimeEntries: false,
},
}),
});
diff --git a/app/assets/javascripts/sidebar/mount_sidebar.js b/app/assets/javascripts/sidebar/mount_sidebar.js
index b37486283ca..a308dc8d13c 100644
--- a/app/assets/javascripts/sidebar/mount_sidebar.js
+++ b/app/assets/javascripts/sidebar/mount_sidebar.js
@@ -6,6 +6,7 @@ import { convertToGraphQLId } from '~/graphql_shared/utils';
import initInviteMembersModal from '~/invite_members/init_invite_members_modal';
import initInviteMembersTrigger from '~/invite_members/init_invite_members_trigger';
import { IssuableType } from '~/issues/constants';
+import { gqlClient } from '~/issues/list/graphql';
import {
isInIssuePage,
isInDesignPage,
@@ -14,33 +15,36 @@ import {
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';
-import SidebarDueDateWidget from '~/sidebar/components/date/sidebar_date_widget.vue';
-import MilestoneDropdown from '~/sidebar/components/milestone/milestone_dropdown.vue';
-import SidebarParticipantsWidget from '~/sidebar/components/participants/sidebar_participants_widget.vue';
-import SidebarReferenceWidget from '~/sidebar/components/reference/sidebar_reference_widget.vue';
-import SidebarDropdownWidget from '~/sidebar/components/sidebar_dropdown_widget.vue';
-import SidebarTodoWidget from '~/sidebar/components/todo_toggle/sidebar_todo_widget.vue';
import { apolloProvider } from '~/graphql_shared/issuable_client';
-import trackShowInviteMemberLink from '~/sidebar/track_invite_members';
-import { DropdownVariant } from '~/vue_shared/components/sidebar/labels_select_vue/constants';
-import LabelsSelectWidget from '~/vue_shared/components/sidebar/labels_select_widget/labels_select_root.vue';
-import { LabelType } from '~/vue_shared/components/sidebar/labels_select_widget/constants';
-import Translate from '../vue_shared/translate';
+import Translate from '~/vue_shared/translate';
+import CollapsedAssigneeList from './components/assignees/collapsed_assignee_list.vue';
import SidebarAssignees from './components/assignees/sidebar_assignees.vue';
-import CopyEmailToClipboard from './components/copy_email_to_clipboard.vue';
+import SidebarAssigneesWidget from './components/assignees/sidebar_assignees_widget.vue';
+import SidebarConfidentialityWidget from './components/confidential/sidebar_confidentiality_widget.vue';
+import CopyEmailToClipboard from './components/copy/copy_email_to_clipboard.vue';
+import SidebarDueDateWidget from './components/date/sidebar_date_widget.vue';
import SidebarEscalationStatus from './components/incidents/sidebar_escalation_status.vue';
+import { DropdownVariant } from './components/labels/labels_select_vue/constants';
+import { LabelType } from './components/labels/labels_select_widget/constants';
+import LabelsSelectWidget from './components/labels/labels_select_widget/labels_select_root.vue';
import IssuableLockForm from './components/lock/issuable_lock_form.vue';
+import MilestoneDropdown from './components/milestone/milestone_dropdown.vue';
+import MoveIssuesButton from './components/move/move_issues_button.vue';
+import SidebarParticipantsWidget from './components/participants/sidebar_participants_widget.vue';
+import SidebarReferenceWidget from './components/copy/sidebar_reference_widget.vue';
import SidebarReviewers from './components/reviewers/sidebar_reviewers.vue';
import SidebarReviewersInputs from './components/reviewers/sidebar_reviewers_inputs.vue';
import SidebarSeverity from './components/severity/sidebar_severity.vue';
+import SidebarDropdownWidget from './components/sidebar_dropdown_widget.vue';
+import StatusDropdown from './components/status/status_dropdown.vue';
import SidebarSubscriptionsWidget from './components/subscriptions/sidebar_subscriptions_widget.vue';
+import SubscriptionsDropdown from './components/subscriptions/subscriptions_dropdown.vue';
import SidebarTimeTracking from './components/time_tracking/sidebar_time_tracking.vue';
+import SidebarTodoWidget from './components/todo_toggle/sidebar_todo_widget.vue';
import { IssuableAttributeType } from './constants';
-import SidebarMoveIssue from './lib/sidebar_move_issue';
import CrmContacts from './components/crm_contacts/crm_contacts.vue';
+import SidebarMoveIssue from './lib/sidebar_move_issue';
+import trackShowInviteMemberLink from './track_invite_members';
Vue.use(Translate);
Vue.use(VueApollo);
@@ -540,7 +544,15 @@ function mountSidebarSubscriptionsWidget() {
function mountSidebarTimeTracking() {
const el = document.querySelector('.js-sidebar-time-tracking-root');
- const { id, iid, fullPath, issuableType, timeTrackingLimitToHours } = getSidebarOptions();
+
+ const {
+ id,
+ iid,
+ fullPath,
+ issuableType,
+ timeTrackingLimitToHours,
+ canCreateTimelogs,
+ } = getSidebarOptions();
if (!el) {
return null;
@@ -558,6 +570,7 @@ function mountSidebarTimeTracking() {
issuableId: id.toString(),
issuableIid: iid.toString(),
limitToHours: timeTrackingLimitToHours,
+ canAddTimeEntries: canCreateTimelogs,
},
}),
});
@@ -635,6 +648,59 @@ function mountCopyEmailToClipboard() {
});
}
+export function mountMoveIssuesButton() {
+ const el = document.querySelector('.js-move-issues');
+
+ if (!el) {
+ return null;
+ }
+
+ Vue.use(VueApollo);
+
+ return new Vue({
+ el,
+ name: 'MoveIssuesRoot',
+ apolloProvider: new VueApollo({
+ defaultClient: gqlClient,
+ }),
+ render: (createElement) =>
+ createElement(MoveIssuesButton, {
+ props: {
+ projectFullPath: el.dataset.projectFullPath,
+ projectsFetchPath: el.dataset.projectsFetchPath,
+ },
+ }),
+ });
+}
+
+export function mountStatusDropdown() {
+ const el = document.querySelector('.js-status-dropdown');
+
+ if (!el) {
+ return null;
+ }
+
+ return new Vue({
+ el,
+ name: 'StatusDropdownRoot',
+ render: (createElement) => createElement(StatusDropdown),
+ });
+}
+
+export function mountSubscriptionsDropdown() {
+ const el = document.querySelector('.js-subscriptions-dropdown');
+
+ if (!el) {
+ return null;
+ }
+
+ return new Vue({
+ el,
+ name: 'SubscriptionsDropdownRoot',
+ render: (createElement) => createElement(SubscriptionsDropdown),
+ });
+}
+
const isAssigneesWidgetShown =
(isInIssuePage() || isInDesignPage() || isInMRPage()) && gon.features.issueAssigneesWidget;
diff --git a/app/assets/javascripts/sidebar/queries/create_timelog.mutation.graphql b/app/assets/javascripts/sidebar/queries/create_timelog.mutation.graphql
new file mode 100644
index 00000000000..a8692387a46
--- /dev/null
+++ b/app/assets/javascripts/sidebar/queries/create_timelog.mutation.graphql
@@ -0,0 +1,17 @@
+#import "~/graphql_shared/fragments/issue_time_tracking.fragment.graphql"
+#import "~/graphql_shared/fragments/merge_request_time_tracking.fragment.graphql"
+
+mutation createTimelog($input: TimelogCreateInput!) {
+ timelogCreate(input: $input) {
+ errors
+ timelog {
+ id
+ issue {
+ ...IssueTimeTrackingFragment
+ }
+ mergeRequest {
+ ...MergeRequestTimeTrackingFragment
+ }
+ }
+ }
+}
diff --git a/app/assets/javascripts/sidebar/components/time_tracking/graphql/mutations/delete_timelog.mutation.graphql b/app/assets/javascripts/sidebar/queries/delete_timelog.mutation.graphql
index 6e916893b5a..6e916893b5a 100644
--- a/app/assets/javascripts/sidebar/components/time_tracking/graphql/mutations/delete_timelog.mutation.graphql
+++ b/app/assets/javascripts/sidebar/queries/delete_timelog.mutation.graphql
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/queries/get_alert_assignees.query.graphql b/app/assets/javascripts/sidebar/queries/get_alert_assignees.query.graphql
index bb6c7181e5c..171eca50eab 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/queries/get_alert_assignees.query.graphql
+++ b/app/assets/javascripts/sidebar/queries/get_alert_assignees.query.graphql
@@ -9,6 +9,7 @@ query alertAssignees(
workspace: project(fullPath: $fullPath) {
id
issuable: alertManagementAlert(domain: $domain, iid: $iid) {
+ id
iid
assignees {
nodes {
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/queries/get_issue_assignees.query.graphql b/app/assets/javascripts/sidebar/queries/get_issue_assignees.query.graphql
index 4af07366a6d..4af07366a6d 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/queries/get_issue_assignees.query.graphql
+++ b/app/assets/javascripts/sidebar/queries/get_issue_assignees.query.graphql
diff --git a/app/assets/javascripts/sidebar/components/crm_contacts/queries/get_issue_crm_contacts.query.graphql b/app/assets/javascripts/sidebar/queries/get_issue_crm_contacts.query.graphql
index 30a0af10d56..30a0af10d56 100644
--- a/app/assets/javascripts/sidebar/components/crm_contacts/queries/get_issue_crm_contacts.query.graphql
+++ b/app/assets/javascripts/sidebar/queries/get_issue_crm_contacts.query.graphql
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/queries/get_issue_participants.query.graphql b/app/assets/javascripts/sidebar/queries/get_issue_participants.query.graphql
index eae5e96ac46..eae5e96ac46 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/queries/get_issue_participants.query.graphql
+++ b/app/assets/javascripts/sidebar/queries/get_issue_participants.query.graphql
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/queries/get_issue_timelogs.query.graphql b/app/assets/javascripts/sidebar/queries/get_issue_timelogs.query.graphql
index b127b8ec5a9..b127b8ec5a9 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/queries/get_issue_timelogs.query.graphql
+++ b/app/assets/javascripts/sidebar/queries/get_issue_timelogs.query.graphql
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/queries/get_merge_request_reviewers.query.graphql b/app/assets/javascripts/sidebar/queries/get_merge_request_reviewers.query.graphql
index f087ca6c982..f087ca6c982 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/queries/get_merge_request_reviewers.query.graphql
+++ b/app/assets/javascripts/sidebar/queries/get_merge_request_reviewers.query.graphql
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/queries/get_mr_assignees.query.graphql b/app/assets/javascripts/sidebar/queries/get_mr_assignees.query.graphql
index f70cd723f2e..f70cd723f2e 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/queries/get_mr_assignees.query.graphql
+++ b/app/assets/javascripts/sidebar/queries/get_mr_assignees.query.graphql
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/queries/get_mr_participants.query.graphql b/app/assets/javascripts/sidebar/queries/get_mr_participants.query.graphql
index 2781ac71f31..2781ac71f31 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/queries/get_mr_participants.query.graphql
+++ b/app/assets/javascripts/sidebar/queries/get_mr_participants.query.graphql
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/queries/get_mr_timelogs.query.graphql b/app/assets/javascripts/sidebar/queries/get_mr_timelogs.query.graphql
index 17f548b44b5..17f548b44b5 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/queries/get_mr_timelogs.query.graphql
+++ b/app/assets/javascripts/sidebar/queries/get_mr_timelogs.query.graphql
diff --git a/app/assets/javascripts/sidebar/components/crm_contacts/queries/issue_crm_contacts.fragment.graphql b/app/assets/javascripts/sidebar/queries/issue_crm_contacts.fragment.graphql
index 750e1f1d1af..750e1f1d1af 100644
--- a/app/assets/javascripts/sidebar/components/crm_contacts/queries/issue_crm_contacts.fragment.graphql
+++ b/app/assets/javascripts/sidebar/queries/issue_crm_contacts.fragment.graphql
diff --git a/app/assets/javascripts/sidebar/components/crm_contacts/queries/issue_crm_contacts.subscription.graphql b/app/assets/javascripts/sidebar/queries/issue_crm_contacts.subscription.graphql
index f3b6e4ec06f..f3b6e4ec06f 100644
--- a/app/assets/javascripts/sidebar/components/crm_contacts/queries/issue_crm_contacts.subscription.graphql
+++ b/app/assets/javascripts/sidebar/queries/issue_crm_contacts.subscription.graphql
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/queries/merge_request_reviewers.subscription.graphql b/app/assets/javascripts/sidebar/queries/merge_request_reviewers.subscription.graphql
index a1b16b378b3..a1b16b378b3 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/queries/merge_request_reviewers.subscription.graphql
+++ b/app/assets/javascripts/sidebar/queries/merge_request_reviewers.subscription.graphql
diff --git a/app/assets/javascripts/issuable/bulk_update_sidebar/components/graphql/mutations/move_issue.mutation.graphql b/app/assets/javascripts/sidebar/queries/move_issue.mutation.graphql
index d350072425b..d350072425b 100644
--- a/app/assets/javascripts/issuable/bulk_update_sidebar/components/graphql/mutations/move_issue.mutation.graphql
+++ b/app/assets/javascripts/sidebar/queries/move_issue.mutation.graphql
diff --git a/app/assets/javascripts/sidebar/components/severity/graphql/mutations/update_issuable_severity.mutation.graphql b/app/assets/javascripts/sidebar/queries/update_issuable_severity.mutation.graphql
index c9d36dfdb67..c9d36dfdb67 100644
--- a/app/assets/javascripts/sidebar/components/severity/graphql/mutations/update_issuable_severity.mutation.graphql
+++ b/app/assets/javascripts/sidebar/queries/update_issuable_severity.mutation.graphql
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/queries/update_issue_assignees.mutation.graphql b/app/assets/javascripts/sidebar/queries/update_issue_assignees.mutation.graphql
index 24de5ea4fe3..24de5ea4fe3 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/queries/update_issue_assignees.mutation.graphql
+++ b/app/assets/javascripts/sidebar/queries/update_issue_assignees.mutation.graphql
diff --git a/app/assets/javascripts/sidebar/components/lock/mutations/update_issue_lock.mutation.graphql b/app/assets/javascripts/sidebar/queries/update_issue_lock.mutation.graphql
index cb9ee6abc9b..cb9ee6abc9b 100644
--- a/app/assets/javascripts/sidebar/components/lock/mutations/update_issue_lock.mutation.graphql
+++ b/app/assets/javascripts/sidebar/queries/update_issue_lock.mutation.graphql
diff --git a/app/assets/javascripts/sidebar/components/lock/mutations/update_merge_request_lock.mutation.graphql b/app/assets/javascripts/sidebar/queries/update_merge_request_lock.mutation.graphql
index 11eb3611006..11eb3611006 100644
--- a/app/assets/javascripts/sidebar/components/lock/mutations/update_merge_request_lock.mutation.graphql
+++ b/app/assets/javascripts/sidebar/queries/update_merge_request_lock.mutation.graphql
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/queries/update_mr_assignees.mutation.graphql b/app/assets/javascripts/sidebar/queries/update_mr_assignees.mutation.graphql
index 5fec2ccbdfb..5fec2ccbdfb 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/queries/update_mr_assignees.mutation.graphql
+++ b/app/assets/javascripts/sidebar/queries/update_mr_assignees.mutation.graphql
diff --git a/app/assets/javascripts/sidebar/sidebar_mediator.js b/app/assets/javascripts/sidebar/sidebar_mediator.js
index 912f0fdcbef..c6a66ab2275 100644
--- a/app/assets/javascripts/sidebar/sidebar_mediator.js
+++ b/app/assets/javascripts/sidebar/sidebar_mediator.js
@@ -1,9 +1,9 @@
-import Store from '~/sidebar/stores/sidebar_store';
import { createAlert } from '~/flash';
import { __ } from '~/locale';
import toast from '~/vue_shared/plugins/global_toast';
-import { visitUrl } from '../lib/utils/url_utility';
+import { visitUrl } from '~/lib/utils/url_utility';
import Service from './services/sidebar_service';
+import Store from './stores/sidebar_store';
export default class SidebarMediator {
constructor(options) {
@@ -31,6 +31,9 @@ export default class SidebarMediator {
assignYourself() {
this.store.addAssignee(this.store.currentUser);
}
+ addSelfReview() {
+ this.store.addReviewer(this.store.currentUser);
+ }
async saveAssignees(field) {
const selected = this.store.assignees.map((u) => u.id);
@@ -56,12 +59,14 @@ export default class SidebarMediator {
}
async saveReviewers(field) {
- const selected = this.store.reviewers.map((u) => u.id);
+ const selectedReviewers = this.store.reviewers;
+ const selectedIds = selectedReviewers.map((u) => u.id);
+ const suggestedSelectedIds = selectedReviewers.filter((u) => u.suggested).map((u) => u.id);
// If there are no ids, that means we have to unassign (which is id = 0)
// And it only accepts an array, hence [0]
- const reviewers = selected.length === 0 ? [0] : selected;
- const data = { reviewer_ids: reviewers };
+ const reviewers = selectedIds.length === 0 ? [0] : selectedIds;
+ const data = { reviewer_ids: reviewers, suggested_reviewer_ids: suggestedSelectedIds };
try {
const res = await this.service.update(field, data);
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/todo_toggle/utils.js b/app/assets/javascripts/sidebar/utils.js
index 098ab72dfb5..6b90fb80abf 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/todo_toggle/utils.js
+++ b/app/assets/javascripts/sidebar/utils.js
@@ -1,4 +1,7 @@
-import { __ } from '~/locale';
+import { __, s__ } from '~/locale';
+import { STATUS_LABELS } from './constants';
+
+export const getStatusLabel = (status) => STATUS_LABELS[status] ?? s__('IncidentManagement|None');
export const todoLabel = (hasTodo) => {
return hasTodo ? __('Mark as done') : __('Add a to do');
diff --git a/app/assets/javascripts/snippets/components/snippet_description_view.vue b/app/assets/javascripts/snippets/components/snippet_description_view.vue
index 737a131ce7c..ab2ff6e0ef8 100644
--- a/app/assets/javascripts/snippets/components/snippet_description_view.vue
+++ b/app/assets/javascripts/snippets/components/snippet_description_view.vue
@@ -1,5 +1,5 @@
<script>
-import { GlSafeHtmlDirective as SafeHtml } from '@gitlab/ui';
+import SafeHtml from '~/vue_shared/directives/safe_html';
import MarkdownFieldView from '~/vue_shared/components/markdown/field_view.vue';
export default {
diff --git a/app/assets/javascripts/surveys/merge_request_experience/app.vue b/app/assets/javascripts/surveys/merge_request_experience/app.vue
index df114c27908..6e90ad2e0fd 100644
--- a/app/assets/javascripts/surveys/merge_request_experience/app.vue
+++ b/app/assets/javascripts/surveys/merge_request_experience/app.vue
@@ -1,6 +1,7 @@
<script>
-import { GlButton, GlSprintf, GlSafeHtmlDirective, GlTooltipDirective } from '@gitlab/ui';
+import { GlButton, GlSprintf, GlTooltipDirective } from '@gitlab/ui';
import gitlabLogo from '@gitlab/svgs/dist/illustrations/gitlab_logo.svg';
+import SafeHtml from '~/vue_shared/directives/safe_html';
import { s__, __ } from '~/locale';
import UserCalloutDismisser from '~/vue_shared/components/user_callout_dismisser.vue';
import SatisfactionRate from '~/surveys/components/satisfaction_rate.vue';
@@ -30,7 +31,7 @@ export default {
SatisfactionRate,
},
directives: {
- safeHtml: GlSafeHtmlDirective,
+ SafeHtml,
tooltip: GlTooltipDirective,
},
mixins: [Tracking.mixin()],
diff --git a/app/assets/javascripts/tags/init_new_tag_ref_selector.js b/app/assets/javascripts/tags/init_new_tag_ref_selector.js
new file mode 100644
index 00000000000..11c7516f16c
--- /dev/null
+++ b/app/assets/javascripts/tags/init_new_tag_ref_selector.js
@@ -0,0 +1,23 @@
+import Vue from 'vue';
+import RefSelector from '~/ref/components/ref_selector.vue';
+
+export default function initNewTagRefSelector() {
+ const el = document.querySelector('.js-new-tag-ref-selector');
+
+ if (el) {
+ const { projectId, defaultBranchName, hiddenInputName } = el.dataset;
+ // eslint-disable-next-line no-new
+ new Vue({
+ el,
+ render(createComponent) {
+ return createComponent(RefSelector, {
+ props: {
+ value: defaultBranchName,
+ name: hiddenInputName,
+ projectId,
+ },
+ });
+ },
+ });
+ }
+}
diff --git a/app/assets/javascripts/terms/components/app.vue b/app/assets/javascripts/terms/components/app.vue
index a54a198faed..eecf32f83df 100644
--- a/app/assets/javascripts/terms/components/app.vue
+++ b/app/assets/javascripts/terms/components/app.vue
@@ -1,13 +1,13 @@
<script>
-import $ from 'jquery';
-import { GlButton, GlIntersectionObserver, GlSafeHtmlDirective as SafeHtml } from '@gitlab/ui';
+import { GlButton, GlIntersectionObserver } from '@gitlab/ui';
+import SafeHtml from '~/vue_shared/directives/safe_html';
import { FLASH_TYPES, FLASH_CLOSED_EVENT } from '~/flash';
import { isLoggedIn } from '~/lib/utils/common_utils';
import { __ } from '~/locale';
import csrf from '~/lib/utils/csrf';
-import '~/behaviors/markdown/render_gfm';
import { trackTrialAcceptTerms } from '~/google_tag_manager';
+import { renderGFM } from '~/behaviors/markdown/render_gfm';
export default {
name: 'TermsApp',
@@ -54,7 +54,7 @@ export default {
},
methods: {
renderGFM() {
- $(this.$refs.gfmContainer).renderGFM();
+ renderGFM(this.$refs.gfmContainer);
},
handleBottomReached() {
this.acceptDisabled = false;
@@ -81,7 +81,7 @@ export default {
<template>
<div>
- <div class="gl-card-body gl-relative gl-pb-0 gl-px-0" data-qa-selector="terms_content">
+ <div class="gl-relative gl-pb-0 gl-px-0" data-qa-selector="terms_content">
<div
class="terms-fade gl-absolute gl-left-5 gl-right-5 gl-bottom-0 gl-h-11 gl-pointer-events-none"
></div>
@@ -96,7 +96,7 @@ export default {
</gl-intersection-observer>
</div>
</div>
- <div v-if="isLoggedIn" class="gl-card-footer gl-display-flex gl-justify-content-end">
+ <div v-if="isLoggedIn" class="gl-display-flex gl-justify-content-end">
<form v-if="permissions.canDecline" method="post" :action="paths.decline">
<gl-button type="submit">{{ $options.i18n.decline }}</gl-button>
<input :value="$options.csrf.token" type="hidden" name="authenticity_token" />
diff --git a/app/assets/javascripts/terraform/components/init_command_modal.vue b/app/assets/javascripts/terraform/components/init_command_modal.vue
index 2cb10d4ae23..0d8a883972f 100644
--- a/app/assets/javascripts/terraform/components/init_command_modal.vue
+++ b/app/assets/javascripts/terraform/components/init_command_modal.vue
@@ -39,11 +39,13 @@ export default {
},
methods: {
getModalInfoCopyStr() {
+ const stateNameEncoded = encodeURIComponent(this.stateName);
+
return `export GITLAB_ACCESS_TOKEN=<YOUR-ACCESS-TOKEN>
terraform init \\
- -backend-config="address=${this.terraformApiUrl}/${this.stateName}" \\
- -backend-config="lock_address=${this.terraformApiUrl}/${this.stateName}/lock" \\
- -backend-config="unlock_address=${this.terraformApiUrl}/${this.stateName}/lock" \\
+ -backend-config="address=${this.terraformApiUrl}/${stateNameEncoded}" \\
+ -backend-config="lock_address=${this.terraformApiUrl}/${stateNameEncoded}/lock" \\
+ -backend-config="unlock_address=${this.terraformApiUrl}/${stateNameEncoded}/lock" \\
-backend-config="username=${this.username}" \\
-backend-config="password=$GITLAB_ACCESS_TOKEN" \\
-backend-config="lock_method=POST" \\
diff --git a/app/assets/javascripts/tooltips/components/tooltips.vue b/app/assets/javascripts/tooltips/components/tooltips.vue
index 1ad18508294..a4dc783f1e4 100644
--- a/app/assets/javascripts/tooltips/components/tooltips.vue
+++ b/app/assets/javascripts/tooltips/components/tooltips.vue
@@ -1,6 +1,7 @@
<script>
-import { GlTooltip, GlSafeHtmlDirective as SafeHtml } from '@gitlab/ui';
+import { GlTooltip } from '@gitlab/ui';
import { uniqueId } from 'lodash';
+import SafeHtml from '~/vue_shared/directives/safe_html';
const getTooltipTitle = (element) => {
return element.getAttribute('title') || element.dataset.title;
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/approvals/approvals.vue b/app/assets/javascripts/vue_merge_request_widget/components/approvals/approvals.vue
index 2cfeb7a4bcb..eb93f42e2f3 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/approvals/approvals.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/approvals/approvals.vue
@@ -189,8 +189,11 @@ export default {
.then((data) => {
this.mr.setApprovals(data);
- eventHub.$emit('MRWidgetUpdateRequested');
- eventHub.$emit('ApprovalUpdated');
+ if (!window.gon?.features?.realtimeMrStatusChange) {
+ eventHub.$emit('MRWidgetUpdateRequested');
+ eventHub.$emit('ApprovalUpdated');
+ }
+
this.$emit('updated');
})
.catch(errFn)
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/deployment/memory_usage.vue b/app/assets/javascripts/vue_merge_request_widget/components/deployment/memory_usage.vue
index 1256b3a8e52..c7d34d45f06 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/deployment/memory_usage.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/deployment/memory_usage.vue
@@ -1,7 +1,7 @@
<script>
import { GlLoadingIcon, GlSprintf, GlLink } from '@gitlab/ui';
import { backOff } from '~/lib/utils/common_utils';
-import statusCodes from '~/lib/utils/http_status';
+import { HTTP_STATUS_NO_CONTENT } from '~/lib/utils/http_status';
import { bytesToMiB } from '~/lib/utils/number_utils';
import { s__ } from '~/locale';
import MemoryGraph from '~/vue_shared/components/memory_graph.vue';
@@ -107,7 +107,7 @@ export default {
backOff((next, stop) => {
MRWidgetService.fetchMetrics(this.metricsUrl)
.then((res) => {
- if (res.status === statusCodes.NO_CONTENT) {
+ if (res.status === HTTP_STATUS_NO_CONTENT) {
this.backOffRequestCounter += 1;
/* eslint-disable no-unused-expressions */
this.backOffRequestCounter < 3 ? next() : stop(res);
@@ -118,7 +118,7 @@ export default {
.catch(stop);
})
.then((res) => {
- if (res.status === statusCodes.NO_CONTENT) {
+ if (res.status === HTTP_STATUS_NO_CONTENT) {
return res;
}
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/extensions/base.vue b/app/assets/javascripts/vue_merge_request_widget/components/extensions/base.vue
index 3d03dbd9db3..e8cc9b2eb2a 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/extensions/base.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/extensions/base.vue
@@ -1,12 +1,7 @@
<script>
-import {
- GlButton,
- GlLoadingIcon,
- GlSafeHtmlDirective,
- GlTooltipDirective,
- GlIntersectionObserver,
-} from '@gitlab/ui';
+import { GlButton, GlLoadingIcon, GlTooltipDirective, GlIntersectionObserver } from '@gitlab/ui';
import * as Sentry from '@sentry/browser';
+import SafeHtml from '~/vue_shared/directives/safe_html';
import { DynamicScroller, DynamicScrollerItem } from 'vendor/vue-virtual-scroller';
import { sprintf, s__, __ } from '~/locale';
import Poll from '~/lib/utils/poll';
@@ -40,7 +35,7 @@ export default {
StateContainer,
},
directives: {
- SafeHtml: GlSafeHtmlDirective,
+ SafeHtml,
GlTooltip: GlTooltipDirective,
},
data() {
@@ -323,19 +318,23 @@ export default {
@mouseup="onRowMouseUp"
>
<div
+ :class="{ 'gl-h-full': isLoadingSummary }"
class="media-body gl-display-flex gl-flex-direction-row! gl-w-full"
data-testid="widget-extension-top-level"
>
- <div class="gl-flex-grow-1" data-testid="widget-extension-top-level-summary">
+ <div
+ class="gl-flex-grow-1 gl-display-flex gl-align-items-center"
+ data-testid="widget-extension-top-level-summary"
+ >
<template v-if="isLoadingSummary">{{ widgetLoadingText }}</template>
<template v-else-if="hasFetchError">{{ widgetErrorText }}</template>
- <div v-else>
+ <template v-else>
<span v-safe-html="hydratedSummary.subject"></span>
<template v-if="hydratedSummary.meta">
<br />
<span v-safe-html="hydratedSummary.meta" class="gl-font-sm"></span>
</template>
- </div>
+ </template>
</div>
<actions
:widget="$options.label || $options.name"
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/extensions/child_content.vue b/app/assets/javascripts/vue_merge_request_widget/components/extensions/child_content.vue
index a10e5efa0e7..fa369d23b6c 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/extensions/child_content.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/extensions/child_content.vue
@@ -1,6 +1,7 @@
<script>
-import { GlBadge, GlLink, GlSafeHtmlDirective, GlModalDirective } from '@gitlab/ui';
+import { GlBadge, GlLink, GlModalDirective } from '@gitlab/ui';
import { isArray } from 'lodash';
+import SafeHtml from '~/vue_shared/directives/safe_html';
import Actions from '../action_buttons.vue';
import StatusIcon from './status_icon.vue';
import { generateText } from './utils';
@@ -14,7 +15,7 @@ export default {
Actions,
},
directives: {
- SafeHtml: GlSafeHtmlDirective,
+ SafeHtml,
GlModal: GlModalDirective,
},
props: {
@@ -97,7 +98,12 @@ export default {
<div v-if="data.supportingText">
<p v-safe-html="generateText(data.supportingText)" class="gl-m-0"></p>
</div>
- <gl-badge v-if="data.badge" :variant="data.badge.variant || 'info'">
+ <gl-badge
+ v-if="data.badge"
+ :variant="data.badge.variant || 'info'"
+ size="sm"
+ class="gl-ml-2"
+ >
{{ data.badge.text }}
</gl-badge>
</div>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_author.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_author.vue
index f71b1fbc539..79ea2624ec5 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_author.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_author.vue
@@ -1,8 +1,11 @@
<script>
-import { GlTooltipDirective } from '@gitlab/ui';
+import { GlTooltipDirective, GlLink } from '@gitlab/ui';
export default {
name: 'MrWidgetAuthor',
+ components: {
+ GlLink,
+ },
directives: {
GlTooltip: GlTooltipDirective,
},
@@ -28,13 +31,16 @@ export default {
};
</script>
<template>
- <a
+ <gl-link
v-gl-tooltip
:href="authorUrl"
:title="showAuthorName ? null : author.name"
- class="author-link inline"
+ class="mr-widget-author"
>
- <img :src="avatarUrl" class="avatar avatar-inline s16" />
- <span v-if="showAuthorName" class="author">{{ author.name }}</span>
- </a>
+ <img :src="avatarUrl" :alt="author.name" class="avatar avatar-inline s16" /><span
+ v-if="showAuthorName"
+ class="author"
+ >{{ author.name }}</span
+ >
+ </gl-link>
</template>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue
index 97c6de37054..d8a361066f4 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue
@@ -7,8 +7,8 @@ import {
GlSprintf,
GlTooltip,
GlTooltipDirective,
- GlSafeHtmlDirective,
} from '@gitlab/ui';
+import SafeHtml from '~/vue_shared/directives/safe_html';
import { s__, n__ } from '~/locale';
import CiIcon from '~/vue_shared/components/ci_icon.vue';
import PipelineArtifacts from '~/pipelines/components/pipelines_list/pipelines_artifacts.vue';
@@ -33,7 +33,7 @@ export default {
},
directives: {
GlTooltip: GlTooltipDirective,
- SafeHtml: GlSafeHtmlDirective,
+ SafeHtml,
},
props: {
pipeline: {
@@ -190,7 +190,7 @@ export default {
</template>
<template v-else-if="hasPipeline">
<a :href="status.details_path" class="gl-align-self-center gl-mr-3">
- <ci-icon :status="status" :size="24" />
+ <ci-icon :status="status" :size="24" class="gl-display-flex" />
</a>
<div class="ci-widget-container d-flex">
<div class="ci-widget-content">
@@ -277,9 +277,9 @@ export default {
v-if="pipeline.details.stages"
:downstream-pipelines="pipeline.triggered"
:is-merge-train="isMergeTrain"
+ :pipeline-path="pipeline.path"
:stages="pipeline.details.stages"
:upstream-pipeline="pipeline.triggered_by"
- stages-class="mr-widget-pipeline-stages"
/>
<pipeline-artifacts :pipeline-id="pipeline.id" :artifacts="artifacts" class="gl-ml-3" />
</span>
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 870972156c5..1fd1e264c25 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,6 @@
<script>
-import { GlSafeHtmlDirective as SafeHtml, GlLink } from '@gitlab/ui';
+import { GlLink } from '@gitlab/ui';
+import SafeHtml from '~/vue_shared/directives/safe_html';
import { s__, n__ } from '~/locale';
export default {
@@ -54,16 +55,16 @@ export default {
</script>
<template>
<section>
- <p v-if="relatedLinks.closing" class="gl-display-inline gl-m-0">
+ <p v-if="relatedLinks.closing" class="gl-display-inline gl-m-0 gl-font-sm!">
{{ closesText }}
<span v-safe-html="relatedLinks.closing"></span>
</p>
- <p v-if="relatedLinks.mentioned" class="gl-display-inline gl-m-0">
+ <p v-if="relatedLinks.mentioned" class="gl-display-inline gl-m-0 gl-font-sm!">
<span v-if="relatedLinks.closing">&middot;</span>
{{ n__('mrWidget|Mentions issue', 'mrWidget|Mentions issues', relatedLinks.mentionedCount) }}
<span v-safe-html="relatedLinks.mentioned"></span>
</p>
- <p v-if="shouldShowAssignToMeLink" class="gl-display-inline gl-m-0">
+ <p v-if="shouldShowAssignToMeLink" class="gl-display-inline gl-m-0 gl-font-sm!">
<span>
<gl-link rel="nofollow" data-method="post" :href="relatedLinks.assignToMe">{{
assignIssueText
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/state_container.vue b/app/assets/javascripts/vue_merge_request_widget/components/state_container.vue
index 66e33a08a12..9a3555d3e11 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/state_container.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/state_container.vue
@@ -54,7 +54,7 @@ export default {
<template>
<div
- class="mr-widget-body media mr-widget-body-line-height-1 gl-line-height-normal"
+ class="mr-widget-body media gl-display-flex gl-align-items-center"
:class="wrapperClasses"
v-on="$listeners"
>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/merge_checks_failed.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/merge_checks_failed.vue
index 38b99dae264..e5688091cc7 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/merge_checks_failed.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/merge_checks_failed.vue
@@ -1,6 +1,6 @@
<script>
import { s__ } from '~/locale';
-import StatusIcon from '../mr_widget_status_icon.vue';
+import StateContainer from '../state_container.vue';
import { DETAILED_MERGE_STATUS } from '../../constants';
export default {
@@ -12,7 +12,7 @@ export default {
externalStatusChecksFailed: s__('mrWidget|Merge blocked: all status checks must pass.'),
},
components: {
- StatusIcon,
+ StateContainer,
},
props: {
mr: {
@@ -37,10 +37,11 @@ export default {
</script>
<template>
- <div class="mr-widget-body media gl-flex-wrap">
- <status-icon status="failed" />
- <p class="media-body gl-m-0! gl-font-weight-bold gl-text-black-normal!">
+ <state-container :mr="mr" status="failed">
+ <span
+ class="gl-ml-3 gl-font-weight-bold gl-w-100 gl-flex-grow-1 gl-md-mr-3 gl-ml-0! gl-text-body!"
+ >
{{ failedText }}
- </p>
- </div>
+ </span>
+ </state-container>
</template>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_closed.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_closed.vue
index 806f8f939a6..6bcf88713a5 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_closed.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_closed.vue
@@ -1,7 +1,17 @@
<script>
+import api from '~/api';
+import showGlobalToast from '~/vue_shared/plugins/global_toast';
+
import MrWidgetAuthorTime from '../mr_widget_author_time.vue';
import StateContainer from '../state_container.vue';
+import {
+ MR_WIDGET_CLOSED_REOPEN,
+ MR_WIDGET_CLOSED_REOPENING,
+ MR_WIDGET_CLOSED_RELOADING,
+ MR_WIDGET_CLOSED_REOPEN_FAILURE,
+} from '../../i18n';
+
export default {
name: 'MRWidgetClosed',
components: {
@@ -14,10 +24,62 @@ export default {
required: true,
},
},
+ data() {
+ return {
+ isPending: false,
+ isReloading: false,
+ };
+ },
+ computed: {
+ reopenText() {
+ let text = MR_WIDGET_CLOSED_REOPEN;
+
+ if (this.isPending) {
+ text = MR_WIDGET_CLOSED_REOPENING;
+ } else if (this.isReloading) {
+ text = MR_WIDGET_CLOSED_RELOADING;
+ }
+
+ return text;
+ },
+ actions() {
+ if (!window.gon?.current_user_id) {
+ return [];
+ }
+
+ return [
+ {
+ text: this.reopenText,
+ loading: this.isPending || this.isReloading,
+ onClick: this.reopen,
+ testId: 'extension-actions-reopen-button',
+ },
+ ];
+ },
+ },
+ methods: {
+ reopen() {
+ this.isPending = true;
+
+ api
+ .updateMergeRequest(this.mr.targetProjectId, this.mr.iid, { state_event: 'reopen' })
+ .then(() => {
+ this.isReloading = true;
+
+ window.location.reload();
+ })
+ .catch(() => {
+ showGlobalToast(MR_WIDGET_CLOSED_REOPEN_FAILURE);
+ })
+ .finally(() => {
+ this.isPending = false;
+ });
+ },
+ },
};
</script>
<template>
- <state-container :mr="mr" status="closed">
+ <state-container :mr="mr" status="closed" :actions="actions">
<mr-widget-author-time
:action-text="s__('mrWidget|Closed by')"
:author="mr.metrics.closedBy"
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 4902c9b45e8..850a4e2fd56 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,5 +1,6 @@
<script>
-import { GlButton, GlSprintf, GlLink, GlSafeHtmlDirective } from '@gitlab/ui';
+import { GlButton, GlSprintf, GlLink } from '@gitlab/ui';
+import SafeHtml from '~/vue_shared/directives/safe_html';
import emptyStateSVG from 'icons/_mr_widget_empty_state.svg';
import api from '~/api';
import { helpPagePath } from '~/helpers/help_page_helper';
@@ -12,7 +13,7 @@ export default {
GlLink,
},
directives: {
- SafeHtml: GlSafeHtmlDirective,
+ SafeHtml,
},
props: {
mr: {
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 c54672cd0f8..23b163e2c6a 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
@@ -20,6 +20,8 @@ import simplePoll from '~/lib/utils/simple_poll';
import { __, s__, n__ } from '~/locale';
import SmartInterval from '~/smart_interval';
import { helpPagePath } from '~/helpers/help_page_helper';
+import { convertToGraphQLId } from '~/graphql_shared/utils';
+import readyToMergeSubscription from '~/vue_merge_request_widget/queries/states/ready_to_merge.subscription.graphql';
import {
AUTO_MERGE_STRATEGIES,
WARNING,
@@ -87,6 +89,31 @@ export default {
this.initPolling();
}
},
+ subscribeToMore: {
+ document() {
+ return readyToMergeSubscription;
+ },
+ skip() {
+ return !this.mr?.id || this.loading || !window.gon?.features?.realtimeMrStatusChange;
+ },
+ variables() {
+ return {
+ issuableId: convertToGraphQLId('MergeRequest', this.mr?.id),
+ };
+ },
+ updateQuery(
+ _,
+ {
+ subscriptionData: {
+ data: { mergeRequestMergeStatusUpdated },
+ },
+ },
+ ) {
+ if (mergeRequestMergeStatusUpdated) {
+ this.state = mergeRequestMergeStatusUpdated;
+ }
+ },
+ },
},
},
components: {
@@ -295,7 +322,7 @@ export default {
return this.mr.divergedCommitsCount > 0;
},
showMergeDetailsHeader() {
- return ['readyToMerge'].indexOf(this.mr.state) >= 0;
+ return !['readyToMerge'].includes(this.mr.state);
},
},
mounted() {
@@ -467,8 +494,9 @@ export default {
<template>
<div
+ :class="{ 'gl-bg-gray-10': mr.state !== 'closed' && mr.state !== 'merged' }"
data-testid="ready_to_merge_state"
- class="gl-border-t-1 gl-border-t-solid gl-border-gray-100 gl-bg-gray-10 gl-pl-7"
+ class="gl-border-t-1 gl-border-t-solid gl-border-gray-100 gl-pl-7"
>
<div v-if="loading" class="mr-widget-body">
<div class="gl-w-full mr-ready-to-merge-loader">
@@ -481,7 +509,9 @@ export default {
</div>
</div>
<template v-else>
- <div class="mr-widget-body mr-widget-body-ready-merge media mr-widget-body-line-height-1">
+ <div
+ class="mr-widget-body mr-widget-body-ready-merge media gl-display-flex gl-align-items-center"
+ >
<div class="media-body">
<div class="mr-widget-body-controls gl-display-flex gl-align-items-center gl-flex-wrap">
<template v-if="shouldShowMergeControls">
@@ -555,7 +585,19 @@ export default {
</li>
</ul>
</div>
- <div class="gl-w-full gl-text-gray-500 gl-mb-3 gl-md-mb-0 gl-md-pb-5">
+ <div
+ class="gl-w-full gl-text-gray-500 gl-mb-3 gl-md-mb-0 gl-md-pb-5 mr-widget-merge-details"
+ >
+ <template v-if="sourceHasDivergedFromTarget">
+ <gl-sprintf :message="$options.i18n.sourceDivergedFromTargetText">
+ <template #link>
+ <gl-link :href="mr.targetBranchPath">{{
+ $options.i18n.divergedCommits(mr.divergedCommitsCount)
+ }}</gl-link>
+ </template>
+ </gl-sprintf>
+ &middot;
+ </template>
<added-commit-message
:is-squash-enabled="squashBeforeMerge"
:is-fast-forward-enabled="!shouldShowMergeEdit"
@@ -631,7 +673,7 @@ export default {
class="gl-w-full gl-order-n1 mr-widget-merge-details"
data-qa-selector="merged_status_content"
>
- <p v-if="showMergeDetailsHeader" class="gl-mb-3 gl-text-gray-900">
+ <p v-if="showMergeDetailsHeader" class="gl-mb-2 gl-text-gray-900">
{{ __('Merge details') }}
</p>
<ul class="gl-pl-4 gl-mb-0 gl-ml-3 gl-text-gray-600">
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 074758e33b2..9f3748599dc 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/unresolved_discussions.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/unresolved_discussions.vue
@@ -26,7 +26,7 @@ export default {
<template>
<state-container :mr="mr" status="failed">
<span
- class="gl-ml-3 gl-font-weight-bold gl-w-100 gl-flex-grow-1 gl-md-mr-3 gl-ml-0! gl-text-body! gl-align-self-start"
+ class="gl-ml-3 gl-font-weight-bold gl-w-100 gl-flex-grow-1 gl-md-mr-3 gl-ml-0! gl-text-body!"
>
{{ s__('mrWidget|Merge blocked: all threads must be resolved.') }}
</span>
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 ef5be0fbfcd..01f9b4757a0 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
@@ -94,6 +94,7 @@ export default {
errors: [],
mergeRequest: {
__typename: 'MergeRequest',
+ id: this.mr.issuableId,
mergeableDiscussionsState: true,
title: this.mr.title,
draft: false,
@@ -111,7 +112,10 @@ export default {
}) => {
toast(__('Marked as ready. Merging is now allowed.'));
$('.merge-request .detail-page-description .title').text(title);
- eventHub.$emit('MRWidgetUpdateRequested');
+
+ if (!window.gon?.features?.realtimeMrStatusChange) {
+ eventHub.$emit('MRWidgetUpdateRequested');
+ }
},
)
.catch(() =>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/widget/action_buttons.vue b/app/assets/javascripts/vue_merge_request_widget/components/widget/action_buttons.vue
new file mode 100644
index 00000000000..6655af92a55
--- /dev/null
+++ b/app/assets/javascripts/vue_merge_request_widget/components/widget/action_buttons.vue
@@ -0,0 +1,134 @@
+<script>
+import { GlButton, GlDropdown, GlDropdownItem, GlTooltipDirective } from '@gitlab/ui';
+import { sprintf, __ } from '~/locale';
+
+export default {
+ components: {
+ GlButton,
+ GlDropdown,
+ GlDropdownItem,
+ },
+ directives: {
+ GlTooltip: GlTooltipDirective,
+ },
+ props: {
+ widget: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ tertiaryButtons: {
+ type: Array,
+ required: false,
+ default: () => [],
+ },
+ },
+ data: () => {
+ return {
+ timeout: null,
+ updatingTooltip: false,
+ };
+ },
+ computed: {
+ dropdownLabel() {
+ if (!this.widget) return undefined;
+
+ return sprintf(__('%{widget} options'), { widget: this.widget });
+ },
+ },
+ methods: {
+ onClickAction(action) {
+ this.$emit('clickedAction', action);
+
+ if (action.onClick) {
+ action.onClick();
+ }
+
+ if (action.tooltipOnClick) {
+ this.updatingTooltip = true;
+ this.$root.$emit('bv::show::tooltip', action.id);
+
+ clearTimeout(this.timeout);
+
+ this.timeout = setTimeout(() => {
+ this.updatingTooltip = false;
+ this.$root.$emit('bv::hide::tooltip', action.id);
+ }, 1000);
+ }
+ },
+ setTooltip(btn) {
+ if (this.updatingTooltip && btn.tooltipOnClick) {
+ return btn.tooltipOnClick;
+ }
+
+ return btn.tooltipText;
+ },
+ actionButtonQaSelector(btn) {
+ if (btn.dataQaSelector) {
+ return btn.dataQaSelector;
+ }
+ return 'mr_widget_extension_actions_button';
+ },
+ },
+};
+</script>
+
+<template>
+ <div class="gl-display-flex gl-align-items-flex-start">
+ <gl-dropdown
+ v-if="tertiaryButtons.length"
+ v-gl-tooltip
+ :title="__('Options')"
+ :text="dropdownLabel"
+ icon="ellipsis_v"
+ no-caret
+ category="tertiary"
+ right
+ lazy
+ text-sr-only
+ size="small"
+ toggle-class="gl-p-2!"
+ class="gl-display-block gl-md-display-none!"
+ >
+ <gl-dropdown-item
+ v-for="(btn, index) in tertiaryButtons"
+ :key="index"
+ :href="btn.href"
+ :target="btn.target"
+ :data-clipboard-text="btn.dataClipboardText"
+ :data-method="btn.dataMethod"
+ @click="onClickAction(btn)"
+ >
+ {{ btn.text }}
+ </gl-dropdown-item>
+ </gl-dropdown>
+ <template v-if="tertiaryButtons.length">
+ <gl-button
+ v-for="(btn, index) in tertiaryButtons"
+ :id="btn.id"
+ :key="index"
+ v-gl-tooltip.hover
+ :title="setTooltip(btn)"
+ :href="btn.href"
+ :target="btn.target"
+ :class="[{ 'gl-mr-3': index !== tertiaryButtons.length - 1 }, btn.class]"
+ :data-clipboard-text="btn.dataClipboardText"
+ :data-qa-selector="actionButtonQaSelector(btn)"
+ :data-method="btn.dataMethod"
+ :icon="btn.icon"
+ :data-testid="btn.testId || 'extension-actions-button'"
+ :variant="btn.variant || 'confirm'"
+ :loading="btn.loading"
+ :disabled="btn.loading"
+ category="tertiary"
+ size="small"
+ class="gl-display-none gl-md-display-block gl-float-left"
+ @click="onClickAction(btn)"
+ >
+ <template v-if="btn.text">
+ {{ btn.text }}
+ </template>
+ </gl-button>
+ </template>
+ </div>
+</template>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/widget/app.vue b/app/assets/javascripts/vue_merge_request_widget/components/widget/app.vue
index 2f52ac70833..18aa85484ea 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/widget/app.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/widget/app.vue
@@ -20,13 +20,14 @@ export default {
role="region"
:aria-label="__('Merge request reports')"
data-testid="mr-widget-app"
+ class="mr-widget-section"
>
<component
:is="widget"
v-for="(widget, index) in widgets"
:key="widget.name || index"
:mr="mr"
- :class="{ 'mr-widget-border-top': index === 0 }"
+ class="mr-widget-section"
/>
</section>
</template>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/widget/dynamic_content.vue b/app/assets/javascripts/vue_merge_request_widget/components/widget/dynamic_content.vue
index 4d66c75719b..cdce7c6625a 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/widget/dynamic_content.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/widget/dynamic_content.vue
@@ -1,8 +1,9 @@
<script>
-import { GlBadge, GlLink, GlSafeHtmlDirective } from '@gitlab/ui';
-import Actions from '../action_buttons.vue';
+import { GlBadge, GlLink } from '@gitlab/ui';
+import SafeHtml from '~/vue_shared/directives/safe_html';
import { generateText } from '../extensions/utils';
import ContentRow from './widget_content_row.vue';
+import Actions from './action_buttons.vue';
export default {
name: 'DynamicContent',
@@ -13,7 +14,7 @@ export default {
ContentRow,
},
directives: {
- SafeHtml: GlSafeHtmlDirective,
+ SafeHtml,
},
props: {
data: {
@@ -81,10 +82,8 @@ export default {
v-if="data.children && data.children.length > 0 && level === 2"
class="gl-m-0 gl-p-0 gl-list-style-none"
>
- <li>
+ <li v-for="(childData, index) in data.children" :key="childData.id || index">
<dynamic-content
- v-for="(childData, index) in data.children"
- :key="childData.id || index"
:data="childData"
:widget-name="widgetName"
:level="3"
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/widget/status_icon.vue b/app/assets/javascripts/vue_merge_request_widget/components/widget/status_icon.vue
index 181b8cfad9a..6d17ac98d7f 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/widget/status_icon.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/widget/status_icon.vue
@@ -48,9 +48,9 @@ export default {
:class="{
[iconClassNameText]: !isLoading,
[`mr-widget-status-icon-level-${level}`]: !isLoading,
- 'gl-mr-3': level === 1,
+ 'gl-w-6 gl-h-6 gl--flex-center': level === 1,
}"
- class="gl-relative gl-w-6 gl-h-6 gl-rounded-full gl--flex-center"
+ class="gl-relative gl-rounded-full gl-mr-3"
>
<gl-loading-icon v-if="isLoading" size="md" inline />
<gl-icon
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/widget/widget.vue b/app/assets/javascripts/vue_merge_request_widget/components/widget/widget.vue
index cea7fb8260a..cdf35033021 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/widget/widget.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/widget/widget.vue
@@ -1,22 +1,18 @@
<script>
-import {
- GlButton,
- GlLink,
- GlTooltipDirective,
- GlLoadingIcon,
- GlSafeHtmlDirective,
-} from '@gitlab/ui';
+import { GlButton, GlLink, GlTooltipDirective, GlLoadingIcon } from '@gitlab/ui';
import * as Sentry from '@sentry/browser';
import { normalizeHeaders } from '~/lib/utils/common_utils';
+import SafeHtml from '~/vue_shared/directives/safe_html';
import { sprintf, __ } from '~/locale';
import Poll from '~/lib/utils/poll';
import HelpPopover from '~/vue_shared/components/help_popover.vue';
-import ActionButtons from '../action_buttons.vue';
+import { DynamicScroller, DynamicScrollerItem } from 'vendor/vue-virtual-scroller';
import { EXTENSION_ICONS } from '../../constants';
import { createTelemetryHub } from '../extensions/telemetry';
import ContentRow from './widget_content_row.vue';
import DynamicContent from './dynamic_content.vue';
import StatusIcon from './status_icon.vue';
+import ActionButtons from './action_buttons.vue';
const FETCH_TYPE_COLLAPSED = 'collapsed';
const FETCH_TYPE_EXPANDED = 'expanded';
@@ -31,11 +27,13 @@ export default {
GlLoadingIcon,
ContentRow,
DynamicContent,
+ DynamicScroller,
+ DynamicScrollerItem,
HelpPopover,
},
directives: {
GlTooltip: GlTooltipDirective,
- SafeHtml: GlSafeHtmlDirective,
+ SafeHtml,
},
props: {
/**
@@ -258,6 +256,7 @@ export default {
<div class="gl-display-flex">
<help-popover
v-if="helpPopover"
+ icon="information-o"
:options="helpPopover.options"
:class="{ 'gl-mr-3': actionButtons.length > 0 }"
>
@@ -309,7 +308,7 @@ export default {
<div v-if="isLoadingExpandedContent" class="report-block-container gl-text-center">
<gl-loading-icon size="sm" inline /> {{ loadingText }}
</div>
- <div v-else class="gl-px-5 gl-display-flex">
+ <div v-else class="gl-pl-5 gl-display-flex" :class="{ 'gl-pr-5': $scopedSlots.content }">
<content-row
v-if="contentError"
:level="2"
@@ -322,12 +321,25 @@ export default {
</content-row>
<div v-else class="gl-w-full">
<slot name="content">
- <dynamic-content
- v-for="(data, index) in content"
- :key="data.id || index"
- :data="data"
- :widget-name="widgetName"
- />
+ <dynamic-scroller
+ v-if="content"
+ :items="content"
+ :min-item-size="32"
+ :style="{ maxHeight: '170px' }"
+ data-testid="dynamic-content-scroller"
+ class="gl-pr-5"
+ >
+ <template #default="{ item, index, active }">
+ <dynamic-scroller-item :item="item" :active="active">
+ <dynamic-content
+ :key="item.id || index"
+ :data="item"
+ :widget-name="widgetName"
+ :level="2"
+ />
+ </dynamic-scroller-item>
+ </template>
+ </dynamic-scroller>
</slot>
</div>
</div>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/widget/widget_content_row.vue b/app/assets/javascripts/vue_merge_request_widget/components/widget/widget_content_row.vue
index 1fd1e325863..543136dc659 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/widget/widget_content_row.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/widget/widget_content_row.vue
@@ -1,10 +1,11 @@
<script>
-import { GlSafeHtmlDirective, GlLink } from '@gitlab/ui';
+import { GlLink } from '@gitlab/ui';
import { __ } from '~/locale';
import HelpPopover from '~/vue_shared/components/help_popover.vue';
-import ActionButtons from '../action_buttons.vue';
+import SafeHtml from '~/vue_shared/directives/safe_html';
import { EXTENSION_ICONS } from '../../constants';
import { generateText } from '../extensions/utils';
+import ActionButtons from './action_buttons.vue';
import StatusIcon from './status_icon.vue';
export default {
@@ -15,7 +16,7 @@ export default {
ActionButtons,
},
directives: {
- SafeHtml: GlSafeHtmlDirective,
+ SafeHtml,
},
props: {
level: {
@@ -67,6 +68,9 @@ export default {
shouldShowHeaderActions() {
return Boolean(this.helpPopover) || this.actionButtons?.length > 0;
},
+ hasActionButtons() {
+ return this.actionButtons.length > 0;
+ },
},
i18n: {
learnMore: __('Learn more'),
@@ -75,10 +79,15 @@ export default {
</script>
<template>
<div
- class="gl-w-full gl-display-flex mr-widget-content-row gl-align-items-baseline"
+ class="gl-w-full gl-display-flex gl-align-items-baseline"
:class="{ 'gl-border-t gl-py-3 gl-pl-7': level === 2 }"
>
- <status-icon v-if="statusIconName" :level="2" :name="widgetName" :icon-name="statusIconName" />
+ <status-icon
+ v-if="statusIconName && !header"
+ :level="2"
+ :name="widgetName"
+ :icon-name="statusIconName"
+ />
<div class="gl-w-full">
<div class="gl-display-flex">
<slot name="header">
@@ -95,7 +104,12 @@ export default {
v-if="shouldShowHeaderActions"
class="gl-ml-auto gl-display-flex gl-align-items-baseline"
>
- <help-popover v-if="helpPopover" :options="helpPopover.options">
+ <help-popover
+ v-if="helpPopover"
+ :options="helpPopover.options"
+ :class="{ 'gl-mr-3': hasActionButtons }"
+ icon="information-o"
+ >
<template v-if="helpPopover.content">
<p
v-if="helpPopover.content.text"
@@ -112,14 +126,19 @@ export default {
</template>
</help-popover>
<action-buttons
- v-if="actionButtons.length > 0"
+ v-if="hasActionButtons"
:widget="widgetName"
:tertiary-buttons="actionButtons"
- :class="{ 'gl-ml-2': helpPopover }"
/>
</div>
</div>
<div class="gl-display-flex gl-align-items-baseline gl-w-full">
+ <status-icon
+ v-if="statusIconName && header"
+ :level="2"
+ :name="widgetName"
+ :icon-name="statusIconName"
+ />
<slot name="body"></slot>
</div>
</div>
diff --git a/app/assets/javascripts/vue_merge_request_widget/extensions/code_quality/constants.js b/app/assets/javascripts/vue_merge_request_widget/extensions/code_quality/constants.js
new file mode 100644
index 00000000000..03af21a5019
--- /dev/null
+++ b/app/assets/javascripts/vue_merge_request_widget/extensions/code_quality/constants.js
@@ -0,0 +1,31 @@
+import { n__, s__, sprintf } from '~/locale';
+
+export const i18n = {
+ label: s__('ciReport|Code Quality'),
+ loading: s__('ciReport|Code Quality is loading'),
+ error: s__('ciReport|Code Quality failed to load results'),
+ noChanges: s__(`ciReport|Code Quality hasn't changed.`),
+ prependText: s__(`ciReport|in`),
+ fixed: s__(`ciReport|Fixed`),
+ pluralReport: (errors) =>
+ sprintf(
+ n__(
+ '%{strong_start}%{errors}%{strong_end} point',
+ '%{strong_start}%{errors}%{strong_end} points',
+ errors.length,
+ ),
+ {
+ errors: errors.length,
+ },
+ false,
+ ),
+ singularReport: (errors) => n__('%d point', '%d points', errors.length),
+ improvementAndDegradationCopy: (improvement, degradation) =>
+ sprintf(
+ s__(`ciReport|Code Quality improved on ${improvement} and degraded on ${degradation}.`),
+ ),
+ improvedCopy: (improvements) =>
+ sprintf(s__(`ciReport|Code Quality improved on ${improvements}.`)),
+ degradedCopy: (degradations) =>
+ sprintf(s__(`ciReport|Code Quality degraded on ${degradations}.`)),
+};
diff --git a/app/assets/javascripts/vue_merge_request_widget/extensions/code_quality/index.js b/app/assets/javascripts/vue_merge_request_widget/extensions/code_quality/index.js
index 68347ac269e..394f8979a53 100644
--- a/app/assets/javascripts/vue_merge_request_widget/extensions/code_quality/index.js
+++ b/app/assets/javascripts/vue_merge_request_widget/extensions/code_quality/index.js
@@ -1,54 +1,33 @@
-import { n__, s__, sprintf } from '~/locale';
import axios from '~/lib/utils/axios_utils';
import { EXTENSION_ICONS } from '~/vue_merge_request_widget/constants';
-import { SEVERITY_ICONS_EXTENSION } from '~/reports/codequality_report/constants';
-import { parseCodeclimateMetrics } from '~/reports/codequality_report/store/utils/codequality_parser';
+import { SEVERITY_ICONS_MR_WIDGET } from '~/ci/reports/codequality_report/constants';
+import { HTTP_STATUS_NO_CONTENT } from '~/lib/utils/http_status';
+import { parseCodeclimateMetrics } from '~/ci/reports/codequality_report/store/utils/codequality_parser';
import { capitalizeFirstCharacter } from '~/lib/utils/text_utility';
+import { i18n } from './constants';
export default {
name: 'WidgetCodeQuality',
+ enablePolling: true,
props: ['codeQuality', 'blobPath'],
- i18n: {
- label: s__('ciReport|Code Quality'),
- loading: s__('ciReport|Code Quality test metrics results are being parsed'),
- error: s__('ciReport|Code Quality failed loading results'),
- },
+ i18n,
computed: {
- summary() {
- const { newErrors, resolvedErrors, errorSummary } = this.collapsedData;
- if (errorSummary.errored >= 1 && errorSummary.resolved >= 1) {
- const improvements = sprintf(
- n__(
- '%{strong_start}%{errors}%{strong_end} point',
- '%{strong_start}%{errors}%{strong_end} points',
- resolvedErrors.length,
- ),
- {
- errors: resolvedErrors.length,
- },
- false,
- );
+ summary(data) {
+ const { newErrors, resolvedErrors, errorSummary, parsingInProgress } = data;
- const degradations = sprintf(
- n__(
- '%{strong_start}%{errors}%{strong_end} point',
- '%{strong_start}%{errors}%{strong_end} points',
- newErrors.length,
- ),
- { errors: newErrors.length },
- false,
- );
- return sprintf(
- s__(`ciReport|Code Quality improved on ${improvements} and degraded on ${degradations}.`),
+ if (parsingInProgress) {
+ return i18n.loading;
+ } else if (errorSummary.errored >= 1 && errorSummary.resolved >= 1) {
+ return i18n.improvementAndDegradationCopy(
+ i18n.pluralReport(resolvedErrors),
+ i18n.pluralReport(newErrors),
);
} else if (errorSummary.resolved >= 1) {
- const improvements = n__('%d point', '%d points', resolvedErrors.length);
- return sprintf(s__(`ciReport|Code Quality improved on ${improvements}.`));
+ return i18n.improvedCopy(i18n.singularReport(resolvedErrors));
} else if (errorSummary.errored >= 1) {
- const degradations = n__('%d point', '%d points', newErrors.length);
- return sprintf(s__(`ciReport|Code Quality degraded on ${degradations}.`));
+ return i18n.degradedCopy(i18n.singularReport(newErrors));
}
- return s__(`ciReport|No changes to Code Quality.`);
+ return i18n.noChanges;
},
statusIcon() {
if (this.collapsedData.errorSummary?.errored >= 1) {
@@ -59,18 +38,17 @@ export default {
},
methods: {
fetchCollapsedData() {
- return Promise.all([this.fetchReport(this.codeQuality)]).then((values) => {
+ return axios.get(this.codeQuality).then((response) => {
+ const { data = {}, status } = response;
return {
- resolvedErrors: parseCodeclimateMetrics(
- values[0].resolved_errors,
- this.blobPath.head_path,
- ),
- newErrors: parseCodeclimateMetrics(values[0].new_errors, this.blobPath.head_path),
- existingErrors: parseCodeclimateMetrics(
- values[0].existing_errors,
- this.blobPath.head_path,
- ),
- errorSummary: values[0].summary,
+ ...response,
+ data: {
+ parsingInProgress: status === HTTP_STATUS_NO_CONTENT,
+ resolvedErrors: parseCodeclimateMetrics(data.resolved_errors, this.blobPath.head_path),
+ newErrors: parseCodeclimateMetrics(data.new_errors, this.blobPath.head_path),
+ existingErrors: parseCodeclimateMetrics(data.existing_errors, this.blobPath.head_path),
+ errorSummary: data.summary,
+ },
};
});
},
@@ -81,12 +59,12 @@ export default {
return fullData.push({
text: `${capitalizeFirstCharacter(e.severity)} - ${e.description}`,
subtext: {
- prependText: s__(`ciReport|in`),
+ prependText: i18n.prependText,
text: `${e.file_path}:${e.line}`,
href: e.urlPath,
},
icon: {
- name: SEVERITY_ICONS_EXTENSION[e.severity],
+ name: SEVERITY_ICONS_MR_WIDGET[e.severity],
},
});
});
@@ -95,12 +73,16 @@ export default {
return fullData.push({
text: `${capitalizeFirstCharacter(e.severity)} - ${e.description}`,
subtext: {
- prependText: s__(`ciReport|in`),
+ prependText: i18n.prependText,
text: `${e.file_path}:${e.line}`,
href: e.urlPath,
},
icon: {
- name: SEVERITY_ICONS_EXTENSION[e.severity],
+ name: SEVERITY_ICONS_MR_WIDGET[e.severity],
+ },
+ badge: {
+ variant: 'neutral',
+ text: i18n.fixed,
},
});
});
diff --git a/app/assets/javascripts/vue_merge_request_widget/i18n.js b/app/assets/javascripts/vue_merge_request_widget/i18n.js
index 454a14faabb..5380bcae003 100644
--- a/app/assets/javascripts/vue_merge_request_widget/i18n.js
+++ b/app/assets/javascripts/vue_merge_request_widget/i18n.js
@@ -25,3 +25,10 @@ export const MERGE_TRAIN_BUTTON_TEXT = {
failed: __('Start merge train...'),
passed: __('Start merge train'),
};
+
+export const MR_WIDGET_CLOSED_REOPEN = __('Reopen');
+export const MR_WIDGET_CLOSED_REOPENING = __('Reopening...');
+export const MR_WIDGET_CLOSED_RELOADING = __('Refreshing...');
+export const MR_WIDGET_CLOSED_REOPEN_FAILURE = __(
+ 'An error occurred. Unable to reopen this merge request.',
+);
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 b96bdcb3833..00024a594dc 100644
--- a/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue
@@ -1,10 +1,10 @@
<script>
-import { GlSafeHtmlDirective } from '@gitlab/ui';
import { isEmpty } from 'lodash';
import {
registerExtension,
registeredExtensions,
} from '~/vue_merge_request_widget/components/extensions';
+import SafeHtml from '~/vue_shared/directives/safe_html';
import MrWidgetApprovals from 'ee_else_ce/vue_merge_request_widget/components/approvals/approvals.vue';
import MRWidgetService from 'ee_else_ce/vue_merge_request_widget/services/mr_widget_service';
import MRWidgetStore from 'ee_else_ce/vue_merge_request_widget/stores/mr_widget_store';
@@ -15,6 +15,7 @@ import notify from '~/lib/utils/notify';
import { sprintf, s__, __ } from '~/locale';
import Project from '~/pages/projects/project';
import SmartInterval from '~/smart_interval';
+import { convertToGraphQLId } from '~/graphql_shared/utils';
import { setFaviconOverlay } from '../lib/utils/favicon';
import Loading from './components/loading.vue';
import MrWidgetAlertMessage from './components/mr_widget_alert_message.vue';
@@ -46,18 +47,20 @@ import { STATE_MACHINE, stateToComponentMap } from './constants';
import eventHub from './event_hub';
import mergeRequestQueryVariablesMixin from './mixins/merge_request_query_variables';
import getStateQuery from './queries/get_state.query.graphql';
+import getStateSubscription from './queries/get_state.subscription.graphql';
import terraformExtension from './extensions/terraform';
import accessibilityExtension from './extensions/accessibility';
import codeQualityExtension from './extensions/code_quality';
import testReportExtension from './extensions/test_report';
import ReportWidgetContainer from './components/report_widget_container.vue';
+import MrWidgetReadyToMerge from './components/states/new_ready_to_merge.vue';
export default {
// False positive i18n lint: https://gitlab.com/gitlab-org/frontend/eslint-plugin-i18n/issues/25
// eslint-disable-next-line @gitlab/require-i18n-strings
name: 'MRWidget',
directives: {
- SafeHtml: GlSafeHtmlDirective,
+ SafeHtml,
},
components: {
Loading,
@@ -76,7 +79,7 @@ export default {
MrWidgetNothingToMerge: NothingToMergeState,
MrWidgetNotAllowed: NotAllowedState,
MrWidgetMissingBranch: MissingBranchState,
- MrWidgetReadyToMerge: () => import('./components/states/new_ready_to_merge.vue'),
+ MrWidgetReadyToMerge,
ShaMismatch,
MrWidgetChecking: CheckingState,
MrWidgetUnresolvedDiscussions: UnresolvedDiscussionsState,
@@ -108,6 +111,31 @@ export default {
this.loading = false;
}
},
+ subscribeToMore: {
+ document() {
+ return getStateSubscription;
+ },
+ skip() {
+ return !this.mr?.id || this.loading || !window.gon?.features?.realtimeMrStatusChange;
+ },
+ variables() {
+ return {
+ issuableId: convertToGraphQLId('MergeRequest', this.mr?.id),
+ };
+ },
+ updateQuery(
+ _,
+ {
+ subscriptionData: {
+ data: { mergeRequestMergeStatusUpdated },
+ },
+ },
+ ) {
+ if (mergeRequestMergeStatusUpdated) {
+ this.mr.setGraphqlSubscriptionData(mergeRequestMergeStatusUpdated);
+ }
+ },
+ },
},
},
mixins: [mergeRequestQueryVariablesMixin],
@@ -128,6 +156,7 @@ export default {
machineState: store?.machineValue || STATE_MACHINE.definition.initial,
loading: true,
recomputeComponentName: 0,
+ issuableId: false,
};
},
computed: {
@@ -545,6 +574,7 @@ export default {
<mr-widget-approvals v-if="shouldRenderApprovals" :mr="mr" :service="service" />
<report-widget-container>
<extensions-container v-if="hasExtensions" :mr="mr" />
+ <widget-container v-if="mr && shouldShowSecurityExtension" :mr="mr" />
<security-reports-app
v-if="shouldRenderSecurityReport && !shouldShowSecurityExtension"
:pipeline-id="mr.pipeline.id"
@@ -580,8 +610,6 @@ export default {
</mr-widget-alert-message>
</div>
- <widget-container v-if="mr" :mr="mr" />
-
<div class="mr-widget-section" data-qa-selector="mr_widget_content">
<component :is="componentName" :mr="mr" :service="service" />
<ready-to-merge
diff --git a/app/assets/javascripts/vue_merge_request_widget/queries/get_state.subscription.graphql b/app/assets/javascripts/vue_merge_request_widget/queries/get_state.subscription.graphql
new file mode 100644
index 00000000000..c7b53db1221
--- /dev/null
+++ b/app/assets/javascripts/vue_merge_request_widget/queries/get_state.subscription.graphql
@@ -0,0 +1,7 @@
+subscription getStateSubscription($issuableId: IssuableID!) {
+ mergeRequestMergeStatusUpdated(issuableId: $issuableId) {
+ ... on MergeRequest {
+ detailedMergeStatus
+ }
+ }
+}
diff --git a/app/assets/javascripts/vue_merge_request_widget/queries/states/ready_to_merge.fragment.graphql b/app/assets/javascripts/vue_merge_request_widget/queries/states/ready_to_merge.fragment.graphql
index 54770e6579a..9b0420cc7fa 100644
--- a/app/assets/javascripts/vue_merge_request_widget/queries/states/ready_to_merge.fragment.graphql
+++ b/app/assets/javascripts/vue_merge_request_widget/queries/states/ready_to_merge.fragment.graphql
@@ -1,44 +1,11 @@
+#import "./ready_to_merge_merge_request.fragment.graphql"
+
fragment ReadyToMerge on Project {
id
onlyAllowMergeIfPipelineSucceeds
mergeRequestsFfOnlyEnabled
squashReadOnly
mergeRequest(iid: $iid) {
- id
- autoMergeEnabled
- shouldRemoveSourceBranch
- forceRemoveSourceBranch
- defaultMergeCommitMessage
- defaultSquashCommitMessage
- squash
- squashOnMerge
- availableAutoMergeStrategies
- hasCi
- mergeable
- mergeWhenPipelineSucceeds
- commitCount
- diffHeadSha
- userPermissions {
- canMerge
- removeSourceBranch
- updateMergeRequest
- }
- targetBranch
- mergeError
- commitsWithoutMergeCommits {
- nodes {
- id
- sha
- shortId
- title
- message
- }
- }
- headPipeline {
- id
- status
- path
- active
- }
+ ...ReadyToMergeMergeRequest
}
}
diff --git a/app/assets/javascripts/vue_merge_request_widget/queries/states/ready_to_merge.subscription.graphql b/app/assets/javascripts/vue_merge_request_widget/queries/states/ready_to_merge.subscription.graphql
new file mode 100644
index 00000000000..8aba172e09c
--- /dev/null
+++ b/app/assets/javascripts/vue_merge_request_widget/queries/states/ready_to_merge.subscription.graphql
@@ -0,0 +1,9 @@
+#import "./ready_to_merge_merge_request.fragment.graphql"
+
+subscription readyToMergeSubscription($issuableId: IssuableID!) {
+ mergeRequestMergeStatusUpdated(issuableId: $issuableId) {
+ ... on MergeRequest {
+ ...ReadyToMergeMergeRequest
+ }
+ }
+}
diff --git a/app/assets/javascripts/vue_merge_request_widget/queries/states/ready_to_merge_merge_request.fragment.graphql b/app/assets/javascripts/vue_merge_request_widget/queries/states/ready_to_merge_merge_request.fragment.graphql
new file mode 100644
index 00000000000..276e2d4d63f
--- /dev/null
+++ b/app/assets/javascripts/vue_merge_request_widget/queries/states/ready_to_merge_merge_request.fragment.graphql
@@ -0,0 +1,39 @@
+fragment ReadyToMergeMergeRequest on MergeRequest {
+ id
+ detailedMergeStatus
+ autoMergeEnabled
+ shouldRemoveSourceBranch
+ forceRemoveSourceBranch
+ defaultMergeCommitMessage
+ defaultSquashCommitMessage
+ squash
+ squashOnMerge
+ availableAutoMergeStrategies
+ hasCi
+ mergeable
+ mergeWhenPipelineSucceeds
+ commitCount
+ diffHeadSha
+ userPermissions {
+ canMerge
+ removeSourceBranch
+ updateMergeRequest
+ }
+ targetBranch
+ mergeError
+ commitsWithoutMergeCommits {
+ nodes {
+ id
+ sha
+ shortId
+ title
+ message
+ }
+ }
+ headPipeline {
+ id
+ status
+ path
+ active
+ }
+}
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 86ce032ea3d..85df2ea63c8 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
@@ -30,6 +30,7 @@ export default class MergeRequestStore {
this.machineValue = this.stateMachine.value;
this.mergeDetailsCollapsed = window.innerWidth < 768;
this.mergeError = data.mergeError;
+ this.id = data.id;
this.setPaths(data);
@@ -177,6 +178,7 @@ export default class MergeRequestStore {
this.updateStatusState(mergeRequest.state);
+ this.issuableId = mergeRequest.id;
this.projectArchived = project.archived;
this.onlyAllowMergeIfPipelineSucceeds = project.onlyAllowMergeIfPipelineSucceeds;
this.allowMergeOnSkippedPipeline = project.allowMergeOnSkippedPipeline;
@@ -206,6 +208,12 @@ export default class MergeRequestStore {
this.setState();
}
+ setGraphqlSubscriptionData(data) {
+ this.detailedMergeStatus = data.detailedMergeStatus;
+
+ this.setState();
+ }
+
updateStatusState(state) {
if (this.mergeRequestState !== state && badgeState.updateStatus) {
badgeState.updateStatus();
diff --git a/app/assets/javascripts/vue_shared/alert_details/components/alert_details.vue b/app/assets/javascripts/vue_shared/alert_details/components/alert_details.vue
index 96c2ffa929c..6803d609dbc 100644
--- a/app/assets/javascripts/vue_shared/alert_details/components/alert_details.vue
+++ b/app/assets/javascripts/vue_shared/alert_details/components/alert_details.vue
@@ -9,9 +9,9 @@ import {
GlTabs,
GlTab,
GlButton,
- GlSafeHtmlDirective,
} from '@gitlab/ui';
import * as Sentry from '@sentry/browser';
+import SafeHtml from '~/vue_shared/directives/safe_html';
import highlightCurrentUser from '~/behaviors/markdown/highlight_current_user';
import { fetchPolicies } from '~/lib/graphql';
import { toggleContainerClasses } from '~/lib/utils/dom_utils';
@@ -41,7 +41,7 @@ export default {
reportedAtWithTool: s__('AlertManagement|Reported %{when} by %{tool}'),
},
directives: {
- SafeHtml: GlSafeHtmlDirective,
+ SafeHtml,
},
severityLabels: SEVERITY_LEVELS,
tabsConfig: [
@@ -369,10 +369,10 @@ export default {
<alert-details-table :alert="alert" :loading="loading" :statuses="statuses" />
</gl-tab>
- <metric-images-tab
- :data-testid="$options.tabsConfig[1].id"
- :title="$options.tabsConfig[1].title"
- />
+ <gl-tab :title="$options.tabsConfig[1].title">
+ <metric-images-tab :data-testid="$options.tabsConfig[1].id" />
+ </gl-tab>
+
<gl-tab :data-testid="$options.tabsConfig[2].id" :title="$options.tabsConfig[2].title">
<div v-if="alert.notes.nodes.length > 0" class="issuable-discussion">
<ul class="notes main-notes-list timeline">
diff --git a/app/assets/javascripts/vue_shared/alert_details/components/alert_status.vue b/app/assets/javascripts/vue_shared/alert_details/components/alert_status.vue
index 672761af1cf..8d2ef20b381 100644
--- a/app/assets/javascripts/vue_shared/alert_details/components/alert_status.vue
+++ b/app/assets/javascripts/vue_shared/alert_details/components/alert_status.vue
@@ -106,7 +106,7 @@ export default {
@keydown.esc.native="$emit('hide-dropdown')"
@hide="$emit('hide-dropdown')"
>
- <p v-if="isSidebar" class="gl-new-dropdown-header-top" data-testid="dropdown-header">
+ <p v-if="isSidebar" class="gl-dropdown-header-top" data-testid="dropdown-header">
{{ s__('AlertManagement|Assign status') }}
</p>
<div class="dropdown-content dropdown-body">
diff --git a/app/assets/javascripts/vue_shared/alert_details/components/sidebar/sidebar_assignees.vue b/app/assets/javascripts/vue_shared/alert_details/components/sidebar/sidebar_assignees.vue
index 72dcc16b57a..4ec301b946b 100644
--- a/app/assets/javascripts/vue_shared/alert_details/components/sidebar/sidebar_assignees.vue
+++ b/app/assets/javascripts/vue_shared/alert_details/components/sidebar/sidebar_assignees.vue
@@ -242,7 +242,7 @@ export default {
@keydown.esc.native="hideDropdown"
@hide="hideDropdown"
>
- <p class="gl-new-dropdown-header-top">
+ <p class="gl-dropdown-header-top">
{{ __('Assign To') }}
</p>
<gl-search-box-by-type v-model.trim="search" :placeholder="__('Search users')" />
diff --git a/app/assets/javascripts/vue_shared/alert_details/components/sidebar/sidebar_header.vue b/app/assets/javascripts/vue_shared/alert_details/components/sidebar/sidebar_header.vue
index 832b154b312..b3ee01f3a24 100644
--- a/app/assets/javascripts/vue_shared/alert_details/components/sidebar/sidebar_header.vue
+++ b/app/assets/javascripts/vue_shared/alert_details/components/sidebar/sidebar_header.vue
@@ -1,5 +1,5 @@
<script>
-import ToggleSidebar from '~/vue_shared/components/sidebar/toggle_sidebar.vue';
+import ToggleSidebar from '~/sidebar/components/toggle/toggle_sidebar.vue';
import SidebarTodo from './sidebar_todo.vue';
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 6b774b2a734..3c73f42b6b1 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,6 @@
<script>
-import { GlIcon, GlSafeHtmlDirective } from '@gitlab/ui';
+import { GlIcon } from '@gitlab/ui';
+import SafeHtml from '~/vue_shared/directives/safe_html';
import NoteHeader from '~/notes/components/note_header.vue';
export default {
@@ -8,7 +9,7 @@ export default {
GlIcon,
},
directives: {
- SafeHtml: GlSafeHtmlDirective,
+ SafeHtml,
},
props: {
note: {
diff --git a/app/assets/javascripts/vue_shared/alert_details/graphql/mutations/alert_set_assignees.mutation.graphql b/app/assets/javascripts/vue_shared/alert_details/graphql/mutations/alert_set_assignees.mutation.graphql
index 33091f1ba5e..b04d5773a37 100644
--- a/app/assets/javascripts/vue_shared/alert_details/graphql/mutations/alert_set_assignees.mutation.graphql
+++ b/app/assets/javascripts/vue_shared/alert_details/graphql/mutations/alert_set_assignees.mutation.graphql
@@ -8,6 +8,7 @@ mutation alertSetAssignees($fullPath: ID!, $assigneeUsernames: [String!]!, $iid:
) {
errors
issuable: alert {
+ id
iid
assignees {
nodes {
diff --git a/app/assets/javascripts/vue_shared/components/actions_button.vue b/app/assets/javascripts/vue_shared/components/actions_button.vue
index c6c22f9c61f..175aef59ae5 100644
--- a/app/assets/javascripts/vue_shared/components/actions_button.vue
+++ b/app/assets/javascripts/vue_shared/components/actions_button.vue
@@ -1,11 +1,5 @@
<script>
-import {
- GlDropdown,
- GlDropdownItem,
- GlDropdownDivider,
- GlButton,
- GlTooltipDirective,
-} from '@gitlab/ui';
+import { GlDropdown, GlDropdownItem, GlDropdownDivider, GlButton, GlTooltip } from '@gitlab/ui';
export default {
components: {
@@ -13,11 +7,14 @@ export default {
GlDropdownItem,
GlDropdownDivider,
GlButton,
- },
- directives: {
- GlTooltip: GlTooltipDirective,
+ GlTooltip,
},
props: {
+ id: {
+ type: String,
+ required: false,
+ default: '',
+ },
actions: {
type: Array,
required: true,
@@ -37,6 +34,11 @@ export default {
required: false,
default: 'default',
},
+ showActionTooltip: {
+ type: Boolean,
+ required: false,
+ default: true,
+ },
},
computed: {
hasMultipleActions() {
@@ -51,6 +53,7 @@ export default {
this.$emit('select', action.key);
},
handleClick(action, evt) {
+ this.$emit('actionClicked', { action });
return action.handle?.(evt);
},
},
@@ -58,46 +61,51 @@ export default {
</script>
<template>
- <gl-dropdown
- v-if="hasMultipleActions"
- v-gl-tooltip="selectedAction.tooltip"
- :text="selectedAction.text"
- :split-href="selectedAction.href"
- :variant="variant"
- :category="category"
- split
- data-qa-selector="action_dropdown"
- @click="handleClick(selectedAction, $event)"
- >
- <template #button-content>
- <span class="gl-new-dropdown-button-text" v-bind="selectedAction.attrs">
- {{ selectedAction.text }}
- </span>
- </template>
- <template v-for="(action, index) in actions">
- <gl-dropdown-item
- :key="action.key"
- is-check-item
- :is-checked="action.key === selectedAction.key"
- :secondary-text="action.secondaryText"
- :data-qa-selector="`${action.key}_menu_item`"
- :data-testid="`action_${action.key}`"
- @click="handleItemClick(action)"
- >
- <span class="gl-font-weight-bold">{{ action.text }}</span>
- </gl-dropdown-item>
- <gl-dropdown-divider v-if="index != actions.length - 1" :key="action.key + '_divider'" />
- </template>
- </gl-dropdown>
- <gl-button
- v-else-if="selectedAction"
- v-gl-tooltip="selectedAction.tooltip"
- v-bind="selectedAction.attrs"
- :variant="variant"
- :category="category"
- :href="selectedAction.href"
- @click="handleClick(selectedAction, $event)"
- >
- {{ selectedAction.text }}
- </gl-button>
+ <span>
+ <gl-dropdown
+ v-if="hasMultipleActions"
+ :id="id"
+ :text="selectedAction.text"
+ :split-href="selectedAction.href"
+ :variant="variant"
+ :category="category"
+ split
+ data-qa-selector="action_dropdown"
+ @click="handleClick(selectedAction, $event)"
+ >
+ <template #button-content>
+ <span class="gl-dropdown-button-text" v-bind="selectedAction.attrs">
+ {{ selectedAction.text }}
+ </span>
+ </template>
+ <template v-for="(action, index) in actions">
+ <gl-dropdown-item
+ :key="action.key"
+ is-check-item
+ :is-checked="action.key === selectedAction.key"
+ :secondary-text="action.secondaryText"
+ :data-qa-selector="`${action.key}_menu_item`"
+ :data-testid="`action_${action.key}`"
+ @click="handleItemClick(action)"
+ >
+ <span class="gl-font-weight-bold">{{ action.text }}</span>
+ </gl-dropdown-item>
+ <gl-dropdown-divider v-if="index != actions.length - 1" :key="action.key + '_divider'" />
+ </template>
+ </gl-dropdown>
+ <gl-button
+ v-else-if="selectedAction"
+ :id="id"
+ v-bind="selectedAction.attrs"
+ :variant="variant"
+ :category="category"
+ :href="selectedAction.href"
+ @click="handleClick(selectedAction, $event)"
+ >
+ {{ selectedAction.text }}
+ </gl-button>
+ <gl-tooltip v-if="selectedAction.tooltip && showActionTooltip" :target="id">
+ {{ selectedAction.tooltip }}
+ </gl-tooltip>
+ </span>
</template>
diff --git a/app/assets/javascripts/vue_shared/components/awards_list.vue b/app/assets/javascripts/vue_shared/components/awards_list.vue
index f5d8811e83c..cb38b3e13bb 100644
--- a/app/assets/javascripts/vue_shared/components/awards_list.vue
+++ b/app/assets/javascripts/vue_shared/components/awards_list.vue
@@ -1,6 +1,7 @@
<script>
-import { GlIcon, GlButton, GlTooltipDirective, GlSafeHtmlDirective } from '@gitlab/ui';
+import { GlIcon, GlButton, GlTooltipDirective } from '@gitlab/ui';
import { groupBy } from 'lodash';
+import SafeHtml from '~/vue_shared/directives/safe_html';
import EmojiPicker from '~/emoji/components/picker.vue';
import { __, sprintf } from '~/locale';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
@@ -17,7 +18,7 @@ export default {
},
directives: {
GlTooltip: GlTooltipDirective,
- SafeHtml: GlSafeHtmlDirective,
+ SafeHtml,
},
mixins: [glFeatureFlagsMixin()],
props: {
@@ -158,10 +159,7 @@ export default {
return;
}
- // 100 and 1234 emoji are a number. Callback for v-for click sends it as a string
- const parsedName = /^[0-9]+$/.test(awardName) ? Number(awardName) : awardName;
-
- this.$emit('award', parsedName);
+ this.$emit('award', awardName);
if (document.activeElement) document.activeElement.blur();
},
diff --git a/app/assets/javascripts/vue_shared/components/blob_viewers/rich_viewer.vue b/app/assets/javascripts/vue_shared/components/blob_viewers/rich_viewer.vue
index ed0eb9cc0b8..49181bb847d 100644
--- a/app/assets/javascripts/vue_shared/components/blob_viewers/rich_viewer.vue
+++ b/app/assets/javascripts/vue_shared/components/blob_viewers/rich_viewer.vue
@@ -1,5 +1,5 @@
<script>
-import { GlSafeHtmlDirective as SafeHtml } from '@gitlab/ui';
+import SafeHtml from '~/vue_shared/directives/safe_html';
import { handleBlobRichViewer } from '~/blob/viewer';
import MarkdownFieldView from '~/vue_shared/components/markdown/field_view.vue';
import ViewerMixin from './mixins';
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 0117c06c3d5..c7a76af7f74 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,6 @@
<script>
-import { GlIcon, GlSafeHtmlDirective } from '@gitlab/ui';
+import { GlIcon } from '@gitlab/ui';
+import SafeHtml from '~/vue_shared/directives/safe_html';
import { HIGHLIGHT_CLASS_NAME } from './constants';
import ViewerMixin from './mixins';
@@ -9,7 +10,7 @@ export default {
GlIcon,
},
directives: {
- SafeHtml: GlSafeHtmlDirective,
+ SafeHtml,
},
mixins: [ViewerMixin],
inject: ['blobHash'],
diff --git a/app/assets/javascripts/vue_shared/components/code_block_highlighted.vue b/app/assets/javascripts/vue_shared/components/code_block_highlighted.vue
index 65b08b608e8..352d03befc3 100644
--- a/app/assets/javascripts/vue_shared/components/code_block_highlighted.vue
+++ b/app/assets/javascripts/vue_shared/components/code_block_highlighted.vue
@@ -1,5 +1,5 @@
<script>
-import { GlSafeHtmlDirective } from '@gitlab/ui';
+import SafeHtml from '~/vue_shared/directives/safe_html';
import languageLoader from '~/content_editor/services/highlight_js_language_loader';
import CodeBlock from './code_block.vue';
@@ -7,7 +7,7 @@ import CodeBlock from './code_block.vue';
export default {
name: 'CodeBlockHighlighted',
directives: {
- SafeHtml: GlSafeHtmlDirective,
+ SafeHtml,
},
components: {
CodeBlock,
diff --git a/app/assets/javascripts/vue_shared/components/confirm_danger/confirm_danger_modal.vue b/app/assets/javascripts/vue_shared/components/confirm_danger/confirm_danger_modal.vue
index 7a982bc035a..d0a634d8e54 100644
--- a/app/assets/javascripts/vue_shared/components/confirm_danger/confirm_danger_modal.vue
+++ b/app/assets/javascripts/vue_shared/components/confirm_danger/confirm_danger_modal.vue
@@ -1,12 +1,6 @@
<script>
-import {
- GlAlert,
- GlModal,
- GlFormGroup,
- GlFormInput,
- GlSafeHtmlDirective as SafeHtml,
- GlSprintf,
-} from '@gitlab/ui';
+import { GlAlert, GlModal, GlFormGroup, GlFormInput, GlSprintf } from '@gitlab/ui';
+import SafeHtml from '~/vue_shared/directives/safe_html';
import {
CONFIRM_DANGER_MODAL_BUTTON,
CONFIRM_DANGER_MODAL_TITLE,
diff --git a/app/assets/javascripts/vue_shared/components/confirm_modal.vue b/app/assets/javascripts/vue_shared/components/confirm_modal.vue
index 72504e5bc50..664c3578785 100644
--- a/app/assets/javascripts/vue_shared/components/confirm_modal.vue
+++ b/app/assets/javascripts/vue_shared/components/confirm_modal.vue
@@ -1,6 +1,7 @@
<script>
-import { GlModal, GlSafeHtmlDirective as SafeHtml } from '@gitlab/ui';
+import { GlModal } from '@gitlab/ui';
import { uniqueId } from 'lodash';
+import SafeHtml from '~/vue_shared/directives/safe_html';
import csrf from '~/lib/utils/csrf';
import eventHub, { EVENT_OPEN_CONFIRM_MODAL } from './confirm_modal_eventhub';
import DomElementListener from './dom_element_listener.vue';
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 3ecfac10f9c..00d12654ee3 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,10 +1,10 @@
<script>
-import { GlSkeletonLoader, GlSafeHtmlDirective as SafeHtml } from '@gitlab/ui';
-import $ from 'jquery';
-import '~/behaviors/markdown/render_gfm';
+import { GlSkeletonLoader } from '@gitlab/ui';
import { forEach, escape } from 'lodash';
+import SafeHtml from '~/vue_shared/directives/safe_html';
import axios from '~/lib/utils/axios_utils';
import { __ } from '~/locale';
+import { renderGFM } from '~/behaviors/markdown/render_gfm';
const { CancelToken } = axios;
let axiosSource;
@@ -96,7 +96,7 @@ export default {
this.isLoading = false;
this.$nextTick(() => {
- $(this.$refs.markdownPreview).renderGFM();
+ renderGFM(this.$refs.markdownPreview);
});
})
.catch(() => {
diff --git a/app/assets/javascripts/vue_shared/components/date_time_picker/date_time_picker.vue b/app/assets/javascripts/vue_shared/components/date_time_picker/date_time_picker.vue
index 181c1b89e31..d8a2789a419 100644
--- a/app/assets/javascripts/vue_shared/components/date_time_picker/date_time_picker.vue
+++ b/app/assets/javascripts/vue_shared/components/date_time_picker/date_time_picker.vue
@@ -265,7 +265,6 @@ export default {
<gl-dropdown-item
v-for="(option, index) in options"
:key="index"
- data-qa-selector="quick_range_item"
:active="isOptionActive(option)"
active-class="active"
@click="setQuickRange(option)"
diff --git a/app/assets/javascripts/vue_shared/components/dismissible_alert.vue b/app/assets/javascripts/vue_shared/components/dismissible_alert.vue
index 0621ec14c6c..8395bc89790 100644
--- a/app/assets/javascripts/vue_shared/components/dismissible_alert.vue
+++ b/app/assets/javascripts/vue_shared/components/dismissible_alert.vue
@@ -1,5 +1,6 @@
<script>
-import { GlAlert, GlSafeHtmlDirective as SafeHtml } from '@gitlab/ui';
+import { GlAlert } from '@gitlab/ui';
+import SafeHtml from '~/vue_shared/directives/safe_html';
export default {
name: 'DismissibleAlert',
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 755ce004aa9..993b4c11c0e 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
@@ -8,52 +8,44 @@ export const FILTER_ANY = 'Any';
export const FILTER_CURRENT = 'Current';
export const FILTER_UPCOMING = 'Upcoming';
export const FILTER_STARTED = 'Started';
-export const FILTER_NONE_ANY = [FILTER_NONE, FILTER_ANY];
+
+export const FILTERS_NONE_ANY = [FILTER_NONE, FILTER_ANY];
export const OPERATOR_IS = '=';
export const OPERATOR_IS_TEXT = __('is');
-export const OPERATOR_IS_NOT = '!=';
-export const OPERATOR_IS_NOT_TEXT = __('is not one of');
+export const OPERATOR_NOT = '!=';
+export const OPERATOR_NOT_TEXT = __('is not one of');
export const OPERATOR_OR = '||';
export const OPERATOR_OR_TEXT = __('is one of');
-export const OPERATOR_IS_ONLY = [{ value: OPERATOR_IS, description: OPERATOR_IS_TEXT }];
-export const OPERATOR_IS_NOT_ONLY = [{ value: OPERATOR_IS_NOT, description: OPERATOR_IS_NOT_TEXT }];
-export const OPERATOR_OR_ONLY = [{ value: OPERATOR_OR, description: OPERATOR_OR_TEXT }];
-export const OPERATOR_IS_AND_IS_NOT = [...OPERATOR_IS_ONLY, ...OPERATOR_IS_NOT_ONLY];
-export const OPERATOR_IS_NOT_OR = [
- ...OPERATOR_IS_ONLY,
- ...OPERATOR_IS_NOT_ONLY,
- ...OPERATOR_OR_ONLY,
-];
-
-export const DEFAULT_LABEL_NONE = { value: FILTER_NONE, text: __('None'), title: __('None') };
-export const DEFAULT_LABEL_ANY = { value: FILTER_ANY, text: __('Any'), title: __('Any') };
-export const DEFAULT_NONE_ANY = [DEFAULT_LABEL_NONE, DEFAULT_LABEL_ANY];
+export const OPERATORS_IS = [{ value: OPERATOR_IS, description: OPERATOR_IS_TEXT }];
+export const OPERATORS_NOT = [{ value: OPERATOR_NOT, description: OPERATOR_NOT_TEXT }];
+export const OPERATORS_OR = [{ value: OPERATOR_OR, description: OPERATOR_OR_TEXT }];
+export const OPERATORS_IS_NOT = [...OPERATORS_IS, ...OPERATORS_NOT];
+export const OPERATORS_IS_NOT_OR = [...OPERATORS_IS, ...OPERATORS_NOT, ...OPERATORS_OR];
-export const DEFAULT_MILESTONE_UPCOMING = {
+export const OPTION_NONE = { value: FILTER_NONE, text: __('None'), title: __('None') };
+export const OPTION_ANY = { value: FILTER_ANY, text: __('Any'), title: __('Any') };
+export const OPTION_CURRENT = { value: FILTER_CURRENT, text: __('Current') };
+export const OPTION_STARTED = { value: FILTER_STARTED, text: __('Started'), title: __('Started') };
+export const OPTION_UPCOMING = {
value: FILTER_UPCOMING,
text: __('Upcoming'),
title: __('Upcoming'),
};
-export const DEFAULT_MILESTONE_STARTED = {
- value: FILTER_STARTED,
- text: __('Started'),
- title: __('Started'),
-};
-export const DEFAULT_MILESTONES = DEFAULT_NONE_ANY.concat([
- DEFAULT_MILESTONE_UPCOMING,
- DEFAULT_MILESTONE_STARTED,
-]);
-export const SortDirection = {
+export const OPTIONS_NONE_ANY = [OPTION_NONE, OPTION_ANY];
+
+export const DEFAULT_MILESTONES = OPTIONS_NONE_ANY.concat([OPTION_UPCOMING, OPTION_STARTED]);
+
+export const SORT_DIRECTION = {
descending: 'descending',
ascending: 'ascending',
};
-export const FILTERED_SEARCH_LABELS = 'labels';
export const FILTERED_SEARCH_TERM = 'filtered-search-term';
+export const TOKEN_TITLE_APPROVED_BY = __('Approved-By');
export const TOKEN_TITLE_ASSIGNEE = s__('SearchToken|Assignee');
export const TOKEN_TITLE_AUTHOR = __('Author');
export const TOKEN_TITLE_CONFIDENTIAL = __('Confidential');
@@ -63,11 +55,14 @@ export const TOKEN_TITLE_MILESTONE = __('Milestone');
export const TOKEN_TITLE_MY_REACTION = __('My-Reaction');
export const TOKEN_TITLE_ORGANIZATION = s__('Crm|Organization');
export const TOKEN_TITLE_RELEASE = __('Release');
+export const TOKEN_TITLE_REVIEWER = s__('SearchToken|Reviewer');
export const TOKEN_TITLE_SOURCE_BRANCH = __('Source Branch');
export const TOKEN_TITLE_STATUS = __('Status');
export const TOKEN_TITLE_TARGET_BRANCH = __('Target Branch');
export const TOKEN_TITLE_TYPE = __('Type');
+export const TOKEN_TITLE_SEARCH_WITHIN = __('Search Within');
+export const TOKEN_TYPE_APPROVED_BY = 'approved-by';
export const TOKEN_TYPE_ASSIGNEE = 'assignee';
export const TOKEN_TYPE_AUTHOR = 'author';
export const TOKEN_TYPE_CONFIDENTIAL = 'confidential';
@@ -84,5 +79,11 @@ export const TOKEN_TYPE_MILESTONE = 'milestone';
export const TOKEN_TYPE_MY_REACTION = 'my-reaction';
export const TOKEN_TYPE_ORGANIZATION = 'organization';
export const TOKEN_TYPE_RELEASE = 'release';
+export const TOKEN_TYPE_REVIEWER = 'reviewer';
+export const TOKEN_TYPE_SOURCE_BRANCH = 'source-branch';
+export const TOKEN_TYPE_STATUS = 'status';
+export const TOKEN_TYPE_TARGET_BRANCH = 'target-branch';
export const TOKEN_TYPE_TYPE = 'type';
export const TOKEN_TYPE_WEIGHT = 'weight';
+
+export const TOKEN_TYPE_SEARCH_WITHIN = 'in';
diff --git a/app/assets/javascripts/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue b/app/assets/javascripts/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue
index 0d0787e7033..34f64dddc41 100644
--- a/app/assets/javascripts/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue
+++ b/app/assets/javascripts/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue
@@ -15,7 +15,7 @@ import RecentSearchesStore from '~/filtered_search/stores/recent_searches_store'
import { createAlert } from '~/flash';
import { __ } from '~/locale';
-import { SortDirection } from './constants';
+import { SORT_DIRECTION } from './constants';
import { filterEmptySearchTerm, stripQuotes, uniqueTokens } from './filtered_search_utils';
export default {
@@ -107,7 +107,7 @@ export default {
recentSearches: [],
filterValue: this.initialFilterValue,
selectedSortOption: this.sortOptions[0],
- selectedSortDirection: SortDirection.descending,
+ selectedSortDirection: SORT_DIRECTION.descending,
};
},
computed: {
@@ -130,12 +130,12 @@ export default {
);
},
sortDirectionIcon() {
- return this.selectedSortDirection === SortDirection.ascending
+ return this.selectedSortDirection === SORT_DIRECTION.ascending
? 'sort-lowest'
: 'sort-highest';
},
sortDirectionTooltip() {
- return this.selectedSortDirection === SortDirection.ascending
+ return this.selectedSortDirection === SORT_DIRECTION.ascending
? __('Sort direction: Ascending')
: __('Sort direction: Descending');
},
@@ -267,9 +267,9 @@ export default {
},
handleSortDirectionClick() {
this.selectedSortDirection =
- this.selectedSortDirection === SortDirection.ascending
- ? SortDirection.descending
- : SortDirection.ascending;
+ this.selectedSortDirection === SORT_DIRECTION.ascending
+ ? SORT_DIRECTION.descending
+ : SORT_DIRECTION.ascending;
this.$emit('onSort', this.selectedSortOption.sortDirection[this.selectedSortDirection]);
},
handleHistoryItemSelected(filters) {
diff --git a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/base_token.vue b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/base_token.vue
index 6a4ff07c999..b0fa3e4c27e 100644
--- a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/base_token.vue
+++ b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/base_token.vue
@@ -9,7 +9,7 @@ import {
} from '@gitlab/ui';
import { debounce } from 'lodash';
-import { DEBOUNCE_DELAY, FILTER_NONE_ANY, OPERATOR_IS_NOT } from '../constants';
+import { DEBOUNCE_DELAY, FILTERS_NONE_ANY, OPERATOR_NOT } from '../constants';
import {
getRecentlyUsedSuggestions,
setTokenValueToRecentlyUsed,
@@ -100,9 +100,9 @@ export default {
return this.getActiveTokenValue(this.suggestions, this.value.data);
},
availableDefaultSuggestions() {
- if (this.value.operator === OPERATOR_IS_NOT) {
+ if (this.value.operator === OPERATOR_NOT) {
return this.defaultSuggestions.filter(
- (suggestion) => !FILTER_NONE_ANY.includes(suggestion.value),
+ (suggestion) => !FILTERS_NONE_ANY.includes(suggestion.value),
);
}
return this.defaultSuggestions;
diff --git a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/crm_contact_token.vue b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/crm_contact_token.vue
index d34cfb922a9..e0fa06c159e 100644
--- a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/crm_contact_token.vue
+++ b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/crm_contact_token.vue
@@ -8,7 +8,7 @@ import { isPositiveInteger } from '~/lib/utils/number_utils';
import { __ } from '~/locale';
import searchCrmContactsQuery from '../queries/search_crm_contacts.query.graphql';
-import { DEFAULT_NONE_ANY } from '../constants';
+import { OPTIONS_NONE_ANY } from '../constants';
import BaseToken from './base_token.vue';
@@ -39,7 +39,7 @@ export default {
},
computed: {
defaultContacts() {
- return this.config.defaultContacts || DEFAULT_NONE_ANY;
+ return this.config.defaultContacts || OPTIONS_NONE_ANY;
},
namespace() {
return this.config.isProject ? ITEM_TYPE.PROJECT : ITEM_TYPE.GROUP;
diff --git a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/crm_organization_token.vue b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/crm_organization_token.vue
index c7c9350ee93..3f030c8698c 100644
--- a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/crm_organization_token.vue
+++ b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/crm_organization_token.vue
@@ -8,7 +8,7 @@ import { isPositiveInteger } from '~/lib/utils/number_utils';
import { __ } from '~/locale';
import searchCrmOrganizationsQuery from '../queries/search_crm_organizations.query.graphql';
-import { DEFAULT_NONE_ANY } from '../constants';
+import { OPTIONS_NONE_ANY } from '../constants';
import BaseToken from './base_token.vue';
@@ -39,7 +39,7 @@ export default {
},
computed: {
defaultOrganizations() {
- return this.config.defaultOrganizations || DEFAULT_NONE_ANY;
+ return this.config.defaultOrganizations || OPTIONS_NONE_ANY;
},
namespace() {
return this.config.isProject ? ITEM_TYPE.PROJECT : ITEM_TYPE.GROUP;
diff --git a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/emoji_token.vue b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/emoji_token.vue
index 929823f7308..74905dc2ae0 100644
--- a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/emoji_token.vue
+++ b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/emoji_token.vue
@@ -3,7 +3,7 @@ import { GlFilteredSearchSuggestion } from '@gitlab/ui';
import { createAlert } from '~/flash';
import { __ } from '~/locale';
import BaseToken from '~/vue_shared/components/filtered_search_bar/tokens/base_token.vue';
-import { DEFAULT_NONE_ANY } from '../constants';
+import { OPTIONS_NONE_ANY } from '../constants';
import { stripQuotes } from '../filtered_search_utils';
export default {
@@ -33,7 +33,7 @@ export default {
},
computed: {
defaultEmojis() {
- return this.config.defaultEmojis || DEFAULT_NONE_ANY;
+ return this.config.defaultEmojis || OPTIONS_NONE_ANY;
},
},
methods: {
diff --git a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/label_token.vue b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/label_token.vue
index bce0c11aafd..71c50ef292a 100644
--- a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/label_token.vue
+++ b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/label_token.vue
@@ -5,7 +5,7 @@ import { createAlert } from '~/flash';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import { __ } from '~/locale';
-import { DEFAULT_NONE_ANY } from '../constants';
+import { OPTIONS_NONE_ANY } from '../constants';
import { stripQuotes } from '../filtered_search_utils';
import BaseToken from './base_token.vue';
@@ -38,7 +38,7 @@ export default {
},
computed: {
defaultLabels() {
- return this.config.defaultLabels || DEFAULT_NONE_ANY;
+ return this.config.defaultLabels || OPTIONS_NONE_ANY;
},
},
methods: {
diff --git a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/release_token.vue b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/release_token.vue
index 59701b4959e..6d681aab3ca 100644
--- a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/release_token.vue
+++ b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/release_token.vue
@@ -3,7 +3,7 @@ import { GlFilteredSearchSuggestion } from '@gitlab/ui';
import { createAlert } from '~/flash';
import { __ } from '~/locale';
import BaseToken from '~/vue_shared/components/filtered_search_bar/tokens/base_token.vue';
-import { DEFAULT_NONE_ANY } from '../constants';
+import { OPTIONS_NONE_ANY } from '../constants';
export default {
components: {
@@ -32,7 +32,7 @@ export default {
},
computed: {
defaultReleases() {
- return this.config.defaultReleases || DEFAULT_NONE_ANY;
+ return this.config.defaultReleases || OPTIONS_NONE_ANY;
},
},
methods: {
diff --git a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/author_token.vue b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/user_token.vue
index 7c184a3c391..28e65c1185f 100644
--- a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/author_token.vue
+++ b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/user_token.vue
@@ -4,7 +4,7 @@ import { compact } from 'lodash';
import { createAlert } from '~/flash';
import { __ } from '~/locale';
-import { DEFAULT_NONE_ANY } from '../constants';
+import { OPTIONS_NONE_ANY } from '../constants';
import BaseToken from './base_token.vue';
@@ -30,30 +30,30 @@ export default {
},
data() {
return {
- authors: this.config.initialAuthors || [],
+ users: this.config.initialUsers || [],
loading: false,
};
},
computed: {
- defaultAuthors() {
- return this.config.defaultAuthors || DEFAULT_NONE_ANY;
+ defaultUsers() {
+ return this.config.defaultUsers || OPTIONS_NONE_ANY;
},
- preloadedAuthors() {
- return this.config.preloadedAuthors || [];
+ preloadedUsers() {
+ return this.config.preloadedUsers || [];
},
},
methods: {
- getActiveAuthor(authors, data) {
- return authors.find((author) => author.username.toLowerCase() === data.toLowerCase());
+ getActiveUser(users, data) {
+ return users.find((user) => user.username.toLowerCase() === data.toLowerCase());
},
- getAvatarUrl(author) {
- return author.avatarUrl || author.avatar_url;
+ getAvatarUrl(user) {
+ return user.avatarUrl || user.avatar_url;
},
- fetchAuthors(searchTerm) {
+ fetchUsers(searchTerm) {
this.loading = true;
const fetchPromise = this.config.fetchPath
- ? this.config.fetchAuthors(this.config.fetchPath, searchTerm)
- : this.config.fetchAuthors(searchTerm);
+ ? this.config.fetchUsers(this.config.fetchPath, searchTerm)
+ : this.config.fetchUsers(searchTerm);
fetchPromise
.then((res) => {
@@ -62,7 +62,7 @@ export default {
// return response differently
// TODO: rm when completed https://gitlab.com/gitlab-org/gitlab/-/issues/345756
- this.authors = Array.isArray(res) ? compact(res) : compact(res.data);
+ this.users = Array.isArray(res) ? compact(res) : compact(res.data);
})
.catch(() =>
createAlert({
@@ -83,12 +83,12 @@ export default {
:value="value"
:active="active"
:suggestions-loading="loading"
- :suggestions="authors"
- :get-active-token-value="getActiveAuthor"
- :default-suggestions="defaultAuthors"
- :preloaded-suggestions="preloadedAuthors"
+ :suggestions="users"
+ :get-active-token-value="getActiveUser"
+ :default-suggestions="defaultUsers"
+ :preloaded-suggestions="preloadedUsers"
v-bind="$attrs"
- @fetch-suggestions="fetchAuthors"
+ @fetch-suggestions="fetchUsers"
v-on="$listeners"
>
<template #view="{ viewTokenProps: { inputValue, activeTokenValue } }">
@@ -102,15 +102,15 @@ export default {
</template>
<template #suggestions-list="{ suggestions }">
<gl-filtered-search-suggestion
- v-for="author in suggestions"
- :key="author.username"
- :value="author.username"
+ v-for="user in suggestions"
+ :key="user.username"
+ :value="user.username"
>
<div class="gl-display-flex">
- <gl-avatar :size="32" :src="getAvatarUrl(author)" />
+ <gl-avatar :size="32" :src="getAvatarUrl(user)" />
<div>
- <div>{{ author.name }}</div>
- <div>@{{ author.username }}</div>
+ <div>{{ user.name }}</div>
+ <div>@{{ user.username }}</div>
</div>
</div>
</gl-filtered-search-suggestion>
diff --git a/app/assets/javascripts/vue_shared/components/group_select/group_select.vue b/app/assets/javascripts/vue_shared/components/group_select/group_select.vue
index 1de6c0121bc..5db723e1e5a 100644
--- a/app/assets/javascripts/vue_shared/components/group_select/group_select.vue
+++ b/app/assets/javascripts/vue_shared/components/group_select/group_select.vue
@@ -1,6 +1,6 @@
<script>
import { debounce } from 'lodash';
-import { GlListbox } from '@gitlab/ui';
+import { GlCollapsibleListbox } from '@gitlab/ui';
import axios from '~/lib/utils/axios_utils';
import Api from '~/api';
import { __ } from '~/locale';
@@ -18,7 +18,7 @@ const MINIMUM_QUERY_LENGTH = 3;
export default {
components: {
- GlListbox,
+ GlCollapsibleListbox,
},
props: {
inputName: {
@@ -167,7 +167,7 @@ export default {
<template>
<div>
- <gl-listbox
+ <gl-collapsible-listbox
ref="listbox"
v-model="selected"
:header-text="$options.i18n.selectGroup"
@@ -188,7 +188,7 @@ export default {
</div>
<div class="gl-text-gray-300">{{ item.full_path }}</div>
</template>
- </gl-listbox>
+ </gl-collapsible-listbox>
<div class="flash-container"></div>
<input :id="inputId" data-testid="input" type="hidden" :name="inputName" :value="inputValue" />
</div>
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 96f7427dda1..3c4ae08d2f7 100644
--- a/app/assets/javascripts/vue_shared/components/header_ci_component.vue
+++ b/app/assets/javascripts/vue_shared/components/header_ci_component.vue
@@ -1,12 +1,6 @@
<script>
-import {
- GlTooltipDirective,
- GlButton,
- GlSafeHtmlDirective,
- GlAvatarLink,
- GlAvatarLabeled,
- GlTooltip,
-} from '@gitlab/ui';
+import { GlTooltipDirective, GlButton, GlAvatarLink, GlAvatarLabeled, GlTooltip } from '@gitlab/ui';
+import SafeHtml from '~/vue_shared/directives/safe_html';
import { isGid, getIdFromGraphQLId } from '~/graphql_shared/utils';
import { glEmojiTag } from '~/emoji';
import { __, sprintf } from '~/locale';
@@ -31,7 +25,7 @@ export default {
},
directives: {
GlTooltip: GlTooltipDirective,
- SafeHtml: GlSafeHtmlDirective,
+ SafeHtml,
},
EMOJI_REF: 'EMOJI_REF',
props: {
diff --git a/app/assets/javascripts/vue_shared/components/help_popover.vue b/app/assets/javascripts/vue_shared/components/help_popover.vue
index f349aa78bac..92d468cf970 100644
--- a/app/assets/javascripts/vue_shared/components/help_popover.vue
+++ b/app/assets/javascripts/vue_shared/components/help_popover.vue
@@ -1,5 +1,6 @@
<script>
-import { GlButton, GlPopover, GlSafeHtmlDirective } from '@gitlab/ui';
+import { GlButton, GlPopover } from '@gitlab/ui';
+import SafeHtml from '~/vue_shared/directives/safe_html';
/**
* Render a button with a question mark icon
@@ -12,7 +13,7 @@ export default {
GlPopover,
},
directives: {
- SafeHtml: GlSafeHtmlDirective,
+ SafeHtml,
},
props: {
options: {
diff --git a/app/assets/javascripts/vue_shared/components/listbox_input/listbox_input.stories.js b/app/assets/javascripts/vue_shared/components/listbox_input/listbox_input.stories.js
new file mode 100644
index 00000000000..4106de371cb
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/listbox_input/listbox_input.stories.js
@@ -0,0 +1,26 @@
+import ListboxInput from './listbox_input.vue';
+
+export default {
+ component: ListboxInput,
+ title: 'vue_shared/listbox_input',
+};
+
+const Template = (args, { argTypes }) => ({
+ components: { ListboxInput },
+ data() {
+ return { selected: null };
+ },
+ props: Object.keys(argTypes),
+ template: '<listbox-input v-model="selected" v-bind="$props" />',
+});
+
+export const Default = Template.bind({});
+Default.args = {
+ name: 'input_name',
+ defaultToggleText: 'Select an option',
+ items: [
+ { text: 'Option 1', value: '1' },
+ { text: 'Option 2', value: '2' },
+ { text: 'Option 3', value: '3' },
+ ],
+};
diff --git a/app/assets/javascripts/vue_shared/components/listbox_input/listbox_input.vue b/app/assets/javascripts/vue_shared/components/listbox_input/listbox_input.vue
new file mode 100644
index 00000000000..b1809e6a9f3
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/listbox_input/listbox_input.vue
@@ -0,0 +1,110 @@
+<script>
+import { GlListbox } from '@gitlab/ui';
+import { __ } from '~/locale';
+
+const MIN_ITEMS_COUNT_FOR_SEARCHING = 20;
+
+export default {
+ i18n: {
+ noResultsText: __('No results found'),
+ },
+ components: {
+ GlListbox,
+ },
+ model: GlListbox.model,
+ props: {
+ name: {
+ type: String,
+ required: true,
+ },
+ defaultToggleText: {
+ type: String,
+ required: true,
+ },
+ selected: {
+ type: String,
+ required: false,
+ default: null,
+ },
+ items: {
+ type: GlListbox.props.items.type,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ searchString: '',
+ };
+ },
+ computed: {
+ allOptions() {
+ const allOptions = [];
+
+ const getOptions = (options) => {
+ for (let i = 0; i < options.length; i += 1) {
+ const option = options[i];
+ if (option.options) {
+ getOptions(option.options);
+ } else {
+ allOptions.push(option);
+ }
+ }
+ };
+ getOptions(this.items);
+
+ return allOptions;
+ },
+ isGrouped() {
+ return this.items.some((item) => item.options !== undefined);
+ },
+ isSearchable() {
+ return this.allOptions.length > MIN_ITEMS_COUNT_FOR_SEARCHING;
+ },
+ filteredItems() {
+ const searchString = this.searchString.toLowerCase();
+
+ if (!searchString) {
+ return this.items;
+ }
+
+ if (this.isGrouped) {
+ return this.items
+ .map(({ text, options }) => {
+ return {
+ text,
+ options: options.filter((option) => option.text.toLowerCase().includes(searchString)),
+ };
+ })
+ .filter(({ options }) => options.length);
+ }
+
+ return this.items.filter((item) => item.text.toLowerCase().includes(searchString));
+ },
+ toggleText() {
+ return this.selected
+ ? this.allOptions.find((option) => option.value === this.selected).text
+ : this.defaultToggleText;
+ },
+ },
+ methods: {
+ search(searchString) {
+ this.searchString = searchString;
+ },
+ },
+};
+</script>
+
+<template>
+ <div>
+ <gl-listbox
+ :selected="selected"
+ :toggle-text="toggleText"
+ :items="filteredItems"
+ :searchable="isSearchable"
+ :no-results-text="$options.i18n.noResultsText"
+ @search="search"
+ @select="$emit($options.model.event, $event)"
+ />
+ <input ref="input" type="hidden" :name="name" :value="selected" />
+ </div>
+</template>
diff --git a/app/assets/javascripts/vue_shared/components/markdown/apply_suggestion.vue b/app/assets/javascripts/vue_shared/components/markdown/apply_suggestion.vue
index caec49c557a..f51ec715678 100644
--- a/app/assets/javascripts/vue_shared/components/markdown/apply_suggestion.vue
+++ b/app/assets/javascripts/vue_shared/components/markdown/apply_suggestion.vue
@@ -74,7 +74,7 @@ export default {
@submit="onApply"
/>
<gl-button
- class="gl-w-auto! gl-mt-3 gl-text-center! gl-hover-text-white! gl-transition-medium! float-right"
+ class="gl-w-auto! gl-mt-3 gl-text-center! gl-transition-medium! float-right"
category="primary"
variant="confirm"
data-qa-selector="commit_with_custom_message_button"
diff --git a/app/assets/javascripts/vue_shared/components/markdown/field.vue b/app/assets/javascripts/vue_shared/components/markdown/field.vue
index 657e4498b53..b5f2602af5e 100644
--- a/app/assets/javascripts/vue_shared/components/markdown/field.vue
+++ b/app/assets/javascripts/vue_shared/components/markdown/field.vue
@@ -1,15 +1,16 @@
<script>
-import { GlIcon, GlSafeHtmlDirective } from '@gitlab/ui';
+import { GlIcon } from '@gitlab/ui';
import $ from 'jquery';
-import '~/behaviors/markdown/render_gfm';
import { debounce, unescape } from 'lodash';
import { createAlert } from '~/flash';
import GLForm from '~/gl_form';
+import SafeHtml from '~/vue_shared/directives/safe_html';
import axios from '~/lib/utils/axios_utils';
import { stripHtml } from '~/lib/utils/text_utility';
import { __, sprintf } from '~/locale';
import Suggestions from '~/vue_shared/components/markdown/suggestions.vue';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
+import { renderGFM } from '~/behaviors/markdown/render_gfm';
import MarkdownHeader from './header.vue';
import MarkdownToolbar from './toolbar.vue';
@@ -25,7 +26,7 @@ export default {
Suggestions,
},
directives: {
- SafeHtml: GlSafeHtmlDirective,
+ SafeHtml,
},
mixins: [glFeatureFlagsMixin()],
props: {
@@ -313,7 +314,9 @@ export default {
this.markdownPreview = data.body || __('Nothing to preview.');
this.$nextTick()
- .then(() => $(this.$refs['markdown-preview']).renderGFM())
+ .then(() => {
+ renderGFM(this.$refs['markdown-preview']);
+ })
.catch(() =>
createAlert({
message: __('Error rendering Markdown preview'),
diff --git a/app/assets/javascripts/vue_shared/components/markdown/field_view.vue b/app/assets/javascripts/vue_shared/components/markdown/field_view.vue
index d77123371f2..84d40db07bb 100644
--- a/app/assets/javascripts/vue_shared/components/markdown/field_view.vue
+++ b/app/assets/javascripts/vue_shared/components/markdown/field_view.vue
@@ -1,15 +1,9 @@
<script>
-import $ from 'jquery';
-import '~/behaviors/markdown/render_gfm';
+import { renderGFM } from '~/behaviors/markdown/render_gfm';
export default {
mounted() {
- this.renderGFM();
- },
- methods: {
- renderGFM() {
- $(this.$el).renderGFM();
- },
+ renderGFM(this.$el);
},
};
</script>
diff --git a/app/assets/javascripts/vue_shared/components/markdown/markdown_editor.vue b/app/assets/javascripts/vue_shared/components/markdown/markdown_editor.vue
index c0712e46613..d01eae0308f 100644
--- a/app/assets/javascripts/vue_shared/components/markdown/markdown_editor.vue
+++ b/app/assets/javascripts/vue_shared/components/markdown/markdown_editor.vue
@@ -82,6 +82,11 @@ export default {
required: false,
default: false,
},
+ useBottomToolbar: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
},
data() {
return {
@@ -197,6 +202,7 @@ export default {
:uploads-path="uploadsPath"
:markdown="value"
:autofocus="contentEditorAutofocused"
+ :use-bottom-toolbar="useBottomToolbar"
@initialized="setEditorAsAutofocused"
@change="updateMarkdownFromContentEditor"
@loading="disableSwitchEditingControl"
diff --git a/app/assets/javascripts/vue_shared/components/markdown/suggestion_diff_row.vue b/app/assets/javascripts/vue_shared/components/markdown/suggestion_diff_row.vue
index a04f8616acb..0b598d3acaf 100644
--- a/app/assets/javascripts/vue_shared/components/markdown/suggestion_diff_row.vue
+++ b/app/assets/javascripts/vue_shared/components/markdown/suggestion_diff_row.vue
@@ -1,5 +1,5 @@
<script>
-import { GlSafeHtmlDirective as SafeHtml } from '@gitlab/ui';
+import SafeHtml from '~/vue_shared/directives/safe_html';
export default {
name: 'SuggestionDiffRow',
diff --git a/app/assets/javascripts/vue_shared/components/markdown/suggestions.vue b/app/assets/javascripts/vue_shared/components/markdown/suggestions.vue
index 30d72332c90..c307601e670 100644
--- a/app/assets/javascripts/vue_shared/components/markdown/suggestions.vue
+++ b/app/assets/javascripts/vue_shared/components/markdown/suggestions.vue
@@ -1,6 +1,6 @@
<script>
-import { GlSafeHtmlDirective as SafeHtml } from '@gitlab/ui';
import Vue from 'vue';
+import SafeHtml from '~/vue_shared/directives/safe_html';
import { createAlert } from '~/flash';
import { __ } from '~/locale';
import SuggestionDiff from './suggestion_diff.vue';
diff --git a/app/assets/javascripts/vue_shared/components/markdown_drawer/makrdown_drawer.stories.js b/app/assets/javascripts/vue_shared/components/markdown_drawer/markdown_drawer.stories.js
index 03bd64e2a57..03bd64e2a57 100644
--- a/app/assets/javascripts/vue_shared/components/markdown_drawer/makrdown_drawer.stories.js
+++ b/app/assets/javascripts/vue_shared/components/markdown_drawer/markdown_drawer.stories.js
diff --git a/app/assets/javascripts/vue_shared/components/markdown_drawer/markdown_drawer.vue b/app/assets/javascripts/vue_shared/components/markdown_drawer/markdown_drawer.vue
index a4b509f8656..379f22fdc6f 100644
--- a/app/assets/javascripts/vue_shared/components/markdown_drawer/markdown_drawer.vue
+++ b/app/assets/javascripts/vue_shared/components/markdown_drawer/markdown_drawer.vue
@@ -1,9 +1,9 @@
<script>
-import { GlSafeHtmlDirective as SafeHtml, GlDrawer, GlAlert, GlSkeletonLoader } from '@gitlab/ui';
-import $ from 'jquery';
-import '~/behaviors/markdown/render_gfm';
+import { GlDrawer, GlAlert, GlSkeletonLoader } from '@gitlab/ui';
+import SafeHtml from '~/vue_shared/directives/safe_html';
import { s__ } from '~/locale';
import { contentTop } from '~/lib/utils/common_utils';
+import { renderGFM } from '~/behaviors/markdown/render_gfm';
import { getRenderedMarkdown } from './utils/fetch';
export const cache = {};
@@ -34,13 +34,9 @@ export default {
title: '',
body: null,
open: false,
+ drawerTop: '0px',
};
},
- computed: {
- drawerOffsetTop() {
- return `${contentTop()}px`;
- },
- },
watch: {
documentPath: {
immediate: true,
@@ -76,18 +72,23 @@ export default {
cache[this.documentPath] = { title, body };
}
},
+ getDrawerTop() {
+ this.drawerTop = `${contentTop()}px`;
+ },
renderGLFM() {
this.$nextTick(() => {
- $(this.$refs['content-element']).renderGFM();
+ renderGFM(this.$refs['content-element']);
});
},
closeDrawer() {
this.open = false;
},
toggleDrawer() {
+ this.getDrawerTop();
this.open = !this.open;
},
openDrawer() {
+ this.getDrawerTop();
this.open = true;
},
},
@@ -97,7 +98,7 @@ export default {
};
</script>
<template>
- <gl-drawer :header-height="drawerOffsetTop" :open="open" header-sticky @close="closeDrawer">
+ <gl-drawer :header-height="drawerTop" :open="open" header-sticky @close="closeDrawer">
<template #title>
<h4 data-testid="title-element" class="gl-m-0">{{ title }}</h4>
</template>
diff --git a/app/assets/javascripts/vue_shared/components/markdown_drawer/utils/fetch.js b/app/assets/javascripts/vue_shared/components/markdown_drawer/utils/fetch.js
index 7c8e1bc160a..27237f2f16b 100644
--- a/app/assets/javascripts/vue_shared/components/markdown_drawer/utils/fetch.js
+++ b/app/assets/javascripts/vue_shared/components/markdown_drawer/utils/fetch.js
@@ -16,7 +16,7 @@ export const getRenderedMarkdown = (documentPath) => {
return axios
.get(helpPagePath(documentPath))
.then(({ data }) => {
- const { body, title } = splitDocument(data.html);
+ const { body, title } = splitDocument(data);
return {
body,
title,
diff --git a/app/assets/javascripts/vue_shared/components/metric_images/metric_images_tab.vue b/app/assets/javascripts/vue_shared/components/metric_images/metric_images_tab.vue
index e23721da223..2cadc87eca3 100644
--- a/app/assets/javascripts/vue_shared/components/metric_images/metric_images_tab.vue
+++ b/app/assets/javascripts/vue_shared/components/metric_images/metric_images_tab.vue
@@ -1,5 +1,5 @@
<script>
-import { GlFormGroup, GlFormInput, GlLoadingIcon, GlModal, GlTab } from '@gitlab/ui';
+import { GlFormGroup, GlFormInput, GlLoadingIcon, GlModal } from '@gitlab/ui';
import { mapState, mapActions } from 'vuex';
import { __, s__ } from '~/locale';
import UploadDropzone from '~/vue_shared/components/upload_dropzone/upload_dropzone.vue';
@@ -11,7 +11,6 @@ export default {
GlFormInput,
GlLoadingIcon,
GlModal,
- GlTab,
MetricImagesTable,
UploadDropzone,
},
@@ -82,7 +81,7 @@ export default {
</script>
<template>
- <gl-tab :title="s__('Incident|Metrics')" data-testid="metrics-tab">
+ <div>
<div v-if="isLoadingMetricImages">
<gl-loading-icon class="gl-p-5" size="sm" />
</div>
@@ -117,5 +116,5 @@ export default {
:drop-description-message="$options.i18n.dropDescription"
@change="openMetricDialog"
/>
- </gl-tab>
+ </div>
</template>
diff --git a/app/assets/javascripts/vue_shared/components/notes/placeholder_note.vue b/app/assets/javascripts/vue_shared/components/notes/placeholder_note.vue
index cf34a60c363..748d6082abd 100644
--- a/app/assets/javascripts/vue_shared/components/notes/placeholder_note.vue
+++ b/app/assets/javascripts/vue_shared/components/notes/placeholder_note.vue
@@ -16,8 +16,9 @@
* :note="{body: 'This is a note'}"
* />
*/
-import { GlSafeHtmlDirective as SafeHtml, GlAvatarLink, GlAvatar } from '@gitlab/ui';
+import { GlAvatarLink, GlAvatar } from '@gitlab/ui';
import { mapGetters } from 'vuex';
+import SafeHtml from '~/vue_shared/directives/safe_html';
import { renderMarkdown } from '~/notes/utils';
import TimelineEntryItem from '~/vue_shared/components/notes/timeline_entry_item.vue';
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 1ae5045b34f..1cbbdf0deb0 100644
--- a/app/assets/javascripts/vue_shared/components/notes/system_note.vue
+++ b/app/assets/javascripts/vue_shared/components/notes/system_note.vue
@@ -16,22 +16,17 @@
* }"
* />
*/
-import {
- GlButton,
- GlSkeletonLoader,
- GlTooltipDirective,
- GlIcon,
- GlSafeHtmlDirective as SafeHtml,
-} from '@gitlab/ui';
+import { GlButton, GlSkeletonLoader, GlTooltipDirective, GlIcon } from '@gitlab/ui';
import $ from 'jquery';
import { mapGetters, mapActions, mapState } from 'vuex';
+import SafeHtml from '~/vue_shared/directives/safe_html';
import descriptionVersionHistoryMixin from 'ee_else_ce/notes/mixins/description_version_history';
-import '~/behaviors/markdown/render_gfm';
import axios from '~/lib/utils/axios_utils';
import { __ } from '~/locale';
import NoteHeader from '~/notes/components/note_header.vue';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { spriteIcon } from '~/lib/utils/common_utils';
+import { renderGFM } from '~/behaviors/markdown/render_gfm';
import TimelineEntryItem from './timeline_entry_item.vue';
const MAX_VISIBLE_COMMIT_LIST_COUNT = 3;
@@ -94,7 +89,7 @@ export default {
},
},
mounted() {
- $(this.$refs['gfm-content']).renderGFM();
+ renderGFM(this.$refs['gfm-content']);
},
methods: {
...mapActions(['fetchDescriptionVersion', 'softDeleteDescriptionVersion']),
@@ -205,7 +200,7 @@ export default {
<tr v-for="line in lines" v-once :key="line.line_code" class="line_holder">
<td
:class="line.type"
- class="diff-line-num old_line gl-border-bottom-0! gl-border-top-0!"
+ class="diff-line-num old_line gl-border-bottom-0! gl-border-top-0! gl-border-0! gl-rounded-0!"
>
{{ line.old_line }}
</td>
@@ -217,7 +212,7 @@ export default {
</td>
<td
:class="line.type"
- class="line_content gl-display-table-cell!"
+ class="line_content gl-display-table-cell! gl-border-0! gl-rounded-0!"
v-html="line.rich_text /* eslint-disable-line vue/no-v-html */"
></td>
</tr>
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 867222279b2..57e3a97244e 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,22 +1,19 @@
<script>
-import {
- GlAlert,
- GlBadge,
- GlPagination,
- GlTab,
- GlTabs,
- GlSafeHtmlDirective as SafeHtml,
-} from '@gitlab/ui';
+import { GlAlert, GlBadge, GlPagination, GlTab, GlTabs } from '@gitlab/ui';
+import SafeHtml from '~/vue_shared/directives/safe_html';
import Api from '~/api';
import { updateHistory, setUrlParams } from '~/lib/utils/url_utility';
import Tracking from '~/tracking';
import {
- OPERATOR_IS_ONLY,
+ FILTERED_SEARCH_TERM,
+ OPERATORS_IS,
TOKEN_TITLE_ASSIGNEE,
TOKEN_TITLE_AUTHOR,
+ TOKEN_TYPE_ASSIGNEE,
+ TOKEN_TYPE_AUTHOR,
} from '~/vue_shared/components/filtered_search_bar/constants';
import FilteredSearchBar from '~/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue';
-import AuthorToken from '~/vue_shared/components/filtered_search_bar/tokens/author_token.vue';
+import UserToken from '~/vue_shared/components/filtered_search_bar/tokens/user_token.vue';
import { initialPaginationState, defaultI18n, defaultPageSize } from './constants';
import { isAny } from './utils';
@@ -95,7 +92,7 @@ export default {
filterSearchTokens: {
type: Array,
required: false,
- default: () => ['author_username', 'assignee_username'],
+ default: () => [TOKEN_TYPE_AUTHOR, TOKEN_TYPE_ASSIGNEE],
},
},
data() {
@@ -113,26 +110,26 @@ export default {
defaultTokens() {
return [
{
- type: 'author_username',
+ type: TOKEN_TYPE_AUTHOR,
icon: 'user',
title: TOKEN_TITLE_AUTHOR,
unique: true,
symbol: '@',
- token: AuthorToken,
- operators: OPERATOR_IS_ONLY,
+ token: UserToken,
+ operators: OPERATORS_IS,
fetchPath: this.projectPath,
- fetchAuthors: Api.projectUsers.bind(Api),
+ fetchUsers: Api.projectUsers.bind(Api),
},
{
- type: 'assignee_username',
+ type: TOKEN_TYPE_ASSIGNEE,
icon: 'user',
title: TOKEN_TITLE_ASSIGNEE,
unique: true,
symbol: '@',
- token: AuthorToken,
- operators: OPERATOR_IS_ONLY,
+ token: UserToken,
+ operators: OPERATORS_IS,
fetchPath: this.projectPath,
- fetchAuthors: Api.projectUsers.bind(Api),
+ fetchUsers: Api.projectUsers.bind(Api),
},
];
},
@@ -144,14 +141,14 @@ export default {
if (this.authorUsername) {
value.push({
- type: 'author_username',
+ type: TOKEN_TYPE_AUTHOR,
value: { data: this.authorUsername },
});
}
if (this.assigneeUsername) {
value.push({
- type: 'assignee_username',
+ type: TOKEN_TYPE_ASSIGNEE,
value: { data: this.assigneeUsername },
});
}
@@ -226,13 +223,13 @@ export default {
filters.forEach((filter) => {
if (typeof filter === 'object') {
switch (filter.type) {
- case 'author_username':
+ case TOKEN_TYPE_AUTHOR:
filterParams.authorUsername = isAny(filter.value.data);
break;
- case 'assignee_username':
+ case TOKEN_TYPE_ASSIGNEE:
filterParams.assigneeUsername = isAny(filter.value.data);
break;
- case 'filtered-search-term':
+ case FILTERED_SEARCH_TERM:
if (filter.value.data !== '') filterParams.search = filter.value.data;
break;
default:
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 66643ff4026..16bc8070dc1 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,9 +1,10 @@
<script>
-import { GlButton, GlIcon, GlSafeHtmlDirective as SafeHtml } from '@gitlab/ui';
+import { GlButton, GlIcon } from '@gitlab/ui';
import { isString } from 'lodash';
import highlight from '~/lib/utils/highlight';
import { truncateNamespace } from '~/lib/utils/text_utility';
import ProjectAvatar from '~/vue_shared/components/project_avatar.vue';
+import SafeHtml from '~/vue_shared/directives/safe_html';
export default {
name: 'ProjectListItem',
diff --git a/app/assets/javascripts/vue_shared/components/registry/registry_search.vue b/app/assets/javascripts/vue_shared/components/registry/registry_search.vue
index 8c9c7c63db1..c990baaa2f3 100644
--- a/app/assets/javascripts/vue_shared/components/registry/registry_search.vue
+++ b/app/assets/javascripts/vue_shared/components/registry/registry_search.vue
@@ -1,7 +1,7 @@
<script>
import { GlSorting, GlSortingItem, GlFilteredSearch } from '@gitlab/ui';
-import { FILTERED_SEARCH_TERM } from '~/packages_and_registries/shared/constants';
import { SORT_DIRECTION_UI } from '~/search/sort/constants';
+import { FILTERED_SEARCH_TERM } from '~/vue_shared/components/filtered_search_bar/constants';
const ASCENDING_ORDER = 'asc';
const DESCENDING_ORDER = 'desc';
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/todo_toggle/todo_button.stories.js b/app/assets/javascripts/vue_shared/components/sidebar/todo_toggle/todo_button.stories.js
deleted file mode 100644
index 465ee9aa0d4..00000000000
--- a/app/assets/javascripts/vue_shared/components/sidebar/todo_toggle/todo_button.stories.js
+++ /dev/null
@@ -1,21 +0,0 @@
-import TodoButton from './todo_button.vue';
-
-export default {
- component: TodoButton,
- title: 'vue_shared/sidebar/todo_toggle/todo_button',
-};
-
-const Template = (args, { argTypes }) => ({
- components: { TodoButton },
- props: Object.keys(argTypes),
- template: '<todo-button v-bind="$props" v-on="$props" />',
-});
-
-export const Default = Template.bind({});
-Default.argTypes = {
- isTodo: {
- description: 'True if to-do is unresolved (i.e. not "done")',
- control: { type: 'boolean' },
- },
- click: { action: 'clicked' },
-};
diff --git a/app/assets/javascripts/vue_shared/components/source_viewer/components/chunk.vue b/app/assets/javascripts/vue_shared/components/source_viewer/components/chunk.vue
index a2d8b7cbd15..28a16cd846a 100644
--- a/app/assets/javascripts/vue_shared/components/source_viewer/components/chunk.vue
+++ b/app/assets/javascripts/vue_shared/components/source_viewer/components/chunk.vue
@@ -1,6 +1,6 @@
<script>
-import { GlIntersectionObserver, GlSafeHtmlDirective } from '@gitlab/ui';
-import { scrollToElement } from '~/lib/utils/common_utils';
+import { GlIntersectionObserver } from '@gitlab/ui';
+import LineHighlighter from '~/blob/line_highlighter';
import ChunkLine from './chunk_line.vue';
/*
@@ -20,9 +20,6 @@ export default {
ChunkLine,
GlIntersectionObserver,
},
- directives: {
- SafeHtml: GlSafeHtmlDirective,
- },
props: {
isFirstChunk: {
type: Boolean,
@@ -84,12 +81,14 @@ export default {
return;
}
- window.requestIdleCallback(() => {
+ window.requestIdleCallback(async () => {
this.isLoading = false;
const { hash } = this.$route;
if (hash && this.totalChunks > 0 && this.totalChunks === this.chunkIndex + 1) {
// when the last chunk is loaded scroll to the hash
- scrollToElement(hash, { behavior: 'auto' });
+ await this.$nextTick();
+ const lineHighlighter = new LineHighlighter({ scrollBehavior: 'auto' });
+ lineHighlighter.highlightHash(hash);
}
});
},
diff --git a/app/assets/javascripts/vue_shared/components/source_viewer/components/chunk_line.vue b/app/assets/javascripts/vue_shared/components/source_viewer/components/chunk_line.vue
index 0bf19f83d86..ce6741f33b1 100644
--- a/app/assets/javascripts/vue_shared/components/source_viewer/components/chunk_line.vue
+++ b/app/assets/javascripts/vue_shared/components/source_viewer/components/chunk_line.vue
@@ -1,11 +1,11 @@
<script>
-import { GlSafeHtmlDirective } from '@gitlab/ui';
+import SafeHtml from '~/vue_shared/directives/safe_html';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { getPageParamValue, getPageSearchString } from '~/blob/utils';
export default {
directives: {
- SafeHtml: GlSafeHtmlDirective,
+ SafeHtml,
},
mixins: [glFeatureFlagMixin()],
props: {
diff --git a/app/assets/javascripts/vue_shared/components/source_viewer/plugins/link_dependencies.js b/app/assets/javascripts/vue_shared/components/source_viewer/plugins/link_dependencies.js
index fca2616f069..cd15916851c 100644
--- a/app/assets/javascripts/vue_shared/components/source_viewer/plugins/link_dependencies.js
+++ b/app/assets/javascripts/vue_shared/components/source_viewer/plugins/link_dependencies.js
@@ -4,6 +4,7 @@ import godepsJsonLinker from './utils/godeps_json_linker';
import gemfileLinker from './utils/gemfile_linker';
import podspecJsonLinker from './utils/podspec_json_linker';
import composerJsonLinker from './utils/composer_json_linker';
+import goSumLinker from './utils/go_sum_linker';
const DEPENDENCY_LINKERS = {
package_json: packageJsonLinker,
@@ -12,6 +13,7 @@ const DEPENDENCY_LINKERS = {
gemfile: gemfileLinker,
podspec_json: podspecJsonLinker,
composer_json: composerJsonLinker,
+ go_sum: goSumLinker,
};
/**
diff --git a/app/assets/javascripts/vue_shared/components/source_viewer/plugins/utils/go_sum_linker.js b/app/assets/javascripts/vue_shared/components/source_viewer/plugins/utils/go_sum_linker.js
new file mode 100644
index 00000000000..b290dfa78b9
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/source_viewer/plugins/utils/go_sum_linker.js
@@ -0,0 +1,34 @@
+import { createLink } from './dependency_linker_util';
+
+const openTag = '<span class="">';
+const closeTag = '</span>';
+const TAG_URL = 'https://sum.golang.org/lookup/';
+const GO_PACKAGE_URL = 'https://pkg.go.dev/';
+
+const DEPENDENCY_REGEX = new RegExp(
+ /*
+ * Detects dependencies inside of content that is highlighted by Highlight.js
+ * Example: '<span class="">cloud.google.com/Go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=</span>'
+ * Group 1 (packagePath): 'cloud.google.com/Go/bigquery'
+ * Group 2 (version): 'v1.0.1/go.mod'
+ * Group 3 (base64url): 'i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o='
+ */
+ `${openTag}(.*) (v.*) h1:(.*)${closeTag}`,
+ 'gm',
+);
+
+const handleReplace = (packagePath, version, tag) => {
+ const lowercasePath = packagePath.toLowerCase();
+ const packageHref = `${GO_PACKAGE_URL}${lowercasePath}`;
+ const packageLink = createLink(packageHref, packagePath);
+ const tagHref = `${TAG_URL}${lowercasePath}@${version.split('/go.mod')[0]}`;
+ const tagLink = createLink(tagHref, tag);
+
+ return `${openTag}${packageLink} ${version} h1:${tagLink}${closeTag}`;
+};
+
+export default (result) => {
+ return result.value.replace(DEPENDENCY_REGEX, (_, packagePath, version, tag) =>
+ handleReplace(packagePath, version, tag),
+ );
+};
diff --git a/app/assets/javascripts/vue_shared/components/source_viewer/source_viewer.vue b/app/assets/javascripts/vue_shared/components/source_viewer/source_viewer.vue
index f621a23734a..0cfee93ce5d 100644
--- a/app/assets/javascripts/vue_shared/components/source_viewer/source_viewer.vue
+++ b/app/assets/javascripts/vue_shared/components/source_viewer/source_viewer.vue
@@ -1,5 +1,5 @@
<script>
-import { GlSafeHtmlDirective, GlLoadingIcon } from '@gitlab/ui';
+import { GlLoadingIcon } from '@gitlab/ui';
import LineHighlighter from '~/blob/line_highlighter';
import eventHub from '~/notes/event_hub';
import languageLoader from '~/content_editor/services/highlight_js_language_loader';
@@ -28,9 +28,6 @@ export default {
GlLoadingIcon,
Chunk,
},
- directives: {
- SafeHtml: GlSafeHtmlDirective,
- },
mixins: [Tracking.mixin()],
props: {
blob: {
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 80c1fcbacfa..d06bc7b8f98 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
@@ -4,11 +4,11 @@ import {
GlLink,
GlSkeletonLoader,
GlIcon,
- GlSafeHtmlDirective,
GlSprintf,
GlButton,
GlAvatarLabeled,
} from '@gitlab/ui';
+import SafeHtml from '~/vue_shared/directives/safe_html';
import { glEmojiTag } from '~/emoji';
import { createAlert } from '~/flash';
import { followUser, unfollowUser } from '~/rest_api';
@@ -44,7 +44,7 @@ export default {
GlAvatarLabeled,
},
directives: {
- SafeHtml: GlSafeHtmlDirective,
+ SafeHtml,
},
mixins: [Tracking.mixin()],
props: {
diff --git a/app/assets/javascripts/vue_shared/components/web_ide_link.vue b/app/assets/javascripts/vue_shared/components/web_ide_link.vue
index 6d179b3dc92..383dc27ea5e 100644
--- a/app/assets/javascripts/vue_shared/components/web_ide_link.vue
+++ b/app/assets/javascripts/vue_shared/components/web_ide_link.vue
@@ -1,14 +1,16 @@
<script>
-import { GlModal, GlSprintf, GlLink } from '@gitlab/ui';
+import { GlModal, GlSprintf, GlLink, GlPopover } from '@gitlab/ui';
import { s__, __ } from '~/locale';
+import UserCalloutDismisser from '~/vue_shared/components/user_callout_dismisser.vue';
import ActionsButton from '~/vue_shared/components/actions_button.vue';
import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue';
import ConfirmForkModal from '~/vue_shared/components/confirm_fork_modal.vue';
+import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
-const KEY_EDIT = 'edit';
-const KEY_WEB_IDE = 'webide';
-const KEY_GITPOD = 'gitpod';
-const KEY_PIPELINE_EDITOR = 'pipeline_editor';
+export const KEY_EDIT = 'edit';
+export const KEY_WEB_IDE = 'webide';
+export const KEY_GITPOD = 'gitpod';
+export const KEY_PIPELINE_EDITOR = 'pipeline_editor';
export const i18n = {
modal: {
@@ -25,6 +27,9 @@ export const i18n = {
),
};
+export const PREFERRED_EDITOR_KEY = 'gl-web-ide-button-selected';
+export const PREFERRED_EDITOR_RESET_KEY = 'gl-web-ide-button-selected-reset';
+
export default {
components: {
ActionsButton,
@@ -32,9 +37,12 @@ export default {
GlModal,
GlSprintf,
GlLink,
+ GlPopover,
ConfirmForkModal,
+ UserCalloutDismisser,
},
i18n,
+ mixins: [glFeatureFlagsMixin()],
props: {
isFork: {
type: Boolean,
@@ -131,6 +139,11 @@ export default {
required: false,
default: '',
},
+ webIdePromoPopoverImg: {
+ type: String,
+ required: false,
+ default: '',
+ },
},
data() {
return {
@@ -296,6 +309,12 @@ export default {
},
};
},
+ displayVscodeWebIdeCallout() {
+ return this.glFeatures.vscodeWebIde && !this.showEditButton;
+ },
+ },
+ mounted() {
+ this.resetPreferredEditor();
},
methods: {
select(key) {
@@ -304,41 +323,109 @@ export default {
showModal(dataKey) {
this[dataKey] = true;
},
+ resetPreferredEditor() {
+ if (!this.glFeatures.vscodeWebIde || this.showEditButton) {
+ return;
+ }
+
+ if (localStorage.getItem(PREFERRED_EDITOR_RESET_KEY) === 'true') {
+ return;
+ }
+
+ localStorage.setItem(PREFERRED_EDITOR_KEY, KEY_WEB_IDE);
+ localStorage.setItem(PREFERRED_EDITOR_RESET_KEY, true);
+
+ this.select(KEY_WEB_IDE);
+ },
+ dismissCalloutOnActionClicked(dismiss) {
+ if (this.displayVscodeWebIdeCallout) {
+ dismiss();
+ }
+ },
},
+ webIdeButtonId: 'web-ide-link',
+ PREFERRED_EDITOR_KEY,
};
</script>
<template>
- <div class="gl-sm-ml-3">
- <actions-button
- :actions="actions"
- :selected-key="selection"
- :variant="isBlob ? 'confirm' : 'default'"
- :category="isBlob ? 'primary' : 'secondary'"
- @select="select"
- />
- <local-storage-sync
- storage-key="gl-web-ide-button-selected"
- :value="selection"
- as-string
- @input="select"
- />
- <gl-modal
- v-if="computedShowGitpodButton && !gitpodEnabled"
- v-model="showEnableGitpodModal"
- v-bind="enableGitpodModalProps"
- >
- <gl-sprintf :message="$options.i18n.modal.content">
- <template #link="{ content }">
- <gl-link :href="userPreferencesGitpodPath">{{ content }}</gl-link>
- </template>
- </gl-sprintf>
- </gl-modal>
- <confirm-fork-modal
- v-if="showWebIdeButton || showEditButton"
- v-model="showForkModal"
- :modal-id="forkModalId"
- :fork-path="forkPath"
- />
- </div>
+ <user-callout-dismisser
+ :skip-query="!displayVscodeWebIdeCallout"
+ feature-name="vscode_web_ide_callout"
+ >
+ <template #default="{ dismiss, shouldShowCallout }">
+ <div class="gl-sm-ml-3">
+ <actions-button
+ :id="$options.webIdeButtonId"
+ :actions="actions"
+ :selected-key="selection"
+ :variant="isBlob ? 'confirm' : 'default'"
+ :category="isBlob ? 'primary' : 'secondary'"
+ :show-action-tooltip="!displayVscodeWebIdeCallout || !shouldShowCallout"
+ @select="select"
+ @actionClicked="dismissCalloutOnActionClicked(dismiss)"
+ />
+ <local-storage-sync
+ :storage-key="$options.PREFERRED_EDITOR_KEY"
+ :value="selection"
+ as-string
+ @input="select"
+ />
+ <gl-modal
+ v-if="computedShowGitpodButton && !gitpodEnabled"
+ v-model="showEnableGitpodModal"
+ v-bind="enableGitpodModalProps"
+ >
+ <gl-sprintf :message="$options.i18n.modal.content">
+ <template #link="{ content }">
+ <gl-link :href="userPreferencesGitpodPath">{{ content }}</gl-link>
+ </template>
+ </gl-sprintf>
+ </gl-modal>
+ <confirm-fork-modal
+ v-if="showWebIdeButton || showEditButton"
+ v-model="showForkModal"
+ :modal-id="forkModalId"
+ :fork-path="forkPath"
+ />
+ <gl-popover
+ v-if="displayVscodeWebIdeCallout"
+ :target="$options.webIdeButtonId"
+ :show="shouldShowCallout"
+ :css-classes="['web-ide-promo-popover']"
+ :boundary-padding="80"
+ show-close-button
+ triggers="manual"
+ @close-button-clicked="dismiss"
+ >
+ <img
+ :src="webIdePromoPopoverImg"
+ class="web-ide-promo-popover-illustration"
+ width="280"
+ height="140"
+ />
+ <div class="gl-mx-2">
+ <h5 class="gl-mt-3 gl-mb-3">{{ __('The new Web IDE') }}</h5>
+ <p>
+ {{
+ __(
+ 'VS Code in your browser. View code and make changes from the same UI as in your local IDE.',
+ )
+ }}
+ </p>
+ <gl-link
+ class="gl-button btn btn-confirm block gl-mb-4 gl-mt-5"
+ variant="confirm"
+ category="primary"
+ target="_blank"
+ :href="webIdeUrl"
+ block
+ >
+ {{ __('Try it out now') }}
+ </gl-link>
+ </div>
+ </gl-popover>
+ </div>
+ </template>
+ </user-callout-dismisser>
</template>
diff --git a/app/assets/javascripts/vue_shared/constants.js b/app/assets/javascripts/vue_shared/constants.js
index a851f84ed2f..2f85a29fb84 100644
--- a/app/assets/javascripts/vue_shared/constants.js
+++ b/app/assets/javascripts/vue_shared/constants.js
@@ -13,7 +13,9 @@ export const SHORT_DATE_FORMAT = 'd mmm, yyyy';
export const ISO_SHORT_FORMAT = 'yyyy-mm-dd';
-export const DATE_FORMATS = [SHORT_DATE_FORMAT, ISO_SHORT_FORMAT];
+export const LONG_DATE_FORMAT_WITH_TZ = 'yyyy-mm-dd HH:MM:ss Z';
+
+export const DATE_FORMATS = [SHORT_DATE_FORMAT, ISO_SHORT_FORMAT, LONG_DATE_FORMAT_WITH_TZ];
const getTimeLabel = (days) => n__('1 day', '%d days', days);
diff --git a/app/assets/javascripts/vue_shared/issuable/create/components/issuable_form.vue b/app/assets/javascripts/vue_shared/issuable/create/components/issuable_form.vue
index 25799171905..2644befc902 100644
--- a/app/assets/javascripts/vue_shared/issuable/create/components/issuable_form.vue
+++ b/app/assets/javascripts/vue_shared/issuable/create/components/issuable_form.vue
@@ -1,8 +1,8 @@
<script>
import { GlForm, GlFormInput, GlFormGroup } from '@gitlab/ui';
import MarkdownField from '~/vue_shared/components/markdown/field.vue';
-import { DropdownVariant } from '~/vue_shared/components/sidebar/labels_select_vue/constants';
-import LabelsSelect from '~/vue_shared/components/sidebar/labels_select_vue/labels_select_root.vue';
+import { DropdownVariant } from '~/sidebar/components/labels/labels_select_vue/constants';
+import LabelsSelect from '~/sidebar/components/labels/labels_select_vue/labels_select_root.vue';
export default {
LabelSelectVariant: DropdownVariant,
diff --git a/app/assets/javascripts/vue_shared/issuable/create/components/issuable_label_selector.vue b/app/assets/javascripts/vue_shared/issuable/create/components/issuable_label_selector.vue
new file mode 100644
index 00000000000..b3f9c8d9fcd
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/issuable/create/components/issuable_label_selector.vue
@@ -0,0 +1,92 @@
+<script>
+import { GlFormGroup, GlIcon } from '@gitlab/ui';
+import { getIdFromGraphQLId } from '~/graphql_shared/utils';
+import LabelsSelect from '~/sidebar/components/labels/labels_select_widget/labels_select_root.vue';
+import { __ } from '~/locale';
+
+export default {
+ components: {
+ GlFormGroup,
+ GlIcon,
+ LabelsSelect,
+ },
+ inject: [
+ 'allowLabelRemove',
+ 'attrWorkspacePath',
+ 'fieldName',
+ 'fullPath',
+ 'labelsFilterBasePath',
+ 'initialLabels',
+ 'issuableType',
+ 'labelType',
+ 'variant',
+ 'workspaceType',
+ ],
+ data() {
+ return {
+ selectedLabels: this.initialLabels || [],
+ };
+ },
+ methods: {
+ handleUpdateSelectedLabels({ labels }) {
+ this.selectedLabels = labels.map((label) => ({ ...label, id: getIdFromGraphQLId(label.id) }));
+ },
+ handleLabelRemove(labelId) {
+ this.selectedLabels = this.selectedLabels.filter((label) => label.id !== labelId);
+ },
+ },
+ i18n: {
+ fieldLabel: __('Labels'),
+ dropdownButtonText: __('Select label'),
+ listTitle: __('Select label'),
+ createTitle: __('Create project label'),
+ manageTitle: __('Manage project labels'),
+ emptySelection: __('None'),
+ },
+};
+</script>
+
+<template>
+ <gl-form-group class="row" label-class="gl-display-none">
+ <label class="col-12 gl-display-flex gl-align-center gl-mb-1">
+ {{ $options.i18n.fieldLabel }}
+ <div class="gl-ml-3">
+ <gl-icon name="labels" />
+ <span class="gl-font-base gl-line-height-24">{{ selectedLabels.length }}</span>
+ </div>
+ </label>
+ <div class="col-12">
+ <div class="issuable-form-select-holder">
+ <input
+ v-for="selectedLabel in selectedLabels"
+ :key="selectedLabel.id"
+ :value="selectedLabel.id"
+ :name="fieldName"
+ type="hidden"
+ />
+ <labels-select
+ class="block labels"
+ :allow-label-remove="allowLabelRemove"
+ :allow-multiselect="true"
+ :show-embedded-labels-list="true"
+ :full-path="fullPath"
+ :attr-workspace-path="attrWorkspacePath"
+ :labels-filter-base-path="labelsFilterBasePath"
+ :dropdown-button-text="$options.i18n.dropdownButtonText"
+ :labels-list-title="$options.i18n.listTitle"
+ :footer-create-label-title="$options.i18n.createTitle"
+ :footer-manage-label-title="$options.i18n.manageTitle"
+ :variant="variant"
+ :workspace-type="workspaceType"
+ :issuable-type="issuableType"
+ :label-create-type="labelType"
+ :selected-labels="selectedLabels"
+ @updateSelectedLabels="handleUpdateSelectedLabels"
+ @onLabelRemove="handleLabelRemove"
+ >
+ {{ $options.i18n.emptySelection }}
+ </labels-select>
+ </div>
+ </div>
+ </gl-form-group>
+</template>
diff --git a/app/assets/javascripts/vue_shared/issuable/list/components/issuable_item.vue b/app/assets/javascripts/vue_shared/issuable/list/components/issuable_item.vue
index 30b7b073ac3..5b303b9a314 100644
--- a/app/assets/javascripts/vue_shared/issuable/list/components/issuable_item.vue
+++ b/app/assets/javascripts/vue_shared/issuable/list/components/issuable_item.vue
@@ -318,8 +318,8 @@ export default {
<slot name="statistics"></slot>
<li
v-if="showDiscussions"
- data-testid="issuable-discussions"
- class="issuable-comments gl-display-none gl-sm-display-block"
+ class="gl-display-none gl-sm-display-block"
+ data-testid="issuable-comments"
>
<gl-link
v-gl-tooltip.top
diff --git a/app/assets/javascripts/vue_shared/issuable/list/components/issuable_list_root.vue b/app/assets/javascripts/vue_shared/issuable/list/components/issuable_list_root.vue
index dd3d7c8f4d6..5b6c5bf6e03 100644
--- a/app/assets/javascripts/vue_shared/issuable/list/components/issuable_list_root.vue
+++ b/app/assets/javascripts/vue_shared/issuable/list/components/issuable_list_root.vue
@@ -331,6 +331,7 @@ export default {
<slot name="sidebar-items" :checked-issuables="bulkEditIssuables"></slot>
</template>
</issuable-bulk-edit-sidebar>
+ <slot name="list-body"></slot>
<ul v-if="issuablesLoading" class="content-list">
<li v-for="n in skeletonItemCount" :key="n" class="issue gl-px-5! gl-py-5!">
<gl-skeleton-loader />
diff --git a/app/assets/javascripts/vue_shared/issuable/show/components/issuable_description.vue b/app/assets/javascripts/vue_shared/issuable/show/components/issuable_description.vue
index d4e9120ff17..ce1851ab873 100644
--- a/app/assets/javascripts/vue_shared/issuable/show/components/issuable_description.vue
+++ b/app/assets/javascripts/vue_shared/issuable/show/components/issuable_description.vue
@@ -1,7 +1,6 @@
<script>
-import { GlSafeHtmlDirective as SafeHtml } from '@gitlab/ui';
-import $ from 'jquery';
-import '~/behaviors/markdown/render_gfm';
+import SafeHtml from '~/vue_shared/directives/safe_html';
+import { renderGFM } from '~/behaviors/markdown/render_gfm';
export default {
directives: {
@@ -26,12 +25,7 @@ export default {
},
},
mounted() {
- this.renderGFM();
- },
- methods: {
- renderGFM() {
- $(this.$refs.gfmContainer).renderGFM();
- },
+ renderGFM(this.$refs.gfmContainer);
},
};
</script>
diff --git a/app/assets/javascripts/vue_shared/issuable/show/components/issuable_title.vue b/app/assets/javascripts/vue_shared/issuable/show/components/issuable_title.vue
index 35124bd15d2..fd94245b7c9 100644
--- a/app/assets/javascripts/vue_shared/issuable/show/components/issuable_title.vue
+++ b/app/assets/javascripts/vue_shared/issuable/show/components/issuable_title.vue
@@ -1,12 +1,6 @@
<script>
-import {
- GlIcon,
- GlBadge,
- GlButton,
- GlIntersectionObserver,
- GlTooltipDirective,
- GlSafeHtmlDirective as SafeHtml,
-} from '@gitlab/ui';
+import { GlIcon, GlBadge, GlButton, GlIntersectionObserver, GlTooltipDirective } from '@gitlab/ui';
+import SafeHtml from '~/vue_shared/directives/safe_html';
import { __ } from '~/locale';
import { IssuableStates } from '~/vue_shared/issuable/list/constants';
diff --git a/app/assets/javascripts/vue_shared/new_namespace/components/legacy_container.vue b/app/assets/javascripts/vue_shared/new_namespace/components/legacy_container.vue
index e42720bf1db..ae40076ca96 100644
--- a/app/assets/javascripts/vue_shared/new_namespace/components/legacy_container.vue
+++ b/app/assets/javascripts/vue_shared/new_namespace/components/legacy_container.vue
@@ -1,4 +1,6 @@
<script>
+import projectNew from '~/projects/project_new';
+
export default {
inheritAttrs: false,
props: {
@@ -16,6 +18,7 @@ export default {
this.source = legacyEntry.parentNode;
this.$el.appendChild(legacyEntry);
legacyEntry.classList.add('active');
+ projectNew.bindEvents();
}
},
diff --git a/app/assets/javascripts/vue_shared/new_namespace/components/welcome.vue b/app/assets/javascripts/vue_shared/new_namespace/components/welcome.vue
index 5cd2018bb8c..b6a459f21e0 100644
--- a/app/assets/javascripts/vue_shared/new_namespace/components/welcome.vue
+++ b/app/assets/javascripts/vue_shared/new_namespace/components/welcome.vue
@@ -1,5 +1,5 @@
<script>
-import { GlSafeHtmlDirective as SafeHtml } from '@gitlab/ui';
+import SafeHtml from '~/vue_shared/directives/safe_html';
import Tracking from '~/tracking';
export default {
diff --git a/app/assets/javascripts/vue_shared/new_namespace/new_namespace_page.vue b/app/assets/javascripts/vue_shared/new_namespace/new_namespace_page.vue
index 624ae7027d5..318adec2319 100644
--- a/app/assets/javascripts/vue_shared/new_namespace/new_namespace_page.vue
+++ b/app/assets/javascripts/vue_shared/new_namespace/new_namespace_page.vue
@@ -1,5 +1,6 @@
<script>
-import { GlBreadcrumb, GlIcon, GlSafeHtmlDirective as SafeHtml } from '@gitlab/ui';
+import { GlBreadcrumb, GlIcon } from '@gitlab/ui';
+import SafeHtml from '~/vue_shared/directives/safe_html';
import NewTopLevelGroupAlert from '~/groups/components/new_top_level_group_alert.vue';
import LegacyContainer from './components/legacy_container.vue';
diff --git a/app/assets/javascripts/vue_shared/security_reports/security_reports_app.vue b/app/assets/javascripts/vue_shared/security_reports/security_reports_app.vue
index 0e1975e1c09..b739baad5d7 100644
--- a/app/assets/javascripts/vue_shared/security_reports/security_reports_app.vue
+++ b/app/assets/javascripts/vue_shared/security_reports/security_reports_app.vue
@@ -2,8 +2,8 @@
import { mapActions, mapGetters } from 'vuex';
import { createAlert } from '~/flash';
import { s__ } from '~/locale';
-import ReportSection from '~/reports/components/report_section.vue';
-import { ERROR, SLOT_SUCCESS, SLOT_LOADING, SLOT_ERROR } from '~/reports/constants';
+import ReportSection from '~/ci/reports/components/report_section.vue';
+import { ERROR, SLOT_SUCCESS, SLOT_LOADING, SLOT_ERROR } from '~/ci/reports/constants';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import HelpIcon from './components/help_icon.vue';
import SecurityReportDownloadDropdown from './components/security_report_download_dropdown.vue';
diff --git a/app/assets/javascripts/vue_shared/security_reports/store/getters.js b/app/assets/javascripts/vue_shared/security_reports/store/getters.js
index 08f6bcca15b..c274f531139 100644
--- a/app/assets/javascripts/vue_shared/security_reports/store/getters.js
+++ b/app/assets/javascripts/vue_shared/security_reports/store/getters.js
@@ -1,5 +1,5 @@
import { s__, sprintf } from '~/locale';
-import { LOADING, ERROR, SUCCESS } from '~/reports/constants';
+import { LOADING, ERROR, SUCCESS } from '~/ci/reports/constants';
import { TRANSLATION_IS_LOADING } from './messages';
import { countVulnerabilities, groupedTextBuilder } from './utils';
diff --git a/app/assets/javascripts/vue_shared/security_reports/store/utils.js b/app/assets/javascripts/vue_shared/security_reports/store/utils.js
index a6628fa0f9f..f3cb5fc16f0 100644
--- a/app/assets/javascripts/vue_shared/security_reports/store/utils.js
+++ b/app/assets/javascripts/vue_shared/security_reports/store/utils.js
@@ -29,7 +29,13 @@ export const fetchDiffData = (state, endpoint, category) => {
*/
export const enrichVulnerabilityWithFeedback = (vulnerability, feedback = []) =>
feedback
- .filter((fb) => fb.project_fingerprint === vulnerability.project_fingerprint)
+ .filter((fb) =>
+ // Some records still have a `finding_uuid` with null, we need to fallback to using `project_fingerprint` in those cases. Once all entries have been fixed, we can remove the fallback.
+ // related epic: https://gitlab.com/groups/gitlab-org/-/epics/2791
+ fb.finding_uuid !== null
+ ? fb.finding_uuid === vulnerability.finding_uuid
+ : fb.project_fingerprint === vulnerability.project_fingerprint,
+ )
.reduce((vuln, fb) => {
if (fb.feedback_type === FEEDBACK_TYPE_DISMISSAL) {
return {
diff --git a/app/assets/javascripts/webhooks/components/push_events.vue b/app/assets/javascripts/webhooks/components/push_events.vue
index 677f06314e0..91d7e21500a 100644
--- a/app/assets/javascripts/webhooks/components/push_events.vue
+++ b/app/assets/javascripts/webhooks/components/push_events.vue
@@ -33,7 +33,7 @@ export default {
<template>
<div>
- <gl-form-checkbox v-model="pushEventsData">{{ s__('Webhooks|Push events') }}</gl-form-checkbox>
+ <gl-form-checkbox v-model="pushEventsData">{{ __('Push events') }}</gl-form-checkbox>
<input type="hidden" :value="pushEventsData" name="hook[push_events]" />
<div v-if="pushEventsData" class="gl-pl-6">
diff --git a/app/assets/javascripts/webhooks/constants.js b/app/assets/javascripts/webhooks/constants.js
index 6710a418117..96632b47e6b 100644
--- a/app/assets/javascripts/webhooks/constants.js
+++ b/app/assets/javascripts/webhooks/constants.js
@@ -7,13 +7,13 @@ export const BRANCH_FILTER_REGEX = 'regex';
export const WILDCARD_CODE_STABLE = '*-stable';
export const WILDCARD_CODE_PRODUCTION = 'production/*';
-export const REGEX_CODE = '(feature|hotfix)/*';
+export const REGEX_CODE = '^(feature|hotfix)/';
export const descriptionText = {
[BRANCH_FILTER_WILDCARD]: s__(
'Webhooks|Wildcards such as %{WILDCARD_CODE_STABLE} or %{WILDCARD_CODE_PRODUCTION} are supported.',
),
- [BRANCH_FILTER_REGEX]: s__('Webhooks|Regex such as %{REGEX_CODE} is supported.'),
+ [BRANCH_FILTER_REGEX]: s__('Webhooks|Regular expressions such as %{REGEX_CODE} are supported.'),
};
export const MASK_ITEM_VALUE_HIDDEN = '************';
diff --git a/app/assets/javascripts/whats_new/components/feature.vue b/app/assets/javascripts/whats_new/components/feature.vue
index c954a86e593..044a6db6d93 100644
--- a/app/assets/javascripts/whats_new/components/feature.vue
+++ b/app/assets/javascripts/whats_new/components/feature.vue
@@ -1,5 +1,6 @@
<script>
-import { GlBadge, GlIcon, GlLink, GlSafeHtmlDirective, GlButton } from '@gitlab/ui';
+import { GlBadge, GlIcon, GlLink, GlButton } from '@gitlab/ui';
+import SafeHtml from '~/vue_shared/directives/safe_html';
import { dateInWords, isValidDate } from '~/lib/utils/datetime_utility';
export default {
@@ -10,7 +11,7 @@ export default {
GlButton,
},
directives: {
- SafeHtml: GlSafeHtmlDirective,
+ SafeHtml,
},
props: {
feature: {
diff --git a/app/assets/javascripts/work_items/components/notes/system_note.vue b/app/assets/javascripts/work_items/components/notes/system_note.vue
new file mode 100644
index 00000000000..92a2fcaf1df
--- /dev/null
+++ b/app/assets/javascripts/work_items/components/notes/system_note.vue
@@ -0,0 +1,229 @@
+<script>
+/**
+ * Common component to render a system note, icon and user information.
+ *
+ * This component need not be used with any store neither has any vuex dependency
+ *
+ * @example
+ * <system-note
+ * :note="{
+ * id: String,
+ * author: Object,
+ * createdAt: String,
+ * bodyHtml: String,
+ * systemNoteIconName: String
+ * }"
+ * />
+ */
+import { GlButton, GlSkeletonLoader, GlTooltipDirective, GlIcon } from '@gitlab/ui';
+import $ from 'jquery';
+import { renderGFM } from '~/behaviors/markdown/render_gfm';
+import SafeHtml from '~/vue_shared/directives/safe_html';
+import descriptionVersionHistoryMixin from 'ee_else_ce/notes/mixins/description_version_history';
+import axios from '~/lib/utils/axios_utils';
+import { getLocationHash } from '~/lib/utils/url_utility';
+import { __ } from '~/locale';
+import NoteHeader from '~/notes/components/note_header.vue';
+import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
+import TimelineEntryItem from '~/vue_shared/components/notes/timeline_entry_item.vue';
+
+const MAX_VISIBLE_COMMIT_LIST_COUNT = 3;
+
+export default {
+ i18n: {
+ deleteButtonLabel: __('Remove description history'),
+ },
+ name: 'SystemNote',
+ components: {
+ GlIcon,
+ NoteHeader,
+ TimelineEntryItem,
+ GlButton,
+ GlSkeletonLoader,
+ },
+ directives: {
+ GlTooltip: GlTooltipDirective,
+ SafeHtml,
+ },
+ mixins: [descriptionVersionHistoryMixin, glFeatureFlagsMixin()],
+ props: {
+ note: {
+ type: Object,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ expanded: false,
+ lines: [],
+ showLines: false,
+ loadingDiff: false,
+ isLoadingDescriptionVersion: false,
+ };
+ },
+ computed: {
+ targetNoteHash() {
+ return getLocationHash();
+ },
+ descriptionVersions() {
+ return [];
+ },
+ noteAnchorId() {
+ return `note_${this.note.id}`;
+ },
+ isTargetNote() {
+ return this.targetNoteHash === this.noteAnchorId;
+ },
+ toggleIcon() {
+ return this.expanded ? 'chevron-up' : 'chevron-down';
+ },
+ // following 2 methods taken from code in `collapseLongCommitList` of notes.js:
+ actionTextHtml() {
+ return $(this.note.bodyHtml).unwrap().html();
+ },
+ hasMoreCommits() {
+ return $(this.note.bodyHtml).filter('ul').children().length > MAX_VISIBLE_COMMIT_LIST_COUNT;
+ },
+ descriptionVersion() {
+ return this.descriptionVersions[this.note.description_version_id];
+ },
+ },
+ mounted() {
+ renderGFM(this.$refs['gfm-content']);
+ },
+ methods: {
+ fetchDescriptionVersion() {},
+ softDeleteDescriptionVersion() {},
+
+ async toggleDiff() {
+ this.showLines = !this.showLines;
+
+ if (!this.lines.length) {
+ this.loadingDiff = true;
+ const { data } = await axios.get(this.note.outdated_line_change_path);
+
+ this.lines = data.map((l) => ({
+ ...l,
+ rich_text: l.rich_text.replace(/^[+ -]/, ''),
+ }));
+ this.loadingDiff = false;
+ }
+ },
+ },
+ safeHtmlConfig: {
+ ADD_TAGS: ['use'], // to support icon SVGs
+ },
+ userColorSchemeClass: window.gon.user_color_scheme,
+};
+</script>
+
+<template>
+ <timeline-entry-item
+ :id="noteAnchorId"
+ :class="{ target: isTargetNote, 'pr-0': shouldShowDescriptionVersion }"
+ class="note system-note note-wrapper"
+ >
+ <div class="timeline-icon"><gl-icon :name="note.systemNoteIconName" /></div>
+ <div class="timeline-content">
+ <div class="note-header">
+ <note-header
+ :author="note.author"
+ :created-at="note.createdAt"
+ :note-id="note.id"
+ :is-system-note="true"
+ >
+ <span ref="gfm-content" v-safe-html="actionTextHtml"></span>
+ <template
+ v-if="canSeeDescriptionVersion || note.outdated_line_change_path"
+ #extra-controls
+ >
+ &middot;
+ <gl-button
+ v-if="canSeeDescriptionVersion"
+ variant="link"
+ :icon="descriptionVersionToggleIcon"
+ data-testid="compare-btn"
+ class="gl-vertical-align-text-bottom gl-font-sm!"
+ @click="toggleDescriptionVersion"
+ >{{ __('Compare with previous version') }}</gl-button
+ >
+ <gl-button
+ v-if="note.outdated_line_change_path"
+ :icon="showLines ? 'chevron-up' : 'chevron-down'"
+ variant="link"
+ data-testid="outdated-lines-change-btn"
+ class="gl-vertical-align-text-bottom gl-font-sm!"
+ @click="toggleDiff"
+ >
+ {{ __('Compare changes') }}
+ </gl-button>
+ </template>
+ </note-header>
+ </div>
+ <div class="note-body">
+ <div
+ v-safe-html="note.bodyHtml"
+ :class="{ 'system-note-commit-list': hasMoreCommits, 'hide-shade': expanded }"
+ class="note-text md"
+ ></div>
+ <div v-if="hasMoreCommits" class="flex-list">
+ <div class="system-note-commit-list-toggler flex-row" @click="expanded = !expanded">
+ <gl-icon :name="toggleIcon" :size="8" class="gl-mr-2" />
+ <span>{{ __('Toggle commit list') }}</span>
+ </div>
+ </div>
+ <div v-if="shouldShowDescriptionVersion" class="description-version pt-2">
+ <pre v-if="isLoadingDescriptionVersion" class="loading-state">
+ <gl-skeleton-loader />
+ </pre>
+ <pre v-else v-safe-html="descriptionVersion" class="wrapper mt-2"></pre>
+ <gl-button
+ v-if="displayDeleteButton"
+ v-gl-tooltip
+ :title="$options.i18n.deleteButtonLabel"
+ :aria-label="$options.i18n.deleteButtonLabel"
+ variant="default"
+ category="tertiary"
+ icon="remove"
+ class="delete-description-history"
+ data-testid="delete-description-version-button"
+ @click="deleteDescriptionVersion"
+ />
+ </div>
+ <div
+ v-if="lines.length && showLines"
+ class="diff-content outdated-lines-wrapper gl-border-solid gl-border-1 gl-border-gray-200 gl-mt-4 gl-rounded-small gl-overflow-hidden"
+ >
+ <table
+ :class="$options.userColorSchemeClass"
+ class="code js-syntax-highlight"
+ data-testid="outdated-lines"
+ >
+ <tr v-for="line in lines" v-once :key="line.line_code" class="line_holder">
+ <td
+ :class="line.type"
+ class="diff-line-num old_line gl-border-bottom-0! gl-border-top-0! gl-border-0! gl-rounded-0!"
+ >
+ {{ line.old_line }}
+ </td>
+ <td
+ :class="line.type"
+ class="diff-line-num new_line gl-border-bottom-0! gl-border-top-0!"
+ >
+ {{ line.new_line }}
+ </td>
+ <td
+ :class="line.type"
+ class="line_content gl-display-table-cell! gl-border-0! gl-rounded-0!"
+ v-html="line.rich_text /* eslint-disable-line vue/no-v-html */"
+ ></td>
+ </tr>
+ </table>
+ </div>
+ <div v-else-if="showLines" class="mt-4">
+ <gl-skeleton-loader />
+ </div>
+ </div>
+ </div>
+ </timeline-entry-item>
+</template>
diff --git a/app/assets/javascripts/work_items/components/work_item_assignees.vue b/app/assets/javascripts/work_items/components/work_item_assignees.vue
index 4d6a27f61ac..c2980405a19 100644
--- a/app/assets/javascripts/work_items/components/work_item_assignees.vue
+++ b/app/assets/javascripts/work_items/components/work_item_assignees.vue
@@ -202,6 +202,7 @@ export default {
if (!this.allowsMultipleAssignees) {
this.localAssignees = assignees.length > 0 ? [assignees[assignees.length - 1]] : [];
this.isEditing = false;
+ this.setAssignees(this.assigneeIds);
return;
}
this.localAssignees = assignees;
diff --git a/app/assets/javascripts/work_items/components/work_item_description.vue b/app/assets/javascripts/work_items/components/work_item_description.vue
index 57930951856..07da0279b41 100644
--- a/app/assets/javascripts/work_items/components/work_item_description.vue
+++ b/app/assets/javascripts/work_items/components/work_item_description.vue
@@ -1,5 +1,5 @@
<script>
-import { GlButton, GlFormGroup } from '@gitlab/ui';
+import { GlAlert, GlButton, GlFormGroup } from '@gitlab/ui';
import * as Sentry from '@sentry/browser';
import { helpPagePath } from '~/helpers/help_page_helper';
import { getDraft, clearDraft, updateDraft } from '~/lib/utils/autosave';
@@ -19,6 +19,7 @@ import WorkItemDescriptionRendered from './work_item_description_rendered.vue';
export default {
components: {
EditedAt,
+ GlAlert,
GlButton,
GlFormGroup,
MarkdownEditor,
@@ -54,6 +55,7 @@ export default {
isSubmittingWithKeydown: false,
descriptionText: '',
descriptionHtml: '',
+ conflictedDescription: '',
};
},
apollo: {
@@ -68,11 +70,17 @@ export default {
return this.fetchByIid ? data.workspace.workItems.nodes[0] : data.workItem;
},
skip() {
- return !this.workItemId;
+ return !this.queryVariables.id && !this.queryVariables.iid;
},
result() {
- this.descriptionText = this.workItemDescription?.description;
- this.descriptionHtml = this.workItemDescription?.descriptionHtml;
+ if (this.isEditing) {
+ if (this.descriptionText !== this.workItemDescription?.description) {
+ this.conflictedDescription = this.workItemDescription?.description;
+ }
+ } else {
+ this.descriptionText = this.workItemDescription?.description;
+ this.descriptionHtml = this.workItemDescription?.descriptionHtml;
+ }
},
error() {
this.$emit('error', i18n.fetchError);
@@ -94,6 +102,9 @@ export default {
canEdit() {
return this.workItem?.userPermissions?.updateWorkItem || false;
},
+ hasConflicts() {
+ return Boolean(this.conflictedDescription);
+ },
tracking() {
return {
category: TRACKING_CATEGORY_SHOW,
@@ -196,6 +207,7 @@ export default {
this.isEditing = false;
clearDraft(this.autosaveKey);
+ this.conflictedDescription = '';
} catch (error) {
this.$emit('error', error.message);
Sentry.captureException(error);
@@ -224,7 +236,7 @@ export default {
label-for="work-item-description"
>
<markdown-editor
- v-if="glFeatures.workItemsMvc2"
+ v-if="glFeatures.workItemsMvc"
class="gl-my-3 common-note-form"
:value="descriptionText"
:render-markdown-path="markdownPreviewPath"
@@ -235,6 +247,7 @@ export default {
form-field-name="work-item-description"
enable-autocomplete
init-on-autofocus
+ use-bottom-toolbar
@input="setDescriptionText"
@keydown.meta.enter="updateWorkItem"
@keydown.ctrl.enter="updateWorkItem"
@@ -246,7 +259,7 @@ export default {
:is-submitting="isSubmitting"
:markdown-preview-path="markdownPreviewPath"
:markdown-docs-path="$options.markdownDocsPath"
- class="gl-p-3 bordered-box gl-mt-5"
+ class="gl-px-3 bordered-box gl-mt-5"
>
<template #textarea>
<textarea
@@ -267,17 +280,59 @@ export default {
</template>
</markdown-field>
<div class="gl-display-flex">
- <gl-button
- category="primary"
- variant="confirm"
- :loading="isSubmitting"
- data-testid="save-description"
- @click="updateWorkItem"
- >{{ __('Save') }}
- </gl-button>
- <gl-button category="tertiary" class="gl-ml-3" data-testid="cancel" @click="cancelEditing"
- >{{ __('Cancel') }}
- </gl-button>
+ <gl-alert
+ v-if="hasConflicts"
+ :dismissible="false"
+ variant="danger"
+ class="gl-w-full"
+ data-testid="work-item-description-conflicts"
+ >
+ <p>
+ {{
+ s__(
+ "WorkItem|Someone edited the description at the same time you did. If you save it will overwrite their changes. Please confirm you'd like to save your edits.",
+ )
+ }}
+ </p>
+ <details class="gl-mb-5">
+ <summary class="gl-text-blue-500">{{ s__('WorkItem|View current version') }}</summary>
+ <textarea
+ class="note-textarea js-gfm-input js-autosize markdown-area gl-p-3"
+ readonly
+ :value="conflictedDescription"
+ ></textarea>
+ </details>
+ <template #actions>
+ <gl-button
+ category="primary"
+ variant="confirm"
+ :loading="isSubmitting"
+ data-testid="save-description"
+ @click="updateWorkItem"
+ >{{ s__('WorkItem|Save and overwrite') }}
+ </gl-button>
+ <gl-button
+ category="secondary"
+ class="gl-ml-3"
+ data-testid="cancel"
+ @click="cancelEditing"
+ >{{ s__('WorkItem|Discard changes') }}
+ </gl-button>
+ </template>
+ </gl-alert>
+ <template v-else>
+ <gl-button
+ category="primary"
+ variant="confirm"
+ :loading="isSubmitting"
+ data-testid="save-description"
+ @click="updateWorkItem"
+ >{{ __('Save') }}
+ </gl-button>
+ <gl-button category="tertiary" class="gl-ml-3" data-testid="cancel" @click="cancelEditing"
+ >{{ __('Cancel') }}
+ </gl-button>
+ </template>
</div>
</gl-form-group>
<work-item-description-rendered
diff --git a/app/assets/javascripts/work_items/components/work_item_description_rendered.vue b/app/assets/javascripts/work_items/components/work_item_description_rendered.vue
index e6f8a301c5e..d58983c013d 100644
--- a/app/assets/javascripts/work_items/components/work_item_description_rendered.vue
+++ b/app/assets/javascripts/work_items/components/work_item_description_rendered.vue
@@ -1,13 +1,14 @@
<script>
-import { GlButton, GlSafeHtmlDirective } from '@gitlab/ui';
-import $ from 'jquery';
-import '~/behaviors/markdown/render_gfm';
+import { GlButton, GlTooltipDirective } from '@gitlab/ui';
+import SafeHtml from '~/vue_shared/directives/safe_html';
+import { renderGFM } from '~/behaviors/markdown/render_gfm';
const isCheckbox = (target) => target?.classList.contains('task-list-item-checkbox');
export default {
directives: {
- SafeHtml: GlSafeHtmlDirective,
+ SafeHtml,
+ GlTooltip: GlTooltipDirective,
},
components: {
GlButton,
@@ -45,7 +46,7 @@ export default {
async renderGFM() {
await this.$nextTick();
- $(this.$refs['gfm-content']).renderGFM();
+ renderGFM(this.$refs['gfm-content']);
if (this.canEdit) {
this.checkboxes = this.$el.querySelectorAll('.task-list-item-checkbox');
@@ -93,14 +94,16 @@ export default {
<template>
<div class="gl-mb-5 gl-border-t gl-pt-5">
- <div class="gl-display-inline-flex gl-align-items-center gl-mb-5">
+ <div class="gl-display-inline-flex gl-align-items-center gl-mb-3">
<label class="d-block col-form-label gl-mr-5">{{ __('Description') }}</label>
<gl-button
v-if="canEdit"
+ v-gl-tooltip
class="gl-ml-auto"
icon="pencil"
data-testid="edit-description"
:aria-label="__('Edit description')"
+ :title="__('Edit description')"
@click="$emit('startEditing')"
/>
</div>
@@ -111,6 +114,7 @@ export default {
ref="gfm-content"
v-safe-html="descriptionHtml"
class="md gl-mb-5 gl-min-h-8"
+ data-testid="work-item-description"
@change="toggleCheckboxes"
></div>
</div>
diff --git a/app/assets/javascripts/work_items/components/work_item_detail.vue b/app/assets/javascripts/work_items/components/work_item_detail.vue
index 7e9fa24e3f5..cb45a05de89 100644
--- a/app/assets/javascripts/work_items/components/work_item_detail.vue
+++ b/app/assets/javascripts/work_items/components/work_item_detail.vue
@@ -1,5 +1,6 @@
<script>
import { isEmpty } from 'lodash';
+import { produce } from 'immer';
import {
GlAlert,
GlSkeletonLoader,
@@ -11,10 +12,11 @@ import {
GlEmptyState,
} from '@gitlab/ui';
import noAccessSvg from '@gitlab/svgs/dist/illustrations/analytics/no-access.svg';
+import * as Sentry from '@sentry/browser';
import { s__ } from '~/locale';
import { parseBoolean } from '~/lib/utils/common_utils';
+import { getParameterByName } from '~/lib/utils/url_utility';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
-import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue';
import WorkItemTypeIcon from '~/work_items/components/work_item_type_icon.vue';
import {
i18n,
@@ -23,10 +25,14 @@ import {
WIDGET_TYPE_DESCRIPTION,
WIDGET_TYPE_START_AND_DUE_DATE,
WIDGET_TYPE_WEIGHT,
+ WIDGET_TYPE_PROGRESS,
WIDGET_TYPE_HIERARCHY,
- WORK_ITEM_VIEWED_STORAGE_KEY,
WIDGET_TYPE_MILESTONE,
WIDGET_TYPE_ITERATION,
+ WIDGET_TYPE_HEALTH_STATUS,
+ WORK_ITEM_TYPE_VALUE_ISSUE,
+ WORK_ITEM_TYPE_VALUE_OBJECTIVE,
+ WIDGET_TYPE_NOTES,
} from '../constants';
import workItemDatesSubscription from '../graphql/work_item_dates.subscription.graphql';
@@ -37,6 +43,7 @@ import updateWorkItemMutation from '../graphql/update_work_item.mutation.graphql
import updateWorkItemTaskMutation from '../graphql/update_work_item_task.mutation.graphql';
import { getWorkItemQuery } from '../utils';
+import WorkItemTree from './work_item_links/work_item_tree.vue';
import WorkItemActions from './work_item_actions.vue';
import WorkItemState from './work_item_state.vue';
import WorkItemTitle from './work_item_title.vue';
@@ -45,7 +52,7 @@ import WorkItemDueDate from './work_item_due_date.vue';
import WorkItemAssignees from './work_item_assignees.vue';
import WorkItemLabels from './work_item_labels.vue';
import WorkItemMilestone from './work_item_milestone.vue';
-import WorkItemInformation from './work_item_information.vue';
+import WorkItemNotes from './work_item_notes.vue';
export default {
i18n,
@@ -68,11 +75,14 @@ export default {
WorkItemTitle,
WorkItemState,
WorkItemWeight: () => import('ee_component/work_items/components/work_item_weight.vue'),
- WorkItemInformation,
- LocalStorageSync,
+ WorkItemProgress: () => import('ee_component/work_items/components/work_item_progress.vue'),
WorkItemTypeIcon,
WorkItemIteration: () => import('ee_component/work_items/components/work_item_iteration.vue'),
+ WorkItemHealthStatus: () =>
+ import('ee_component/work_items/components/work_item_health_status.vue'),
WorkItemMilestone,
+ WorkItemTree,
+ WorkItemNotes,
},
mixins: [glFeatureFlagMixin()],
inject: ['fullPath'],
@@ -87,7 +97,7 @@ export default {
required: false,
default: null,
},
- iid: {
+ workItemIid: {
type: String,
required: false,
default: null,
@@ -103,7 +113,6 @@ export default {
error: undefined,
updateError: undefined,
workItem: {},
- showInfoBanner: true,
updateInProgress: false,
};
},
@@ -201,17 +210,31 @@ export default {
fullPath() {
return this.workItem?.project.fullPath;
},
+ workItemsMvcEnabled() {
+ return this.glFeatures.workItemsMvc;
+ },
workItemsMvc2Enabled() {
return this.glFeatures.workItemsMvc2;
},
parentWorkItem() {
return this.isWidgetPresent(WIDGET_TYPE_HIERARCHY)?.parent;
},
+ parentWorkItemType() {
+ return this.parentWorkItem?.workItemType?.name;
+ },
+ parentWorkItemIconName() {
+ return this.parentWorkItem?.workItemType?.iconName;
+ },
parentWorkItemConfidentiality() {
return this.parentWorkItem?.confidential;
},
parentUrl() {
- return `../../issues/${this.parentWorkItem?.iid}`;
+ // Once more types are moved to have Work Items involved
+ // we need to handle this properly.
+ if (this.parentWorkItemType === WORK_ITEM_TYPE_VALUE_ISSUE) {
+ return `../../issues/${this.parentWorkItem?.iid}`;
+ }
+ return this.parentWorkItem?.webUrl;
},
workItemIconName() {
return this.workItem?.workItemType?.iconName;
@@ -234,41 +257,48 @@ export default {
workItemWeight() {
return this.isWidgetPresent(WIDGET_TYPE_WEIGHT);
},
+ workItemProgress() {
+ return this.isWidgetPresent(WIDGET_TYPE_PROGRESS);
+ },
workItemHierarchy() {
return this.isWidgetPresent(WIDGET_TYPE_HIERARCHY);
},
workItemIteration() {
return this.isWidgetPresent(WIDGET_TYPE_ITERATION);
},
+ workItemHealthStatus() {
+ return this.isWidgetPresent(WIDGET_TYPE_HEALTH_STATUS);
+ },
workItemMilestone() {
return this.isWidgetPresent(WIDGET_TYPE_MILESTONE);
},
+ workItemNotes() {
+ return this.isWidgetPresent(WIDGET_TYPE_NOTES);
+ },
fetchByIid() {
- return this.glFeatures.useIidInWorkItemsPath && parseBoolean(this.$route.query.iid_path);
+ return this.glFeatures.useIidInWorkItemsPath && parseBoolean(getParameterByName('iid_path'));
},
queryVariables() {
return this.fetchByIid
? {
fullPath: this.fullPath,
- iid: this.iid,
+ iid: this.workItemIid,
}
: {
id: this.workItemId,
};
},
- },
- beforeDestroy() {
- /** make sure that if the user has not even dismissed the alert ,
- * should no be able to see the information next time and update the local storage * */
- this.dismissBanner();
+ children() {
+ const widgetHierarchy = this.workItem.widgets.find(
+ (widget) => widget.type === WIDGET_TYPE_HIERARCHY,
+ );
+ return widgetHierarchy.children.nodes;
+ },
},
methods: {
isWidgetPresent(type) {
return this.workItem?.widgets?.find((widget) => widget.type === type);
},
- dismissBanner() {
- this.showInfoBanner = false;
- },
toggleConfidentiality(confidentialStatus) {
this.updateInProgress = true;
let updateMutation = updateWorkItemMutation;
@@ -321,8 +351,76 @@ export default {
this.error = this.$options.i18n.fetchError;
document.title = s__('404|Not found');
},
+ addChild(child) {
+ const { defaultClient: client } = this.$apollo.provider.clients;
+ this.toggleChildFromCache(child, child.id, client);
+ },
+ toggleChildFromCache(workItem, childId, store) {
+ const sourceData = store.readQuery({
+ query: getWorkItemQuery(this.fetchByIid),
+ variables: this.queryVariables,
+ });
+
+ const newData = produce(sourceData, (draftState) => {
+ const widgetHierarchy = draftState.workItem.widgets.find(
+ (widget) => widget.type === WIDGET_TYPE_HIERARCHY,
+ );
+
+ const index = widgetHierarchy.children.nodes.findIndex((child) => child.id === childId);
+
+ if (index >= 0) {
+ widgetHierarchy.children.nodes.splice(index, 1);
+ } else {
+ widgetHierarchy.children.nodes.unshift(workItem);
+ }
+ });
+
+ store.writeQuery({
+ query: getWorkItemQuery(this.fetchByIid),
+ variables: this.queryVariables,
+ data: newData,
+ });
+ },
+ async updateWorkItem(workItem, childId, parentId) {
+ return this.$apollo.mutate({
+ mutation: updateWorkItemMutation,
+ variables: { input: { id: childId, hierarchyWidget: { parentId } } },
+ update: (store) => this.toggleChildFromCache(workItem, childId, store),
+ });
+ },
+ async undoChildRemoval(workItem, childId) {
+ try {
+ const { data } = await this.updateWorkItem(workItem, childId, this.workItem.id);
+
+ if (data.workItemUpdate.errors.length === 0) {
+ this.activeToast?.hide();
+ }
+ } catch (error) {
+ this.updateError = s__('WorkItem|Something went wrong while undoing child removal.');
+ Sentry.captureException(error);
+ } finally {
+ this.activeToast?.hide();
+ }
+ },
+ async removeChild(childId) {
+ try {
+ const { data } = await this.updateWorkItem(null, childId, null);
+
+ if (data.workItemUpdate.errors.length === 0) {
+ this.activeToast = this.$toast.show(s__('WorkItem|Child removed'), {
+ action: {
+ text: s__('WorkItem|Undo'),
+ onClick: this.undoChildRemoval.bind(this, data.workItemUpdate.workItem, childId),
+ },
+ });
+ }
+ } catch (error) {
+ this.updateError = s__('WorkItem|Something went wrong while removing child.');
+ Sentry.captureException(error);
+ }
+ },
},
- WORK_ITEM_VIEWED_STORAGE_KEY,
+ WORK_ITEM_TYPE_VALUE_OBJECTIVE,
};
</script>
@@ -347,14 +445,14 @@ export default {
<div class="gl-display-flex gl-align-items-center" data-testid="work-item-body">
<ul
v-if="parentWorkItem"
- class="list-unstyled gl-display-flex gl-mr-auto gl-max-w-26 gl-md-max-w-50p gl-min-w-0 gl-mb-0"
+ class="list-unstyled gl-display-flex gl-mr-auto gl-max-w-26 gl-md-max-w-50p gl-min-w-0 gl-mb-0 gl-z-index-0"
data-testid="work-item-parent"
>
<li class="gl-ml-n4 gl-display-flex gl-align-items-center gl-overflow-hidden">
<gl-button
v-gl-tooltip.hover
class="gl-text-truncate gl-max-w-full"
- icon="issues"
+ :icon="parentWorkItemIconName"
category="tertiary"
:href="parentUrl"
:title="parentWorkItem.title"
@@ -411,16 +509,6 @@ export default {
@click="$emit('close')"
/>
</div>
- <local-storage-sync
- v-model="showInfoBanner"
- :storage-key="$options.WORK_ITEM_VIEWED_STORAGE_KEY"
- >
- <work-item-information
- v-if="showInfoBanner && !error"
- :show-info-banner="showInfoBanner"
- @work-item-banner-dismissed="dismissBanner"
- />
- </local-storage-sync>
<work-item-title
v-if="workItem.title"
:work-item-id="workItem.id"
@@ -465,19 +553,17 @@ export default {
:work-item-type="workItemType"
@error="updateError = $event"
/>
- <template v-if="workItemsMvc2Enabled">
- <work-item-milestone
- v-if="workItemMilestone"
- :work-item-id="workItem.id"
- :work-item-milestone="workItemMilestone.milestone"
- :work-item-type="workItemType"
- :fetch-by-iid="fetchByIid"
- :query-variables="queryVariables"
- :can-update="canUpdate"
- :full-path="fullPath"
- @error="updateError = $event"
- />
- </template>
+ <work-item-milestone
+ v-if="workItemMilestone"
+ :work-item-id="workItem.id"
+ :work-item-milestone="workItemMilestone.milestone"
+ :work-item-type="workItemType"
+ :fetch-by-iid="fetchByIid"
+ :query-variables="queryVariables"
+ :can-update="canUpdate"
+ :full-path="fullPath"
+ @error="updateError = $event"
+ />
<work-item-weight
v-if="workItemWeight"
class="gl-mb-5"
@@ -489,20 +575,38 @@ export default {
:query-variables="queryVariables"
@error="updateError = $event"
/>
- <template v-if="workItemsMvc2Enabled">
- <work-item-iteration
- v-if="workItemIteration"
- class="gl-mb-5"
- :iteration="workItemIteration.iteration"
- :can-update="canUpdate"
- :work-item-id="workItem.id"
- :work-item-type="workItemType"
- :fetch-by-iid="fetchByIid"
- :query-variables="queryVariables"
- :full-path="fullPath"
- @error="updateError = $event"
- />
- </template>
+ <work-item-progress
+ v-if="workItemProgress"
+ class="gl-mb-5"
+ :can-update="canUpdate"
+ :progress="workItemProgress.progress"
+ :work-item-id="workItem.id"
+ :work-item-type="workItemType"
+ :fetch-by-iid="fetchByIid"
+ :query-variables="queryVariables"
+ @error="updateError = $event"
+ />
+ <work-item-iteration
+ v-if="workItemIteration"
+ class="gl-mb-5"
+ :iteration="workItemIteration.iteration"
+ :can-update="canUpdate"
+ :work-item-id="workItem.id"
+ :work-item-type="workItemType"
+ :fetch-by-iid="fetchByIid"
+ :query-variables="queryVariables"
+ :full-path="fullPath"
+ @error="updateError = $event"
+ />
+ <work-item-health-status
+ v-if="workItemHealthStatus"
+ class="gl-mb-5"
+ :health-status="workItemHealthStatus.healthStatus"
+ :can-update="canUpdate"
+ :work-item-id="workItem.id"
+ :work-item-type="workItemType"
+ @error="updateError = $event"
+ />
<work-item-description
v-if="hasDescriptionWidget"
:work-item-id="workItem.id"
@@ -512,6 +616,27 @@ export default {
class="gl-pt-5"
@error="updateError = $event"
/>
+ <work-item-tree
+ v-if="workItemType === $options.WORK_ITEM_TYPE_VALUE_OBJECTIVE"
+ :work-item-type="workItemType"
+ :work-item-id="workItem.id"
+ :children="children"
+ :can-update="canUpdate"
+ :project-path="fullPath"
+ @addWorkItemChild="addChild"
+ @removeChild="removeChild"
+ />
+ <template v-if="workItemsMvc2Enabled">
+ <work-item-notes
+ v-if="workItemNotes"
+ :work-item-id="workItem.id"
+ :query-variables="queryVariables"
+ :full-path="fullPath"
+ :fetch-by-iid="fetchByIid"
+ class="gl-pt-5"
+ @error="updateError = $event"
+ />
+ </template>
<gl-empty-state
v-if="error"
:title="$options.i18n.fetchErrorTitle"
diff --git a/app/assets/javascripts/work_items/components/work_item_detail_modal.vue b/app/assets/javascripts/work_items/components/work_item_detail_modal.vue
index 39a662a6c54..e8726814aaf 100644
--- a/app/assets/javascripts/work_items/components/work_item_detail_modal.vue
+++ b/app/assets/javascripts/work_items/components/work_item_detail_modal.vue
@@ -20,6 +20,11 @@ export default {
required: false,
default: null,
},
+ workItemIid: {
+ type: String,
+ required: false,
+ default: null,
+ },
issueGid: {
type: String,
required: false,
@@ -134,6 +139,7 @@ export default {
size="lg"
modal-id="work-item-detail-modal"
header-class="gl-p-0 gl-pb-2!"
+ scrollable
@hide="closeModal"
>
<gl-alert v-if="error" variant="danger" @dismiss="error = false">
@@ -144,6 +150,7 @@ export default {
is-modal
:work-item-parent-id="issueGid"
:work-item-id="workItemId"
+ :work-item-iid="workItemIid"
class="gl-p-5 gl-mt-n3"
@close="hide"
@deleteWorkItem="deleteWorkItem"
diff --git a/app/assets/javascripts/work_items/components/work_item_information.vue b/app/assets/javascripts/work_items/components/work_item_information.vue
deleted file mode 100644
index ce75cc98a75..00000000000
--- a/app/assets/javascripts/work_items/components/work_item_information.vue
+++ /dev/null
@@ -1,53 +0,0 @@
-<script>
-import { GlAlert, GlSprintf, GlLink } from '@gitlab/ui';
-import { s__ } from '~/locale';
-import { helpPagePath } from '~/helpers/help_page_helper';
-
-export default {
- i18n: {
- learnTasksLinkText: s__('WorkItem|Learn about tasks.'),
- tasksInformationTitle: s__('WorkItem|Introducing tasks'),
- tasksInformationBody: s__(
- 'WorkItem|Use tasks to break down your work in an issue into smaller pieces. %{learnMoreLink}',
- ),
- },
- helpPageLinks: {
- tasksDocLinkPath: helpPagePath('user/tasks'),
- },
- components: {
- GlAlert,
- GlSprintf,
- GlLink,
- },
- props: {
- showInfoBanner: {
- type: Boolean,
- required: false,
- default: true,
- },
- },
- emits: ['work-item-banner-dismissed'],
-};
-</script>
-
-<template>
- <section class="gl-display-block gl-mb-2">
- <gl-alert
- v-if="showInfoBanner"
- variant="tip"
- :title="$options.i18n.tasksInformationTitle"
- data-testid="work-item-information"
- class="gl-mt-3"
- @dismiss="$emit('work-item-banner-dismissed')"
- >
- <gl-sprintf :message="$options.i18n.tasksInformationBody">
- <template #learnMoreLink>
- <gl-link :href="$options.helpPageLinks.tasksDocLinkPath">{{
- $options.i18n.learnTasksLinkText
- }}</gl-link>
- </template>
- ></gl-sprintf
- >
- </gl-alert>
- </section>
-</template>
diff --git a/app/assets/javascripts/work_items/components/work_item_labels.vue b/app/assets/javascripts/work_items/components/work_item_labels.vue
index 22af3c653e9..45fb0f7f21a 100644
--- a/app/assets/javascripts/work_items/components/work_item_labels.vue
+++ b/app/assets/javascripts/work_items/components/work_item_labels.vue
@@ -3,8 +3,8 @@ import { GlTokenSelector, GlLabel, GlSkeletonLoader } from '@gitlab/ui';
import { debounce, uniqueId, without } from 'lodash';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import Tracking from '~/tracking';
-import labelSearchQuery from '~/vue_shared/components/sidebar/labels_select_widget/graphql/project_labels.query.graphql';
-import LabelItem from '~/vue_shared/components/sidebar/labels_select_widget/label_item.vue';
+import labelSearchQuery from '~/sidebar/components/labels/labels_select_widget/graphql/project_labels.query.graphql';
+import LabelItem from '~/sidebar/components/labels/labels_select_widget/label_item.vue';
import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
import { isScopedLabel } from '~/lib/utils/common_utils';
import workItemLabelsSubscription from 'ee_else_ce/work_items/graphql/work_item_labels.subscription.graphql';
@@ -83,7 +83,7 @@ export default {
return this.fetchByIid ? data.workspace.workItems.nodes[0] : data.workItem;
},
skip() {
- return !this.workItemId;
+ return !this.queryVariables.id && !this.queryVariables.iid;
},
error() {
this.$emit('error', i18n.fetchError);
diff --git a/app/assets/javascripts/work_items/components/work_item_links/index.js b/app/assets/javascripts/work_items/components/work_item_links/index.js
index 0251dcc33fa..edad0e9b616 100644
--- a/app/assets/javascripts/work_items/components/work_item_links/index.js
+++ b/app/assets/javascripts/work_items/components/work_item_links/index.js
@@ -17,6 +17,7 @@ export default function initWorkItemLinks() {
wiHasIssueWeightsFeature,
iid,
wiHasIterationsFeature,
+ wiHasIssuableHealthStatusFeature,
} = workItemLinksRoot.dataset;
// eslint-disable-next-line no-new
@@ -33,6 +34,7 @@ export default function initWorkItemLinks() {
fullPath: projectPath,
hasIssueWeightsFeature: wiHasIssueWeightsFeature,
hasIterationsFeature: wiHasIterationsFeature,
+ hasIssuableHealthStatusFeature: wiHasIssuableHealthStatusFeature,
},
render: (createElement) =>
createElement('work-item-links', {
diff --git a/app/assets/javascripts/work_items/components/work_item_links/okr_actions_split_button.vue b/app/assets/javascripts/work_items/components/work_item_links/okr_actions_split_button.vue
new file mode 100644
index 00000000000..dc5bcdc3dcc
--- /dev/null
+++ b/app/assets/javascripts/work_items/components/work_item_links/okr_actions_split_button.vue
@@ -0,0 +1,66 @@
+<script>
+import { GlDropdown, GlDropdownSectionHeader, GlDropdownItem, GlDropdownDivider } from '@gitlab/ui';
+
+import { s__ } from '~/locale';
+
+const objectiveActionItems = [
+ {
+ title: s__('OKR|New objective'),
+ eventName: 'showCreateObjectiveForm',
+ },
+ {
+ title: s__('OKR|Existing objective'),
+ eventName: 'showAddObjectiveForm',
+ },
+];
+
+const keyResultActionItems = [
+ {
+ title: s__('OKR|New key result'),
+ eventName: 'showCreateKeyResultForm',
+ },
+ {
+ title: s__('OKR|Existing key result'),
+ eventName: 'showAddKeyResultForm',
+ },
+];
+
+export default {
+ keyResultActionItems,
+ objectiveActionItems,
+ components: {
+ GlDropdown,
+ GlDropdownSectionHeader,
+ GlDropdownItem,
+ GlDropdownDivider,
+ },
+ methods: {
+ change({ eventName }) {
+ this.$emit(eventName);
+ },
+ },
+};
+</script>
+
+<template>
+ <gl-dropdown :text="__('Add')" size="small" right>
+ <gl-dropdown-section-header>{{ __('Objective') }}</gl-dropdown-section-header>
+ <gl-dropdown-item
+ v-for="item in $options.objectiveActionItems"
+ :key="item.eventName"
+ @click="change(item)"
+ >
+ {{ item.title }}
+ </gl-dropdown-item>
+
+ <gl-dropdown-divider />
+ <gl-dropdown-section-header>{{ __('Key result') }}</gl-dropdown-section-header>
+ <gl-dropdown-item
+ v-for="item in $options.keyResultActionItems"
+ :key="item.eventName"
+ @click="change(item)"
+ >
+ {{ item.title }}
+ </gl-dropdown-item>
+ </gl-dropdown>
+</template>
diff --git a/app/assets/javascripts/work_items/components/work_item_links/work_item_link_child.vue b/app/assets/javascripts/work_items/components/work_item_links/work_item_link_child.vue
index 34874908f9b..763f2f338a3 100644
--- a/app/assets/javascripts/work_items/components/work_item_links/work_item_link_child.vue
+++ b/app/assets/javascripts/work_items/components/work_item_links/work_item_link_child.vue
@@ -1,19 +1,35 @@
<script>
-import { GlButton, GlIcon, GlTooltipDirective } from '@gitlab/ui';
+import { GlButton, GlLink, GlIcon, GlTooltipDirective } from '@gitlab/ui';
-import { __ } from '~/locale';
+import { __, s__ } from '~/locale';
+import { createAlert } from '~/flash';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import RichTimestampTooltip from '~/vue_shared/components/rich_timestamp_tooltip.vue';
-import { STATE_OPEN } from '../../constants';
+import {
+ STATE_OPEN,
+ TASK_TYPE_NAME,
+ WORK_ITEM_TYPE_VALUE_OBJECTIVE,
+ WIDGET_TYPE_MILESTONE,
+ WIDGET_TYPE_HIERARCHY,
+ WIDGET_TYPE_ASSIGNEES,
+ WIDGET_TYPE_LABELS,
+ WORK_ITEM_NAME_TO_ICON_MAP,
+} from '../../constants';
+import getWorkItemTreeQuery from '../../graphql/work_item_tree.query.graphql';
+import WorkItemLinkChildMetadata from './work_item_link_child_metadata.vue';
import WorkItemLinksMenu from './work_item_links_menu.vue';
+import WorkItemTreeChildren from './work_item_tree_children.vue';
export default {
components: {
+ GlLink,
GlButton,
GlIcon,
RichTimestampTooltip,
+ WorkItemLinkChildMetadata,
WorkItemLinksMenu,
+ WorkItemTreeChildren,
},
directives: {
GlTooltip: GlTooltipDirective,
@@ -35,16 +51,48 @@ export default {
type: Object,
required: true,
},
+ hasIndirectChildren: {
+ type: Boolean,
+ required: false,
+ default: true,
+ },
+ workItemType: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ },
+ data() {
+ return {
+ isExpanded: false,
+ children: [],
+ isLoadingChildren: false,
+ };
},
computed: {
+ canHaveChildren() {
+ return this.workItemType === WORK_ITEM_TYPE_VALUE_OBJECTIVE;
+ },
+ allowsScopedLabels() {
+ return this.getWidgetByType(this.childItem, WIDGET_TYPE_LABELS)?.allowsScopedLabels;
+ },
isItemOpen() {
return this.childItem.state === STATE_OPEN;
},
- iconClass() {
- return this.isItemOpen ? 'gl-text-green-500' : 'gl-text-blue-500';
+ childItemType() {
+ return this.childItem.workItemType.name;
},
iconName() {
- return this.isItemOpen ? 'issue-open-m' : 'issue-close';
+ if (this.childItemType === TASK_TYPE_NAME) {
+ return this.isItemOpen ? 'issue-open-m' : 'issue-close';
+ }
+ return WORK_ITEM_NAME_TO_ICON_MAP[this.childItemType];
+ },
+ iconClass() {
+ if (this.childItemType === TASK_TYPE_NAME) {
+ return this.isItemOpen ? 'gl-text-green-500' : 'gl-text-blue-500';
+ }
+ return '';
},
stateTimestamp() {
return this.isItemOpen ? this.childItem.createdAt : this.childItem.closedAt;
@@ -55,55 +103,161 @@ export default {
childPath() {
return `/${this.projectPath}/-/work_items/${getIdFromGraphQLId(this.childItem.id)}`;
},
+ hasChildren() {
+ return this.getWidgetByType(this.childItem, WIDGET_TYPE_HIERARCHY)?.hasChildren;
+ },
+ chevronType() {
+ return this.isExpanded ? 'chevron-down' : 'chevron-right';
+ },
+ chevronTooltip() {
+ return this.isExpanded ? __('Collapse') : __('Expand');
+ },
+ hasMetadata() {
+ return this.milestone || this.assignees.length > 0 || this.labels.length > 0;
+ },
+ milestone() {
+ return this.getWidgetByType(this.childItem, WIDGET_TYPE_MILESTONE)?.milestone;
+ },
+ assignees() {
+ return this.getWidgetByType(this.childItem, WIDGET_TYPE_ASSIGNEES)?.assignees?.nodes || [];
+ },
+ labels() {
+ return this.getWidgetByType(this.childItem, WIDGET_TYPE_LABELS)?.labels?.nodes || [];
+ },
+ },
+ methods: {
+ toggleItem() {
+ this.isExpanded = !this.isExpanded;
+ if (this.children.length === 0 && this.hasChildren) {
+ this.fetchChildren();
+ }
+ },
+ getWidgetByType(workItem, widgetType) {
+ return workItem?.widgets?.find((widget) => widget.type === widgetType);
+ },
+ async fetchChildren() {
+ this.isLoadingChildren = true;
+ try {
+ const { data } = await this.$apollo.query({
+ query: getWorkItemTreeQuery,
+ variables: {
+ id: this.childItem.id,
+ },
+ });
+ this.children = this.getWidgetByType(data?.workItem, WIDGET_TYPE_HIERARCHY).children.nodes;
+ } catch (error) {
+ this.isExpanded = !this.isExpanded;
+ createAlert({
+ message: s__('Hierarchy|Something went wrong while fetching children.'),
+ captureError: true,
+ error,
+ });
+ } finally {
+ this.isLoadingChildren = false;
+ }
+ },
},
};
</script>
<template>
- <div
- class="gl-relative gl-display-flex gl-overflow-break-word gl-min-w-0 gl-bg-white gl-mb-3 gl-py-3 gl-px-4 gl-border gl-border-gray-100 gl-rounded-base gl-line-height-32"
- data-testid="links-child"
- >
- <div class="gl-overflow-hidden gl-display-flex gl-align-items-center gl-flex-grow-1">
- <span :id="`stateIcon-${childItem.id}`" class="gl-mr-3" data-testid="item-status-icon">
- <gl-icon :name="iconName" :class="iconClass" :aria-label="stateTimestampTypeText" />
- </span>
- <rich-timestamp-tooltip
- :target="`stateIcon-${childItem.id}`"
- :raw-timestamp="stateTimestamp"
- :timestamp-type-text="stateTimestampTypeText"
- />
- <gl-icon
- v-if="childItem.confidential"
- v-gl-tooltip.top
- name="eye-slash"
- class="gl-mr-2 gl-text-orange-500"
- data-testid="confidential-icon"
- :aria-label="__('Confidential')"
- :title="__('Confidential')"
- />
- <gl-button
- :href="childPath"
- category="tertiary"
- variant="link"
- class="gl-text-truncate gl-max-w-80 gl-text-black-normal!"
- @click="$emit('click', childItem.id, $event)"
- @mouseover="$emit('mouseover', childItem.id, $event)"
- @mouseout="$emit('mouseout', childItem.id, $event)"
- >
- {{ childItem.title }}
- </gl-button>
- </div>
+ <div>
<div
- v-if="canUpdate"
- class="gl-ml-0 gl-sm-ml-auto! gl-display-inline-flex gl-align-items-center"
+ class="gl-display-flex gl-align-items-flex-start gl-mb-3"
+ :class="{ 'gl-ml-6': canHaveChildren && !hasChildren && hasIndirectChildren }"
>
- <work-item-links-menu
- :work-item-id="childItem.id"
- :parent-work-item-id="issuableGid"
- data-testid="links-menu"
- @removeChild="$emit('remove', childItem.id)"
+ <gl-button
+ v-if="hasChildren"
+ v-gl-tooltip.viewport
+ :title="chevronTooltip"
+ :aria-label="chevronTooltip"
+ :icon="chevronType"
+ category="tertiary"
+ :loading="isLoadingChildren"
+ class="gl-px-0! gl-py-3! gl-mr-3"
+ data-testid="expand-child"
+ @click="toggleItem"
/>
+ <div
+ class="gl-relative gl-display-flex gl-flex-grow-1 gl-overflow-break-word gl-min-w-0 gl-bg-white gl-py-3 gl-px-4 gl-border gl-border-gray-100 gl-rounded-base gl-line-height-32"
+ data-testid="links-child"
+ >
+ <span
+ :id="`stateIcon-${childItem.id}`"
+ class="gl-mr-3"
+ :class="{ 'gl-display-flex': hasMetadata }"
+ data-testid="item-status-icon"
+ >
+ <gl-icon
+ class="gl-text-secondary"
+ :class="iconClass"
+ :name="iconName"
+ :aria-label="stateTimestampTypeText"
+ />
+ </span>
+ <div
+ class="gl-display-flex gl-flex-grow-1"
+ :class="{
+ 'gl-flex-direction-column gl-align-items-flex-start': hasMetadata,
+ 'gl-align-items-center': !hasMetadata,
+ }"
+ >
+ <div class="gl-display-flex">
+ <rich-timestamp-tooltip
+ :target="`stateIcon-${childItem.id}`"
+ :raw-timestamp="stateTimestamp"
+ :timestamp-type-text="stateTimestampTypeText"
+ />
+ <gl-icon
+ v-if="childItem.confidential"
+ v-gl-tooltip.top
+ name="eye-slash"
+ class="gl-mr-2 gl-text-orange-500"
+ data-testid="confidential-icon"
+ :aria-label="__('Confidential')"
+ :title="__('Confidential')"
+ />
+ <gl-link
+ :href="childPath"
+ class="gl-overflow-wrap-break gl-line-height-normal gl-text-black-normal! gl-font-weight-bold"
+ data-testid="item-title"
+ @click="$emit('click', $event)"
+ @mouseover="$emit('mouseover')"
+ @mouseout="$emit('mouseout')"
+ >
+ {{ childItem.title }}
+ </gl-link>
+ </div>
+ <work-item-link-child-metadata
+ v-if="hasMetadata"
+ :allows-scoped-labels="allowsScopedLabels"
+ :milestone="milestone"
+ :assignees="assignees"
+ :labels="labels"
+ class="gl-mt-3"
+ />
+ </div>
+ <div
+ v-if="canUpdate"
+ class="gl-ml-0 gl-sm-ml-auto! gl-display-inline-flex gl-align-items-center"
+ >
+ <work-item-links-menu
+ :work-item-id="childItem.id"
+ :parent-work-item-id="issuableGid"
+ data-testid="links-menu"
+ @removeChild="$emit('removeChild', childItem.id)"
+ />
+ </div>
+ </div>
</div>
+ <work-item-tree-children
+ v-if="isExpanded"
+ :project-path="projectPath"
+ :can-update="canUpdate"
+ :work-item-id="issuableGid"
+ :work-item-type="workItemType"
+ :children="children"
+ @removeChild="fetchChildren"
+ />
</div>
</template>
diff --git a/app/assets/javascripts/work_items/components/work_item_links/work_item_link_child_metadata.vue b/app/assets/javascripts/work_items/components/work_item_links/work_item_link_child_metadata.vue
new file mode 100644
index 00000000000..7be7e1f3496
--- /dev/null
+++ b/app/assets/javascripts/work_items/components/work_item_links/work_item_link_child_metadata.vue
@@ -0,0 +1,123 @@
+<script>
+import { GlLabel, GlAvatar, GlAvatarLink, GlAvatarsInline, GlTooltipDirective } from '@gitlab/ui';
+
+import { s__, sprintf } from '~/locale';
+import { isScopedLabel } from '~/lib/utils/common_utils';
+
+import ItemMilestone from '~/issuable/components/issue_milestone.vue';
+
+export default {
+ components: {
+ GlLabel,
+ GlAvatar,
+ GlAvatarLink,
+ GlAvatarsInline,
+ ItemMilestone,
+ },
+ directives: {
+ GlTooltip: GlTooltipDirective,
+ },
+ props: {
+ allowsScopedLabels: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ milestone: {
+ type: Object,
+ required: false,
+ default: null,
+ },
+ assignees: {
+ type: Array,
+ required: false,
+ default: () => [],
+ },
+ labels: {
+ type: Array,
+ required: false,
+ default: () => [],
+ },
+ },
+ computed: {
+ assigneesCollapsedTooltip() {
+ if (this.assignees.length > 2) {
+ return sprintf(s__('WorkItem|%{count} more assignees'), {
+ count: this.assignees.length - 2,
+ });
+ }
+ return '';
+ },
+ assigneesContainerClass() {
+ if (this.assignees.length === 2) {
+ return 'fixed-width-avatars-2';
+ } else if (this.assignees.length > 2) {
+ return 'fixed-width-avatars-3';
+ }
+ return '';
+ },
+ labelsContainerClass() {
+ if (this.milestone || this.assignees.length) {
+ return 'gl-sm-ml-5';
+ }
+ return '';
+ },
+ },
+ methods: {
+ showScopedLabel(label) {
+ return isScopedLabel(label) && this.allowsScopedLabels;
+ },
+ },
+};
+</script>
+
+<template>
+ <div class="gl-display-flex gl-flex-wrap gl-align-items-center">
+ <item-milestone
+ v-if="milestone"
+ :milestone="milestone"
+ class="gl-display-flex gl-align-items-center gl-mr-5 gl-max-w-15 gl-text-secondary! gl-cursor-help! gl-text-decoration-none!"
+ />
+ <gl-avatars-inline
+ v-if="assignees.length"
+ :avatars="assignees"
+ :collapsed="true"
+ :max-visible="2"
+ :avatar-size="24"
+ badge-tooltip-prop="name"
+ :badge-sr-only-text="assigneesCollapsedTooltip"
+ :class="assigneesContainerClass"
+ >
+ <template #avatar="{ avatar }">
+ <gl-avatar-link v-gl-tooltip target="blank" :href="avatar.webUrl" :title="avatar.name">
+ <gl-avatar :src="avatar.avatarUrl" :size="24" />
+ </gl-avatar-link>
+ </template>
+ </gl-avatars-inline>
+ <div v-if="labels.length" class="gl-display-flex gl-flex-wrap" :class="labelsContainerClass">
+ <gl-label
+ v-for="label in labels"
+ :key="label.id"
+ :title="label.title"
+ :background-color="label.color"
+ :description="label.description"
+ :scoped="showScopedLabel(label)"
+ class="gl-mt-2 gl-sm-mt-0 gl-mr-2 gl-mb-auto gl-label-sm"
+ tooltip-placement="top"
+ />
+ </div>
+ </div>
+</template>
+
+<style scoped>
+/**
+ * These overrides are needed to address https://gitlab.com/gitlab-org/gitlab-ui/-/issues/865
+ */
+.fixed-width-avatars-2 {
+ width: 42px !important;
+}
+
+.fixed-width-avatars-3 {
+ width: 67px !important;
+}
+</style>
diff --git a/app/assets/javascripts/work_items/components/work_item_links/work_item_links.vue b/app/assets/javascripts/work_items/components/work_item_links/work_item_links.vue
index 3d469b790a1..faadb5fa6fa 100644
--- a/app/assets/javascripts/work_items/components/work_item_links/work_item_links.vue
+++ b/app/assets/javascripts/work_items/components/work_item_links/work_item_links.vue
@@ -9,13 +9,15 @@ import {
GlTooltipDirective,
} from '@gitlab/ui';
import { produce } from 'immer';
+import { isEmpty } from 'lodash';
import { s__ } from '~/locale';
import { convertToGraphQLId, getIdFromGraphQLId } from '~/graphql_shared/utils';
import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
import { TYPE_WORK_ITEM } from '~/graphql_shared/constants';
+import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import getIssueDetailsQuery from 'ee_else_ce/work_items/graphql/get_issue_details.query.graphql';
-import { isMetaKey } from '~/lib/utils/common_utils';
-import { setUrlParams, updateHistory } from '~/lib/utils/url_utility';
+import { isMetaKey, parseBoolean } from '~/lib/utils/common_utils';
+import { setUrlParams, updateHistory, getParameterByName } from '~/lib/utils/url_utility';
import {
FORM_TYPES,
@@ -26,6 +28,7 @@ import {
import getWorkItemLinksQuery from '../../graphql/work_item_links.query.graphql';
import updateWorkItemMutation from '../../graphql/update_work_item.mutation.graphql';
import workItemQuery from '../../graphql/work_item.query.graphql';
+import workItemByIidQuery from '../../graphql/work_item_by_iid.query.graphql';
import WorkItemDetailModal from '../work_item_detail_modal.vue';
import WorkItemLinkChild from './work_item_link_child.vue';
import WorkItemLinksForm from './work_item_links_form.vue';
@@ -45,6 +48,7 @@ export default {
directives: {
GlTooltip: GlTooltipDirective,
},
+ mixins: [glFeatureFlagMixin()],
inject: ['projectPath', 'iid'],
props: {
workItemId: {
@@ -72,6 +76,18 @@ export default {
error(e) {
this.error = e.message || this.$options.i18n.fetchError;
},
+ async result() {
+ const { id, iid } = this.childUrlParams;
+ this.activeChild = this.fetchByIid
+ ? this.children.find((child) => child.iid === iid) ?? {}
+ : this.children.find((child) => child.id === id) ?? {};
+ await this.$nextTick();
+ if (!isEmpty(this.activeChild)) {
+ this.$refs.modal.show();
+ return;
+ }
+ this.updateWorkItemIdUrlQuery();
+ },
},
parentIssue: {
query: getIssueDetailsQuery,
@@ -90,7 +106,7 @@ export default {
return {
isShownAddForm: false,
isOpen: true,
- activeChildId: null,
+ activeChild: {},
activeToast: null,
prefetchedWorkItem: null,
error: undefined,
@@ -139,6 +155,29 @@ export default {
childrenCountLabel() {
return this.isLoading && this.children.length === 0 ? '...' : this.children.length;
},
+ fetchByIid() {
+ return this.glFeatures.useIidInWorkItemsPath && parseBoolean(getParameterByName('iid_path'));
+ },
+ childUrlParams() {
+ const params = {};
+ if (this.fetchByIid) {
+ const iid = getParameterByName('work_item_iid');
+ if (iid) {
+ params.iid = iid;
+ }
+ } else {
+ const workItemId = getParameterByName('work_item_id');
+ if (workItemId) {
+ params.id = convertToGraphQLId(TYPE_WORK_ITEM, workItemId);
+ }
+ }
+ return params;
+ },
+ },
+ mounted() {
+ if (!isEmpty(this.childUrlParams)) {
+ this.addWorkItemQuery(this.childUrlParams);
+ }
},
methods: {
toggle() {
@@ -159,29 +198,29 @@ export default {
const { defaultClient: client } = this.$apollo.provider.clients;
this.toggleChildFromCache(child, child.id, client);
},
- openChild(childItemId, e) {
+ openChild(child, e) {
if (isMetaKey(e)) {
return;
}
e.preventDefault();
- this.activeChildId = childItemId;
+ this.activeChild = child;
this.$refs.modal.show();
- this.updateWorkItemIdUrlQuery(childItemId);
+ this.updateWorkItemIdUrlQuery(child);
},
- closeModal() {
- this.activeChildId = null;
- this.updateWorkItemIdUrlQuery(undefined);
+ async closeModal() {
+ this.activeChild = {};
+ this.updateWorkItemIdUrlQuery();
},
handleWorkItemDeleted(childId) {
const { defaultClient: client } = this.$apollo.provider.clients;
this.toggleChildFromCache(null, childId, client);
this.activeToast = this.$toast.show(s__('WorkItem|Task deleted'));
},
- updateWorkItemIdUrlQuery(childItemId) {
- updateHistory({
- url: setUrlParams({ work_item_id: getIdFromGraphQLId(childItemId) }),
- replace: true,
- });
+ updateWorkItemIdUrlQuery({ id, iid } = {}) {
+ const params = this.fetchByIid
+ ? { work_item_iid: iid }
+ : { work_item_id: getIdFromGraphQLId(id) };
+ updateHistory({ url: setUrlParams(params), replace: true });
},
toggleChildFromCache(workItem, childId, store) {
const sourceData = store.readQuery({
@@ -235,16 +274,31 @@ export default {
});
}
},
- prefetchWorkItem(id) {
+ addWorkItemQuery({ id, iid }) {
+ const variables = this.fetchByIid
+ ? {
+ fullPath: this.projectPath,
+ iid,
+ }
+ : {
+ id,
+ };
+ this.$apollo.addSmartQuery('prefetchedWorkItem', {
+ query() {
+ return this.fetchByIid ? workItemByIidQuery : workItemQuery;
+ },
+ variables,
+ update(data) {
+ return this.fetchByIid ? data.workspace.workItems.nodes[0] : data.workItem;
+ },
+ context: {
+ isSingleRequest: true,
+ },
+ });
+ },
+ prefetchWorkItem({ id, iid }) {
this.prefetch = setTimeout(
- () =>
- this.$apollo.addSmartQuery('prefetchedWorkItem', {
- query: workItemQuery,
- variables: {
- id,
- },
- update: (data) => data.workItem,
- }),
+ () => this.addWorkItemQuery({ id, iid }),
DEFAULT_DEBOUNCE_AND_THROTTLE_MS,
);
},
@@ -355,16 +409,17 @@ export default {
:can-update="canUpdate"
:issuable-gid="issuableGid"
:child-item="child"
- @click="openChild"
- @mouseover="prefetchWorkItem"
+ @click="openChild(child, $event)"
+ @mouseover="prefetchWorkItem(child)"
@mouseout="clearPrefetching"
- @remove="removeChild"
+ @removeChild="removeChild"
/>
<work-item-detail-modal
ref="modal"
- :work-item-id="activeChildId"
+ :work-item-id="activeChild.id"
+ :work-item-iid="activeChild.iid"
@close="closeModal"
- @workItemDeleted="handleWorkItemDeleted(activeChildId)"
+ @workItemDeleted="handleWorkItemDeleted(activeChild.id)"
/>
</template>
</div>
diff --git a/app/assets/javascripts/work_items/components/work_item_links/work_item_links_form.vue b/app/assets/javascripts/work_items/components/work_item_links/work_item_links_form.vue
index 095ea86e0d8..5cf0c4154bb 100644
--- a/app/assets/javascripts/work_items/components/work_item_links/work_item_links_form.vue
+++ b/app/assets/javascripts/work_items/components/work_item_links/work_item_links_form.vue
@@ -9,7 +9,16 @@ import projectWorkItemTypesQuery from '~/work_items/graphql/project_work_item_ty
import projectWorkItemsQuery from '../../graphql/project_work_items.query.graphql';
import updateWorkItemMutation from '../../graphql/update_work_item.mutation.graphql';
import createWorkItemMutation from '../../graphql/create_work_item.mutation.graphql';
-import { FORM_TYPES, TASK_TYPE_NAME } from '../../constants';
+import {
+ FORM_TYPES,
+ WORK_ITEMS_TYPE_MAP,
+ WORK_ITEM_TYPE_ENUM_TASK,
+ I18N_WORK_ITEM_CREATE_BUTTON_LABEL,
+ I18N_WORK_ITEM_SEARCH_INPUT_PLACEHOLDER,
+ I18N_WORK_ITEM_ADD_BUTTON_LABEL,
+ I18N_WORK_ITEM_ADD_MULTIPLE_BUTTON_LABEL,
+ sprintfWorkItem,
+} from '../../constants';
export default {
components: {
@@ -52,6 +61,11 @@ export default {
type: String,
required: true,
},
+ childrenType: {
+ type: String,
+ required: false,
+ default: WORK_ITEM_TYPE_ENUM_TASK,
+ },
},
apollo: {
workItemTypes: {
@@ -71,7 +85,7 @@ export default {
return {
projectPath: this.projectPath,
searchTerm: this.search?.title || this.search,
- types: ['TASK'],
+ types: [this.childrenType],
in: this.search ? 'TITLE' : undefined,
};
},
@@ -79,7 +93,9 @@ export default {
return !this.searchStarted;
},
update(data) {
- return data.workspace.workItems.nodes.filter((wi) => !this.childrenIds.includes(wi.id));
+ return data.workspace.workItems.nodes.filter(
+ (wi) => !this.childrenIds.includes(wi.id) && this.issuableGid !== wi.id,
+ );
},
},
},
@@ -99,14 +115,14 @@ export default {
let workItemInput = {
title: this.search?.title || this.search,
projectPath: this.projectPath,
- workItemTypeId: this.taskWorkItemType,
+ workItemTypeId: this.childWorkItemType,
hierarchyWidget: {
parentId: this.issuableGid,
},
confidential: this.parentConfidential,
};
- if (this.associateMilestone) {
+ if (this.parentMilestoneId) {
workItemInput = {
...workItemInput,
milestoneWidget: {
@@ -114,46 +130,62 @@ export default {
},
};
}
+
+ if (this.associateIteration) {
+ workItemInput = {
+ ...workItemInput,
+ iterationWidget: {
+ iterationId: this.parentIterationId,
+ },
+ };
+ }
+
return workItemInput;
},
+ workItemsMvcEnabled() {
+ return this.glFeatures.workItemsMvc;
+ },
workItemsMvc2Enabled() {
return this.glFeatures.workItemsMvc2;
},
isCreateForm() {
return this.formType === FORM_TYPES.create;
},
+ childrenTypeName() {
+ return WORK_ITEMS_TYPE_MAP[this.childrenType]?.name;
+ },
addOrCreateButtonLabel() {
if (this.isCreateForm) {
- return this.$options.i18n.createChildOptionLabel;
+ return sprintfWorkItem(I18N_WORK_ITEM_CREATE_BUTTON_LABEL, this.childrenTypeName);
} else if (this.workItemsToAdd.length > 1) {
- return this.$options.i18n.addTasksButtonLabel;
+ return sprintfWorkItem(I18N_WORK_ITEM_ADD_MULTIPLE_BUTTON_LABEL, this.childrenTypeName);
}
- return this.$options.i18n.addTaskButtonLabel;
+ return sprintfWorkItem(I18N_WORK_ITEM_ADD_BUTTON_LABEL, this.childrenTypeName);
},
addOrCreateMethod() {
return this.isCreateForm ? this.createChild : this.addChild;
},
- taskWorkItemType() {
- return this.workItemTypes.find((type) => type.name === TASK_TYPE_NAME)?.id;
+ childWorkItemType() {
+ return this.workItemTypes.find((type) => type.name === this.childrenTypeName)?.id;
},
parentIterationId() {
return this.parentIteration?.id;
},
associateIteration() {
- return this.parentIterationId && this.hasIterationsFeature && this.workItemsMvc2Enabled;
+ return this.parentIterationId && this.hasIterationsFeature;
},
parentMilestoneId() {
return this.parentMilestone?.id;
},
- associateMilestone() {
- return this.parentMilestoneId && this.workItemsMvc2Enabled;
- },
isSubmitButtonDisabled() {
return this.isCreateForm ? this.search.length === 0 : this.workItemsToAdd.length === 0;
},
isLoading() {
return this.$apollo.queries.availableWorkItems.loading;
},
+ addInputPlaceholder() {
+ return sprintfWorkItem(I18N_WORK_ITEM_SEARCH_INPUT_PLACEHOLDER, this.childrenTypeName);
+ },
},
created() {
this.debouncedSearchKeyUpdate = debounce(this.setSearchKey, DEFAULT_DEBOUNCE_AND_THROTTLE_MS);
@@ -206,13 +238,6 @@ export default {
} else {
this.unsetError();
this.$emit('addWorkItemChild', data.workItemCreate.workItem);
- /**
- * call update mutation only when there is an iteration associated with the issue
- */
- // TODO: setting the iteration should be moved to the creation mutation once the backend is done
- if (this.associateIteration) {
- this.addIterationToWorkItem(data.workItemCreate.workItem.id);
- }
}
})
.catch(() => {
@@ -223,19 +248,6 @@ export default {
this.childToCreateTitle = null;
});
},
- async addIterationToWorkItem(workItemId) {
- await this.$apollo.mutate({
- mutation: updateWorkItemMutation,
- variables: {
- input: {
- id: workItemId,
- iterationWidget: {
- iterationId: this.parentIterationId,
- },
- },
- },
- });
- },
setSearchKey(value) {
this.search = value;
},
@@ -253,17 +265,13 @@ export default {
},
i18n: {
inputLabel: __('Title'),
- addTaskButtonLabel: s__('WorkItem|Add task'),
- addTasksButtonLabel: s__('WorkItem|Add tasks'),
addChildErrorMessage: s__(
'WorkItem|Something went wrong when trying to add a child. Please try again.',
),
- createChildOptionLabel: s__('WorkItem|Create task'),
createChildErrorMessage: s__(
'WorkItem|Something went wrong when trying to create a child. Please try again.',
),
createPlaceholder: s__('WorkItem|Add a title'),
- addPlaceholder: s__('WorkItem|Search existing tasks'),
fieldValidationMessage: __('Maximum of 255 characters'),
},
};
@@ -296,7 +304,7 @@ export default {
v-model="workItemsToAdd"
:dropdown-items="availableWorkItems"
:loading="isLoading"
- :placeholder="$options.i18n.addPlaceholder"
+ :placeholder="addInputPlaceholder"
menu-class="gl-dropdown-menu-wide dropdown-reduced-height gl-min-h-7!"
class="gl-mb-4"
data-testid="work-item-token-select-input"
diff --git a/app/assets/javascripts/work_items/components/work_item_links/work_item_tree.vue b/app/assets/javascripts/work_items/components/work_item_links/work_item_tree.vue
new file mode 100644
index 00000000000..f06de2ca048
--- /dev/null
+++ b/app/assets/javascripts/work_items/components/work_item_links/work_item_tree.vue
@@ -0,0 +1,244 @@
+<script>
+import { GlButton } from '@gitlab/ui';
+import { isEmpty } from 'lodash';
+import { __ } from '~/locale';
+import { convertToGraphQLId } from '~/graphql_shared/utils';
+import { TYPE_WORK_ITEM } from '~/graphql_shared/constants';
+import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
+import { parseBoolean } from '~/lib/utils/common_utils';
+import { getParameterByName } from '~/lib/utils/url_utility';
+import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
+
+import {
+ FORM_TYPES,
+ WIDGET_TYPE_HIERARCHY,
+ WORK_ITEMS_TREE_TEXT_MAP,
+ WORK_ITEM_TYPE_ENUM_OBJECTIVE,
+ WORK_ITEM_TYPE_ENUM_KEY_RESULT,
+ WORK_ITEM_TYPE_VALUE_OBJECTIVE,
+} from '../../constants';
+import workItemQuery from '../../graphql/work_item.query.graphql';
+import workItemByIidQuery from '../../graphql/work_item_by_iid.query.graphql';
+import OkrActionsSplitButton from './okr_actions_split_button.vue';
+import WorkItemLinksForm from './work_item_links_form.vue';
+import WorkItemLinkChild from './work_item_link_child.vue';
+
+export default {
+ FORM_TYPES,
+ WORK_ITEMS_TREE_TEXT_MAP,
+ WORK_ITEM_TYPE_ENUM_OBJECTIVE,
+ WORK_ITEM_TYPE_ENUM_KEY_RESULT,
+ components: {
+ GlButton,
+ OkrActionsSplitButton,
+ WorkItemLinksForm,
+ WorkItemLinkChild,
+ },
+ mixins: [glFeatureFlagMixin()],
+ props: {
+ workItemType: {
+ type: String,
+ required: true,
+ },
+ workItemId: {
+ type: String,
+ required: true,
+ },
+ children: {
+ type: Array,
+ required: false,
+ default: () => [],
+ },
+ canUpdate: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ projectPath: {
+ type: String,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ isShownAddForm: false,
+ isOpen: true,
+ error: null,
+ formType: null,
+ childType: null,
+ prefetchedWorkItem: null,
+ };
+ },
+ computed: {
+ toggleIcon() {
+ return this.isOpen ? 'chevron-lg-up' : 'chevron-lg-down';
+ },
+ toggleLabel() {
+ return this.isOpen ? __('Collapse') : __('Expand');
+ },
+ fetchByIid() {
+ return this.glFeatures.useIidInWorkItemsPath && parseBoolean(getParameterByName('iid_path'));
+ },
+ childrenIds() {
+ return this.children.map((c) => c.id);
+ },
+ hasIndirectChildren() {
+ return this.children
+ .map(
+ (child) => child.widgets?.find((widget) => widget.type === WIDGET_TYPE_HIERARCHY) || {},
+ )
+ .some((hierarchy) => hierarchy.hasChildren);
+ },
+ childUrlParams() {
+ const params = {};
+ if (this.fetchByIid) {
+ const iid = getParameterByName('work_item_iid');
+ if (iid) {
+ params.iid = iid;
+ }
+ } else {
+ const workItemId = getParameterByName('work_item_id');
+ if (workItemId) {
+ params.id = convertToGraphQLId(TYPE_WORK_ITEM, workItemId);
+ }
+ }
+ return params;
+ },
+ },
+ mounted() {
+ if (!isEmpty(this.childUrlParams)) {
+ this.addWorkItemQuery(this.childUrlParams);
+ }
+ },
+ methods: {
+ toggle() {
+ this.isOpen = !this.isOpen;
+ },
+ showAddForm(formType, childType) {
+ this.isOpen = true;
+ this.isShownAddForm = true;
+ this.formType = formType;
+ this.childType = childType;
+ this.$nextTick(() => {
+ this.$refs.wiLinksForm.$refs.wiTitleInput?.$el.focus();
+ });
+ },
+ hideAddForm() {
+ this.isShownAddForm = false;
+ },
+ addWorkItemQuery({ id, iid }) {
+ const variables = this.fetchByIid
+ ? {
+ fullPath: this.projectPath,
+ iid,
+ }
+ : {
+ id,
+ };
+ this.$apollo.addSmartQuery('prefetchedWorkItem', {
+ query() {
+ return this.fetchByIid ? workItemByIidQuery : workItemQuery;
+ },
+ variables,
+ update(data) {
+ return this.fetchByIid ? data.workspace.workItems.nodes[0] : data.workItem;
+ },
+ context: {
+ isSingleRequest: true,
+ },
+ });
+ },
+ prefetchWorkItem({ id, iid }) {
+ if (this.workItemType !== WORK_ITEM_TYPE_VALUE_OBJECTIVE) {
+ this.prefetch = setTimeout(
+ () => this.addWorkItemQuery({ id, iid }),
+ DEFAULT_DEBOUNCE_AND_THROTTLE_MS,
+ );
+ }
+ },
+ clearPrefetching() {
+ if (this.prefetch) {
+ clearTimeout(this.prefetch);
+ }
+ },
+ },
+};
+</script>
+
+<template>
+ <div
+ class="gl-rounded-base gl-border-1 gl-border-solid gl-border-gray-100 gl-bg-gray-10 gl-mt-4"
+ data-testid="work-item-tree"
+ >
+ <div
+ class="gl-px-5 gl-py-3 gl-display-flex gl-justify-content-space-between"
+ :class="{ 'gl-border-b-1 gl-border-b-solid gl-border-b-gray-100': isOpen }"
+ >
+ <div class="gl-display-flex gl-flex-grow-1">
+ <h5 class="gl-m-0 gl-line-height-24">
+ {{ $options.WORK_ITEMS_TREE_TEXT_MAP[workItemType].title }}
+ </h5>
+ </div>
+ <okr-actions-split-button
+ @showCreateObjectiveForm="
+ showAddForm($options.FORM_TYPES.create, $options.WORK_ITEM_TYPE_ENUM_OBJECTIVE)
+ "
+ @showAddObjectiveForm="
+ showAddForm($options.FORM_TYPES.add, $options.WORK_ITEM_TYPE_ENUM_OBJECTIVE)
+ "
+ @showCreateKeyResultForm="
+ showAddForm($options.FORM_TYPES.create, $options.WORK_ITEM_TYPE_ENUM_KEY_RESULT)
+ "
+ @showAddKeyResultForm="
+ showAddForm($options.FORM_TYPES.add, $options.WORK_ITEM_TYPE_ENUM_KEY_RESULT)
+ "
+ />
+ <div class="gl-border-l-1 gl-border-l-solid gl-border-l-gray-100 gl-pl-3 gl-ml-3">
+ <gl-button
+ category="tertiary"
+ size="small"
+ :icon="toggleIcon"
+ :aria-label="toggleLabel"
+ data-testid="toggle-tree"
+ @click="toggle"
+ />
+ </div>
+ </div>
+ <div
+ v-if="isOpen"
+ class="gl-bg-gray-10 gl-rounded-bottom-left-base gl-rounded-bottom-right-base"
+ :class="{ 'gl-p-5 gl-pb-3': !error }"
+ data-testid="tree-body"
+ >
+ <div v-if="!isShownAddForm && !error && children.length === 0" data-testid="tree-empty">
+ <p class="gl-mb-3">
+ {{ $options.WORK_ITEMS_TREE_TEXT_MAP[workItemType].empty }}
+ </p>
+ </div>
+ <work-item-links-form
+ v-if="isShownAddForm"
+ ref="wiLinksForm"
+ data-testid="add-tree-form"
+ :issuable-gid="workItemId"
+ :form-type="formType"
+ :children-type="childType"
+ :children-ids="childrenIds"
+ @addWorkItemChild="$emit('addWorkItemChild', $event)"
+ @cancel="hideAddForm"
+ />
+ <work-item-link-child
+ v-for="child in children"
+ :key="child.id"
+ :project-path="projectPath"
+ :can-update="canUpdate"
+ :issuable-gid="workItemId"
+ :child-item="child"
+ :work-item-type="workItemType"
+ :has-indirect-children="hasIndirectChildren"
+ @mouseover="prefetchWorkItem(child)"
+ @mouseout="clearPrefetching"
+ @removeChild="$emit('removeChild', $event)"
+ />
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/work_items/components/work_item_links/work_item_tree_children.vue b/app/assets/javascripts/work_items/components/work_item_links/work_item_tree_children.vue
new file mode 100644
index 00000000000..911cac4de88
--- /dev/null
+++ b/app/assets/javascripts/work_items/components/work_item_links/work_item_tree_children.vue
@@ -0,0 +1,68 @@
+<script>
+import { createAlert } from '~/flash';
+import { s__ } from '~/locale';
+
+import updateWorkItemMutation from '../../graphql/update_work_item.mutation.graphql';
+
+export default {
+ components: {
+ WorkItemLinkChild: () => import('./work_item_link_child.vue'),
+ },
+ props: {
+ workItemType: {
+ type: String,
+ required: true,
+ },
+ workItemId: {
+ type: String,
+ required: true,
+ },
+ children: {
+ type: Array,
+ required: false,
+ default: () => [],
+ },
+ canUpdate: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ projectPath: {
+ type: String,
+ required: true,
+ },
+ },
+ methods: {
+ async updateWorkItem(childId) {
+ try {
+ await this.$apollo.mutate({
+ mutation: updateWorkItemMutation,
+ variables: { input: { id: childId, hierarchyWidget: { parentId: null } } },
+ });
+ this.$emit('removeChild');
+ } catch (error) {
+ createAlert({
+ message: s__('Hierarchy|Something went wrong while removing a child item.'),
+ captureError: true,
+ error,
+ });
+ }
+ },
+ },
+};
+</script>
+
+<template>
+ <div class="gl-ml-6">
+ <work-item-link-child
+ v-for="child in children"
+ :key="child.id"
+ :project-path="projectPath"
+ :can-update="canUpdate"
+ :issuable-gid="workItemId"
+ :child-item="child"
+ :work-item-type="workItemType"
+ @removeChild="updateWorkItem"
+ />
+ </div>
+</template>
diff --git a/app/assets/javascripts/work_items/components/work_item_milestone.vue b/app/assets/javascripts/work_items/components/work_item_milestone.vue
index a8d3b57aae0..6ed230b8ad4 100644
--- a/app/assets/javascripts/work_items/components/work_item_milestone.vue
+++ b/app/assets/javascripts/work_items/components/work_item_milestone.vue
@@ -13,6 +13,7 @@ import { debounce } from 'lodash';
import Tracking from '~/tracking';
import { s__, __ } from '~/locale';
import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
+import { MILESTONE_STATE } from '~/sidebar/constants';
import projectMilestonesQuery from '~/sidebar/queries/project_milestones.query.graphql';
import updateWorkItemMutation from '~/work_items/graphql/update_work_item.mutation.graphql';
import {
@@ -118,6 +119,7 @@ export default {
return {
'gl-text-gray-500!': this.canUpdate && this.isNoMilestone,
'is-not-focused': !this.isFocused,
+ 'gl-min-w-20': true,
};
},
},
@@ -139,6 +141,7 @@ export default {
return {
fullPath: this.fullPath,
title: this.searchTerm,
+ state: MILESTONE_STATE.ACTIVE,
first: 20,
};
},
@@ -214,9 +217,10 @@ export default {
<template>
<gl-form-group
- class="work-item-dropdown"
+ class="work-item-dropdown gl-flex-nowrap"
:label="$options.i18n.MILESTONE"
- label-class="gl-pb-0! gl-overflow-wrap-break gl-mt-3"
+ label-for="milestone-value"
+ label-class="gl-pb-0! gl-mt-3 gl-overflow-wrap-break"
label-cols="3"
label-cols-lg="2"
>
@@ -229,6 +233,8 @@ export default {
</span>
<gl-dropdown
v-else
+ id="milestone-value"
+ class="gl-pl-0 gl-max-w-full"
:toggle-class="dropdownClasses"
:text="dropdownText"
:loading="updateInProgress"
diff --git a/app/assets/javascripts/work_items/components/work_item_notes.vue b/app/assets/javascripts/work_items/components/work_item_notes.vue
new file mode 100644
index 00000000000..91e90589a93
--- /dev/null
+++ b/app/assets/javascripts/work_items/components/work_item_notes.vue
@@ -0,0 +1,109 @@
+<script>
+import { GlSkeletonLoader } from '@gitlab/ui';
+import { s__ } from '~/locale';
+import SystemNote from '~/work_items/components/notes/system_note.vue';
+import { i18n, DEFAULT_PAGE_SIZE_NOTES } from '~/work_items/constants';
+import { getWorkItemNotesQuery } from '~/work_items/utils';
+
+export default {
+ i18n: {
+ ACTIVITY_LABEL: s__('WorkItem|Activity'),
+ },
+ loader: {
+ repeat: 10,
+ width: 1000,
+ height: 40,
+ },
+ components: {
+ SystemNote,
+ GlSkeletonLoader,
+ },
+ props: {
+ workItemId: {
+ type: String,
+ required: true,
+ },
+ queryVariables: {
+ type: Object,
+ required: true,
+ },
+ fullPath: {
+ type: String,
+ required: true,
+ },
+ fetchByIid: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ },
+ computed: {
+ areNotesLoading() {
+ return this.$apollo.queries.workItemNotes.loading;
+ },
+ notes() {
+ return this.workItemNotes?.nodes;
+ },
+ pageInfo() {
+ return this.workItemNotes?.pageInfo;
+ },
+ },
+ apollo: {
+ workItemNotes: {
+ query() {
+ return getWorkItemNotesQuery(this.fetchByIid);
+ },
+ context: {
+ isSingleRequest: true,
+ },
+ variables() {
+ return {
+ ...this.queryVariables,
+ pageSize: DEFAULT_PAGE_SIZE_NOTES,
+ };
+ },
+ update(data) {
+ const workItemWidgets = this.fetchByIid
+ ? data.workspace?.workItems?.nodes[0]?.widgets
+ : data.workItem?.widgets;
+ return workItemWidgets.find((widget) => widget.type === 'NOTES').discussions || [];
+ },
+ skip() {
+ return !this.queryVariables.id && !this.queryVariables.iid;
+ },
+ error() {
+ this.$emit('error', i18n.fetchError);
+ },
+ },
+ },
+};
+</script>
+
+<template>
+ <div class="gl-border-t gl-mt-5">
+ <label class="gl-mb-0">{{ $options.i18n.ACTIVITY_LABEL }}</label>
+ <div v-if="areNotesLoading" class="gl-mt-5">
+ <gl-skeleton-loader
+ v-for="index in $options.loader.repeat"
+ :key="index"
+ :width="$options.loader.width"
+ :height="$options.loader.height"
+ preserve-aspect-ratio="xMinYMax meet"
+ >
+ <circle cx="20" cy="20" r="16" />
+ <rect width="500" x="45" y="15" height="10" rx="4" />
+ </gl-skeleton-loader>
+ </div>
+ <div v-else class="issuable-discussion gl-mb-5 work-item-notes">
+ <template v-if="notes && notes.length">
+ <ul class="notes main-notes-list timeline">
+ <system-note
+ v-for="note in notes"
+ :key="note.notes.nodes[0].id"
+ :note="note.notes.nodes[0]"
+ />
+ </ul>
+ </template>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/work_items/components/work_item_type_icon.vue b/app/assets/javascripts/work_items/components/work_item_type_icon.vue
index 96a6493357c..32678e29fa4 100644
--- a/app/assets/javascripts/work_items/components/work_item_type_icon.vue
+++ b/app/assets/javascripts/work_items/components/work_item_type_icon.vue
@@ -33,6 +33,11 @@ export default {
},
computed: {
iconName() {
+ // TODO: Remove this once https://gitlab.com/gitlab-org/gitlab-svgs/-/merge_requests/865
+ // is merged and updated in GitLab repo.
+ if (this.workItemIconName === 'issue-type-keyresult') {
+ return 'issue-type-key-result';
+ }
return (
this.workItemIconName || WORK_ITEMS_TYPE_MAP[this.workItemType]?.icon || 'issue-type-issue'
);
diff --git a/app/assets/javascripts/work_items/constants.js b/app/assets/javascripts/work_items/constants.js
index 8b47c24de7d..3cd17f4d360 100644
--- a/app/assets/javascripts/work_items/constants.js
+++ b/app/assets/javascripts/work_items/constants.js
@@ -16,17 +16,23 @@ export const WIDGET_TYPE_DESCRIPTION = 'DESCRIPTION';
export const WIDGET_TYPE_LABELS = 'LABELS';
export const WIDGET_TYPE_START_AND_DUE_DATE = 'START_AND_DUE_DATE';
export const WIDGET_TYPE_WEIGHT = 'WEIGHT';
+export const WIDGET_TYPE_PROGRESS = 'PROGRESS';
export const WIDGET_TYPE_HIERARCHY = 'HIERARCHY';
export const WIDGET_TYPE_MILESTONE = 'MILESTONE';
export const WIDGET_TYPE_ITERATION = 'ITERATION';
-
-export const WORK_ITEM_VIEWED_STORAGE_KEY = 'gl-show-work-item-banner';
+export const WIDGET_TYPE_NOTES = 'NOTES';
+export const WIDGET_TYPE_HEALTH_STATUS = 'HEALTH_STATUS';
export const WORK_ITEM_TYPE_ENUM_INCIDENT = 'INCIDENT';
export const WORK_ITEM_TYPE_ENUM_ISSUE = 'ISSUE';
export const WORK_ITEM_TYPE_ENUM_TASK = 'TASK';
export const WORK_ITEM_TYPE_ENUM_TEST_CASE = 'TEST_CASE';
export const WORK_ITEM_TYPE_ENUM_REQUIREMENTS = 'REQUIREMENTS';
+export const WORK_ITEM_TYPE_ENUM_OBJECTIVE = 'OBJECTIVE';
+export const WORK_ITEM_TYPE_ENUM_KEY_RESULT = 'KEY_RESULT';
+
+export const WORK_ITEM_TYPE_VALUE_ISSUE = 'Issue';
+export const WORK_ITEM_TYPE_VALUE_OBJECTIVE = 'Objective';
export const i18n = {
fetchErrorTitle: s__('WorkItem|Work item not found'),
@@ -61,6 +67,13 @@ export const I18N_WORK_ITEM_FETCH_ITERATIONS_ERROR = s__(
'WorkItem|Something went wrong when fetching iterations. Please try again.',
);
+export const I18N_WORK_ITEM_CREATE_BUTTON_LABEL = s__('WorkItem|Create %{workItemType}');
+export const I18N_WORK_ITEM_ADD_BUTTON_LABEL = s__('WorkItem|Add %{workItemType}');
+export const I18N_WORK_ITEM_ADD_MULTIPLE_BUTTON_LABEL = s__('WorkItem|Add %{workItemType}s');
+export const I18N_WORK_ITEM_SEARCH_INPUT_PLACEHOLDER = s__(
+ 'WorkItem|Search existing %{workItemType}s',
+);
+
export const sprintfWorkItem = (msg, workItemTypeArg) => {
const workItemType = workItemTypeArg || s__('WorkItem|Work item');
return capitalizeFirstCharacter(
@@ -100,11 +113,45 @@ export const WORK_ITEMS_TYPE_MAP = {
icon: `issue-type-requirements`,
name: s__('WorkItem|Requirements'),
},
+ [WORK_ITEM_TYPE_ENUM_OBJECTIVE]: {
+ icon: `issue-type-objective`,
+ name: s__('WorkItem|Objective'),
+ },
+ [WORK_ITEM_TYPE_ENUM_KEY_RESULT]: {
+ icon: `issue-type-issue`,
+ name: s__('WorkItem|Key Result'),
+ },
+};
+
+export const WORK_ITEMS_TREE_TEXT_MAP = {
+ [WORK_ITEM_TYPE_VALUE_OBJECTIVE]: {
+ title: s__('WorkItem|Child objectives and key results'),
+ empty: s__('WorkItem|No objectives or key results are currently assigned.'),
+ },
+ [WORK_ITEM_TYPE_VALUE_ISSUE]: {
+ title: s__('WorkItem|Tasks'),
+ empty: s__(
+ 'WorkItem|No tasks are currently assigned. Use tasks to break down this issue into smaller parts.',
+ ),
+ },
+};
+
+export const WORK_ITEM_NAME_TO_ICON_MAP = {
+ Issue: 'issue-type-issue',
+ Task: 'issue-type-task',
+ Objective: 'issue-type-objective',
+ // eslint-disable-next-line @gitlab/require-i18n-strings
+ 'Key Result': 'issue-type-key-result',
};
export const FORM_TYPES = {
create: 'create',
add: 'add',
+ [WORK_ITEM_TYPE_ENUM_OBJECTIVE]: {
+ icon: `issue-type-issue`,
+ name: s__('WorkItem|Objective'),
+ },
};
export const DEFAULT_PAGE_SIZE_ASSIGNEES = 10;
+export const DEFAULT_PAGE_SIZE_NOTES = 100;
diff --git a/app/assets/javascripts/work_items/graphql/discussion.fragment.graphql b/app/assets/javascripts/work_items/graphql/discussion.fragment.graphql
new file mode 100644
index 00000000000..62ced6bdfea
--- /dev/null
+++ b/app/assets/javascripts/work_items/graphql/discussion.fragment.graphql
@@ -0,0 +1,12 @@
+#import "~/graphql_shared/fragments/user.fragment.graphql"
+
+fragment Discussion on Note {
+ id
+ body
+ bodyHtml
+ systemNoteIconName
+ createdAt
+ author {
+ ...User
+ }
+}
diff --git a/app/assets/javascripts/work_items/graphql/milestone.fragment.graphql b/app/assets/javascripts/work_items/graphql/milestone.fragment.graphql
index 58140aff89e..5c93370aac9 100644
--- a/app/assets/javascripts/work_items/graphql/milestone.fragment.graphql
+++ b/app/assets/javascripts/work_items/graphql/milestone.fragment.graphql
@@ -2,4 +2,7 @@ fragment MilestoneFragment on Milestone {
expired
id
title
+ state
+ startDate
+ dueDate
}
diff --git a/app/assets/javascripts/work_items/graphql/work_item_links.query.graphql b/app/assets/javascripts/work_items/graphql/work_item_links.query.graphql
index 7b63d9c7ca3..7fcf622cdb2 100644
--- a/app/assets/javascripts/work_items/graphql/work_item_links.query.graphql
+++ b/app/assets/javascripts/work_items/graphql/work_item_links.query.graphql
@@ -20,9 +20,12 @@ query workItemLinksQuery($id: WorkItemID!) {
children {
nodes {
id
+ iid
confidential
workItemType {
id
+ name
+ iconName
}
title
state
diff --git a/app/assets/javascripts/work_items/graphql/work_item_metadata_widgets.fragment.graphql b/app/assets/javascripts/work_items/graphql/work_item_metadata_widgets.fragment.graphql
new file mode 100644
index 00000000000..baefcdaea93
--- /dev/null
+++ b/app/assets/javascripts/work_items/graphql/work_item_metadata_widgets.fragment.graphql
@@ -0,0 +1,29 @@
+#import "~/graphql_shared/fragments/label.fragment.graphql"
+#import "~/graphql_shared/fragments/user.fragment.graphql"
+#import "~/work_items/graphql/milestone.fragment.graphql"
+
+fragment WorkItemMetadataWidgets on WorkItemWidget {
+ ... on WorkItemWidgetMilestone {
+ type
+ milestone {
+ ...MilestoneFragment
+ }
+ }
+ ... on WorkItemWidgetAssignees {
+ type
+ assignees {
+ nodes {
+ ...User
+ }
+ }
+ }
+ ... on WorkItemWidgetLabels {
+ type
+ allowsScopedLabels
+ labels {
+ nodes {
+ ...Label
+ }
+ }
+ }
+}
diff --git a/app/assets/javascripts/work_items/graphql/work_item_notes.query.graphql b/app/assets/javascripts/work_items/graphql/work_item_notes.query.graphql
new file mode 100644
index 00000000000..9439f22f955
--- /dev/null
+++ b/app/assets/javascripts/work_items/graphql/work_item_notes.query.graphql
@@ -0,0 +1,27 @@
+#import "~/graphql_shared/fragments/page_info.fragment.graphql"
+#import "~/work_items/graphql/discussion.fragment.graphql"
+
+query workItemNotes($id: WorkItemID!, $after: String, $pageSize: Int) {
+ workItem(id: $id) {
+ id
+ iid
+ widgets {
+ ... on WorkItemWidgetNotes {
+ type
+ discussions(first: $pageSize, after: $after, filter: ONLY_ACTIVITY) {
+ pageInfo {
+ ...PageInfo
+ }
+ nodes {
+ id
+ notes {
+ nodes {
+ ...Discussion
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/app/assets/javascripts/work_items/graphql/work_item_notes_by_iid.query.graphql b/app/assets/javascripts/work_items/graphql/work_item_notes_by_iid.query.graphql
new file mode 100644
index 00000000000..3e0960f3f54
--- /dev/null
+++ b/app/assets/javascripts/work_items/graphql/work_item_notes_by_iid.query.graphql
@@ -0,0 +1,32 @@
+#import "~/graphql_shared/fragments/page_info.fragment.graphql"
+#import "~/work_items/graphql/discussion.fragment.graphql"
+
+query workItemNotesByIid($fullPath: ID!, $iid: String, $after: String, $pageSize: Int) {
+ workspace: project(fullPath: $fullPath) {
+ id
+ workItems(iid: $iid) {
+ nodes {
+ id
+ iid
+ widgets {
+ ... on WorkItemWidgetNotes {
+ type
+ discussions(first: $pageSize, after: $after, filter: ONLY_ACTIVITY) {
+ pageInfo {
+ ...PageInfo
+ }
+ nodes {
+ id
+ notes {
+ nodes {
+ ...Discussion
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/app/assets/javascripts/work_items/graphql/work_item_tree.query.graphql b/app/assets/javascripts/work_items/graphql/work_item_tree.query.graphql
new file mode 100644
index 00000000000..006ca29e01c
--- /dev/null
+++ b/app/assets/javascripts/work_items/graphql/work_item_tree.query.graphql
@@ -0,0 +1,53 @@
+#import "~/graphql_shared/fragments/label.fragment.graphql"
+#import "~/graphql_shared/fragments/user.fragment.graphql"
+#import "./work_item_metadata_widgets.fragment.graphql"
+
+query workItemTreeQuery($id: WorkItemID!) {
+ workItem(id: $id) {
+ id
+ workItemType {
+ id
+ name
+ iconName
+ }
+ title
+ userPermissions {
+ deleteWorkItem
+ updateWorkItem
+ }
+ confidential
+ widgets {
+ type
+ ... on WorkItemWidgetHierarchy {
+ type
+ parent {
+ id
+ }
+ children {
+ nodes {
+ id
+ iid
+ confidential
+ workItemType {
+ id
+ name
+ iconName
+ }
+ title
+ state
+ createdAt
+ closedAt
+ widgets {
+ ... on WorkItemWidgetHierarchy {
+ type
+ hasChildren
+ }
+ ...WorkItemMetadataWidgets
+ }
+ }
+ }
+ }
+ ...WorkItemMetadataWidgets
+ }
+ }
+}
diff --git a/app/assets/javascripts/work_items/graphql/work_item_widgets.fragment.graphql b/app/assets/javascripts/work_items/graphql/work_item_widgets.fragment.graphql
index b9715c21c27..cf3374e1737 100644
--- a/app/assets/javascripts/work_items/graphql/work_item_widgets.fragment.graphql
+++ b/app/assets/javascripts/work_items/graphql/work_item_widgets.fragment.graphql
@@ -1,6 +1,7 @@
#import "~/graphql_shared/fragments/label.fragment.graphql"
#import "~/graphql_shared/fragments/user.fragment.graphql"
#import "~/work_items/graphql/milestone.fragment.graphql"
+#import "./work_item_metadata_widgets.fragment.graphql"
fragment WorkItemWidgets on WorkItemWidget {
... on WorkItemWidgetDescription {
@@ -38,15 +39,39 @@ fragment WorkItemWidgets on WorkItemWidget {
}
... on WorkItemWidgetHierarchy {
type
+ hasChildren
parent {
id
iid
title
confidential
+ webUrl
+ workItemType {
+ id
+ name
+ iconName
+ }
}
children {
nodes {
id
+ confidential
+ workItemType {
+ id
+ name
+ iconName
+ }
+ title
+ state
+ createdAt
+ closedAt
+ widgets {
+ ... on WorkItemWidgetHierarchy {
+ type
+ hasChildren
+ }
+ ...WorkItemMetadataWidgets
+ }
}
}
}
@@ -56,4 +81,7 @@ fragment WorkItemWidgets on WorkItemWidget {
...MilestoneFragment
}
}
+ ... on WorkItemWidgetNotes {
+ type
+ }
}
diff --git a/app/assets/javascripts/work_items/index.js b/app/assets/javascripts/work_items/index.js
index 4fbcdfe2b96..a056fde6928 100644
--- a/app/assets/javascripts/work_items/index.js
+++ b/app/assets/javascripts/work_items/index.js
@@ -6,7 +6,14 @@ import { createRouter } from './router';
export const initWorkItemsRoot = () => {
const el = document.querySelector('#js-work-items');
- const { fullPath, hasIssueWeightsFeature, issuesListPath, hasIterationsFeature } = el.dataset;
+ const {
+ fullPath,
+ hasIssueWeightsFeature,
+ issuesListPath,
+ hasIterationsFeature,
+ hasOkrsFeature,
+ hasIssuableHealthStatusFeature,
+ } = el.dataset;
return new Vue({
el,
@@ -15,9 +22,12 @@ export const initWorkItemsRoot = () => {
apolloProvider,
provide: {
fullPath,
+ projectPath: fullPath,
hasIssueWeightsFeature: parseBoolean(hasIssueWeightsFeature),
+ hasOkrsFeature: parseBoolean(hasOkrsFeature),
issuesListPath,
hasIterationsFeature: parseBoolean(hasIterationsFeature),
+ hasIssuableHealthStatusFeature: parseBoolean(hasIssuableHealthStatusFeature),
},
render(createElement) {
return createElement(App);
diff --git a/app/assets/javascripts/work_items/pages/work_item_root.vue b/app/assets/javascripts/work_items/pages/work_item_root.vue
index 1c00bd16263..d04d4942253 100644
--- a/app/assets/javascripts/work_items/pages/work_item_root.vue
+++ b/app/assets/javascripts/work_items/pages/work_item_root.vue
@@ -70,6 +70,10 @@ export default {
<template>
<div>
<gl-alert v-if="error" variant="danger" @dismiss="error = ''">{{ error }}</gl-alert>
- <work-item-detail :work-item-id="gid" :iid="id" @deleteWorkItem="deleteWorkItem($event)" />
+ <work-item-detail
+ :work-item-id="gid"
+ :work-item-iid="id"
+ @deleteWorkItem="deleteWorkItem($event)"
+ />
</div>
</template>
diff --git a/app/assets/javascripts/work_items/utils.js b/app/assets/javascripts/work_items/utils.js
index 17f9c882c2d..e58fd19ea31 100644
--- a/app/assets/javascripts/work_items/utils.js
+++ b/app/assets/javascripts/work_items/utils.js
@@ -1,6 +1,12 @@
import workItemQuery from './graphql/work_item.query.graphql';
import workItemByIidQuery from './graphql/work_item_by_iid.query.graphql';
+import workItemNotesIdQuery from './graphql/work_item_notes.query.graphql';
+import workItemNotesByIidQuery from './graphql/work_item_notes_by_iid.query.graphql';
export function getWorkItemQuery(isFetchedByIid) {
return isFetchedByIid ? workItemByIidQuery : workItemQuery;
}
+
+export function getWorkItemNotesQuery(isFetchedByIid) {
+ return isFetchedByIid ? workItemNotesByIidQuery : workItemNotesIdQuery;
+}
diff --git a/app/assets/stylesheets/_page_specific_files.scss b/app/assets/stylesheets/_page_specific_files.scss
index 6878e9a10d7..fa5d2bf7972 100644
--- a/app/assets/stylesheets/_page_specific_files.scss
+++ b/app/assets/stylesheets/_page_specific_files.scss
@@ -4,20 +4,16 @@
@import './pages/events';
@import './pages/groups';
@import './pages/hierarchy';
-@import './pages/issuable';
@import './pages/issues';
@import './pages/labels';
@import './pages/login';
@import './pages/ml_experiment_tracking';
@import './pages/merge_requests';
-@import './pages/monitor';
@import './pages/note_form';
@import './pages/notes';
@import './pages/pipelines';
@import './pages/profile';
@import './pages/projects';
@import './pages/registry';
-@import './pages/search';
@import './pages/settings';
@import './pages/storage_quota';
-@import './pages/users';
diff --git a/app/assets/stylesheets/components/content_editor.scss b/app/assets/stylesheets/components/content_editor.scss
index 1b6a0208ca7..44b06c0ff12 100644
--- a/app/assets/stylesheets/components/content_editor.scss
+++ b/app/assets/stylesheets/components/content_editor.scss
@@ -130,6 +130,18 @@
background-color: var(--gl-color-chip-color);
}
+.content-editor-comment {
+ &::before {
+ content: '<!--';
+ }
+
+ &::after {
+ content: '-->';
+ }
+}
+
+
+
.bubble-menu-form {
width: 320px;
}
diff --git a/app/assets/stylesheets/components/ref_selector.scss b/app/assets/stylesheets/components/ref_selector.scss
index ded911c2492..f7a9367499e 100644
--- a/app/assets/stylesheets/components/ref_selector.scss
+++ b/app/assets/stylesheets/components/ref_selector.scss
@@ -6,7 +6,7 @@
width: 20rem;
&,
- .gl-new-dropdown-inner {
+ .gl-dropdown-inner {
max-height: $dropdown-max-height-lg;
}
}
diff --git a/app/assets/stylesheets/fonts.scss b/app/assets/stylesheets/fonts.scss
new file mode 100644
index 00000000000..a6ecca88bd4
--- /dev/null
+++ b/app/assets/stylesheets/fonts.scss
@@ -0,0 +1,32 @@
+/* -------------------------------------------------------
+Inter variable font.
+
+Usage:
+ html { font-family: 'GitLab Sans', sans-serif; }
+*/
+@font-face {
+ font-family: 'GitLab Sans';
+ font-weight: 100 900;
+ font-display: optional;
+ font-style: normal;
+ font-named-instance: 'Regular'; /* stylelint-disable property-no-unknown */
+ src: font-url('gitlab-sans/GitLabSans.woff2') format('woff2');
+}
+
+/* -------------------------------------------------------
+Monospaced font: JetBrains Mono.
+
+Usage:
+ html { font-family: 'JetBrains Mono', sans-serif; }
+*/
+@font-face {
+ font-family: 'JetBrains Mono';
+ font-display: optional;
+ font-style: normal;
+ src: font-url('jetbrains-mono/JetBrainsMono.woff2') format('woff2');
+}
+
+:root {
+ --default-mono-font: 'JetBrains Mono', 'Menlo';
+ --default-regular-font: 'GitLab Sans', -apple-system;
+}
diff --git a/app/assets/stylesheets/framework/common.scss b/app/assets/stylesheets/framework/common.scss
index be8a890320f..14e756a5c21 100644
--- a/app/assets/stylesheets/framework/common.scss
+++ b/app/assets/stylesheets/framework/common.scss
@@ -1,9 +1,9 @@
/** COLORS **/
.cgray { color: $gl-text-color; }
-.clgray { color: $common-gray-light; }
+.clgray { color: $gray-200; }
.cred { color: $red-500; }
.cgreen { color: $green-600; }
-.cdark { color: $common-gray-dark; }
+.cdark { color: $gray-800; }
.fwhite { fill: $white; }
.fgray { fill: $gray-500; }
diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss
index c5a34ca4b31..0acda85f527 100644
--- a/app/assets/stylesheets/framework/dropdowns.scss
+++ b/app/assets/stylesheets/framework/dropdowns.scss
@@ -5,7 +5,7 @@
// for Snippets is introduced and Clone button is relocated, we won't
// need this style.
// Issue for the refactoring: https://gitlab.com/gitlab-org/gitlab/-/issues/213327
- &.gl-new-dropdown button.dropdown-toggle {
+ &.gl-dropdown button.dropdown-toggle {
@include gl-display-inline-flex;
}
@@ -41,7 +41,7 @@
max-height: $extended-max-height;
// See comment below for explanation
- .gl-new-dropdown-inner {
+ .gl-dropdown-inner {
max-height: $extended-max-height - 2px;
}
}
@@ -54,12 +54,12 @@
width: 100%;
}
- // `GlDropdown` specifies the `max-height` of `.gl-new-dropdown-inner`
+ // `GlDropdown` specifies the `max-height` of `.gl-dropdown-inner`
// as `$dropdown-max-height`, but the `max-height` rule above forces
// the parent `.dropdown-menu` to be _slightly_ too small because of
// the 1px borders. The workaround below overrides the @gitlab/ui style
// to avoid a double scroll bar.
- .gl-new-dropdown-inner {
+ .gl-dropdown-inner {
max-height: $dropdown-max-height - 2px;
}
}
@@ -285,7 +285,7 @@
list-style: none;
> a,
- button,
+ > button,
.gl-button.btn-link,
.menu-item {
@include dropdown-link;
@@ -1027,7 +1027,7 @@
// This class won't be needed once we can add a prop for this in the GitLab UI component
// https://gitlab.com/gitlab-org/gitlab-ui/-/issues/966
-.gl-new-dropdown {
+.gl-dropdown {
.gl-dropdown-menu-wide {
width: $gl-dropdown-width-wide;
}
@@ -1035,7 +1035,7 @@
// This class won't be needed once we can add a prop for this in the GitLab UI component
// https://gitlab.com/gitlab-org/gitlab-ui/-/issues/966
-.gl-new-dropdown.gl-dropdown-menu-full-width {
+.gl-dropdown.gl-dropdown-menu-full-width {
.dropdown-menu {
width: 100%;
}
diff --git a/app/assets/stylesheets/framework/emojis.scss b/app/assets/stylesheets/framework/emojis.scss
index 68a3493670d..16ad6f62c64 100644
--- a/app/assets/stylesheets/framework/emojis.scss
+++ b/app/assets/stylesheets/framework/emojis.scss
@@ -36,7 +36,7 @@ gl-emoji {
}
}
-.emoji-picker .gl-new-dropdown .dropdown-menu {
+.emoji-picker .gl-dropdown .dropdown-menu {
width: 350px;
}
@@ -48,6 +48,6 @@ gl-emoji {
border-bottom-color: var(--gl-theme-accent, $theme-indigo-500);
}
-.emoji-picker .gl-new-dropdown-inner > :last-child {
+.emoji-picker .gl-dropdown-inner > :last-child {
padding-bottom: 0;
}
diff --git a/app/assets/stylesheets/framework/filters.scss b/app/assets/stylesheets/framework/filters.scss
index 37b61d36911..b35175f4ef6 100644
--- a/app/assets/stylesheets/framework/filters.scss
+++ b/app/assets/stylesheets/framework/filters.scss
@@ -252,7 +252,7 @@
z-index: 1;
&:hover .clear-search-icon {
- color: $common-gray-dark;
+ color: $gray-800;
}
}
}
@@ -433,8 +433,7 @@
.search-token-target-branch {
.value {
- font-family: $monospace-font;
- font-size: $gl-font-size-monospace;
+ @include gl-font-monospace;
}
}
diff --git a/app/assets/stylesheets/framework/forms.scss b/app/assets/stylesheets/framework/forms.scss
index bba995a6de3..e86edff3f13 100644
--- a/app/assets/stylesheets/framework/forms.scss
+++ b/app/assets/stylesheets/framework/forms.scss
@@ -14,6 +14,28 @@ input[type='text'].danger {
text-shadow: 0 1px 1px $white;
}
+/**
+ * When form input type is number, Firefox & Safari show the up/down arrows
+ * on the right side of the input persistently, while Chrome shows it only
+ * on hover or focus, this fix allows us to hide the arrows in all browsers.
+ * You can conditionally add/remove `hide-spinners` class to have consistent
+ * behaviour across browsers.
+ */
+
+/* stylelint-disable property-no-vendor-prefix */
+input[type='number'].hide-spinners {
+ -moz-appearance: textfield;
+ appearance: textfield;
+
+ &::-webkit-inner-spin-button,
+ &::-webkit-outer-spin-button {
+ -webkit-appearance: none;
+ appearance: none;
+ margin: 0;
+ }
+}
+/* stylelint-enable property-no-vendor-prefix */
+
.datetime-controls {
select {
width: 100px;
@@ -204,6 +226,22 @@ label {
}
}
+.show-password-complexity-errors {
+ .form-control:not(textarea) {
+ height: 34px;
+ }
+
+ .password-complexity-error-outline {
+ border: 1px solid $red-500;
+
+ &:focus {
+ box-shadow: 0 0 0 1px $red-500 inset, 0 1px 1px $gl-field-focus-shadow inset,
+ 0 0 4px 0 $gl-field-focus-shadow-error;
+ border: 0 none;
+ }
+ }
+}
+
.input-icon-wrapper,
.select-wrapper {
position: relative;
diff --git a/app/assets/stylesheets/framework/header.scss b/app/assets/stylesheets/framework/header.scss
index ed41d10f3b2..4b1efcc1e9a 100644
--- a/app/assets/stylesheets/framework/header.scss
+++ b/app/assets/stylesheets/framework/header.scss
@@ -1,3 +1,7 @@
+$search-input-field-min-width: 320px;
+$search-input-field-max-width: 640px;
+$search-input-field-x-min-width: 200px;
+
.navbar-gitlab {
padding: 0 16px;
z-index: $header-zindex;
@@ -76,6 +80,57 @@
.navbar-collapse > ul.nav > li:not(.d-none) {
margin: 0 2px;
}
+
+ .header-search-new {
+ max-width: $search-input-field-max-width;
+ }
+
+ &.header-search-is-active {
+ .global-search-container {
+ flex-grow: 1;
+ }
+ }
+ }
+
+ .header-search {
+ min-width: $search-input-field-min-width;
+
+ // This is a temporary workaround!
+ // the button in GitLab UI Search components need to be updated to not be the small size
+ // see in Figma: https://www.figma.com/file/qEddyqCrI7kPSBjGmwkZzQ/Component-library?node-id=43905%3A45540
+ .gl-search-box-by-type-clear.btn-sm {
+ padding: 0.5rem !important;
+ }
+
+ @include media-breakpoint-between(md, lg) {
+ min-width: $search-input-field-x-min-width;
+ }
+
+ &.is-searching {
+ .in-search-scope-help {
+ position: absolute;
+ top: $gl-spacing-scale-2;
+ right: 2.125rem;
+ z-index: 2;
+ }
+ }
+
+ &.is-not-focused {
+ .gl-search-box-by-type-clear {
+ display: none;
+ }
+ }
+
+ .keyboard-shortcut-helper {
+ transform: translateY(calc(50% - 2px));
+ box-shadow: none;
+ border-color: transparent;
+ }
+ }
+
+ .header-search-dropdown-menu {
+ max-height: $dropdown-max-height;
+ top: 100%;
}
.navbar-collapse {
@@ -555,7 +610,7 @@
}
.top-nav-container-view {
- .gl-new-dropdown & .gl-search-box-by-type {
+ .gl-dropdown & .gl-search-box-by-type {
@include gl-m-0;
}
diff --git a/app/assets/stylesheets/framework/kbd.scss b/app/assets/stylesheets/framework/kbd.scss
index 7dd0ae47834..16e0214c703 100644
--- a/app/assets/stylesheets/framework/kbd.scss
+++ b/app/assets/stylesheets/framework/kbd.scss
@@ -1,10 +1,10 @@
kbd {
display: inline-block;
padding: 3px 5px;
- font-size: $gl-font-size-monospace-sm;
+ @include gl-font-sm;
line-height: 10px;
color: var(--gray-700, $gray-700);
- vertical-align: middle;
+ vertical-align: unset;
background-color: var(--gray-10, $gray-10);
border-width: 1px;
border-style: solid;
diff --git a/app/assets/stylesheets/framework/mixins.scss b/app/assets/stylesheets/framework/mixins.scss
index 47856f1a0d3..628406d5889 100644
--- a/app/assets/stylesheets/framework/mixins.scss
+++ b/app/assets/stylesheets/framework/mixins.scss
@@ -450,7 +450,7 @@
}
@mixin avatar-counter($border-radius: 1em) {
- background-color: $gray-darkest;
+ background-color: $gray-400;
color: $white;
border: 1px solid $gray-normal;
border-radius: $border-radius;
diff --git a/app/assets/stylesheets/framework/sidebar.scss b/app/assets/stylesheets/framework/sidebar.scss
index 7878e08e549..eb34d91476b 100644
--- a/app/assets/stylesheets/framework/sidebar.scss
+++ b/app/assets/stylesheets/framework/sidebar.scss
@@ -198,17 +198,17 @@
}
}
+ $line-height: map-get($spacers, 4) + px-to-rem(2px);
+
&-icon {
/**
* 2px extra is to give a little more height than needed
* to hide timeline line before/after the element starts/ends
*/
- height: map-get($spacers, 4) + px-to-rem(2px);
+ height: $line-height;
z-index: 1;
position: relative;
- top: -3px;
padding: $gl-padding-4 0;
- background-color: $body-bg;
&.opened {
color: $green-500;
@@ -220,7 +220,7 @@
}
&-content {
- line-height: initial;
+ line-height: $line-height;
margin-left: $gl-padding-8;
}
}
@@ -280,3 +280,639 @@
grid-area: user;
}
}
+
+@mixin right-sidebar {
+ position: fixed;
+ top: $header-height;
+ // 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 $gl-transition-duration-medium;
+ background-color: $white;
+ z-index: 200;
+ overflow: hidden;
+
+}
+
+.right-sidebar {
+ &:not(.right-sidebar-merge-requests) {
+ @include right-sidebar;
+ }
+
+ &.right-sidebar-merge-requests {
+ @include media-breakpoint-down(md) {
+ @include right-sidebar;
+ z-index: 251;
+ }
+ }
+
+ @include media-breakpoint-down(sm) {
+ z-index: 251;
+ }
+
+ a:not(.btn) {
+ color: inherit;
+
+ &:hover {
+ color: $blue-800;
+ }
+ }
+
+ .gl-label .gl-label-link:hover {
+ color: inherit;
+ }
+
+ .btn-link {
+ color: inherit;
+ }
+
+ .issuable-header-text {
+ margin-top: 7px;
+ }
+
+ .gutter-toggle {
+ display: flex;
+ align-items: center;
+ margin-left: 20px;
+ padding: 4px;
+ border-radius: 4px;
+ height: 24px;
+
+ &:hover {
+ color: $gl-text-color;
+ background: $gray-50;
+ }
+
+ &:hover,
+ &:focus {
+ text-decoration: none;
+ }
+ }
+
+ &.right-sidebar-merge-requests {
+ .block,
+ .sidebar-contained-width,
+ .issuable-sidebar-header {
+ width: 100%;
+ }
+
+ .block {
+ @include media-breakpoint-up(lg) {
+ padding: $gl-spacing-scale-4 0 $gl-spacing-scale-5;
+ }
+
+ &.participants {
+ border-bottom: 0;
+ }
+ }
+ }
+
+ .block,
+ .sidebar-contained-width,
+ .issuable-sidebar-header {
+ @include clearfix;
+ padding: $gl-spacing-scale-4 0 $gl-spacing-scale-5;
+ border-bottom: 1px solid $border-gray-normal;
+ // This prevents the mess when resizing the sidebar
+ // of elements repositioning themselves..
+ width: $gutter-inner-width;
+ // --
+
+ &:last-child {
+ border: 0;
+ }
+
+ &.assignee {
+ .author-link {
+ display: block;
+ position: relative;
+
+ &:hover {
+ .author {
+ text-decoration: underline;
+ }
+ }
+ }
+ }
+
+ &.time-tracking,
+ &.participants,
+ &.subscriptions,
+ &.with-sub-blocks {
+ padding-top: $gl-spacing-scale-5;
+ }
+ }
+
+ .block-first {
+ padding-top: 0;
+ }
+
+ .title {
+ color: $gl-text-color;
+ line-height: $gl-line-height-20;
+
+ .avatar {
+ margin-left: 0;
+ }
+ }
+
+ .selectbox {
+ display: none;
+
+ &.show {
+ display: block;
+ }
+ }
+
+ .btn-clipboard:hover {
+ color: $gl-text-color;
+ }
+
+ .issuable-sidebar {
+ height: 100%;
+
+ &:not(.is-merge-request) {
+ overflow-y: scroll;
+ overflow-x: hidden;
+ -webkit-overflow-scrolling: touch;
+ }
+
+ &.is-merge-request {
+ @include media-breakpoint-down(sm) {
+ overflow-y: scroll;
+ overflow-x: hidden;
+ -webkit-overflow-scrolling: touch;
+ }
+ }
+ }
+
+ &.right-sidebar-expanded {
+ &:not(.right-sidebar-merge-requests) {
+ width: $gutter-width;
+ }
+
+ .value {
+ line-height: 1;
+ }
+
+ .issuable-sidebar {
+ padding: 0 20px;
+
+ &.is-merge-request {
+ @include media-breakpoint-up(lg) {
+ padding: 0;
+
+ .issuable-context-form {
+ --initial-top: calc(#{$header-height} + 76px);
+ --top: var(--initial-top);
+
+ @include gl-sticky;
+ @include gl-overflow-auto;
+
+ top: var(--top);
+ height: calc(100vh - var(--top));
+ padding: 0 15px;
+ margin-bottom: calc(var(--top) * -1);
+
+ .with-performance-bar & {
+ --top: calc(var(--initial-top) + #{$performance-bar-height});
+ }
+
+ .with-system-header & {
+ --top: calc(var(--initial-top) + #{$system-header-height});
+ }
+
+ .with-performance-bar.with-system-header & {
+ --top: calc(var(--initial-top) + #{$system-header-height} + #{$performance-bar-height});
+ }
+ }
+ }
+ }
+ }
+
+ &:not(.boards-sidebar):not([data-signed-in]):not([data-always-show-toggle]) {
+ .issuable-sidebar-header {
+ display: none;
+ }
+ }
+
+ .light {
+ font-weight: $gl-font-weight-normal;
+ }
+
+ .no-value {
+ color: $gl-text-color-secondary;
+ }
+
+ .sidebar-collapsed-icon {
+ display: none;
+ }
+
+ .gutter-toggle {
+ text-align: center;
+ }
+
+ .title .gutter-toggle {
+ margin-top: 0;
+ }
+
+ .assignee .user-list .avatar {
+ margin: 0;
+ }
+
+ .hide-expanded {
+ display: none;
+ }
+ }
+
+ &.right-sidebar-collapsed {
+ /* Extra small devices (phones, less than 768px) */
+ display: none;
+ /* Small devices (tablets, 768px and up) */
+
+ &:not(.right-sidebar-merge-requests) {
+ @include media-breakpoint-up(sm) {
+ display: block;
+ }
+ }
+
+ &.right-sidebar-merge-requests {
+ @include media-breakpoint-up(lg) {
+ display: block;
+ }
+ }
+
+ width: $gutter-collapsed-width;
+ padding: 0;
+
+ .block,
+ .sidebar-contained-width,
+ .issuable-sidebar-header {
+ width: $gutter-collapsed-width - 2px;
+ padding: 0;
+ border-bottom: 0;
+ overflow: hidden;
+ }
+
+ .block,
+ .gutter-toggle,
+ .sidebar-collapsed-container {
+ &.with-sub-blocks .sub-block:hover,
+ &:not(.with-sub-blocks):hover {
+ background-color: $gray-100;
+ }
+ }
+
+ .participants {
+ border-bottom: 1px solid $border-gray-normal;
+ }
+
+ .hide-collapsed {
+ display: none;
+ }
+
+ .gutter-toggle {
+ width: 100%;
+ height: $sidebar-toggle-height;
+ margin-left: 0;
+ border-bottom: 1px solid $border-white-normal;
+ border-radius: 0;
+ }
+
+ a.gutter-toggle {
+ display: flex;
+ justify-content: center;
+ flex-direction: column;
+ text-align: center;
+ }
+
+ .merge-icon {
+ height: 12px;
+ width: 12px;
+ bottom: -5px;
+ right: 4px;
+ }
+
+ .sidebar-collapsed-icon {
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ align-items: center;
+ width: 100%;
+ height: $sidebar-toggle-height;
+ text-align: center;
+ color: $gl-text-color-secondary;
+
+ > svg {
+ fill: $gl-text-color-secondary;
+ }
+
+ &:hover:not(.disabled),
+ &:hover .todo-undone {
+ color: $gl-text-color;
+
+ > svg {
+ fill: $gl-text-color;
+ }
+ }
+
+ .todo-undone {
+ color: $blue-600;
+ fill: $blue-600;
+ }
+
+ .author {
+ display: none;
+ }
+
+ .btn-clipboard {
+ /*
+ This change should be temporary, because the DOM currently gets
+ generated from a ruby definition in `app/helpers/button_helper.rb`.
+ As soon as the `copy to clipboard` button will be transferred to
+ Vue this should be adjusted as well.
+ */
+ flex: 1;
+ align-self: stretch;
+ padding: 0;
+
+ border: 0;
+ background: transparent;
+ color: $gl-text-color-secondary;
+
+ &:hover {
+ color: $gl-text-color;
+ }
+ }
+
+ &.multiple-users {
+ display: flex;
+ justify-content: center;
+ }
+ }
+
+ .sidebar-avatar-counter {
+ width: 24px;
+ height: 24px;
+ border-radius: 12px;
+
+ ~.merge-icon {
+ bottom: 0;
+ }
+ }
+
+ .sidebar-collapsed-user {
+ padding-bottom: 0;
+
+ .author-link {
+ padding-left: 0;
+
+ .avatar {
+ position: static;
+ margin: 0;
+ }
+ }
+ }
+
+ .issuable-header-btn {
+ display: none;
+ }
+
+ .multiple-users {
+ .btn-link {
+ padding: 0;
+ border: 0;
+
+ .avatar {
+ margin: 0;
+ }
+ }
+
+ .btn-link:first-child {
+ position: absolute;
+ left: 10px;
+ z-index: 1;
+ }
+
+ .btn-link:last-child {
+ position: absolute;
+ right: 10px;
+
+ &:hover {
+ text-decoration: none;
+ }
+ }
+ }
+
+ .milestone-title span,
+ .collapse-truncated-title {
+ @include str-truncated(100%);
+ display: block;
+ margin: 0 4px;
+ }
+ }
+
+ .dropdown-menu-toggle {
+ width: 100%;
+ padding-top: 6px;
+ }
+
+ .dropdown-menu {
+ width: 100%;
+
+ /*
+ * Overwrite hover style for dropdown items, so that they are not blue
+ * This should be removed during dev of https://gitlab.com/gitlab-org/gitlab-foss/issues/44040
+ */
+ li a {
+ &:hover,
+ &:active,
+ &:focus,
+ &.is-focused {
+ @include dropdown-item-hover;
+ }
+ }
+
+ }
+}
+
+.with-performance-bar .right-sidebar {
+ top: calc(#{$header-height} + #{$performance-bar-height});
+}
+
+.sidebar-move-issue-confirmation-button {
+ width: 100%;
+
+ &.is-loading {
+ .sidebar-move-issue-confirmation-loading-icon {
+ display: inline-block;
+ }
+ }
+}
+
+.sidebar-move-issue-confirmation-loading-icon {
+ display: none;
+}
+
+.issuable-show-labels {
+ .gl-label {
+ margin-bottom: 5px;
+ margin-right: 5px;
+ }
+
+ a {
+ display: inline-block;
+
+ .color-label {
+ padding: 4px $grid-size;
+ border-radius: $label-border-radius;
+ margin-right: 4px;
+ margin-bottom: 4px;
+ }
+
+ &:hover .color-label {
+ text-decoration: underline;
+ }
+ }
+
+ &.has-labels {
+ // this font size is a fix to
+ // prevent unintended spacing between labels
+ // which shows up when rendering markup has white-space
+ // characters present.
+ // see: https://css-tricks.com/fighting-the-space-between-inline-block-elements/#article-header-id-3
+ font-size: 0;
+ margin-bottom: -5px;
+ }
+}
+
+.assignee,
+.reviewer {
+ .merge-icon {
+ color: $orange-400;
+ position: absolute;
+ bottom: -3px;
+ right: -3px;
+ filter: drop-shadow(0 0 0.5px $white) drop-shadow(0 0 1px $white) drop-shadow(0 0 2px $white);
+ }
+}
+
+.participants-author {
+ &:nth-of-type(8n) {
+ padding-right: 0;
+ }
+
+ .avatar.avatar-inline {
+ margin: 0;
+ }
+}
+
+.participants-more,
+.user-list-more {
+ margin-left: 5px;
+
+ a,
+ .btn-link {
+ color: $gl-text-color-secondary;
+ }
+
+ .btn-link {
+ padding: 0;
+ }
+
+ .btn-link:hover {
+ color: $blue-800;
+ text-decoration: none;
+ }
+
+ .btn-link:focus {
+ text-decoration: none;
+ }
+}
+
+.sidebar-help-wrap {
+ .sidebar-help-state {
+ margin: 16px -20px -20px;
+ padding: 16px 20px;
+ }
+
+ .help-state-toggle-enter-active {
+ transition: all 0.8s ease;
+ }
+
+ .help-state-toggle-leave-active {
+ transition: all 0.5s ease;
+ }
+
+ .help-state-toggle-enter,
+ .help-state-toggle-leave-active {
+ opacity: 0;
+ }
+}
+
+.time-tracker {
+ .sidebar-collapsed-icon {
+ > .stopwatch-svg {
+ display: inline-block;
+ }
+
+ svg {
+ width: 16px;
+ height: 16px;
+ fill: $gl-text-color-secondary;
+ }
+
+ &:hover svg {
+ fill: $gl-text-color;
+ }
+ }
+
+ .compare-meter {
+ &.over_estimate {
+ .time-remaining,
+ .compare-value.spent {
+ color: $red-500;
+ }
+ }
+ }
+
+ .compare-display-container {
+ font-size: 13px;
+ }
+}
+
+/*
+ * Following overrides are done to prevent
+ * legacy dropdown styles from influencing
+ * GitLab UI components used within GlDropdown
+ */
+.right-sidebar-collapsed {
+ .sidebar-grouped-item {
+ .sidebar-collapsed-icon {
+ margin-bottom: 0;
+ }
+
+ .sidebar-collapsed-divider {
+ line-height: 5px;
+ font-size: 12px;
+ color: $gray-500;
+
+ + .sidebar-collapsed-icon {
+ padding-top: 0;
+ }
+ }
+ }
+}
+
+@include media-breakpoint-down(sm) {
+ // Overriding the following rule with the negative margin
+ // https://gitlab.com/gitlab-org/gitlab/-/blob/146c43c931c3743a140529307aea616e4aa9ff21/app/assets/stylesheets/framework/sidebar.scss#L1-5
+ .container-fluid {
+ .issuable-list,
+ .issues-filters,
+ .epics-filters {
+ margin: 0 (-$gl-padding);
+ }
+ }
+}
diff --git a/app/assets/stylesheets/framework/typography.scss b/app/assets/stylesheets/framework/typography.scss
index 2c2d8a2b592..0a475845fd3 100644
--- a/app/assets/stylesheets/framework/typography.scss
+++ b/app/assets/stylesheets/framework/typography.scss
@@ -81,6 +81,7 @@
word-wrap: break-word;
overflow-wrap: break-word;
word-break: keep-all;
+ @include gl-font-base;
}
h1 {
@@ -322,7 +323,6 @@
pre {
margin-bottom: 16px;
- font-size: 13px;
line-height: 1.6em;
overflow-x: auto;
border-radius: $border-radius-default;
@@ -587,7 +587,7 @@
}
}
- .gl-new-dropdown-item {
+ .gl-dropdown-item {
margin: 0;
padding: 0;
line-height: 1rem;
@@ -658,7 +658,7 @@ pre {
display: block;
padding: $gl-padding-8 $input-horizontal-padding;
margin: 0 0 $gl-padding-8;
- font-size: $gl-font-size-monospace;
+ @include gl-font-base;
word-break: break-all;
word-wrap: break-word;
color: $gl-text-color;
@@ -680,7 +680,7 @@ code {
}
.monospace {
- font-family: $monospace-font;
+ @include gl-font-monospace;
}
.weight-normal {
@@ -706,7 +706,7 @@ code {
*/
textarea.js-gfm-input {
font-family: $monospace-font;
- font-size: $gl-font-size-monospace;
+ @include gl-font-base;
}
h1,
@@ -772,3 +772,8 @@ textarea {
wbr {
display: inline-block;
}
+
+// The font used in Monaco editor - Web IDE, Snippets, single file editor
+:root {
+ --code-editor-font: #{$monospace-font};
+}
diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss
index 99284ea0a64..ec8ffaf8c53 100644
--- a/app/assets/stylesheets/framework/variables.scss
+++ b/app/assets/stylesheets/framework/variables.scss
@@ -83,18 +83,9 @@ $darken-dark-factor: 10% !default;
$darken-border-factor: 5% !default;
$darken-border-dashed-factor: 25% !default;
-$white: #fff !default;
-$white-normal: #f0f0f0 !default;
-$white-dark: #eaeaea !default;
-$white-transparent: rgba($white, 0.8) !default;
-
$purple: #6d49cb !default;
$purple-light: #ede8fb !default;
-$black: #000 !default;
-$black-transparent: rgba(0, 0, 0, 0.3) !default;
-$almost-black: #242424 !default;
-
$green-50: #ecf4ee !default;
$green-100: #c3e6cd !default;
$green-200: #91d4a8 !default;
@@ -183,6 +174,15 @@ $t-gray-a-08: rgba($gray-950, 0.08) !default;
$t-gray-a-16: rgba($gray-950, 0.16) !default;
$t-gray-a-24: rgba($gray-950, 0.24) !default;
+$white: #fff !default;
+$white-normal: $gray-50 !default;
+$white-dark: darken($gray-50, 2) !default;
+$white-transparent: rgba($white, 0.8) !default;
+
+$black: #000 !default;
+$black-transparent: $t-gray-a-24 !default;
+$almost-black: $gray-950 !default;
+
$greens: (
'50': $green-50,
'100': $green-100,
@@ -350,17 +350,17 @@ $theme-light-red-700: #a62e21;
// Data visualization color palette
-$data-viz-blue-50: #e9ebff;
-$data-viz-blue-100: #d4dcfa;
-$data-viz-blue-200: #b7c6ff;
-$data-viz-blue-300: #97acff;
-$data-viz-blue-400: #748eff;
-$data-viz-blue-500: #5772ff;
-$data-viz-blue-600: #445cf2;
-$data-viz-blue-700: #3547de;
-$data-viz-blue-800: #232fcf;
-$data-viz-blue-900: #1e23a8;
-$data-viz-blue-950: #11118a;
+$data-viz-blue-50: #e9ebff !default;
+$data-viz-blue-100: #d2dcff !default;
+$data-viz-blue-200: #b7c6ff !default;
+$data-viz-blue-300: #97acff !default;
+$data-viz-blue-400: #7992f5 !default;
+$data-viz-blue-500: #617ae2 !default;
+$data-viz-blue-600: #4e65cd !default;
+$data-viz-blue-700: #3f51ae !default;
+$data-viz-blue-800: #374291 !default;
+$data-viz-blue-900: #303470 !default;
+$data-viz-blue-950: #2a2b59 !default;
$border-white-light: darken($white, $darken-border-factor) !default;
$border-white-normal: darken($white-normal, $darken-border-factor) !default;
@@ -380,7 +380,7 @@ $well-expand-item: #e8f2f7 !default;
$well-inner-border: #eef0f2 !default;
$well-light-border: #f1f1f1;
$well-light-text-color: #5b6169;
-$nav-active-bg: rgba($black, 0.08);
+$nav-active-bg: $t-gray-a-08;
/*
* Text
@@ -555,11 +555,13 @@ $diff-jagged-border-gradient-color: darken($white-normal, 8%);
/*
* Fonts
*/
-$monospace-font: 'Menlo', 'DejaVu Sans Mono', 'Liberation Mono', 'Consolas', 'Ubuntu Mono',
- 'Courier New', 'andale mono', 'lucida console', monospace;
-$regular-font: -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';
+$monospace-font: var(--default-mono-font, 'Menlo'), 'DejaVu Sans Mono', 'Liberation Mono', 'Consolas',
+ 'Ubuntu Mono', 'Courier New', 'andale mono', 'lucida console', monospace;
+$regular-font: var(--default-regular-font, -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';
+$gl-monospace-font: $monospace-font;
+$gl-regular-font: $regular-font;
/*
* Dropdowns
@@ -730,12 +732,6 @@ $commit-message-text-area-bg: rgba(0, 0, 0, 0);
$commit-stat-summary-height: 36px;
/*
-* Common
-*/
-$common-gray-light: #bbb;
-$common-gray-dark: #444;
-
-/*
* Files
*/
$logs-li-color: #888;
@@ -784,16 +780,6 @@ $fade-mask-transition-curve: ease-in-out;
$login-brand-holder-color: #888;
/*
-* Projects
-*/
-$project-option-descr-color: #54565b;
-
-/*
- * Monitor Charts
- */
-$chart-tooltip-max-width: 512px;
-
-/*
Stat Graph
*/
$stat-graph-common-bg: #f3f3f3;
@@ -822,7 +808,6 @@ Pipeline Graph
$ci-action-icon-size: 22px;
$ci-action-icon-size-lg: 24px;
$pipeline-dropdown-line-height: 20px;
-$pipeline-dropdown-status-icon-size: 18px;
$ci-action-dropdown-button-size: 24px;
$ci-action-dropdown-svg-size: 12px;
diff --git a/app/assets/stylesheets/page_bundles/_pipeline_mixins.scss b/app/assets/stylesheets/page_bundles/_pipeline_mixins.scss
index 613d27a2f39..ed15e352b7d 100644
--- a/app/assets/stylesheets/page_bundles/_pipeline_mixins.scss
+++ b/app/assets/stylesheets/page_bundles/_pipeline_mixins.scss
@@ -33,8 +33,8 @@
.ci-action-icon-container {
position: absolute;
- right: 8px;
- top: 8px;
+ right: 11px;
+ top: 7px;
&.ci-action-icon-wrapper {
height: $ci-action-dropdown-button-size;
@@ -84,25 +84,6 @@
&.non-details-job-component {
padding: $gl-padding-8 $gl-btn-horz-padding;
}
-
- .ci-job-name-component {
- align-items: center;
- display: flex;
- flex: 1;
- }
-
- .ci-status-icon {
- position: relative;
-
- > svg {
- width: $pipeline-dropdown-status-icon-size;
- height: $pipeline-dropdown-status-icon-size;
- margin: 3px 0;
- position: relative;
- overflow: visible;
- display: block;
- }
- }
}
// ensure .mini-pipeline-graph-dropdown-item has hover style when action-icon is hovered
diff --git a/app/assets/stylesheets/page_bundles/alert_management_details.scss b/app/assets/stylesheets/page_bundles/alert_management_details.scss
index 2eaf4517710..d67dadafa9e 100644
--- a/app/assets/stylesheets/page_bundles/alert_management_details.scss
+++ b/app/assets/stylesheets/page_bundles/alert_management_details.scss
@@ -28,7 +28,7 @@
@include gl-pt-8;
}
- .gl-new-dropdown-item-text-wrapper {
+ .gl-dropdown-item-text-wrapper {
@include gl-py-0;
}
}
diff --git a/app/assets/stylesheets/page_bundles/boards.scss b/app/assets/stylesheets/page_bundles/boards.scss
index 0cc1fb40e4a..bdbcf7ab58c 100644
--- a/app/assets/stylesheets/page_bundles/boards.scss
+++ b/app/assets/stylesheets/page_bundles/boards.scss
@@ -185,13 +185,15 @@
}
.issue-boards-content.is-focused {
+ $focus-mode-z-index: 9000;
+
position: fixed;
width: 100%;
height: 100%;
top: 0;
left: 0;
background: var(--white, $white);
- z-index: 9000;
+ z-index: $focus-mode-z-index;
@include media-breakpoint-down(sm) {
padding-top: 10px;
@@ -201,13 +203,24 @@
height: calc(100vh - #{$issue-boards-filter-height});
}
- .boards-sidebar {
- height: 100%;
- top: 0;
+ // Use !important for these as top and z-index are set on style attribute
+ // in gitlab-ui; https://gitlab.com/gitlab-org/gitlab-ui/-/issues/1805
+ ~ #js-right-sidebar-portal .boards-sidebar {
+ top: 0 !important;
+ z-index: calc(#{$focus-mode-z-index} + 1) !important;
}
}
.boards-sidebar {
+ top: $header-height !important;
+ height: auto;
+ bottom: 0;
+ padding-bottom: 0.5rem;
+
+ .with-performance-bar & {
+ top: calc(#{$header-height} + #{$performance-bar-height}) !important;
+ }
+
.sidebar-collapsed-icon {
@include gl-display-none;
}
diff --git a/app/assets/stylesheets/page_bundles/clusters.scss b/app/assets/stylesheets/page_bundles/clusters.scss
index 4f29ff4b1ad..4d75159e87a 100644
--- a/app/assets/stylesheets/page_bundles/clusters.scss
+++ b/app/assets/stylesheets/page_bundles/clusters.scss
@@ -6,7 +6,7 @@
@include gl-w-full;
order: -1;
- .gl-new-dropdown,
+ .gl-dropdown,
.split-content-button {
@include gl-w-full;
}
@@ -24,3 +24,9 @@
.cluster-button-container:focus-within {
@include gl-focus;
}
+
+.select-agent-dropdown {
+ .gl-button-text {
+ @include gl-flex-grow-1;
+ }
+}
diff --git a/app/assets/stylesheets/page_bundles/ide.scss b/app/assets/stylesheets/page_bundles/ide.scss
index ec75c53d026..c3688f4a138 100644
--- a/app/assets/stylesheets/page_bundles/ide.scss
+++ b/app/assets/stylesheets/page_bundles/ide.scss
@@ -27,13 +27,6 @@ $ide-commit-header-height: 48px;
@include str-truncated(250px);
}
-.ide-layout {
- // Fix for iOS 13+, the height of the page is actually less than
- // 100vh because of the presence of the bottom bar
- max-height: 100%;
- position: fixed;
-}
-
.ide-view {
position: relative;
margin-top: 0;
@@ -522,13 +515,6 @@ $ide-commit-header-height: 48px;
}
}
-.ide-loading {
- display: flex;
- height: 100%;
- align-items: center;
- justify-content: center;
-}
-
.ide-empty-state {
display: flex;
height: 100vh;
diff --git a/app/assets/stylesheets/page_bundles/incidents.scss b/app/assets/stylesheets/page_bundles/incidents.scss
index de246fa14b9..e807c4c0bbb 100644
--- a/app/assets/stylesheets/page_bundles/incidents.scss
+++ b/app/assets/stylesheets/page_bundles/incidents.scss
@@ -4,13 +4,10 @@
.main-notes-list::before {
content: none;
}
+}
- .timeline-event-note {
- p {
- margin-bottom: 0;
- font-size: 0.875rem;
- }
- }
+.timeline-event {
+ grid-template-columns: #{$gl-spacing-scale-9} minmax(0, 1fr) #{$gl-spacing-scale-7};
}
/**
diff --git a/app/assets/stylesheets/page_bundles/issuable.scss b/app/assets/stylesheets/page_bundles/issuable.scss
new file mode 100644
index 00000000000..f364170c99f
--- /dev/null
+++ b/app/assets/stylesheets/page_bundles/issuable.scss
@@ -0,0 +1,183 @@
+@import 'mixins_and_variables_and_functions';
+
+.status-box {
+ padding: 0 $gl-btn-padding;
+ border-radius: $border-radius-default;
+ display: block;
+ float: left;
+ margin-right: $gl-padding-8;
+ color: var(--white, $white);
+ font-size: $gl-font-size;
+ line-height: $gl-line-height-24;
+}
+
+.issuable-warning-icon {
+ background-color: var(--orange-50, $orange-50);
+ border-radius: $border-radius-default;
+ color: var(--orange-600, $orange-600);
+ width: $issuable-warning-size;
+ height: $issuable-warning-size;
+ text-align: center;
+ margin-right: $issuable-warning-icon-margin;
+ line-height: $gl-line-height-24;
+ flex: 0 0 auto;
+}
+
+.limit-container-width {
+ .flash-container,
+ .detail-page-header,
+ .page-content-header,
+ .commit-box,
+ .info-well,
+ .commit-ci-menu,
+ .files-changed-inner,
+ .limited-header-width,
+ .limited-width-notes {
+ @include fixed-width-container;
+ }
+
+ .issuable-details {
+ .detail-page-description,
+ .mr-source-target,
+ .mr-state-widget,
+ .merge-manually {
+ @include fixed-width-container;
+ }
+ }
+
+ .merge-request-details {
+ .emoji-list-container {
+ @include fixed-width-container;
+ }
+ }
+}
+
+.issuable-details {
+ section {
+ .issuable-discussion {
+ margin-right: 1px;
+ }
+ }
+
+ .title-container {
+ display: flex;
+ align-items: flex-start;
+ }
+
+ .title {
+ padding: 0;
+ margin-bottom: $gl-padding;
+ border-bottom: 0;
+ word-wrap: break-word;
+ overflow-wrap: break-word;
+ min-width: 0;
+ width: 100%;
+ text-align: initial;
+ }
+
+ .btn-edit {
+ margin-left: auto;
+ }
+}
+
+.detail-page-description {
+ padding: 16px 0;
+
+ small {
+ color: var(--gray-500, $gray-500);
+ }
+}
+
+.edited-text {
+ color: var(--gray-500, $gray-500);
+ display: block;
+ margin: 16px 0 0;
+ font-size: 85%;
+
+ .author-link {
+ color: var(--gray-500, $gray-500);
+ }
+}
+
+.user-item {
+ padding: 5px;
+ flex-basis: 20%;
+
+ .user-link {
+ display: inline-block;
+ }
+}
+
+.issuable-gutter-toggle {
+ @include media-breakpoint-down(sm) {
+ margin-left: $btn-side-margin;
+ }
+}
+
+.issuable-meta {
+ flex: 1;
+ display: inline-block;
+ font-size: 14px;
+ align-self: center;
+ overflow: hidden;
+ text-overflow: ellipsis;
+
+ .user-status-emoji {
+ margin-left: $gl-padding-4;
+ margin-right: 0;
+ }
+}
+
+.js-issuable-selector-wrap {
+ .js-issuable-selector {
+ width: 100%;
+ }
+
+ @include media-breakpoint-down(sm) {
+ margin-bottom: $gl-padding;
+ }
+}
+
+.add-issuable-form-input-wrapper {
+ &.focus {
+ border-color: var(--gray-700, $gray-700);
+ @include gl-focus;
+
+ input {
+ @include gl-shadow-none;
+ }
+ }
+
+ .gl-show-field-errors &.form-control:not(textarea) {
+ height: auto;
+ }
+}
+
+/*
+ * Following overrides are done to prevent
+ * legacy dropdown styles from influencing
+ * GitLab UI components used within GlDropdown
+ */
+.issuable-move-dropdown {
+ .b-dropdown-form {
+ @include gl-p-0;
+ }
+
+ .gl-search-box-by-type button.gl-clear-icon-button:hover {
+ @include gl-bg-transparent;
+ }
+
+ .issuable-move-button:not(.disabled):hover {
+ @include gl-text-white;
+ }
+}
+
+.suggestion-footer {
+ font-size: 12px;
+ line-height: 15px;
+
+ .avatar {
+ margin-top: -3px;
+ border: 0;
+ }
+}
diff --git a/app/assets/stylesheets/page_bundles/issuable_list.scss b/app/assets/stylesheets/page_bundles/issuable_list.scss
new file mode 100644
index 00000000000..b08e129a805
--- /dev/null
+++ b/app/assets/stylesheets/page_bundles/issuable_list.scss
@@ -0,0 +1,96 @@
+@import 'mixins_and_variables_and_functions';
+
+.issuable-list {
+ li {
+ .issuable-info-container {
+ flex: 1;
+ display: flex;
+ }
+
+ .issuable-main-info {
+ flex: 1 auto;
+ margin-right: 10px;
+ min-width: 0;
+
+ .issue-weight-icon,
+ .issue-estimate-icon {
+ vertical-align: sub;
+ }
+ }
+
+ .issuable-meta {
+ display: flex;
+ flex-direction: column;
+ align-items: flex-end;
+ flex: 1 0 auto;
+
+ .controls {
+ margin-bottom: 2px;
+ line-height: 20px;
+ padding: 0;
+ }
+ }
+
+ @include media-breakpoint-down(xs) {
+ .issuable-meta {
+ .controls li {
+ margin-right: 0;
+ }
+ }
+ }
+
+ .issue-check {
+ min-width: 15px;
+ }
+
+ .issuable-milestone,
+ .issuable-info,
+ .task-status,
+ .issuable-timestamp {
+ font-weight: $gl-font-weight-normal;
+ color: var(--gray-500, $gl-text-color-secondary);
+
+ a {
+ color: var(--gl-text-color, $gl-text-color);
+ }
+
+ .gl-label-link {
+ color: inherit;
+
+ &:hover {
+ text-decoration: none;
+
+ .gl-label-text:last-of-type {
+ text-decoration: underline;
+ }
+ }
+ }
+
+ .milestone {
+ color: var(--gray-700, $gray-700);
+ }
+ }
+
+ @media(max-width: map-get($grid-breakpoints, lg)-1) {
+ .task-status,
+ .issuable-due-date,
+ .issuable-weight,
+ .project-ref-path {
+ display: none;
+ }
+ }
+ }
+}
+
+.issuable-list li,
+.issuable-info-container .controls {
+ .avatar-counter {
+ display: inline-block;
+ vertical-align: middle;
+ min-width: 16px;
+ line-height: 14px;
+ height: 16px;
+ padding-left: 2px;
+ padding-right: 2px;
+ }
+}
diff --git a/app/assets/stylesheets/page_bundles/merge_requests.scss b/app/assets/stylesheets/page_bundles/merge_requests.scss
index 771428b49e0..4950561bcb7 100644
--- a/app/assets/stylesheets/page_bundles/merge_requests.scss
+++ b/app/assets/stylesheets/page_bundles/merge_requests.scss
@@ -329,6 +329,8 @@ $tabs-holder-z-index: 250;
top: 0;
// !important is required to override inline styles of resizable sidebar
width: 100% !important;
+ // avoid sticky elements overlap header and other elements
+ z-index: 1;
}
.tree-list-holder {
@@ -339,11 +341,13 @@ $tabs-holder-z-index: 250;
}
.ci-widget-container {
+ align-items: center;
justify-content: space-between;
flex: 1;
flex-direction: row;
@include media-breakpoint-down(sm) {
+ align-items: initial;
flex-direction: column;
.dropdown .mini-pipeline-graph-dropdown-menu.dropdown-menu {
@@ -632,13 +636,6 @@ $tabs-holder-z-index: 250;
margin: 3px 0;
}
- .ci-status-icon svg {
- margin: 3px 0;
- position: relative;
- overflow: visible;
- display: block;
- }
-
.normal {
flex: 1;
flex-basis: auto;
@@ -673,10 +670,6 @@ $tabs-holder-z-index: 250;
}
.mr-widget-body {
- &:not(.mr-widget-body-line-height-1) {
- line-height: 24px;
- }
-
@include clearfix;
.approve-btn {
@@ -1003,7 +996,7 @@ $tabs-holder-z-index: 250;
max-width: 650px;
max-height: calc(100vh - 50px);
- .gl-new-dropdown-inner {
+ .gl-dropdown-inner {
max-height: none !important;
}
@@ -1038,7 +1031,7 @@ $tabs-holder-z-index: 250;
}
}
- .gl-new-dropdown-contents {
+ .gl-dropdown-contents {
padding: $gl-spacing-scale-4 !important;
}
@@ -1048,20 +1041,28 @@ $tabs-holder-z-index: 250;
}
.mr-widget-merge-details {
+ *,
+ & {
+ @include gl-font-sm;
+ }
+
+ p {
+ @include gl-font-base;
+ }
+
li:not(:last-child) {
- @include gl-mb-3;
+ @include gl-mb-2;
}
}
-.mr-ready-merge-related-links,
-.mr-widget-merge-details {
- a {
- @include gl-text-decoration-underline;
+.mr-ready-merge-related-links a,
+.mr-widget-merge-details a,
+.mr-widget-author {
+ @include gl-text-decoration-underline;
- &:hover,
- &:focus {
- @include gl-text-decoration-none;
- }
+ &:hover,
+ &:focus {
+ @include gl-text-decoration-none;
}
}
@@ -1075,36 +1076,20 @@ $tabs-holder-z-index: 250;
}
}
-.detail-page-header-actions {
- .gl-toggle {
- @include gl-ml-auto;
- @include gl-rounded-pill;
- @include gl-w-9;
-
- &.is-checked:hover {
- background-color: $blue-500;
- }
- }
-}
-
.page-with-icon-sidebar .issue-sticky-header {
--width: calc(100% - #{$contextual-sidebar-collapsed-width});
}
.merge-request-notification-toggle {
+ .gl-toggle {
+ @include gl-ml-auto;
+ }
+
.gl-toggle-label {
@include gl-font-weight-normal;
}
}
-.dropdown-menu li button.gl-toggle:not(.is-checked) {
- background: $gray-400;
-}
-
-.mr-widget-content-row:first-child {
- border-top: 0;
-}
-
.mr-widget-status-icon-level-1::before {
@include gl-content-empty;
@include gl-absolute;
diff --git a/app/assets/stylesheets/page_bundles/milestone.scss b/app/assets/stylesheets/page_bundles/milestone.scss
index 63bcb83e747..9ee6d17cb50 100644
--- a/app/assets/stylesheets/page_bundles/milestone.scss
+++ b/app/assets/stylesheets/page_bundles/milestone.scss
@@ -1,17 +1,7 @@
@import 'page_bundles/mixins_and_variables_and_functions';
-$status-box-line-height: 26px;
-
-.issues-sortable-list .str-truncated {
- max-width: 90%;
-}
-
.milestones {
.milestone {
- h4 {
- font-weight: $gl-font-weight-bold;
- }
-
.progress {
width: 100%;
height: 6px;
diff --git a/app/assets/stylesheets/page_bundles/oncall_schedules.scss b/app/assets/stylesheets/page_bundles/oncall_schedules.scss
index 91fd2d42657..b995724ec7c 100644
--- a/app/assets/stylesheets/page_bundles/oncall_schedules.scss
+++ b/app/assets/stylesheets/page_bundles/oncall_schedules.scss
@@ -9,7 +9,7 @@
@include gl-w-full;
}
- .gl-new-dropdown-item-text-primary {
+ .gl-dropdown-item-text-primary {
@include gl-overflow-hidden;
@include gl-text-overflow-ellipsis;
}
diff --git a/app/assets/stylesheets/page_bundles/pipelines.scss b/app/assets/stylesheets/page_bundles/pipelines.scss
index 4946bbbebe5..f9c49b0e6ca 100644
--- a/app/assets/stylesheets/page_bundles/pipelines.scss
+++ b/app/assets/stylesheets/page_bundles/pipelines.scss
@@ -70,33 +70,20 @@
}
}
-// Mini Pipelines
-
-.stage-cell {
- .stage-container {
- &:last-child {
- margin-right: 0;
- }
-
- // Hack to show a button tooltip inline
- button.has-tooltip + .tooltip {
- min-width: 105px;
- }
-
- // Bootstrap way of showing the content inline for anchors.
- a.has-tooltip {
- white-space: nowrap;
- }
+// Pipeline mini graph
+.pipeline-mini-graph-stage-container {
+ &:last-child {
+ margin-right: 0;
+ }
- &:not(:last-child) {
- &::after {
- content: '';
- border-bottom: 2px solid $gray-200;
- position: absolute;
- right: -4px;
- top: 11px;
- width: 4px;
- }
+ &:not(:last-child) {
+ &::after {
+ content: '';
+ border-bottom: 2px solid $gray-200;
+ position: absolute;
+ right: -4px;
+ top: 11px;
+ width: 4px;
}
}
}
diff --git a/app/assets/stylesheets/pages/search.scss b/app/assets/stylesheets/page_bundles/search.scss
index 1bca04e5eb1..10da541ed8d 100644
--- a/app/assets/stylesheets/pages/search.scss
+++ b/app/assets/stylesheets/page_bundles/search.scss
@@ -1,17 +1,16 @@
+@import 'mixins_and_variables_and_functions';
+
$search-dropdown-max-height: 400px;
$search-avatar-size: 16px;
$search-sidebar-min-width: 240px;
$search-sidebar-max-width: 300px;
-$search-input-field-x-min-width: 200px;
-$search-input-field-min-width: 320px;
-$search-input-field-max-width: 640px;
$search-keyboard-shortcut: '/';
$border-radius-medium: 3px;
.search-results {
.search-result-row {
- border-bottom: 1px solid $border-color;
+ border-bottom: 1px solid var(--border-color, $border-color);
padding-bottom: $gl-padding;
margin-bottom: $gl-padding;
@@ -28,74 +27,6 @@ $border-radius-medium: 3px;
}
}
-.search form:hover,
-.file-finder-input:hover,
-.issuable-search-form:hover,
-.search-text-input:hover,
-.form-control:hover,
-:not[readonly] {
- border-color: lighten($blue-300, 20%);
- box-shadow: 0 0 4px lighten($dropdown-input-focus-shadow, 20%);
-}
-
-input[type='checkbox']:hover {
- box-shadow: 0 0 2px 2px lighten($dropdown-input-focus-shadow, 20%),
- 0 0 0 1px lighten($dropdown-input-focus-shadow, 20%);
-}
-
-.header-content {
- .header-search-new {
- max-width: $search-input-field-max-width;
- }
-
- &.header-search-is-active {
- .global-search-container {
- flex-grow: 1;
- }
- }
-}
-
-.header-search {
- min-width: $search-input-field-min-width;
-
- // This is a temporary workaround!
- // the button in GitLab UI Search components need to be updated to not be the small size
- // see in Figma: https://www.figma.com/file/qEddyqCrI7kPSBjGmwkZzQ/Component-library?node-id=43905%3A45540
- .gl-search-box-by-type-clear.btn-sm {
- padding: 0.5rem !important;
- }
-
- @include media-breakpoint-between(md, lg) {
- min-width: $search-input-field-x-min-width;
- }
-
- &.is-searching {
- .in-search-scope-help {
- position: absolute;
- top: $gl-spacing-scale-2;
- right: 2.125rem;
- z-index: 2;
- }
- }
-
- &.is-not-focused {
- .gl-search-box-by-type-clear {
- display: none;
- }
- }
-
- .keyboard-shortcut-helper {
- transform: translateY(calc(50% - 2px));
- box-shadow: none;
- border-color: transparent;
- }
-}
-
-.header-search-dropdown-menu {
- max-height: $dropdown-max-height;
- top: 100%;
-}
-
.search {
margin: 0 8px;
@@ -171,7 +102,7 @@ input[type='checkbox']:hover {
.dropdown-header {
// Necessary because deprecatedJQueryDropdown doesn't support a second style of headers
font-weight: $gl-font-weight-bold;
- color: $gl-text-color;
+ color: var(--gl-text-color, $gl-text-color);
font-size: $gl-font-size;
line-height: 16px;
}
@@ -194,24 +125,24 @@ input[type='checkbox']:hover {
&.search-active {
form {
- border-color: $blue-300;
+ border-color: var(--blue-300, $blue-300);
box-shadow: none;
.search-input-wrap {
.search-icon,
.clear-icon {
- color: $gl-text-color-tertiary;
+ color: var(--gray-400, $gl-text-color-tertiary);
transition: color ease-in-out $default-transition-duration;
}
}
.search-input {
- color: $gl-text-color;
+ color: var(--gl-text-color, $gl-text-color);
transition: color ease-in-out $default-transition-duration;
}
.search-input::placeholder {
- color: $gl-text-color-tertiary;
+ color: var(--gray-400, $gl-text-color-tertiary);
}
}
}
@@ -230,7 +161,7 @@ input[type='checkbox']:hover {
.inline-search-icon {
position: relative;
margin-right: 4px;
- color: $gl-text-color-secondary;
+ color: var(--gray-500, $gl-text-color-secondary);
}
.identicon,
@@ -244,7 +175,7 @@ input[type='checkbox']:hover {
width: $search-avatar-size;
height: $search-avatar-size;
border-radius: 50%;
- border: 1px solid $gray-normal;
+ border: 1px solid var(--gray-50, $gray-normal);
}
}
@@ -265,7 +196,7 @@ input[type='checkbox']:hover {
position: absolute;
left: 10px;
top: 9px;
- color: $gray-darkest;
+ color: var(--gray-700, $gray-darkest);
pointer-events: none;
}
@@ -283,7 +214,7 @@ input[type='checkbox']:hover {
.btn-search,
.btn-success,
.dropdown-menu-toggle,
- .gl-new-dropdown {
+ .gl-dropdown {
width: 100%;
margin-top: 5px;
@@ -302,7 +233,7 @@ input[type='checkbox']:hover {
}
.dropdown-menu-toggle,
- .gl-new-dropdown {
+ .gl-dropdown {
@include media-breakpoint-up(sm) {
width: 180px;
margin-top: 0;
@@ -317,7 +248,7 @@ input[type='checkbox']:hover {
}
.dropdown-menu-toggle,
- .gl-new-dropdown {
+ .gl-dropdown {
@include media-breakpoint-up(lg) {
width: 240px;
}
diff --git a/app/assets/stylesheets/page_bundles/settings.scss b/app/assets/stylesheets/page_bundles/settings.scss
new file mode 100644
index 00000000000..9037eb7ae62
--- /dev/null
+++ b/app/assets/stylesheets/page_bundles/settings.scss
@@ -0,0 +1,209 @@
+@import 'mixins_and_variables_and_functions';
+
+@keyframes expandMaxHeight {
+ 0% {
+ max-height: 0;
+ }
+
+ 99% {
+ max-height: 100vh;
+ }
+
+ 100% {
+ max-height: none;
+ }
+}
+
+@keyframes collapseMaxHeight {
+ 0% {
+ max-height: 100vh;
+ }
+
+ 100% {
+ max-height: 0;
+ }
+}
+
+.settings {
+ // border-top for each item except the top one
+ border-top: 1px solid var(--border-color, $border-color);
+
+ &:first-of-type {
+ margin-top: 10px;
+ padding-top: 0;
+ border: 0;
+ }
+
+ + div .settings:first-of-type {
+ margin-top: 0;
+ border-top: 1px solid var(--border-color, $border-color);
+ }
+
+ &.animating {
+ overflow: hidden;
+ }
+}
+
+.settings-header {
+ position: relative;
+ padding: $gl-padding-24 110px 0 0;
+
+ h4 {
+ margin-top: 0;
+ }
+
+ .settings-title {
+ cursor: pointer;
+ }
+
+ button {
+ position: absolute;
+ top: 20px;
+ right: 6px;
+ min-width: 80px;
+ }
+}
+
+.settings-content {
+ max-height: 1px;
+ overflow-y: hidden;
+ padding-right: 110px;
+ animation: collapseMaxHeight 300ms ease-out;
+ // Keep the section from expanding when we scroll over it
+ pointer-events: none;
+
+ .settings.expanded & {
+ max-height: none;
+ overflow-y: visible;
+ animation: expandMaxHeight 300ms ease-in;
+ // Reset and allow clicks again when expanded
+ pointer-events: auto;
+ }
+
+ .settings.no-animate & {
+ animation: none;
+ }
+
+ @media(max-width: map-get($grid-breakpoints, md)-1) {
+ padding-right: 20px;
+ }
+
+ &::before {
+ content: ' ';
+ display: block;
+ height: 1px;
+ overflow: hidden;
+ margin-bottom: 4px;
+ }
+
+ &::after {
+ content: ' ';
+ display: block;
+ height: 1px;
+ overflow: hidden;
+ margin-top: 20px;
+ }
+
+ .sub-section {
+ margin-bottom: 32px;
+ padding: 16px;
+ border: 1px solid var(--border-color, $border-color);
+ background-color: var(--gray-light, $gray-light);
+ }
+
+ .bs-callout,
+ .form-check:first-child,
+ .form-check .form-text.text-muted,
+ .form-check + .form-text.text-muted {
+ margin-top: 0;
+ }
+
+ .form-check .form-text.text-muted {
+ margin-bottom: $grid-size;
+ }
+}
+
+.settings-list-icon {
+ color: var(--gray-500, $gl-text-color-secondary);
+ font-size: $default-icon-size;
+ line-height: 42px;
+}
+
+.settings-message {
+ padding: 5px;
+ line-height: 1.3;
+ color: var(--gray-900, $gray-900);
+ background-color: var(--orange-50, $orange-50);
+ border: 1px solid var(--orange-200, $orange-200);
+ border-radius: $gl-border-radius-base;
+}
+
+.prometheus-metrics-monitoring {
+ .card {
+ .card-toggle {
+ width: 14px;
+ }
+
+ .badge.badge-pill {
+ font-size: 12px;
+ line-height: 12px;
+ }
+
+ .card-header .label-count {
+ color: var(--white, $white);
+ background: var(--gray-800, $gray-800);
+ }
+
+ .card-body {
+ padding: 0;
+ }
+
+ .flash-container {
+ margin-bottom: 0;
+ cursor: default;
+
+ .flash-notice {
+ border-radius: 0;
+ }
+ }
+ }
+
+ .custom-monitored-metrics {
+ .card-header {
+ display: flex;
+ align-items: center;
+ }
+
+ .custom-metric {
+ display: flex;
+ align-items: center;
+ }
+
+ .custom-metric-link-bold {
+ font-weight: $gl-font-weight-bold;
+ text-decoration: none;
+ }
+ }
+
+ .loading-metrics .metrics-load-spinner {
+ color: var(--gray-700, $gray-700);
+ }
+
+ .metrics-list {
+ margin-bottom: 0;
+
+ li {
+ padding: $gl-padding;
+
+ .badge.badge-pill {
+ margin-left: 5px;
+ background: $badge-bg;
+ }
+
+ /* Ensure we don't add border if there's only single li */
+ + li {
+ border-top: 1px solid var(--border-color, $border-color);
+ }
+ }
+ }
+}
diff --git a/app/assets/stylesheets/page_bundles/todos.scss b/app/assets/stylesheets/page_bundles/todos.scss
index 3eacf98688e..b35f5b38740 100644
--- a/app/assets/stylesheets/page_bundles/todos.scss
+++ b/app/assets/stylesheets/page_bundles/todos.scss
@@ -7,8 +7,16 @@
.todos-list > .todo {
// workaround because we cannot use border-collapse
+ padding: 6px 12px !important;
border-top: 1px solid transparent;
+ span:not(.todo-label),
+ button,
+ a:not(.todo-target-link),
+ time {
+ @include gl-relative;
+ }
+
// overwrite border style of .content-list
&:last-child {
border-bottom: 1px solid transparent;
@@ -38,25 +46,66 @@
.todo-item {
@include transition(opacity);
- .todo-label,
- .todo-project {
- a {
- color: var(--blue-600, $blue-600);
- }
+ .todo-label a::before {
+ // Make area of the todo item clickable by expanding the area around the todo link
+ @include gl-content-empty;
+ @include gl-absolute;
+ @include gl-left-0;
+ @include gl-right-0;
+ @include gl-top-0;
+ @include gl-bottom-0;
+ z-index: 9;
}
+}
- .todo-body {
- p {
- color: var(--gl-text-color, $gl-text-color);
- }
+.todo-title {
+ margin-right: 2.5rem;
- .gl-label-scoped {
- --label-inset-border: inset 0 0 0 1px currentColor;
- }
+ @include media-breakpoint-up(sm) {
+ @include gl-mr-0;
+ @include gl-text-overflow-ellipsis;
+ @include gl-white-space-nowrap;
+ @include gl-overflow-hidden;
+ }
+}
- @include media-breakpoint-down(sm) {
- border-left: 2px solid var(--border-color, $border-color);
- padding-left: 10px;
- }
+.todo-body {
+ p {
+ @include gl-display-inline;
+ color: var(--gl-text-color, $gl-text-color);
+ }
+
+ pre.code.highlight {
+ @include gl-py-0;
+ @include gl-px-1;
+ @include gl-m-0;
+ @include gl-bg-gray-50;
+ @include gl-border-0;
+ @include gl-rounded-base;
+ @include gl-display-inline-flex;
+ @include gl-text-body;
+ }
+
+ .gl-label-scoped {
+ --label-inset-border: inset 0 0 0 1px currentColor;
+ }
+
+ .avatar {
+ @include gl-mb-0;
+ }
+}
+
+.todo-actions,
+.todo-body .todo-avatar,
+.todos-list > .todo a:not(.todo-target-link) {
+ z-index: 11 !important;
+}
+
+.todo-actions {
+ @include gl-absolute;
+ @include gl-right-0;
+
+ @include media-breakpoint-up(sm) {
+ @include gl-relative;
}
}
diff --git a/app/assets/stylesheets/page_bundles/tree.scss b/app/assets/stylesheets/page_bundles/tree.scss
index 58e55e11f7e..50d9684c7d2 100644
--- a/app/assets/stylesheets/page_bundles/tree.scss
+++ b/app/assets/stylesheets/page_bundles/tree.scss
@@ -205,3 +205,18 @@
.blob-content-holder {
margin-top: $gl-padding;
}
+
+
+.web-ide-promo-popover {
+ box-shadow: 0 0 18px -1.9px rgba(119, 89, 194, 0.16),
+ 0 0 12.9px -1.7px rgba(119, 89, 194, 0.16), 0 0 9.2px -1.4px rgba(119, 89, 194, 0.16),
+ 0 0 6.4px -1.1px rgba(119, 89, 194, 0.16), 0 0 4.5px -0.8px rgba(119, 89, 194, 0.16),
+ 0 0 3px -0.6px rgba(119, 89, 194, 0.16), 0 0 1.8px -0.3px rgba(119, 89, 194, 0.16),
+ 0 0 0.6px rgba(119, 89, 194, 0.16);
+ z-index: 999;
+}
+
+.web-ide-promo-popover-illustration {
+ width: calc(100% + 24px);
+ margin: -28px -12px 0;
+}
diff --git a/app/assets/stylesheets/pages/users.scss b/app/assets/stylesheets/page_bundles/users.scss
index 3dcc17df61a..d4cd28504fc 100644
--- a/app/assets/stylesheets/pages/users.scss
+++ b/app/assets/stylesheets/page_bundles/users.scss
@@ -1,3 +1,5 @@
+@import 'mixins_and_variables_and_functions';
+
.user-search-form {
position: relative;
diff --git a/app/assets/stylesheets/page_bundles/work_items.scss b/app/assets/stylesheets/page_bundles/work_items.scss
index 820a1a0b53e..4766f124e5b 100644
--- a/app/assets/stylesheets/page_bundles/work_items.scss
+++ b/app/assets/stylesheets/page_bundles/work_items.scss
@@ -81,4 +81,8 @@
}
}
}
+
+ > .col {
+ min-width: 0;
+ }
}
diff --git a/app/assets/stylesheets/pages/colors.scss b/app/assets/stylesheets/pages/colors.scss
index 20e072b9903..d1917948c88 100644
--- a/app/assets/stylesheets/pages/colors.scss
+++ b/app/assets/stylesheets/pages/colors.scss
@@ -22,3 +22,11 @@
display: none;
}
}
+
+.warning-title {
+ color: var(--gray-900, $gray-900);
+}
+
+.danger-title {
+ color: var(--red-500, $red-500);
+}
diff --git a/app/assets/stylesheets/pages/commits.scss b/app/assets/stylesheets/pages/commits.scss
index 19318d87731..dd24e3fcb5d 100644
--- a/app/assets/stylesheets/pages/commits.scss
+++ b/app/assets/stylesheets/pages/commits.scss
@@ -338,11 +338,6 @@
color: $gl-text-color;
}
-.commit .gpg-popover-help-link {
- display: block;
- color: $link-color;
-}
-
.add-review-item {
.gl-tab-nav-item {
height: 100%;
diff --git a/app/assets/stylesheets/pages/events.scss b/app/assets/stylesheets/pages/events.scss
index ce8dd6684f2..f6c79a4eca2 100644
--- a/app/assets/stylesheets/pages/events.scss
+++ b/app/assets/stylesheets/pages/events.scss
@@ -4,7 +4,7 @@
*/
.event-item {
font-size: $gl-font-size;
- padding: $gl-padding 0 $gl-padding 56px;
+ padding: $gl-padding 0 $gl-padding $gl-spacing-scale-8;
border-bottom: 1px solid $white-normal;
color: $gl-text-color-secondary;
position: relative;
diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss
deleted file mode 100644
index 1b6e7954366..00000000000
--- a/app/assets/stylesheets/pages/issuable.scss
+++ /dev/null
@@ -1,912 +0,0 @@
-.status-box {
- padding: 0 $gl-btn-padding;
- border-radius: $border-radius-default;
- display: block;
- float: left;
- margin-right: $gl-padding-8;
- color: $white;
- font-size: $gl-font-size;
- line-height: $gl-line-height-24;
-}
-
-.issuable-warning-icon {
- background-color: $orange-50;
- border-radius: $border-radius-default;
- color: $orange-600;
- width: $issuable-warning-size;
- height: $issuable-warning-size;
- text-align: center;
- margin-right: $issuable-warning-icon-margin;
- line-height: $gl-line-height-24;
- flex: 0 0 auto;
-}
-
-.limit-container-width {
- .flash-container,
- .detail-page-header,
- .page-content-header,
- .commit-box,
- .info-well,
- .commit-ci-menu,
- .files-changed-inner,
- .limited-header-width,
- .limited-width-notes {
- @include fixed-width-container;
- }
-
- .issuable-details {
- .detail-page-description,
- .mr-source-target,
- .mr-state-widget,
- .merge-manually {
- @include fixed-width-container;
- }
- }
-
- .merge-request-details {
- .emoji-list-container {
- @include fixed-width-container;
- }
- }
-}
-
-.issuable-details {
- section {
- .issuable-discussion {
- margin-right: 1px;
- }
- }
-
- .title-container {
- display: flex;
- align-items: flex-start;
- }
-
- .title {
- padding: 0;
- margin-bottom: $gl-padding;
- border-bottom: 0;
- word-wrap: break-word;
- overflow-wrap: break-word;
- min-width: 0;
- width: 100%;
- text-align: initial;
- }
-
- .btn-edit {
- margin-left: auto;
- }
-}
-
-.issuable-show-labels {
- .gl-label {
- margin-bottom: 5px;
- margin-right: 5px;
- }
-
- a {
- display: inline-block;
-
- .color-label {
- padding: 4px $grid-size;
- border-radius: $label-border-radius;
- margin-right: 4px;
- margin-bottom: 4px;
- }
-
- &:hover .color-label {
- text-decoration: underline;
- }
- }
-
- &.has-labels {
- // this font size is a fix to
- // prevent unintended spacing between labels
- // which shows up when rendering markup has white-space
- // characters present.
- // see: https://css-tricks.com/fighting-the-space-between-inline-block-elements/#article-header-id-3
- font-size: 0;
- margin-bottom: -5px;
- }
-}
-
-.assignee,
-.reviewer {
- .merge-icon {
- color: $orange-400;
- position: absolute;
- bottom: -3px;
- right: -3px;
- filter: drop-shadow(0 0 0.5px $white) drop-shadow(0 0 1px $white) drop-shadow(0 0 2px $white);
- }
-}
-
-@mixin right-sidebar {
- position: fixed;
- top: $header-height;
- // 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 $gl-transition-duration-medium;
- background-color: $white;
- z-index: 200;
- overflow: hidden;
-
-}
-
-.right-sidebar {
- &:not(.right-sidebar-merge-requests) {
- @include right-sidebar;
- }
-
- &.right-sidebar-merge-requests {
- @include media-breakpoint-down(md) {
- @include right-sidebar;
- z-index: 251;
- }
- }
-
- @include media-breakpoint-down(sm) {
- z-index: 251;
- }
-
- a:not(.btn) {
- color: inherit;
-
- &:hover {
- color: $blue-800;
- }
- }
-
- .gl-label .gl-label-link:hover {
- color: inherit;
- }
-
- .btn-link {
- color: inherit;
- }
-
- .issuable-header-text {
- margin-top: 7px;
- }
-
- .gutter-toggle {
- display: flex;
- align-items: center;
- margin-left: 20px;
- padding: 4px;
- border-radius: 4px;
- height: 24px;
-
- &:hover {
- color: $gl-text-color;
- background: $gray-50;
- }
-
- &:hover,
- &:focus {
- text-decoration: none;
- }
- }
-
- &.right-sidebar-merge-requests {
- .block,
- .sidebar-contained-width,
- .issuable-sidebar-header {
- width: 100%;
- }
-
- .block {
- @include media-breakpoint-up(lg) {
- padding: $gl-spacing-scale-4 0 $gl-spacing-scale-5;
- }
-
- &.participants {
- border-bottom: 0;
- }
- }
- }
-
- .block,
- .sidebar-contained-width,
- .issuable-sidebar-header {
- @include clearfix;
- padding: $gl-spacing-scale-4 0 $gl-spacing-scale-5;
- border-bottom: 1px solid $border-gray-normal;
- // This prevents the mess when resizing the sidebar
- // of elements repositioning themselves..
- width: $gutter-inner-width;
- // --
-
- &:last-child {
- border: 0;
- }
-
- &.assignee {
- .author-link {
- display: block;
- position: relative;
-
- &:hover {
- .author {
- text-decoration: underline;
- }
- }
- }
- }
-
- &.time-tracking,
- &.participants,
- &.subscriptions,
- &.with-sub-blocks {
- padding-top: $gl-spacing-scale-5;
- }
- }
-
- .block-first {
- padding-top: 0;
- }
-
- .title {
- color: $gl-text-color;
- line-height: $gl-line-height-20;
-
- .avatar {
- margin-left: 0;
- }
- }
-
- .selectbox {
- display: none;
-
- &.show {
- display: block;
- }
- }
-
- .btn-clipboard:hover {
- color: $gl-text-color;
- }
-
- .issuable-sidebar {
- height: 100%;
-
- &:not(.is-merge-request) {
- overflow-y: scroll;
- overflow-x: hidden;
- -webkit-overflow-scrolling: touch;
- }
-
- &.is-merge-request {
- @include media-breakpoint-down(sm) {
- overflow-y: scroll;
- overflow-x: hidden;
- -webkit-overflow-scrolling: touch;
- }
- }
- }
-
- &.right-sidebar-expanded {
- &:not(.right-sidebar-merge-requests) {
- width: $gutter-width;
- }
-
- .value {
- line-height: 1;
- }
-
- .issuable-sidebar {
- padding: 0 20px;
-
- &.is-merge-request {
- @include media-breakpoint-up(lg) {
- padding: 0;
-
- .issuable-context-form {
- --initial-top: calc(#{$header-height} + 76px);
- --top: var(--initial-top);
-
- @include gl-sticky;
- @include gl-overflow-auto;
-
- top: var(--top);
- height: calc(100vh - var(--top));
- padding: 0 15px;
- margin-bottom: calc(var(--top) * -1);
-
- .with-performance-bar & {
- --top: calc(var(--initial-top) + #{$performance-bar-height});
- }
-
- .with-system-header & {
- --top: calc(var(--initial-top) + #{$system-header-height});
- }
-
- .with-performance-bar.with-system-header & {
- --top: calc(var(--initial-top) + #{$system-header-height} + #{$performance-bar-height});
- }
- }
- }
- }
- }
-
- &:not(.boards-sidebar):not([data-signed-in]):not([data-always-show-toggle]) {
- .issuable-sidebar-header {
- display: none;
- }
- }
-
- .light {
- font-weight: $gl-font-weight-normal;
- }
-
- .no-value {
- color: $gl-text-color-secondary;
- }
-
- .sidebar-collapsed-icon {
- display: none;
- }
-
- .gutter-toggle {
- text-align: center;
- }
-
- .title .gutter-toggle {
- margin-top: 0;
- }
-
- .assignee .user-list .avatar {
- margin: 0;
- }
-
- .hide-expanded {
- display: none;
- }
- }
-
- &.right-sidebar-collapsed {
- /* Extra small devices (phones, less than 768px) */
- display: none;
- /* Small devices (tablets, 768px and up) */
-
- &:not(.right-sidebar-merge-requests) {
- @include media-breakpoint-up(sm) {
- display: block;
- }
- }
-
- &.right-sidebar-merge-requests {
- @include media-breakpoint-up(lg) {
- display: block;
- }
- }
-
- width: $gutter-collapsed-width;
- padding: 0;
-
- .block,
- .sidebar-contained-width,
- .issuable-sidebar-header {
- width: $gutter-collapsed-width - 2px;
- padding: 0;
- border-bottom: 0;
- overflow: hidden;
- }
-
- .block,
- .gutter-toggle,
- .sidebar-collapsed-container {
- &.with-sub-blocks .sub-block:hover,
- &:not(.with-sub-blocks):hover {
- background-color: $gray-100;
- }
- }
-
- .participants {
- border-bottom: 1px solid $border-gray-normal;
- }
-
- .hide-collapsed {
- display: none;
- }
-
- .gutter-toggle {
- width: 100%;
- height: $sidebar-toggle-height;
- margin-left: 0;
- border-bottom: 1px solid $border-white-normal;
- border-radius: 0;
- }
-
- a.gutter-toggle {
- display: flex;
- justify-content: center;
- flex-direction: column;
- text-align: center;
- }
-
- .merge-icon {
- height: 12px;
- width: 12px;
- bottom: -5px;
- right: 4px;
- }
-
- .sidebar-collapsed-icon {
- display: flex;
- flex-direction: column;
- justify-content: center;
- align-items: center;
- width: 100%;
- height: $sidebar-toggle-height;
- text-align: center;
- color: $gl-text-color-secondary;
-
- > svg {
- fill: $gl-text-color-secondary;
- }
-
- &:hover:not(.disabled),
- &:hover .todo-undone {
- color: $gl-text-color;
-
- > svg {
- fill: $gl-text-color;
- }
- }
-
- .todo-undone {
- color: $blue-600;
- fill: $blue-600;
- }
-
- .author {
- display: none;
- }
-
- .avatar-counter:hover {
- color: $gl-text-color-secondary;
- border-color: $gl-text-color-secondary;
- }
-
- .btn-clipboard {
- /*
- This change should be temporary, because the DOM currently gets
- generated from a ruby definition in `app/helpers/button_helper.rb`.
- As soon as the `copy to clipboard` button will be transferred to
- Vue this should be adjusted as well.
- */
- flex: 1;
- align-self: stretch;
- padding: 0;
-
- border: 0;
- background: transparent;
- color: $gl-text-color-secondary;
-
- &:hover {
- color: $gl-text-color;
- }
- }
-
- &.multiple-users {
- display: flex;
- justify-content: center;
- }
- }
-
- .sidebar-avatar-counter {
- width: 24px;
- height: 24px;
- border-radius: 12px;
-
- ~.merge-icon {
- bottom: 0;
- }
- }
-
- .sidebar-collapsed-user {
- padding-bottom: 0;
-
- .author-link {
- padding-left: 0;
-
- .avatar {
- position: static;
- margin: 0;
- }
- }
- }
-
- .issuable-header-btn {
- display: none;
- }
-
- .multiple-users {
- .btn-link {
- padding: 0;
- border: 0;
-
- .avatar {
- margin: 0;
- }
- }
-
- .btn-link:first-child {
- position: absolute;
- left: 10px;
- z-index: 1;
- }
-
- .btn-link:last-child {
- position: absolute;
- right: 10px;
-
- &:hover {
- text-decoration: none;
- }
- }
- }
-
- .milestone-title span,
- .collapse-truncated-title {
- @include str-truncated(100%);
- display: block;
- margin: 0 4px;
- }
- }
-
- .dropdown-menu-toggle {
- width: 100%;
- padding-top: 6px;
- }
-
- .dropdown-menu {
- width: 100%;
-
- /*
- * Overwrite hover style for dropdown items, so that they are not blue
- * This should be removed during dev of https://gitlab.com/gitlab-org/gitlab-foss/issues/44040
- */
- li a {
- &:hover,
- &:active,
- &:focus,
- &.is-focused {
- @include dropdown-item-hover;
- }
- }
-
- }
-}
-
-.with-performance-bar .right-sidebar {
- top: calc(#{$header-height} + #{$performance-bar-height});
-}
-
-.sidebar-move-issue-confirmation-button {
- width: 100%;
-
- &.is-loading {
- .sidebar-move-issue-confirmation-loading-icon {
- display: inline-block;
- }
- }
-}
-
-.sidebar-move-issue-confirmation-loading-icon {
- display: none;
-}
-
-.detail-page-description {
- padding: 16px 0;
-
- small {
- color: $gray-500;
- }
-}
-
-.edited-text {
- color: $gray-500;
- display: block;
- margin: 16px 0 0;
- font-size: 85%;
-
- .author-link {
- color: $gray-500;
- }
-}
-
-.participants-author {
- &:nth-of-type(8n) {
- padding-right: 0;
- }
-
- .avatar.avatar-inline {
- margin: 0;
- }
-}
-
-.user-item {
- padding: 5px;
- flex-basis: 20%;
-
- .user-link {
- display: inline-block;
- }
-}
-
-.participants-more,
-.user-list-more {
- margin-left: 5px;
-
- a,
- .btn-link {
- color: $gl-text-color-secondary;
- }
-
- .btn-link {
- padding: 0;
- }
-
- .btn-link:hover {
- color: $blue-800;
- text-decoration: none;
- }
-
- .btn-link:focus {
- text-decoration: none;
- }
-}
-
-.issuable-gutter-toggle {
- @include media-breakpoint-down(sm) {
- margin-left: $btn-side-margin;
- }
-}
-
-.issuable-meta {
- flex: 1;
- display: inline-block;
- font-size: 14px;
- align-self: center;
- overflow: hidden;
- text-overflow: ellipsis;
-
- .user-status-emoji {
- margin-left: $gl-padding-4;
- margin-right: 0;
- }
-}
-
-.js-issuable-selector-wrap {
- .js-issuable-selector {
- width: 100%;
- }
-
- @include media-breakpoint-down(sm) {
- margin-bottom: $gl-padding;
- }
-}
-
-.issuable-list {
- li {
- .issuable-info-container {
- flex: 1;
- display: flex;
- }
-
- .issuable-main-info {
- flex: 1 auto;
- margin-right: 10px;
- min-width: 0;
-
- .issue-weight-icon,
- .issue-estimate-icon {
- vertical-align: sub;
- }
- }
-
- .issuable-meta {
- display: flex;
- flex-direction: column;
- align-items: flex-end;
- flex: 1 0 auto;
-
- .controls {
- margin-bottom: 2px;
- line-height: 20px;
- padding: 0;
- }
- }
-
- @include media-breakpoint-down(xs) {
- .issuable-meta {
- .controls li {
- margin-right: 0;
- }
- }
- }
-
- .issue-check {
- min-width: 15px;
- }
-
- .issuable-milestone,
- .issuable-info,
- .task-status,
- .issuable-timestamp {
- font-weight: $gl-font-weight-normal;
- color: $gl-text-color-secondary;
-
- a {
- color: $gl-text-color;
- }
-
- .gl-label-link {
- color: inherit;
-
- &:hover {
- text-decoration: none;
-
- .gl-label-text:last-of-type {
- text-decoration: underline;
- }
- }
- }
-
- .milestone {
- color: $gray-700;
- }
- }
-
- @media(max-width: map-get($grid-breakpoints, lg)-1) {
- .task-status,
- .issuable-due-date,
- .issuable-weight,
- .project-ref-path {
- display: none;
- }
- }
- }
-}
-
-.issuable-list li,
-.issuable-info-container .controls {
- .avatar-counter {
- display: inline-block;
- vertical-align: middle;
- min-width: 16px;
- line-height: 14px;
- height: 16px;
- padding-left: 2px;
- padding-right: 2px;
- }
-}
-
-.add-issuable-form-input-wrapper {
- &.focus {
- border-color: $gray-700;
- @include gl-focus;
-
- input {
- @include gl-shadow-none;
- }
- }
-
- .gl-show-field-errors &.form-control:not(textarea) {
- height: auto;
- }
-}
-
-.sidebar-help-wrap {
- .sidebar-help-state {
- margin: 16px -20px -20px;
- padding: 16px 20px;
- }
-
- .help-state-toggle-enter-active {
- transition: all 0.8s ease;
- }
-
- .help-state-toggle-leave-active {
- transition: all 0.5s ease;
- }
-
- .help-state-toggle-enter,
- .help-state-toggle-leave-active {
- opacity: 0;
- }
-}
-
-.time-tracker {
- .sidebar-collapsed-icon {
- > .stopwatch-svg {
- display: inline-block;
- }
-
- svg {
- width: 16px;
- height: 16px;
- fill: $gl-text-color-secondary;
- }
-
- &:hover svg {
- fill: $gl-text-color;
- }
- }
-
- .compare-meter {
- &.over_estimate {
- .time-remaining,
- .compare-value.spent {
- color: $red-500;
- }
- }
- }
-
- .compare-display-container {
- font-size: 13px;
- }
-}
-
-/*
- * Following overrides are done to prevent
- * legacy dropdown styles from influencing
- * GitLab UI components used within GlDropdown
- */
-.issuable-move-dropdown {
- .b-dropdown-form {
- @include gl-p-0;
- }
-
- .gl-search-box-by-type button.gl-clear-icon-button:hover {
- @include gl-bg-transparent;
- }
-
- .issuable-move-button:not(.disabled):hover {
- @include gl-text-white;
- }
-}
-
-.right-sidebar-collapsed {
- .sidebar-grouped-item {
- .sidebar-collapsed-icon {
- margin-bottom: 0;
- }
-
- .sidebar-collapsed-divider {
- line-height: 5px;
- font-size: 12px;
- color: $gray-500;
-
- + .sidebar-collapsed-icon {
- padding-top: 0;
- }
- }
- }
-}
-
-.suggestion-footer {
- font-size: 12px;
- line-height: 15px;
-
- .avatar {
- margin-top: -3px;
- border: 0;
- }
-}
-
-@include media-breakpoint-down(sm) {
- // Overriding the following rule with the negative margin
- // https://gitlab.com/gitlab-org/gitlab/-/blob/146c43c931c3743a140529307aea616e4aa9ff21/app/assets/stylesheets/framework/sidebar.scss#L1-5
- .container-fluid {
- .issuable-list,
- .issues-filters,
- .epics-filters {
- margin: 0 (-$gl-padding);
- }
- }
-}
diff --git a/app/assets/stylesheets/pages/login.scss b/app/assets/stylesheets/pages/login.scss
index d4ad6da7f4d..360ea20733d 100644
--- a/app/assets/stylesheets/pages/login.scss
+++ b/app/assets/stylesheets/pages/login.scss
@@ -262,7 +262,7 @@
.footer-container,
hr.footer-fixed {
- position: absolute;
+ position: fixed;
bottom: 0;
left: 0;
right: 0;
diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss
index b016d0a1068..6b662359a67 100644
--- a/app/assets/stylesheets/pages/merge_requests.scss
+++ b/app/assets/stylesheets/pages/merge_requests.scss
@@ -382,3 +382,9 @@ $comparison-empty-state-height: 62px;
.survey-slide-up-enter-active {
@include gl-transition-slow;
}
+
+.mr-compare-dropdown {
+ .gl-button-text {
+ @include gl-w-full;
+ }
+}
diff --git a/app/assets/stylesheets/pages/ml_experiment_tracking.scss b/app/assets/stylesheets/pages/ml_experiment_tracking.scss
index 2dff51cff92..c1582f2090b 100644
--- a/app/assets/stylesheets/pages/ml_experiment_tracking.scss
+++ b/app/assets/stylesheets/pages/ml_experiment_tracking.scss
@@ -14,3 +14,9 @@
color: $gl-text-color;
}
}
+
+table.candidate-details {
+ td {
+ padding: $gl-spacing-scale-3;
+ }
+}
diff --git a/app/assets/stylesheets/pages/monitor.scss b/app/assets/stylesheets/pages/monitor.scss
deleted file mode 100644
index 25ff5abd774..00000000000
--- a/app/assets/stylesheets/pages/monitor.scss
+++ /dev/null
@@ -1,5 +0,0 @@
-.chart-tooltip > .popover {
- min-width: 0;
- width: max-content;
- max-width: $chart-tooltip-max-width;
-}
diff --git a/app/assets/stylesheets/pages/note_form.scss b/app/assets/stylesheets/pages/note_form.scss
index cb77c31d59a..adeab227670 100644
--- a/app/assets/stylesheets/pages/note_form.scss
+++ b/app/assets/stylesheets/pages/note_form.scss
@@ -287,8 +287,7 @@ table {
.discussion-reply-holder {
.reply-placeholder-text-field {
- font-family: $monospace-font;
- font-size: $gl-font-size-monospace;
+ @include gl-font-monospace;
border-radius: $gl-border-radius-base;
width: 100%;
resize: none;
diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss
index fa3c87490f1..75d52424fd9 100644
--- a/app/assets/stylesheets/pages/notes.scss
+++ b/app/assets/stylesheets/pages/notes.scss
@@ -4,7 +4,7 @@ $system-note-svg-size: 1rem;
@mixin vertical-line($left) {
&::before {
content: '';
- border-left: 2px solid var(--gray-10, $gray-10);
+ border-left: 2px solid var(--gray-50, $gray-50);
position: absolute;
top: $gl-padding-6;
bottom: 0;
@@ -60,6 +60,10 @@ $system-note-svg-size: 1rem;
padding: $gl-padding-4 $gl-padding-8;
}
+ &.draft-note .timeline-content:not(.flash-container) {
+ border: 0;
+ }
+
.note-header-info {
min-height: 2rem;
display: flex;
@@ -81,7 +85,7 @@ $system-note-svg-size: 1rem;
margin-top: 5px;
}
- .timeline-content {
+ .timeline-content:not(.flash-container) {
margin-left: 2.5rem;
border-left: 1px solid $border-color;
border-right: 1px solid $border-color;
@@ -93,16 +97,26 @@ $system-note-svg-size: 1rem;
}
}
+ &.draft-note .timeline-content:not(.flash-container) {
+ margin-left: 0;
+ border-top-left-radius: 0;
+ border-top-right-radius: 0;
+ }
+
&:not(:first-of-type) .timeline-entry-inner {
margin-left: 2.5rem;
border-left: 1px solid $border-color;
border-right: 1px solid $border-color;
background-color: $white;
- .timeline-content {
+ .timeline-content:not(.flash-container) {
padding: $gl-padding-8 $gl-padding-8 $gl-padding-8 $gl-padding;
}
+ .timeline-discussion-body-footer {
+ padding: 0 $gl-padding-8 0;
+ }
+
.timeline-avatar {
margin: $gl-padding-8 0 0 $gl-padding;
}
@@ -111,6 +125,12 @@ $system-note-svg-size: 1rem;
margin-left: 2rem;
}
}
+
+ &:last-of-type .timeline-entry-inner {
+ border-bottom: 1px solid $border-color;
+ border-bottom-left-radius: $gl-border-radius-base;
+ border-bottom-right-radius: $gl-border-radius-base;
+ }
}
.diff-content {
@@ -416,17 +436,17 @@ $system-note-svg-size: 1rem;
.timeline-icon {
display: flex;
align-items: center;
- background-color: $gray-10;
+ background-color: $gray-50;
width: $system-note-icon-size;
height: $system-note-icon-size;
- border: 1px solid $gray-10;
+ border: 1px solid $gray-50;
border-radius: $system-note-icon-size;
margin: -6px 0 0;
svg {
width: $system-note-svg-size;
height: $system-note-svg-size;
- fill: $gray-400;
+ fill: $gray-600;
display: block;
margin: 0 auto;
}
@@ -1050,7 +1070,7 @@ $system-note-svg-size: 1rem;
padding-left: 0;
ul.notes li.note-wrapper {
- .timeline-content {
+ .timeline-content:not(.flash-container) {
padding: $gl-padding-8 $gl-padding-8 $gl-padding-8 $gl-padding;
}
@@ -1066,7 +1086,7 @@ $system-note-svg-size: 1rem;
border-right: 0;
}
- div.discussion-reply-holder {
+ .discussion-reply-holder {
margin-left: 0;
}
}
@@ -1097,7 +1117,7 @@ $system-note-svg-size: 1rem;
}
}
- .draft-note-component .draft-note.timeline-entry {
+ .draft-note-component.draft-note.timeline-entry {
.timeline-content:not(.flash-container) {
padding: $gl-padding-8 $gl-padding-8 $gl-padding-8 $gl-padding;
}
diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss
index bf20204cfd9..15a32ea8ad3 100644
--- a/app/assets/stylesheets/pages/projects.scss
+++ b/app/assets/stylesheets/pages/projects.scss
@@ -247,7 +247,7 @@
.repository-languages-bar {
height: 8px;
- margin-bottom: $gl-padding-8;
+ margin-bottom: $gl-padding;
background-color: $white;
border-radius: $border-radius-default;
@@ -562,7 +562,7 @@
// Remove once gitlab/ui solution is implemented
// https://gitlab.com/gitlab-org/gitlab-ui/-/issues/1158
// https://gitlab.com/gitlab-org/gitlab/-/issues/300405
- .gl-new-dropdown-button-text {
+ .gl-dropdown-button-text {
@include str-truncated;
}
}
@@ -654,86 +654,3 @@
}
}
}
-
-.project-filters {
- .btn svg {
- color: $gray-700;
- }
-
- .button-filter-group {
- .btn {
- width: 96px;
- }
-
- a {
- color: $black;
- }
-
- .active {
- background: $btn-active-gray;
- }
- }
-
- .filtered-search-dropdown-label {
- min-width: 68px;
-
- @include media-breakpoint-down(xs) {
- min-width: 60px;
- }
- }
-
- .filtered-search {
- min-width: 30%;
- flex-basis: 0;
-
- .project-filter-form .project-filter-form-field {
- padding-right: $gl-padding-8;
- }
-
- .filtered-search,
- .filtered-search-nav,
- .filtered-search-dropdown {
- flex-basis: 0;
- }
-
- @include media-breakpoint-down(lg) {
- min-width: 15%;
-
- .project-filter-form-field {
- min-width: 150px;
- }
- }
-
- @include media-breakpoint-down(md) {
- min-width: 30%;
- }
- }
-
- .filtered-search-box {
- border-radius: 3px 0 0 3px;
- }
-
- .dropdown-menu-toggle {
- margin-left: $gl-padding-8;
- }
-
- @include media-breakpoint-down(md) {
- .extended-filtered-search-box {
- min-width: 55%;
- }
-
- .filtered-search-dropdown {
- width: 50%;
-
- .dropdown-menu-toggle {
- width: 100%;
- }
- }
- }
-
- @include media-breakpoint-down(xs) {
- .filtered-search-dropdown {
- width: 100%;
- }
- }
-}
diff --git a/app/assets/stylesheets/pages/settings.scss b/app/assets/stylesheets/pages/settings.scss
index c364b233803..2d78ab82b7d 100644
--- a/app/assets/stylesheets/pages/settings.scss
+++ b/app/assets/stylesheets/pages/settings.scss
@@ -1,149 +1,3 @@
-@keyframes expandMaxHeight {
- 0% {
- max-height: 0;
- }
-
- 99% {
- max-height: 100vh;
- }
-
- 100% {
- max-height: none;
- }
-}
-
-@keyframes collapseMaxHeight {
- 0% {
- max-height: 100vh;
- }
-
- 100% {
- max-height: 0;
- }
-}
-
-.settings {
- // border-top for each item except the top one
- border-top: 1px solid $border-color;
-
- &:first-of-type {
- margin-top: 10px;
- padding-top: 0;
- border: 0;
- }
-
- + div .settings:first-of-type {
- margin-top: 0;
- border-top: 1px solid $border-color;
- }
-
- &.animating {
- overflow: hidden;
- }
-}
-
-.settings-header {
- position: relative;
- padding: 24px 110px 0 0;
-
- h4 {
- margin-top: 0;
- }
-
- .settings-title {
- cursor: pointer;
- }
-
- button {
- position: absolute;
- top: 20px;
- right: 6px;
- min-width: 80px;
- }
-}
-
-.settings-content {
- max-height: 1px;
- overflow-y: hidden;
- padding-right: 110px;
- animation: collapseMaxHeight 300ms ease-out;
- // Keep the section from expanding when we scroll over it
- pointer-events: none;
-
- .settings.expanded & {
- max-height: none;
- overflow-y: visible;
- animation: expandMaxHeight 300ms ease-in;
- // Reset and allow clicks again when expanded
- pointer-events: auto;
- }
-
- .settings.no-animate & {
- animation: none;
- }
-
- @media(max-width: map-get($grid-breakpoints, md)-1) {
- padding-right: 20px;
- }
-
- &::before {
- content: ' ';
- display: block;
- height: 1px;
- overflow: hidden;
- margin-bottom: 4px;
- }
-
- &::after {
- content: ' ';
- display: block;
- height: 1px;
- overflow: hidden;
- margin-top: 20px;
- }
-
- .sub-section {
- margin-bottom: 32px;
- padding: 16px;
- border: 1px solid $border-color;
- background-color: $gray-light;
- }
-
- .bs-callout,
- .form-check:first-child,
- .form-check .form-text.text-muted,
- .form-check + .form-text.text-muted {
- margin-top: 0;
- }
-
- .form-check .form-text.text-muted {
- margin-bottom: $grid-size;
- }
-}
-
-.settings-list-icon {
- color: $gl-text-color-secondary;
- font-size: $default-icon-size;
- line-height: 42px;
-}
-
-.settings-message {
- padding: 5px;
- line-height: 1.3;
- color: $gray-900;
- background-color: $orange-50;
- border: 1px solid $orange-200;
- border-radius: $border-radius-base;
-}
-
-.warning-title {
- color: $gray-900;
-}
-
-.danger-title {
- color: $red-500;
-}
-
.integration-settings-form {
.card.card-body,
.info-well {
@@ -160,13 +14,13 @@
.option-title {
font-weight: $gl-font-weight-normal;
display: inline-block;
- color: $gl-text-color;
+ color: var(--gl-text-color, $gl-text-color);
vertical-align: top;
}
.option-description,
.option-disabled-reason {
- color: $project-option-descr-color;
+ color: var(--gray-700, $gray-700);
}
.option-disabled-reason {
@@ -188,79 +42,9 @@
}
}
-.prometheus-metrics-monitoring {
- .card {
- .card-toggle {
- width: 14px;
- }
-
- .badge.badge-pill {
- font-size: 12px;
- line-height: 12px;
- }
-
- .card-header .label-count {
- color: $white;
- background: $common-gray-dark;
- }
-
- .card-body {
- padding: 0;
- }
-
- .flash-container {
- margin-bottom: 0;
- cursor: default;
-
- .flash-notice {
- border-radius: 0;
- }
- }
- }
-
- .custom-monitored-metrics {
- .card-header {
- display: flex;
- align-items: center;
- }
-
- .custom-metric {
- display: flex;
- align-items: center;
- }
-
- .custom-metric-link-bold {
- font-weight: $gl-font-weight-bold;
- text-decoration: none;
- }
- }
-
- .loading-metrics .metrics-load-spinner {
- color: $gray-700;
- }
-
- .metrics-list {
- margin-bottom: 0;
-
- li {
- padding: $gl-padding;
-
- .badge.badge-pill {
- margin-left: 5px;
- background: $badge-bg;
- }
-
- /* Ensure we don't add border if there's only single li */
- + li {
- border-top: 1px solid $border-color;
- }
- }
- }
-}
-
.saml-settings.info-well {
.form-control[readonly] {
- background: $white;
+ background: var(--white, $white);
}
}
@@ -275,8 +59,8 @@
}
.btn-clipboard {
- background-color: $white;
- border: 1px solid $gray-100;
+ background-color: var(--white, $white);
+ border: 1px solid var(--gray-100, $gray-100);
}
.deploy-token-help-block {
@@ -294,7 +78,7 @@
.ci-secure-files-table {
table {
thead {
- border-bottom: 1px solid $white-normal;
+ border-bottom: 1px solid var(--gray-50, $gray-50);
}
tr {
diff --git a/app/assets/stylesheets/startup/startup-dark.scss b/app/assets/stylesheets/startup/startup-dark.scss
index 11131cc1a4b..c7e55289b11 100644
--- a/app/assets/stylesheets/startup/startup-dark.scss
+++ b/app/assets/stylesheets/startup/startup-dark.scss
@@ -2,25 +2,6 @@
// Please see the feedback issue for more details and help:
// https://gitlab.com/gitlab-org/gitlab/-/issues/331812
@charset "UTF-8";
-:root {
- color-scheme: dark;
-}
-body.gl-dark {
- --gray-10: #1f1e24;
- --gray-50: #333238;
- --gray-100: #434248;
- --gray-200: #535158;
- --gray-700: #bfbfc3;
- --gray-900: #ececef;
- --green-100: #0d532a;
- --green-700: #91d4a8;
- --gl-text-color: #ececef;
- --border-color: #4f4f4f;
- --black: #fff;
-}
-:root {
- --white: #333;
-}
*,
*::before,
*::after {
@@ -36,9 +17,10 @@ header {
}
body {
margin: 0;
- 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-family: var(--default-regular-font, -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: 1rem;
font-weight: 400;
line-height: 1.5;
@@ -66,8 +48,9 @@ a:not([href]):not([class]) {
text-decoration: none;
}
kbd {
- font-family: "Menlo", "DejaVu Sans Mono", "Liberation Mono", "Consolas",
- "Ubuntu Mono", "Courier New", "andale mono", "lucida console", monospace;
+ font-family: var(--default-mono-font, "Menlo"), "DejaVu Sans Mono",
+ "Liberation Mono", "Consolas", "Ubuntu Mono", "Courier New", "andale mono",
+ "lucida console", monospace;
font-size: 1em;
}
img {
@@ -117,7 +100,7 @@ button::-moz-focus-inner,
kbd {
padding: 0.2rem 0.4rem;
font-size: 90%;
- color: #333;
+ color: #333238;
background-color: #ececef;
border-radius: 0.2rem;
}
@@ -142,7 +125,7 @@ kbd kbd {
font-weight: 400;
line-height: 1.5;
color: #ececef;
- background-color: #333;
+ background-color: #333238;
background-clip: padding-box;
border: 1px solid #737278;
border-radius: 0.25rem;
@@ -158,7 +141,7 @@ kbd kbd {
opacity: 1;
}
.form-control:disabled {
- background-color: #333238;
+ background-color: #24232a;
opacity: 1;
}
.form-inline {
@@ -215,7 +198,7 @@ kbd kbd {
color: #ececef;
text-align: left;
list-style: none;
- background-color: #333;
+ background-color: #333238;
background-clip: padding-box;
border: 1px solid rgba(255, 255, 255, 0.15);
border-radius: 0.25rem;
@@ -410,7 +393,7 @@ a.gl-badge.badge-info:active {
background-color: #0b5cad;
}
a.gl-badge.badge-info:active {
- box-shadow: 0 0 0 1px #333, 0 0 0 3px #1f75cb;
+ box-shadow: 0 0 0 1px #333238, 0 0 0 3px #1f75cb;
outline: none;
}
.gl-badge.badge-success {
@@ -423,7 +406,7 @@ a.gl-badge.badge-success:active {
background-color: #24663b;
}
a.gl-badge.badge-success:active {
- box-shadow: 0 0 0 1px #333, 0 0 0 3px #1f75cb;
+ box-shadow: 0 0 0 1px #333238, 0 0 0 3px #1f75cb;
outline: none;
}
.gl-badge.badge-warning {
@@ -436,7 +419,7 @@ a.gl-badge.badge-warning:active {
background-color: #8f4700;
}
a.gl-badge.badge-warning:active {
- box-shadow: 0 0 0 1px #333, 0 0 0 3px #1f75cb;
+ box-shadow: 0 0 0 1px #333238, 0 0 0 3px #1f75cb;
outline: none;
}
.gl-button .gl-badge {
@@ -444,10 +427,11 @@ a.gl-badge.badge-warning:active {
}
.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";
+ background-color: #333238;
+ font-family: var(--default-regular-font, -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;
@@ -526,12 +510,21 @@ a.gl-badge.badge-warning:active {
font-size: 0.875rem;
border-radius: 0.25rem;
}
+.gl-button.gl-button .gl-button-text {
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ padding-top: 1px;
+ padding-bottom: 1px;
+ margin-top: -1px;
+ margin-bottom: -1px;
+}
.gl-button.gl-button.btn-default {
- background-color: #333;
+ background-color: #333238;
}
.gl-button.gl-button.btn-default:active,
.gl-button.gl-button.btn-default.active {
- box-shadow: inset 0 0 0 1px #a4a3a8, 0 0 0 1px #333, 0 0 0 3px #1f75cb;
+ box-shadow: inset 0 0 0 1px #a4a3a8, 0 0 0 1px #333238, 0 0 0 3px #1f75cb;
outline: none;
background-color: #434248;
}
@@ -613,7 +606,7 @@ html {
font-size: 0.875rem;
font-weight: 400;
padding: 6px 10px;
- background-color: #333;
+ background-color: #333238;
border-color: #434248;
color: #ececef;
color: #ececef;
@@ -625,7 +618,7 @@ html {
}
.btn:active,
.btn.active {
- background-color: #444;
+ background-color: #434248;
border-color: #4f4f4f;
color: #ececef;
}
@@ -649,7 +642,7 @@ html {
position: relative;
}
.dropdown-menu-toggle:active {
- box-shadow: 0 0 0 1px #333, 0 0 0 3px #1f75cb;
+ box-shadow: 0 0 0 1px #333238, 0 0 0 3px #1f75cb;
outline: none;
}
.search-input-container .dropdown-menu {
@@ -657,7 +650,7 @@ html {
}
.dropdown-menu-toggle {
padding: 6px 8px 6px 10px;
- background-color: #333;
+ background-color: #333238;
color: #ececef;
font-size: 14px;
text-align: left;
@@ -689,7 +682,7 @@ html {
font-size: 0.875rem;
font-weight: 400;
padding: 8px 0;
- background-color: #333;
+ background-color: #333238;
border: 1px solid #434248;
border-radius: 0.25rem;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
@@ -704,7 +697,7 @@ html {
list-style: none;
}
.dropdown-menu li > a,
-.dropdown-menu li button {
+.dropdown-menu li > button {
background: transparent;
border: 0;
border-radius: 0;
@@ -721,16 +714,16 @@ html {
width: 100%;
}
.dropdown-menu li > a:active,
-.dropdown-menu li button:active {
- background-color: #4f4f4f;
+.dropdown-menu li > button:active {
+ background-color: #4e4c53;
color: #ececef;
outline: 0;
text-decoration: none;
}
.dropdown-menu li > a:active,
-.dropdown-menu li button:active {
- box-shadow: inset 0 0 0 2px #1f75cb, inset 0 0 0 3px #333,
- inset 0 0 0 1px #333;
+.dropdown-menu li > button:active {
+ box-shadow: inset 0 0 0 2px #1f75cb, inset 0 0 0 3px #333238,
+ inset 0 0 0 1px #333238;
outline: none;
}
.dropdown-menu .divider {
@@ -765,7 +758,7 @@ html {
input {
border-radius: 0.25rem;
color: #ececef;
- background-color: #333;
+ background-color: #333238;
}
.form-control {
border-radius: 4px;
@@ -777,10 +770,10 @@ input {
kbd {
display: inline-block;
padding: 3px 5px;
- font-size: 0.6875rem;
+ font-size: 0.75rem;
line-height: 10px;
color: var(--gray-700, #bfbfc3);
- vertical-align: middle;
+ vertical-align: unset;
background-color: var(--gray-10, #1f1e24);
border-width: 1px;
border-style: solid;
@@ -840,6 +833,22 @@ kbd {
.navbar-gitlab .header-content .navbar-collapse > ul.nav > li:not(.d-none) {
margin: 0 2px;
}
+.navbar-gitlab .header-content .header-search-new {
+ max-width: 640px;
+}
+.navbar-gitlab .header-search {
+ min-width: 320px;
+}
+@media (min-width: 768px) and (max-width: 1199.98px) {
+ .navbar-gitlab .header-search {
+ min-width: 200px;
+ }
+}
+.navbar-gitlab .header-search .keyboard-shortcut-helper {
+ transform: translateY(calc(50% - 2px));
+ box-shadow: none;
+ border-color: transparent;
+}
.navbar-gitlab .navbar-collapse {
flex: 0 0 auto;
border-top: 0;
@@ -1010,7 +1019,7 @@ kbd {
float: left;
margin-right: 5px;
border-radius: 50%;
- border: 1px solid #333;
+ border: 1px solid #333238;
}
.notification-dot {
background-color: #9e5400;
@@ -1049,7 +1058,7 @@ kbd {
}
.context-header .avatar-container {
flex: 0 0 32px;
- background-color: #333;
+ background-color: #333238;
}
.context-header .sidebar-context-title {
overflow: hidden;
@@ -1142,7 +1151,7 @@ kbd {
font-weight: 600;
}
.nav-sidebar li.active:not(.fly-out-top-item) > a:not(.has-sub-items) {
- background-color: rgba(255, 255, 255, 0.08);
+ background-color: rgba(251, 250, 253, 0.08);
}
.nav-sidebar ul {
padding-left: 0;
@@ -1189,7 +1198,7 @@ kbd {
margin-bottom: -0.25rem;
margin-top: 0;
position: relative;
- color: #333;
+ color: #333238;
background: var(--black, #fff);
}
.nav-sidebar
@@ -1417,7 +1426,7 @@ kbd {
.close-nav-button {
height: 48px;
padding: 0 16px;
- background-color: #333238;
+ background-color: #24232a;
border: 0;
color: #89888d;
display: flex;
@@ -1523,87 +1532,6 @@ svg.s12 {
svg.s16 {
vertical-align: -3px;
}
-.header-content .header-search-new {
- max-width: 640px;
-}
-.header-search {
- min-width: 320px;
-}
-@media (min-width: 768px) and (max-width: 1199.98px) {
- .header-search {
- min-width: 200px;
- }
-}
-.header-search .keyboard-shortcut-helper {
- transform: translateY(calc(50% - 2px));
- box-shadow: none;
- border-color: transparent;
-}
-.search {
- margin: 0 8px;
-}
-.search form {
- display: block;
- margin: 0;
- padding: 4px;
- width: 200px;
- line-height: 24px;
- height: 32px;
- border: 0;
- border-radius: 4px;
-}
-@media (min-width: 1200px) {
- .search form {
- width: 320px;
- }
-}
-.search .search-input {
- border: 0;
- font-size: 14px;
- padding: 0 20px 0 0;
- margin-left: 5px;
- line-height: 25px;
- width: 98%;
- color: #333;
- background: none;
-}
-.search .search-input-container {
- display: flex;
- position: relative;
-}
-.search .search-input-wrap {
- width: 100%;
-}
-.search .search-input-wrap .search-icon,
-.search .search-input-wrap .clear-icon {
- position: absolute;
- right: 5px;
- top: 4px;
-}
-.search .search-input-wrap .search-icon {
- user-select: none;
-}
-.search .search-input-wrap .clear-icon {
- display: none;
-}
-.search .search-input-wrap .dropdown {
- position: static;
-}
-.search .search-input-wrap .dropdown-menu {
- left: -5px;
- max-height: 400px;
- overflow: auto;
-}
-@media (min-width: 1200px) {
- .search .search-input-wrap .dropdown-menu {
- width: 320px;
- }
-}
-.search .identicon {
- flex-basis: 16px;
- flex-shrink: 0;
- margin-right: 4px;
-}
.avatar,
.avatar-container {
float: left;
@@ -1627,7 +1555,7 @@ svg.s16 {
width: 40px;
height: 40px;
padding: 0;
- background: #222;
+ background: #212027;
overflow: hidden;
box-shadow: inset 0 0 0 1px rgba(251, 250, 253, 0.1);
}
@@ -1705,97 +1633,25 @@ svg.s16 {
}
:root {
color-scheme: dark;
-}
-body.gl-dark {
--gray-10: #1f1e24;
--gray-50: #333238;
--gray-100: #434248;
--gray-200: #535158;
- --gray-300: #626168;
- --gray-400: #737278;
- --gray-500: #89888d;
- --gray-600: #a4a3a8;
--gray-700: #bfbfc3;
- --gray-800: #dcdcde;
--gray-900: #ececef;
- --gray-950: #fbfafd;
- --green-50: #0a4020;
--green-100: #0d532a;
- --green-200: #24663b;
- --green-300: #217645;
- --green-400: #108548;
- --green-500: #2da160;
- --green-600: #52b87a;
--green-700: #91d4a8;
- --green-800: #c3e6cd;
- --green-900: #ecf4ee;
- --green-950: #f1fdf6;
- --blue-50: #033464;
- --blue-100: #064787;
- --blue-200: #0b5cad;
- --blue-300: #1068bf;
- --blue-400: #1f75cb;
- --blue-500: #428fdc;
- --blue-600: #63a6e9;
- --blue-700: #9dc7f1;
- --blue-800: #cbe2f9;
- --blue-900: #e9f3fc;
- --blue-950: #f2f9ff;
- --orange-50: #5c2900;
- --orange-100: #703800;
- --orange-200: #8f4700;
- --orange-300: #9e5400;
- --orange-400: #ab6100;
- --orange-500: #c17d10;
- --orange-600: #d99530;
- --orange-700: #e9be74;
- --orange-800: #f5d9a8;
- --orange-900: #fdf1dd;
- --orange-950: #fff4e1;
- --red-50: #660e00;
- --red-100: #8d1300;
- --red-200: #ae1800;
- --red-300: #c91c00;
- --red-400: #dd2b0e;
- --red-500: #ec5941;
- --red-600: #f57f6c;
- --red-700: #fcb5aa;
- --red-800: #fdd4cd;
- --red-900: #fcf1ef;
- --red-950: #fff4f3;
- --indigo-50: #1a1a40;
- --indigo-100: #292961;
- --indigo-200: #393982;
- --indigo-300: #4b4ba3;
- --indigo-400: #5b5bbd;
- --indigo-500: #6666c4;
- --indigo-600: #7c7ccc;
- --indigo-700: #a6a6de;
- --indigo-800: #d1d1f0;
- --indigo-900: #ebebfa;
- --indigo-950: #f7f7ff;
- --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;
- --dark-icon-color-purple-1: #524a68;
- --dark-icon-color-purple-2: #715bae;
- --dark-icon-color-purple-3: #9a79f7;
- --dark-icon-color-orange-1: #665349;
- --dark-icon-color-orange-2: #b37a5d;
--gl-text-color: #ececef;
- --border-color: #4f4f4f;
- --white: #333;
+ --border-color: #434248;
+ --white: #333238;
+ --black: #fff;
+}
+body.gl-dark {
+ color-scheme: dark;
+ --gray-10: #1f1e24;
+ --border-color: #434248;
+ --white: #333238;
--black: #fff;
- --gray-light: #333238;
- --svg-status-bg: #333;
}
.nav-sidebar,
.toggle-sidebar-button,
@@ -1830,7 +1686,7 @@ body.gl-dark .navbar-gitlab .navbar-sub-nav > li.active > button,
body.gl-dark .navbar-gitlab .navbar-nav > li.active > a,
body.gl-dark .navbar-gitlab .navbar-nav > li.active > button {
color: #ececef;
- background-color: #333;
+ background-color: #333238;
}
body.gl-dark .navbar-gitlab .navbar-sub-nav {
color: #ececef;
@@ -1862,10 +1718,10 @@ body.gl-dark
}
body.gl-dark .navbar-gitlab .nav > li.active > a {
color: #ececef;
- background-color: #333;
+ background-color: #333238;
}
body.gl-dark .navbar-gitlab .nav > li.active > a .notification-dot {
- border-color: #333;
+ border-color: #333238;
}
body.gl-dark
.navbar-gitlab
@@ -1947,100 +1803,6 @@ body.gl-dark .navbar-gitlab .search form .search-input {
color: var(--gl-text-color);
}
-:root {
- color-scheme: dark;
-}
-body.gl-dark {
- --gray-10: #1f1e24;
- --gray-50: #333238;
- --gray-100: #434248;
- --gray-200: #535158;
- --gray-300: #626168;
- --gray-400: #737278;
- --gray-500: #89888d;
- --gray-600: #a4a3a8;
- --gray-700: #bfbfc3;
- --gray-800: #dcdcde;
- --gray-900: #ececef;
- --gray-950: #fbfafd;
- --green-50: #0a4020;
- --green-100: #0d532a;
- --green-200: #24663b;
- --green-300: #217645;
- --green-400: #108548;
- --green-500: #2da160;
- --green-600: #52b87a;
- --green-700: #91d4a8;
- --green-800: #c3e6cd;
- --green-900: #ecf4ee;
- --green-950: #f1fdf6;
- --blue-50: #033464;
- --blue-100: #064787;
- --blue-200: #0b5cad;
- --blue-300: #1068bf;
- --blue-400: #1f75cb;
- --blue-500: #428fdc;
- --blue-600: #63a6e9;
- --blue-700: #9dc7f1;
- --blue-800: #cbe2f9;
- --blue-900: #e9f3fc;
- --blue-950: #f2f9ff;
- --orange-50: #5c2900;
- --orange-100: #703800;
- --orange-200: #8f4700;
- --orange-300: #9e5400;
- --orange-400: #ab6100;
- --orange-500: #c17d10;
- --orange-600: #d99530;
- --orange-700: #e9be74;
- --orange-800: #f5d9a8;
- --orange-900: #fdf1dd;
- --orange-950: #fff4e1;
- --red-50: #660e00;
- --red-100: #8d1300;
- --red-200: #ae1800;
- --red-300: #c91c00;
- --red-400: #dd2b0e;
- --red-500: #ec5941;
- --red-600: #f57f6c;
- --red-700: #fcb5aa;
- --red-800: #fdd4cd;
- --red-900: #fcf1ef;
- --red-950: #fff4f3;
- --indigo-50: #1a1a40;
- --indigo-100: #292961;
- --indigo-200: #393982;
- --indigo-300: #4b4ba3;
- --indigo-400: #5b5bbd;
- --indigo-500: #6666c4;
- --indigo-600: #7c7ccc;
- --indigo-700: #a6a6de;
- --indigo-800: #d1d1f0;
- --indigo-900: #ebebfa;
- --indigo-950: #f7f7ff;
- --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;
- --dark-icon-color-purple-1: #524a68;
- --dark-icon-color-purple-2: #715bae;
- --dark-icon-color-purple-3: #9a79f7;
- --dark-icon-color-orange-1: #665349;
- --dark-icon-color-orange-2: #b37a5d;
- --gl-text-color: #ececef;
- --border-color: #4f4f4f;
- --white: #333;
- --black: #fff;
- --gray-light: #333238;
- --svg-status-bg: #333;
-}
.tab-width-8 {
tab-size: 8;
}
diff --git a/app/assets/stylesheets/startup/startup-general.scss b/app/assets/stylesheets/startup/startup-general.scss
index 7fb373bb6f4..f24b6fb9e81 100644
--- a/app/assets/stylesheets/startup/startup-general.scss
+++ b/app/assets/stylesheets/startup/startup-general.scss
@@ -17,9 +17,10 @@ header {
}
body {
margin: 0;
- 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-family: var(--default-regular-font, -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: 1rem;
font-weight: 400;
line-height: 1.5;
@@ -47,8 +48,9 @@ a:not([href]):not([class]) {
text-decoration: none;
}
kbd {
- font-family: "Menlo", "DejaVu Sans Mono", "Liberation Mono", "Consolas",
- "Ubuntu Mono", "Courier New", "andale mono", "lucida console", monospace;
+ font-family: var(--default-mono-font, "Menlo"), "DejaVu Sans Mono",
+ "Liberation Mono", "Consolas", "Ubuntu Mono", "Courier New", "andale mono",
+ "lucida console", monospace;
font-size: 1em;
}
img {
@@ -426,9 +428,10 @@ a.gl-badge.badge-warning:active {
.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-family: var(--default-regular-font, -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;
@@ -507,6 +510,15 @@ a.gl-badge.badge-warning:active {
font-size: 0.875rem;
border-radius: 0.25rem;
}
+.gl-button.gl-button .gl-button-text {
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ padding-top: 1px;
+ padding-bottom: 1px;
+ margin-top: -1px;
+ margin-bottom: -1px;
+}
.gl-button.gl-button.btn-default {
background-color: #fff;
}
@@ -606,8 +618,8 @@ html {
}
.btn:active,
.btn.active {
- background-color: #eaeaea;
- border-color: #e3e3e3;
+ background-color: #e6e6ea;
+ border-color: #dedee3;
color: #333238;
}
.btn svg {
@@ -685,7 +697,7 @@ html {
list-style: none;
}
.dropdown-menu li > a,
-.dropdown-menu li button {
+.dropdown-menu li > button {
background: transparent;
border: 0;
border-radius: 0;
@@ -702,14 +714,14 @@ html {
width: 100%;
}
.dropdown-menu li > a:active,
-.dropdown-menu li button:active {
+.dropdown-menu li > button:active {
background-color: #ececef;
color: #333238;
outline: 0;
text-decoration: none;
}
.dropdown-menu li > a:active,
-.dropdown-menu li button:active {
+.dropdown-menu li > button:active {
box-shadow: inset 0 0 0 2px #428fdc, inset 0 0 0 3px #fff,
inset 0 0 0 1px #fff;
outline: none;
@@ -758,10 +770,10 @@ input {
kbd {
display: inline-block;
padding: 3px 5px;
- font-size: 0.6875rem;
+ font-size: 0.75rem;
line-height: 10px;
color: var(--gray-700, #535158);
- vertical-align: middle;
+ vertical-align: unset;
background-color: var(--gray-10, #fbfafd);
border-width: 1px;
border-style: solid;
@@ -821,6 +833,22 @@ kbd {
.navbar-gitlab .header-content .navbar-collapse > ul.nav > li:not(.d-none) {
margin: 0 2px;
}
+.navbar-gitlab .header-content .header-search-new {
+ max-width: 640px;
+}
+.navbar-gitlab .header-search {
+ min-width: 320px;
+}
+@media (min-width: 768px) and (max-width: 1199.98px) {
+ .navbar-gitlab .header-search {
+ min-width: 200px;
+ }
+}
+.navbar-gitlab .header-search .keyboard-shortcut-helper {
+ transform: translateY(calc(50% - 2px));
+ box-shadow: none;
+ border-color: transparent;
+}
.navbar-gitlab .navbar-collapse {
flex: 0 0 auto;
border-top: 0;
@@ -1123,7 +1151,7 @@ kbd {
font-weight: 600;
}
.nav-sidebar li.active:not(.fly-out-top-item) > a:not(.has-sub-items) {
- background-color: rgba(0, 0, 0, 0.08);
+ background-color: rgba(31, 30, 36, 0.08);
}
.nav-sidebar ul {
padding-left: 0;
@@ -1504,87 +1532,6 @@ svg.s12 {
svg.s16 {
vertical-align: -3px;
}
-.header-content .header-search-new {
- max-width: 640px;
-}
-.header-search {
- min-width: 320px;
-}
-@media (min-width: 768px) and (max-width: 1199.98px) {
- .header-search {
- min-width: 200px;
- }
-}
-.header-search .keyboard-shortcut-helper {
- transform: translateY(calc(50% - 2px));
- box-shadow: none;
- border-color: transparent;
-}
-.search {
- margin: 0 8px;
-}
-.search form {
- display: block;
- margin: 0;
- padding: 4px;
- width: 200px;
- line-height: 24px;
- height: 32px;
- border: 0;
- border-radius: 4px;
-}
-@media (min-width: 1200px) {
- .search form {
- width: 320px;
- }
-}
-.search .search-input {
- border: 0;
- font-size: 14px;
- padding: 0 20px 0 0;
- margin-left: 5px;
- line-height: 25px;
- width: 98%;
- color: #fff;
- background: none;
-}
-.search .search-input-container {
- display: flex;
- position: relative;
-}
-.search .search-input-wrap {
- width: 100%;
-}
-.search .search-input-wrap .search-icon,
-.search .search-input-wrap .clear-icon {
- position: absolute;
- right: 5px;
- top: 4px;
-}
-.search .search-input-wrap .search-icon {
- user-select: none;
-}
-.search .search-input-wrap .clear-icon {
- display: none;
-}
-.search .search-input-wrap .dropdown {
- position: static;
-}
-.search .search-input-wrap .dropdown-menu {
- left: -5px;
- max-height: 400px;
- overflow: auto;
-}
-@media (min-width: 1200px) {
- .search .search-input-wrap .dropdown-menu {
- width: 320px;
- }
-}
-.search .identicon {
- flex-basis: 16px;
- flex-shrink: 0;
- margin-right: 4px;
-}
.avatar,
.avatar-container {
float: left;
diff --git a/app/assets/stylesheets/startup/startup-signin.scss b/app/assets/stylesheets/startup/startup-signin.scss
index 7ae158b3930..d8afff1a200 100644
--- a/app/assets/stylesheets/startup/startup-signin.scss
+++ b/app/assets/stylesheets/startup/startup-signin.scss
@@ -16,9 +16,10 @@ header {
}
body {
margin: 0;
- 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-family: var(--default-regular-font, -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: 1rem;
font-weight: 400;
line-height: 1.5;
@@ -390,9 +391,10 @@ input.btn-block[type="button"] {
.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-family: var(--default-regular-font, -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;
@@ -647,8 +649,8 @@ body.navless {
}
.btn:active,
.btn.active {
- background-color: #eaeaea;
- border-color: #e3e3e3;
+ background-color: #e6e6ea;
+ border-color: #dedee3;
color: #333238;
}
.btn svg {
@@ -954,7 +956,7 @@ svg {
}
.devise-layout-html body .footer-container,
.devise-layout-html body hr.footer-fixed {
- position: absolute;
+ position: fixed;
bottom: 0;
left: 0;
right: 0;
@@ -988,6 +990,9 @@ svg {
.gl-justify-content-center {
justify-content: center;
}
+.gl-justify-content-space-between {
+ justify-content: space-between;
+}
.gl-float-right {
float: right;
}
diff --git a/app/assets/stylesheets/themes/_dark.scss b/app/assets/stylesheets/themes/_dark.scss
index a3474d2ed50..8db91fd9908 100644
--- a/app/assets/stylesheets/themes/_dark.scss
+++ b/app/assets/stylesheets/themes/_dark.scss
@@ -11,6 +11,20 @@ $gray-800: #dcdcde;
$gray-900: #ececef;
$gray-950: #fbfafd;
+$gray-lightest: lighten($gray-10, 1);
+$gray-light: lighten($gray-10, 2);
+$gray-lighter: darken($gray-50, 4);
+$gray-normal: $gray-50;
+$gray-dark: darken($gray-100, 2);
+$gray-darker: darken($gray-200, 2);
+$gray-darkest: $gray-700;
+
+$black: #fff;
+$black-normal: $gray-900;
+$white: $gray-50;
+$white-normal: $gray-50;
+$white-dark: $gray-100;
+
$green-50: #0a4020;
$green-100: #0d532a;
$green-200: #24663b;
@@ -83,159 +97,21 @@ $purple-800: #cbbbf2;
$purple-900: #e1d8f9;
$purple-950: #f4f0ff;
-$gray-lightest: #222;
-$gray-light: $gray-50;
-$gray-lighter: #303030;
-$gray-normal: #333;
-$gray-dark: $gray-100;
-$gray-darker: #4f4f4f;
-$gray-darkest: #c4c4c4;
-
-$black: #fff;
-$black-normal: $gray-900;
-$white: #333;
-$white-light: #2b2b2b;
-$white-normal: #333;
-$white-dark: #444;
-
$theme-indigo-50: #1a1a40;
$border-color: #4f4f4f;
-:root {
- color-scheme: dark;
-}
-
-body.gl-dark {
- --gray-10: #{$gray-10};
- --gray-50: #{$gray-50};
- --gray-100: #{$gray-100};
- --gray-200: #{$gray-200};
- --gray-300: #{$gray-300};
- --gray-400: #{$gray-400};
- --gray-500: #{$gray-500};
- --gray-600: #{$gray-600};
- --gray-700: #{$gray-700};
- --gray-800: #{$gray-800};
- --gray-900: #{$gray-900};
- --gray-950: #{$gray-950};
-
- --green-50: #{$green-50};
- --green-100: #{$green-100};
- --green-200: #{$green-200};
- --green-300: #{$green-300};
- --green-400: #{$green-400};
- --green-500: #{$green-500};
- --green-600: #{$green-600};
- --green-700: #{$green-700};
- --green-800: #{$green-800};
- --green-900: #{$green-900};
- --green-950: #{$green-950};
-
- --blue-50: #{$blue-50};
- --blue-100: #{$blue-100};
- --blue-200: #{$blue-200};
- --blue-300: #{$blue-300};
- --blue-400: #{$blue-400};
- --blue-500: #{$blue-500};
- --blue-600: #{$blue-600};
- --blue-700: #{$blue-700};
- --blue-800: #{$blue-800};
- --blue-900: #{$blue-900};
- --blue-950: #{$blue-950};
-
- --orange-50: #{$orange-50};
- --orange-100: #{$orange-100};
- --orange-200: #{$orange-200};
- --orange-300: #{$orange-300};
- --orange-400: #{$orange-400};
- --orange-500: #{$orange-500};
- --orange-600: #{$orange-600};
- --orange-700: #{$orange-700};
- --orange-800: #{$orange-800};
- --orange-900: #{$orange-900};
- --orange-950: #{$orange-950};
-
- --red-50: #{$red-50};
- --red-100: #{$red-100};
- --red-200: #{$red-200};
- --red-300: #{$red-300};
- --red-400: #{$red-400};
- --red-500: #{$red-500};
- --red-600: #{$red-600};
- --red-700: #{$red-700};
- --red-800: #{$red-800};
- --red-900: #{$red-900};
- --red-950: #{$red-950};
-
- --indigo-50: #{$indigo-50};
- --indigo-100: #{$indigo-100};
- --indigo-200: #{$indigo-200};
- --indigo-300: #{$indigo-300};
- --indigo-400: #{$indigo-400};
- --indigo-500: #{$indigo-500};
- --indigo-600: #{$indigo-600};
- --indigo-700: #{$indigo-700};
- --indigo-800: #{$indigo-800};
- --indigo-900: #{$indigo-900};
- --indigo-950: #{$indigo-950};
-
- --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};
-
- --dark-icon-color-purple-1: #524a68;
- --dark-icon-color-purple-2: #715bae;
- --dark-icon-color-purple-3: #9a79f7;
- --dark-icon-color-orange-1: #665349;
- --dark-icon-color-orange-2: #b37a5d;
-
- --gl-text-color: #{$gray-900};
- --border-color: #{$border-color};
-
- --white: #{$white};
- --black: #{$black};
- --gray-light: #{$gray-50};
-
- --svg-status-bg: #{$white};
-
- .gl-button.gl-button,
- .gl-button.gl-button.btn-block {
- &.btn-default,
- &.btn-dashed,
- &.btn-info,
- &.btn-success,
- &.btn-danger,
- &.btn-confirm {
- &-tertiary {
- mix-blend-mode: screen;
- }
- }
- }
-
- .gl-datepicker-theme {
- .pika-prev,
- .pika-next {
- filter: invert(0.9);
- }
-
- .is-selected > .pika-button {
- color: $gray-900;
- }
-
- :not(.is-selected) > .pika-button:hover {
- background-color: $gray-200;
- }
- }
-}
+$data-viz-blue-50: #2a2b59;
+$data-viz-blue-100: #303470;
+$data-viz-blue-200: #374291;
+$data-viz-blue-300: #3f51ae;
+$data-viz-blue-400: #4e65cd;
+$data-viz-blue-500: #617ae2;
+$data-viz-blue-600: #7992f5;
+$data-viz-blue-700: #97acff;
+$data-viz-blue-800: #b7c6ff;
+$data-viz-blue-900: #d2dcff;
+$data-viz-blue-950: #e9ebff;
$border-white-normal: $border-color;
@@ -265,11 +141,3 @@ $line-removed-dark: $red-200;
$well-expand-item: $gray-200;
$well-inner-border: $gray-200;
-
-$calendar-activity-colors: (
- #404040,
- #1e23a8,
- #445cf2,
- #97acff,
- #e9ebff
-);
diff --git a/app/assets/stylesheets/themes/dark_mode_overrides.scss b/app/assets/stylesheets/themes/dark_mode_overrides.scss
index a0d19c3de2a..bb97261a1ca 100644
--- a/app/assets/stylesheets/themes/dark_mode_overrides.scss
+++ b/app/assets/stylesheets/themes/dark_mode_overrides.scss
@@ -2,6 +2,149 @@
@import 'page_bundles/mixins_and_variables_and_functions';
@import './themes/theme_helper';
+:root {
+ color-scheme: dark;
+ --gray-10: #{$gray-10};
+ --gray-50: #{$gray-50};
+ --gray-100: #{$gray-100};
+ --gray-200: #{$gray-200};
+ --gray-300: #{$gray-300};
+ --gray-400: #{$gray-400};
+ --gray-500: #{$gray-500};
+ --gray-600: #{$gray-600};
+ --gray-700: #{$gray-700};
+ --gray-800: #{$gray-800};
+ --gray-900: #{$gray-900};
+ --gray-950: #{$gray-950};
+
+ --green-50: #{$green-50};
+ --green-100: #{$green-100};
+ --green-200: #{$green-200};
+ --green-300: #{$green-300};
+ --green-400: #{$green-400};
+ --green-500: #{$green-500};
+ --green-600: #{$green-600};
+ --green-700: #{$green-700};
+ --green-800: #{$green-800};
+ --green-900: #{$green-900};
+ --green-950: #{$green-950};
+
+ --blue-50: #{$blue-50};
+ --blue-100: #{$blue-100};
+ --blue-200: #{$blue-200};
+ --blue-300: #{$blue-300};
+ --blue-400: #{$blue-400};
+ --blue-500: #{$blue-500};
+ --blue-600: #{$blue-600};
+ --blue-700: #{$blue-700};
+ --blue-800: #{$blue-800};
+ --blue-900: #{$blue-900};
+ --blue-950: #{$blue-950};
+
+ --orange-50: #{$orange-50};
+ --orange-100: #{$orange-100};
+ --orange-200: #{$orange-200};
+ --orange-300: #{$orange-300};
+ --orange-400: #{$orange-400};
+ --orange-500: #{$orange-500};
+ --orange-600: #{$orange-600};
+ --orange-700: #{$orange-700};
+ --orange-800: #{$orange-800};
+ --orange-900: #{$orange-900};
+ --orange-950: #{$orange-950};
+
+ --red-50: #{$red-50};
+ --red-100: #{$red-100};
+ --red-200: #{$red-200};
+ --red-300: #{$red-300};
+ --red-400: #{$red-400};
+ --red-500: #{$red-500};
+ --red-600: #{$red-600};
+ --red-700: #{$red-700};
+ --red-800: #{$red-800};
+ --red-900: #{$red-900};
+ --red-950: #{$red-950};
+
+ --indigo-50: #{$indigo-50};
+ --indigo-100: #{$indigo-100};
+ --indigo-200: #{$indigo-200};
+ --indigo-300: #{$indigo-300};
+ --indigo-400: #{$indigo-400};
+ --indigo-500: #{$indigo-500};
+ --indigo-600: #{$indigo-600};
+ --indigo-700: #{$indigo-700};
+ --indigo-800: #{$indigo-800};
+ --indigo-900: #{$indigo-900};
+ --indigo-950: #{$indigo-950};
+
+ --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};
+
+ --dark-icon-color-purple-1: #524a68;
+ --dark-icon-color-purple-2: #715bae;
+ --dark-icon-color-purple-3: #9a79f7;
+ --dark-icon-color-orange-1: #665349;
+ --dark-icon-color-orange-2: #b37a5d;
+
+ --gl-text-color: #{$gray-900};
+ --border-color: #{$border-color};
+
+ --white: #{$white};
+ --black: #{$black};
+ --gray-light: #{$gray-50};
+
+ --svg-status-bg: #{$white};
+}
+
+body.gl-dark {
+ // redefine some colors and values to prevent sourcegraph conflicts
+ color-scheme: dark;
+ --gray-10: #{$gray-10};
+ --border-color: #{$border-color};
+ --white: #{$white};
+ --black: #{$black};
+}
+
+.gl-dark {
+ .gl-button.gl-button,
+ .gl-button.gl-button.btn-block {
+ &.btn-default,
+ &.btn-dashed,
+ &.btn-info,
+ &.btn-success,
+ &.btn-danger,
+ &.btn-confirm {
+ &-tertiary {
+ mix-blend-mode: screen;
+ }
+ }
+ }
+
+ .gl-datepicker-theme {
+ .pika-prev,
+ .pika-next {
+ filter: invert(0.9);
+ }
+
+ .is-selected > .pika-button {
+ color: $gray-900;
+ }
+
+ :not(.is-selected) > .pika-button:hover {
+ background-color: $gray-200;
+ }
+ }
+}
+
// Some hacks and overrides for things that don't properly support dark mode
.gl-label {
filter: brightness(0.9) contrast(1.1);
diff --git a/app/assets/stylesheets/utilities.scss b/app/assets/stylesheets/utilities.scss
index 4be4fc82d04..714dd932147 100644
--- a/app/assets/stylesheets/utilities.scss
+++ b/app/assets/stylesheets/utilities.scss
@@ -236,6 +236,13 @@ to @gitlab/ui by https://gitlab.com/gitlab-org/gitlab-ui/-/issues/1709
}
}
+// TODO: Remove once https: //gitlab.com/gitlab-org/gitlab-ui/-/merge_requests/3198 is merged
+.gl-sm-ml-5 {
+ @include gl-media-breakpoint-up(sm) {
+ @include gl-ml-5;
+ }
+}
+
/* End gitlab-ui#1709 */
/*
@@ -251,3 +258,8 @@ to @gitlab/ui by https://gitlab.com/gitlab-org/gitlab-ui/-/issues/1709
.gl-flex-flow-row-wrap {
flex-flow: row wrap;
}
+
+// Will be moved to @gitlab/ui by https://gitlab.com/gitlab-org/gitlab-ui/-/issues/2098
+.gl-max-w-0 {
+ max-width: 0;
+}