Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
Diffstat (limited to 'app/assets/javascripts')
-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
843 files changed, 11665 insertions, 5552 deletions
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;
+}