summaryrefslogtreecommitdiff
path: root/app/assets/javascripts
diff options
context:
space:
mode:
Diffstat (limited to 'app/assets/javascripts')
-rw-r--r--app/assets/javascripts/add_context_commits_modal/components/add_context_commits_modal_wrapper.vue5
-rw-r--r--app/assets/javascripts/admin/statistics_panel/components/app.vue4
-rw-r--r--app/assets/javascripts/admin/users/components/usage_ping_disabled.vue (renamed from app/assets/javascripts/admin/cohorts/components/usage_ping_disabled.vue)0
-rw-r--r--app/assets/javascripts/admin/users/components/user_actions.vue115
-rw-r--r--app/assets/javascripts/admin/users/components/user_avatar.vue15
-rw-r--r--app/assets/javascripts/admin/users/components/user_date.vue29
-rw-r--r--app/assets/javascripts/admin/users/components/users_table.vue18
-rw-r--r--app/assets/javascripts/admin/users/constants.js4
-rw-r--r--app/assets/javascripts/admin/users/index.js25
-rw-r--r--app/assets/javascripts/admin/users/tabs.js23
-rw-r--r--app/assets/javascripts/admin/users/utils.js7
-rw-r--r--app/assets/javascripts/alert_management/components/alert_management_table.vue10
-rw-r--r--app/assets/javascripts/alert_management/constants.js31
-rw-r--r--app/assets/javascripts/alert_management/list.js2
-rw-r--r--app/assets/javascripts/alerts_settings/components/alert_mapping_builder.vue75
-rw-r--r--app/assets/javascripts/alerts_settings/components/alerts_settings_form.vue43
-rw-r--r--app/assets/javascripts/alerts_settings/components/alerts_settings_wrapper.vue12
-rw-r--r--app/assets/javascripts/alerts_settings/components/mocks/gitlabFields.json112
-rw-r--r--app/assets/javascripts/alerts_settings/components/mocks/parsedMapping.json78
-rw-r--r--app/assets/javascripts/alerts_settings/graphql/mutations/create_http_integration.mutation.graphql18
-rw-r--r--app/assets/javascripts/alerts_settings/index.js10
-rw-r--r--app/assets/javascripts/alerts_settings/utils/mapping_transformations.js61
-rw-r--r--app/assets/javascripts/analytics/instance_statistics/components/app.vue2
-rw-r--r--app/assets/javascripts/api.js44
-rw-r--r--app/assets/javascripts/api/user_api.js4
-rw-r--r--app/assets/javascripts/awards_handler.js4
-rw-r--r--app/assets/javascripts/badges/components/badge_form.vue4
-rw-r--r--app/assets/javascripts/badges/components/badge_list.vue2
-rw-r--r--app/assets/javascripts/badges/store/actions.js2
-rw-r--r--app/assets/javascripts/badges/store/mutations.js2
-rw-r--r--app/assets/javascripts/batch_comments/components/draft_note.vue3
-rw-r--r--app/assets/javascripts/batch_comments/components/preview_item.vue2
-rw-r--r--app/assets/javascripts/batch_comments/stores/modules/batch_comments/actions.js26
-rw-r--r--app/assets/javascripts/behaviors/autosize.js12
-rw-r--r--app/assets/javascripts/behaviors/index.js30
-rw-r--r--app/assets/javascripts/behaviors/markdown/nodes/table_header_row.js2
-rw-r--r--app/assets/javascripts/behaviors/markdown/render_gfm.js2
-rw-r--r--app/assets/javascripts/behaviors/markdown/render_mermaid.js2
-rw-r--r--app/assets/javascripts/behaviors/quick_submit.js2
-rw-r--r--app/assets/javascripts/behaviors/shortcuts/keybindings.js7
-rw-r--r--app/assets/javascripts/behaviors/shortcuts/shortcuts.js17
-rw-r--r--app/assets/javascripts/behaviors/shortcuts/shortcuts_issuable.js6
-rw-r--r--app/assets/javascripts/behaviors/shortcuts/shortcuts_wiki.js2
-rw-r--r--app/assets/javascripts/blob/balsamiq_viewer.js2
-rw-r--r--app/assets/javascripts/blob/blob_file_dropzone.js2
-rw-r--r--app/assets/javascripts/blob/components/blob_header.vue2
-rw-r--r--app/assets/javascripts/blob/template_selectors/ci_syntax_yaml_selector.js2
-rw-r--r--app/assets/javascripts/blob/template_selectors/ci_yaml_selector.js2
-rw-r--r--app/assets/javascripts/blob/template_selectors/dockerfile_selector.js2
-rw-r--r--app/assets/javascripts/blob/template_selectors/gitignore_selector.js2
-rw-r--r--app/assets/javascripts/blob/template_selectors/license_selector.js2
-rw-r--r--app/assets/javascripts/blob/template_selectors/metrics_dashboard_selector.js2
-rw-r--r--app/assets/javascripts/blob/template_selectors/type_selector.js2
-rw-r--r--app/assets/javascripts/blob/viewer/index.js4
-rw-r--r--app/assets/javascripts/blob_edit/blob_bundle.js4
-rw-r--r--app/assets/javascripts/blob_edit/edit_blob.js4
-rw-r--r--app/assets/javascripts/boards/boards_util.js14
-rw-r--r--app/assets/javascripts/boards/components/board_add_new_column_trigger.vue21
-rw-r--r--app/assets/javascripts/boards/components/board_assignee_dropdown.vue56
-rw-r--r--app/assets/javascripts/boards/components/board_card.vue7
-rw-r--r--app/assets/javascripts/boards/components/board_card_layout.vue26
-rw-r--r--app/assets/javascripts/boards/components/board_card_layout_deprecated.vue102
-rw-r--r--app/assets/javascripts/boards/components/board_column.vue2
-rw-r--r--app/assets/javascripts/boards/components/board_column_deprecated.vue3
-rw-r--r--app/assets/javascripts/boards/components/board_configuration_options.vue6
-rw-r--r--app/assets/javascripts/boards/components/board_content.vue4
-rw-r--r--app/assets/javascripts/boards/components/board_form.vue3
-rw-r--r--app/assets/javascripts/boards/components/board_list.vue4
-rw-r--r--app/assets/javascripts/boards/components/board_list_deprecated.vue16
-rw-r--r--app/assets/javascripts/boards/components/board_list_header.vue39
-rw-r--r--app/assets/javascripts/boards/components/board_list_header_deprecated.vue35
-rw-r--r--app/assets/javascripts/boards/components/board_new_issue.vue4
-rw-r--r--app/assets/javascripts/boards/components/board_new_issue_deprecated.vue4
-rw-r--r--app/assets/javascripts/boards/components/board_settings_sidebar.vue15
-rw-r--r--app/assets/javascripts/boards/components/board_sidebar.js9
-rw-r--r--app/assets/javascripts/boards/components/issue_card_inner.vue8
-rw-r--r--app/assets/javascripts/boards/components/issue_card_inner_deprecated.vue4
-rw-r--r--app/assets/javascripts/boards/components/modal/footer.vue2
-rw-r--r--app/assets/javascripts/boards/components/modal/header.vue4
-rw-r--r--app/assets/javascripts/boards/components/modal/index.vue2
-rw-r--r--app/assets/javascripts/boards/components/new_list_dropdown.js35
-rw-r--r--app/assets/javascripts/boards/components/project_select_deprecated.vue4
-rw-r--r--app/assets/javascripts/boards/components/sidebar/board_sidebar_due_date.vue16
-rw-r--r--app/assets/javascripts/boards/components/sidebar/board_sidebar_issue_title.vue56
-rw-r--r--app/assets/javascripts/boards/components/sidebar/board_sidebar_milestone_select.vue73
-rw-r--r--app/assets/javascripts/boards/components/toggle_focus.vue52
-rw-r--r--app/assets/javascripts/boards/constants.js12
-rw-r--r--app/assets/javascripts/boards/filtered_search_boards.js2
-rw-r--r--app/assets/javascripts/boards/graphql/project_milestones.query.graphql (renamed from app/assets/javascripts/boards/graphql/group_milestones.query.graphql)6
-rw-r--r--app/assets/javascripts/boards/index.js21
-rw-r--r--app/assets/javascripts/boards/models/issue.js2
-rw-r--r--app/assets/javascripts/boards/models/iteration.js9
-rw-r--r--app/assets/javascripts/boards/models/list.js8
-rw-r--r--app/assets/javascripts/boards/stores/actions.js33
-rw-r--r--app/assets/javascripts/boards/stores/getters.js7
-rw-r--r--app/assets/javascripts/boards/stores/mutation_types.js5
-rw-r--r--app/assets/javascripts/boards/stores/mutations.js24
-rw-r--r--app/assets/javascripts/boards/stores/state.js3
-rw-r--r--app/assets/javascripts/boards/toggle_focus.js48
-rw-r--r--app/assets/javascripts/branches/components/divergence_graph.vue2
-rw-r--r--app/assets/javascripts/build_artifacts.js2
-rw-r--r--app/assets/javascripts/captcha/init_recaptcha_script.js48
-rw-r--r--app/assets/javascripts/ci_settings_pipeline_triggers/index.js2
-rw-r--r--app/assets/javascripts/ci_variable_list/components/ci_variable_popover.vue4
-rw-r--r--app/assets/javascripts/ci_variable_list/index.js2
-rw-r--r--app/assets/javascripts/ci_variable_list/store/actions.js2
-rw-r--r--app/assets/javascripts/ci_variable_list/store/mutations.js2
-rw-r--r--app/assets/javascripts/clusters/clusters_bundle.js4
-rw-r--r--app/assets/javascripts/clusters/components/application_row.vue3
-rw-r--r--app/assets/javascripts/clusters/components/applications.vue10
-rw-r--r--app/assets/javascripts/clusters/components/ingress_modsecurity_settings.vue4
-rw-r--r--app/assets/javascripts/clusters/components/knative_domain_editor.vue2
-rw-r--r--app/assets/javascripts/clusters/components/remove_cluster_confirmation.vue82
-rw-r--r--app/assets/javascripts/clusters_list/components/clusters.vue4
-rw-r--r--app/assets/javascripts/clusters_list/store/actions.js2
-rw-r--r--app/assets/javascripts/code_navigation/components/app.vue2
-rw-r--r--app/assets/javascripts/code_navigation/store/actions.js2
-rw-r--r--app/assets/javascripts/commit/pipelines/pipelines_table.vue2
-rw-r--r--app/assets/javascripts/compare_autocomplete.js4
-rw-r--r--app/assets/javascripts/contributors/components/contributors.vue2
-rw-r--r--app/assets/javascripts/create_cluster/eks_cluster/components/service_credentials_form.vue14
-rw-r--r--app/assets/javascripts/create_cluster/eks_cluster/store/actions.js6
-rw-r--r--app/assets/javascripts/create_cluster/eks_cluster/store/index.js10
-rw-r--r--app/assets/javascripts/create_cluster/gke_cluster/store/actions.js2
-rw-r--r--app/assets/javascripts/create_cluster/init_create_cluster.js2
-rw-r--r--app/assets/javascripts/custom_metrics/components/custom_metrics_form.vue2
-rw-r--r--app/assets/javascripts/cycle_analytics/components/banner.vue2
-rw-r--r--app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js6
-rw-r--r--app/assets/javascripts/deploy_freeze/store/actions.js2
-rw-r--r--app/assets/javascripts/deploy_keys/components/key.vue2
-rw-r--r--app/assets/javascripts/deploy_keys/components/keys_panel.vue2
-rw-r--r--app/assets/javascripts/design_management/components/design_notes/design_discussion.vue6
-rw-r--r--app/assets/javascripts/design_management/components/design_notes/design_note.vue4
-rw-r--r--app/assets/javascripts/design_management/components/design_overlay.vue2
-rw-r--r--app/assets/javascripts/design_management/components/design_sidebar.vue4
-rw-r--r--app/assets/javascripts/design_management/components/design_todo_button.vue2
-rw-r--r--app/assets/javascripts/design_management/components/list/item.vue21
-rw-r--r--app/assets/javascripts/design_management/components/toolbar/index.vue2
-rw-r--r--app/assets/javascripts/design_management/mixins/all_designs.js2
-rw-r--r--app/assets/javascripts/design_management/pages/index.vue2
-rw-r--r--app/assets/javascripts/design_management/utils/error_messages.js8
-rw-r--r--app/assets/javascripts/diffs/components/app.vue31
-rw-r--r--app/assets/javascripts/diffs/components/compare_versions.vue4
-rw-r--r--app/assets/javascripts/diffs/components/diff_content.vue12
-rw-r--r--app/assets/javascripts/diffs/components/diff_discussion_reply.vue9
-rw-r--r--app/assets/javascripts/diffs/components/diff_file.vue58
-rw-r--r--app/assets/javascripts/diffs/components/diff_file_header.vue70
-rw-r--r--app/assets/javascripts/diffs/components/diff_line_note_form.vue10
-rw-r--r--app/assets/javascripts/diffs/components/diff_row.vue21
-rw-r--r--app/assets/javascripts/diffs/components/diff_row_utils.js4
-rw-r--r--app/assets/javascripts/diffs/components/diff_view.vue24
-rw-r--r--app/assets/javascripts/diffs/components/image_diff_overlay.vue2
-rw-r--r--app/assets/javascripts/diffs/components/inline_diff_view.vue2
-rw-r--r--app/assets/javascripts/diffs/components/parallel_diff_view.vue2
-rw-r--r--app/assets/javascripts/diffs/i18n.js7
-rw-r--r--app/assets/javascripts/diffs/store/actions.js30
-rw-r--r--app/assets/javascripts/diffs/store/getters.js7
-rw-r--r--app/assets/javascripts/diffs/store/modules/diff_state.js1
-rw-r--r--app/assets/javascripts/diffs/store/mutations.js17
-rw-r--r--app/assets/javascripts/diffs/utils/diff_file.js6
-rw-r--r--app/assets/javascripts/diffs/utils/file_reviews.js25
-rw-r--r--app/assets/javascripts/dropzone_input.js6
-rw-r--r--app/assets/javascripts/due_date_select.js2
-rw-r--r--app/assets/javascripts/editor/constants.js5
-rw-r--r--app/assets/javascripts/editor/editor_lite.js217
-rw-r--r--app/assets/javascripts/editor/extensions/editor_ci_schema_ext.js2
-rw-r--r--app/assets/javascripts/environments/components/container.vue29
-rw-r--r--app/assets/javascripts/environments/components/deploy_board.vue5
-rw-r--r--app/assets/javascripts/environments/components/environment_delete.vue5
-rw-r--r--app/assets/javascripts/environments/components/environment_item.vue2
-rw-r--r--app/assets/javascripts/environments/components/environment_monitoring.vue2
-rw-r--r--app/assets/javascripts/environments/components/environment_rollback.vue2
-rw-r--r--app/assets/javascripts/environments/components/environment_stop.vue3
-rw-r--r--app/assets/javascripts/environments/components/environments_app.vue32
-rw-r--r--app/assets/javascripts/environments/components/environments_table.vue21
-rw-r--r--app/assets/javascripts/environments/components/stop_environment_modal.vue2
-rw-r--r--app/assets/javascripts/environments/folder/environments_folder_bundle.js4
-rw-r--r--app/assets/javascripts/environments/folder/environments_folder_view.vue18
-rw-r--r--app/assets/javascripts/environments/index.js6
-rw-r--r--app/assets/javascripts/environments/stores/environments_store.js2
-rw-r--r--app/assets/javascripts/error_tracking/components/error_details.vue4
-rw-r--r--app/assets/javascripts/error_tracking/components/error_tracking_list.vue2
-rw-r--r--app/assets/javascripts/error_tracking/details.js2
-rw-r--r--app/assets/javascripts/error_tracking/store/actions.js4
-rw-r--r--app/assets/javascripts/error_tracking/store/details/actions.js4
-rw-r--r--app/assets/javascripts/error_tracking/store/list/actions.js4
-rw-r--r--app/assets/javascripts/error_tracking/store/list/mutations.js2
-rw-r--r--app/assets/javascripts/error_tracking_settings/store/mutations.js2
-rw-r--r--app/assets/javascripts/feature_flags/components/edit_feature_flag.vue4
-rw-r--r--app/assets/javascripts/feature_flags/components/feature_flags.vue12
-rw-r--r--app/assets/javascripts/feature_flags/components/feature_flags_tab.vue64
-rw-r--r--app/assets/javascripts/feature_flags/components/feature_flags_table.vue2
-rw-r--r--app/assets/javascripts/feature_flags/components/form.vue29
-rw-r--r--app/assets/javascripts/feature_flags/components/new_feature_flag.vue13
-rw-r--r--app/assets/javascripts/feature_flags/store/edit/actions.js2
-rw-r--r--app/assets/javascripts/feature_flags/store/edit/mutations.js2
-rw-r--r--app/assets/javascripts/feature_flags/store/index/actions.js2
-rw-r--r--app/assets/javascripts/feature_flags/store/index/mutations.js2
-rw-r--r--app/assets/javascripts/feature_flags/store/new/actions.js2
-rw-r--r--app/assets/javascripts/feature_highlight/feature_highlight.js2
-rw-r--r--app/assets/javascripts/filtered_search/available_dropdown_mappings.js2
-rw-r--r--app/assets/javascripts/filtered_search/dropdown_ajax_filter.js2
-rw-r--r--app/assets/javascripts/filtered_search/dropdown_emoji.js2
-rw-r--r--app/assets/javascripts/filtered_search/dropdown_hint.js2
-rw-r--r--app/assets/javascripts/filtered_search/dropdown_non_user.js2
-rw-r--r--app/assets/javascripts/filtered_search/filtered_search_manager.js18
-rw-r--r--app/assets/javascripts/filtered_search/filtered_search_visual_tokens.js2
-rw-r--r--app/assets/javascripts/filtered_search/issuable_filtered_search_token_keys.js2
-rw-r--r--app/assets/javascripts/filtered_search/recent_searches_root.js19
-rw-r--r--app/assets/javascripts/filtered_search/recent_searches_storage_keys.js2
-rw-r--r--app/assets/javascripts/filtered_search/services/recent_searches_service.js2
-rw-r--r--app/assets/javascripts/frequent_items/components/frequent_items_list.vue2
-rw-r--r--app/assets/javascripts/frequent_items/components/frequent_items_search_input.vue9
-rw-r--r--app/assets/javascripts/frequent_items/index.js2
-rw-r--r--app/assets/javascripts/frequent_items/store/actions.js4
-rw-r--r--app/assets/javascripts/gfm_auto_complete.js14
-rw-r--r--app/assets/javascripts/gl_form.js2
-rw-r--r--app/assets/javascripts/grafana_integration/components/grafana_integration.vue4
-rw-r--r--app/assets/javascripts/graphql_shared/fragments/alert_note.fragment.graphql2
-rw-r--r--app/assets/javascripts/graphql_shared/mutations/alert_status_update.mutation.graphql (renamed from app/assets/javascripts/graphql_shared/mutations/update_alert_status.mutation.graphql)2
-rw-r--r--app/assets/javascripts/graphql_shared/queries/get_alerts.query.graphql2
-rw-r--r--app/assets/javascripts/group.js2
-rw-r--r--app/assets/javascripts/group_label_subscription.js2
-rw-r--r--app/assets/javascripts/groups/components/group_item.vue14
-rw-r--r--app/assets/javascripts/groups/groups_filterable_list.js2
-rw-r--r--app/assets/javascripts/groups/index.js4
-rw-r--r--app/assets/javascripts/groups/members/constants.js4
-rw-r--r--app/assets/javascripts/groups/members/utils.js47
-rw-r--r--app/assets/javascripts/groups_select.js2
-rw-r--r--app/assets/javascripts/header.js6
-rw-r--r--app/assets/javascripts/ide/components/activity_bar.vue8
-rw-r--r--app/assets/javascripts/ide/components/commit_sidebar/actions.vue13
-rw-r--r--app/assets/javascripts/ide/components/commit_sidebar/form.vue91
-rw-r--r--app/assets/javascripts/ide/components/commit_sidebar/list_item.vue6
-rw-r--r--app/assets/javascripts/ide/components/ide.vue20
-rw-r--r--app/assets/javascripts/ide/components/ide_review.vue2
-rw-r--r--app/assets/javascripts/ide/components/ide_side_bar.vue2
-rw-r--r--app/assets/javascripts/ide/components/ide_sidebar_nav.vue3
-rw-r--r--app/assets/javascripts/ide/components/ide_status_bar.vue4
-rw-r--r--app/assets/javascripts/ide/components/ide_status_list.vue2
-rw-r--r--app/assets/javascripts/ide/components/ide_tree.vue4
-rw-r--r--app/assets/javascripts/ide/components/merge_requests/list.vue2
-rw-r--r--app/assets/javascripts/ide/components/new_dropdown/index.vue3
-rw-r--r--app/assets/javascripts/ide/components/new_dropdown/upload.vue2
-rw-r--r--app/assets/javascripts/ide/components/panes/right.vue2
-rw-r--r--app/assets/javascripts/ide/components/pipelines/list.vue3
-rw-r--r--app/assets/javascripts/ide/components/preview/clientside.vue4
-rw-r--r--app/assets/javascripts/ide/components/preview/navigator.vue1
-rw-r--r--app/assets/javascripts/ide/components/repo_commit_section.vue2
-rw-r--r--app/assets/javascripts/ide/components/repo_editor.vue8
-rw-r--r--app/assets/javascripts/ide/components/terminal/session.vue2
-rw-r--r--app/assets/javascripts/ide/components/terminal/terminal.vue2
-rw-r--r--app/assets/javascripts/ide/constants.js7
-rw-r--r--app/assets/javascripts/ide/index.js4
-rw-r--r--app/assets/javascripts/ide/lib/common/model.js4
-rw-r--r--app/assets/javascripts/ide/lib/diff/controller.js2
-rw-r--r--app/assets/javascripts/ide/lib/editor.js4
-rw-r--r--app/assets/javascripts/ide/lib/errors.js4
-rw-r--r--app/assets/javascripts/ide/lib/mirror.js2
-rw-r--r--app/assets/javascripts/ide/stores/actions.js2
-rw-r--r--app/assets/javascripts/ide/stores/getters.js9
-rw-r--r--app/assets/javascripts/ide/stores/modules/commit/actions.js10
-rw-r--r--app/assets/javascripts/ide/stores/modules/commit/constants.js9
-rw-r--r--app/assets/javascripts/ide/stores/modules/commit/getters.js4
-rw-r--r--app/assets/javascripts/ide/stores/modules/file_templates/actions.js2
-rw-r--r--app/assets/javascripts/ide/stores/modules/file_templates/getters.js2
-rw-r--r--app/assets/javascripts/ide/stores/modules/terminal_sync/actions.js2
-rw-r--r--app/assets/javascripts/ide/stores/mutations/file.js2
-rw-r--r--app/assets/javascripts/ide/stores/utils.js2
-rw-r--r--app/assets/javascripts/ide/utils.js2
-rw-r--r--app/assets/javascripts/image_diff/image_diff.js2
-rw-r--r--app/assets/javascripts/import_entities/import_groups/components/import_table.vue204
-rw-r--r--app/assets/javascripts/import_entities/import_groups/graphql/client_factory.js42
-rw-r--r--app/assets/javascripts/import_entities/import_groups/graphql/queries/bulk_import_source_groups.query.graphql14
-rw-r--r--app/assets/javascripts/import_entities/import_groups/graphql/services/status_poller.js5
-rw-r--r--app/assets/javascripts/import_entities/import_groups/index.js13
-rw-r--r--app/assets/javascripts/import_entities/import_projects/store/actions.js4
-rw-r--r--app/assets/javascripts/import_entities/import_projects/store/mutations.js2
-rw-r--r--app/assets/javascripts/incidents/components/incidents_list.vue6
-rw-r--r--app/assets/javascripts/incidents_settings/components/incidents_settings_tabs.vue4
-rw-r--r--app/assets/javascripts/init_changes_dropdown.js2
-rw-r--r--app/assets/javascripts/init_issuable_sidebar.js2
-rw-r--r--app/assets/javascripts/integrations/edit/components/dynamic_field.vue2
-rw-r--r--app/assets/javascripts/integrations/edit/components/integration_form.vue171
-rw-r--r--app/assets/javascripts/integrations/edit/components/jira_issues_fields.vue41
-rw-r--r--app/assets/javascripts/integrations/edit/index.js22
-rw-r--r--app/assets/javascripts/integrations/edit/store/actions.js15
-rw-r--r--app/assets/javascripts/integrations/edit/store/mutation_types.js4
-rw-r--r--app/assets/javascripts/integrations/edit/store/mutations.js9
-rw-r--r--app/assets/javascripts/integrations/edit/store/state.js3
-rw-r--r--app/assets/javascripts/integrations/integration_settings_form.js55
-rw-r--r--app/assets/javascripts/invite_member/components/invite_member_modal.vue5
-rw-r--r--app/assets/javascripts/invite_members/components/invite_members_modal.vue7
-rw-r--r--app/assets/javascripts/invite_members/components/members_token_select.vue2
-rw-r--r--app/assets/javascripts/issuable_bulk_update_actions.js1
-rw-r--r--app/assets/javascripts/issuable_bulk_update_sidebar.js10
-rw-r--r--app/assets/javascripts/issuable_list/components/issuable_item.vue8
-rw-r--r--app/assets/javascripts/issuable_list/components/issuable_list_root.vue5
-rw-r--r--app/assets/javascripts/issuable_list/components/issuable_tabs.vue7
-rw-r--r--app/assets/javascripts/issuable_show/components/issuable_header.vue2
-rw-r--r--app/assets/javascripts/issuable_suggestions/components/app.vue2
-rw-r--r--app/assets/javascripts/issue.js2
-rw-r--r--app/assets/javascripts/issue_show/components/app.vue4
-rw-r--r--app/assets/javascripts/issue_show/components/description.vue2
-rw-r--r--app/assets/javascripts/issue_show/components/fields/description.vue4
-rw-r--r--app/assets/javascripts/issue_show/components/fields/description_template.vue1
-rw-r--r--app/assets/javascripts/issue_show/components/fields/title.vue2
-rw-r--r--app/assets/javascripts/issue_show/components/form.vue4
-rw-r--r--app/assets/javascripts/issue_show/components/header_actions.vue6
-rw-r--r--app/assets/javascripts/issue_show/components/incidents/incident_tabs.vue6
-rw-r--r--app/assets/javascripts/issue_show/incident.js2
-rw-r--r--app/assets/javascripts/issue_status_select.js2
-rw-r--r--app/assets/javascripts/issues_list/components/issuable.vue2
-rw-r--r--app/assets/javascripts/issues_list/components/issuables_list_app.vue4
-rw-r--r--app/assets/javascripts/issues_list/components/jira_issues_list_root.vue2
-rw-r--r--app/assets/javascripts/jira_connect/api.js20
-rw-r--r--app/assets/javascripts/jira_connect/components/app.vue64
-rw-r--r--app/assets/javascripts/jira_connect/components/groups_list.vue70
-rw-r--r--app/assets/javascripts/jira_connect/components/groups_list_item.vue48
-rw-r--r--app/assets/javascripts/jira_connect/index.js93
-rw-r--r--app/assets/javascripts/jira_connect/store/index.js5
-rw-r--r--app/assets/javascripts/jira_import/components/jira_import_form.vue27
-rw-r--r--app/assets/javascripts/jira_import/queries/search_project_members.query.graphql13
-rw-r--r--app/assets/javascripts/jobs/components/artifacts_block.vue29
-rw-r--r--app/assets/javascripts/jobs/components/environments_block.vue4
-rw-r--r--app/assets/javascripts/jobs/components/erased_block.vue20
-rw-r--r--app/assets/javascripts/jobs/components/job_app.vue11
-rw-r--r--app/assets/javascripts/jobs/components/log/duration_badge.vue9
-rw-r--r--app/assets/javascripts/jobs/components/sidebar.vue11
-rw-r--r--app/assets/javascripts/jobs/components/sidebar_job_details_container.vue15
-rw-r--r--app/assets/javascripts/jobs/components/stuck_block.vue2
-rw-r--r--app/assets/javascripts/jobs/components/trigger_block.vue55
-rw-r--r--app/assets/javascripts/jobs/index.js2
-rw-r--r--app/assets/javascripts/jobs/store/actions.js2
-rw-r--r--app/assets/javascripts/jobs/store/getters.js10
-rw-r--r--app/assets/javascripts/label_manager.js2
-rw-r--r--app/assets/javascripts/labels_select.js4
-rw-r--r--app/assets/javascripts/lib/graphql.js17
-rw-r--r--app/assets/javascripts/lib/utils/array_utility.js20
-rw-r--r--app/assets/javascripts/lib/utils/color_utils.js20
-rw-r--r--app/assets/javascripts/lib/utils/common_utils.js10
-rw-r--r--app/assets/javascripts/lib/utils/constants.js6
-rw-r--r--app/assets/javascripts/lib/utils/datetime_utility.js155
-rw-r--r--app/assets/javascripts/lib/utils/number_utils.js2
-rw-r--r--app/assets/javascripts/lib/utils/unit_format/formatter_factory.js3
-rw-r--r--app/assets/javascripts/logs/components/environment_logs.vue9
-rw-r--r--app/assets/javascripts/logs/components/log_simple_filters.vue2
-rw-r--r--app/assets/javascripts/logs/stores/mutations.js2
-rw-r--r--app/assets/javascripts/main.js5
-rw-r--r--app/assets/javascripts/members/components/action_buttons/access_request_action_buttons.vue2
-rw-r--r--app/assets/javascripts/members/components/action_buttons/invite_action_buttons.vue2
-rw-r--r--app/assets/javascripts/members/components/action_buttons/remove_group_link_button.vue1
-rw-r--r--app/assets/javascripts/members/components/action_buttons/user_action_buttons.vue2
-rw-r--r--app/assets/javascripts/members/components/app.vue (renamed from app/assets/javascripts/groups/members/components/app.vue)8
-rw-r--r--app/assets/javascripts/members/components/avatars/user_avatar.vue7
-rw-r--r--app/assets/javascripts/members/components/filter_sort/filter_sort_container.vue2
-rw-r--r--app/assets/javascripts/members/components/modals/remove_group_link_modal.vue1
-rw-r--r--app/assets/javascripts/members/components/table/members_table.vue12
-rw-r--r--app/assets/javascripts/members/components/table/members_table_cell.vue8
-rw-r--r--app/assets/javascripts/members/constants.js5
-rw-r--r--app/assets/javascripts/members/index.js (renamed from app/assets/javascripts/groups/members/index.js)6
-rw-r--r--app/assets/javascripts/members/store/actions.js6
-rw-r--r--app/assets/javascripts/members/store/mutations.js18
-rw-r--r--app/assets/javascripts/members/utils.js60
-rw-r--r--app/assets/javascripts/merge_conflicts/components/diff_file_editor.js7
-rw-r--r--app/assets/javascripts/merge_conflicts/components/inline_conflict_lines.js5
-rw-r--r--app/assets/javascripts/merge_conflicts/components/parallel_conflict_lines.js3
-rw-r--r--app/assets/javascripts/merge_conflicts/merge_conflicts_bundle.js8
-rw-r--r--app/assets/javascripts/merge_request.js2
-rw-r--r--app/assets/javascripts/merge_request/components/status_box.vue6
-rw-r--r--app/assets/javascripts/milestone_select.js19
-rw-r--r--app/assets/javascripts/milestones/components/milestone_results_section.vue7
-rw-r--r--app/assets/javascripts/mirrors/mirror_repos.js2
-rw-r--r--app/assets/javascripts/monitoring/components/alert_widget.vue7
-rw-r--r--app/assets/javascripts/monitoring/components/charts/column.vue4
-rw-r--r--app/assets/javascripts/monitoring/components/charts/gauge.vue2
-rw-r--r--app/assets/javascripts/monitoring/components/charts/single_stat.vue14
-rw-r--r--app/assets/javascripts/monitoring/components/charts/stacked_column.vue4
-rw-r--r--app/assets/javascripts/monitoring/components/charts/time_series.vue6
-rw-r--r--app/assets/javascripts/monitoring/components/dashboard.vue19
-rw-r--r--app/assets/javascripts/monitoring/components/dashboard_actions_menu.vue6
-rw-r--r--app/assets/javascripts/monitoring/components/dashboard_header.vue7
-rw-r--r--app/assets/javascripts/monitoring/components/dashboard_panel.vue9
-rw-r--r--app/assets/javascripts/monitoring/components/links_section.vue4
-rw-r--r--app/assets/javascripts/monitoring/components/variables_section.vue4
-rw-r--r--app/assets/javascripts/monitoring/monitoring_app.js19
-rw-r--r--app/assets/javascripts/monitoring/stores/actions.js14
-rw-r--r--app/assets/javascripts/monitoring/stores/mutations.js4
-rw-r--r--app/assets/javascripts/monitoring/stores/state.js2
-rw-r--r--app/assets/javascripts/monitoring/stores/utils.js2
-rw-r--r--app/assets/javascripts/mr_notes/index.js7
-rw-r--r--app/assets/javascripts/mr_notes/init_notes.js2
-rw-r--r--app/assets/javascripts/mr_popover/index.js2
-rw-r--r--app/assets/javascripts/namespace_select.js4
-rw-r--r--app/assets/javascripts/notes.js13
-rw-r--r--app/assets/javascripts/notes/components/comment_field_layout.vue2
-rw-r--r--app/assets/javascripts/notes/components/comment_form.vue28
-rw-r--r--app/assets/javascripts/notes/components/discussion_actions.vue2
-rw-r--r--app/assets/javascripts/notes/components/discussion_notes.vue4
-rw-r--r--app/assets/javascripts/notes/components/discussion_notes_replies_wrapper.vue11
-rw-r--r--app/assets/javascripts/notes/components/note_actions.vue5
-rw-r--r--app/assets/javascripts/notes/components/note_awards_list.vue2
-rw-r--r--app/assets/javascripts/notes/components/note_body.vue6
-rw-r--r--app/assets/javascripts/notes/components/note_form.vue91
-rw-r--r--app/assets/javascripts/notes/components/noteable_discussion.vue49
-rw-r--r--app/assets/javascripts/notes/components/noteable_note.vue13
-rw-r--r--app/assets/javascripts/notes/components/notes_app.vue39
-rw-r--r--app/assets/javascripts/notes/components/timeline_toggle.vue2
-rw-r--r--app/assets/javascripts/notes/components/toggle_replies_widget.vue21
-rw-r--r--app/assets/javascripts/notes/mixins/autosave.js2
-rw-r--r--app/assets/javascripts/notes/stores/actions.js30
-rw-r--r--app/assets/javascripts/notes/stores/getters.js2
-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.js36
-rw-r--r--app/assets/javascripts/notifications/components/notifications_dropdown.vue180
-rw-r--r--app/assets/javascripts/notifications/components/notifications_dropdown_item.vue42
-rw-r--r--app/assets/javascripts/notifications/constants.js27
-rw-r--r--app/assets/javascripts/notifications/index.js42
-rw-r--r--app/assets/javascripts/notifications_dropdown.js2
-rw-r--r--app/assets/javascripts/onboarding_issues/index.js128
-rw-r--r--app/assets/javascripts/operation_settings/components/metrics_settings.vue4
-rw-r--r--app/assets/javascripts/packages/details/components/app.vue4
-rw-r--r--app/assets/javascripts/packages/details/components/installation_commands.vue2
-rw-r--r--app/assets/javascripts/packages/details/components/package_title.vue2
-rw-r--r--app/assets/javascripts/packages/details/index.js2
-rw-r--r--app/assets/javascripts/packages/details/store/actions.js2
-rw-r--r--app/assets/javascripts/packages/list/components/package_search.vue97
-rw-r--r--app/assets/javascripts/packages/list/components/packages_filter.vue21
-rw-r--r--app/assets/javascripts/packages/list/components/packages_list_app.vue87
-rw-r--r--app/assets/javascripts/packages/list/components/packages_sort.vue60
-rw-r--r--app/assets/javascripts/packages/list/components/tokens/package_type_token.vue26
-rw-r--r--app/assets/javascripts/packages/list/constants.js6
-rw-r--r--app/assets/javascripts/packages/list/packages_list_app_bundle.js2
-rw-r--r--app/assets/javascripts/packages/list/stores/actions.js9
-rw-r--r--app/assets/javascripts/packages/list/stores/mutation_types.js1
-rw-r--r--app/assets/javascripts/packages/list/stores/mutations.js10
-rw-r--r--app/assets/javascripts/packages/list/stores/state.js5
-rw-r--r--app/assets/javascripts/packages/shared/components/package_list_row.vue8
-rw-r--r--app/assets/javascripts/packages/shared/components/package_tags.vue4
-rw-r--r--app/assets/javascripts/packages/shared/components/packages_list_loader.vue4
-rw-r--r--app/assets/javascripts/packages_and_registries/settings/group/bundle.js5
-rw-r--r--app/assets/javascripts/packages_and_registries/settings/group/components/group_settings_app.vue68
-rw-r--r--app/assets/javascripts/packages_and_registries/settings/group/constants.js9
-rw-r--r--app/assets/javascripts/packages_and_registries/settings/group/graphql/mutations/update_group_packages_settings.mutation.graphql9
-rw-r--r--app/assets/javascripts/packages_and_registries/settings/group/graphql/queries/get_group_packages_settings.query.graphql8
-rw-r--r--app/assets/javascripts/pages/admin/abuse_reports/abuse_reports.js2
-rw-r--r--app/assets/javascripts/pages/admin/abuse_reports/index.js2
-rw-r--r--app/assets/javascripts/pages/admin/application_settings/general/index.js6
-rw-r--r--app/assets/javascripts/pages/admin/cohorts/index.js22
-rw-r--r--app/assets/javascripts/pages/admin/groups/new/index.js2
-rw-r--r--app/assets/javascripts/pages/admin/index.js2
-rw-r--r--app/assets/javascripts/pages/admin/jobs/index/index.js3
-rw-r--r--app/assets/javascripts/pages/admin/projects/index/index.js3
-rw-r--r--app/assets/javascripts/pages/admin/runners/index.js3
-rw-r--r--app/assets/javascripts/pages/admin/users/components/delete_user_modal.vue58
-rw-r--r--app/assets/javascripts/pages/admin/users/index.js7
-rw-r--r--app/assets/javascripts/pages/groups/group_members/index.js20
-rw-r--r--app/assets/javascripts/pages/groups/new/group_path_validator.js2
-rw-r--r--app/assets/javascripts/pages/groups/new/index.js2
-rw-r--r--app/assets/javascripts/pages/groups/settings/ci_cd/show/index.js3
-rw-r--r--app/assets/javascripts/pages/groups/shared/group_details.js15
-rw-r--r--app/assets/javascripts/pages/milestones/shared/delete_milestone_modal_init.js3
-rw-r--r--app/assets/javascripts/pages/profiles/notifications/show/index.js2
-rw-r--r--app/assets/javascripts/pages/profiles/show/index.js4
-rw-r--r--app/assets/javascripts/pages/projects/activity/index.js6
-rw-r--r--app/assets/javascripts/pages/projects/alert_management/details/index.js2
-rw-r--r--app/assets/javascripts/pages/projects/blame/show/index.js2
-rw-r--r--app/assets/javascripts/pages/projects/clusters/show/index.js2
-rw-r--r--app/assets/javascripts/pages/projects/commit/pipelines/index.js2
-rw-r--r--app/assets/javascripts/pages/projects/commit/show/index.js6
-rw-r--r--app/assets/javascripts/pages/projects/compare/index/index.js3
-rw-r--r--app/assets/javascripts/pages/projects/edit/index.js8
-rw-r--r--app/assets/javascripts/pages/projects/edit/mount_search_settings.js12
-rw-r--r--app/assets/javascripts/pages/projects/find_file/show/index.js14
-rw-r--r--app/assets/javascripts/pages/projects/forks/new/components/fork_groups_list_item.vue4
-rw-r--r--app/assets/javascripts/pages/projects/incidents/show/index.js4
-rw-r--r--app/assets/javascripts/pages/projects/index.js2
-rw-r--r--app/assets/javascripts/pages/projects/issues/index/index.js2
-rw-r--r--app/assets/javascripts/pages/projects/issues/service_desk/index.js2
-rw-r--r--app/assets/javascripts/pages/projects/issues/show/index.js4
-rw-r--r--app/assets/javascripts/pages/projects/jobs/index/index.js9
-rw-r--r--app/assets/javascripts/pages/projects/labels/index/index.js3
-rw-r--r--app/assets/javascripts/pages/projects/merge_requests/show/index.js6
-rw-r--r--app/assets/javascripts/pages/projects/new/index.js4
-rw-r--r--app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/pipeline_schedules_callout.vue2
-rw-r--r--app/assets/javascripts/pages/projects/pipeline_schedules/shared/init_form.js2
-rw-r--r--app/assets/javascripts/pages/projects/project.js5
-rw-r--r--app/assets/javascripts/pages/projects/project_members/index.js64
-rw-r--r--app/assets/javascripts/pages/projects/releases/index/index.js2
-rw-r--r--app/assets/javascripts/pages/projects/settings/ci_cd/show/index.js3
-rw-r--r--app/assets/javascripts/pages/projects/settings/repository/show/index.js2
-rw-r--r--app/assets/javascripts/pages/projects/shared/permissions/components/settings_panel.vue77
-rw-r--r--app/assets/javascripts/pages/projects/shared/permissions/mixins/settings_pannel_mixin.js1
-rw-r--r--app/assets/javascripts/pages/projects/show/index.js15
-rw-r--r--app/assets/javascripts/pages/projects/wikis/index.js2
-rw-r--r--app/assets/javascripts/pages/search/show/index.js4
-rw-r--r--app/assets/javascripts/pages/search/show/search.js54
-rw-r--r--app/assets/javascripts/pages/sessions/new/index.js2
-rw-r--r--app/assets/javascripts/pages/shared/mount_runner_instructions.js32
-rw-r--r--app/assets/javascripts/pages/shared/wikis/index.js2
-rw-r--r--app/assets/javascripts/performance_bar/components/add_request.vue2
-rw-r--r--app/assets/javascripts/performance_bar/components/detailed_metric.vue6
-rw-r--r--app/assets/javascripts/performance_bar/components/performance_bar_app.vue17
-rw-r--r--app/assets/javascripts/performance_bar/performance_bar_log.js2
-rw-r--r--app/assets/javascripts/performance_bar/services/performance_bar_service.js2
-rw-r--r--app/assets/javascripts/pipeline_editor/components/commit/commit_section.vue114
-rw-r--r--app/assets/javascripts/pipeline_editor/components/header/pipeline_editor_header.vue30
-rw-r--r--app/assets/javascripts/pipeline_editor/components/header/validation_segment.vue (renamed from app/assets/javascripts/pipeline_editor/components/info/validation_segment.vue)2
-rw-r--r--app/assets/javascripts/pipeline_editor/components/lint/ci_lint_results.vue2
-rw-r--r--app/assets/javascripts/pipeline_editor/components/pipeline_editor_tabs.vue62
-rw-r--r--app/assets/javascripts/pipeline_editor/components/text_editor.vue28
-rw-r--r--app/assets/javascripts/pipeline_editor/components/ui/confirm_unsaved_changes_dialog.vue26
-rw-r--r--app/assets/javascripts/pipeline_editor/constants.js7
-rw-r--r--app/assets/javascripts/pipeline_editor/graphql/mutations/commit_ci_file.mutation.graphql2
-rw-r--r--app/assets/javascripts/pipeline_editor/graphql/queries/client/commit_sha.graphql3
-rw-r--r--app/assets/javascripts/pipeline_editor/index.js27
-rw-r--r--app/assets/javascripts/pipeline_editor/pipeline_editor_app.vue267
-rw-r--r--app/assets/javascripts/pipeline_editor/pipeline_editor_home.vue43
-rw-r--r--app/assets/javascripts/pipeline_new/components/pipeline_new_form.vue26
-rw-r--r--app/assets/javascripts/pipelines/components/dag/dag.vue4
-rw-r--r--app/assets/javascripts/pipelines/components/dag/dag_graph.vue4
-rw-r--r--app/assets/javascripts/pipelines/components/graph/action_component.vue3
-rw-r--r--app/assets/javascripts/pipelines/components/graph/constants.js6
-rw-r--r--app/assets/javascripts/pipelines/components/graph/graph_component.vue21
-rw-r--r--app/assets/javascripts/pipelines/components/graph/graph_component_legacy.vue2
-rw-r--r--app/assets/javascripts/pipelines/components/graph/job_item.vue7
-rw-r--r--app/assets/javascripts/pipelines/components/graph/linked_pipeline.vue3
-rw-r--r--app/assets/javascripts/pipelines/components/graph/linked_pipelines_column.vue29
-rw-r--r--app/assets/javascripts/pipelines/components/graph/stage_column_component.vue18
-rw-r--r--app/assets/javascripts/pipelines/components/graph_shared/drawing_utils.js4
-rw-r--r--app/assets/javascripts/pipelines/components/graph_shared/links_inner.vue22
-rw-r--r--app/assets/javascripts/pipelines/components/graph_shared/links_layer.vue6
-rw-r--r--app/assets/javascripts/pipelines/components/legacy_header_component.vue132
-rw-r--r--app/assets/javascripts/pipelines/components/pipeline_graph/pipeline_graph.vue26
-rw-r--r--app/assets/javascripts/pipelines/components/pipelines_list/pipeline_url.vue9
-rw-r--r--app/assets/javascripts/pipelines/components/pipelines_list/pipelines.vue8
-rw-r--r--app/assets/javascripts/pipelines/components/pipelines_list/pipelines_actions.vue68
-rw-r--r--app/assets/javascripts/pipelines/components/pipelines_list/pipelines_table.vue2
-rw-r--r--app/assets/javascripts/pipelines/components/pipelines_list/pipelines_table_row.vue8
-rw-r--r--app/assets/javascripts/pipelines/components/pipelines_list/stage.vue135
-rw-r--r--app/assets/javascripts/pipelines/components/pipelines_list/tokens/pipeline_branch_name_token.vue2
-rw-r--r--app/assets/javascripts/pipelines/components/pipelines_list/tokens/pipeline_tag_name_token.vue2
-rw-r--r--app/assets/javascripts/pipelines/components/test_reports/test_case_details.vue38
-rw-r--r--app/assets/javascripts/pipelines/components/test_reports/test_suite_table.vue2
-rw-r--r--app/assets/javascripts/pipelines/pipeline_details_bundle.js97
-rw-r--r--app/assets/javascripts/pipelines/pipeline_details_dag.js2
-rw-r--r--app/assets/javascripts/pipelines/pipeline_details_mediator.js2
-rw-r--r--app/assets/javascripts/pipelines/services/pipelines_service.js2
-rw-r--r--app/assets/javascripts/pipelines/stores/test_reports/actions.js10
-rw-r--r--app/assets/javascripts/pipelines/stores/test_reports/utils.js10
-rw-r--r--app/assets/javascripts/pipelines/utils.js4
-rw-r--r--app/assets/javascripts/profile/account/index.js3
-rw-r--r--app/assets/javascripts/profile/profile.js2
-rw-r--r--app/assets/javascripts/project_label_subscription.js2
-rw-r--r--app/assets/javascripts/projects/commit/components/branches_dropdown.vue1
-rw-r--r--app/assets/javascripts/projects/commit/components/form_modal.vue5
-rw-r--r--app/assets/javascripts/projects/commit/components/form_trigger.vue5
-rw-r--r--app/assets/javascripts/projects/commit/constants.js9
-rw-r--r--app/assets/javascripts/projects/commit/index.js11
-rw-r--r--app/assets/javascripts/projects/commit/init_cherry_pick_commit_modal.js51
-rw-r--r--app/assets/javascripts/projects/commit/init_cherry_pick_commit_trigger.js20
-rw-r--r--app/assets/javascripts/projects/commit/init_revert_commit_modal.js2
-rw-r--r--app/assets/javascripts/projects/commit/init_revert_commit_trigger.js8
-rw-r--r--app/assets/javascripts/projects/commit/store/actions.js2
-rw-r--r--app/assets/javascripts/projects/commit_box/info/index.js4
-rw-r--r--app/assets/javascripts/projects/commits/store/actions.js2
-rw-r--r--app/assets/javascripts/projects/compare/components/app.vue89
-rw-r--r--app/assets/javascripts/projects/compare/components/revision_dropdown.vue145
-rw-r--r--app/assets/javascripts/projects/compare/index.js33
-rw-r--r--app/assets/javascripts/projects/components/project_delete_button.vue4
-rw-r--r--app/assets/javascripts/projects/experiment_new_project_creation/components/app.vue4
-rw-r--r--app/assets/javascripts/projects/members/constants.js1
-rw-r--r--app/assets/javascripts/projects/members/utils.js8
-rw-r--r--app/assets/javascripts/projects/pipelines/charts/components/app.vue197
-rw-r--r--app/assets/javascripts/projects/pipelines/charts/components/pipeline_charts.vue196
-rw-r--r--app/assets/javascripts/projects/settings/access_dropdown.js2
-rw-r--r--app/assets/javascripts/projects/settings_service_desk/components/service_desk_root.vue67
-rw-r--r--app/assets/javascripts/projects/settings_service_desk/components/service_desk_setting.vue20
-rw-r--r--app/assets/javascripts/projects/settings_service_desk/event_hub.js3
-rw-r--r--app/assets/javascripts/projects/settings_service_desk/index.js70
-rw-r--r--app/assets/javascripts/projects/settings_service_desk/services/service_desk_service.js23
-rw-r--r--app/assets/javascripts/prometheus_metrics/custom_metrics.js4
-rw-r--r--app/assets/javascripts/prometheus_metrics/prometheus_metrics.js2
-rw-r--r--app/assets/javascripts/protected_branches/protected_branch_create.js2
-rw-r--r--app/assets/javascripts/protected_branches/protected_branch_edit.js4
-rw-r--r--app/assets/javascripts/protected_tags/protected_tag_create.js4
-rw-r--r--app/assets/javascripts/protected_tags/protected_tag_edit.js2
-rw-r--r--app/assets/javascripts/ref/stores/mutations.js4
-rw-r--r--app/assets/javascripts/registry/explorer/components/delete_image.vue77
-rw-r--r--app/assets/javascripts/registry/explorer/components/details_page/empty_state.vue44
-rw-r--r--app/assets/javascripts/registry/explorer/components/details_page/empty_tags_state.vue33
-rw-r--r--app/assets/javascripts/registry/explorer/components/details_page/tags_list.vue2
-rw-r--r--app/assets/javascripts/registry/explorer/components/details_page/tags_list_row.vue2
-rw-r--r--app/assets/javascripts/registry/explorer/constants/details.js64
-rw-r--r--app/assets/javascripts/registry/explorer/index.js10
-rw-r--r--app/assets/javascripts/registry/explorer/pages/details.vue97
-rw-r--r--app/assets/javascripts/registry/explorer/pages/list.vue72
-rw-r--r--app/assets/javascripts/related_issues/components/add_issuable_form.vue2
-rw-r--r--app/assets/javascripts/related_issues/components/related_issuable_input.vue2
-rw-r--r--app/assets/javascripts/related_issues/components/related_issues_block.vue6
-rw-r--r--app/assets/javascripts/related_issues/components/related_issues_root.vue2
-rw-r--r--app/assets/javascripts/releases/components/app_edit_new.vue2
-rw-r--r--app/assets/javascripts/releases/components/asset_links_form.vue2
-rw-r--r--app/assets/javascripts/releases/components/release_block_assets.vue2
-rw-r--r--app/assets/javascripts/releases/stores/modules/detail/actions.js2
-rw-r--r--app/assets/javascripts/releases/stores/modules/detail/mutations.js2
-rw-r--r--app/assets/javascripts/releases/stores/modules/list/actions.js2
-rw-r--r--app/assets/javascripts/reports/accessibility_report/store/getters.js2
-rw-r--r--app/assets/javascripts/reports/codequality_report/grouped_codequality_reports_app.vue16
-rw-r--r--app/assets/javascripts/reports/codequality_report/store/actions.js19
-rw-r--r--app/assets/javascripts/reports/codequality_report/store/getters.js2
-rw-r--r--app/assets/javascripts/reports/codequality_report/store/mutations.js5
-rw-r--r--app/assets/javascripts/reports/codequality_report/store/state.js2
-rw-r--r--app/assets/javascripts/reports/codequality_report/store/utils/codequality_comparison.js4
-rw-r--r--app/assets/javascripts/reports/components/grouped_test_reports_app.vue12
-rw-r--r--app/assets/javascripts/reports/components/issue_body.js2
-rw-r--r--app/assets/javascripts/reports/components/report_section.vue2
-rw-r--r--app/assets/javascripts/reports/store/actions.js2
-rw-r--r--app/assets/javascripts/repository/components/tree_content.vue4
-rw-r--r--app/assets/javascripts/repository/index.js6
-rw-r--r--app/assets/javascripts/repository/mixins/preload.js2
-rw-r--r--app/assets/javascripts/repository/pages/index.vue2
-rw-r--r--app/assets/javascripts/right_sidebar.js2
-rw-r--r--app/assets/javascripts/search/highlight_blob_search_result.js4
-rw-r--r--app/assets/javascripts/search/index.js13
-rw-r--r--app/assets/javascripts/search/sort/components/app.vue103
-rw-r--r--app/assets/javascripts/search/sort/constants.js19
-rw-r--r--app/assets/javascripts/search/sort/index.js27
-rw-r--r--app/assets/javascripts/search/topbar/components/app.vue73
-rw-r--r--app/assets/javascripts/search/topbar/components/group_filter.vue3
-rw-r--r--app/assets/javascripts/search/topbar/components/project_filter.vue5
-rw-r--r--app/assets/javascripts/search/topbar/components/searchable_dropdown.vue2
-rw-r--r--app/assets/javascripts/search/topbar/index.js31
-rw-r--r--app/assets/javascripts/search_autocomplete.js4
-rw-r--r--app/assets/javascripts/search_settings/components/search_settings.vue14
-rw-r--r--app/assets/javascripts/search_settings/index.js27
-rw-r--r--app/assets/javascripts/search_settings/mount.js23
-rw-r--r--app/assets/javascripts/self_monitor/components/self_monitor_form.vue5
-rw-r--r--app/assets/javascripts/sentry_error_stack_trace/index.js2
-rw-r--r--app/assets/javascripts/serverless/components/area.vue2
-rw-r--r--app/assets/javascripts/serverless/components/environment_row.vue2
-rw-r--r--app/assets/javascripts/serverless/components/function_row.vue2
-rw-r--r--app/assets/javascripts/serverless/components/functions.vue24
-rw-r--r--app/assets/javascripts/serverless/store/actions.js2
-rw-r--r--app/assets/javascripts/set_status_modal/set_status_modal_wrapper.vue9
-rw-r--r--app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.vue2
-rw-r--r--app/assets/javascripts/sidebar/components/confidential/edit_form.vue2
-rw-r--r--app/assets/javascripts/sidebar/components/lock/edit_form_buttons.vue2
-rw-r--r--app/assets/javascripts/sidebar/components/reviewers/reviewer_avatar_link.vue4
-rw-r--r--app/assets/javascripts/sidebar/components/reviewers/reviewers.vue4
-rw-r--r--app/assets/javascripts/sidebar/components/reviewers/sidebar_reviewers.vue6
-rw-r--r--app/assets/javascripts/sidebar/components/reviewers/uncollapsed_reviewer_list.vue82
-rw-r--r--app/assets/javascripts/sidebar/components/severity/sidebar_severity.vue2
-rw-r--r--app/assets/javascripts/sidebar/components/subscriptions/subscriptions.vue15
-rw-r--r--app/assets/javascripts/sidebar/components/time_tracking/help_state.vue2
-rw-r--r--app/assets/javascripts/sidebar/components/time_tracking/sidebar_time_tracking.vue3
-rw-r--r--app/assets/javascripts/sidebar/components/time_tracking/time_tracker.vue9
-rw-r--r--app/assets/javascripts/sidebar/components/todo_toggle/todo.vue4
-rw-r--r--app/assets/javascripts/sidebar/mount_milestone_sidebar.js2
-rw-r--r--app/assets/javascripts/sidebar/mount_sidebar.js20
-rw-r--r--app/assets/javascripts/sidebar/queries/reviewer_rereview.mutation.graphql5
-rw-r--r--app/assets/javascripts/sidebar/services/sidebar_service.js13
-rw-r--r--app/assets/javascripts/sidebar/sidebar_mediator.js14
-rw-r--r--app/assets/javascripts/sidebar/stores/sidebar_store.js8
-rw-r--r--app/assets/javascripts/single_file_diff.js2
-rw-r--r--app/assets/javascripts/snippets/components/show.vue8
-rw-r--r--app/assets/javascripts/snippets/components/snippet_blob_actions_edit.vue2
-rw-r--r--app/assets/javascripts/snippets/components/snippet_blob_view.vue2
-rw-r--r--app/assets/javascripts/snippets/components/snippet_header.vue2
-rw-r--r--app/assets/javascripts/snippets/components/snippet_visibility_edit.vue2
-rw-r--r--app/assets/javascripts/snippets/utils/blob.js4
-rw-r--r--app/assets/javascripts/static_site_editor/components/edit_area.vue8
-rw-r--r--app/assets/javascripts/static_site_editor/components/edit_drawer.vue7
-rw-r--r--app/assets/javascripts/static_site_editor/components/edit_meta_modal.vue3
-rw-r--r--app/assets/javascripts/subscription_select.js2
-rw-r--r--app/assets/javascripts/templates/issuable_template_selector.js2
-rw-r--r--app/assets/javascripts/terraform/components/states_table.vue22
-rw-r--r--app/assets/javascripts/terraform/components/states_table_actions.vue50
-rw-r--r--app/assets/javascripts/terraform/components/terraform_list.vue2
-rw-r--r--app/assets/javascripts/terraform/graphql/fragments/state.fragment.graphql4
-rw-r--r--app/assets/javascripts/terraform/graphql/mutations/add_data_to_state.mutation.graphql3
-rw-r--r--app/assets/javascripts/terraform/graphql/resolvers.js41
-rw-r--r--app/assets/javascripts/terraform/index.js12
-rw-r--r--app/assets/javascripts/ui_development_kit.js2
-rw-r--r--app/assets/javascripts/user_lists/store/show/mutations.js2
-rw-r--r--app/assets/javascripts/users_select/index.js6
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/approvals/approvals.vue4
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/artifacts_list_app.vue2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.vue4
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue29
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline_container.vue2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/mr_widget_status_icon.vue3
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/mr_widget_suggest_pipeline.vue2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_auto_merge_enabled.vue22
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_failed_to_merge.vue12
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.vue12
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_missing_branch.vue2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_rebase.vue2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/nothing_to_merge.vue27
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/pipeline_failed.vue26
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue27
-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/terraform/mr_widget_terraform_container.vue2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/terraform/terraform_plan.vue24
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/index.js8
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/mixins/ready_to_merge.js3
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue23
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/queries/get_state.query.graphql1
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/queries/states/auto_merge_enabled.fragment.graphql1
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/queries/states/auto_merge_enabled.query.graphql1
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js19
-rw-r--r--app/assets/javascripts/vue_shared/alert_details/components/alert_details.vue (renamed from app/assets/javascripts/alert_management/components/alert_details.vue)23
-rw-r--r--app/assets/javascripts/vue_shared/alert_details/components/alert_metrics.vue (renamed from app/assets/javascripts/alert_management/components/alert_metrics.vue)0
-rw-r--r--app/assets/javascripts/vue_shared/alert_details/components/alert_sidebar.vue (renamed from app/assets/javascripts/alert_management/components/alert_sidebar.vue)3
-rw-r--r--app/assets/javascripts/vue_shared/alert_details/components/alert_status.vue (renamed from app/assets/javascripts/alert_management/components/alert_status.vue)14
-rw-r--r--app/assets/javascripts/vue_shared/alert_details/components/alert_summary_row.vue (renamed from app/assets/javascripts/alert_management/components/alert_summary_row.vue)0
-rw-r--r--app/assets/javascripts/vue_shared/alert_details/components/sidebar/sidebar_assignee.vue (renamed from app/assets/javascripts/alert_management/components/sidebar/sidebar_assignee.vue)0
-rw-r--r--app/assets/javascripts/vue_shared/alert_details/components/sidebar/sidebar_assignees.vue (renamed from app/assets/javascripts/alert_management/components/sidebar/sidebar_assignees.vue)0
-rw-r--r--app/assets/javascripts/vue_shared/alert_details/components/sidebar/sidebar_header.vue (renamed from app/assets/javascripts/alert_management/components/sidebar/sidebar_header.vue)2
-rw-r--r--app/assets/javascripts/vue_shared/alert_details/components/sidebar/sidebar_status.vue (renamed from app/assets/javascripts/alert_management/components/sidebar/sidebar_status.vue)0
-rw-r--r--app/assets/javascripts/vue_shared/alert_details/components/sidebar/sidebar_todo.vue (renamed from app/assets/javascripts/alert_management/components/sidebar/sidebar_todo.vue)6
-rw-r--r--app/assets/javascripts/vue_shared/alert_details/components/system_notes/system_note.vue (renamed from app/assets/javascripts/alert_management/components/system_notes/system_note.vue)0
-rw-r--r--app/assets/javascripts/vue_shared/alert_details/constants.js29
-rw-r--r--app/assets/javascripts/vue_shared/alert_details/graphql/fragments/alert_detail_item.fragment.graphql (renamed from app/assets/javascripts/alert_management/graphql/fragments/detail_item.fragment.graphql)0
-rw-r--r--app/assets/javascripts/vue_shared/alert_details/graphql/mutations/alert_issue_create.mutation.graphql (renamed from app/assets/javascripts/alert_management/graphql/mutations/create_issue_from_alert.mutation.graphql)0
-rw-r--r--app/assets/javascripts/vue_shared/alert_details/graphql/mutations/alert_set_assignees.mutation.graphql (renamed from app/assets/javascripts/alert_management/graphql/mutations/alert_set_assignees.mutation.graphql)0
-rw-r--r--app/assets/javascripts/vue_shared/alert_details/graphql/mutations/alert_sidebar_status.mutation.graphql (renamed from app/assets/javascripts/alert_management/graphql/mutations/toggle_sidebar_status.mutation.graphql)0
-rw-r--r--app/assets/javascripts/vue_shared/alert_details/graphql/mutations/alert_todo_create.mutation.graphql (renamed from app/assets/javascripts/alert_management/graphql/mutations/alert_todo_create.mutation.graphql)2
-rw-r--r--app/assets/javascripts/vue_shared/alert_details/graphql/queries/alert_details.query.graphql (renamed from app/assets/javascripts/alert_management/graphql/queries/details.query.graphql)2
-rw-r--r--app/assets/javascripts/vue_shared/alert_details/graphql/queries/alert_sidebar_status.query.graphql (renamed from app/assets/javascripts/alert_management/graphql/queries/sidebar_status.query.graphql)0
-rw-r--r--app/assets/javascripts/vue_shared/alert_details/index.js (renamed from app/assets/javascripts/alert_management/details.js)27
-rw-r--r--app/assets/javascripts/vue_shared/alert_details/router.js (renamed from app/assets/javascripts/alert_management/router.js)0
-rw-r--r--app/assets/javascripts/vue_shared/components/awards_list.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/blob_viewers/rich_viewer.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/color_picker/color_picker.vue61
-rw-r--r--app/assets/javascripts/vue_shared/components/diff_viewer/viewers/image_diff_viewer.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/dropdown/dropdown_button.vue6
-rw-r--r--app/assets/javascripts/vue_shared/components/editor_lite.vue5
-rw-r--r--app/assets/javascripts/vue_shared/components/file_finder/index.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/file_icon.vue10
-rw-r--r--app/assets/javascripts/vue_shared/components/file_row.vue1
-rw-r--r--app/assets/javascripts/vue_shared/components/gfm_autocomplete/utils.js4
-rw-r--r--app/assets/javascripts/vue_shared/components/gl_modal_vuex.vue5
-rw-r--r--app/assets/javascripts/vue_shared/components/header_ci_component.vue6
-rw-r--r--app/assets/javascripts/vue_shared/components/help_popover.vue40
-rw-r--r--app/assets/javascripts/vue_shared/components/issue/related_issuable_item.vue4
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/field.vue9
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/header.vue1
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/suggestion_diff.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/suggestion_diff_header.vue9
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/suggestions.vue14
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/toolbar.vue4
-rw-r--r--app/assets/javascripts/vue_shared/components/modal_copy_button.vue3
-rw-r--r--app/assets/javascripts/vue_shared/components/notes/system_note.vue4
-rw-r--r--app/assets/javascripts/vue_shared/components/paginated_table_with_search_and_tabs/paginated_table_with_search_and_tabs.vue4
-rw-r--r--app/assets/javascripts/vue_shared/components/registry/list_item.vue16
-rw-r--r--app/assets/javascripts/vue_shared/components/rich_content_editor/services/editor_service.js2
-rw-r--r--app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/render_html_block.js4
-rw-r--r--app/assets/javascripts/vue_shared/components/rich_content_editor/services/sanitize_html.js2
-rw-r--r--app/assets/javascripts/vue_shared/components/runner_instructions/constants.js18
-rw-r--r--app/assets/javascripts/vue_shared/components/runner_instructions/graphql/queries/get_runner_platforms.query.graphql20
-rw-r--r--app/assets/javascripts/vue_shared/components/runner_instructions/graphql/queries/get_runner_setup.query.graphql16
-rw-r--r--app/assets/javascripts/vue_shared/components/runner_instructions/runner_instructions.vue261
-rw-r--r--app/assets/javascripts/vue_shared/components/settings/settings_block.vue45
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/date_picker.vue4
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select/base.vue3
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/labels_select_root.vue9
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/store/mutations.js2
-rw-r--r--app/assets/javascripts/vue_shared/components/todo_button.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/tooltip_on_truncate.vue2
-rw-r--r--app/assets/javascripts/vue_shared/mixins/ci_pagination_api_mixin.js2
-rw-r--r--app/assets/javascripts/vue_shared/security_reports/security_reports_app.vue98
-rw-r--r--app/assets/javascripts/vue_shared/security_reports/store/getters.js2
-rw-r--r--app/assets/javascripts/vue_shared/security_reports/store/modules/sast/actions.js2
-rw-r--r--app/assets/javascripts/vue_shared/security_reports/store/modules/sast/mutations.js2
-rw-r--r--app/assets/javascripts/webpack_non_compiled_placeholder.js24
-rw-r--r--app/assets/javascripts/whats_new/components/app.vue4
-rw-r--r--app/assets/javascripts/whats_new/store/actions.js2
779 files changed, 7462 insertions, 4201 deletions
diff --git a/app/assets/javascripts/add_context_commits_modal/components/add_context_commits_modal_wrapper.vue b/app/assets/javascripts/add_context_commits_modal/components/add_context_commits_modal_wrapper.vue
index c58ded3f1f5..a9c35d7929e 100644
--- a/app/assets/javascripts/add_context_commits_modal/components/add_context_commits_modal_wrapper.vue
+++ b/app/assets/javascripts/add_context_commits_modal/components/add_context_commits_modal_wrapper.vue
@@ -1,10 +1,11 @@
<script>
import { mapState, mapActions } from 'vuex';
import { GlModal, GlTabs, GlTab, GlSearchBoxByType, GlSprintf } from '@gitlab/ui';
+import { BV_SHOW_MODAL } from '~/lib/utils/constants';
import ReviewTabContainer from '~/add_context_commits_modal/components/review_tab_container.vue';
import { s__ } from '~/locale';
-import eventHub from '../event_hub';
import { deprecatedCreateFlash as createFlash } from '~/flash';
+import eventHub from '../event_hub';
import {
findCommitIndex,
setCommitStatus,
@@ -119,7 +120,7 @@ export default {
openModal() {
this.searchCommits();
this.fetchContextCommits();
- this.$root.$emit('bv::show::modal', 'add-review-item');
+ this.$root.$emit(BV_SHOW_MODAL, 'add-review-item');
},
handleTabChange(tabIndex) {
if (tabIndex === 0) {
diff --git a/app/assets/javascripts/admin/statistics_panel/components/app.vue b/app/assets/javascripts/admin/statistics_panel/components/app.vue
index 29077d926cf..7e1bb3ed0ce 100644
--- a/app/assets/javascripts/admin/statistics_panel/components/app.vue
+++ b/app/assets/javascripts/admin/statistics_panel/components/app.vue
@@ -26,8 +26,8 @@ export default {
</script>
<template>
- <div class="info-well">
- <div class="well-segment admin-well admin-well-statistics">
+ <div class="gl-card">
+ <div class="gl-card-body">
<h4>{{ __('Statistics') }}</h4>
<gl-loading-icon v-if="isLoading" size="md" class="my-3" />
<template v-else>
diff --git a/app/assets/javascripts/admin/cohorts/components/usage_ping_disabled.vue b/app/assets/javascripts/admin/users/components/usage_ping_disabled.vue
index 5da38495010..5da38495010 100644
--- a/app/assets/javascripts/admin/cohorts/components/usage_ping_disabled.vue
+++ b/app/assets/javascripts/admin/users/components/usage_ping_disabled.vue
diff --git a/app/assets/javascripts/admin/users/components/user_actions.vue b/app/assets/javascripts/admin/users/components/user_actions.vue
new file mode 100644
index 00000000000..6c7c434cdf4
--- /dev/null
+++ b/app/assets/javascripts/admin/users/components/user_actions.vue
@@ -0,0 +1,115 @@
+<script>
+import {
+ GlButton,
+ GlDropdown,
+ GlDropdownItem,
+ GlDropdownSectionHeader,
+ GlDropdownDivider,
+} from '@gitlab/ui';
+import { s__, __ } from '~/locale';
+import { convertArrayToCamelCase } from '~/lib/utils/common_utils';
+import { generateUserPaths } from '../utils';
+
+export default {
+ components: {
+ GlButton,
+ GlDropdown,
+ GlDropdownItem,
+ GlDropdownSectionHeader,
+ GlDropdownDivider,
+ },
+ props: {
+ user: {
+ type: Object,
+ required: true,
+ },
+ paths: {
+ type: Object,
+ required: true,
+ },
+ },
+ computed: {
+ userActions() {
+ return convertArrayToCamelCase(this.user.actions);
+ },
+ dropdownActions() {
+ return this.userActions.filter((a) => a !== 'edit');
+ },
+ dropdownDeleteActions() {
+ return this.dropdownActions.filter((a) => a.includes('delete'));
+ },
+ dropdownSafeActions() {
+ return this.dropdownActions.filter((a) => !this.dropdownDeleteActions.includes(a));
+ },
+ hasDropdownActions() {
+ return this.dropdownActions.length > 0;
+ },
+ hasDeleteActions() {
+ return this.dropdownDeleteActions.length > 0;
+ },
+ hasEditAction() {
+ return this.userActions.includes('edit');
+ },
+ userPaths() {
+ return generateUserPaths(this.paths, this.user.username);
+ },
+ },
+ methods: {
+ isLdapAction(action) {
+ return action === 'ldapBlocked';
+ },
+ },
+ i18n: {
+ edit: __('Edit'),
+ settings: __('Settings'),
+ unlock: __('Unlock'),
+ block: s__('AdminUsers|Block'),
+ unblock: s__('AdminUsers|Unblock'),
+ approve: s__('AdminUsers|Approve'),
+ reject: s__('AdminUsers|Reject'),
+ deactivate: s__('AdminUsers|Deactivate'),
+ activate: s__('AdminUsers|Activate'),
+ ldapBlocked: s__('AdminUsers|Cannot unblock LDAP blocked users'),
+ delete: s__('AdminUsers|Delete user'),
+ deleteWithContributions: s__('AdminUsers|Delete user and contributions'),
+ },
+};
+</script>
+
+<template>
+ <div class="gl-display-flex gl-justify-content-end">
+ <gl-button v-if="hasEditAction" data-testid="edit" :href="userPaths.edit">{{
+ $options.i18n.edit
+ }}</gl-button>
+
+ <gl-dropdown
+ v-if="hasDropdownActions"
+ data-testid="actions"
+ right
+ class="gl-ml-2"
+ icon="settings"
+ >
+ <gl-dropdown-section-header>{{ $options.i18n.settings }}</gl-dropdown-section-header>
+
+ <template v-for="action in dropdownSafeActions">
+ <gl-dropdown-item v-if="isLdapAction(action)" :key="action" :data-testid="action">
+ {{ $options.i18n.ldap }}
+ </gl-dropdown-item>
+ <gl-dropdown-item v-else :key="action" :href="userPaths[action]" :data-testid="action">
+ {{ $options.i18n[action] }}
+ </gl-dropdown-item>
+ </template>
+
+ <gl-dropdown-divider v-if="hasDeleteActions" />
+
+ <gl-dropdown-item
+ v-for="action in dropdownDeleteActions"
+ :key="action"
+ :href="userPaths[action]"
+ :data-testid="`delete-${action}`"
+ >
+ <span class="gl-text-red-500">{{ $options.i18n[action] }}</span>
+ </gl-dropdown-item>
+ </gl-dropdown>
+ </div>
+</template>
diff --git a/app/assets/javascripts/admin/users/components/user_avatar.vue b/app/assets/javascripts/admin/users/components/user_avatar.vue
index 4f79c4fd451..ff0e91fcb8f 100644
--- a/app/assets/javascripts/admin/users/components/user_avatar.vue
+++ b/app/assets/javascripts/admin/users/components/user_avatar.vue
@@ -1,12 +1,17 @@
<script>
-import { GlAvatarLink, GlAvatarLabeled, GlBadge } from '@gitlab/ui';
-import { USER_AVATAR_SIZE } from '../constants';
+import { GlAvatarLink, GlAvatarLabeled, GlBadge, GlIcon, GlTooltipDirective } from '@gitlab/ui';
+import { truncate } from '~/lib/utils/text_utility';
+import { USER_AVATAR_SIZE, LENGTH_OF_USER_NOTE_TOOLTIP } from '../constants';
export default {
+ directives: {
+ GlTooltip: GlTooltipDirective,
+ },
components: {
GlAvatarLink,
GlAvatarLabeled,
GlBadge,
+ GlIcon,
},
props: {
user: {
@@ -22,6 +27,9 @@ export default {
adminUserHref() {
return this.adminUserPath.replace('id', this.user.username);
},
+ userNoteShort() {
+ return truncate(this.user.note, LENGTH_OF_USER_NOTE_TOOLTIP);
+ },
},
USER_AVATAR_SIZE,
};
@@ -42,6 +50,9 @@ export default {
:sub-label="user.email"
>
<template #meta>
+ <div v-if="user.note" class="gl-text-gray-500 gl-p-1">
+ <gl-icon v-gl-tooltip="userNoteShort" name="document" />
+ </div>
<div v-for="(badge, idx) in user.badges" :key="idx" class="gl-p-1">
<gl-badge class="gl-display-flex!" size="sm" :variant="badge.variant">{{
badge.text
diff --git a/app/assets/javascripts/admin/users/components/user_date.vue b/app/assets/javascripts/admin/users/components/user_date.vue
new file mode 100644
index 00000000000..38dddbf72c2
--- /dev/null
+++ b/app/assets/javascripts/admin/users/components/user_date.vue
@@ -0,0 +1,29 @@
+<script>
+import { formatDate } from '~/lib/utils/datetime_utility';
+import { __ } from '~/locale';
+import { SHORT_DATE_FORMAT } from '../constants';
+
+export default {
+ props: {
+ date: {
+ type: String,
+ required: false,
+ default: null,
+ },
+ },
+ computed: {
+ formattedDate() {
+ const { date } = this;
+ if (date === null) {
+ return __('Never');
+ }
+ return formatDate(new Date(date), SHORT_DATE_FORMAT);
+ },
+ },
+};
+</script>
+<template>
+ <span>
+ {{ formattedDate }}
+ </span>
+</template>
diff --git a/app/assets/javascripts/admin/users/components/users_table.vue b/app/assets/javascripts/admin/users/components/users_table.vue
index 15e31935a4c..0eefe1070ff 100644
--- a/app/assets/javascripts/admin/users/components/users_table.vue
+++ b/app/assets/javascripts/admin/users/components/users_table.vue
@@ -2,6 +2,8 @@
import { GlTable } from '@gitlab/ui';
import { __ } from '~/locale';
import UserAvatar from './user_avatar.vue';
+import UserActions from './user_actions.vue';
+import UserDate from './user_date.vue';
const DEFAULT_TH_CLASSES =
'gl-bg-transparent! gl-border-b-solid! gl-border-b-gray-100! gl-p-5! gl-border-b-1!';
@@ -11,6 +13,8 @@ export default {
components: {
GlTable,
UserAvatar,
+ UserActions,
+ UserDate,
},
props: {
users: {
@@ -62,7 +66,19 @@ export default {
stacked="md"
>
<template #cell(name)="{ item: user }">
- <UserAvatar :user="user" :admin-user-path="paths.adminUser" />
+ <user-avatar :user="user" :admin-user-path="paths.adminUser" />
+ </template>
+
+ <template #cell(createdAt)="{ item: { createdAt } }">
+ <user-date :date="createdAt" />
+ </template>
+
+ <template #cell(lastActivityOn)="{ item: { lastActivityOn } }">
+ <user-date :date="lastActivityOn" show-never />
+ </template>
+
+ <template #cell(settings)="{ item: user }">
+ <user-actions :user="user" :paths="paths" />
</template>
</gl-table>
</div>
diff --git a/app/assets/javascripts/admin/users/constants.js b/app/assets/javascripts/admin/users/constants.js
index 675fcf00c39..e26643cad60 100644
--- a/app/assets/javascripts/admin/users/constants.js
+++ b/app/assets/javascripts/admin/users/constants.js
@@ -1 +1,5 @@
export const USER_AVATAR_SIZE = 32;
+
+export const SHORT_DATE_FORMAT = 'd mmm, yyyy';
+
+export const LENGTH_OF_USER_NOTE_TOOLTIP = 100;
diff --git a/app/assets/javascripts/admin/users/index.js b/app/assets/javascripts/admin/users/index.js
index f35b57c4e1a..00a84259c22 100644
--- a/app/assets/javascripts/admin/users/index.js
+++ b/app/assets/javascripts/admin/users/index.js
@@ -1,8 +1,9 @@
import Vue from 'vue';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
+import UsagePingDisabled from './components/usage_ping_disabled.vue';
import AdminUsersApp from './components/app.vue';
-export default function (el = document.querySelector('#js-admin-users-app')) {
+export const initAdminUsersApp = (el = document.querySelector('#js-admin-users-app')) => {
if (!el) {
return false;
}
@@ -19,4 +20,24 @@ export default function (el = document.querySelector('#js-admin-users-app')) {
},
}),
});
-}
+};
+
+export const initCohortsEmptyState = (el = document.querySelector('#js-cohorts-empty-state')) => {
+ if (!el) {
+ return false;
+ }
+
+ const { emptyStateSvgPath, enableUsagePingLink, docsLink } = el.dataset;
+
+ return new Vue({
+ el,
+ provide: {
+ svgPath: emptyStateSvgPath,
+ primaryButtonPath: enableUsagePingLink,
+ docsLink,
+ },
+ render(h) {
+ return h(UsagePingDisabled);
+ },
+ });
+};
diff --git a/app/assets/javascripts/admin/users/tabs.js b/app/assets/javascripts/admin/users/tabs.js
new file mode 100644
index 00000000000..9ada77396c7
--- /dev/null
+++ b/app/assets/javascripts/admin/users/tabs.js
@@ -0,0 +1,23 @@
+import { historyPushState } from '~/lib/utils/common_utils';
+import { mergeUrlParams } from '~/lib/utils/url_utility';
+
+const COHORTS_PANE = 'cohorts';
+
+const tabClickHandler = (e) => {
+ const { hash } = e.currentTarget;
+ const tab = hash === `#${COHORTS_PANE}` ? COHORTS_PANE : null;
+ const newUrl = mergeUrlParams({ tab }, window.location.href);
+ historyPushState(newUrl);
+};
+
+const initTabs = () => {
+ const tabLinks = document.querySelectorAll('.js-users-tab-item a');
+
+ if (tabLinks.length) {
+ tabLinks.forEach((tabLink) => {
+ tabLink.addEventListener('click', (e) => tabClickHandler(e));
+ });
+ }
+};
+
+export default initTabs;
diff --git a/app/assets/javascripts/admin/users/utils.js b/app/assets/javascripts/admin/users/utils.js
new file mode 100644
index 00000000000..f6c1091ba27
--- /dev/null
+++ b/app/assets/javascripts/admin/users/utils.js
@@ -0,0 +1,7 @@
+export const generateUserPaths = (paths, id) => {
+ return Object.fromEntries(
+ Object.entries(paths).map(([action, genericPath]) => {
+ return [action, genericPath.replace('id', id)];
+ }),
+ );
+};
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 2bad15faa85..dae52a530ac 100644
--- a/app/assets/javascripts/alert_management/components/alert_management_table.vue
+++ b/app/assets/javascripts/alert_management/components/alert_management_table.vue
@@ -23,14 +23,10 @@ import {
} from '~/vue_shared/components/paginated_table_with_search_and_tabs/constants';
import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue';
import { convertToSnakeCase } from '~/lib/utils/text_utility';
+import AlertStatus from '~/vue_shared/alert_details/components/alert_status.vue';
import getAlertsQuery from '~/graphql_shared/queries/get_alerts.query.graphql';
import getAlertsCountByStatus from '../graphql/queries/get_count_by_status.query.graphql';
-import {
- ALERTS_STATUS_TABS,
- ALERTS_SEVERITY_LABELS,
- trackAlertListViewsOptions,
-} from '../constants';
-import AlertStatus from './alert_status.vue';
+import { ALERTS_STATUS_TABS, SEVERITY_LEVELS, trackAlertListViewsOptions } from '../constants';
const TH_TEST_ID = { 'data-testid': 'alert-management-severity-sort' };
@@ -96,7 +92,7 @@ export default {
sortable: true,
},
],
- severityLabels: ALERTS_SEVERITY_LABELS,
+ severityLabels: SEVERITY_LEVELS,
statusTabs: ALERTS_STATUS_TABS,
components: {
GlAlert,
diff --git a/app/assets/javascripts/alert_management/constants.js b/app/assets/javascripts/alert_management/constants.js
index b79a64646eb..c98d3865621 100644
--- a/app/assets/javascripts/alert_management/constants.js
+++ b/app/assets/javascripts/alert_management/constants.js
@@ -1,12 +1,12 @@
import { s__ } from '~/locale';
-export const ALERTS_SEVERITY_LABELS = {
- CRITICAL: s__('AlertManagement|Critical'),
- HIGH: s__('AlertManagement|High'),
- MEDIUM: s__('AlertManagement|Medium'),
- LOW: s__('AlertManagement|Low'),
- INFO: s__('AlertManagement|Info'),
- UNKNOWN: s__('AlertManagement|Unknown'),
+export const SEVERITY_LEVELS = {
+ CRITICAL: s__('severity|Critical'),
+ HIGH: s__('severity|High'),
+ MEDIUM: s__('severity|Medium'),
+ LOW: s__('severity|Low'),
+ INFO: s__('severity|Info'),
+ UNKNOWN: s__('severity|Unknown'),
};
export const ALERTS_STATUS_TABS = [
@@ -46,20 +46,3 @@ export const trackAlertListViewsOptions = {
category: 'Alert Management',
action: 'view_alerts_list',
};
-
-/**
- * Tracks snowplow event when user views alert details
- */
-export const trackAlertsDetailsViewsOptions = {
- category: 'Alert Management',
- action: 'view_alert_details',
-};
-
-/**
- * Tracks snowplow event when alert status is updated
- */
-export const trackAlertStatusUpdateOptions = {
- category: 'Alert Management',
- action: 'update_alert_status',
- label: 'Status',
-};
diff --git a/app/assets/javascripts/alert_management/list.js b/app/assets/javascripts/alert_management/list.js
index b484841ed2c..4b18dee7806 100644
--- a/app/assets/javascripts/alert_management/list.js
+++ b/app/assets/javascripts/alert_management/list.js
@@ -3,6 +3,7 @@ import VueApollo from 'vue-apollo';
import { defaultDataIdFromObject } from 'apollo-cache-inmemory';
import createDefaultClient from '~/lib/graphql';
import { parseBoolean } from '~/lib/utils/common_utils';
+import { PAGE_CONFIG } from '~/vue_shared/alert_details/constants';
import AlertManagementList from './components/alert_management_list_wrapper.vue';
Vue.use(VueApollo);
@@ -59,6 +60,7 @@ export default () => {
populatingAlertsHelpUrl,
emptyAlertSvgPath,
alertManagementEnabled: parseBoolean(alertManagementEnabled),
+ trackAlertStatusUpdateOptions: PAGE_CONFIG.OPERATIONS.TRACK_ALERT_STATUS_UPDATE_OPTIONS,
userCanEnableAlertManagement: parseBoolean(userCanEnableAlertManagement),
},
apolloProvider,
diff --git a/app/assets/javascripts/alerts_settings/components/alert_mapping_builder.vue b/app/assets/javascripts/alerts_settings/components/alert_mapping_builder.vue
index c52e9f5c264..66d6af6f0a4 100644
--- a/app/assets/javascripts/alerts_settings/components/alert_mapping_builder.vue
+++ b/app/assets/javascripts/alerts_settings/components/alert_mapping_builder.vue
@@ -8,11 +8,14 @@ import {
GlSearchBoxByType,
GlTooltipDirective as GlTooltip,
} from '@gitlab/ui';
+import { cloneDeep } from 'lodash';
import { s__, __ } from '~/locale';
-// Mocks will be removed when integrating with BE is ready
-// data format is defined and will be the same as mocked (maybe with some minor changes)
-// feature rollout plan - https://gitlab.com/gitlab-org/gitlab/-/issues/262707#note_442529171
-import gitlabFieldsMock from './mocks/gitlabFields.json';
+import { capitalizeFirstCharacter } from '~/lib/utils/text_utility';
+import {
+ getMappingData,
+ getPayloadFields,
+ transformForSave,
+} from '../utils/mapping_transformations';
export const i18n = {
columns: {
@@ -40,18 +43,25 @@ export default {
directives: {
GlTooltip,
},
- inject: {
- gitlabAlertFields: {
- default: gitlabFieldsMock,
- },
- },
props: {
- payloadFields: {
+ alertFields: {
+ type: Array,
+ required: true,
+ validator: (fields) => {
+ return (
+ fields.length &&
+ fields.every(({ name, types, label }) => {
+ return typeof name === 'string' && Array.isArray(types) && typeof label === 'string';
+ })
+ );
+ },
+ },
+ parsedPayload: {
type: Array,
required: false,
default: () => [],
},
- mapping: {
+ savedMapping: {
type: Array,
required: false,
default: () => [],
@@ -59,31 +69,18 @@ export default {
},
data() {
return {
- gitlabFields: this.gitlabAlertFields,
+ gitlabFields: cloneDeep(this.alertFields),
};
},
computed: {
+ payloadFields() {
+ return getPayloadFields(this.parsedPayload);
+ },
mappingData() {
- return this.gitlabFields.map((gitlabField) => {
- const mappingFields = this.payloadFields.filter(({ type }) =>
- type.some((t) => gitlabField.compatibleTypes.includes(t)),
- );
-
- const foundMapping = this.mapping.find(
- ({ alertFieldName }) => alertFieldName === gitlabField.name,
- );
-
- const { fallbackAlertPaths, payloadAlertPaths } = foundMapping || {};
-
- return {
- mapping: payloadAlertPaths,
- fallback: fallbackAlertPaths,
- searchTerm: '',
- fallbackSearchTerm: '',
- mappingFields,
- ...gitlabField,
- };
- });
+ return getMappingData(this.gitlabFields, this.payloadFields, this.savedMapping);
+ },
+ hasFallbackColumn() {
+ return this.gitlabFields.some(({ numberOfFallbacks }) => Boolean(numberOfFallbacks));
},
},
methods: {
@@ -91,6 +88,7 @@ export default {
const fieldIndex = this.gitlabFields.findIndex((field) => field.name === gitlabKey);
const updatedField = { ...this.gitlabFields[fieldIndex], ...{ [valueKey]: mappingKey } };
Vue.set(this.gitlabFields, fieldIndex, updatedField);
+ this.$emit('onMappingUpdate', transformForSave(this.mappingData));
},
setSearchTerm(search = '', searchFieldKey, gitlabKey) {
const fieldIndex = this.gitlabFields.findIndex((field) => field.name === gitlabKey);
@@ -99,7 +97,6 @@ export default {
},
filterFields(searchTerm = '', fields) {
const search = searchTerm.toLowerCase();
-
return fields.filter((field) => field.label.toLowerCase().includes(search));
},
isSelected(fieldValue, mapping) {
@@ -111,8 +108,10 @@ export default {
this.$options.i18n.makeSelection
);
},
- getFieldValue({ label, type }) {
- return `${label} (${type.join(__(' or '))})`;
+ getFieldValue({ label, types }) {
+ const type = types.map((t) => capitalizeFirstCharacter(t.toLowerCase())).join(__(' or '));
+
+ return `${label} (${type})`;
},
noResults(searchTerm, fields) {
return !this.filterFields(searchTerm, fields).length;
@@ -131,7 +130,11 @@ export default {
<h5 id="parsedFieldsHeader" class="gl-display-table-cell gl-py-3 gl-pr-3">
{{ $options.i18n.columns.payloadKeyTitle }}
</h5>
- <h5 id="fallbackFieldsHeader" class="gl-display-table-cell gl-py-3 gl-pr-3">
+ <h5
+ v-if="hasFallbackColumn"
+ id="fallbackFieldsHeader"
+ class="gl-display-table-cell gl-py-3 gl-pr-3"
+ >
{{ $options.i18n.columns.fallbackKeyTitle }}
<gl-icon
v-gl-tooltip
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 1ae7f826ce6..cef20321ce2 100644
--- a/app/assets/javascripts/alerts_settings/components/alerts_settings_form.vue
+++ b/app/assets/javascripts/alerts_settings/components/alerts_settings_form.vue
@@ -15,8 +15,6 @@ import {
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { s__ } from '~/locale';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
-import MappingBuilder from './alert_mapping_builder.vue';
-import AlertSettingsFormHelpBlock from './alert_settings_form_help_block.vue';
import getCurrentIntegrationQuery from '../graphql/queries/get_current_integration.query.graphql';
import {
integrationTypes,
@@ -24,6 +22,8 @@ import {
targetPrometheusUrlPlaceholder,
typeSet,
} from '../constants';
+import MappingBuilder from './alert_mapping_builder.vue';
+import AlertSettingsFormHelpBlock from './alert_settings_form_help_block.vue';
// Mocks will be removed when integrating with BE is ready
// data format is defined and will be the same as mocked (maybe with some minor changes)
// feature rollout plan - https://gitlab.com/gitlab-org/gitlab/-/issues/262707#note_442529171
@@ -125,6 +125,9 @@ export default {
prometheus: {
default: {},
},
+ multiIntegrations: {
+ default: false,
+ },
},
props: {
loading: {
@@ -135,6 +138,11 @@ export default {
type: Boolean,
required: true,
},
+ alertFields: {
+ type: Array,
+ required: false,
+ default: null,
+ },
},
apollo: {
currentIntegration: {
@@ -152,6 +160,7 @@ export default {
},
resetSamplePayloadConfirmed: false,
customMapping: null,
+ mapping: [],
parsingPayload: false,
currentIntegration: null,
};
@@ -195,14 +204,16 @@ export default {
},
showMappingBuilder() {
return (
+ this.multiIntegrations &&
this.glFeatures.multipleHttpIntegrationsCustomMapping &&
- this.selectedIntegration === typeSet.http
+ this.selectedIntegration === typeSet.http &&
+ this.alertFields?.length
);
},
- mappingBuilderFields() {
+ parsedSamplePayload() {
return this.customMapping?.samplePayload?.payloadAlerFields?.nodes;
},
- mappingBuilderMapping() {
+ savedMapping() {
return this.customMapping?.storedMapping?.nodes;
},
hasSamplePayload() {
@@ -255,9 +266,20 @@ export default {
},
submit() {
const { name, apiUrl } = this.integrationForm;
+ const customMappingVariables = this.glFeatures.multipleHttpIntegrationsCustomMapping
+ ? {
+ payloadAttributeMappings: this.mapping,
+ payloadExample: this.integrationTestPayload.json,
+ }
+ : {};
+
const variables =
this.selectedIntegration === typeSet.http
- ? { name, active: this.active }
+ ? {
+ name,
+ active: this.active,
+ ...customMappingVariables,
+ }
: { apiUrl, active: this.active };
const integrationPayload = { type: this.selectedIntegration, variables };
@@ -336,6 +358,9 @@ export default {
this.integrationTestPayload.json = res?.samplePayload.body;
});
},
+ updateMapping(mapping) {
+ this.mapping = mapping;
+ },
},
};
</script>
@@ -541,8 +566,10 @@ export default {
>
<span>{{ $options.i18n.integrationFormSteps.step5.intro }}</span>
<mapping-builder
- :payload-fields="mappingBuilderFields"
- :mapping="mappingBuilderMapping"
+ :parsed-payload="parsedSamplePayload"
+ :saved-mapping="savedMapping"
+ :alert-fields="alertFields"
+ @onMappingUpdate="updateMapping"
/>
</gl-form-group>
</div>
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 d0cac066ffa..71d094dbe6e 100644
--- a/app/assets/javascripts/alerts_settings/components/alerts_settings_wrapper.vue
+++ b/app/assets/javascripts/alerts_settings/components/alerts_settings_wrapper.vue
@@ -12,8 +12,6 @@ import destroyHttpIntegrationMutation from '../graphql/mutations/destroy_http_in
import resetHttpTokenMutation from '../graphql/mutations/reset_http_token.mutation.graphql';
import resetPrometheusTokenMutation from '../graphql/mutations/reset_prometheus_token.mutation.graphql';
import updateCurrentIntergrationMutation from '../graphql/mutations/update_current_intergration.mutation.graphql';
-import IntegrationsList from './alerts_integrations_list.vue';
-import AlertSettingsForm from './alerts_settings_form.vue';
import service from '../services';
import { typeSet } from '../constants';
import {
@@ -27,6 +25,8 @@ import {
UPDATE_INTEGRATION_ERROR,
INTEGRATION_PAYLOAD_TEST_ERROR,
} from '../utils/error_messages';
+import AlertSettingsForm from './alerts_settings_form.vue';
+import IntegrationsList from './alerts_integrations_list.vue';
export default {
typeSet,
@@ -57,6 +57,13 @@ export default {
default: false,
},
},
+ props: {
+ alertFields: {
+ type: Array,
+ required: false,
+ default: null,
+ },
+ },
apollo: {
integrations: {
fetchPolicy: fetchPolicies.CACHE_AND_NETWORK,
@@ -312,6 +319,7 @@ export default {
<alert-settings-form
:loading="isUpdating"
:can-add-integration="canAddIntegration"
+ :alert-fields="alertFields"
@create-new-integration="createNewIntegration"
@update-integration="updateIntegration"
@reset-token="resetToken"
diff --git a/app/assets/javascripts/alerts_settings/components/mocks/gitlabFields.json b/app/assets/javascripts/alerts_settings/components/mocks/gitlabFields.json
deleted file mode 100644
index ac559a30eda..00000000000
--- a/app/assets/javascripts/alerts_settings/components/mocks/gitlabFields.json
+++ /dev/null
@@ -1,112 +0,0 @@
-[
- {
- "name": "title",
- "label": "Title",
- "type": [
- "String"
- ],
- "compatibleTypes": [
- "String",
- "Number",
- "DateTime"
- ],
- "numberOfFallbacks": 1
- },
- {
- "name": "description",
- "label": "Description",
- "type": [
- "String"
- ],
- "compatibleTypes": [
- "String",
- "Number",
- "DateTime"
- ]
- },
- {
- "name": "startTime",
- "label": "Start time",
- "type": [
- "DateTime"
- ],
- "compatibleTypes": [
- "Number",
- "DateTime"
- ]
- },
- {
- "name": "service",
- "label": "Service",
- "type": [
- "String"
- ],
- "compatibleTypes": [
- "String",
- "Number",
- "DateTime"
- ]
- },
- {
- "name": "monitoringTool",
- "label": "Monitoring tool",
- "type": [
- "String"
- ],
- "compatibleTypes": [
- "String",
- "Number",
- "DateTime"
- ]
- },
- {
- "name": "hosts",
- "label": "Hosts",
- "type": [
- "String",
- "Array"
- ],
- "compatibleTypes": [
- "String",
- "Array",
- "Number",
- "DateTime"
- ]
- },
- {
- "name": "severity",
- "label": "Severity",
- "type": [
- "String"
- ],
- "compatibleTypes": [
- "String",
- "Number",
- "DateTime"
- ]
- },
- {
- "name": "fingerprint",
- "label": "Fingerprint",
- "type": [
- "String"
- ],
- "compatibleTypes": [
- "String",
- "Number",
- "DateTime"
- ]
- },
- {
- "name": "environment",
- "label": "Environment",
- "type": [
- "String"
- ],
- "compatibleTypes": [
- "String",
- "Number",
- "DateTime"
- ]
- }
-]
diff --git a/app/assets/javascripts/alerts_settings/components/mocks/parsedMapping.json b/app/assets/javascripts/alerts_settings/components/mocks/parsedMapping.json
index 5326678155d..80fbebf2a60 100644
--- a/app/assets/javascripts/alerts_settings/components/mocks/parsedMapping.json
+++ b/app/assets/javascripts/alerts_settings/components/mocks/parsedMapping.json
@@ -4,95 +4,69 @@
"payloadAlerFields": {
"nodes": [
{
- "name": "dashboardId",
+ "path": ["dashboardId"],
"label": "Dashboard Id",
- "type": [
- "Number"
- ]
+ "type": "string"
},
{
- "name": "evalMatches",
+ "path": ["evalMatches"],
"label": "Eval Matches",
- "type": [
- "Array"
- ]
+ "type": "array"
},
{
- "name": "createdAt",
+ "path": ["createdAt"],
"label": "Created At",
- "type": [
- "DateTime"
- ]
+ "type": "datetime"
},
{
- "name": "imageUrl",
+ "path": ["imageUrl"],
"label": "Image Url",
- "type": [
- "String"
- ]
+ "type": "string"
},
{
- "name": "message",
+ "path": ["message"],
"label": "Message",
- "type": [
- "String"
- ]
+ "type": "string"
},
{
- "name": "orgId",
+ "path": ["orgId"],
"label": "Org Id",
- "type": [
- "Number"
- ]
+ "type": "string"
},
{
- "name": "panelId",
+ "path": ["panelId"],
"label": "Panel Id",
- "type": [
- "String"
- ]
+ "type": "string"
},
{
- "name": "ruleId",
+ "path": ["ruleId"],
"label": "Rule Id",
- "type": [
- "Number"
- ]
+ "type": "string"
},
{
- "name": "ruleName",
+ "path": ["ruleName"],
"label": "Rule Name",
- "type": [
- "String"
- ]
+ "type": "string"
},
{
- "name": "ruleUrl",
+ "path": ["ruleUrl"],
"label": "Rule Url",
- "type": [
- "String"
- ]
+ "type": "string"
},
{
- "name": "state",
+ "path": ["state"],
"label": "State",
- "type": [
- "String"
- ]
+ "type": "string"
},
{
- "name": "title",
+ "path": ["title"],
"label": "Title",
- "type": [
- "String"
- ]
+ "type": "string"
},
{
- "name": "tags",
+ "path": ["tags", "tag"],
"label": "Tags",
- "type": [
- "Object"
- ]
+ "type": "string"
}
]
}
diff --git a/app/assets/javascripts/alerts_settings/graphql/mutations/create_http_integration.mutation.graphql b/app/assets/javascripts/alerts_settings/graphql/mutations/create_http_integration.mutation.graphql
index d1dacbad40a..f3fc10b4bd4 100644
--- a/app/assets/javascripts/alerts_settings/graphql/mutations/create_http_integration.mutation.graphql
+++ b/app/assets/javascripts/alerts_settings/graphql/mutations/create_http_integration.mutation.graphql
@@ -1,7 +1,21 @@
#import "../fragments/integration_item.fragment.graphql"
-mutation createHttpIntegration($projectPath: ID!, $name: String!, $active: Boolean!) {
- httpIntegrationCreate(input: { projectPath: $projectPath, name: $name, active: $active }) {
+mutation createHttpIntegration(
+ $projectPath: ID!
+ $name: String!
+ $active: Boolean!
+ $payloadExample: JsonString
+ $payloadAttributeMappings: [AlertManagementPayloadAlertFieldInput!]
+) {
+ httpIntegrationCreate(
+ input: {
+ projectPath: $projectPath
+ name: $name
+ active: $active
+ payloadExample: $payloadExample
+ payloadAttributeMappings: $payloadAttributeMappings
+ }
+ ) {
errors
integration {
...IntegrationItem
diff --git a/app/assets/javascripts/alerts_settings/index.js b/app/assets/javascripts/alerts_settings/index.js
index 85858956987..973f5d4ec54 100644
--- a/app/assets/javascripts/alerts_settings/index.js
+++ b/app/assets/javascripts/alerts_settings/index.js
@@ -31,6 +31,7 @@ export default (el) => {
url,
projectPath,
multiIntegrations,
+ alertFields,
} = el.dataset;
return new Vue({
@@ -60,7 +61,14 @@ export default (el) => {
},
apolloProvider,
render(createElement) {
- return createElement('alert-settings-wrapper');
+ return createElement('alert-settings-wrapper', {
+ props: {
+ alertFields:
+ gon.features?.multipleHttpIntegrationsCustomMapping && parseBoolean(multiIntegrations)
+ ? JSON.parse(alertFields)
+ : null,
+ },
+ });
},
});
};
diff --git a/app/assets/javascripts/alerts_settings/utils/mapping_transformations.js b/app/assets/javascripts/alerts_settings/utils/mapping_transformations.js
new file mode 100644
index 00000000000..a86103540c0
--- /dev/null
+++ b/app/assets/javascripts/alerts_settings/utils/mapping_transformations.js
@@ -0,0 +1,61 @@
+/**
+ * Given data for GitLab alert fields, parsed payload fields data and previously stored mapping (if any)
+ * creates an object in a form convenient to build UI && interact with it
+ * @param {Object} gitlabFields - structure describing GitLab alert fields
+ * @param {Object} payloadFields - parsed from sample JSON sample alert fields
+ * @param {Object} savedMapping - GitLab fields to parsed fields mapping
+ *
+ * @return {Object} mapping data for UI mapping builder
+ */
+export const getMappingData = (gitlabFields, payloadFields, savedMapping) => {
+ return gitlabFields.map((gitlabField) => {
+ // find fields from payload that match gitlab alert field by type
+ const mappingFields = payloadFields.filter(({ type }) => gitlabField.types.includes(type));
+
+ // find the mapping that was previously stored
+ const foundMapping = savedMapping.find(({ fieldName }) => fieldName === gitlabField.name);
+
+ const { fallbackAlertPaths, payloadAlertPaths } = foundMapping || {};
+
+ return {
+ mapping: payloadAlertPaths,
+ fallback: fallbackAlertPaths,
+ searchTerm: '',
+ fallbackSearchTerm: '',
+ mappingFields,
+ ...gitlabField,
+ };
+ });
+};
+
+/**
+ * Based on mapping data configured by the user creates an object in a format suitable for save on BE
+ * @param {Object} mappingData - structure describing mapping between GitLab fields and parsed payload fields
+ *
+ * @return {Object} mapping data to send to BE
+ */
+export const transformForSave = (mappingData) => {
+ return mappingData.reduce((acc, field) => {
+ const mapped = field.mappingFields.find(({ name }) => name === field.mapping);
+ if (mapped) {
+ const { path, type, label } = mapped;
+ acc.push({
+ fieldName: field.name.toUpperCase(),
+ path,
+ type: type.toUpperCase(),
+ label,
+ });
+ }
+ return acc;
+ }, []);
+};
+
+/**
+ * Adds `name` prop to each provided by BE parsed payload field
+ * @param {Object} payload - parsed sample payload
+ *
+ * @return {Object} same as input with an extra `name` property which basically serves as a key to make a match
+ */
+export const getPayloadFields = (payload) => {
+ return payload.map((field) => ({ ...field, name: field.path.join('_') }));
+};
diff --git a/app/assets/javascripts/analytics/instance_statistics/components/app.vue b/app/assets/javascripts/analytics/instance_statistics/components/app.vue
index 8df4d2e2524..cdf1b1bbe2b 100644
--- a/app/assets/javascripts/analytics/instance_statistics/components/app.vue
+++ b/app/assets/javascripts/analytics/instance_statistics/components/app.vue
@@ -1,10 +1,10 @@
<script>
+import { TODAY, TOTAL_DAYS_TO_SHOW, START_DATE } from '../constants';
import InstanceCounts from './instance_counts.vue';
import InstanceStatisticsCountChart from './instance_statistics_count_chart.vue';
import UsersChart from './users_chart.vue';
import ProjectsAndGroupsChart from './projects_and_groups_chart.vue';
import ChartsConfig from './charts_config';
-import { TODAY, TOTAL_DAYS_TO_SHOW, START_DATE } from '../constants';
export default {
name: 'InstanceStatisticsApp',
diff --git a/app/assets/javascripts/api.js b/app/assets/javascripts/api.js
index 0a3db8ad3a6..0e42c0ae913 100644
--- a/app/assets/javascripts/api.js
+++ b/app/assets/javascripts/api.js
@@ -1,7 +1,7 @@
-import axios from './lib/utils/axios_utils';
-import { joinPaths } from './lib/utils/url_utility';
import { deprecatedCreateFlash as flash } from '~/flash';
import { __ } from '~/locale';
+import axios from './lib/utils/axios_utils';
+import { joinPaths } from './lib/utils/url_utility';
const DEFAULT_PER_PAGE = 20;
@@ -83,6 +83,9 @@ const Api = {
featureFlagUserList: '/api/:version/projects/:id/feature_flags_user_lists/:list_iid',
billableGroupMembersPath: '/api/:version/groups/:id/billable_members',
containerRegistryDetailsPath: '/api/:version/registry/repositories/:id/',
+ projectNotificationSettingsPath: '/api/:version/projects/:id/notification_settings',
+ groupNotificationSettingsPath: '/api/:version/groups/:id/notification_settings',
+ notificationSettingsPath: '/api/:version/notification_settings',
group(groupId, callback = () => {}) {
const url = Api.buildUrl(Api.groupPath).replace(':id', groupId);
@@ -179,9 +182,9 @@ const Api = {
});
},
- groupLabels(namespace) {
+ groupLabels(namespace, options = {}) {
const url = Api.buildUrl(Api.groupLabelsPath).replace(':namespace_path', namespace);
- return axios.get(url).then(({ data }) => data);
+ return axios.get(url, options).then(({ data }) => data);
},
// Return namespaces list. Filtered by query
@@ -442,10 +445,11 @@ const Api = {
});
},
- applySuggestion(id, message) {
+ applySuggestion(id, message = '') {
const url = Api.buildUrl(Api.applySuggestionPath).replace(':id', encodeURIComponent(id));
+ const params = gon.features?.suggestionsCustomCommit ? { commit_message: message } : false;
- return axios.put(url, { commit_message: message });
+ return axios.put(url, params);
},
applySuggestionBatch(ids) {
@@ -905,6 +909,34 @@ const Api = {
return { data, headers };
});
},
+
+ async updateNotificationSettings(projectId, groupId, data = {}) {
+ let url = Api.buildUrl(this.notificationSettingsPath);
+
+ if (projectId) {
+ url = Api.buildUrl(this.projectNotificationSettingsPath).replace(':id', projectId);
+ } else if (groupId) {
+ url = Api.buildUrl(this.groupNotificationSettingsPath).replace(':id', groupId);
+ }
+
+ const result = await axios.put(url, data);
+
+ return result;
+ },
+
+ async getNotificationSettings(projectId, groupId) {
+ let url = Api.buildUrl(this.notificationSettingsPath);
+
+ if (projectId) {
+ url = Api.buildUrl(this.projectNotificationSettingsPath).replace(':id', projectId);
+ } else if (groupId) {
+ url = Api.buildUrl(this.groupNotificationSettingsPath).replace(':id', groupId);
+ }
+
+ const result = await axios.get(url);
+
+ return result;
+ },
};
export default Api;
diff --git a/app/assets/javascripts/api/user_api.js b/app/assets/javascripts/api/user_api.js
index e5983ec3c58..5efc7063efa 100644
--- a/app/assets/javascripts/api/user_api.js
+++ b/app/assets/javascripts/api/user_api.js
@@ -1,8 +1,8 @@
+import { deprecatedCreateFlash as flash } from '~/flash';
+import { __ } from '~/locale';
import axios from '../lib/utils/axios_utils';
import { buildApiUrl } from './api_utils';
import { DEFAULT_PER_PAGE } from './constants';
-import { deprecatedCreateFlash as flash } from '~/flash';
-import { __ } from '~/locale';
const USER_COUNTS_PATH = '/api/:version/user_counts';
const USERS_PATH = '/api/:version/users.json';
diff --git a/app/assets/javascripts/awards_handler.js b/app/assets/javascripts/awards_handler.js
index 22717a3f84c..288acd1b2f1 100644
--- a/app/assets/javascripts/awards_handler.js
+++ b/app/assets/javascripts/awards_handler.js
@@ -4,12 +4,12 @@ import $ from 'jquery';
import { uniq } from 'lodash';
import { GlBreakpointInstance as bp } from '@gitlab/ui/dist/utils';
import Cookies from 'js-cookie';
+import * as Emoji from '~/emoji';
+import { dispose, fixTitle } from '~/tooltips';
import { __ } from './locale';
import { isInVueNoteablePage } from './lib/utils/dom_utils';
import { deprecatedCreateFlash as flash } from './flash';
import axios from './lib/utils/axios_utils';
-import * as Emoji from '~/emoji';
-import { dispose, fixTitle } from '~/tooltips';
const animationEndEventString = 'animationend webkitAnimationEnd MSAnimationEnd oAnimationEnd';
const transitionEndEventString = 'transitionend webkitTransitionEnd oTransitionEnd MSTransitionEnd';
diff --git a/app/assets/javascripts/badges/components/badge_form.vue b/app/assets/javascripts/badges/components/badge_form.vue
index 9e09f527a39..0beb6bed9ea 100644
--- a/app/assets/javascripts/badges/components/badge_form.vue
+++ b/app/assets/javascripts/badges/components/badge_form.vue
@@ -179,7 +179,7 @@ export default {
id="badge-link-url"
v-model="linkUrl"
type="URL"
- class="form-control"
+ class="form-control gl-form-input"
required
@input="debouncedPreview"
/>
@@ -194,7 +194,7 @@ export default {
id="badge-image-url"
v-model="imageUrl"
type="URL"
- class="form-control"
+ class="form-control gl-form-input"
required
@input="debouncedPreview"
/>
diff --git a/app/assets/javascripts/badges/components/badge_list.vue b/app/assets/javascripts/badges/components/badge_list.vue
index 04c2d4a7493..811ec6d333b 100644
--- a/app/assets/javascripts/badges/components/badge_list.vue
+++ b/app/assets/javascripts/badges/components/badge_list.vue
@@ -1,8 +1,8 @@
<script>
import { mapState } from 'vuex';
import { GlLoadingIcon } from '@gitlab/ui';
-import BadgeListRow from './badge_list_row.vue';
import { GROUP_BADGE } from '../constants';
+import BadgeListRow from './badge_list_row.vue';
export default {
name: 'BadgeList',
diff --git a/app/assets/javascripts/badges/store/actions.js b/app/assets/javascripts/badges/store/actions.js
index 3377f6c0996..17a03564432 100644
--- a/app/assets/javascripts/badges/store/actions.js
+++ b/app/assets/javascripts/badges/store/actions.js
@@ -1,6 +1,6 @@
import axios from '~/lib/utils/axios_utils';
-import types from './mutation_types';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
+import types from './mutation_types';
export const transformBackendBadge = (badge) => ({
...convertObjectPropsToCamelCase(badge, true),
diff --git a/app/assets/javascripts/badges/store/mutations.js b/app/assets/javascripts/badges/store/mutations.js
index 3f4689aeb17..9e27af21ed6 100644
--- a/app/assets/javascripts/badges/store/mutations.js
+++ b/app/assets/javascripts/badges/store/mutations.js
@@ -1,5 +1,5 @@
-import types from './mutation_types';
import { PROJECT_BADGE } from '../constants';
+import types from './mutation_types';
const reorderBadges = (badges) =>
badges.sort((a, b) => {
diff --git a/app/assets/javascripts/batch_comments/components/draft_note.vue b/app/assets/javascripts/batch_comments/components/draft_note.vue
index 74069b61f07..5564bca6df0 100644
--- a/app/assets/javascripts/batch_comments/components/draft_note.vue
+++ b/app/assets/javascripts/batch_comments/components/draft_note.vue
@@ -3,8 +3,8 @@
import { mapActions, mapGetters, mapState } from 'vuex';
import { GlButton } from '@gitlab/ui';
import NoteableNote from '~/notes/components/noteable_note.vue';
-import PublishButton from './publish_button.vue';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
+import PublishButton from './publish_button.vue';
export default {
components: {
@@ -79,7 +79,6 @@ export default {
</script>
<template>
<article
- role="article"
class="draft-note-component note-wrapper"
@mouseenter="handleMouseEnter(draft)"
@mouseleave="handleMouseLeave(draft)"
diff --git a/app/assets/javascripts/batch_comments/components/preview_item.vue b/app/assets/javascripts/batch_comments/components/preview_item.vue
index 3e93168f0e2..589734df795 100644
--- a/app/assets/javascripts/batch_comments/components/preview_item.vue
+++ b/app/assets/javascripts/batch_comments/components/preview_item.vue
@@ -3,13 +3,13 @@ import { mapGetters } from 'vuex';
import { GlSprintf, GlIcon } from '@gitlab/ui';
import { IMAGE_DIFF_POSITION_TYPE } from '~/diffs/constants';
import { sprintf, __ } from '~/locale';
-import resolvedStatusMixin from '../mixins/resolved_status';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import {
getStartLineNumber,
getEndLineNumber,
getLineClasses,
} from '~/notes/components/multiline_comment_utils';
+import resolvedStatusMixin from '../mixins/resolved_status';
export default {
components: {
diff --git a/app/assets/javascripts/batch_comments/stores/modules/batch_comments/actions.js b/app/assets/javascripts/batch_comments/stores/modules/batch_comments/actions.js
index a29409c52ae..bd7909bfa76 100644
--- a/app/assets/javascripts/batch_comments/stores/modules/batch_comments/actions.js
+++ b/app/assets/javascripts/batch_comments/stores/modules/batch_comments/actions.js
@@ -2,8 +2,8 @@ import { deprecatedCreateFlash as flash } from '~/flash';
import { __ } from '~/locale';
import { scrollToElement } from '~/lib/utils/common_utils';
import service from '../../../services/drafts_service';
-import * as types from './mutation_types';
import { CHANGES_TAB, DISCUSSION_TAB, SHOW_TAB } from '../../../constants';
+import * as types from './mutation_types';
export const saveDraft = ({ dispatch }, draft) =>
dispatch('saveNote', { ...draft, isDraft: true }, { root: true });
@@ -67,13 +67,23 @@ export const publishReview = ({ commit, dispatch, getters }) => {
.catch(() => commit(types.RECEIVE_PUBLISH_REVIEW_ERROR));
};
-export const updateDiscussionsAfterPublish = ({ dispatch, getters, rootGetters }) =>
- dispatch('fetchDiscussions', { path: getters.getNotesData.discussionsPath }, { root: true }).then(
- () =>
- dispatch('diffs/assignDiscussionsToDiff', rootGetters.discussionsStructuredByLineCode, {
- root: true,
- }),
- );
+export const updateDiscussionsAfterPublish = async ({ dispatch, getters, rootGetters }) => {
+ if (window.gon?.features?.paginatedNotes) {
+ await dispatch('stopPolling', null, { root: true });
+ await dispatch('fetchData', null, { root: true });
+ await dispatch('restartPolling', null, { root: true });
+ } else {
+ await dispatch(
+ 'fetchDiscussions',
+ { path: getters.getNotesData.discussionsPath },
+ { root: true },
+ );
+ }
+
+ dispatch('diffs/assignDiscussionsToDiff', rootGetters.discussionsStructuredByLineCode, {
+ root: true,
+ });
+};
export const updateDraft = (
{ commit, getters },
diff --git a/app/assets/javascripts/behaviors/autosize.js b/app/assets/javascripts/behaviors/autosize.js
index a5404539c17..181d841a068 100644
--- a/app/assets/javascripts/behaviors/autosize.js
+++ b/app/assets/javascripts/behaviors/autosize.js
@@ -1,13 +1,11 @@
import Autosize from 'autosize';
import { waitForCSSLoaded } from '~/helpers/startup_css_helper';
-document.addEventListener('DOMContentLoaded', () => {
- waitForCSSLoaded(() => {
- const autosizeEls = document.querySelectorAll('.js-autosize');
+waitForCSSLoaded(() => {
+ const autosizeEls = document.querySelectorAll('.js-autosize');
- Autosize(autosizeEls);
- Autosize.update(autosizeEls);
+ Autosize(autosizeEls);
+ Autosize.update(autosizeEls);
- autosizeEls.forEach((el) => el.classList.add('js-autosize-initialized'));
- });
+ autosizeEls.forEach((el) => el.classList.add('js-autosize-initialized'));
});
diff --git a/app/assets/javascripts/behaviors/index.js b/app/assets/javascripts/behaviors/index.js
index 75659bbf685..669bc90dcb9 100644
--- a/app/assets/javascripts/behaviors/index.js
+++ b/app/assets/javascripts/behaviors/index.js
@@ -25,19 +25,17 @@ initPageShortcuts();
initCollapseSidebarOnWindowResize();
initSelect2Dropdowns();
-document.addEventListener('DOMContentLoaded', () => {
- window.requestIdleCallback(
- () => {
- // Check if we have to Load GFM Input
- const $gfmInputs = $('.js-gfm-input:not(.js-gfm-input-initialized)');
- if ($gfmInputs.length) {
- import(/* webpackChunkName: 'initGFMInput' */ './markdown/gfm_auto_complete')
- .then(({ default: initGFMInput }) => {
- initGFMInput($gfmInputs);
- })
- .catch(() => {});
- }
- },
- { timeout: 500 },
- );
-});
+window.requestIdleCallback(
+ () => {
+ // Check if we have to Load GFM Input
+ const $gfmInputs = $('.js-gfm-input:not(.js-gfm-input-initialized)');
+ if ($gfmInputs.length) {
+ import(/* webpackChunkName: 'initGFMInput' */ './markdown/gfm_auto_complete')
+ .then(({ default: initGFMInput }) => {
+ initGFMInput($gfmInputs);
+ })
+ .catch(() => {});
+ }
+ },
+ { timeout: 500 },
+);
diff --git a/app/assets/javascripts/behaviors/markdown/nodes/table_header_row.js b/app/assets/javascripts/behaviors/markdown/nodes/table_header_row.js
index 6e3c16f0a08..2cb2bb9e7fe 100644
--- a/app/assets/javascripts/behaviors/markdown/nodes/table_header_row.js
+++ b/app/assets/javascripts/behaviors/markdown/nodes/table_header_row.js
@@ -1,7 +1,7 @@
/* eslint-disable class-methods-use-this */
-import TableRow from './table_row';
import { HIGHER_PARSE_RULE_PRIORITY } from '../constants';
+import TableRow from './table_row';
const CENTER_ALIGN = 'center';
diff --git a/app/assets/javascripts/behaviors/markdown/render_gfm.js b/app/assets/javascripts/behaviors/markdown/render_gfm.js
index 5e9d80e1529..e0d5f34ba06 100644
--- a/app/assets/javascripts/behaviors/markdown/render_gfm.js
+++ b/app/assets/javascripts/behaviors/markdown/render_gfm.js
@@ -1,10 +1,10 @@
import $ from 'jquery';
import syntaxHighlight from '~/syntax_highlight';
+import initUserPopovers from '../../user_popovers';
import renderMath from './render_math';
import renderMermaid from './render_mermaid';
import renderMetrics from './render_metrics';
import highlightCurrentUser from './highlight_current_user';
-import initUserPopovers from '../../user_popovers';
// Render GitLab flavoured Markdown
//
diff --git a/app/assets/javascripts/behaviors/markdown/render_mermaid.js b/app/assets/javascripts/behaviors/markdown/render_mermaid.js
index 479782a1f1f..0cb13815c7e 100644
--- a/app/assets/javascripts/behaviors/markdown/render_mermaid.js
+++ b/app/assets/javascripts/behaviors/markdown/render_mermaid.js
@@ -140,7 +140,7 @@ function renderMermaids($els) {
'Warning: Displaying this diagram might cause performance issues on this page.',
)}</div>
<div class="gl-alert-actions">
- <button class="js-lazy-render-mermaid btn gl-alert-action btn-warning btn-md new-gl-button">Display</button>
+ <button class="js-lazy-render-mermaid btn gl-alert-action btn-warning btn-md gl-button">Display</button>
</div>
</div>
<button type="button" class="close" data-dismiss="alert" aria-label="Close">
diff --git a/app/assets/javascripts/behaviors/quick_submit.js b/app/assets/javascripts/behaviors/quick_submit.js
index 68e831252d6..12a2baed6e2 100644
--- a/app/assets/javascripts/behaviors/quick_submit.js
+++ b/app/assets/javascripts/behaviors/quick_submit.js
@@ -1,8 +1,8 @@
import $ from 'jquery';
import '../commons/bootstrap';
-import { isInIssuePage } from '../lib/utils/common_utils';
import { __ } from '~/locale';
import { add, show, hide } from '~/tooltips';
+import { isInIssuePage } from '../lib/utils/common_utils';
// Quick Submit behavior
//
diff --git a/app/assets/javascripts/behaviors/shortcuts/keybindings.js b/app/assets/javascripts/behaviors/shortcuts/keybindings.js
index 10832583783..87e78de99b8 100644
--- a/app/assets/javascripts/behaviors/shortcuts/keybindings.js
+++ b/app/assets/javascripts/behaviors/shortcuts/keybindings.js
@@ -29,6 +29,7 @@ export const customizations = parsedCustomizations;
// All available commands
export const TOGGLE_PERFORMANCE_BAR = 'globalShortcuts.togglePerformanceBar';
+export const TOGGLE_CANARY = 'globalShortcuts.toggleCanary';
/** All keybindings, grouped and ordered with descriptions */
export const keybindingGroups = [
@@ -42,6 +43,12 @@ export const keybindingGroups = [
// eslint-disable-next-line @gitlab/require-i18n-strings
defaultKeys: ['p b'],
},
+ {
+ description: s__('KeyboardShortcuts|Toggle GitLab Next'),
+ command: TOGGLE_CANARY,
+ // eslint-disable-next-line @gitlab/require-i18n-strings
+ defaultKeys: ['g x'],
+ },
],
},
]
diff --git a/app/assets/javascripts/behaviors/shortcuts/shortcuts.js b/app/assets/javascripts/behaviors/shortcuts/shortcuts.js
index 50d2399b312..6cdf083378b 100644
--- a/app/assets/javascripts/behaviors/shortcuts/shortcuts.js
+++ b/app/assets/javascripts/behaviors/shortcuts/shortcuts.js
@@ -3,13 +3,13 @@ import Cookies from 'js-cookie';
import Mousetrap from 'mousetrap';
import Vue from 'vue';
import { flatten } from 'lodash';
-import { disableShortcuts, shouldDisableShortcuts } from './shortcuts_toggle';
-import ShortcutsToggle from './shortcuts_toggle.vue';
+import { parseBoolean, getCspNonceValue } from '~/lib/utils/common_utils';
import axios from '../../lib/utils/axios_utils';
import { refreshCurrentPage, visitUrl } from '../../lib/utils/url_utility';
import findAndFollowLink from '../../lib/utils/navigation_utility';
-import { parseBoolean, getCspNonceValue } from '~/lib/utils/common_utils';
-import { keysFor, TOGGLE_PERFORMANCE_BAR } from './keybindings';
+import { disableShortcuts, shouldDisableShortcuts } from './shortcuts_toggle';
+import ShortcutsToggle from './shortcuts_toggle.vue';
+import { keysFor, TOGGLE_PERFORMANCE_BAR, TOGGLE_CANARY } from './keybindings';
const defaultStopCallback = Mousetrap.prototype.stopCallback;
Mousetrap.prototype.stopCallback = function customStopCallback(e, element, combo) {
@@ -72,6 +72,7 @@ export default class Shortcuts {
Mousetrap.bind('/', Shortcuts.focusSearch);
Mousetrap.bind('f', this.focusFilter.bind(this));
Mousetrap.bind(keysFor(TOGGLE_PERFORMANCE_BAR), Shortcuts.onTogglePerfBar);
+ Mousetrap.bind(keysFor(TOGGLE_CANARY), Shortcuts.onToggleCanary);
const findFileURL = document.body.dataset.findFile;
@@ -124,6 +125,14 @@ export default class Shortcuts {
refreshCurrentPage();
}
+ static onToggleCanary(e) {
+ e.preventDefault();
+ const canaryCookieName = 'gitlab_canary';
+ const currentValue = parseBoolean(Cookies.get(canaryCookieName));
+ Cookies.set(canaryCookieName, (!currentValue).toString(), { expires: 365, path: '/' });
+ refreshCurrentPage();
+ }
+
static toggleMarkdownPreview(e) {
// Check if short-cut was triggered while in Write Mode
const $target = $(e.target);
diff --git a/app/assets/javascripts/behaviors/shortcuts/shortcuts_issuable.js b/app/assets/javascripts/behaviors/shortcuts/shortcuts_issuable.js
index 5e8ddeb6af7..14b6ca4474b 100644
--- a/app/assets/javascripts/behaviors/shortcuts/shortcuts_issuable.js
+++ b/app/assets/javascripts/behaviors/shortcuts/shortcuts_issuable.js
@@ -1,11 +1,11 @@
import $ from 'jquery';
import Mousetrap from 'mousetrap';
-import Sidebar from '../../right_sidebar';
-import Shortcuts from './shortcuts';
-import { CopyAsGFM } from '../markdown/copy_as_gfm';
import { getSelectedFragment } from '~/lib/utils/common_utils';
import { isElementVisible } from '~/lib/utils/dom_utils';
import { clickCopyToClipboardButton } from '~/behaviors/copy_to_clipboard';
+import { CopyAsGFM } from '../markdown/copy_as_gfm';
+import Sidebar from '../../right_sidebar';
+import Shortcuts from './shortcuts';
export default class ShortcutsIssuable extends Shortcuts {
constructor() {
diff --git a/app/assets/javascripts/behaviors/shortcuts/shortcuts_wiki.js b/app/assets/javascripts/behaviors/shortcuts/shortcuts_wiki.js
index 8b7e6a56d25..c609936a02a 100644
--- a/app/assets/javascripts/behaviors/shortcuts/shortcuts_wiki.js
+++ b/app/assets/javascripts/behaviors/shortcuts/shortcuts_wiki.js
@@ -1,6 +1,6 @@
import Mousetrap from 'mousetrap';
-import ShortcutsNavigation from './shortcuts_navigation';
import findAndFollowLink from '../../lib/utils/navigation_utility';
+import ShortcutsNavigation from './shortcuts_navigation';
export default class ShortcutsWiki extends ShortcutsNavigation {
constructor() {
diff --git a/app/assets/javascripts/blob/balsamiq_viewer.js b/app/assets/javascripts/blob/balsamiq_viewer.js
index fc86f630c4e..c9152db509a 100644
--- a/app/assets/javascripts/blob/balsamiq_viewer.js
+++ b/app/assets/javascripts/blob/balsamiq_viewer.js
@@ -1,6 +1,6 @@
+import { __ } from '~/locale';
import { deprecatedCreateFlash as Flash } from '../flash';
import BalsamiqViewer from './balsamiq/balsamiq_viewer';
-import { __ } from '~/locale';
function onError() {
const flash = new Flash(__('Balsamiq file could not be loaded.'));
diff --git a/app/assets/javascripts/blob/blob_file_dropzone.js b/app/assets/javascripts/blob/blob_file_dropzone.js
index 19bad64155d..f3e273bf082 100644
--- a/app/assets/javascripts/blob/blob_file_dropzone.js
+++ b/app/assets/javascripts/blob/blob_file_dropzone.js
@@ -2,10 +2,10 @@
import $ from 'jquery';
import Dropzone from 'dropzone';
+import { sprintf, __ } from '~/locale';
import { visitUrl } from '../lib/utils/url_utility';
import { HIDDEN_CLASS } from '../lib/utils/constants';
import csrf from '../lib/utils/csrf';
-import { sprintf, __ } from '~/locale';
Dropzone.autoDiscover = false;
diff --git a/app/assets/javascripts/blob/components/blob_header.vue b/app/assets/javascripts/blob/components/blob_header.vue
index a4a43b7a94e..8960d45b29c 100644
--- a/app/assets/javascripts/blob/components/blob_header.vue
+++ b/app/assets/javascripts/blob/components/blob_header.vue
@@ -71,7 +71,7 @@ export default {
</template>
</blob-filepath>
- <div class="gl-display-none gl-display-sm-flex">
+ <div class="gl-display-none gl-sm-display-flex">
<viewer-switcher v-if="showViewerSwitcher" v-model="viewer" />
<slot name="actions"></slot>
diff --git a/app/assets/javascripts/blob/template_selectors/ci_syntax_yaml_selector.js b/app/assets/javascripts/blob/template_selectors/ci_syntax_yaml_selector.js
index 9370e170571..c30ff4f1290 100644
--- a/app/assets/javascripts/blob/template_selectors/ci_syntax_yaml_selector.js
+++ b/app/assets/javascripts/blob/template_selectors/ci_syntax_yaml_selector.js
@@ -1,5 +1,5 @@
-import FileTemplateSelector from '../file_template_selector';
import initDeprecatedJQueryDropdown from '~/deprecated_jquery_dropdown';
+import FileTemplateSelector from '../file_template_selector';
export default class BlobCiSyntaxYamlSelector extends FileTemplateSelector {
constructor({ mediator }) {
diff --git a/app/assets/javascripts/blob/template_selectors/ci_yaml_selector.js b/app/assets/javascripts/blob/template_selectors/ci_yaml_selector.js
index 3879a6c5742..0cdfd153675 100644
--- a/app/assets/javascripts/blob/template_selectors/ci_yaml_selector.js
+++ b/app/assets/javascripts/blob/template_selectors/ci_yaml_selector.js
@@ -1,5 +1,5 @@
-import FileTemplateSelector from '../file_template_selector';
import initDeprecatedJQueryDropdown from '~/deprecated_jquery_dropdown';
+import FileTemplateSelector from '../file_template_selector';
export default class BlobCiYamlSelector extends FileTemplateSelector {
constructor({ mediator }) {
diff --git a/app/assets/javascripts/blob/template_selectors/dockerfile_selector.js b/app/assets/javascripts/blob/template_selectors/dockerfile_selector.js
index 5d976c5acdb..42c7a4bd408 100644
--- a/app/assets/javascripts/blob/template_selectors/dockerfile_selector.js
+++ b/app/assets/javascripts/blob/template_selectors/dockerfile_selector.js
@@ -1,6 +1,6 @@
-import FileTemplateSelector from '../file_template_selector';
import { __ } from '~/locale';
import initDeprecatedJQueryDropdown from '~/deprecated_jquery_dropdown';
+import FileTemplateSelector from '../file_template_selector';
export default class DockerfileSelector extends FileTemplateSelector {
constructor({ mediator }) {
diff --git a/app/assets/javascripts/blob/template_selectors/gitignore_selector.js b/app/assets/javascripts/blob/template_selectors/gitignore_selector.js
index 1bb1cbb74de..50a11692e98 100644
--- a/app/assets/javascripts/blob/template_selectors/gitignore_selector.js
+++ b/app/assets/javascripts/blob/template_selectors/gitignore_selector.js
@@ -1,5 +1,5 @@
-import FileTemplateSelector from '../file_template_selector';
import initDeprecatedJQueryDropdown from '~/deprecated_jquery_dropdown';
+import FileTemplateSelector from '../file_template_selector';
export default class BlobGitignoreSelector extends FileTemplateSelector {
constructor({ mediator }) {
diff --git a/app/assets/javascripts/blob/template_selectors/license_selector.js b/app/assets/javascripts/blob/template_selectors/license_selector.js
index affa20997e9..4ae5dc70a70 100644
--- a/app/assets/javascripts/blob/template_selectors/license_selector.js
+++ b/app/assets/javascripts/blob/template_selectors/license_selector.js
@@ -1,5 +1,5 @@
-import FileTemplateSelector from '../file_template_selector';
import initDeprecatedJQueryDropdown from '~/deprecated_jquery_dropdown';
+import FileTemplateSelector from '../file_template_selector';
export default class BlobLicenseSelector extends FileTemplateSelector {
constructor({ mediator }) {
diff --git a/app/assets/javascripts/blob/template_selectors/metrics_dashboard_selector.js b/app/assets/javascripts/blob/template_selectors/metrics_dashboard_selector.js
index 42adab05ce3..8b10b02ae1d 100644
--- a/app/assets/javascripts/blob/template_selectors/metrics_dashboard_selector.js
+++ b/app/assets/javascripts/blob/template_selectors/metrics_dashboard_selector.js
@@ -1,5 +1,5 @@
-import FileTemplateSelector from '../file_template_selector';
import initDeprecatedJQueryDropdown from '~/deprecated_jquery_dropdown';
+import FileTemplateSelector from '../file_template_selector';
export default class MetricsDashboardSelector extends FileTemplateSelector {
constructor({ mediator }) {
diff --git a/app/assets/javascripts/blob/template_selectors/type_selector.js b/app/assets/javascripts/blob/template_selectors/type_selector.js
index f74f7535d99..65e7ff0594c 100644
--- a/app/assets/javascripts/blob/template_selectors/type_selector.js
+++ b/app/assets/javascripts/blob/template_selectors/type_selector.js
@@ -1,5 +1,5 @@
-import FileTemplateSelector from '../file_template_selector';
import initDeprecatedJQueryDropdown from '~/deprecated_jquery_dropdown';
+import FileTemplateSelector from '../file_template_selector';
export default class FileTemplateTypeSelector extends FileTemplateSelector {
constructor({ mediator, dropdownData }) {
diff --git a/app/assets/javascripts/blob/viewer/index.js b/app/assets/javascripts/blob/viewer/index.js
index 4e6ec20ec64..3a55e18d2ff 100644
--- a/app/assets/javascripts/blob/viewer/index.js
+++ b/app/assets/javascripts/blob/viewer/index.js
@@ -1,11 +1,11 @@
import $ from 'jquery';
import '~/behaviors/markdown/render_gfm';
+import { __ } from '~/locale';
+import { fixTitle } from '~/tooltips';
import { deprecatedCreateFlash as Flash } from '../../flash';
import { handleLocationHash } from '../../lib/utils/common_utils';
import axios from '../../lib/utils/axios_utils';
import eventHub from '../../notes/event_hub';
-import { __ } from '~/locale';
-import { fixTitle } from '~/tooltips';
const loadRichBlobViewer = (type) => {
switch (type) {
diff --git a/app/assets/javascripts/blob_edit/blob_bundle.js b/app/assets/javascripts/blob_edit/blob_bundle.js
index 7c2217a59e9..76c88efc12b 100644
--- a/app/assets/javascripts/blob_edit/blob_bundle.js
+++ b/app/assets/javascripts/blob_edit/blob_bundle.js
@@ -1,12 +1,12 @@
/* eslint-disable no-new */
import $ from 'jquery';
-import NewCommitForm from '../new_commit_form';
import { deprecatedCreateFlash as createFlash } from '~/flash';
-import BlobFileDropzone from '../blob/blob_file_dropzone';
import initPopover from '~/blob/suggest_gitlab_ci_yml';
import { disableButtonIfEmptyField, setCookie } from '~/lib/utils/common_utils';
import Tracking from '~/tracking';
+import BlobFileDropzone from '../blob/blob_file_dropzone';
+import NewCommitForm from '../new_commit_form';
const initPopovers = () => {
const suggestEl = document.querySelector('.js-suggest-gitlab-ci-yml');
diff --git a/app/assets/javascripts/blob_edit/edit_blob.js b/app/assets/javascripts/blob_edit/edit_blob.js
index c7f66a357f3..f3b0f4ab57c 100644
--- a/app/assets/javascripts/blob_edit/edit_blob.js
+++ b/app/assets/javascripts/blob_edit/edit_blob.js
@@ -1,12 +1,12 @@
import $ from 'jquery';
import axios from '~/lib/utils/axios_utils';
import { deprecatedCreateFlash as createFlash } from '~/flash';
-import { BLOB_EDITOR_ERROR, BLOB_PREVIEW_ERROR } from './constants';
-import TemplateSelectorMediator from '../blob/file_template_mediator';
import { addEditorMarkdownListeners } from '~/lib/utils/text_markdown';
import EditorLite from '~/editor/editor_lite';
import { FileTemplateExtension } from '~/editor/extensions/editor_file_template_ext';
import { insertFinalNewline } from '~/lib/utils/text_utility';
+import TemplateSelectorMediator from '../blob/file_template_mediator';
+import { BLOB_EDITOR_ERROR, BLOB_PREVIEW_ERROR } from './constants';
export default class EditBlob {
// The options object has:
diff --git a/app/assets/javascripts/boards/boards_util.js b/app/assets/javascripts/boards/boards_util.js
index 965d3571f42..13ad820477f 100644
--- a/app/assets/javascripts/boards/boards_util.js
+++ b/app/assets/javascripts/boards/boards_util.js
@@ -1,6 +1,6 @@
import { sortBy } from 'lodash';
-import { ListType } from './constants';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
+import { ListType, NOT_FILTER } from './constants';
export function getMilestone() {
return null;
@@ -144,6 +144,17 @@ export function isListDraggable(list) {
return list.listType !== ListType.backlog && list.listType !== ListType.closed;
}
+export function transformNotFilters(filters) {
+ return Object.keys(filters)
+ .filter((key) => key.startsWith(NOT_FILTER))
+ .reduce((obj, key) => {
+ return {
+ ...obj,
+ [key.substring(4, key.length - 1)]: filters[key],
+ };
+ }, {});
+}
+
// EE-specific feature. Find the implementation in the `ee/`-folder
export function transformBoardConfig() {
return '';
@@ -157,4 +168,5 @@ export default {
fullLabelId,
fullIterationId,
isListDraggable,
+ transformNotFilters,
};
diff --git a/app/assets/javascripts/boards/components/board_add_new_column_trigger.vue b/app/assets/javascripts/boards/components/board_add_new_column_trigger.vue
new file mode 100644
index 00000000000..ea68df9ce12
--- /dev/null
+++ b/app/assets/javascripts/boards/components/board_add_new_column_trigger.vue
@@ -0,0 +1,21 @@
+<script>
+import { GlButton } from '@gitlab/ui';
+import { mapActions } from 'vuex';
+
+export default {
+ components: {
+ GlButton,
+ },
+ methods: {
+ ...mapActions(['setAddColumnFormVisibility']),
+ },
+};
+</script>
+
+<template>
+ <span class="gl-ml-4">
+ <gl-button variant="success" @click="setAddColumnFormVisibility(true)"
+ >{{ __('Create list') }}
+ </gl-button>
+ </span>
+</template>
diff --git a/app/assets/javascripts/boards/components/board_assignee_dropdown.vue b/app/assets/javascripts/boards/components/board_assignee_dropdown.vue
index 5d381f9a570..c5cb61ae21c 100644
--- a/app/assets/javascripts/boards/components/board_assignee_dropdown.vue
+++ b/app/assets/javascripts/boards/components/board_assignee_dropdown.vue
@@ -39,50 +39,51 @@ export default {
data() {
return {
search: '',
- participants: [],
+ issueParticipants: [],
selected: [],
};
},
apollo: {
- participants: {
- query() {
- return this.isSearchEmpty ? getIssueParticipants : searchUsers;
- },
+ issueParticipants: {
+ query: getIssueParticipants,
variables() {
- if (this.isSearchEmpty) {
- return {
- id: `gid://gitlab/Issue/${this.activeIssue.iid}`,
- };
- }
-
return {
- search: this.search,
+ id: `gid://gitlab/Issue/${this.activeIssue.iid}`,
};
},
update(data) {
- if (this.isSearchEmpty) {
- return data.issue?.participants?.nodes || [];
- }
-
- return data.users?.nodes || [];
+ return data.issue?.participants?.nodes || [];
},
- debounce() {
- const { noSearchDelay, searchDelay } = this.$options;
-
- return this.isSearchEmpty ? noSearchDelay : searchDelay;
+ },
+ searchUsers: {
+ query: searchUsers,
+ variables() {
+ return {
+ search: this.search,
+ };
},
+ update: (data) => data.users?.nodes || [],
+ skip() {
+ return this.isSearchEmpty;
+ },
+ debounce: 250,
},
},
computed: {
...mapGetters(['activeIssue']),
...mapState(['isSettingAssignees']),
+ participants() {
+ return this.isSearchEmpty ? this.issueParticipants : this.searchUsers;
+ },
assigneeText() {
return n__('Assignee', '%d Assignees', this.selected.length);
},
unSelectedFiltered() {
- return this.participants.filter(({ username }) => {
- return !this.selectedUserNames.includes(username);
- });
+ return (
+ this.participants?.filter(({ username }) => {
+ return !this.selectedUserNames.includes(username);
+ }) || []
+ );
},
selectedIsEmpty() {
return this.selected.length === 0;
@@ -96,6 +97,11 @@ export default {
currentUser() {
return gon?.current_username;
},
+ isLoading() {
+ return (
+ this.$apollo.queries.issueParticipants?.loading || this.$apollo.queries.searchUsers?.loading
+ );
+ },
},
created() {
this.selected = cloneDeep(this.activeIssue.assignees);
@@ -147,7 +153,7 @@ export default {
<gl-search-box-by-type v-model.trim="search" />
</template>
<template #items>
- <gl-loading-icon v-if="$apollo.queries.participants.loading" size="lg" />
+ <gl-loading-icon v-if="isLoading" size="lg" />
<template v-else>
<gl-dropdown-item
:is-checked="selectedIsEmpty"
diff --git a/app/assets/javascripts/boards/components/board_card.vue b/app/assets/javascripts/boards/components/board_card.vue
index 31050eef83d..e6009343626 100644
--- a/app/assets/javascripts/boards/components/board_card.vue
+++ b/app/assets/javascripts/boards/components/board_card.vue
@@ -1,13 +1,14 @@
<script>
-import BoardCardLayout from './board_card_layout.vue';
-import eventHub from '../eventhub';
import sidebarEventHub from '~/sidebar/event_hub';
+import eventHub from '../eventhub';
import boardsStore from '../stores/boards_store';
+import BoardCardLayout from './board_card_layout.vue';
+import BoardCardLayoutDeprecated from './board_card_layout_deprecated.vue';
export default {
name: 'BoardsIssueCard',
components: {
- BoardCardLayout,
+ BoardCardLayout: gon.features?.graphqlBoardLists ? BoardCardLayout : BoardCardLayoutDeprecated,
},
props: {
list: {
diff --git a/app/assets/javascripts/boards/components/board_card_layout.vue b/app/assets/javascripts/boards/components/board_card_layout.vue
index 0a2301394c1..5e3c3702519 100644
--- a/app/assets/javascripts/boards/components/board_card_layout.vue
+++ b/app/assets/javascripts/boards/components/board_card_layout.vue
@@ -1,17 +1,13 @@
<script>
-import { mapActions, mapGetters } from 'vuex';
-import IssueCardInner from './issue_card_inner.vue';
-import IssueCardInnerDeprecated from './issue_card_inner_deprecated.vue';
-import boardsStore from '../stores/boards_store';
-import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
+import { mapActions, mapGetters, mapState } from 'vuex';
import { ISSUABLE } from '~/boards/constants';
+import IssueCardInner from './issue_card_inner.vue';
export default {
name: 'BoardCardLayout',
components: {
- IssueCardInner: gon.features?.graphqlBoardLists ? IssueCardInner : IssueCardInnerDeprecated,
+ IssueCardInner,
},
- mixins: [glFeatureFlagMixin()],
props: {
list: {
type: Object,
@@ -42,17 +38,17 @@ export default {
data() {
return {
showDetail: false,
- multiSelect: boardsStore.multiSelect,
};
},
computed: {
+ ...mapState(['selectedBoardItems']),
...mapGetters(['isSwimlanesOn']),
multiSelectVisible() {
- return this.multiSelect.list.findIndex((issue) => issue.id === this.issue.id) > -1;
+ return this.selectedBoardItems.findIndex((boardItem) => boardItem.id === this.issue.id) > -1;
},
},
methods: {
- ...mapActions(['setActiveId']),
+ ...mapActions(['setActiveId', 'toggleBoardItemMultiSelection']),
mouseDown() {
this.showDetail = true;
},
@@ -63,16 +59,16 @@ export default {
// Don't do anything if this happened on a no trigger element
if (e.target.classList.contains('js-no-trigger')) return;
- if (this.glFeatures.graphqlBoardLists || this.isSwimlanesOn) {
+ const isMultiSelect = e.ctrlKey || e.metaKey;
+
+ if (!isMultiSelect) {
this.setActiveId({ id: this.issue.id, sidebarType: ISSUABLE });
- return;
+ } else {
+ this.toggleBoardItemMultiSelection(this.issue);
}
- const isMultiSelect = e.ctrlKey || e.metaKey;
-
if (this.showDetail || isMultiSelect) {
this.showDetail = false;
- this.$emit('show', { event: e, isMultiSelect });
}
},
},
diff --git a/app/assets/javascripts/boards/components/board_card_layout_deprecated.vue b/app/assets/javascripts/boards/components/board_card_layout_deprecated.vue
new file mode 100644
index 00000000000..78a581690fd
--- /dev/null
+++ b/app/assets/javascripts/boards/components/board_card_layout_deprecated.vue
@@ -0,0 +1,102 @@
+<script>
+import { mapActions, mapGetters } from 'vuex';
+import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
+import { ISSUABLE } from '~/boards/constants';
+import boardsStore from '../stores/boards_store';
+import IssueCardInner from './issue_card_inner.vue';
+import IssueCardInnerDeprecated from './issue_card_inner_deprecated.vue';
+
+export default {
+ name: 'BoardCardLayout',
+ components: {
+ IssueCardInner: gon.features?.graphqlBoardLists ? IssueCardInner : IssueCardInnerDeprecated,
+ },
+ mixins: [glFeatureFlagMixin()],
+ props: {
+ list: {
+ type: Object,
+ default: () => ({}),
+ required: false,
+ },
+ issue: {
+ type: Object,
+ default: () => ({}),
+ required: false,
+ },
+ disabled: {
+ type: Boolean,
+ default: false,
+ required: false,
+ },
+ index: {
+ type: Number,
+ default: 0,
+ required: false,
+ },
+ isActive: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ },
+ data() {
+ return {
+ showDetail: false,
+ multiSelect: boardsStore.multiSelect,
+ };
+ },
+ computed: {
+ ...mapGetters(['isSwimlanesOn']),
+ multiSelectVisible() {
+ return this.multiSelect.list.findIndex((issue) => issue.id === this.issue.id) > -1;
+ },
+ },
+ methods: {
+ ...mapActions(['setActiveId']),
+ mouseDown() {
+ this.showDetail = true;
+ },
+ mouseMove() {
+ this.showDetail = false;
+ },
+ showIssue(e) {
+ // Don't do anything if this happened on a no trigger element
+ if (e.target.classList.contains('js-no-trigger')) return;
+
+ if (this.glFeatures.graphqlBoardLists || this.isSwimlanesOn) {
+ this.setActiveId({ id: this.issue.id, sidebarType: ISSUABLE });
+ return;
+ }
+
+ const isMultiSelect = e.ctrlKey || e.metaKey;
+
+ if (this.showDetail || isMultiSelect) {
+ this.showDetail = false;
+ this.$emit('show', { event: e, isMultiSelect });
+ }
+ },
+ },
+};
+</script>
+
+<template>
+ <li
+ :class="{
+ 'multi-select': multiSelectVisible,
+ 'user-can-drag': !disabled && issue.id,
+ 'is-disabled': disabled || !issue.id,
+ 'is-active': isActive,
+ }"
+ :index="index"
+ :data-issue-id="issue.id"
+ :data-issue-iid="issue.iid"
+ :data-issue-path="issue.referencePath"
+ data-testid="board_card"
+ class="board-card gl-p-5 gl-rounded-base"
+ @mousedown="mouseDown"
+ @mousemove="mouseMove"
+ @mouseup="showIssue($event)"
+ >
+ <issue-card-inner :list="list" :issue="issue" :update-filters="true" />
+ </li>
+</template>
diff --git a/app/assets/javascripts/boards/components/board_column.vue b/app/assets/javascripts/boards/components/board_column.vue
index 9f0eef844f6..2586351208c 100644
--- a/app/assets/javascripts/boards/components/board_column.vue
+++ b/app/assets/javascripts/boards/components/board_column.vue
@@ -1,8 +1,8 @@
<script>
import { mapGetters, mapActions, mapState } from 'vuex';
import BoardListHeader from 'ee_else_ce/boards/components/board_list_header.vue';
-import BoardList from './board_list.vue';
import { isListDraggable } from '../boards_util';
+import BoardList from './board_list.vue';
export default {
components: {
diff --git a/app/assets/javascripts/boards/components/board_column_deprecated.vue b/app/assets/javascripts/boards/components/board_column_deprecated.vue
index 35688efceb4..a8f1577d092 100644
--- a/app/assets/javascripts/boards/components/board_column_deprecated.vue
+++ b/app/assets/javascripts/boards/components/board_column_deprecated.vue
@@ -2,9 +2,9 @@
// This component is being replaced in favor of './board_column.vue' for GraphQL boards
import Sortable from 'sortablejs';
import BoardListHeader from 'ee_else_ce/boards/components/board_list_header_deprecated.vue';
-import BoardList from './board_list_deprecated.vue';
import boardsStore from '../stores/boards_store';
import { getBoardSortableDefaultOptions, sortableEnd } from '../mixins/sortable_default_options';
+import BoardList from './board_list_deprecated.vue';
export default {
components: {
@@ -46,6 +46,7 @@ export default {
watch: {
filter: {
handler() {
+ // eslint-disable-next-line vue/no-mutating-props
this.list.page = 1;
this.list.getIssues(true).catch(() => {
// TODO: handle request error
diff --git a/app/assets/javascripts/boards/components/board_configuration_options.vue b/app/assets/javascripts/boards/components/board_configuration_options.vue
index b8ee930a8c9..4d79f2a4bc6 100644
--- a/app/assets/javascripts/boards/components/board_configuration_options.vue
+++ b/app/assets/javascripts/boards/components/board_configuration_options.vue
@@ -14,6 +14,10 @@ export default {
type: Boolean,
required: true,
},
+ readonly: {
+ type: Boolean,
+ required: true,
+ },
},
};
</script>
@@ -28,12 +32,14 @@ export default {
</p>
<gl-form-checkbox
:checked="!hideBacklogList"
+ :disabled="readonly"
data-testid="backlog-list-checkbox"
@change="$emit('update:hideBacklogList', !hideBacklogList)"
>{{ __('Show the Open list') }}
</gl-form-checkbox>
<gl-form-checkbox
:checked="!hideClosedList"
+ :disabled="readonly"
data-testid="closed-list-checkbox"
@change="$emit('update:hideClosedList', !hideClosedList)"
>{{ __('Show the Closed list') }}
diff --git a/app/assets/javascripts/boards/components/board_content.vue b/app/assets/javascripts/boards/components/board_content.vue
index 19254343208..49c6a144b1a 100644
--- a/app/assets/javascripts/boards/components/board_content.vue
+++ b/app/assets/javascripts/boards/components/board_content.vue
@@ -3,11 +3,11 @@ import Draggable from 'vuedraggable';
import { mapState, mapGetters, mapActions } from 'vuex';
import { sortBy } from 'lodash';
import { GlAlert } from '@gitlab/ui';
-import BoardColumnDeprecated from './board_column_deprecated.vue';
-import BoardColumn from './board_column.vue';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import defaultSortableConfig from '~/sortable/sortable_config';
import { sortableEnd, sortableStart } from '~/boards/mixins/sortable_default_options';
+import BoardColumn from './board_column.vue';
+import BoardColumnDeprecated from './board_column_deprecated.vue';
export default {
components: {
diff --git a/app/assets/javascripts/boards/components/board_form.vue b/app/assets/javascripts/boards/components/board_form.vue
index c701ecd3040..879f62ee6ff 100644
--- a/app/assets/javascripts/boards/components/board_form.vue
+++ b/app/assets/javascripts/boards/components/board_form.vue
@@ -8,10 +8,10 @@ import { convertToGraphQLId } from '~/graphql_shared/utils';
import boardsStore from '~/boards/stores/boards_store';
import { fullLabelId, fullBoardId } from '../boards_util';
-import BoardConfigurationOptions from './board_configuration_options.vue';
import updateBoardMutation from '../graphql/board_update.mutation.graphql';
import createBoardMutation from '../graphql/board_create.mutation.graphql';
import destroyBoardMutation from '../graphql/board_destroy.mutation.graphql';
+import BoardConfigurationOptions from './board_configuration_options.vue';
const boardDefaults = {
id: false,
@@ -308,6 +308,7 @@ export default {
<board-configuration-options
:hide-backlog-list.sync="board.hide_backlog_list"
:hide-closed-list.sync="board.hide_closed_list"
+ :readonly="readonly"
/>
<board-scope
diff --git a/app/assets/javascripts/boards/components/board_list.vue b/app/assets/javascripts/boards/components/board_list.vue
index b6e4d0980fa..d3c12d2b86d 100644
--- a/app/assets/javascripts/boards/components/board_list.vue
+++ b/app/assets/javascripts/boards/components/board_list.vue
@@ -4,10 +4,10 @@ import { mapActions, mapState } from 'vuex';
import { GlLoadingIcon } from '@gitlab/ui';
import defaultSortableConfig from '~/sortable/sortable_config';
import { sortableStart, sortableEnd } from '~/boards/mixins/sortable_default_options';
+import { sprintf, __ } from '~/locale';
+import eventHub from '../eventhub';
import BoardNewIssue from './board_new_issue.vue';
import BoardCard from './board_card.vue';
-import eventHub from '../eventhub';
-import { sprintf, __ } from '~/locale';
export default {
name: 'BoardList',
diff --git a/app/assets/javascripts/boards/components/board_list_deprecated.vue b/app/assets/javascripts/boards/components/board_list_deprecated.vue
index 24900346bda..72d98149153 100644
--- a/app/assets/javascripts/boards/components/board_list_deprecated.vue
+++ b/app/assets/javascripts/boards/components/board_list_deprecated.vue
@@ -1,17 +1,18 @@
<script>
import { Sortable, MultiDrag } from 'sortablejs';
import { GlLoadingIcon } from '@gitlab/ui';
-import boardNewIssue from './board_new_issue_deprecated.vue';
-import boardCard from './board_card.vue';
-import eventHub from '../eventhub';
-import boardsStore from '../stores/boards_store';
import { sprintf, __ } from '~/locale';
import { deprecatedCreateFlash as createFlash } from '~/flash';
+import { BV_HIDE_TOOLTIP } from '~/lib/utils/constants';
+import eventHub from '../eventhub';
+import boardsStore from '../stores/boards_store';
import {
getBoardSortableDefaultOptions,
sortableStart,
sortableEnd,
} from '../mixins/sortable_default_options';
+import boardCard from './board_card.vue';
+import boardNewIssue from './board_new_issue_deprecated.vue';
// This component is being replaced in favor of './board_list.vue' for GraphQL boards
@@ -63,6 +64,7 @@ export default {
watch: {
filters: {
handler() {
+ // eslint-disable-next-line vue/no-mutating-props
this.list.loadingMore = false;
this.$refs.list.scrollTop = 0;
},
@@ -75,6 +77,7 @@ export default {
this.list.issuesSize > this.list.issues.length &&
this.list.isExpanded
) {
+ // eslint-disable-next-line vue/no-mutating-props
this.list.page += 1;
this.list.getIssues(false).catch(() => {
// TODO: handle request error
@@ -165,7 +168,7 @@ export default {
boardsStore.startMoving(list, issue);
- this.$root.$emit('bv::hide::tooltip');
+ this.$root.$emit(BV_HIDE_TOOLTIP);
sortableStart();
},
@@ -283,6 +286,7 @@ export default {
* issue indexes are far apart, this logic should ever kick in.
*/
setTimeout(() => {
+ // eslint-disable-next-line vue/no-mutating-props
this.list.issues.splice(i, 1);
}, 0);
});
@@ -386,10 +390,12 @@ export default {
loadNextPage() {
const getIssues = this.list.nextPage();
const loadingDone = () => {
+ // eslint-disable-next-line vue/no-mutating-props
this.list.loadingMore = false;
};
if (getIssues) {
+ // eslint-disable-next-line vue/no-mutating-props
this.list.loadingMore = true;
getIssues.then(loadingDone).catch(loadingDone);
}
diff --git a/app/assets/javascripts/boards/components/board_list_header.vue b/app/assets/javascripts/boards/components/board_list_header.vue
index 06f39eceb08..dc779609b86 100644
--- a/app/assets/javascripts/boards/components/board_list_header.vue
+++ b/app/assets/javascripts/boards/components/board_list_header.vue
@@ -10,13 +10,14 @@ import {
GlTooltipDirective,
} from '@gitlab/ui';
import { n__, s__, __ } from '~/locale';
-import AccessorUtilities from '../../lib/utils/accessor';
-import IssueCount from './issue_count.vue';
-import eventHub from '../eventhub';
import sidebarEventHub from '~/sidebar/event_hub';
-import { inactiveId, LIST, ListType } from '../constants';
import { isScopedLabel } from '~/lib/utils/common_utils';
import { isListDraggable } from '~/boards/boards_util';
+import { BV_HIDE_TOOLTIP } from '~/lib/utils/constants';
+import { inactiveId, LIST, ListType } from '../constants';
+import eventHub from '../eventhub';
+import AccessorUtilities from '../../lib/utils/accessor';
+import IssueCount from './issue_count.vue';
export default {
i18n: {
@@ -85,16 +86,16 @@ export default {
return !this.disabled && this.listType !== ListType.closed;
},
showMilestoneListDetails() {
- return (
- this.listType === ListType.milestone &&
- this.list.milestone &&
- (!this.list.collapsed || !this.isSwimlanesHeader)
- );
+ return this.listType === ListType.milestone && this.list.milestone && this.showListDetails;
},
showAssigneeListDetails() {
- return (
- this.listType === ListType.assignee && (!this.list.collapsed || !this.isSwimlanesHeader)
- );
+ return this.listType === ListType.assignee && this.showListDetails;
+ },
+ showIterationListDetails() {
+ return this.listType === ListType.iteration && this.showListDetails;
+ },
+ showListDetails() {
+ return !this.list.collapsed || !this.isSwimlanesHeader;
},
issuesCount() {
return this.list.issuesCount;
@@ -147,6 +148,7 @@ export default {
eventHub.$emit(`toggle-issue-form-${this.list.id}`);
},
toggleExpanded() {
+ // eslint-disable-next-line vue/no-mutating-props
this.list.collapsed = !this.list.collapsed;
if (!this.isLoggedIn) {
@@ -157,7 +159,7 @@ export default {
// When expanding/collapsing, the tooltip on the caret button sometimes stays open.
// Close all tooltips manually to prevent dangling tooltips.
- this.$root.$emit('bv::hide::tooltip');
+ this.$root.$emit(BV_HIDE_TOOLTIP);
},
addToLocalStorage() {
if (AccessorUtilities.isLocalStorageAccessSafe()) {
@@ -216,6 +218,17 @@ export default {
<gl-icon name="timer" />
</span>
+ <span
+ v-if="showIterationListDetails"
+ aria-hidden="true"
+ :class="{
+ 'gl-mt-3 gl-rotate-90': list.collapsed,
+ 'gl-mr-2': !list.collapsed,
+ }"
+ >
+ <gl-icon name="iteration" />
+ </span>
+
<a
v-if="showAssigneeListDetails"
:href="list.assignee.webUrl"
diff --git a/app/assets/javascripts/boards/components/board_list_header_deprecated.vue b/app/assets/javascripts/boards/components/board_list_header_deprecated.vue
index 21147f1616c..54cc56dcbeb 100644
--- a/app/assets/javascripts/boards/components/board_list_header_deprecated.vue
+++ b/app/assets/javascripts/boards/components/board_list_header_deprecated.vue
@@ -10,13 +10,14 @@ import {
GlTooltipDirective,
} from '@gitlab/ui';
import { n__, s__ } from '~/locale';
+import sidebarEventHub from '~/sidebar/event_hub';
+import { isScopedLabel } from '~/lib/utils/common_utils';
+import { BV_HIDE_TOOLTIP } from '~/lib/utils/constants';
import AccessorUtilities from '../../lib/utils/accessor';
-import IssueCount from './issue_count.vue';
import boardsStore from '../stores/boards_store';
import eventHub from '../eventhub';
-import sidebarEventHub from '~/sidebar/event_hub';
import { inactiveId, LIST, ListType } from '../constants';
-import { isScopedLabel } from '~/lib/utils/common_utils';
+import IssueCount from './issue_count.vue';
// This component is being replaced in favor of './board_list_header.vue' for GraphQL boards
@@ -77,14 +78,16 @@ export default {
return !this.disabled && this.listType !== ListType.closed;
},
showMilestoneListDetails() {
- return (
- this.list.type === 'milestone' &&
- this.list.milestone &&
- (this.list.isExpanded || !this.isSwimlanesHeader)
- );
+ return this.list.type === 'milestone' && this.list.milestone && this.showListDetails;
},
showAssigneeListDetails() {
- return this.list.type === 'assignee' && (this.list.isExpanded || !this.isSwimlanesHeader);
+ return this.list.type === 'assignee' && this.showListDetails;
+ },
+ showIterationListDetails() {
+ return this.listType === ListType.iteration && this.showListDetails;
+ },
+ showListDetails() {
+ return this.list.isExpanded || !this.isSwimlanesHeader;
},
issuesCount() {
return this.list.issuesSize;
@@ -131,6 +134,7 @@ export default {
eventHub.$emit(`toggle-issue-form-${this.list.id}`);
},
toggleExpanded() {
+ // eslint-disable-next-line vue/no-mutating-props
this.list.isExpanded = !this.list.isExpanded;
if (!this.isLoggedIn) {
@@ -141,7 +145,7 @@ export default {
// When expanding/collapsing, the tooltip on the caret button sometimes stays open.
// Close all tooltips manually to prevent dangling tooltips.
- this.$root.$emit('bv::hide::tooltip');
+ this.$root.$emit(BV_HIDE_TOOLTIP);
},
addToLocalStorage() {
if (AccessorUtilities.isLocalStorageAccessSafe()) {
@@ -201,6 +205,17 @@ export default {
<gl-icon name="timer" />
</span>
+ <span
+ v-if="showIterationListDetails"
+ aria-hidden="true"
+ :class="{
+ 'gl-mt-3 gl-rotate-90': !list.isExpanded,
+ 'gl-mr-2': list.isExpanded,
+ }"
+ >
+ <gl-icon name="iteration" />
+ </span>
+
<a
v-if="showAssigneeListDetails"
:href="list.assignee.path"
diff --git a/app/assets/javascripts/boards/components/board_new_issue.vue b/app/assets/javascripts/boards/components/board_new_issue.vue
index 14d28643046..29a88cf705e 100644
--- a/app/assets/javascripts/boards/components/board_new_issue.vue
+++ b/app/assets/javascripts/boards/components/board_new_issue.vue
@@ -2,10 +2,10 @@
import { mapActions, mapState } from 'vuex';
import { GlButton } from '@gitlab/ui';
import { getMilestone } from 'ee_else_ce/boards/boards_util';
-import eventHub from '../eventhub';
-import ProjectSelect from './project_select.vue';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { __ } from '~/locale';
+import eventHub from '../eventhub';
+import ProjectSelect from './project_select.vue';
export default {
name: 'BoardNewIssue',
diff --git a/app/assets/javascripts/boards/components/board_new_issue_deprecated.vue b/app/assets/javascripts/boards/components/board_new_issue_deprecated.vue
index 4fc58742783..eff87ff110e 100644
--- a/app/assets/javascripts/boards/components/board_new_issue_deprecated.vue
+++ b/app/assets/javascripts/boards/components/board_new_issue_deprecated.vue
@@ -2,10 +2,10 @@
import { GlButton } from '@gitlab/ui';
import { getMilestone } from 'ee_else_ce/boards/boards_util';
import ListIssue from 'ee_else_ce/boards/models/issue';
+import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import eventHub from '../eventhub';
-import ProjectSelect from './project_select_deprecated.vue';
import boardsStore from '../stores/boards_store';
-import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
+import ProjectSelect from './project_select_deprecated.vue';
// This component is being replaced in favor of './board_new_issue.vue' for GraphQL boards
diff --git a/app/assets/javascripts/boards/components/board_settings_sidebar.vue b/app/assets/javascripts/boards/components/board_settings_sidebar.vue
index f362fc60bd3..50831b074f4 100644
--- a/app/assets/javascripts/boards/components/board_settings_sidebar.vue
+++ b/app/assets/javascripts/boards/components/board_settings_sidebar.vue
@@ -5,17 +5,13 @@ import { __ } from '~/locale';
import boardsStore from '~/boards/stores/boards_store';
import eventHub from '~/sidebar/event_hub';
import { isScopedLabel } from '~/lib/utils/common_utils';
-import { LIST } from '~/boards/constants';
+import { LIST, ListType, ListTypeTitles } from '~/boards/constants';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
// NOTE: need to revisit how we handle headerHeight, because we have so many different header and footer options.
export default {
headerHeight: process.env.NODE_ENV === 'development' ? '75px' : '40px',
listSettingsText: __('List settings'),
- assignee: 'assignee',
- milestone: 'milestone',
- label: 'label',
- labelListText: __('Label'),
components: {
GlButton,
GlDrawer,
@@ -33,6 +29,11 @@ export default {
default: false,
},
},
+ data() {
+ return {
+ ListType,
+ };
+ },
computed: {
...mapGetters(['isSidebarOpen', 'shouldUseGraphQL']),
...mapState(['activeId', 'sidebarType', 'boardLists']),
@@ -56,7 +57,7 @@ export default {
return this.activeList.type || this.activeList.listType || null;
},
listTypeTitle() {
- return this.$options.labelListText;
+ return ListTypeTitles[ListType.label];
},
showSidebar() {
return this.sidebarType === LIST;
@@ -98,7 +99,7 @@ export default {
>
<template #header>{{ $options.listSettingsText }}</template>
<template v-if="isSidebarOpen">
- <div v-if="boardListType === $options.label">
+ <div v-if="boardListType === ListType.label">
<label class="js-list-label gl-display-block">{{ listTypeTitle }}</label>
<gl-label
:title="activeListLabel.title"
diff --git a/app/assets/javascripts/boards/components/board_sidebar.js b/app/assets/javascripts/boards/components/board_sidebar.js
index bf3dc5c608f..f9ab38b32c4 100644
--- a/app/assets/javascripts/boards/components/board_sidebar.js
+++ b/app/assets/javascripts/boards/components/board_sidebar.js
@@ -1,4 +1,7 @@
-/* eslint-disable no-new */
+// This is a true violation of @gitlab/no-runtime-template-compiler, as it
+// relies on app/views/shared/boards/components/_sidebar.html.haml for its
+// template.
+/* eslint-disable no-new, @gitlab/no-runtime-template-compiler */
import $ from 'jquery';
import Vue from 'vue';
@@ -15,9 +18,9 @@ import Assignees from '~/sidebar/components/assignees/assignees.vue';
import Subscriptions from '~/sidebar/components/subscriptions/subscriptions.vue';
import TimeTracker from '~/sidebar/components/time_tracking/time_tracker.vue';
import MilestoneSelect from '~/milestone_select';
-import RemoveBtn from './sidebar/remove_issue.vue';
-import boardsStore from '../stores/boards_store';
import { isScopedLabel } from '~/lib/utils/common_utils';
+import boardsStore from '../stores/boards_store';
+import RemoveBtn from './sidebar/remove_issue.vue';
export default Vue.extend({
components: {
diff --git a/app/assets/javascripts/boards/components/issue_card_inner.vue b/app/assets/javascripts/boards/components/issue_card_inner.vue
index 457d0d4dcd6..8723c1bb7f3 100644
--- a/app/assets/javascripts/boards/components/issue_card_inner.vue
+++ b/app/assets/javascripts/boards/components/issue_card_inner.vue
@@ -5,13 +5,13 @@ import { GlLabel, GlTooltipDirective, GlIcon } from '@gitlab/ui';
import issueCardInner from 'ee_else_ce/boards/mixins/issue_card_inner';
import { sprintf, __, n__ } from '~/locale';
import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate.vue';
+import { isScopedLabel } from '~/lib/utils/common_utils';
+import { updateHistory } from '~/lib/utils/url_utility';
import UserAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue';
-import IssueDueDate from './issue_due_date.vue';
-import IssueTimeEstimate from './issue_time_estimate.vue';
import eventHub from '../eventhub';
-import { isScopedLabel } from '~/lib/utils/common_utils';
import { ListType } from '../constants';
-import { updateHistory } from '~/lib/utils/url_utility';
+import IssueDueDate from './issue_due_date.vue';
+import IssueTimeEstimate from './issue_time_estimate.vue';
export default {
components: {
diff --git a/app/assets/javascripts/boards/components/issue_card_inner_deprecated.vue b/app/assets/javascripts/boards/components/issue_card_inner_deprecated.vue
index 75cf1f0b9e1..64caac22391 100644
--- a/app/assets/javascripts/boards/components/issue_card_inner_deprecated.vue
+++ b/app/assets/javascripts/boards/components/issue_card_inner_deprecated.vue
@@ -5,11 +5,11 @@ import { GlLabel, GlTooltipDirective, GlIcon } from '@gitlab/ui';
import issueCardInner from 'ee_else_ce/boards/mixins/issue_card_inner';
import { sprintf, __, n__ } from '~/locale';
import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate.vue';
+import { isScopedLabel } from '~/lib/utils/common_utils';
import UserAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue';
+import boardsStore from '../stores/boards_store';
import IssueDueDate from './issue_due_date.vue';
import IssueTimeEstimate from './issue_time_estimate_deprecated.vue';
-import boardsStore from '../stores/boards_store';
-import { isScopedLabel } from '~/lib/utils/common_utils';
export default {
components: {
diff --git a/app/assets/javascripts/boards/components/modal/footer.vue b/app/assets/javascripts/boards/components/modal/footer.vue
index 10c29977cae..3054ebfc173 100644
--- a/app/assets/javascripts/boards/components/modal/footer.vue
+++ b/app/assets/javascripts/boards/components/modal/footer.vue
@@ -3,10 +3,10 @@ import { GlButton } from '@gitlab/ui';
import footerEEMixin from 'ee_else_ce/boards/mixins/modal_footer';
import { deprecatedCreateFlash as Flash } from '../../../flash';
import { __, n__ } from '../../../locale';
-import ListsDropdown from './lists_dropdown.vue';
import ModalStore from '../../stores/modal_store';
import modalMixin from '../../mixins/modal_mixins';
import boardsStore from '../../stores/boards_store';
+import ListsDropdown from './lists_dropdown.vue';
export default {
components: {
diff --git a/app/assets/javascripts/boards/components/modal/header.vue b/app/assets/javascripts/boards/components/modal/header.vue
index 3e96ecca24c..dfc0d08c3fe 100644
--- a/app/assets/javascripts/boards/components/modal/header.vue
+++ b/app/assets/javascripts/boards/components/modal/header.vue
@@ -2,10 +2,10 @@
/* eslint-disable @gitlab/vue-require-i18n-strings */
import { GlButton } from '@gitlab/ui';
import { __ } from '~/locale';
-import ModalFilters from './filters';
-import ModalTabs from './tabs.vue';
import ModalStore from '../../stores/modal_store';
import modalMixin from '../../mixins/modal_mixins';
+import ModalFilters from './filters';
+import ModalTabs from './tabs.vue';
export default {
components: {
diff --git a/app/assets/javascripts/boards/components/modal/index.vue b/app/assets/javascripts/boards/components/modal/index.vue
index 84d687a46b9..5e4af9814c7 100644
--- a/app/assets/javascripts/boards/components/modal/index.vue
+++ b/app/assets/javascripts/boards/components/modal/index.vue
@@ -3,11 +3,11 @@
import { GlLoadingIcon } from '@gitlab/ui';
import { urlParamsToObject } from '~/lib/utils/common_utils';
import boardsStore from '~/boards/stores/boards_store';
+import ModalStore from '../../stores/modal_store';
import ModalHeader from './header.vue';
import ModalList from './list.vue';
import ModalFooter from './footer.vue';
import EmptyState from './empty_state.vue';
-import ModalStore from '../../stores/modal_store';
export default {
components: {
diff --git a/app/assets/javascripts/boards/components/new_list_dropdown.js b/app/assets/javascripts/boards/components/new_list_dropdown.js
index 2bc54155163..3c9f174dedc 100644
--- a/app/assets/javascripts/boards/components/new_list_dropdown.js
+++ b/app/assets/javascripts/boards/components/new_list_dropdown.js
@@ -4,12 +4,12 @@ import $ from 'jquery';
import { __ } from '~/locale';
import axios from '~/lib/utils/axios_utils';
import { deprecatedCreateFlash as flash } from '~/flash';
-import CreateLabelDropdown from '../../create_label';
-import boardsStore from '../stores/boards_store';
-import { fullLabelId } from '../boards_util';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import store from '~/boards/stores';
import initDeprecatedJQueryDropdown from '~/deprecated_jquery_dropdown';
+import CreateLabelDropdown from '../../create_label';
+import boardsStore from '../stores/boards_store';
+import { fullLabelId } from '../boards_util';
function shouldCreateListGraphQL(label) {
return store.getters.shouldUseGraphQL && !store.getters.getListByLabelId(fullLabelId(label));
@@ -51,16 +51,27 @@ export default function initNewListDropdown() {
initDeprecatedJQueryDropdown($dropdownToggle, {
data(term, callback) {
- axios
- .get($dropdownToggle.attr('data-list-labels-path'))
- .then(({ data }) => callback(data))
- .catch(() => {
- $dropdownToggle.data('bs.dropdown').hide();
- flash(__('Error fetching labels.'));
- });
+ const reqFailed = () => {
+ $dropdownToggle.data('bs.dropdown').hide();
+ flash(__('Error fetching labels.'));
+ };
+
+ if (store.getters.shouldUseGraphQL) {
+ store
+ .dispatch('fetchLabels')
+ .then((data) => callback(data))
+ .catch(reqFailed);
+ } else {
+ axios
+ .get($dropdownToggle.attr('data-list-labels-path'))
+ .then(({ data }) => callback(data))
+ .catch(reqFailed);
+ }
},
renderRow(label) {
- const active = boardsStore.findListByLabelId(label.id);
+ const active = store.getters.shouldUseGraphQL
+ ? store.getters.getListByLabelId(label.id)
+ : boardsStore.findListByLabelId(label.id);
const $li = $('<li />');
const $a = $('<a />', {
class: active ? `is-active js-board-list-${getIdFromGraphQLId(active.id)}` : '',
@@ -87,7 +98,7 @@ export default function initNewListDropdown() {
e.preventDefault();
if (shouldCreateListGraphQL(label)) {
- store.dispatch('createList', { labelId: fullLabelId(label) });
+ store.dispatch('createList', { labelId: label.id });
} else if (!boardsStore.findListByLabelId(label.id)) {
boardsStore.new({
title: label.title,
diff --git a/app/assets/javascripts/boards/components/project_select_deprecated.vue b/app/assets/javascripts/boards/components/project_select_deprecated.vue
index a043dc575ca..d4830e5afbf 100644
--- a/app/assets/javascripts/boards/components/project_select_deprecated.vue
+++ b/app/assets/javascripts/boards/components/project_select_deprecated.vue
@@ -6,10 +6,10 @@ import {
GlSearchBoxByType,
GlLoadingIcon,
} from '@gitlab/ui';
-import eventHub from '../eventhub';
import { s__ } from '~/locale';
-import Api from '../../api';
import { featureAccessLevel } from '~/pages/projects/shared/permissions/constants';
+import eventHub from '../eventhub';
+import Api from '../../api';
import { ListType } from '../constants';
export default {
diff --git a/app/assets/javascripts/boards/components/sidebar/board_sidebar_due_date.vue b/app/assets/javascripts/boards/components/sidebar/board_sidebar_due_date.vue
index 4a664d5beef..373351eca22 100644
--- a/app/assets/javascripts/boards/components/sidebar/board_sidebar_due_date.vue
+++ b/app/assets/javascripts/boards/components/sidebar/board_sidebar_due_date.vue
@@ -88,15 +88,13 @@ export default {
</gl-button>
</div>
</template>
- <template>
- <gl-datepicker
- ref="datePicker"
- :value="parsedDueDate"
- show-clear-button
- @input="setDueDate"
- @clear="setDueDate(null)"
- />
- </template>
+ <gl-datepicker
+ ref="datePicker"
+ :value="parsedDueDate"
+ show-clear-button
+ @input="setDueDate"
+ @clear="setDueDate(null)"
+ />
</board-editable-item>
</template>
<style>
diff --git a/app/assets/javascripts/boards/components/sidebar/board_sidebar_issue_title.vue b/app/assets/javascripts/boards/components/sidebar/board_sidebar_issue_title.vue
index d0e641daf5c..d26d03323fb 100644
--- a/app/assets/javascripts/boards/components/sidebar/board_sidebar_issue_title.vue
+++ b/app/assets/javascripts/boards/components/sidebar/board_sidebar_issue_title.vue
@@ -136,36 +136,34 @@ export default {
<template #collapsed>
<span class="gl-text-gray-800">{{ issue.referencePath }}</span>
</template>
- <template>
- <gl-alert v-if="showChangesAlert" variant="warning" class="gl-mb-5" :dismissible="false">
- {{ $options.i18n.reviewYourChanges }}
- </gl-alert>
- <gl-form @submit.prevent="setTitle">
- <gl-form-group :invalid-feedback="$options.i18n.invalidFeedback" :state="validationState">
- <gl-form-input
- v-model="title"
- v-autofocusonshow
- :placeholder="$options.i18n.issueTitlePlaceholder"
- :state="validationState"
- />
- </gl-form-group>
+ <gl-alert v-if="showChangesAlert" variant="warning" class="gl-mb-5" :dismissible="false">
+ {{ $options.i18n.reviewYourChanges }}
+ </gl-alert>
+ <gl-form @submit.prevent="setTitle">
+ <gl-form-group :invalid-feedback="$options.i18n.invalidFeedback" :state="validationState">
+ <gl-form-input
+ v-model="title"
+ v-autofocusonshow
+ :placeholder="$options.i18n.issueTitlePlaceholder"
+ :state="validationState"
+ />
+ </gl-form-group>
- <div class="gl-display-flex gl-w-full gl-justify-content-space-between gl-mt-5">
- <gl-button
- variant="success"
- size="small"
- data-testid="submit-button"
- :disabled="!title"
- @click="setTitle"
- >
- {{ $options.i18n.submitButton }}
- </gl-button>
+ <div class="gl-display-flex gl-w-full gl-justify-content-space-between gl-mt-5">
+ <gl-button
+ variant="success"
+ size="small"
+ data-testid="submit-button"
+ :disabled="!title"
+ @click="setTitle"
+ >
+ {{ $options.i18n.submitButton }}
+ </gl-button>
- <gl-button size="small" data-testid="cancel-button" @click="cancel">
- {{ $options.i18n.cancelButton }}
- </gl-button>
- </div>
- </gl-form>
- </template>
+ <gl-button size="small" data-testid="cancel-button" @click="cancel">
+ {{ $options.i18n.cancelButton }}
+ </gl-button>
+ </div>
+ </gl-form>
</board-editable-item>
</template>
diff --git a/app/assets/javascripts/boards/components/sidebar/board_sidebar_milestone_select.vue b/app/assets/javascripts/boards/components/sidebar/board_sidebar_milestone_select.vue
index 144a81f009b..a2dbd52369f 100644
--- a/app/assets/javascripts/boards/components/sidebar/board_sidebar_milestone_select.vue
+++ b/app/assets/javascripts/boards/components/sidebar/board_sidebar_milestone_select.vue
@@ -8,11 +8,11 @@ import {
GlDropdownDivider,
GlLoadingIcon,
} from '@gitlab/ui';
-import { fetchPolicies } from '~/lib/graphql';
import BoardEditableItem from '~/boards/components/sidebar/board_editable_item.vue';
-import groupMilestones from '../../graphql/group_milestones.query.graphql';
import createFlash from '~/flash';
+import { BV_DROPDOWN_HIDE } from '~/lib/utils/constants';
import { __, s__ } from '~/locale';
+import projectMilestones from '../../graphql/project_milestones.query.graphql';
export default {
components: {
@@ -34,22 +34,21 @@ export default {
},
apollo: {
milestones: {
- fetchPolicy: fetchPolicies.CACHE_AND_NETWORK,
- query: groupMilestones,
+ query: projectMilestones,
debounce: 250,
skip() {
return !this.edit;
},
variables() {
return {
- fullPath: this.groupFullPath,
+ fullPath: this.projectPath,
searchTitle: this.searchTitle,
state: 'active',
- includeDescendants: true,
+ includeAncestors: true,
};
},
update(data) {
- const edges = data?.group?.milestones?.edges ?? [];
+ const edges = data?.project?.milestones?.edges ?? [];
return edges.map((item) => item.node);
},
error() {
@@ -75,7 +74,7 @@ export default {
},
},
mounted() {
- this.$root.$on('bv::dropdown::hide', () => {
+ this.$root.$on(BV_DROPDOWN_HIDE, () => {
this.$refs.sidebarItem.collapse();
});
},
@@ -122,40 +121,38 @@ export default {
<template v-if="hasMilestone" #collapsed>
<strong class="gl-text-gray-900">{{ activeIssue.milestone.title }}</strong>
</template>
- <template>
- <gl-dropdown
- ref="dropdown"
- :text="dropdownText"
- :header-text="$options.i18n.assignMilestone"
- block
+ <gl-dropdown
+ ref="dropdown"
+ :text="dropdownText"
+ :header-text="$options.i18n.assignMilestone"
+ block
+ >
+ <gl-search-box-by-type ref="search" v-model.trim="searchTitle" class="gl-m-3" />
+ <gl-dropdown-item
+ data-testid="no-milestone-item"
+ :is-check-item="true"
+ :is-checked="!activeIssue.milestone"
+ @click="setMilestone(null)"
>
- <gl-search-box-by-type ref="search" v-model.trim="searchTitle" class="gl-m-3" />
+ {{ $options.i18n.noMilestone }}
+ </gl-dropdown-item>
+ <gl-dropdown-divider />
+ <gl-loading-icon v-if="$apollo.loading" class="gl-py-4" />
+ <template v-else-if="milestones.length > 0">
<gl-dropdown-item
- data-testid="no-milestone-item"
+ v-for="milestone in milestones"
+ :key="milestone.id"
:is-check-item="true"
- :is-checked="!activeIssue.milestone"
- @click="setMilestone(null)"
+ :is-checked="activeIssue.milestone && milestone.id === activeIssue.milestone.id"
+ data-testid="milestone-item"
+ @click="setMilestone(milestone.id)"
>
- {{ $options.i18n.noMilestone }}
+ {{ milestone.title }}
</gl-dropdown-item>
- <gl-dropdown-divider />
- <gl-loading-icon v-if="$apollo.loading" class="gl-py-4" />
- <template v-else-if="milestones.length > 0">
- <gl-dropdown-item
- v-for="milestone in milestones"
- :key="milestone.id"
- :is-check-item="true"
- :is-checked="activeIssue.milestone && milestone.id === activeIssue.milestone.id"
- data-testid="milestone-item"
- @click="setMilestone(milestone.id)"
- >
- {{ milestone.title }}
- </gl-dropdown-item>
- </template>
- <gl-dropdown-text v-else data-testid="no-milestones-found">
- {{ $options.i18n.noMilestonesFound }}
- </gl-dropdown-text>
- </gl-dropdown>
- </template>
+ </template>
+ <gl-dropdown-text v-else data-testid="no-milestones-found">
+ {{ $options.i18n.noMilestonesFound }}
+ </gl-dropdown-text>
+ </gl-dropdown>
</board-editable-item>
</template>
diff --git a/app/assets/javascripts/boards/components/toggle_focus.vue b/app/assets/javascripts/boards/components/toggle_focus.vue
new file mode 100644
index 00000000000..59ee47937c9
--- /dev/null
+++ b/app/assets/javascripts/boards/components/toggle_focus.vue
@@ -0,0 +1,52 @@
+<script>
+import { GlIcon } from '@gitlab/ui';
+import { __ } from '~/locale';
+import { hide } from '~/tooltips';
+
+export default {
+ components: {
+ GlIcon,
+ },
+ props: {
+ issueBoardsContentSelector: {
+ type: String,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ isFullscreen: false,
+ };
+ },
+ methods: {
+ toggleFocusMode() {
+ hide(this.$refs.toggleFocusModeButton);
+
+ const issueBoardsContent = document.querySelector(this.issueBoardsContentSelector);
+ issueBoardsContent.classList.toggle('is-focused');
+
+ this.isFullscreen = !this.isFullscreen;
+ },
+ },
+ i18n: {
+ toggleFocusMode: __('Toggle focus mode'),
+ },
+};
+</script>
+
+<template>
+ <div class="board-extra-actions">
+ <a
+ ref="toggleFocusModeButton"
+ href="#"
+ class="btn btn-default has-tooltip gl-ml-3 js-focus-mode-btn"
+ data-qa-selector="focus_mode_button"
+ role="button"
+ :aria-label="$options.i18n.toggleFocusMode"
+ :title="$options.i18n.toggleFocusMode"
+ @click="toggleFocusMode"
+ >
+ <gl-icon :name="isFullscreen ? 'minimize' : 'maximize'" />
+ </a>
+ </div>
+</template>
diff --git a/app/assets/javascripts/boards/constants.js b/app/assets/javascripts/boards/constants.js
index 9264fac5eda..723aef4875d 100644
--- a/app/assets/javascripts/boards/constants.js
+++ b/app/assets/javascripts/boards/constants.js
@@ -1,3 +1,5 @@
+import { __ } from '~/locale';
+
export const BoardType = {
project: 'project',
group: 'group',
@@ -6,16 +8,26 @@ export const BoardType = {
export const ListType = {
assignee: 'assignee',
milestone: 'milestone',
+ iteration: 'iteration',
backlog: 'backlog',
closed: 'closed',
label: 'label',
};
+export const ListTypeTitles = {
+ assignee: __('Assignee'),
+ milestone: __('Milestone'),
+ iteration: __('Iteration'),
+ label: __('Label'),
+};
+
export const inactiveId = 0;
export const ISSUABLE = 'issuable';
export const LIST = 'list';
+export const NOT_FILTER = 'not[';
+
export default {
BoardType,
ListType,
diff --git a/app/assets/javascripts/boards/filtered_search_boards.js b/app/assets/javascripts/boards/filtered_search_boards.js
index 94b35aadaf1..83413ee216c 100644
--- a/app/assets/javascripts/boards/filtered_search_boards.js
+++ b/app/assets/javascripts/boards/filtered_search_boards.js
@@ -1,10 +1,10 @@
import IssuableFilteredSearchTokenKeys from 'ee_else_ce/filtered_search/issuable_filtered_search_token_keys';
import FilteredSearchManager from 'ee_else_ce/filtered_search/filtered_search_manager';
import { transformBoardConfig } from 'ee_else_ce/boards/boards_util';
+import { updateHistory } from '~/lib/utils/url_utility';
import FilteredSearchContainer from '../filtered_search/container';
import boardsStore from './stores/boards_store';
import vuexstore from './stores';
-import { updateHistory } from '~/lib/utils/url_utility';
export default class FilteredSearchBoards extends FilteredSearchManager {
constructor(store, updateUrl = false, cantEdit = []) {
diff --git a/app/assets/javascripts/boards/graphql/group_milestones.query.graphql b/app/assets/javascripts/boards/graphql/project_milestones.query.graphql
index f2ab12ef4a7..776530ebb83 100644
--- a/app/assets/javascripts/boards/graphql/group_milestones.query.graphql
+++ b/app/assets/javascripts/boards/graphql/project_milestones.query.graphql
@@ -1,11 +1,11 @@
query groupMilestones(
$fullPath: ID!
$state: MilestoneStateEnum
- $includeDescendants: Boolean
+ $includeAncestors: Boolean
$searchTitle: String
) {
- group(fullPath: $fullPath) {
- milestones(state: $state, includeDescendants: $includeDescendants, searchTitle: $searchTitle) {
+ project(fullPath: $fullPath) {
+ milestones(state: $state, includeAncestors: $includeAncestors, searchTitle: $searchTitle) {
edges {
node {
id
diff --git a/app/assets/javascripts/boards/index.js b/app/assets/javascripts/boards/index.js
index ef70a094f7c..5e8dd81438b 100644
--- a/app/assets/javascripts/boards/index.js
+++ b/app/assets/javascripts/boards/index.js
@@ -3,6 +3,7 @@ import { mapActions, mapGetters } from 'vuex';
import 'ee_else_ce/boards/models/issue';
import 'ee_else_ce/boards/models/list';
+import VueApollo from 'vue-apollo';
import BoardSidebar from 'ee_else_ce/boards/components/board_sidebar';
import initNewListDropdown from 'ee_else_ce/boards/components/new_list_dropdown';
import boardConfigToggle from 'ee_else_ce/boards/config_toggle';
@@ -15,7 +16,7 @@ import {
getBoardsModalData,
} from 'ee_else_ce/boards/ee_functions';
-import VueApollo from 'vue-apollo';
+import BoardAddNewColumnTrigger from '~/boards/components/board_add_new_column_trigger.vue';
import BoardContent from '~/boards/components/board_content.vue';
import BoardExtraActions from '~/boards/components/board_extra_actions.vue';
import createDefaultClient from '~/lib/graphql';
@@ -73,6 +74,7 @@ export default () => {
boardsStore.setTimeTrackingLimitToHours($boardApp.dataset.timeTrackingLimitToHours);
}
+ // eslint-disable-next-line @gitlab/no-runtime-template-compiler
issueBoardsApp = new Vue({
el: $boardApp,
components: {
@@ -275,7 +277,7 @@ export default () => {
},
});
- // eslint-disable-next-line no-new
+ // eslint-disable-next-line no-new, @gitlab/no-runtime-template-compiler
new Vue({
el: document.getElementById('js-add-list'),
data: {
@@ -287,6 +289,21 @@ export default () => {
},
});
+ const createColumnTriggerEl = document.querySelector('.js-create-column-trigger');
+ if (createColumnTriggerEl) {
+ // eslint-disable-next-line no-new
+ new Vue({
+ el: createColumnTriggerEl,
+ components: {
+ BoardAddNewColumnTrigger,
+ },
+ store,
+ render(createElement) {
+ return createElement('board-add-new-column-trigger');
+ },
+ });
+ }
+
boardConfigToggle(boardsStore);
const issueBoardsModal = document.getElementById('js-add-issues-btn');
diff --git a/app/assets/javascripts/boards/models/issue.js b/app/assets/javascripts/boards/models/issue.js
index 1e77326ba9c..bc23639df67 100644
--- a/app/assets/javascripts/boards/models/issue.js
+++ b/app/assets/javascripts/boards/models/issue.js
@@ -6,8 +6,8 @@
import axios from '~/lib/utils/axios_utils';
import './label';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
-import IssueProject from './project';
import boardsStore from '../stores/boards_store';
+import IssueProject from './project';
class ListIssue {
constructor(obj) {
diff --git a/app/assets/javascripts/boards/models/iteration.js b/app/assets/javascripts/boards/models/iteration.js
new file mode 100644
index 00000000000..b7bdc204f7c
--- /dev/null
+++ b/app/assets/javascripts/boards/models/iteration.js
@@ -0,0 +1,9 @@
+export default class ListIteration {
+ constructor(obj) {
+ this.id = obj.id;
+ this.title = obj.title;
+ this.state = obj.state;
+ this.webUrl = obj.web_url || obj.webUrl;
+ this.description = obj.description;
+ }
+}
diff --git a/app/assets/javascripts/boards/models/list.js b/app/assets/javascripts/boards/models/list.js
index be02ac7b889..299e025529a 100644
--- a/app/assets/javascripts/boards/models/list.js
+++ b/app/assets/javascripts/boards/models/list.js
@@ -1,10 +1,11 @@
/* eslint-disable class-methods-use-this */
import { __ } from '~/locale';
-import ListLabel from './label';
-import ListAssignee from './assignee';
import { deprecatedCreateFlash as flash } from '~/flash';
import boardsStore from '../stores/boards_store';
+import ListLabel from './label';
+import ListAssignee from './assignee';
import ListMilestone from './milestone';
+import ListIteration from './iteration';
import 'ee_else_ce/boards/models/issue';
const TYPES = {
@@ -57,6 +58,9 @@ class List {
} else if (IS_EE && obj.milestone) {
this.milestone = new ListMilestone(obj.milestone);
this.title = this.milestone.title;
+ } else if (IS_EE && obj.iteration) {
+ this.iteration = new ListIteration(obj.iteration);
+ this.title = this.iteration.title;
}
// doNotFetchIssues is a temporary workaround until issues are fetched using GraphQL on issue boards
diff --git a/app/assets/javascripts/boards/stores/actions.js b/app/assets/javascripts/boards/stores/actions.js
index 1d34f21798a..8c9f86b17a4 100644
--- a/app/assets/javascripts/boards/stores/actions.js
+++ b/app/assets/javascripts/boards/stores/actions.js
@@ -5,7 +5,9 @@ import createGqClient, { fetchPolicies } from '~/lib/graphql';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { convertObjectPropsToCamelCase, urlParamsToObject } from '~/lib/utils/common_utils';
import { BoardType, ListType, inactiveId } from '~/boards/constants';
-import * as types from './mutation_types';
+import createFlash from '~/flash';
+import { __ } from '~/locale';
+import updateAssigneesMutation from '~/vue_shared/components/sidebar/queries/updateAssignees.mutation.graphql';
import {
formatBoardLists,
formatListIssues,
@@ -14,10 +16,8 @@ import {
formatIssue,
formatIssueInput,
updateListPosition,
+ transformNotFilters,
} from '../boards_util';
-import createFlash from '~/flash';
-import { __ } from '~/locale';
-import updateAssigneesMutation from '~/vue_shared/components/sidebar/queries/updateAssignees.mutation.graphql';
import listsIssuesQuery from '../graphql/lists_issues.query.graphql';
import boardLabelsQuery from '../graphql/board_labels.query.graphql';
import createBoardListMutation from '../graphql/board_list_create.mutation.graphql';
@@ -31,6 +31,7 @@ import issueSetSubscriptionMutation from '../graphql/issue_set_subscription.muta
import issueSetMilestoneMutation from '../graphql/issue_set_milestone.mutation.graphql';
import issueSetTitleMutation from '../graphql/issue_set_title.mutation.graphql';
import groupProjectsQuery from '../graphql/group_projects.query.graphql';
+import * as types from './mutation_types';
const notImplemented = () => {
/* eslint-disable-next-line @gitlab/require-i18n-strings */
@@ -66,6 +67,7 @@ export default {
'releaseTag',
'search',
]);
+ filterParams.not = transformNotFilters(filters);
commit(types.SET_FILTERS, filterParams);
},
@@ -153,10 +155,10 @@ export default {
variables,
})
.then(({ data }) => {
- const labels = data[boardType]?.labels;
- return labels.nodes;
- })
- .catch(() => commit(types.RECEIVE_LABELS_FAILURE));
+ const labels = data[boardType]?.labels.nodes;
+ commit(types.RECEIVE_LABELS_SUCCESS, labels);
+ return labels;
+ });
},
moveList: (
@@ -534,6 +536,21 @@ export default {
commit(types.SET_SELECTED_PROJECT, project);
},
+ toggleBoardItemMultiSelection: ({ commit, state }, boardItem) => {
+ const { selectedBoardItems } = state;
+ const index = selectedBoardItems.indexOf(boardItem);
+
+ if (index === -1) {
+ commit(types.ADD_BOARD_ITEM_TO_SELECTION, boardItem);
+ } else {
+ commit(types.REMOVE_BOARD_ITEM_FROM_SELECTION, boardItem);
+ }
+ },
+
+ setAddColumnFormVisibility: ({ commit }, visible) => {
+ commit(types.SET_ADD_COLUMN_FORM_VISIBLE, visible);
+ },
+
fetchBacklog: () => {
notImplemented();
},
diff --git a/app/assets/javascripts/boards/stores/getters.js b/app/assets/javascripts/boards/stores/getters.js
index d72b5c6fb8e..827c1e377cf 100644
--- a/app/assets/javascripts/boards/stores/getters.js
+++ b/app/assets/javascripts/boards/stores/getters.js
@@ -17,8 +17,13 @@ export default {
return state.issues[state.activeId] || {};
},
+ groupPathForActiveIssue: (_, getters) => {
+ const { referencePath = '' } = getters.activeIssue;
+ return referencePath.slice(0, referencePath.indexOf('/'));
+ },
+
projectPathForActiveIssue: (_, getters) => {
- const referencePath = getters.activeIssue.referencePath || '';
+ const { referencePath = '' } = getters.activeIssue;
return referencePath.slice(0, referencePath.indexOf('#'));
},
diff --git a/app/assets/javascripts/boards/stores/mutation_types.js b/app/assets/javascripts/boards/stores/mutation_types.js
index 4697f39498a..5ec0ee158df 100644
--- a/app/assets/javascripts/boards/stores/mutation_types.js
+++ b/app/assets/javascripts/boards/stores/mutation_types.js
@@ -2,7 +2,7 @@ export const SET_INITIAL_BOARD_DATA = 'SET_INITIAL_BOARD_DATA';
export const SET_FILTERS = 'SET_FILTERS';
export const CREATE_LIST_SUCCESS = 'CREATE_LIST_SUCCESS';
export const CREATE_LIST_FAILURE = 'CREATE_LIST_FAILURE';
-export const RECEIVE_LABELS_FAILURE = 'RECEIVE_LABELS_FAILURE';
+export const RECEIVE_LABELS_SUCCESS = 'RECEIVE_LABELS_SUCCESS';
export const GENERATE_DEFAULT_LISTS_FAILURE = 'GENERATE_DEFAULT_LISTS_FAILURE';
export const RECEIVE_BOARD_LISTS_SUCCESS = 'RECEIVE_BOARD_LISTS_SUCCESS';
export const RECEIVE_BOARD_LISTS_FAILURE = 'RECEIVE_BOARD_LISTS_FAILURE';
@@ -40,3 +40,6 @@ export const REQUEST_GROUP_PROJECTS = 'REQUEST_GROUP_PROJECTS';
export const RECEIVE_GROUP_PROJECTS_SUCCESS = 'RECEIVE_GROUP_PROJECTS_SUCCESS';
export const RECEIVE_GROUP_PROJECTS_FAILURE = 'RECEIVE_GROUP_PROJECTS_FAILURE';
export const SET_SELECTED_PROJECT = 'SET_SELECTED_PROJECT';
+export const ADD_BOARD_ITEM_TO_SELECTION = 'ADD_BOARD_ITEM_TO_SELECTION';
+export const REMOVE_BOARD_ITEM_FROM_SELECTION = 'REMOVE_BOARD_ITEM_FROM_SELECTION';
+export const SET_ADD_COLUMN_FORM_VISIBLE = 'SET_ADD_COLUMN_FORM_VISIBLE';
diff --git a/app/assets/javascripts/boards/stores/mutations.js b/app/assets/javascripts/boards/stores/mutations.js
index 6c79b22d308..acf62782fad 100644
--- a/app/assets/javascripts/boards/stores/mutations.js
+++ b/app/assets/javascripts/boards/stores/mutations.js
@@ -1,9 +1,9 @@
import Vue from 'vue';
import { pull, union } from 'lodash';
-import { formatIssue, moveIssueListHelper } from '../boards_util';
-import * as mutationTypes from './mutation_types';
import { s__ } from '~/locale';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
+import { formatIssue, moveIssueListHelper } from '../boards_util';
+import * as mutationTypes from './mutation_types';
const notImplemented = () => {
/* eslint-disable-next-line @gitlab/require-i18n-strings */
@@ -63,8 +63,8 @@ export default {
state.error = s__('Boards|An error occurred while creating the list. Please try again.');
},
- [mutationTypes.RECEIVE_LABELS_FAILURE]: (state) => {
- state.error = s__('Boards|An error occurred while fetching labels. Please reload the page.');
+ [mutationTypes.RECEIVE_LABELS_SUCCESS]: (state, labels) => {
+ state.labels = labels;
},
[mutationTypes.GENERATE_DEFAULT_LISTS_FAILURE]: (state) => {
@@ -258,4 +258,20 @@ export default {
[mutationTypes.SET_SELECTED_PROJECT]: (state, project) => {
state.selectedProject = project;
},
+
+ [mutationTypes.ADD_BOARD_ITEM_TO_SELECTION]: (state, boardItem) => {
+ state.selectedBoardItems = [...state.selectedBoardItems, boardItem];
+ },
+
+ [mutationTypes.REMOVE_BOARD_ITEM_FROM_SELECTION]: (state, boardItem) => {
+ Vue.set(
+ state,
+ 'selectedBoardItems',
+ state.selectedBoardItems.filter((obj) => obj !== boardItem),
+ );
+ },
+
+ [mutationTypes.SET_ADD_COLUMN_FORM_VISIBLE]: (state, visible) => {
+ state.addColumnFormVisible = visible;
+ },
};
diff --git a/app/assets/javascripts/boards/stores/state.js b/app/assets/javascripts/boards/stores/state.js
index aba7da373cf..badbd2d80e5 100644
--- a/app/assets/javascripts/boards/stores/state.js
+++ b/app/assets/javascripts/boards/stores/state.js
@@ -14,6 +14,8 @@ export default () => ({
issues: {},
filterParams: {},
boardConfig: {},
+ labels: [],
+ selectedBoardItems: [],
groupProjects: [],
groupProjectsFlags: {
isLoading: false,
@@ -22,6 +24,7 @@ export default () => ({
},
selectedProject: {},
error: undefined,
+ addColumnFormVisible: false,
// TODO: remove after ce/ee split of board_content.vue
isShowingEpicsSwimlanes: false,
});
diff --git a/app/assets/javascripts/boards/toggle_focus.js b/app/assets/javascripts/boards/toggle_focus.js
index 347deb81846..0a230f72dcc 100644
--- a/app/assets/javascripts/boards/toggle_focus.js
+++ b/app/assets/javascripts/boards/toggle_focus.js
@@ -1,45 +1,17 @@
-import $ from 'jquery';
import Vue from 'vue';
-import { GlIcon } from '@gitlab/ui';
-import { hide } from '~/tooltips';
+import ToggleFocus from './components/toggle_focus.vue';
-export default (ModalStore, boardsStore) => {
- const issueBoardsContent = document.querySelector('.content-wrapper > .js-focus-mode-board');
+export default () => {
+ const issueBoardsContentSelector = '.content-wrapper > .js-focus-mode-board';
return new Vue({
- el: document.getElementById('js-toggle-focus-btn'),
- components: {
- GlIcon,
+ el: '#js-toggle-focus-btn',
+ render(h) {
+ return h(ToggleFocus, {
+ props: {
+ issueBoardsContentSelector,
+ },
+ });
},
- data: {
- modal: ModalStore.store,
- store: boardsStore.state,
- isFullscreen: false,
- },
- methods: {
- toggleFocusMode() {
- const $el = $(this.$refs.toggleFocusModeButton);
- hide($el);
-
- issueBoardsContent.classList.toggle('is-focused');
-
- this.isFullscreen = !this.isFullscreen;
- },
- },
- template: `
- <div class="board-extra-actions">
- <a
- href="#"
- class="btn btn-default has-tooltip gl-ml-3 js-focus-mode-btn"
- data-qa-selector="focus_mode_button"
- role="button"
- aria-label="Toggle focus mode"
- title="Toggle focus mode"
- ref="toggleFocusModeButton"
- @click="toggleFocusMode">
- <gl-icon :name="isFullscreen ? 'minimize' : 'maximize'" />
- </a>
- </div>
- `,
});
};
diff --git a/app/assets/javascripts/branches/components/divergence_graph.vue b/app/assets/javascripts/branches/components/divergence_graph.vue
index deaed694b46..c4b522a43d4 100644
--- a/app/assets/javascripts/branches/components/divergence_graph.vue
+++ b/app/assets/javascripts/branches/components/divergence_graph.vue
@@ -1,7 +1,7 @@
<script>
import { sprintf, __ } from '~/locale';
-import GraphBar from './graph_bar.vue';
import { MAX_COMMIT_COUNT } from '../constants';
+import GraphBar from './graph_bar.vue';
export default {
components: {
diff --git a/app/assets/javascripts/build_artifacts.js b/app/assets/javascripts/build_artifacts.js
index 42e0f8b37bd..ee13e482af5 100644
--- a/app/assets/javascripts/build_artifacts.js
+++ b/app/assets/javascripts/build_artifacts.js
@@ -1,9 +1,9 @@
/* eslint-disable func-names */
import $ from 'jquery';
+import { hide, initTooltips, show } from '~/tooltips';
import { visitUrl } from './lib/utils/url_utility';
import { parseBoolean } from './lib/utils/common_utils';
-import { hide, initTooltips, show } from '~/tooltips';
export default class BuildArtifacts {
constructor() {
diff --git a/app/assets/javascripts/captcha/init_recaptcha_script.js b/app/assets/javascripts/captcha/init_recaptcha_script.js
new file mode 100644
index 00000000000..b9df7604ed1
--- /dev/null
+++ b/app/assets/javascripts/captcha/init_recaptcha_script.js
@@ -0,0 +1,48 @@
+// NOTE: This module will be used in https://gitlab.com/gitlab-org/gitlab/-/merge_requests/52044
+import { memoize } from 'lodash';
+
+export const RECAPTCHA_API_URL_PREFIX = 'https://www.google.com/recaptcha/api.js';
+export const RECAPTCHA_ONLOAD_CALLBACK_NAME = 'recaptchaOnloadCallback';
+
+/**
+ * Adds the Google reCAPTCHA script tag to the head of the document, and
+ * returns a promise of the grecaptcha object
+ * (https://developers.google.com/recaptcha/docs/display#js_api).
+ *
+ * It is memoized, so there will only be one instance of the script tag ever
+ * added to the document.
+ *
+ * See the reCAPTCHA documentation for more details:
+ *
+ * https://developers.google.com/recaptcha/docs/display#explicit_render
+ *
+ */
+export const initRecaptchaScript = memoize(() => {
+ // Appends the the reCAPTCHA script tag to the head of document
+ const appendRecaptchaScript = () => {
+ const script = document.createElement('script');
+ script.src = `${RECAPTCHA_API_URL_PREFIX}?onload=${RECAPTCHA_ONLOAD_CALLBACK_NAME}&render=explicit`;
+ script.classList.add('js-recaptcha-script');
+ document.head.appendChild(script);
+ };
+
+ return new Promise((resolve) => {
+ // This global callback resolves the Promise and is passed by name to the reCAPTCHA script.
+ window[RECAPTCHA_ONLOAD_CALLBACK_NAME] = (val) => {
+ // Let's clean up after ourselves. This is also important for testing, because `window` is NOT cleared between tests.
+ // https://github.com/facebook/jest/issues/1224#issuecomment-444586798.
+ delete window[RECAPTCHA_ONLOAD_CALLBACK_NAME];
+ resolve(val);
+ };
+ appendRecaptchaScript();
+ });
+});
+
+/**
+ * Clears the cached memoization of the default manager.
+ *
+ * This is needed for determinism in tests.
+ */
+export const clearMemoizeCache = () => {
+ initRecaptchaScript.cache.clear();
+};
diff --git a/app/assets/javascripts/ci_settings_pipeline_triggers/index.js b/app/assets/javascripts/ci_settings_pipeline_triggers/index.js
index dc79bbb4d97..f2972133aad 100644
--- a/app/assets/javascripts/ci_settings_pipeline_triggers/index.js
+++ b/app/assets/javascripts/ci_settings_pipeline_triggers/index.js
@@ -1,6 +1,6 @@
import Vue from 'vue';
-import TriggersList from './components/triggers_list.vue';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
+import TriggersList from './components/triggers_list.vue';
const parseJsonArray = (triggers) => {
try {
diff --git a/app/assets/javascripts/ci_variable_list/components/ci_variable_popover.vue b/app/assets/javascripts/ci_variable_list/components/ci_variable_popover.vue
index 431819124c2..6e6527df63f 100644
--- a/app/assets/javascripts/ci_variable_list/components/ci_variable_popover.vue
+++ b/app/assets/javascripts/ci_variable_list/components/ci_variable_popover.vue
@@ -38,7 +38,9 @@ export default {
<template>
<div id="popover-container">
<gl-popover :target="target" triggers="hover" placement="top" container="popover-container">
- <div class="gl-display-flex gl-justify-content-space-between gl-align-items-center">
+ <div
+ class="gl-display-flex gl-justify-content-space-between gl-align-items-center gl-word-break-all"
+ >
<div class="ci-popover-value gl-pr-3">
{{ displayValue }}
</div>
diff --git a/app/assets/javascripts/ci_variable_list/index.js b/app/assets/javascripts/ci_variable_list/index.js
index a28b52d6b57..37b5f7e6df7 100644
--- a/app/assets/javascripts/ci_variable_list/index.js
+++ b/app/assets/javascripts/ci_variable_list/index.js
@@ -1,7 +1,7 @@
import Vue from 'vue';
+import { parseBoolean } from '~/lib/utils/common_utils';
import CiVariableSettings from './components/ci_variable_settings.vue';
import createStore from './store';
-import { parseBoolean } from '~/lib/utils/common_utils';
export default (containerId = 'js-ci-project-variables') => {
const containerEl = document.getElementById(containerId);
diff --git a/app/assets/javascripts/ci_variable_list/store/actions.js b/app/assets/javascripts/ci_variable_list/store/actions.js
index ac595fa0045..350b2190aa7 100644
--- a/app/assets/javascripts/ci_variable_list/store/actions.js
+++ b/app/assets/javascripts/ci_variable_list/store/actions.js
@@ -1,8 +1,8 @@
-import * as types from './mutation_types';
import axios from '~/lib/utils/axios_utils';
import Api from '~/api';
import { deprecatedCreateFlash as createFlash } from '~/flash';
import { __ } from '~/locale';
+import * as types from './mutation_types';
import { prepareDataForApi, prepareDataForDisplay, prepareEnvironments } from './utils';
export const toggleValues = ({ commit }, valueState) => {
diff --git a/app/assets/javascripts/ci_variable_list/store/mutations.js b/app/assets/javascripts/ci_variable_list/store/mutations.js
index 961cecee298..0e7c61cecb8 100644
--- a/app/assets/javascripts/ci_variable_list/store/mutations.js
+++ b/app/assets/javascripts/ci_variable_list/store/mutations.js
@@ -1,5 +1,5 @@
-import * as types from './mutation_types';
import { displayText } from '../constants';
+import * as types from './mutation_types';
export default {
[types.REQUEST_VARIABLES](state) {
diff --git a/app/assets/javascripts/clusters/clusters_bundle.js b/app/assets/javascripts/clusters/clusters_bundle.js
index eb2128b2856..11db8058318 100644
--- a/app/assets/javascripts/clusters/clusters_bundle.js
+++ b/app/assets/javascripts/clusters/clusters_bundle.js
@@ -2,6 +2,8 @@ import Visibility from 'visibilityjs';
import Vue from 'vue';
import { GlToast } from '@gitlab/ui';
import AccessorUtilities from '~/lib/utils/accessor';
+import initProjectSelectDropdown from '~/project_select';
+import initServerlessSurveyBanner from '~/serverless/survey_banner';
import PersistentUserCallout from '../persistent_user_callout';
import { s__, sprintf } from '../locale';
import { deprecatedCreateFlash as Flash } from '../flash';
@@ -13,8 +15,6 @@ import ClustersService from './services/clusters_service';
import ClustersStore from './stores/clusters_store';
import Applications from './components/applications.vue';
import RemoveClusterConfirmation from './components/remove_cluster_confirmation.vue';
-import initProjectSelectDropdown from '~/project_select';
-import initServerlessSurveyBanner from '~/serverless/survey_banner';
const Environments = () => import('ee_component/clusters/components/environments.vue');
diff --git a/app/assets/javascripts/clusters/components/application_row.vue b/app/assets/javascripts/clusters/components/application_row.vue
index 471c1a0b4a2..21870e491ba 100644
--- a/app/assets/javascripts/clusters/components/application_row.vue
+++ b/app/assets/javascripts/clusters/components/application_row.vue
@@ -3,12 +3,11 @@ import { GlLink, GlModalDirective, GlSprintf, GlButton, GlAlert } from '@gitlab/
import { s__, __, sprintf } from '~/locale';
import eventHub from '../event_hub';
import identicon from '../../vue_shared/components/identicon.vue';
+import { APPLICATION_STATUS, ELASTIC_STACK } from '../constants';
import UninstallApplicationButton from './uninstall_application_button.vue';
import UninstallApplicationConfirmationModal from './uninstall_application_confirmation_modal.vue';
import UpdateApplicationConfirmationModal from './update_application_confirmation_modal.vue';
-import { APPLICATION_STATUS, ELASTIC_STACK } from '../constants';
-
export default {
components: {
GlButton,
diff --git a/app/assets/javascripts/clusters/components/applications.vue b/app/assets/javascripts/clusters/components/applications.vue
index e096a29ce7f..62bc1deba8d 100644
--- a/app/assets/javascripts/clusters/components/applications.vue
+++ b/app/assets/javascripts/clusters/components/applications.vue
@@ -10,11 +10,11 @@ import knativeLogo from 'images/cluster_app_logos/knative.png';
import prometheusLogo from 'images/cluster_app_logos/prometheus.png';
import elasticStackLogo from 'images/cluster_app_logos/elastic_stack.png';
import fluentdLogo from 'images/cluster_app_logos/fluentd.png';
-import applicationRow from './application_row.vue';
+import eventHub from '~/clusters/event_hub';
import clipboardButton from '../../vue_shared/components/clipboard_button.vue';
-import KnativeDomainEditor from './knative_domain_editor.vue';
import { CLUSTER_TYPE, PROVIDER_TYPE, APPLICATION_STATUS, INGRESS } from '../constants';
-import eventHub from '~/clusters/event_hub';
+import applicationRow from './application_row.vue';
+import KnativeDomainEditor from './knative_domain_editor.vue';
import CrossplaneProviderStack from './crossplane_provider_stack.vue';
import IngressModsecuritySettings from './ingress_modsecurity_settings.vue';
import FluentdOutputSettings from './fluentd_output_settings.vue';
@@ -349,6 +349,7 @@ export default {
{{ s__('ClusterIntegration|Issuer Email') }}
</label>
<div class="input-group">
+ <!-- eslint-disable vue/no-mutating-props -->
<input
id="cert-manager-issuer-email"
v-model="applications.cert_manager.email"
@@ -356,6 +357,7 @@ export default {
type="text"
class="form-control js-email"
/>
+ <!-- eslint-enable vue/no-mutating-props -->
</div>
<p class="form-text text-muted">
{{
@@ -522,6 +524,7 @@ export default {
<label for="jupyter-hostname">{{ s__('ClusterIntegration|Jupyter Hostname') }}</label>
<div class="input-group">
+ <!-- eslint-disable vue/no-mutating-props -->
<input
id="jupyter-hostname"
v-model="applications.jupyter.hostname"
@@ -529,6 +532,7 @@ export default {
type="text"
class="form-control js-hostname"
/>
+ <!-- eslint-enable vue/no-mutating-props -->
<span class="input-group-append">
<clipboard-button
:text="jupyterHostname"
diff --git a/app/assets/javascripts/clusters/components/ingress_modsecurity_settings.vue b/app/assets/javascripts/clusters/components/ingress_modsecurity_settings.vue
index f05c8db5d56..58a5edb832f 100644
--- a/app/assets/javascripts/clusters/components/ingress_modsecurity_settings.vue
+++ b/app/assets/javascripts/clusters/components/ingress_modsecurity_settings.vue
@@ -11,9 +11,9 @@ import {
GlIcon,
} from '@gitlab/ui';
import modSecurityLogo from 'images/cluster_app_logos/gitlab.png';
-import { s__, __ } from '../../locale';
import { APPLICATION_STATUS, INGRESS, LOGGING_MODE, BLOCKING_MODE } from '~/clusters/constants';
import eventHub from '~/clusters/event_hub';
+import { s__, __ } from '../../locale';
const { UPDATING, UNINSTALLING, INSTALLING, INSTALLED, UPDATED } = APPLICATION_STATUS;
@@ -130,9 +130,11 @@ export default {
},
resetStatus() {
if (this.initialMode !== null) {
+ // eslint-disable-next-line vue/no-mutating-props
this.ingress.modsecurity_mode = this.initialMode;
}
if (this.initialValue !== null) {
+ // eslint-disable-next-line vue/no-mutating-props
this.ingress.modsecurity_enabled = this.initialValue;
}
this.initialValue = null;
diff --git a/app/assets/javascripts/clusters/components/knative_domain_editor.vue b/app/assets/javascripts/clusters/components/knative_domain_editor.vue
index d80bd6f5b42..bf89c288b75 100644
--- a/app/assets/javascripts/clusters/components/knative_domain_editor.vue
+++ b/app/assets/javascripts/clusters/components/knative_domain_editor.vue
@@ -9,10 +9,10 @@ import {
GlButton,
GlAlert,
} from '@gitlab/ui';
-import ClipboardButton from '../../vue_shared/components/clipboard_button.vue';
import { __, s__ } from '~/locale';
import { APPLICATION_STATUS } from '~/clusters/constants';
+import ClipboardButton from '../../vue_shared/components/clipboard_button.vue';
const { UPDATING, UNINSTALLING } = APPLICATION_STATUS;
diff --git a/app/assets/javascripts/clusters/components/remove_cluster_confirmation.vue b/app/assets/javascripts/clusters/components/remove_cluster_confirmation.vue
index c157b04b4f5..c5678173be4 100644
--- a/app/assets/javascripts/clusters/components/remove_cluster_confirmation.vue
+++ b/app/assets/javascripts/clusters/components/remove_cluster_confirmation.vue
@@ -131,48 +131,46 @@ export default {
:title="modalTitle"
kind="danger"
>
- <template>
- <p>{{ warningMessage }}</p>
- <div v-if="confirmCleanup">
- {{ s__('ClusterIntegration|This will permanently delete the following resources:') }}
- <ul>
- <li>
- {{ s__('ClusterIntegration|All installed applications and related resources') }}
- </li>
- <li>
- <gl-sprintf :message="s__('ClusterIntegration|The %{gitlabNamespace} namespace')">
- <template #gitlabNamespace>
- <!-- eslint-disable-next-line @gitlab/vue-require-i18n-strings -->
- <code>{{ 'gitlab-managed-apps' }}</code>
- </template>
- </gl-sprintf>
- </li>
- <li>{{ s__('ClusterIntegration|Any project namespaces') }}</li>
- <!-- eslint-disable @gitlab/vue-require-i18n-strings -->
- <li><code>clusterroles</code></li>
- <li><code>clusterrolebindings</code></li>
- <!-- eslint-enable @gitlab/vue-require-i18n-strings -->
- </ul>
- </div>
- <strong v-html="confirmationTextLabel"></strong>
- <form ref="form" :action="clusterPath" method="post" class="gl-mb-5">
- <input ref="method" type="hidden" name="_method" value="delete" />
- <input :value="csrfToken" type="hidden" name="authenticity_token" />
- <input ref="cleanup" type="hidden" name="cleanup" value="true" />
- <gl-form-input
- v-model="enteredClusterName"
- autofocus
- type="text"
- name="confirm_cluster_name_input"
- autocomplete="off"
- />
- </form>
- <span v-if="confirmCleanup">{{
- s__(
- 'ClusterIntegration|If you do not wish to delete all associated GitLab resources, you can simply remove the integration.',
- )
- }}</span>
- </template>
+ <p>{{ warningMessage }}</p>
+ <div v-if="confirmCleanup">
+ {{ s__('ClusterIntegration|This will permanently delete the following resources:') }}
+ <ul>
+ <li>
+ {{ s__('ClusterIntegration|All installed applications and related resources') }}
+ </li>
+ <li>
+ <gl-sprintf :message="s__('ClusterIntegration|The %{gitlabNamespace} namespace')">
+ <template #gitlabNamespace>
+ <!-- eslint-disable-next-line @gitlab/vue-require-i18n-strings -->
+ <code>{{ 'gitlab-managed-apps' }}</code>
+ </template>
+ </gl-sprintf>
+ </li>
+ <li>{{ s__('ClusterIntegration|Any project namespaces') }}</li>
+ <!-- eslint-disable @gitlab/vue-require-i18n-strings -->
+ <li><code>clusterroles</code></li>
+ <li><code>clusterrolebindings</code></li>
+ <!-- eslint-enable @gitlab/vue-require-i18n-strings -->
+ </ul>
+ </div>
+ <strong v-html="confirmationTextLabel"></strong>
+ <form ref="form" :action="clusterPath" method="post" class="gl-mb-5">
+ <input ref="method" type="hidden" name="_method" value="delete" />
+ <input :value="csrfToken" type="hidden" name="authenticity_token" />
+ <input ref="cleanup" type="hidden" name="cleanup" value="true" />
+ <gl-form-input
+ v-model="enteredClusterName"
+ autofocus
+ type="text"
+ name="confirm_cluster_name_input"
+ autocomplete="off"
+ />
+ </form>
+ <span v-if="confirmCleanup">{{
+ s__(
+ 'ClusterIntegration|If you do not wish to delete all associated GitLab resources, you can simply remove the integration.',
+ )
+ }}</span>
<template #modal-footer>
<gl-button variant="secondary" @click="handleCancel">{{ s__('Cancel') }}</gl-button>
<template v-if="confirmCleanup">
diff --git a/app/assets/javascripts/clusters_list/components/clusters.vue b/app/assets/javascripts/clusters_list/components/clusters.vue
index 53eec5c8a0d..30b683cee41 100644
--- a/app/assets/javascripts/clusters_list/components/clusters.vue
+++ b/app/assets/javascripts/clusters_list/components/clusters.vue
@@ -10,10 +10,10 @@ import {
GlTable,
GlTooltipDirective,
} from '@gitlab/ui';
+import { __, sprintf } from '~/locale';
+import { CLUSTER_TYPES, STATUSES } from '../constants';
import AncestorNotice from './ancestor_notice.vue';
import NodeErrorHelpText from './node_error_help_text.vue';
-import { CLUSTER_TYPES, STATUSES } from '../constants';
-import { __, sprintf } from '~/locale';
export default {
nodeMemoryText: __('%{totalMemory} (%{freeSpacePercentage}%{percentSymbol} free)'),
diff --git a/app/assets/javascripts/clusters_list/store/actions.js b/app/assets/javascripts/clusters_list/store/actions.js
index 97ed0a7ab37..ee85b3e13fb 100644
--- a/app/assets/javascripts/clusters_list/store/actions.js
+++ b/app/assets/javascripts/clusters_list/store/actions.js
@@ -3,8 +3,8 @@ import Poll from '~/lib/utils/poll';
import axios from '~/lib/utils/axios_utils';
import { deprecatedCreateFlash as flash } from '~/flash';
import { __ } from '~/locale';
-import { MAX_REQUESTS } from '../constants';
import { parseIntPagination, normalizeHeaders } from '~/lib/utils/common_utils';
+import { MAX_REQUESTS } from '../constants';
import * as types from './mutation_types';
const allNodesPresent = (clusters, retryCount) => {
diff --git a/app/assets/javascripts/code_navigation/components/app.vue b/app/assets/javascripts/code_navigation/components/app.vue
index 85ec0a60ec5..d38b38947b6 100644
--- a/app/assets/javascripts/code_navigation/components/app.vue
+++ b/app/assets/javascripts/code_navigation/components/app.vue
@@ -1,7 +1,7 @@
<script>
import { mapActions, mapState } from 'vuex';
-import Popover from './popover.vue';
import eventHub from '../../notes/event_hub';
+import Popover from './popover.vue';
export default {
components: {
diff --git a/app/assets/javascripts/code_navigation/store/actions.js b/app/assets/javascripts/code_navigation/store/actions.js
index fb77a70de0b..0b6b8437db5 100644
--- a/app/assets/javascripts/code_navigation/store/actions.js
+++ b/app/assets/javascripts/code_navigation/store/actions.js
@@ -1,6 +1,6 @@
import axios from '~/lib/utils/axios_utils';
-import * as types from './mutation_types';
import { getCurrentHoverElement, setCurrentHoverElement, addInteractionClass } from '../utils';
+import * as types from './mutation_types';
export default {
setInitialData({ commit }, data) {
diff --git a/app/assets/javascripts/commit/pipelines/pipelines_table.vue b/app/assets/javascripts/commit/pipelines/pipelines_table.vue
index fe32868e6d8..fad98a66636 100644
--- a/app/assets/javascripts/commit/pipelines/pipelines_table.vue
+++ b/app/assets/javascripts/commit/pipelines/pipelines_table.vue
@@ -197,7 +197,7 @@ export default {
<gl-button
v-if="canRenderPipelineButton"
block
- class="gl-mt-3 gl-mb-0 gl-display-md-none"
+ class="gl-mt-3 gl-mb-0 gl-md-display-none"
variant="success"
data-testid="run_pipeline_button_mobile"
:loading="state.isRunningMergeRequestPipeline"
diff --git a/app/assets/javascripts/compare_autocomplete.js b/app/assets/javascripts/compare_autocomplete.js
index 82384434e8f..b3958b05d62 100644
--- a/app/assets/javascripts/compare_autocomplete.js
+++ b/app/assets/javascripts/compare_autocomplete.js
@@ -1,12 +1,12 @@
/* eslint-disable func-names */
import $ from 'jquery';
+import { fixTitle } from '~/tooltips';
+import initDeprecatedJQueryDropdown from '~/deprecated_jquery_dropdown';
import { __ } from './locale';
import axios from './lib/utils/axios_utils';
import { deprecatedCreateFlash as flash } from './flash';
import { capitalizeFirstCharacter } from './lib/utils/text_utility';
-import { fixTitle } from '~/tooltips';
-import initDeprecatedJQueryDropdown from '~/deprecated_jquery_dropdown';
export default function initCompareAutocomplete(limitTo = null, clickHandler = () => {}) {
$('.js-compare-dropdown').each(function () {
diff --git a/app/assets/javascripts/contributors/components/contributors.vue b/app/assets/javascripts/contributors/components/contributors.vue
index 86580aa170b..977c2ba907e 100644
--- a/app/assets/javascripts/contributors/components/contributors.vue
+++ b/app/assets/javascripts/contributors/components/contributors.vue
@@ -6,8 +6,8 @@ import { GlAreaChart } from '@gitlab/ui/dist/charts';
import { __ } from '~/locale';
import { getSvgIconPathContent } from '~/lib/utils/icon_utils';
import { getDatesInRange } from '~/lib/utils/datetime_utility';
-import { xAxisLabelFormatter, dateFormatter } from '../utils';
import ResizableChartContainer from '~/vue_shared/components/resizable_chart/resizable_chart_container.vue';
+import { xAxisLabelFormatter, dateFormatter } from '../utils';
export default {
components: {
diff --git a/app/assets/javascripts/create_cluster/eks_cluster/components/service_credentials_form.vue b/app/assets/javascripts/create_cluster/eks_cluster/components/service_credentials_form.vue
index a3f76241bf2..f104eb61e41 100644
--- a/app/assets/javascripts/create_cluster/eks_cluster/components/service_credentials_form.vue
+++ b/app/assets/javascripts/create_cluster/eks_cluster/components/service_credentials_form.vue
@@ -1,11 +1,11 @@
<script>
/* eslint-disable vue/no-v-html */
-import { GlButton, GlFormGroup, GlFormInput, GlIcon, GlLink, GlSprintf } from '@gitlab/ui';
+import { GlButton, GlFormGroup, GlFormInput, GlIcon, GlLink, GlSprintf, GlAlert } from '@gitlab/ui';
import { escape } from 'lodash';
import { mapState, mapActions } from 'vuex';
-import { DEFAULT_REGION } from '../constants';
import { sprintf, s__, __ } from '~/locale';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
+import { DEFAULT_REGION } from '../constants';
export default {
components: {
@@ -16,6 +16,7 @@ export default {
GlLink,
GlSprintf,
ClipboardButton,
+ GlAlert,
},
props: {
accountAndExternalIdsHelpPath: {
@@ -105,9 +106,14 @@ export default {
)
}}
</p>
- <div v-if="createRoleError" class="js-invalid-credentials bs-callout bs-callout-danger">
+ <gl-alert
+ v-if="createRoleError"
+ class="js-invalid-credentials gl-mb-5"
+ variant="danger"
+ :dismissible="false"
+ >
{{ createRoleError }}
- </div>
+ </gl-alert>
<div class="form-row">
<div class="form-group col-md-6">
<label for="gitlab-account-id">{{ __('Account ID') }}</label>
diff --git a/app/assets/javascripts/create_cluster/eks_cluster/store/actions.js b/app/assets/javascripts/create_cluster/eks_cluster/store/actions.js
index 55576efd3b8..6df9645b03a 100644
--- a/app/assets/javascripts/create_cluster/eks_cluster/store/actions.js
+++ b/app/assets/javascripts/create_cluster/eks_cluster/store/actions.js
@@ -1,9 +1,9 @@
-import * as types from './mutation_types';
-import { DEFAULT_REGION } from '../constants';
-import { setAWSConfig } from '../services/aws_services_facade';
import axios from '~/lib/utils/axios_utils';
import { deprecatedCreateFlash as createFlash } from '~/flash';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
+import { setAWSConfig } from '../services/aws_services_facade';
+import { DEFAULT_REGION } from '../constants';
+import * as types from './mutation_types';
const getErrorMessage = (data) => {
const errorKey = Object.keys(data)[0];
diff --git a/app/assets/javascripts/create_cluster/eks_cluster/store/index.js b/app/assets/javascripts/create_cluster/eks_cluster/store/index.js
index 262bbb3167a..ed054989771 100644
--- a/app/assets/javascripts/create_cluster/eks_cluster/store/index.js
+++ b/app/assets/javascripts/create_cluster/eks_cluster/store/index.js
@@ -1,11 +1,5 @@
import Vuex from 'vuex';
-import * as actions from './actions';
-import * as getters from './getters';
-import mutations from './mutations';
-import state from './state';
-
import clusterDropdownStore from '~/create_cluster/store/cluster_dropdown';
-
import {
fetchRoles,
fetchKeyPairs,
@@ -13,6 +7,10 @@ import {
fetchSubnets,
fetchSecurityGroups,
} from '../services/aws_services_facade';
+import * as actions from './actions';
+import * as getters from './getters';
+import mutations from './mutations';
+import state from './state';
const createStore = ({ initialState }) =>
new Vuex.Store({
diff --git a/app/assets/javascripts/create_cluster/gke_cluster/store/actions.js b/app/assets/javascripts/create_cluster/gke_cluster/store/actions.js
index 8977053297a..f4c35dafc22 100644
--- a/app/assets/javascripts/create_cluster/gke_cluster/store/actions.js
+++ b/app/assets/javascripts/create_cluster/gke_cluster/store/actions.js
@@ -1,5 +1,5 @@
-import * as types from './mutation_types';
import gapiLoader from '../gapi_loader';
+import * as types from './mutation_types';
const gapiResourceListRequest = ({ resource, params, commit, mutation, payloadKey }) =>
new Promise((resolve, reject) => {
diff --git a/app/assets/javascripts/create_cluster/init_create_cluster.js b/app/assets/javascripts/create_cluster/init_create_cluster.js
index f97da3d55db..d367d7ec333 100644
--- a/app/assets/javascripts/create_cluster/init_create_cluster.js
+++ b/app/assets/javascripts/create_cluster/init_create_cluster.js
@@ -1,6 +1,6 @@
+import PersistentUserCallout from '~/persistent_user_callout';
import initGkeDropdowns from './gke_cluster';
import initGkeNamespace from './gke_cluster_namespace';
-import PersistentUserCallout from '~/persistent_user_callout';
const newClusterViews = [':clusters:new', ':clusters:create_gcp', ':clusters:create_user'];
diff --git a/app/assets/javascripts/custom_metrics/components/custom_metrics_form.vue b/app/assets/javascripts/custom_metrics/components/custom_metrics_form.vue
index 9c28801306c..1b32225d251 100644
--- a/app/assets/javascripts/custom_metrics/components/custom_metrics_form.vue
+++ b/app/assets/javascripts/custom_metrics/components/custom_metrics_form.vue
@@ -2,9 +2,9 @@
import { GlButton } from '@gitlab/ui';
import { __, s__ } from '~/locale';
import csrf from '~/lib/utils/csrf';
+import { formDataValidator } from '../constants';
import CustomMetricsFormFields from './custom_metrics_form_fields.vue';
import DeleteCustomMetricModal from './delete_custom_metric_modal.vue';
-import { formDataValidator } from '../constants';
export default {
components: {
diff --git a/app/assets/javascripts/cycle_analytics/components/banner.vue b/app/assets/javascripts/cycle_analytics/components/banner.vue
index 4448d909c9b..cf4c35ef12b 100644
--- a/app/assets/javascripts/cycle_analytics/components/banner.vue
+++ b/app/assets/javascripts/cycle_analytics/components/banner.vue
@@ -1,7 +1,7 @@
<script>
/* eslint-disable vue/no-v-html */
-import iconCycleAnalyticsSplash from 'icons/_icon_cycle_analytics_splash.svg';
import { GlIcon } from '@gitlab/ui';
+import iconCycleAnalyticsSplash from 'icons/_icon_cycle_analytics_splash.svg';
export default {
components: {
diff --git a/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js b/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js
index bd5a6cc40c4..56954a4e97f 100644
--- a/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js
+++ b/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js
@@ -1,9 +1,13 @@
+// This is a true violation of @gitlab/no-runtime-template-compiler, as it
+// relies on app/views/projects/cycle_analytics/show.html.haml for its
+// template.
+/* eslint-disable @gitlab/no-runtime-template-compiler */
import $ from 'jquery';
import Vue from 'vue';
import Cookies from 'js-cookie';
import { GlEmptyState, GlLoadingIcon } from '@gitlab/ui';
-import { deprecatedCreateFlash as Flash } from '../flash';
import { __ } from '~/locale';
+import { deprecatedCreateFlash as Flash } from '../flash';
import Translate from '../vue_shared/translate';
import banner from './components/banner.vue';
import stageCodeComponent from './components/stage_code_component.vue';
diff --git a/app/assets/javascripts/deploy_freeze/store/actions.js b/app/assets/javascripts/deploy_freeze/store/actions.js
index 9a75c3cad2f..62045d2517d 100644
--- a/app/assets/javascripts/deploy_freeze/store/actions.js
+++ b/app/assets/javascripts/deploy_freeze/store/actions.js
@@ -1,7 +1,7 @@
-import * as types from './mutation_types';
import Api from '~/api';
import { deprecatedCreateFlash as createFlash } from '~/flash';
import { __ } from '~/locale';
+import * as types from './mutation_types';
export const requestAddFreezePeriod = ({ commit }) => {
commit(types.REQUEST_ADD_FREEZE_PERIOD);
diff --git a/app/assets/javascripts/deploy_keys/components/key.vue b/app/assets/javascripts/deploy_keys/components/key.vue
index 3ddaba7abcc..797f5a6aaf0 100644
--- a/app/assets/javascripts/deploy_keys/components/key.vue
+++ b/app/assets/javascripts/deploy_keys/components/key.vue
@@ -97,7 +97,7 @@ export default {
methods: {
projectTooltipTitle(project) {
return project.can_push
- ? s__('DeployKeys|Write access allowed')
+ ? s__('DeployKeys|Grant write permissions to this key')
: s__('DeployKeys|Read access only');
},
toggleExpanded() {
diff --git a/app/assets/javascripts/deploy_keys/components/keys_panel.vue b/app/assets/javascripts/deploy_keys/components/keys_panel.vue
index 2693cd08cc3..d71f4f5507f 100644
--- a/app/assets/javascripts/deploy_keys/components/keys_panel.vue
+++ b/app/assets/javascripts/deploy_keys/components/keys_panel.vue
@@ -48,7 +48,7 @@ export default {
:project-id="projectId"
/>
</template>
- <div v-else class="settings-message text-center">
+ <div v-else class="settings-message text-center gl-mt-5">
{{ s__('DeployKeys|No deploy keys found. Create one with the form above.') }}
</div>
</div>
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 ea4d5d7b570..ab963990ce3 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
@@ -9,12 +9,12 @@ import allVersionsMixin from '../../mixins/all_versions';
import createNoteMutation from '../../graphql/mutations/create_note.mutation.graphql';
import toggleResolveDiscussionMutation from '../../graphql/mutations/toggle_resolve_discussion.mutation.graphql';
import activeDiscussionQuery from '../../graphql/queries/active_discussion.query.graphql';
-import DesignNote from './design_note.vue';
-import DesignReplyForm from './design_reply_form.vue';
import { ACTIVE_DISCUSSION_SOURCE_TYPES } from '../../constants';
-import ToggleRepliesWidget from './toggle_replies_widget.vue';
import { hasErrors } from '../../utils/cache_update';
import { ADD_DISCUSSION_COMMENT_ERROR } from '../../utils/error_messages';
+import DesignNote from './design_note.vue';
+import DesignReplyForm from './design_reply_form.vue';
+import ToggleRepliesWidget from './toggle_replies_widget.vue';
export default {
components: {
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 421a4dc274a..371f2d06486 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,13 @@
<script>
import { ApolloMutation } from 'vue-apollo';
import { GlTooltipDirective, GlIcon, GlLink, GlSafeHtmlDirective } from '@gitlab/ui';
-import updateNoteMutation from '../../graphql/mutations/update_note.mutation.graphql';
import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue';
import TimelineEntryItem from '~/vue_shared/components/notes/timeline_entry_item.vue';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
-import DesignReplyForm from './design_reply_form.vue';
+import updateNoteMutation from '../../graphql/mutations/update_note.mutation.graphql';
import { findNoteId, extractDesignNoteId } from '../../utils/design_management_utils';
import { hasErrors } from '../../utils/cache_update';
+import DesignReplyForm from './design_reply_form.vue';
export default {
components: {
diff --git a/app/assets/javascripts/design_management/components/design_overlay.vue b/app/assets/javascripts/design_management/components/design_overlay.vue
index 3c2ce693bc0..d19ac198fb2 100644
--- a/app/assets/javascripts/design_management/components/design_overlay.vue
+++ b/app/assets/javascripts/design_management/components/design_overlay.vue
@@ -2,8 +2,8 @@
import { __ } from '~/locale';
import activeDiscussionQuery from '../graphql/queries/active_discussion.query.graphql';
import updateActiveDiscussionMutation from '../graphql/mutations/update_active_discussion.mutation.graphql';
-import DesignNotePin from './design_note_pin.vue';
import { ACTIVE_DISCUSSION_SOURCE_TYPES } from '../constants';
+import DesignNotePin from './design_note_pin.vue';
export default {
name: 'DesignOverlay',
diff --git a/app/assets/javascripts/design_management/components/design_sidebar.vue b/app/assets/javascripts/design_management/components/design_sidebar.vue
index 50b12fd739b..d6063e2e79e 100644
--- a/app/assets/javascripts/design_management/components/design_sidebar.vue
+++ b/app/assets/javascripts/design_management/components/design_sidebar.vue
@@ -3,13 +3,13 @@ import Cookies from 'js-cookie';
import { GlCollapse, GlButton, GlPopover } from '@gitlab/ui';
import { s__ } from '~/locale';
import { parseBoolean } from '~/lib/utils/common_utils';
+import Participants from '~/sidebar/components/participants/participants.vue';
+import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import updateActiveDiscussionMutation from '../graphql/mutations/update_active_discussion.mutation.graphql';
import { extractDiscussions, extractParticipants } from '../utils/design_management_utils';
import { ACTIVE_DISCUSSION_SOURCE_TYPES } from '../constants';
import DesignDiscussion from './design_notes/design_discussion.vue';
-import Participants from '~/sidebar/components/participants/participants.vue';
import DesignTodoButton from './design_todo_button.vue';
-import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
export default {
components: {
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 db14db79989..e80300ad57b 100644
--- a/app/assets/javascripts/design_management/components/design_todo_button.vue
+++ b/app/assets/javascripts/design_management/components/design_todo_button.vue
@@ -1,8 +1,8 @@
<script>
import todoMarkDoneMutation from '~/graphql_shared/mutations/todo_mark_done.mutation.graphql';
+import TodoButton from '~/vue_shared/components/todo_button.vue';
import getDesignQuery from '../graphql/queries/get_design.query.graphql';
import createDesignTodoMutation from '../graphql/mutations/create_design_todo.mutation.graphql';
-import TodoButton from '~/vue_shared/components/todo_button.vue';
import allVersionsMixin from '../mixins/all_versions';
import { updateStoreAfterDeleteDesignTodo } from '../utils/cache_update';
import { findIssueId, findDesignId } from '../utils/design_management_utils';
diff --git a/app/assets/javascripts/design_management/components/list/item.vue b/app/assets/javascripts/design_management/components/list/item.vue
index fa09c7c15cc..67110265b5f 100644
--- a/app/assets/javascripts/design_management/components/list/item.vue
+++ b/app/assets/javascripts/design_management/components/list/item.vue
@@ -1,5 +1,5 @@
<script>
-import { GlLoadingIcon, GlIcon, GlIntersectionObserver } from '@gitlab/ui';
+import { GlLoadingIcon, GlIcon, GlIntersectionObserver, GlTooltipDirective } from '@gitlab/ui';
import Timeago from '~/vue_shared/components/time_ago_tooltip.vue';
import { n__, __ } from '~/locale';
import { DESIGN_ROUTE_NAME } from '../../router/constants';
@@ -11,6 +11,9 @@ export default {
GlIcon,
Timeago,
},
+ directives: {
+ GlTooltip: GlTooltipDirective,
+ },
props: {
id: {
type: [Number, String],
@@ -130,7 +133,7 @@ export default {
<div
class="card-body gl-p-0 gl-display-flex gl-align-items-center gl-justify-content-center gl-overflow-hidden gl-relative"
>
- <div v-if="icon.name" data-testid="designEvent" class="design-event gl-absolute">
+ <div v-if="icon.name" data-testid="design-event" class="gl-top-5 gl-right-5 gl-absolute">
<span :title="icon.tooltip" :aria-label="icon.tooltip">
<gl-icon
:name="icon.name"
@@ -153,9 +156,10 @@ export default {
v-show="showImage"
:src="imageLink"
:alt="filename"
- class="gl-display-block gl-mx-auto gl-max-w-full mh-100 design-img"
+ class="gl-display-block gl-mx-auto gl-max-w-full gl-max-h-full design-img"
data-qa-selector="design_image"
:data-qa-filename="filename"
+ :data-testid="`design-img-${id}`"
@load="onImageLoad"
@error="onImageError"
/>
@@ -163,9 +167,14 @@ export default {
</div>
<div class="card-footer gl-display-flex gl-w-full">
<div class="gl-display-flex gl-flex-direction-column str-truncated-100">
- <span class="gl-font-weight-bold str-truncated-100" data-qa-selector="design_file_name">{{
- filename
- }}</span>
+ <span
+ v-gl-tooltip
+ class="gl-font-weight-bold str-truncated-100"
+ data-qa-selector="design_file_name"
+ :data-testid="`design-img-filename-${id}`"
+ :title="filename"
+ >{{ filename }}</span
+ >
<span v-if="updatedAt" class="str-truncated-100">
{{ __('Updated') }} <timeago :time="updatedAt" tooltip-placement="bottom" />
</span>
diff --git a/app/assets/javascripts/design_management/components/toolbar/index.vue b/app/assets/javascripts/design_management/components/toolbar/index.vue
index 3509a701984..295327b0f05 100644
--- a/app/assets/javascripts/design_management/components/toolbar/index.vue
+++ b/app/assets/javascripts/design_management/components/toolbar/index.vue
@@ -3,9 +3,9 @@ import { GlButton, GlIcon, GlTooltipDirective } from '@gitlab/ui';
import permissionsQuery from 'shared_queries/design_management/design_permissions.query.graphql';
import { __, sprintf } from '~/locale';
import timeagoMixin from '~/vue_shared/mixins/timeago';
-import DesignNavigation from './design_navigation.vue';
import DeleteButton from '../delete_button.vue';
import { DESIGNS_ROUTE_NAME } from '../../router/constants';
+import DesignNavigation from './design_navigation.vue';
export default {
components: {
diff --git a/app/assets/javascripts/design_management/mixins/all_designs.js b/app/assets/javascripts/design_management/mixins/all_designs.js
index 4783382d563..e92f8006a0d 100644
--- a/app/assets/javascripts/design_management/mixins/all_designs.js
+++ b/app/assets/javascripts/design_management/mixins/all_designs.js
@@ -2,8 +2,8 @@ import { propertyOf } from 'lodash';
import getDesignListQuery from 'shared_queries/design_management/get_design_list.query.graphql';
import createFlash, { FLASH_TYPES } from '~/flash';
import { s__ } from '~/locale';
-import allVersionsMixin from './all_versions';
import { DESIGNS_ROUTE_NAME } from '../router/constants';
+import allVersionsMixin from './all_versions';
export default {
mixins: [allVersionsMixin],
diff --git a/app/assets/javascripts/design_management/pages/index.vue b/app/assets/javascripts/design_management/pages/index.vue
index 5c82a7331b6..59f567fc372 100644
--- a/app/assets/javascripts/design_management/pages/index.vue
+++ b/app/assets/javascripts/design_management/pages/index.vue
@@ -6,12 +6,12 @@ import permissionsQuery from 'shared_queries/design_management/design_permission
import createFlash, { FLASH_TYPES } from '~/flash';
import { __, s__, sprintf } from '~/locale';
import { getFilename } from '~/lib/utils/file_upload';
+import DesignDropzone from '~/vue_shared/components/upload_dropzone/upload_dropzone.vue';
import UploadButton from '../components/upload/button.vue';
import DeleteButton from '../components/delete_button.vue';
import Design from '../components/list/item.vue';
import DesignDestroyer from '../components/design_destroyer.vue';
import DesignVersionDropdown from '../components/upload/design_version_dropdown.vue';
-import DesignDropzone from '~/vue_shared/components/upload_dropzone/upload_dropzone.vue';
import uploadDesignMutation from '../graphql/mutations/upload_design.mutation.graphql';
import moveDesignMutation from '../graphql/mutations/move_design.mutation.graphql';
import allDesignsMixin from '../mixins/all_designs';
diff --git a/app/assets/javascripts/design_management/utils/error_messages.js b/app/assets/javascripts/design_management/utils/error_messages.js
index cb4bb6e26a8..e7b2c814bb3 100644
--- a/app/assets/javascripts/design_management/utils/error_messages.js
+++ b/app/assets/javascripts/design_management/utils/error_messages.js
@@ -44,13 +44,13 @@ export const MOVE_DESIGN_ERROR = __(
'Something went wrong when reordering designs. Please try again',
);
-export const CREATE_DESIGN_TODO_ERROR = __('Failed to create To-Do for the design.');
+export const CREATE_DESIGN_TODO_ERROR = __('Failed to create a to-do item for the design.');
-export const CREATE_DESIGN_TODO_EXISTS_ERROR = __('There is already a To-Do for this design.');
+export const CREATE_DESIGN_TODO_EXISTS_ERROR = __('There is already a to-do item for this design.');
-export const DELETE_DESIGN_TODO_ERROR = __('Failed to remove To-Do for the design.');
+export const DELETE_DESIGN_TODO_ERROR = __('Failed to remove a to-do item for the design.');
-export const TOGGLE_TODO_ERROR = __('Failed to toggle To-Do for the design.');
+export const TOGGLE_TODO_ERROR = __('Failed to toggle the to-do status for the design.');
const MAX_SKIPPED_FILES_LISTINGS = 5;
diff --git a/app/assets/javascripts/diffs/components/app.vue b/app/assets/javascripts/diffs/components/app.vue
index 32822fe1fe8..0d74d654cd6 100644
--- a/app/assets/javascripts/diffs/components/app.vue
+++ b/app/assets/javascripts/diffs/components/app.vue
@@ -14,19 +14,9 @@ import { updateHistory } from '~/lib/utils/url_utility';
import notesEventHub from '../../notes/event_hub';
import eventHub from '../event_hub';
-import CompareVersions from './compare_versions.vue';
-import DiffFile from './diff_file.vue';
-import NoChanges from './no_changes.vue';
-import CommitWidget from './commit_widget.vue';
-import TreeList from './tree_list.vue';
-
-import HiddenFilesWarning from './hidden_files_warning.vue';
-import MergeConflictWarning from './merge_conflict_warning.vue';
-import CollapsedFilesWarning from './collapsed_files_warning.vue';
-
import { diffsApp } from '../utils/performance';
import { fileByFile } from '../utils/preferences';
-
+import { reviewStatuses } from '../utils/file_reviews';
import {
TREE_LIST_WIDTH_STORAGE_KEY,
INITIAL_TREE_WIDTH,
@@ -40,6 +30,15 @@ import {
ALERT_COLLAPSED_FILES,
EVT_VIEW_FILE_BY_FILE,
} from '../constants';
+import CompareVersions from './compare_versions.vue';
+import DiffFile from './diff_file.vue';
+import NoChanges from './no_changes.vue';
+import CommitWidget from './commit_widget.vue';
+import TreeList from './tree_list.vue';
+
+import HiddenFilesWarning from './hidden_files_warning.vue';
+import MergeConflictWarning from './merge_conflict_warning.vue';
+import CollapsedFilesWarning from './collapsed_files_warning.vue';
export default {
name: 'DiffsApp',
@@ -169,12 +168,7 @@ export default {
'hasConflicts',
'viewDiffsFileByFile',
]),
- ...mapGetters('diffs', [
- 'whichCollapsedTypes',
- 'isParallelView',
- 'currentDiffIndex',
- 'fileReviews',
- ]),
+ ...mapGetters('diffs', ['whichCollapsedTypes', 'isParallelView', 'currentDiffIndex']),
...mapGetters(['isNotesFetched', 'getNoteableData']),
diffs() {
if (!this.viewDiffsFileByFile) {
@@ -232,6 +226,9 @@ export default {
return visible;
},
+ fileReviews() {
+ return reviewStatuses(this.diffFiles, this.mrReviews);
+ },
},
watch: {
commit(newCommit, oldCommit) {
diff --git a/app/assets/javascripts/diffs/components/compare_versions.vue b/app/assets/javascripts/diffs/components/compare_versions.vue
index 489278fd6ef..8db0a542eb5 100644
--- a/app/assets/javascripts/diffs/components/compare_versions.vue
+++ b/app/assets/javascripts/diffs/components/compare_versions.vue
@@ -3,11 +3,11 @@ import { mapActions, mapGetters, mapState } from 'vuex';
import { GlTooltipDirective, GlLink, GlButton, GlSprintf } from '@gitlab/ui';
import { __ } from '~/locale';
import { polyfillSticky } from '~/lib/utils/sticky';
+import { CENTERED_LIMITED_CONTAINER_CLASSES, EVT_EXPAND_ALL_FILES } from '../constants';
+import eventHub from '../event_hub';
import CompareDropdownLayout from './compare_dropdown_layout.vue';
import SettingsDropdown from './settings_dropdown.vue';
import DiffStats from './diff_stats.vue';
-import { CENTERED_LIMITED_CONTAINER_CLASSES, EVT_EXPAND_ALL_FILES } from '../constants';
-import eventHub from '../event_hub';
export default {
components: {
diff --git a/app/assets/javascripts/diffs/components/diff_content.vue b/app/assets/javascripts/diffs/components/diff_content.vue
index f4e2571dd09..137fcfdca88 100644
--- a/app/assets/javascripts/diffs/components/diff_content.vue
+++ b/app/assets/javascripts/diffs/components/diff_content.vue
@@ -8,17 +8,17 @@ import DiffViewer from '~/vue_shared/components/diff_viewer/diff_viewer.vue';
import NotDiffableViewer from '~/vue_shared/components/diff_viewer/viewers/not_diffable.vue';
import NoPreviewViewer from '~/vue_shared/components/diff_viewer/viewers/no_preview.vue';
import DiffFileDrafts from '~/batch_comments/components/diff_file_drafts.vue';
-import InlineDiffView from './inline_diff_view.vue';
-import ParallelDiffView from './parallel_diff_view.vue';
-import DiffView from './diff_view.vue';
+import { diffViewerModes } from '~/ide/constants';
import userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue';
import NoteForm from '../../notes/components/note_form.vue';
-import ImageDiffOverlay from './image_diff_overlay.vue';
-import DiffDiscussions from './diff_discussions.vue';
import eventHub from '../../notes/event_hub';
import { IMAGE_DIFF_POSITION_TYPE } from '../constants';
import { getDiffMode } from '../store/utils';
-import { diffViewerModes } from '~/ide/constants';
+import InlineDiffView from './inline_diff_view.vue';
+import ParallelDiffView from './parallel_diff_view.vue';
+import DiffView from './diff_view.vue';
+import ImageDiffOverlay from './image_diff_overlay.vue';
+import DiffDiscussions from './diff_discussions.vue';
import { mapInline, mapParallel } from './diff_row_utils';
export default {
diff --git a/app/assets/javascripts/diffs/components/diff_discussion_reply.vue b/app/assets/javascripts/diffs/components/diff_discussion_reply.vue
index 531ebaddacd..a7a6b38235b 100644
--- a/app/assets/javascripts/diffs/components/diff_discussion_reply.vue
+++ b/app/assets/javascripts/diffs/components/diff_discussion_reply.vue
@@ -2,14 +2,12 @@
import { mapGetters } from 'vuex';
import NoteSignedOutWidget from '~/notes/components/note_signed_out_widget.vue';
import ReplyPlaceholder from '~/notes/components/discussion_reply_placeholder.vue';
-import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue';
export default {
name: 'DiffDiscussionReply',
components: {
NoteSignedOutWidget,
ReplyPlaceholder,
- UserAvatarLink,
},
props: {
hasForm: {
@@ -36,13 +34,6 @@ export default {
<template v-if="userCanReply">
<slot v-if="hasForm" name="form"></slot>
<template v-else-if="renderReplyPlaceholder">
- <user-avatar-link
- :link-href="currentUser.path"
- :img-src="currentUser.avatar_url"
- :img-alt="currentUser.name"
- :img-size="40"
- class="d-none d-sm-block"
- />
<reply-placeholder
:button-text="__('Start a new discussion...')"
@onClick="$emit('showNewDiscussionForm')"
diff --git a/app/assets/javascripts/diffs/components/diff_file.vue b/app/assets/javascripts/diffs/components/diff_file.vue
index e613b684345..5659f1a098e 100644
--- a/app/assets/javascripts/diffs/components/diff_file.vue
+++ b/app/assets/javascripts/diffs/components/diff_file.vue
@@ -1,16 +1,16 @@
<script>
import { mapActions, mapGetters, mapState } from 'vuex';
import { escape } from 'lodash';
-import { GlButton, GlLoadingIcon, GlSafeHtmlDirective as SafeHtml } from '@gitlab/ui';
+import { GlButton, GlLoadingIcon, GlSafeHtmlDirective as SafeHtml, GlSprintf } from '@gitlab/ui';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { sprintf } from '~/locale';
import { deprecatedCreateFlash as createFlash } from '~/flash';
import { hasDiff } from '~/helpers/diffs_helper';
-import notesEventHub from '../../notes/event_hub';
-import DiffFileHeader from './diff_file_header.vue';
-import DiffContent from './diff_content.vue';
import { diffViewerErrors } from '~/ide/constants';
-import { collapsedType, isCollapsed } from '../utils/diff_file';
+import notesEventHub from '../../notes/event_hub';
+
+import { collapsedType, isCollapsed, getShortShaFromFile } from '../utils/diff_file';
+
import {
DIFF_FILE_AUTOMATIC_COLLAPSE,
DIFF_FILE_MANUAL_COLLAPSE,
@@ -20,6 +20,8 @@ import {
} from '../constants';
import { DIFF_FILE, GENERIC_ERROR } from '../i18n';
import eventHub from '../event_hub';
+import DiffContent from './diff_content.vue';
+import DiffFileHeader from './diff_file_header.vue';
export default {
components: {
@@ -27,6 +29,7 @@ export default {
DiffContent,
GlButton,
GlLoadingIcon,
+ GlSprintf,
},
directives: {
SafeHtml,
@@ -81,15 +84,11 @@ export default {
...mapState('diffs', ['currentDiffFileId']),
...mapGetters(['isNotesFetched']),
...mapGetters('diffs', ['getDiffFileDiscussions']),
- viewBlobLink() {
- return sprintf(
- this.$options.i18n.blobView,
- {
- linkStart: `<a href="${escape(this.file.view_path)}">`,
- linkEnd: '</a>',
- },
- false,
- );
+ viewBlobHref() {
+ return escape(this.file.view_path);
+ },
+ shortSha() {
+ return getShortShaFromFile(this.file);
},
showLoadingIcon() {
return this.isLoadingCollapsedDiff || (!this.file.renderIt && !this.isCollapsed);
@@ -98,7 +97,7 @@ export default {
return hasDiff(this.file);
},
isFileTooLarge() {
- return this.file.viewer.error === diffViewerErrors.too_large;
+ return !this.manuallyCollapsed && this.file.viewer.error === diffViewerErrors.too_large;
},
errorMessage() {
return !this.manuallyCollapsed ? this.file.viewer.error_message : '';
@@ -144,6 +143,12 @@ export default {
showContent() {
return !this.isCollapsed && !this.isFileTooLarge;
},
+ showLocalFileReviews() {
+ const loggedIn = Boolean(gon.current_user_id);
+ const featureOn = this.glFeatures.localFileReviews;
+
+ return loggedIn && featureOn;
+ },
},
watch: {
'file.file_hash': {
@@ -181,6 +186,10 @@ export default {
if (this.hasDiff) {
this.postRender();
}
+
+ if (this.reviewed && !this.isCollapsed && this.showLocalFileReviews) {
+ this.handleToggle();
+ }
},
beforeDestroy() {
eventHub.$off(EVT_EXPAND_ALL_FILES, this.expandAllListener);
@@ -273,9 +282,11 @@ export default {
:can-current-user-fork="canCurrentUserFork"
:diff-file="file"
:collapsible="true"
+ :reviewed="reviewed"
:expanded="!isCollapsed"
:add-merge-request-buttons="true"
:view-diffs-file-by-file="viewDiffsFileByFile"
+ :show-local-file-reviews="showLocalFileReviews"
class="js-file-title file-title gl-border-1 gl-border-solid gl-border-gray-100"
:class="hasBodyClasses.header"
@toggleFile="handleToggle"
@@ -309,14 +320,27 @@ export default {
data-testid="loader-icon"
/>
<div v-else-if="errorMessage" class="diff-viewer">
- <div v-safe-html="errorMessage" class="nothing-here-block"></div>
+ <div
+ v-if="isFileTooLarge"
+ class="collapsed-file-warning gl-p-7 gl-bg-orange-50 gl-text-center gl-rounded-bottom-left-base gl-rounded-bottom-right-base"
+ >
+ <p class="gl-mb-5">
+ {{ $options.i18n.tooLarge }}
+ </p>
+ <gl-button data-testid="blob-button" category="secondary" :href="viewBlobHref">
+ <gl-sprintf :message="$options.i18n.blobView">
+ <template #commitSha>{{ shortSha }}</template>
+ </gl-sprintf>
+ </gl-button>
+ </div>
+ <div v-else v-safe-html="errorMessage" class="nothing-here-block"></div>
</div>
<template v-else>
<div
v-show="showWarning"
class="collapsed-file-warning gl-p-7 gl-bg-orange-50 gl-text-center gl-rounded-bottom-left-base gl-rounded-bottom-right-base"
>
- <p class="gl-mb-8">
+ <p class="gl-mb-5">
{{ $options.i18n.autoCollapsed }}
</p>
<gl-button
diff --git a/app/assets/javascripts/diffs/components/diff_file_header.vue b/app/assets/javascripts/diffs/components/diff_file_header.vue
index 53d1383b82e..eb842a09d64 100644
--- a/app/assets/javascripts/diffs/components/diff_file_header.vue
+++ b/app/assets/javascripts/diffs/components/diff_file_header.vue
@@ -1,6 +1,6 @@
<script>
import { escape } from 'lodash';
-import { mapActions, mapGetters } from 'vuex';
+import { mapActions, mapGetters, mapState } from 'vuex';
import {
GlTooltipDirective,
GlSafeHtmlDirective,
@@ -10,17 +10,24 @@ import {
GlDropdown,
GlDropdownItem,
GlDropdownDivider,
+ GlFormCheckbox,
GlLoadingIcon,
} from '@gitlab/ui';
+import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import FileIcon from '~/vue_shared/components/file_icon.vue';
import { truncateSha } from '~/lib/utils/text_utility';
import { __, s__, sprintf } from '~/locale';
-import { diffViewerModes } from '~/ide/constants';
-import DiffStats from './diff_stats.vue';
import { scrollToElement } from '~/lib/utils/common_utils';
-import { isCollapsed } from '../utils/diff_file';
+
+import { diffViewerModes } from '~/ide/constants';
+import { collapsedType, isCollapsed } from '../utils/diff_file';
+import { reviewable } from '../utils/file_reviews';
+
+import { DIFF_FILE_AUTOMATIC_COLLAPSE } from '../constants';
+
import { DIFF_FILE_HEADER } from '../i18n';
+import DiffStats from './diff_stats.vue';
export default {
components: {
@@ -33,12 +40,14 @@ export default {
GlDropdown,
GlDropdownItem,
GlDropdownDivider,
+ GlFormCheckbox,
GlLoadingIcon,
},
directives: {
GlTooltip: GlTooltipDirective,
SafeHtml: GlSafeHtmlDirective,
},
+ mixins: [glFeatureFlagsMixin()],
i18n: {
...DIFF_FILE_HEADER,
},
@@ -76,6 +85,16 @@ export default {
required: false,
default: false,
},
+ showLocalFileReviews: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ reviewed: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
},
data() {
return {
@@ -83,6 +102,7 @@ export default {
};
},
computed: {
+ ...mapState('diffs', ['latestDiff']),
...mapGetters('diffs', ['diffHasExpandedDiscussions', 'diffHasDiscussions']),
diffContentIDSelector() {
return `#diff-content-${this.diffFile.file_hash}`;
@@ -170,6 +190,9 @@ export default {
(this.diffFile.edit_path || this.diffFile.ide_edit_path)
);
},
+ isReviewable() {
+ return reviewable(this.diffFile);
+ },
},
methods: {
...mapActions('diffs', [
@@ -177,6 +200,8 @@ export default {
'toggleFileDiscussionWrappers',
'toggleFullDiff',
'toggleActiveFileByHash',
+ 'reviewFile',
+ 'setFileCollapsedByUser',
]),
handleToggleFile() {
this.$emit('toggleFile');
@@ -204,6 +229,26 @@ export default {
setMoreActionsShown(val) {
this.moreActionsShown = val;
},
+ toggleReview(newReviewedStatus) {
+ const autoCollapsed =
+ this.isCollapsed && collapsedType(this.diffFile) === DIFF_FILE_AUTOMATIC_COLLAPSE;
+ const open = this.expanded;
+ const closed = !open;
+ const reviewed = newReviewedStatus;
+
+ this.reviewFile({ file: this.diffFile, reviewed });
+
+ if (reviewed && autoCollapsed) {
+ this.setFileCollapsedByUser({
+ filePath: this.diffFile.file_path,
+ collapsed: true,
+ });
+ }
+
+ if ((open && reviewed) || (closed && !reviewed)) {
+ this.$emit('toggleFile');
+ }
+ },
},
};
</script>
@@ -213,6 +258,8 @@ export default {
ref="header"
:class="{ 'gl-z-dropdown-menu!': moreActionsShown }"
class="js-file-title file-title file-title-flex-parent"
+ data-qa-selector="file_title_container"
+ :data-qa-file-name="filePath"
@click.self="handleToggleFile"
>
<div class="file-header-content">
@@ -289,6 +336,19 @@ export default {
class="file-actions d-flex align-items-center gl-ml-auto gl-align-self-start"
>
<diff-stats :added-lines="diffFile.added_lines" :removed-lines="diffFile.removed_lines" />
+ <gl-form-checkbox
+ v-if="isReviewable && showLocalFileReviews"
+ v-gl-tooltip.hover
+ data-testid="fileReviewCheckbox"
+ class="gl-mb-0"
+ :title="$options.i18n.fileReviewTooltip"
+ :checked="reviewed"
+ @change="toggleReview"
+ >
+ <span class="gl-line-height-20">
+ {{ $options.i18n.fileReviewLabel }}
+ </span>
+ </gl-form-checkbox>
<gl-button-group class="gl-pt-0!">
<gl-button
v-if="diffFile.external_url"
@@ -307,6 +367,7 @@ export default {
right
toggle-class="btn-icon js-diff-more-actions"
class="gl-pt-0!"
+ data-qa-selector="dropdown_button"
@show="setMoreActionsShown(true)"
@hidden="setMoreActionsShown(false)"
>
@@ -340,6 +401,7 @@ export default {
ref="ideEditButton"
:href="diffFile.ide_edit_path"
class="js-ide-edit-blob"
+ data-qa-selector="edit_in_ide_button"
>
{{ __('Edit in Web IDE') }}
</gl-dropdown-item>
diff --git a/app/assets/javascripts/diffs/components/diff_line_note_form.vue b/app/assets/javascripts/diffs/components/diff_line_note_form.vue
index 463b7f5cff4..e4c64bb0e07 100644
--- a/app/assets/javascripts/diffs/components/diff_line_note_form.vue
+++ b/app/assets/javascripts/diffs/components/diff_line_note_form.vue
@@ -6,7 +6,6 @@ import { s__ } from '~/locale';
import noteForm from '../../notes/components/note_form.vue';
import MultilineCommentForm from '../../notes/components/multiline_comment_form.vue';
import autosave from '../../notes/mixins/autosave';
-import userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue';
import { DIFF_NOTE_TYPE, INLINE_DIFF_LINES_KEY, PARALLEL_DIFF_VIEW_TYPE } from '../constants';
import {
commentLineOptions,
@@ -16,7 +15,6 @@ import {
export default {
components: {
noteForm,
- userAvatarLink,
MultilineCommentForm,
},
mixins: [autosave, diffLineNoteFormMixin, glFeatureFlagsMixin()],
@@ -174,14 +172,6 @@ export default {
:comment-line-options="commentLineOptions"
/>
</div>
- <user-avatar-link
- v-if="author"
- :link-href="author.path"
- :img-src="author.avatar_url"
- :img-alt="author.name"
- :img-size="40"
- class="d-none d-sm-block"
- />
<note-form
ref="noteForm"
:is-editing="true"
diff --git a/app/assets/javascripts/diffs/components/diff_row.vue b/app/assets/javascripts/diffs/components/diff_row.vue
index db03da966c3..a8aca6deee6 100644
--- a/app/assets/javascripts/diffs/components/diff_row.vue
+++ b/app/assets/javascripts/diffs/components/diff_row.vue
@@ -1,6 +1,8 @@
<script>
import { mapActions, mapGetters, mapState } from 'vuex';
import { GlTooltipDirective, GlIcon, GlSafeHtmlDirective as SafeHtml } from '@gitlab/ui';
+import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
+import { BV_HIDE_TOOLTIP } from '~/lib/utils/constants';
import {
CONTEXT_LINE_CLASS_NAME,
PARALLEL_DIFF_VIEW_TYPE,
@@ -10,7 +12,6 @@ import {
CONFLICT_THEIR,
CONFLICT_MARKER,
} from '../constants';
-import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import DiffGutterAvatars from './diff_gutter_avatars.vue';
import * as utils from './diff_row_utils';
@@ -99,10 +100,10 @@ export default {
});
},
addCommentTooltipLeft() {
- return utils.addCommentTooltip(this.line.left);
+ return utils.addCommentTooltip(this.line.left, this.glFeatures.dragCommentSelection);
},
addCommentTooltipRight() {
- return utils.addCommentTooltip(this.line.right);
+ return utils.addCommentTooltip(this.line.right, this.glFeatures.dragCommentSelection);
},
emptyCellRightClassMap() {
return { conflict_their: this.line.left?.type === CONFLICT_OUR };
@@ -111,13 +112,7 @@ export default {
return { conflict_our: this.line.right?.type === CONFLICT_THEIR };
},
shouldRenderCommentButton() {
- return (
- this.isLoggedIn &&
- !this.line.isContextLineLeft &&
- !this.line.isMetaLineLeft &&
- !this.line.hasDiscussionsLeft &&
- !this.line.hasDiscussionsRight
- );
+ return this.isLoggedIn && !this.line.isContextLineLeft && !this.line.isMetaLineLeft;
},
isLeftConflictMarker() {
return [CONFLICT_MARKER_OUR, CONFLICT_MARKER_THEIR].includes(this.line.left?.type);
@@ -168,7 +163,7 @@ export default {
this.$emit('enterdragging', { ...line, index });
},
onDragStart(line) {
- this.$root.$emit('bv::hide::tooltip');
+ this.$root.$emit(BV_HIDE_TOOLTIP);
this.dragging = true;
this.$emit('startdragging', line);
},
@@ -199,7 +194,7 @@ export default {
>
<template v-if="!isLeftConflictMarker">
<span
- v-if="shouldRenderCommentButton"
+ v-if="shouldRenderCommentButton && !line.hasDiscussionsLeft"
v-gl-tooltip
data-testid="leftCommentButton"
class="add-diff-note tooltip-wrapper"
@@ -301,7 +296,7 @@ export default {
<div :class="classNameMapCellRight" class="diff-td diff-line-num new_line">
<template v-if="line.right.type !== $options.CONFLICT_MARKER_THEIR">
<span
- v-if="shouldRenderCommentButton"
+ v-if="shouldRenderCommentButton && !line.hasDiscussionsRight"
v-gl-tooltip
data-testid="rightCommentButton"
class="add-diff-note tooltip-wrapper"
diff --git a/app/assets/javascripts/diffs/components/diff_row_utils.js b/app/assets/javascripts/diffs/components/diff_row_utils.js
index 7606c39ad37..cd45474afcd 100644
--- a/app/assets/javascripts/diffs/components/diff_row_utils.js
+++ b/app/assets/javascripts/diffs/components/diff_row_utils.js
@@ -50,11 +50,11 @@ export const classNameMapCell = ({ line, hll, isLoggedIn, isHover }) => {
];
};
-export const addCommentTooltip = (line) => {
+export const addCommentTooltip = (line, dragCommentSelectionEnabled = false) => {
let tooltip;
if (!line) return tooltip;
- tooltip = gon.drag_comment_selection
+ tooltip = dragCommentSelectionEnabled
? __('Add a comment to this line or drag for multiple lines')
: __('Add a comment to this line');
const brokenSymlinks = line.commentsDisabled;
diff --git a/app/assets/javascripts/diffs/components/diff_view.vue b/app/assets/javascripts/diffs/components/diff_view.vue
index 79800f835f4..b710e588360 100644
--- a/app/assets/javascripts/diffs/components/diff_view.vue
+++ b/app/assets/javascripts/diffs/components/diff_view.vue
@@ -2,10 +2,10 @@
import { mapGetters, mapState, mapActions } from 'vuex';
import draftCommentsMixin from '~/diffs/mixins/draft_comments';
import DraftNote from '~/batch_comments/components/draft_note.vue';
+import { getCommentedLines } from '~/notes/components/multiline_comment_utils';
import DiffRow from './diff_row.vue';
import DiffCommentCell from './diff_comment_cell.vue';
import DiffExpansionCell from './diff_expansion_cell.vue';
-import { getCommentedLines } from '~/notes/components/multiline_comment_utils';
export default {
components: {
@@ -61,10 +61,10 @@ export default {
...mapActions(['setSelectedCommentPosition']),
...mapActions('diffs', ['showCommentForm']),
showCommentLeft(line) {
- return !this.inline || line.left;
+ return line.left && !line.right;
},
showCommentRight(line) {
- return !this.inline || (line.right && !line.left);
+ return line.right && !line.left;
},
onStartDragging(line) {
this.dragStart = line;
@@ -138,24 +138,30 @@ export default {
:class="line.commentRowClasses"
class="diff-grid-comments diff-tr notes_holder"
>
- <div v-if="showCommentLeft(line)" class="diff-td notes-content parallel old">
+ <div
+ v-if="line.left || !inline"
+ :class="{ parallel: !inline }"
+ class="diff-td notes-content old"
+ >
<diff-comment-cell
- v-if="line.left"
+ v-if="line.left && (line.left.renderDiscussion || line.left.hasCommentForm)"
:line="line.left"
:diff-file-hash="diffFile.file_hash"
:help-page-path="helpPagePath"
- :has-draft="line.left.hasDraft"
line-position="left"
/>
</div>
- <div v-if="showCommentRight(line)" class="diff-td notes-content parallel new">
+ <div
+ v-if="line.right || !inline"
+ :class="{ parallel: !inline }"
+ class="diff-td notes-content new"
+ >
<diff-comment-cell
- v-if="line.right"
+ v-if="line.right && (line.right.renderDiscussion || line.right.hasCommentForm)"
:line="line.right"
:diff-file-hash="diffFile.file_hash"
:line-index="index"
:help-page-path="helpPagePath"
- :has-draft="line.right.hasDraft"
line-position="right"
/>
</div>
diff --git a/app/assets/javascripts/diffs/components/image_diff_overlay.vue b/app/assets/javascripts/diffs/components/image_diff_overlay.vue
index 6a1e0d8cbd6..54b491faa2a 100644
--- a/app/assets/javascripts/diffs/components/image_diff_overlay.vue
+++ b/app/assets/javascripts/diffs/components/image_diff_overlay.vue
@@ -1,8 +1,8 @@
<script>
import { mapActions, mapGetters } from 'vuex';
import { isArray } from 'lodash';
-import imageDiffMixin from 'ee_else_ce/diffs/mixins/image_diff';
import { GlIcon } from '@gitlab/ui';
+import imageDiffMixin from 'ee_else_ce/diffs/mixins/image_diff';
function calcPercent(pos, size, renderedSize) {
return (((pos / size) * 100) / ((renderedSize / size) * 100)) * 100;
diff --git a/app/assets/javascripts/diffs/components/inline_diff_view.vue b/app/assets/javascripts/diffs/components/inline_diff_view.vue
index 28485a2fdac..fd4e2004857 100644
--- a/app/assets/javascripts/diffs/components/inline_diff_view.vue
+++ b/app/assets/javascripts/diffs/components/inline_diff_view.vue
@@ -3,10 +3,10 @@ import { mapGetters, mapState } from 'vuex';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import draftCommentsMixin from '~/diffs/mixins/draft_comments';
import DraftNote from '~/batch_comments/components/draft_note.vue';
+import { getCommentedLines } from '~/notes/components/multiline_comment_utils';
import inlineDiffTableRow from './inline_diff_table_row.vue';
import DiffCommentCell from './diff_comment_cell.vue';
import DiffExpansionCell from './diff_expansion_cell.vue';
-import { getCommentedLines } from '~/notes/components/multiline_comment_utils';
export default {
components: {
diff --git a/app/assets/javascripts/diffs/components/parallel_diff_view.vue b/app/assets/javascripts/diffs/components/parallel_diff_view.vue
index 21e0bf18dbf..21a5eece125 100644
--- a/app/assets/javascripts/diffs/components/parallel_diff_view.vue
+++ b/app/assets/javascripts/diffs/components/parallel_diff_view.vue
@@ -2,10 +2,10 @@
import { mapGetters, mapState } from 'vuex';
import draftCommentsMixin from '~/diffs/mixins/draft_comments';
import DraftNote from '~/batch_comments/components/draft_note.vue';
+import { getCommentedLines } from '~/notes/components/multiline_comment_utils';
import parallelDiffTableRow from './parallel_diff_table_row.vue';
import DiffCommentCell from './diff_comment_cell.vue';
import DiffExpansionCell from './diff_expansion_cell.vue';
-import { getCommentedLines } from '~/notes/components/multiline_comment_utils';
export default {
components: {
diff --git a/app/assets/javascripts/diffs/i18n.js b/app/assets/javascripts/diffs/i18n.js
index c4ac99ead91..2a061876937 100644
--- a/app/assets/javascripts/diffs/i18n.js
+++ b/app/assets/javascripts/diffs/i18n.js
@@ -1,13 +1,16 @@
-import { __ } from '~/locale';
+import { __, s__ } from '~/locale';
export const GENERIC_ERROR = __('Something went wrong on our end. Please try again!');
export const DIFF_FILE_HEADER = {
optionsDropdownTitle: __('Options'),
+ fileReviewLabel: __('Viewed'),
+ fileReviewTooltip: __('Collapses this file (only for you) until it’s changed again.'),
};
export const DIFF_FILE = {
- blobView: __('You can %{linkStart}view the blob%{linkEnd} instead.'),
+ tooLarge: s__('MRDiffFile|Changes are too large to be shown.'),
+ blobView: s__('MRDiffFile|View file @ %{commitSha}'),
editInFork: __(
"You're not allowed to %{tag_start}edit%{tag_end} files in this project directly. Please fork this project, make your changes there, and submit a merge request.",
),
diff --git a/app/assets/javascripts/diffs/store/actions.js b/app/assets/javascripts/diffs/store/actions.js
index e95e9ac3ee4..32b4b5c57b4 100644
--- a/app/assets/javascripts/diffs/store/actions.js
+++ b/app/assets/javascripts/diffs/store/actions.js
@@ -7,20 +7,11 @@ import { deprecatedCreateFlash as createFlash } from '~/flash';
import { __, s__ } from '~/locale';
import { handleLocationHash, historyPushState, scrollToElement } from '~/lib/utils/common_utils';
import { mergeUrlParams, getLocationHash } from '~/lib/utils/url_utility';
+import { diffViewerModes } from '~/ide/constants';
import TreeWorker from '../workers/tree_worker';
import notesEventHub from '../../notes/event_hub';
import eventHub from '../event_hub';
import {
- getDiffPositionByLineCode,
- getNoteFormData,
- convertExpandLines,
- idleCallback,
- allDiscussionWrappersExpanded,
- prepareDiffData,
- prepareLineForRenamedFile,
-} from './utils';
-import * as types from './mutation_types';
-import {
PARALLEL_DIFF_VIEW_TYPE,
INLINE_DIFF_VIEW_TYPE,
DIFF_VIEW_COOKIE_NAME,
@@ -48,10 +39,19 @@ import {
DIFF_VIEW_ALL_FILES,
DIFF_FILE_BY_FILE_COOKIE_NAME,
} from '../constants';
-import { diffViewerModes } from '~/ide/constants';
import { isCollapsed } from '../utils/diff_file';
import { getDerivedMergeRequestInformation } from '../utils/merge_request';
import { markFileReview, setReviewsForMergeRequest } from '../utils/file_reviews';
+import * as types from './mutation_types';
+import {
+ getDiffPositionByLineCode,
+ getNoteFormData,
+ convertExpandLines,
+ idleCallback,
+ allDiscussionWrappersExpanded,
+ prepareDiffData,
+ prepareLineForRenamedFile,
+} from './utils';
export const setBaseConfig = ({ commit }, options) => {
const {
@@ -749,12 +749,10 @@ export const setFileByFile = ({ commit }, { fileByFile }) => {
);
};
-export function reviewFile({ commit, state, getters }, { file, reviewed = true }) {
+export function reviewFile({ commit, state }, { file, reviewed = true }) {
const { mrPath } = getDerivedMergeRequestInformation({ endpoint: file.load_collapsed_diff_url });
- const reviews = setReviewsForMergeRequest(
- mrPath,
- markFileReview(getters.fileReviews(state), file, reviewed),
- );
+ const reviews = markFileReview(state.mrReviews, file, reviewed);
+ setReviewsForMergeRequest(mrPath, reviews);
commit(types.SET_MR_FILE_REVIEWS, reviews);
}
diff --git a/app/assets/javascripts/diffs/store/getters.js b/app/assets/javascripts/diffs/store/getters.js
index a167b6d4694..149afc01056 100644
--- a/app/assets/javascripts/diffs/store/getters.js
+++ b/app/assets/javascripts/diffs/store/getters.js
@@ -1,11 +1,10 @@
import { __, n__ } from '~/locale';
-import { parallelizeDiffLines } from './utils';
-import { isFileReviewed } from '../utils/file_reviews';
import {
PARALLEL_DIFF_VIEW_TYPE,
INLINE_DIFF_VIEW_TYPE,
INLINE_DIFF_LINES_KEY,
} from '../constants';
+import { parallelizeDiffLines } from './utils';
export * from './getters_versions_dropdowns';
@@ -155,7 +154,3 @@ export const diffLines = (state) => (file, unifiedDiffComponents) => {
state.diffViewType === INLINE_DIFF_VIEW_TYPE,
);
};
-
-export function fileReviews(state) {
- return state.diffFiles.map((file) => isFileReviewed(state.mrReviews, file));
-}
diff --git a/app/assets/javascripts/diffs/store/modules/diff_state.js b/app/assets/javascripts/diffs/store/modules/diff_state.js
index aa89c74cef0..f93435363ec 100644
--- a/app/assets/javascripts/diffs/store/modules/diff_state.js
+++ b/app/assets/javascripts/diffs/store/modules/diff_state.js
@@ -47,4 +47,5 @@ export default () => ({
showSuggestPopover: true,
defaultSuggestionCommitMessage: '',
mrReviews: {},
+ latestDiff: true,
});
diff --git a/app/assets/javascripts/diffs/store/mutations.js b/app/assets/javascripts/diffs/store/mutations.js
index 06f0f2c3dfb..9db29e8491e 100644
--- a/app/assets/javascripts/diffs/store/mutations.js
+++ b/app/assets/javascripts/diffs/store/mutations.js
@@ -1,6 +1,11 @@
import Vue from 'vue';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import {
+ DIFF_FILE_MANUAL_COLLAPSE,
+ DIFF_FILE_AUTOMATIC_COLLAPSE,
+ INLINE_DIFF_LINES_KEY,
+} from '../constants';
+import {
findDiffFile,
addLineReferences,
removeMatchLine,
@@ -9,11 +14,6 @@ import {
isDiscussionApplicableToLine,
updateLineInFile,
} from './utils';
-import {
- DIFF_FILE_MANUAL_COLLAPSE,
- DIFF_FILE_AUTOMATIC_COLLAPSE,
- INLINE_DIFF_LINES_KEY,
-} from '../constants';
import * as types from './mutation_types';
function updateDiffFilesInState(state, files) {
@@ -159,7 +159,12 @@ export default {
[types.SET_LINE_DISCUSSIONS_FOR_FILE](state, { discussion, diffPositionByLineCode, hash }) {
const { latestDiff } = state;
- const discussionLineCodes = [discussion.line_code, ...(discussion.line_codes || [])];
+ const originalStartLineCode = discussion.original_position?.line_range?.start?.line_code;
+ const discussionLineCodes = [
+ discussion.line_code,
+ originalStartLineCode,
+ ...(discussion.line_codes || []),
+ ];
const fileHash = discussion.diff_file.file_hash;
const lineCheck = (line) =>
discussionLineCodes.some(
diff --git a/app/assets/javascripts/diffs/utils/diff_file.js b/app/assets/javascripts/diffs/utils/diff_file.js
index ce0398e75fc..7e6fde320d2 100644
--- a/app/assets/javascripts/diffs/utils/diff_file.js
+++ b/app/assets/javascripts/diffs/utils/diff_file.js
@@ -1,3 +1,5 @@
+import { truncateSha } from '~/lib/utils/text_utility';
+
import {
DIFF_FILE_SYMLINK_MODE,
DIFF_FILE_DELETED_MODE,
@@ -78,3 +80,7 @@ export function isCollapsed(file) {
return collapsedStates[type];
}
+
+export function getShortShaFromFile(file) {
+ return file.content_sha ? truncateSha(String(file.content_sha)) : null;
+}
diff --git a/app/assets/javascripts/diffs/utils/file_reviews.js b/app/assets/javascripts/diffs/utils/file_reviews.js
index 0047955643a..5fafc1714ae 100644
--- a/app/assets/javascripts/diffs/utils/file_reviews.js
+++ b/app/assets/javascripts/diffs/utils/file_reviews.js
@@ -2,6 +2,16 @@ function getFileReviewsKey(mrPath) {
return `${mrPath}-file-reviews`;
}
+export function isFileReviewed(reviews, file) {
+ const fileReviews = reviews[file.file_identifier_hash];
+
+ return file?.id && fileReviews?.length ? new Set(fileReviews).has(file.id) : false;
+}
+
+export function reviewStatuses(files, reviews) {
+ return files.map((file) => isFileReviewed(reviews, file));
+}
+
export function getReviewsForMergeRequest(mrPath) {
const reviewsForMr = localStorage.getItem(getFileReviewsKey(mrPath));
let reviews = {};
@@ -23,23 +33,17 @@ export function setReviewsForMergeRequest(mrPath, reviews) {
return reviews;
}
-export function isFileReviewed(reviews, file) {
- const fileReviews = reviews[file.file_identifier_hash];
-
- return file?.id && fileReviews?.length ? new Set(fileReviews).has(file.id) : false;
-}
-
export function reviewable(file) {
return Boolean(file.id) && Boolean(file.file_identifier_hash);
}
export function markFileReview(reviews, file, reviewed = true) {
const usableReviews = { ...(reviews || {}) };
- let updatedReviews = usableReviews;
+ const updatedReviews = usableReviews;
let fileReviews;
if (reviewable(file)) {
- fileReviews = new Set([...(usableReviews[file.file_identifier_hash] || [])]);
+ fileReviews = new Set(usableReviews[file.file_identifier_hash] || []);
if (reviewed) {
fileReviews.add(file.id);
@@ -47,10 +51,7 @@ export function markFileReview(reviews, file, reviewed = true) {
fileReviews.delete(file.id);
}
- updatedReviews = {
- ...usableReviews,
- [file.file_identifier_hash]: Array.from(fileReviews),
- };
+ updatedReviews[file.file_identifier_hash] = Array.from(fileReviews);
if (updatedReviews[file.file_identifier_hash].length === 0) {
delete updatedReviews[file.file_identifier_hash];
diff --git a/app/assets/javascripts/dropzone_input.js b/app/assets/javascripts/dropzone_input.js
index d7aacfbce60..13db9400777 100644
--- a/app/assets/javascripts/dropzone_input.js
+++ b/app/assets/javascripts/dropzone_input.js
@@ -2,12 +2,12 @@ import $ from 'jquery';
import Dropzone from 'dropzone';
import { escape } from 'lodash';
import './behaviors/preview_markdown';
-import PasteMarkdownTable from './behaviors/markdown/paste_markdown_table';
-import csrf from './lib/utils/csrf';
-import axios from './lib/utils/axios_utils';
import { n__, __ } from '~/locale';
import { getFilename } from '~/lib/utils/file_upload';
import { spriteIcon } from '~/lib/utils/common_utils';
+import PasteMarkdownTable from './behaviors/markdown/paste_markdown_table';
+import csrf from './lib/utils/csrf';
+import axios from './lib/utils/axios_utils';
Dropzone.autoDiscover = false;
diff --git a/app/assets/javascripts/due_date_select.js b/app/assets/javascripts/due_date_select.js
index c311e1b561c..72ffbfd62ce 100644
--- a/app/assets/javascripts/due_date_select.js
+++ b/app/assets/javascripts/due_date_select.js
@@ -3,10 +3,10 @@ import $ from 'jquery';
import Pikaday from 'pikaday';
import dateFormat from 'dateformat';
import { __ } from '~/locale';
+import initDeprecatedJQueryDropdown from '~/deprecated_jquery_dropdown';
import axios from './lib/utils/axios_utils';
import { timeFor, parsePikadayDate, pikadayToString } from './lib/utils/datetime_utility';
import boardsStore from './boards/stores/boards_store';
-import initDeprecatedJQueryDropdown from '~/deprecated_jquery_dropdown';
class DueDateSelect {
constructor({ $dropdown, $loading } = {}) {
diff --git a/app/assets/javascripts/editor/constants.js b/app/assets/javascripts/editor/constants.js
index 8d1a3d17c6e..d9e6a6c13e2 100644
--- a/app/assets/javascripts/editor/constants.js
+++ b/app/assets/javascripts/editor/constants.js
@@ -11,6 +11,11 @@ export const ERROR_INSTANCE_REQUIRED_FOR_EXTENSION = __(
'Editor Lite instance is required to set up an extension.',
);
+export const EDITOR_READY_EVENT = 'editor-ready';
+
+export const EDITOR_TYPE_CODE = 'vs.editor.ICodeEditor';
+export const EDITOR_TYPE_DIFF = 'vs.editor.IDiffEditor';
+
//
// EXTENSIONS' CONSTANTS
//
diff --git a/app/assets/javascripts/editor/editor_lite.js b/app/assets/javascripts/editor/editor_lite.js
index 1808f968b8c..9403783c991 100644
--- a/app/assets/javascripts/editor/editor_lite.js
+++ b/app/assets/javascripts/editor/editor_lite.js
@@ -4,9 +4,14 @@ import languages from '~/ide/lib/languages';
import { defaultEditorOptions } from '~/ide/lib/editor_options';
import { registerLanguages } from '~/ide/utils';
import { joinPaths } from '~/lib/utils/url_utility';
-import { clearDomElement } from './utils';
-import { EDITOR_LITE_INSTANCE_ERROR_NO_EL, URI_PREFIX } from './constants';
import { uuids } from '~/diffs/utils/uuids';
+import { clearDomElement } from './utils';
+import {
+ EDITOR_LITE_INSTANCE_ERROR_NO_EL,
+ URI_PREFIX,
+ EDITOR_READY_EVENT,
+ EDITOR_TYPE_DIFF,
+} from './constants';
export default class EditorLite {
constructor(options = {}) {
@@ -29,15 +34,12 @@ export default class EditorLite {
monacoEditor.setTheme(theme ? themeName : DEFAULT_THEME);
}
- static updateModelLanguage(path, instance) {
- if (!instance) return;
- const model = instance.getModel();
+ static getModelLanguage(path) {
const ext = `.${path.split('.').pop()}`;
const language = monacoLanguages
.getLanguages()
.find((lang) => lang.extensions.indexOf(ext) !== -1);
- const id = language ? language.id : 'plaintext';
- monacoEditor.setModelLanguage(model, id);
+ return language ? language.id : 'plaintext';
}
static pushToImportsArray(arr, toImport) {
@@ -73,50 +75,19 @@ export default class EditorLite {
});
}
- /**
- * Creates a monaco instance with the given options.
- *
- * @param {Object} options Options used to initialize monaco.
- * @param {Element} options.el The element which will be used to create the monacoEditor.
- * @param {string} options.blobPath The path used as the URI of the model. Monaco uses the extension of this path to determine the language.
- * @param {string} options.blobContent The content to initialize the monacoEditor.
- * @param {string} options.blobGlobalId This is used to help globally identify monaco instances that are created with the same blobPath.
- */
- createInstance({
- el = undefined,
- blobPath = '',
- blobContent = '',
- blobGlobalId = uuids()[0],
- extensions = [],
- ...instanceOptions
- } = {}) {
+ static prepareInstance(el) {
if (!el) {
throw new Error(EDITOR_LITE_INSTANCE_ERROR_NO_EL);
}
clearDomElement(el);
- const uriFilePath = joinPaths(URI_PREFIX, blobGlobalId, blobPath);
-
- const model = monacoEditor.createModel(blobContent, undefined, Uri.file(uriFilePath));
-
monacoEditor.onDidCreateEditor(() => {
delete el.dataset.editorLoading;
});
+ }
- const instance = monacoEditor.create(el, {
- ...this.options,
- ...instanceOptions,
- });
- instance.setModel(model);
- instance.onDidDispose(() => {
- const index = this.instances.findIndex((inst) => inst === instance);
- this.instances.splice(index, 1);
- model.dispose();
- });
- instance.updateModelLanguage = (path) => EditorLite.updateModelLanguage(path, instance);
- instance.use = (args) => this.use(args, instance);
-
+ static manageDefaultExtensions(instance, el, extensions) {
EditorLite.loadExtensions(extensions, instance)
.then((modules) => {
if (modules) {
@@ -126,33 +97,167 @@ export default class EditorLite {
}
})
.then(() => {
- el.dispatchEvent(new Event('editor-ready'));
+ el.dispatchEvent(new Event(EDITOR_READY_EVENT));
})
.catch((e) => {
throw e;
});
+ }
+
+ static createEditorModel({
+ blobPath,
+ blobContent,
+ blobOriginalContent,
+ blobGlobalId,
+ instance,
+ isDiff,
+ } = {}) {
+ if (!instance) {
+ return null;
+ }
+ const uriFilePath = joinPaths(URI_PREFIX, blobGlobalId, blobPath);
+ const uri = Uri.file(uriFilePath);
+ const existingModel = monacoEditor.getModel(uri);
+ const model = existingModel || monacoEditor.createModel(blobContent, undefined, uri);
+ if (!isDiff) {
+ instance.setModel(model);
+ return model;
+ }
+ const diffModel = {
+ original: monacoEditor.createModel(
+ blobOriginalContent,
+ EditorLite.getModelLanguage(model.uri.path),
+ ),
+ modified: model,
+ };
+ instance.setModel(diffModel);
+ return diffModel;
+ }
+
+ static convertMonacoToELInstance = (inst) => {
+ const editorLiteInstanceAPI = {
+ updateModelLanguage: (path) => {
+ return EditorLite.instanceUpdateLanguage(inst, path);
+ },
+ use: (exts = []) => {
+ return EditorLite.instanceApplyExtension(inst, exts);
+ },
+ };
+ const handler = {
+ get(target, prop, receiver) {
+ if (Reflect.has(editorLiteInstanceAPI, prop)) {
+ return editorLiteInstanceAPI[prop];
+ }
+ return Reflect.get(target, prop, receiver);
+ },
+ };
+ return new Proxy(inst, handler);
+ };
+
+ static instanceUpdateLanguage(inst, path) {
+ const lang = EditorLite.getModelLanguage(path);
+ const model = inst.getModel();
+ return monacoEditor.setModelLanguage(model, lang);
+ }
+
+ static instanceApplyExtension(inst, exts = []) {
+ const extensions = [].concat(exts);
+ extensions.forEach((extension) => {
+ EditorLite.mixIntoInstance(extension, inst);
+ });
+ return inst;
+ }
+
+ static instanceRemoveFromRegistry(editor, instance) {
+ const index = editor.instances.findIndex((inst) => inst === instance);
+ editor.instances.splice(index, 1);
+ }
+
+ static instanceDisposeModels(editor, instance, model) {
+ const instanceModel = instance.getModel() || model;
+ if (!instanceModel) {
+ return;
+ }
+ if (instance.getEditorType() === EDITOR_TYPE_DIFF) {
+ const { original, modified } = instanceModel;
+ if (original) {
+ original.dispose();
+ }
+ if (modified) {
+ modified.dispose();
+ }
+ } else {
+ instanceModel.dispose();
+ }
+ }
+
+ /**
+ * Creates a monaco instance with the given options.
+ *
+ * @param {Object} options Options used to initialize monaco.
+ * @param {Element} options.el The element which will be used to create the monacoEditor.
+ * @param {string} options.blobPath The path used as the URI of the model. Monaco uses the extension of this path to determine the language.
+ * @param {string} options.blobContent The content to initialize the monacoEditor.
+ * @param {string} options.blobGlobalId This is used to help globally identify monaco instances that are created with the same blobPath.
+ */
+ createInstance({
+ el = undefined,
+ blobPath = '',
+ blobContent = '',
+ blobOriginalContent = '',
+ blobGlobalId = uuids()[0],
+ extensions = [],
+ isDiff = false,
+ ...instanceOptions
+ } = {}) {
+ EditorLite.prepareInstance(el);
+
+ const createEditorFn = isDiff ? 'createDiffEditor' : 'create';
+ const instance = EditorLite.convertMonacoToELInstance(
+ monacoEditor[createEditorFn].call(this, el, {
+ ...this.options,
+ ...instanceOptions,
+ }),
+ );
+
+ let model;
+ if (instanceOptions.model !== null) {
+ model = EditorLite.createEditorModel({
+ blobGlobalId,
+ blobOriginalContent,
+ blobPath,
+ blobContent,
+ instance,
+ isDiff,
+ });
+ }
+
+ instance.onDidDispose(() => {
+ EditorLite.instanceRemoveFromRegistry(this, instance);
+ EditorLite.instanceDisposeModels(this, instance, model);
+ });
+
+ EditorLite.manageDefaultExtensions(instance, el, extensions);
this.instances.push(instance);
return instance;
}
+ createDiffInstance(args) {
+ return this.createInstance({
+ ...args,
+ isDiff: true,
+ });
+ }
+
dispose() {
this.instances.forEach((instance) => instance.dispose());
}
- use(exts = [], instance = null) {
- const extensions = Array.isArray(exts) ? exts : [exts];
- const initExtensions = (inst) => {
- extensions.forEach((extension) => {
- EditorLite.mixIntoInstance(extension, inst);
- });
- };
- if (instance) {
- initExtensions(instance);
- } else {
- this.instances.forEach((inst) => {
- initExtensions(inst);
- });
- }
+ use(exts) {
+ this.instances.forEach((inst) => {
+ inst.use(exts);
+ });
+ return this;
}
}
diff --git a/app/assets/javascripts/editor/extensions/editor_ci_schema_ext.js b/app/assets/javascripts/editor/extensions/editor_ci_schema_ext.js
index eb47c20912e..ae6c89d3942 100644
--- a/app/assets/javascripts/editor/extensions/editor_ci_schema_ext.js
+++ b/app/assets/javascripts/editor/extensions/editor_ci_schema_ext.js
@@ -1,7 +1,7 @@
import Api from '~/api';
import { registerSchema } from '~/ide/utils';
-import { EditorLiteExtension } from './editor_lite_extension_base';
import { EXTENSION_CI_SCHEMA_FILE_NAME_MATCH } from '../constants';
+import { EditorLiteExtension } from './editor_lite_extension_base';
export class CiSchemaExtension extends EditorLiteExtension {
/**
diff --git a/app/assets/javascripts/environments/components/container.vue b/app/assets/javascripts/environments/components/container.vue
index c6b34fecbb7..9e058af56c4 100644
--- a/app/assets/javascripts/environments/components/container.vue
+++ b/app/assets/javascripts/environments/components/container.vue
@@ -26,26 +26,6 @@ export default {
type: Boolean,
required: true,
},
- deployBoardsHelpPath: {
- type: String,
- required: false,
- default: '',
- },
- helpCanaryDeploymentsPath: {
- type: String,
- required: false,
- default: '',
- },
- lockPromotionSvgPath: {
- type: String,
- required: false,
- default: '',
- },
- userCalloutsPath: {
- type: String,
- required: false,
- default: '',
- },
},
methods: {
onChangePage(page) {
@@ -62,14 +42,7 @@ export default {
<slot name="empty-state"></slot>
<div v-if="!isLoading && environments.length > 0" class="table-holder">
- <environment-table
- :environments="environments"
- :can-read-environment="canReadEnvironment"
- :user-callouts-path="userCalloutsPath"
- :lock-promotion-svg-path="lockPromotionSvgPath"
- :help-canary-deployments-path="helpCanaryDeploymentsPath"
- :deploy-boards-help-path="deployBoardsHelpPath"
- />
+ <environment-table :environments="environments" :can-read-environment="canReadEnvironment" />
<table-pagination
v-if="pagination && pagination.totalPages > 1"
diff --git a/app/assets/javascripts/environments/components/deploy_board.vue b/app/assets/javascripts/environments/components/deploy_board.vue
index 07cb968d8d3..9249ea53a84 100644
--- a/app/assets/javascripts/environments/components/deploy_board.vue
+++ b/app/assets/javascripts/environments/components/deploy_board.vue
@@ -44,11 +44,6 @@ export default {
type: Object,
required: true,
},
- deployBoardsHelpPath: {
- type: String,
- required: false,
- default: '',
- },
isLoading: {
type: Boolean,
required: true,
diff --git a/app/assets/javascripts/environments/components/environment_delete.vue b/app/assets/javascripts/environments/components/environment_delete.vue
index 75d92d3295d..0dea04f7c7d 100644
--- a/app/assets/javascripts/environments/components/environment_delete.vue
+++ b/app/assets/javascripts/environments/components/environment_delete.vue
@@ -6,6 +6,7 @@
import { GlTooltipDirective, GlButton, GlModalDirective } from '@gitlab/ui';
import { s__ } from '~/locale';
+import { BV_HIDE_TOOLTIP } from '~/lib/utils/constants';
import eventHub from '../event_hub';
export default {
@@ -40,7 +41,7 @@ export default {
},
methods: {
onClick() {
- this.$root.$emit('bv::hide::tooltip', this.$options.deleteEnvironmentTooltipId);
+ this.$root.$emit(BV_HIDE_TOOLTIP, this.$options.deleteEnvironmentTooltipId);
eventHub.$emit('requestDeleteEnvironment', this.environment);
},
onDeleteEnvironment(environment) {
@@ -59,7 +60,7 @@ export default {
:loading="isLoading"
:title="title"
:aria-label="title"
- class="gl-display-none gl-display-md-block"
+ class="gl-display-none gl-md-display-block"
variant="danger"
category="primary"
icon="remove"
diff --git a/app/assets/javascripts/environments/components/environment_item.vue b/app/assets/javascripts/environments/components/environment_item.vue
index 1724cc692bd..cc55c05b08b 100644
--- a/app/assets/javascripts/environments/components/environment_item.vue
+++ b/app/assets/javascripts/environments/components/environment_item.vue
@@ -549,7 +549,7 @@ export default {
upcomingDeploymentCellClasses() {
return [
this.tableData.upcoming.spacing,
- { 'gl-display-none gl-display-md-block': !this.upcomingDeployment },
+ { 'gl-display-none gl-md-display-block': !this.upcomingDeployment },
];
},
},
diff --git a/app/assets/javascripts/environments/components/environment_monitoring.vue b/app/assets/javascripts/environments/components/environment_monitoring.vue
index 4dc2c0ec1bd..7f70433776d 100644
--- a/app/assets/javascripts/environments/components/environment_monitoring.vue
+++ b/app/assets/javascripts/environments/components/environment_monitoring.vue
@@ -30,7 +30,7 @@ export default {
:href="monitoringUrl"
:title="title"
:aria-label="title"
- class="monitoring-url gl-display-none gl-display-sm-none gl-display-md-block"
+ class="monitoring-url gl-display-none gl-sm-display-none gl-md-display-block"
icon="chart"
rel="noopener noreferrer nofollow"
variant="default"
diff --git a/app/assets/javascripts/environments/components/environment_rollback.vue b/app/assets/javascripts/environments/components/environment_rollback.vue
index 48edde82ce7..397616c654f 100644
--- a/app/assets/javascripts/environments/components/environment_rollback.vue
+++ b/app/assets/javascripts/environments/components/environment_rollback.vue
@@ -68,7 +68,7 @@ export default {
<gl-button
v-gl-tooltip
v-gl-modal.confirm-rollback-modal
- class="gl-display-none gl-display-md-block text-secondary"
+ class="gl-display-none gl-md-display-block text-secondary"
:loading="isLoading"
:title="title"
:icon="isLastDeployment ? 'repeat' : 'redo'"
diff --git a/app/assets/javascripts/environments/components/environment_stop.vue b/app/assets/javascripts/environments/components/environment_stop.vue
index 8e100623199..adb0b05d002 100644
--- a/app/assets/javascripts/environments/components/environment_stop.vue
+++ b/app/assets/javascripts/environments/components/environment_stop.vue
@@ -6,6 +6,7 @@
import { GlTooltipDirective, GlButton, GlModalDirective } from '@gitlab/ui';
import { s__ } from '~/locale';
+import { BV_HIDE_TOOLTIP } from '~/lib/utils/constants';
import eventHub from '../event_hub';
export default {
@@ -40,7 +41,7 @@ export default {
},
methods: {
onClick() {
- this.$root.$emit('bv::hide::tooltip', this.$options.stopEnvironmentTooltipId);
+ this.$root.$emit(BV_HIDE_TOOLTIP, this.$options.stopEnvironmentTooltipId);
eventHub.$emit('requestStopEnvironment', this.environment);
},
onStopEnvironment(environment) {
diff --git a/app/assets/javascripts/environments/components/environments_app.vue b/app/assets/javascripts/environments/components/environments_app.vue
index 6f68c6e864a..1a8a56e892a 100644
--- a/app/assets/javascripts/environments/components/environments_app.vue
+++ b/app/assets/javascripts/environments/components/environments_app.vue
@@ -2,10 +2,10 @@
import { GlBadge, GlButton, GlModalDirective, GlTab, GlTabs } from '@gitlab/ui';
import { deprecatedCreateFlash as Flash } from '~/flash';
import { s__ } from '~/locale';
-import emptyState from './empty_state.vue';
+import CIPaginationMixin from '~/vue_shared/mixins/ci_pagination_api_mixin';
import eventHub from '../event_hub';
import environmentsMixin from '../mixins/environments_mixin';
-import CIPaginationMixin from '~/vue_shared/mixins/ci_pagination_api_mixin';
+import emptyState from './empty_state.vue';
import EnableReviewAppModal from './enable_review_app_modal.vue';
import StopEnvironmentModal from './stop_environment_modal.vue';
import DeleteEnvironmentModal from './delete_environment_modal.vue';
@@ -51,30 +51,10 @@ export default {
type: String,
required: true,
},
- helpCanaryDeploymentsPath: {
- type: String,
- required: false,
- default: '',
- },
helpPagePath: {
type: String,
required: true,
},
- deployBoardsHelpPath: {
- type: String,
- required: false,
- default: '',
- },
- lockPromotionSvgPath: {
- type: String,
- required: false,
- default: '',
- },
- userCalloutsPath: {
- type: String,
- required: false,
- default: '',
- },
},
created() {
@@ -133,7 +113,7 @@ export default {
<confirm-rollback-modal :environment="environmentInRollbackModal" />
<div class="gl-w-full">
- <div class="gl-display-flex gl-flex-direction-column gl-mt-3 gl-display-md-none!">
+ <div class="gl-display-flex gl-flex-direction-column gl-mt-3 gl-md-display-none!">
<gl-button
v-if="state.reviewAppDetails.can_setup_review_app"
v-gl-modal="$options.modal.id"
@@ -167,7 +147,7 @@ export default {
</gl-tab>
<template #tabs-end>
<div
- class="gl-display-none gl-display-md-flex gl-lg-align-items-center gl-lg-flex-direction-row gl-lg-flex-fill-1 gl-lg-justify-content-end gl-lg-mt-0"
+ class="gl-display-none gl-md-display-flex gl-lg-align-items-center gl-lg-flex-direction-row gl-lg-flex-fill-1 gl-lg-justify-content-end gl-lg-mt-0"
>
<gl-button
v-if="state.reviewAppDetails.can_setup_review_app"
@@ -195,10 +175,6 @@ export default {
:environments="state.environments"
:pagination="state.paginationInformation"
:can-read-environment="canReadEnvironment"
- :user-callouts-path="userCalloutsPath"
- :lock-promotion-svg-path="lockPromotionSvgPath"
- :help-canary-deployments-path="helpCanaryDeploymentsPath"
- :deploy-boards-help-path="deployBoardsHelpPath"
@onChangePage="onChangePage"
>
<template v-if="!isLoading && state.environments.length === 0" #empty-state>
diff --git a/app/assets/javascripts/environments/components/environments_table.vue b/app/assets/javascripts/environments/components/environments_table.vue
index bbb56ca6f26..c6a7fe1f57d 100644
--- a/app/assets/javascripts/environments/components/environments_table.vue
+++ b/app/assets/javascripts/environments/components/environments_table.vue
@@ -23,31 +23,11 @@ export default {
required: true,
default: () => [],
},
- deployBoardsHelpPath: {
- type: String,
- required: false,
- default: '',
- },
canReadEnvironment: {
type: Boolean,
required: false,
default: false,
},
- helpCanaryDeploymentsPath: {
- type: String,
- required: false,
- default: '',
- },
- lockPromotionSvgPath: {
- type: String,
- required: false,
- default: '',
- },
- userCalloutsPath: {
- type: String,
- required: false,
- default: '',
- },
},
data() {
return {
@@ -189,7 +169,6 @@ export default {
<div class="deploy-board-container">
<deploy-board
:deploy-board-data="model.deployBoardData"
- :deploy-boards-help-path="deployBoardsHelpPath"
:is-loading="model.isLoadingDeployBoard"
:is-empty="model.isEmptyDeployBoard"
:logs-path="model.logs_path"
diff --git a/app/assets/javascripts/environments/components/stop_environment_modal.vue b/app/assets/javascripts/environments/components/stop_environment_modal.vue
index 0832822520d..828a7098b36 100644
--- a/app/assets/javascripts/environments/components/stop_environment_modal.vue
+++ b/app/assets/javascripts/environments/components/stop_environment_modal.vue
@@ -1,7 +1,7 @@
<script>
import { GlSprintf, GlTooltipDirective, GlModal } from '@gitlab/ui';
-import eventHub from '../event_hub';
import { __, s__ } from '~/locale';
+import eventHub from '../event_hub';
export default {
id: 'stop-environment-modal',
diff --git a/app/assets/javascripts/environments/folder/environments_folder_bundle.js b/app/assets/javascripts/environments/folder/environments_folder_bundle.js
index e4726412f99..1be9a4608cb 100644
--- a/app/assets/javascripts/environments/folder/environments_folder_bundle.js
+++ b/app/assets/javascripts/environments/folder/environments_folder_bundle.js
@@ -1,9 +1,9 @@
import Vue from 'vue';
import VueApollo from 'vue-apollo';
-import environmentsFolderApp from './environments_folder_view.vue';
+import createDefaultClient from '~/lib/graphql';
import { parseBoolean } from '../../lib/utils/common_utils';
import Translate from '../../vue_shared/translate';
-import createDefaultClient from '~/lib/graphql';
+import environmentsFolderApp from './environments_folder_view.vue';
Vue.use(Translate);
Vue.use(VueApollo);
diff --git a/app/assets/javascripts/environments/folder/environments_folder_view.vue b/app/assets/javascripts/environments/folder/environments_folder_view.vue
index dbb60fa4622..d6244cbe4d7 100644
--- a/app/assets/javascripts/environments/folder/environments_folder_view.vue
+++ b/app/assets/javascripts/environments/folder/environments_folder_view.vue
@@ -34,21 +34,6 @@ export default {
type: Boolean,
required: true,
},
- userCalloutsPath: {
- type: String,
- required: false,
- default: '',
- },
- lockPromotionSvgPath: {
- type: String,
- required: false,
- default: '',
- },
- helpCanaryDeploymentsPath: {
- type: String,
- required: false,
- default: '',
- },
},
methods: {
successCallback(resp) {
@@ -88,9 +73,6 @@ export default {
:environments="state.environments"
:pagination="state.paginationInformation"
:can-read-environment="canReadEnvironment"
- :user-callouts-path="userCalloutsPath"
- :lock-promotion-svg-path="lockPromotionSvgPath"
- :help-canary-deployments-path="helpCanaryDeploymentsPath"
@onChangePage="onChangePage"
/>
</div>
diff --git a/app/assets/javascripts/environments/index.js b/app/assets/javascripts/environments/index.js
index 4d734a457ab..68348648e61 100644
--- a/app/assets/javascripts/environments/index.js
+++ b/app/assets/javascripts/environments/index.js
@@ -1,9 +1,9 @@
import Vue from 'vue';
import VueApollo from 'vue-apollo';
-import environmentsComponent from './components/environments_app.vue';
+import createDefaultClient from '~/lib/graphql';
import { parseBoolean } from '../lib/utils/common_utils';
import Translate from '../vue_shared/translate';
-import createDefaultClient from '~/lib/graphql';
+import environmentsComponent from './components/environments_app.vue';
Vue.use(Translate);
Vue.use(VueApollo);
@@ -30,7 +30,6 @@ export default () => {
endpoint: environmentsData.environmentsDataEndpoint,
newEnvironmentPath: environmentsData.newEnvironmentPath,
helpPagePath: environmentsData.helpPagePath,
- deployBoardsHelpPath: environmentsData.deployBoardsHelpPath,
canCreateEnvironment: parseBoolean(environmentsData.canCreateEnvironment),
canReadEnvironment: parseBoolean(environmentsData.canReadEnvironment),
};
@@ -41,7 +40,6 @@ export default () => {
endpoint: this.endpoint,
newEnvironmentPath: this.newEnvironmentPath,
helpPagePath: this.helpPagePath,
- deployBoardsHelpPath: this.deployBoardsHelpPath,
canCreateEnvironment: this.canCreateEnvironment,
canReadEnvironment: this.canReadEnvironment,
},
diff --git a/app/assets/javascripts/environments/stores/environments_store.js b/app/assets/javascripts/environments/stores/environments_store.js
index 8911885e920..f7fdbb03f04 100644
--- a/app/assets/javascripts/environments/stores/environments_store.js
+++ b/app/assets/javascripts/environments/stores/environments_store.js
@@ -1,5 +1,5 @@
-import { setDeployBoard } from './helpers';
import { parseIntPagination, normalizeHeaders } from '~/lib/utils/common_utils';
+import { setDeployBoard } from './helpers';
/**
* Environments Store.
diff --git a/app/assets/javascripts/error_tracking/components/error_details.vue b/app/assets/javascripts/error_tracking/components/error_details.vue
index e21c6b62b91..fca2d5b06e5 100644
--- a/app/assets/javascripts/error_tracking/components/error_details.vue
+++ b/app/assets/javascripts/error_tracking/components/error_details.vue
@@ -16,10 +16,8 @@ import {
import { deprecatedCreateFlash as createFlash } from '~/flash';
import { __, sprintf, n__ } from '~/locale';
import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate.vue';
-import Stacktrace from './stacktrace.vue';
import TrackEventDirective from '~/vue_shared/directives/track_event';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
-import { severityLevel, severityLevelVariant, errorStatus } from './constants';
import Tracking from '~/tracking';
import {
trackClickErrorLinkToSentryOptions,
@@ -28,6 +26,8 @@ import {
} from '../utils';
import query from '../queries/details.query.graphql';
+import { severityLevel, severityLevelVariant, errorStatus } from './constants';
+import Stacktrace from './stacktrace.vue';
const SENTRY_TIMEOUT = 10000;
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 7ccb6253508..b712c8d6e85 100644
--- a/app/assets/javascripts/error_tracking/components/error_tracking_list.vue
+++ b/app/assets/javascripts/error_tracking/components/error_tracking_list.vue
@@ -18,9 +18,9 @@ import { isEmpty } from 'lodash';
import AccessorUtils from '~/lib/utils/accessor';
import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue';
import { __ } from '~/locale';
-import ErrorTrackingActions from './error_tracking_actions.vue';
import Tracking from '~/tracking';
import { trackErrorListViewsOptions, trackErrorStatusUpdateOptions } from '../utils';
+import ErrorTrackingActions from './error_tracking_actions.vue';
export const tableDataClass = 'table-col d-flex d-md-table-cell align-items-center';
diff --git a/app/assets/javascripts/error_tracking/details.js b/app/assets/javascripts/error_tracking/details.js
index 55ab362f805..65cda53626a 100644
--- a/app/assets/javascripts/error_tracking/details.js
+++ b/app/assets/javascripts/error_tracking/details.js
@@ -1,9 +1,9 @@
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import createDefaultClient from '~/lib/graphql';
+import csrf from '~/lib/utils/csrf';
import store from './store';
import ErrorDetails from './components/error_details.vue';
-import csrf from '~/lib/utils/csrf';
Vue.use(VueApollo);
diff --git a/app/assets/javascripts/error_tracking/store/actions.js b/app/assets/javascripts/error_tracking/store/actions.js
index 8f1e7e0b959..a27ebd16956 100644
--- a/app/assets/javascripts/error_tracking/store/actions.js
+++ b/app/assets/javascripts/error_tracking/store/actions.js
@@ -1,8 +1,8 @@
-import service from '../services';
-import * as types from './mutation_types';
import { deprecatedCreateFlash as createFlash } from '~/flash';
import { visitUrl } from '~/lib/utils/url_utility';
import { __ } from '~/locale';
+import service from '../services';
+import * as types from './mutation_types';
export const setStatus = ({ commit }, status) => {
commit(types.SET_ERROR_STATUS, status.toLowerCase());
diff --git a/app/assets/javascripts/error_tracking/store/details/actions.js b/app/assets/javascripts/error_tracking/store/details/actions.js
index 394dec938cf..7319d45bbd2 100644
--- a/app/assets/javascripts/error_tracking/store/details/actions.js
+++ b/app/assets/javascripts/error_tracking/store/details/actions.js
@@ -1,8 +1,8 @@
-import service from '../../services';
-import * as types from './mutation_types';
import { deprecatedCreateFlash as createFlash } from '~/flash';
import Poll from '~/lib/utils/poll';
import { __ } from '~/locale';
+import service from '../../services';
+import * as types from './mutation_types';
let stackTracePoll;
diff --git a/app/assets/javascripts/error_tracking/store/list/actions.js b/app/assets/javascripts/error_tracking/store/list/actions.js
index a242c0e4236..f07e546241a 100644
--- a/app/assets/javascripts/error_tracking/store/list/actions.js
+++ b/app/assets/javascripts/error_tracking/store/list/actions.js
@@ -1,8 +1,8 @@
-import Service from '../../services';
-import * as types from './mutation_types';
import { deprecatedCreateFlash as createFlash } from '~/flash';
import Poll from '~/lib/utils/poll';
import { __ } from '~/locale';
+import Service from '../../services';
+import * as types from './mutation_types';
let eTagPoll;
diff --git a/app/assets/javascripts/error_tracking/store/list/mutations.js b/app/assets/javascripts/error_tracking/store/list/mutations.js
index 84a62fa9024..82d747e83f8 100644
--- a/app/assets/javascripts/error_tracking/store/list/mutations.js
+++ b/app/assets/javascripts/error_tracking/store/list/mutations.js
@@ -1,6 +1,6 @@
-import * as types from './mutation_types';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import AccessorUtils from '~/lib/utils/accessor';
+import * as types from './mutation_types';
export default {
[types.SET_ERRORS](state, data) {
diff --git a/app/assets/javascripts/error_tracking_settings/store/mutations.js b/app/assets/javascripts/error_tracking_settings/store/mutations.js
index 1fc028093c1..2242169aa1e 100644
--- a/app/assets/javascripts/error_tracking_settings/store/mutations.js
+++ b/app/assets/javascripts/error_tracking_settings/store/mutations.js
@@ -1,7 +1,7 @@
import { pick } from 'lodash';
import { convertObjectPropsToCamelCase, parseBoolean } from '~/lib/utils/common_utils';
-import * as types from './mutation_types';
import { projectKeys } from '../utils';
+import * as types from './mutation_types';
export default {
[types.CLEAR_PROJECTS](state) {
diff --git a/app/assets/javascripts/feature_flags/components/edit_feature_flag.vue b/app/assets/javascripts/feature_flags/components/edit_feature_flag.vue
index 210212fa900..b1e60066e11 100644
--- a/app/assets/javascripts/feature_flags/components/edit_feature_flag.vue
+++ b/app/assets/javascripts/feature_flags/components/edit_feature_flag.vue
@@ -91,9 +91,9 @@ export default {
<h3 class="page-title gl-m-0">{{ title }}</h3>
</div>
- <div v-if="error.length" class="alert alert-danger">
+ <gl-alert v-if="error.length" variant="warning" class="gl-mb-5" :dismissible="false">
<p v-for="(message, index) in error" :key="index" class="gl-mb-0">{{ message }}</p>
- </div>
+ </gl-alert>
<feature-flag-form
:name="name"
diff --git a/app/assets/javascripts/feature_flags/components/feature_flags.vue b/app/assets/javascripts/feature_flags/components/feature_flags.vue
index ddeefd7b827..8a0b388b0aa 100644
--- a/app/assets/javascripts/feature_flags/components/feature_flags.vue
+++ b/app/assets/javascripts/feature_flags/components/feature_flags.vue
@@ -3,16 +3,16 @@ import { mapState, mapActions } from 'vuex';
import { isEmpty } from 'lodash';
import { GlAlert, GlButton, GlModalDirective, GlSprintf, GlTabs } from '@gitlab/ui';
-import { FEATURE_FLAG_SCOPE, USER_LIST_SCOPE } from '../constants';
-import FeatureFlagsTab from './feature_flags_tab.vue';
-import FeatureFlagsTable from './feature_flags_table.vue';
-import UserListsTable from './user_lists_table.vue';
import TablePagination from '~/vue_shared/components/pagination/table_pagination.vue';
import {
buildUrlWithCurrentLocation,
getParameterByName,
historyPushState,
} from '~/lib/utils/common_utils';
+import { FEATURE_FLAG_SCOPE, USER_LIST_SCOPE } from '../constants';
+import FeatureFlagsTab from './feature_flags_tab.vue';
+import FeatureFlagsTable from './feature_flags_table.vue';
+import UserListsTable from './user_lists_table.vue';
import ConfigureFeatureFlagsModal from './configure_feature_flags_modal.vue';
@@ -198,7 +198,7 @@ export default {
@token="rotateInstanceId()"
/>
<div :class="topAreaBaseClasses">
- <div class="gl-display-flex gl-flex-direction-column gl-display-md-none!">
+ <div class="gl-display-flex gl-flex-direction-column gl-md-display-none!">
<gl-button
v-if="canUserConfigure"
v-gl-modal="'configure-feature-flags'"
@@ -285,7 +285,7 @@ export default {
</feature-flags-tab>
<template #tabs-end>
<li
- class="gl-display-none gl-display-md-flex gl-align-items-center gl-flex-fill-1 gl-justify-content-end"
+ class="gl-display-none gl-md-display-flex gl-align-items-center gl-flex-fill-1 gl-justify-content-end"
>
<gl-button
v-if="canUserConfigure"
diff --git a/app/assets/javascripts/feature_flags/components/feature_flags_tab.vue b/app/assets/javascripts/feature_flags/components/feature_flags_tab.vue
index 24b0b54d1be..d0df00e446b 100644
--- a/app/assets/javascripts/feature_flags/components/feature_flags_tab.vue
+++ b/app/assets/javascripts/feature_flags/components/feature_flags_tab.vue
@@ -68,41 +68,39 @@ export default {
<span data-testid="feature-flags-tab-title">{{ title }}</span>
<gl-badge size="sm" class="gl-tab-counter-badge">{{ itemCount }}</gl-badge>
</template>
- <template>
- <gl-alert
- v-for="(message, index) in alerts"
- :key="index"
- data-testid="serverErrors"
- variant="danger"
- @dismiss="clearAlert(index)"
- >
- {{ message }}
- </gl-alert>
+ <gl-alert
+ v-for="(message, index) in alerts"
+ :key="index"
+ data-testid="serverErrors"
+ variant="danger"
+ @dismiss="clearAlert(index)"
+ >
+ {{ message }}
+ </gl-alert>
- <gl-loading-icon v-if="isLoading" :label="loadingLabel" size="md" class="gl-mt-4" />
+ <gl-loading-icon v-if="isLoading" :label="loadingLabel" size="md" class="gl-mt-4" />
- <gl-empty-state
- v-else-if="errorState"
- :title="errorTitle"
- :description="s__(`FeatureFlags|Try again in a few moments or contact your support team.`)"
- :svg-path="errorStateSvgPath"
- data-testid="error-state"
- />
+ <gl-empty-state
+ v-else-if="errorState"
+ :title="errorTitle"
+ :description="s__(`FeatureFlags|Try again in a few moments or contact your support team.`)"
+ :svg-path="errorStateSvgPath"
+ data-testid="error-state"
+ />
- <gl-empty-state
- v-else-if="emptyState"
- :title="emptyTitle"
- :svg-path="errorStateSvgPath"
- data-testid="empty-state"
- >
- <template #description>
- {{ emptyDescription }}
- <gl-link :href="featureFlagsHelpPagePath" target="_blank">
- {{ s__('FeatureFlags|More information') }}
- </gl-link>
- </template>
- </gl-empty-state>
- <slot> </slot>
- </template>
+ <gl-empty-state
+ v-else-if="emptyState"
+ :title="emptyTitle"
+ :svg-path="errorStateSvgPath"
+ data-testid="empty-state"
+ >
+ <template #description>
+ {{ emptyDescription }}
+ <gl-link :href="featureFlagsHelpPagePath" target="_blank">
+ {{ s__('FeatureFlags|More information') }}
+ </gl-link>
+ </template>
+ </gl-empty-state>
+ <slot> </slot>
</gl-tab>
</template>
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 f3b199b5aca..04a5e5bc3c5 100644
--- a/app/assets/javascripts/feature_flags/components/feature_flags_table.vue
+++ b/app/assets/javascripts/feature_flags/components/feature_flags_table.vue
@@ -199,7 +199,7 @@ export default {
:key="strategy.id"
data-testid="strategy-badge"
variant="info"
- class="gl-mr-3 gl-mt-2"
+ class="gl-mr-3 gl-mt-2 gl-white-space-normal gl-text-left gl-px-5"
>{{ strategyBadgeText(strategy) }}</gl-badge
>
</template>
diff --git a/app/assets/javascripts/feature_flags/components/form.vue b/app/assets/javascripts/feature_flags/components/form.vue
index 253661ece1f..1e5d8e219d2 100644
--- a/app/assets/javascripts/feature_flags/components/form.vue
+++ b/app/assets/javascripts/feature_flags/components/form.vue
@@ -10,13 +10,11 @@ import {
GlFormCheckbox,
GlSprintf,
GlIcon,
+ GlToggle,
} from '@gitlab/ui';
import RelatedIssuesRoot from '~/related_issues/components/related_issues_root.vue';
import { s__ } from '~/locale';
import featureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
-import ToggleButton from '~/vue_shared/components/toggle_button.vue';
-import EnvironmentsDropdown from './environments_dropdown.vue';
-import Strategy from './strategy.vue';
import {
ROLLOUT_STRATEGY_ALL_USERS,
ROLLOUT_STRATEGY_PERCENT_ROLLOUT,
@@ -27,6 +25,8 @@ import {
LEGACY_FLAG,
} from '../constants';
import { createNewEnvironmentScope } from '../store/helpers';
+import EnvironmentsDropdown from './environments_dropdown.vue';
+import Strategy from './strategy.vue';
export default {
components: {
@@ -37,7 +37,7 @@ export default {
GlTooltip,
GlSprintf,
GlIcon,
- ToggleButton,
+ GlToggle,
EnvironmentsDropdown,
Strategy,
RelatedIssuesRoot,
@@ -372,7 +372,7 @@ export default {
{{ s__('FeatureFlags|Environment Spec') }}
</div>
<div
- class="table-mobile-content js-feature-flag-status d-flex align-items-center justify-content-start"
+ class="table-mobile-content gl-display-flex gl-align-items-center gl-justify-content-start"
>
<p v-if="isAllEnvironment(scope.environmentScope)" class="js-scope-all pl-3">
{{ $options.translations.allEnvironmentsText }}
@@ -398,10 +398,10 @@ export default {
<div class="table-mobile-header" role="rowheader">
{{ s__('FeatureFlags|Status') }}
</div>
- <div class="table-mobile-content js-feature-flag-status">
- <toggle-button
+ <div class="table-mobile-content gl-display-flex gl-justify-content-center">
+ <gl-toggle
:value="scope.active"
- :disabled-input="!active || !canUpdateScope(scope)"
+ :disabled="!active || !canUpdateScope(scope)"
@change="(status) => (scope.active = status)"
/>
</div>
@@ -498,25 +498,26 @@ export default {
<div class="table-mobile-header" role="rowheader">
{{ s__('FeatureFlags|Remove') }}
</div>
- <div class="table-mobile-content js-feature-flag-delete">
+ <div class="table-mobile-content">
<gl-button
v-if="!isAllEnvironment(scope.environmentScope) && canUpdateScope(scope)"
v-gl-tooltip
:title="s__('FeatureFlags|Remove')"
class="js-delete-scope btn-transparent pr-3 pl-3"
icon="clear"
+ data-testid="feature-flag-delete"
@click="removeScope(scope)"
/>
</div>
</div>
</div>
- <div class="js-add-new-scope gl-responsive-table-row" role="row">
+ <div class="gl-responsive-table-row" role="row" data-testid="add-new-scope">
<div class="table-section section-30" role="gridcell">
<div class="table-mobile-header" role="rowheader">
{{ s__('FeatureFlags|Environment Spec') }}
</div>
- <div class="table-mobile-content js-feature-flag-status">
+ <div class="table-mobile-content">
<environments-dropdown
class="js-new-scope-name col-12"
:value="newScope"
@@ -530,9 +531,9 @@ export default {
<div class="table-mobile-header" role="rowheader">
{{ s__('FeatureFlags|Status') }}
</div>
- <div class="table-mobile-content js-feature-flag-status">
- <toggle-button
- :disabled-input="!active"
+ <div class="table-mobile-content gl-display-flex gl-justify-content-center">
+ <gl-toggle
+ :disabled="!active"
:value="false"
@change="createNewScope({ active: true })"
/>
diff --git a/app/assets/javascripts/feature_flags/components/new_feature_flag.vue b/app/assets/javascripts/feature_flags/components/new_feature_flag.vue
index 529fefd7e45..19be57f9d27 100644
--- a/app/assets/javascripts/feature_flags/components/new_feature_flag.vue
+++ b/app/assets/javascripts/feature_flags/components/new_feature_flag.vue
@@ -1,15 +1,16 @@
<script>
+import { GlAlert } from '@gitlab/ui';
import { mapState, mapActions } from 'vuex';
import axios from '~/lib/utils/axios_utils';
-import FeatureFlagForm from './form.vue';
+import featureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { NEW_VERSION_FLAG, ROLLOUT_STRATEGY_ALL_USERS } from '../constants';
import { createNewEnvironmentScope } from '../store/helpers';
-
-import featureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
+import FeatureFlagForm from './form.vue';
export default {
components: {
FeatureFlagForm,
+ GlAlert,
},
mixins: [featureFlagsMixin()],
inject: {
@@ -61,9 +62,9 @@ export default {
<div>
<h3 class="page-title">{{ s__('FeatureFlags|New feature flag') }}</h3>
- <div v-if="error.length" class="alert alert-danger">
- <p v-for="(message, index) in error" :key="index" class="mb-0">{{ message }}</p>
- </div>
+ <gl-alert v-if="error.length" variant="warning" class="gl-mb-5" :dismissible="false">
+ <p v-for="(message, index) in error" :key="index" class="gl-mb-0">{{ message }}</p>
+ </gl-alert>
<feature-flag-form
:cancel-path="path"
diff --git a/app/assets/javascripts/feature_flags/store/edit/actions.js b/app/assets/javascripts/feature_flags/store/edit/actions.js
index c4515e07a00..3ff7dac96e8 100644
--- a/app/assets/javascripts/feature_flags/store/edit/actions.js
+++ b/app/assets/javascripts/feature_flags/store/edit/actions.js
@@ -1,10 +1,10 @@
-import * as types from './mutation_types';
import axios from '~/lib/utils/axios_utils';
import { visitUrl } from '~/lib/utils/url_utility';
import { deprecatedCreateFlash as createFlash } from '~/flash';
import { __ } from '~/locale';
import { NEW_VERSION_FLAG } from '../../constants';
import { mapFromScopesViewModel, mapStrategiesToRails } from '../helpers';
+import * as types from './mutation_types';
/**
* Handles the edition of a feature flag.
diff --git a/app/assets/javascripts/feature_flags/store/edit/mutations.js b/app/assets/javascripts/feature_flags/store/edit/mutations.js
index e60dbaf4a34..777013a2b7d 100644
--- a/app/assets/javascripts/feature_flags/store/edit/mutations.js
+++ b/app/assets/javascripts/feature_flags/store/edit/mutations.js
@@ -1,6 +1,6 @@
-import * as types from './mutation_types';
import { mapToScopesViewModel, mapStrategiesToViewModel } from '../helpers';
import { LEGACY_FLAG } from '../../constants';
+import * as types from './mutation_types';
export default {
[types.REQUEST_FEATURE_FLAG](state) {
diff --git a/app/assets/javascripts/feature_flags/store/index/actions.js b/app/assets/javascripts/feature_flags/store/index/actions.js
index 6b6b3d55e16..4372c280f39 100644
--- a/app/assets/javascripts/feature_flags/store/index/actions.js
+++ b/app/assets/javascripts/feature_flags/store/index/actions.js
@@ -1,6 +1,6 @@
import Api from '~/api';
-import * as types from './mutation_types';
import axios from '~/lib/utils/axios_utils';
+import * as types from './mutation_types';
export const setFeatureFlagsOptions = ({ commit }, options) =>
commit(types.SET_FEATURE_FLAGS_OPTIONS, options);
diff --git a/app/assets/javascripts/feature_flags/store/index/mutations.js b/app/assets/javascripts/feature_flags/store/index/mutations.js
index 910b2ec42d4..25eb7da1c72 100644
--- a/app/assets/javascripts/feature_flags/store/index/mutations.js
+++ b/app/assets/javascripts/feature_flags/store/index/mutations.js
@@ -1,8 +1,8 @@
import Vue from 'vue';
-import * as types from './mutation_types';
import { parseIntPagination, normalizeHeaders } from '~/lib/utils/common_utils';
import { FEATURE_FLAG_SCOPE, USER_LIST_SCOPE } from '../../constants';
import { mapToScopesViewModel } from '../helpers';
+import * as types from './mutation_types';
const mapFlag = (flag) => ({ ...flag, scopes: mapToScopesViewModel(flag.scopes || []) });
diff --git a/app/assets/javascripts/feature_flags/store/new/actions.js b/app/assets/javascripts/feature_flags/store/new/actions.js
index 6d595603819..d0a1c77a69e 100644
--- a/app/assets/javascripts/feature_flags/store/new/actions.js
+++ b/app/assets/javascripts/feature_flags/store/new/actions.js
@@ -1,8 +1,8 @@
-import * as types from './mutation_types';
import axios from '~/lib/utils/axios_utils';
import { visitUrl } from '~/lib/utils/url_utility';
import { NEW_VERSION_FLAG } from '../../constants';
import { mapFromScopesViewModel, mapStrategiesToRails } from '../helpers';
+import * as types from './mutation_types';
/**
* Handles the creation of a new feature flag.
diff --git a/app/assets/javascripts/feature_highlight/feature_highlight.js b/app/assets/javascripts/feature_highlight/feature_highlight.js
index 2da9aadd2b1..124be35f6ca 100644
--- a/app/assets/javascripts/feature_highlight/feature_highlight.js
+++ b/app/assets/javascripts/feature_highlight/feature_highlight.js
@@ -1,6 +1,6 @@
import $ from 'jquery';
-import { getSelector, inserted } from './feature_highlight_helper';
import { togglePopover, mouseenter, debouncedMouseleave } from '../shared/popover';
+import { getSelector, inserted } from './feature_highlight_helper';
export function setupFeatureHighlightPopover(id, debounceTimeout = 300) {
const $selector = $(getSelector(id));
diff --git a/app/assets/javascripts/filtered_search/available_dropdown_mappings.js b/app/assets/javascripts/filtered_search/available_dropdown_mappings.js
index 588bd534224..43365ba8613 100644
--- a/app/assets/javascripts/filtered_search/available_dropdown_mappings.js
+++ b/app/assets/javascripts/filtered_search/available_dropdown_mappings.js
@@ -1,3 +1,4 @@
+import { mergeUrlParams } from '../lib/utils/url_utility';
import DropdownHint from './dropdown_hint';
import DropdownUser from './dropdown_user';
import DropdownNonUser from './dropdown_non_user';
@@ -6,7 +7,6 @@ import NullDropdown from './null_dropdown';
import DropdownAjaxFilter from './dropdown_ajax_filter';
import DropdownOperator from './dropdown_operator';
import DropdownUtils from './dropdown_utils';
-import { mergeUrlParams } from '../lib/utils/url_utility';
export default class AvailableDropdownMappings {
constructor({
diff --git a/app/assets/javascripts/filtered_search/dropdown_ajax_filter.js b/app/assets/javascripts/filtered_search/dropdown_ajax_filter.js
index 2c0c3024d38..99e510ba0c9 100644
--- a/app/assets/javascripts/filtered_search/dropdown_ajax_filter.js
+++ b/app/assets/javascripts/filtered_search/dropdown_ajax_filter.js
@@ -1,9 +1,9 @@
+import { __ } from '~/locale';
import { deprecatedCreateFlash as createFlash } from '../flash';
import AjaxFilter from '../droplab/plugins/ajax_filter';
import FilteredSearchDropdown from './filtered_search_dropdown';
import DropdownUtils from './dropdown_utils';
import FilteredSearchTokenizer from './filtered_search_tokenizer';
-import { __ } from '~/locale';
export default class DropdownAjaxFilter extends FilteredSearchDropdown {
constructor(options = {}) {
diff --git a/app/assets/javascripts/filtered_search/dropdown_emoji.js b/app/assets/javascripts/filtered_search/dropdown_emoji.js
index 001030b5f5f..8fb483162f1 100644
--- a/app/assets/javascripts/filtered_search/dropdown_emoji.js
+++ b/app/assets/javascripts/filtered_search/dropdown_emoji.js
@@ -1,9 +1,9 @@
+import { __ } from '~/locale';
import { deprecatedCreateFlash as Flash } from '../flash';
import Ajax from '../droplab/plugins/ajax';
import Filter from '../droplab/plugins/filter';
import FilteredSearchDropdown from './filtered_search_dropdown';
import DropdownUtils from './dropdown_utils';
-import { __ } from '~/locale';
export default class DropdownEmoji extends FilteredSearchDropdown {
constructor(options = {}) {
diff --git a/app/assets/javascripts/filtered_search/dropdown_hint.js b/app/assets/javascripts/filtered_search/dropdown_hint.js
index 1180f8683a1..6ab4015eb80 100644
--- a/app/assets/javascripts/filtered_search/dropdown_hint.js
+++ b/app/assets/javascripts/filtered_search/dropdown_hint.js
@@ -1,9 +1,9 @@
import Filter from '~/droplab/plugins/filter';
+import { __ } from '~/locale';
import FilteredSearchDropdown from './filtered_search_dropdown';
import DropdownUtils from './dropdown_utils';
import FilteredSearchDropdownManager from './filtered_search_dropdown_manager';
import FilteredSearchVisualTokens from './filtered_search_visual_tokens';
-import { __ } from '~/locale';
export default class DropdownHint extends FilteredSearchDropdown {
constructor(options = {}) {
diff --git a/app/assets/javascripts/filtered_search/dropdown_non_user.js b/app/assets/javascripts/filtered_search/dropdown_non_user.js
index 11261debeda..36c756d6a49 100644
--- a/app/assets/javascripts/filtered_search/dropdown_non_user.js
+++ b/app/assets/javascripts/filtered_search/dropdown_non_user.js
@@ -1,9 +1,9 @@
+import { __ } from '~/locale';
import { deprecatedCreateFlash as Flash } from '../flash';
import Ajax from '../droplab/plugins/ajax';
import Filter from '../droplab/plugins/filter';
import FilteredSearchDropdown from './filtered_search_dropdown';
import DropdownUtils from './dropdown_utils';
-import { __ } from '~/locale';
export default class DropdownNonUser extends FilteredSearchDropdown {
constructor(options = {}) {
diff --git a/app/assets/javascripts/filtered_search/filtered_search_manager.js b/app/assets/javascripts/filtered_search/filtered_search_manager.js
index 11b2eb839ce..755e60dd7d2 100644
--- a/app/assets/javascripts/filtered_search/filtered_search_manager.js
+++ b/app/assets/javascripts/filtered_search/filtered_search_manager.js
@@ -2,26 +2,26 @@ import { last } from 'lodash';
import recentSearchesStorageKeys from 'ee_else_ce/filtered_search/recent_searches_storage_keys';
import { getParameterByName, getUrlParamsArray } from '~/lib/utils/common_utils';
import IssuableFilteredSearchTokenKeys from '~/filtered_search/issuable_filtered_search_token_keys';
+import {
+ ENTER_KEY_CODE,
+ BACKSPACE_KEY_CODE,
+ DELETE_KEY_CODE,
+ UP_KEY_CODE,
+ DOWN_KEY_CODE,
+} from '~/lib/utils/keycodes';
+import { __ } from '~/locale';
import { visitUrl } from '../lib/utils/url_utility';
import { deprecatedCreateFlash as Flash } from '../flash';
+import { addClassIfElementExists } from '../lib/utils/dom_utils';
import FilteredSearchContainer from './container';
import RecentSearchesRoot from './recent_searches_root';
import RecentSearchesStore from './stores/recent_searches_store';
import RecentSearchesService from './services/recent_searches_service';
import eventHub from './event_hub';
-import { addClassIfElementExists } from '../lib/utils/dom_utils';
import FilteredSearchTokenizer from './filtered_search_tokenizer';
import FilteredSearchDropdownManager from './filtered_search_dropdown_manager';
import FilteredSearchVisualTokens from './filtered_search_visual_tokens';
import DropdownUtils from './dropdown_utils';
-import {
- ENTER_KEY_CODE,
- BACKSPACE_KEY_CODE,
- DELETE_KEY_CODE,
- UP_KEY_CODE,
- DOWN_KEY_CODE,
-} from '~/lib/utils/keycodes';
-import { __ } from '~/locale';
export default class FilteredSearchManager {
constructor({
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 4e594dfa910..92899de7e8e 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,5 @@
-import VisualTokenValue from './visual_token_value';
import { objectToQueryString, spriteIcon } from '~/lib/utils/common_utils';
+import VisualTokenValue from './visual_token_value';
import FilteredSearchContainer from './container';
export default class FilteredSearchVisualTokens {
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 46867b184c8..2c58506985a 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
@@ -1,6 +1,6 @@
import { flattenDeep } from 'lodash';
-import FilteredSearchTokenKeys from './filtered_search_token_keys';
import { __ } from '~/locale';
+import FilteredSearchTokenKeys from './filtered_search_token_keys';
export const tokenKeys = [
{
diff --git a/app/assets/javascripts/filtered_search/recent_searches_root.js b/app/assets/javascripts/filtered_search/recent_searches_root.js
index 6c8e77a7fe5..1182cb34210 100644
--- a/app/assets/javascripts/filtered_search/recent_searches_root.js
+++ b/app/assets/javascripts/filtered_search/recent_searches_root.js
@@ -28,19 +28,18 @@ class RecentSearchesRoot {
const { state } = this.store;
this.vm = new Vue({
el: this.wrapperElement,
- components: {
- RecentSearchesDropdownContent,
- },
data() {
return state;
},
- template: `
- <recent-searches-dropdown-content
- :items="recentSearches"
- :is-local-storage-available="isLocalStorageAvailable"
- :allowed-keys="allowedKeys"
- />
- `,
+ render(h) {
+ return h(RecentSearchesDropdownContent, {
+ props: {
+ items: this.recentSearches,
+ isLocalStorageAvailable: this.isLocalStorageAvailable,
+ allowedKeys: this.allowedKeys,
+ },
+ });
+ },
});
}
diff --git a/app/assets/javascripts/filtered_search/recent_searches_storage_keys.js b/app/assets/javascripts/filtered_search/recent_searches_storage_keys.js
index 54d49821d92..446a0e5eb24 100644
--- a/app/assets/javascripts/filtered_search/recent_searches_storage_keys.js
+++ b/app/assets/javascripts/filtered_search/recent_searches_storage_keys.js
@@ -3,4 +3,6 @@ export default {
merge_requests: 'merge-request-recent-searches',
group_members: 'group-members-recent-searches',
group_invited_members: 'group-invited-members-recent-searches',
+ project_members: 'project-members-recent-searches',
+ project_group_links: 'project-group-links-recent-searches',
};
diff --git a/app/assets/javascripts/filtered_search/services/recent_searches_service.js b/app/assets/javascripts/filtered_search/services/recent_searches_service.js
index a056dea928d..56824977a43 100644
--- a/app/assets/javascripts/filtered_search/services/recent_searches_service.js
+++ b/app/assets/javascripts/filtered_search/services/recent_searches_service.js
@@ -1,5 +1,5 @@
-import RecentSearchesServiceError from './recent_searches_service_error';
import AccessorUtilities from '../../lib/utils/accessor';
+import RecentSearchesServiceError from './recent_searches_service_error';
class RecentSearchesService {
constructor(localStorageKey = 'issuable-recent-searches') {
diff --git a/app/assets/javascripts/frequent_items/components/frequent_items_list.vue b/app/assets/javascripts/frequent_items/components/frequent_items_list.vue
index 9d898d1a1a1..6feeb5f03ad 100644
--- a/app/assets/javascripts/frequent_items/components/frequent_items_list.vue
+++ b/app/assets/javascripts/frequent_items/components/frequent_items_list.vue
@@ -1,7 +1,7 @@
<script>
+import { sanitizeItem } from '../utils';
import FrequentItemsListItem from './frequent_items_list_item.vue';
import frequentItemsMixin from './frequent_items_mixin';
-import { sanitizeItem } from '../utils';
export default {
components: {
diff --git a/app/assets/javascripts/frequent_items/components/frequent_items_search_input.vue b/app/assets/javascripts/frequent_items/components/frequent_items_search_input.vue
index 8042e8c7bc9..b4a57a0a619 100644
--- a/app/assets/javascripts/frequent_items/components/frequent_items_search_input.vue
+++ b/app/assets/javascripts/frequent_items/components/frequent_items_search_input.vue
@@ -2,9 +2,8 @@
import { debounce } from 'lodash';
import { mapActions, mapState } from 'vuex';
import { GlIcon } from '@gitlab/ui';
-import eventHub from '../event_hub';
-import frequentItemsMixin from './frequent_items_mixin';
import Tracking from '~/tracking';
+import frequentItemsMixin from './frequent_items_mixin';
const trackingMixin = Tracking.mixin();
@@ -32,12 +31,6 @@ export default {
this.setSearchQuery(this.searchQuery);
}, 500),
},
- mounted() {
- eventHub.$on(`${this.namespace}-dropdownOpen`, this.setFocus);
- },
- beforeDestroy() {
- eventHub.$off(`${this.namespace}-dropdownOpen`, this.setFocus);
- },
methods: {
...mapActions(['setSearchQuery']),
setFocus() {
diff --git a/app/assets/javascripts/frequent_items/index.js b/app/assets/javascripts/frequent_items/index.js
index cef8be37a40..ab36189c239 100644
--- a/app/assets/javascripts/frequent_items/index.js
+++ b/app/assets/javascripts/frequent_items/index.js
@@ -1,8 +1,8 @@
import $ from 'jquery';
import Vue from 'vue';
import Translate from '~/vue_shared/translate';
-import eventHub from './event_hub';
import { createStore } from '~/frequent_items/store';
+import eventHub from './event_hub';
Vue.use(Translate);
diff --git a/app/assets/javascripts/frequent_items/store/actions.js b/app/assets/javascripts/frequent_items/store/actions.js
index f4156487625..90b454d1b42 100644
--- a/app/assets/javascripts/frequent_items/store/actions.js
+++ b/app/assets/javascripts/frequent_items/store/actions.js
@@ -1,7 +1,7 @@
import AccessorUtilities from '~/lib/utils/accessor';
-import * as types from './mutation_types';
-import { getTopFrequentItems } from '../utils';
import { getGroups, getProjects } from '~/rest_api';
+import { getTopFrequentItems } from '../utils';
+import * as types from './mutation_types';
export const setNamespace = ({ commit }, namespace) => {
commit(types.SET_NAMESPACE, namespace);
diff --git a/app/assets/javascripts/gfm_auto_complete.js b/app/assets/javascripts/gfm_auto_complete.js
index cf9ff87f25e..febb108ec71 100644
--- a/app/assets/javascripts/gfm_auto_complete.js
+++ b/app/assets/javascripts/gfm_auto_complete.js
@@ -4,16 +4,20 @@ import { escape, template } from 'lodash';
import { s__ } from '~/locale';
import SidebarMediator from '~/sidebar/sidebar_mediator';
import { isUserBusy } from '~/set_status_modal/utils';
+import axios from '~/lib/utils/axios_utils';
+import * as Emoji from '~/emoji';
import glRegexp from './lib/utils/regexp';
import AjaxCache from './lib/utils/ajax_cache';
-import axios from '~/lib/utils/axios_utils';
import { spriteIcon } from './lib/utils/common_utils';
-import * as Emoji from '~/emoji';
function sanitize(str) {
return str.replace(/<(?:.|\n)*?>/gm, '');
}
+function createMemberSearchString(member) {
+ return `${member.name.replace(/ /g, '')} ${member.username}`;
+}
+
export function membersBeforeSave(members) {
return members.map((member) => {
const GROUP_TYPE = 'Group';
@@ -40,7 +44,7 @@ export function membersBeforeSave(members) {
username: member.username,
avatarTag: autoCompleteAvatar.length === 1 ? txtAvatar : imgAvatar,
title: sanitize(title),
- search: sanitize(`${member.username} ${member.name}`),
+ search: sanitize(createMemberSearchString(member)),
icon: avatarIcon,
availability: member?.availability,
};
@@ -298,9 +302,7 @@ class GfmAutoComplete {
// Cache assignees list for easier filtering later
assignees =
- SidebarMediator.singleton?.store?.assignees?.map(
- (assignee) => `${assignee.username} ${assignee.name}`,
- ) || [];
+ SidebarMediator.singleton?.store?.assignees?.map(createMemberSearchString) || [];
const match = GfmAutoComplete.defaultMatcher(flag, subtext, this.app.controllers);
return match && match.length ? match[1] : null;
diff --git a/app/assets/javascripts/gl_form.js b/app/assets/javascripts/gl_form.js
index 3e777c2dc09..bb5dea877b8 100644
--- a/app/assets/javascripts/gl_form.js
+++ b/app/assets/javascripts/gl_form.js
@@ -1,9 +1,9 @@
import $ from 'jquery';
import autosize from 'autosize';
import GfmAutoComplete, { defaultAutocompleteConfig } from 'ee_else_ce/gfm_auto_complete';
+import { disableButtonIfEmptyField } from '~/lib/utils/common_utils';
import dropzoneInput from './dropzone_input';
import { addMarkdownListeners, removeMarkdownListeners } from './lib/utils/text_markdown';
-import { disableButtonIfEmptyField } from '~/lib/utils/common_utils';
export default class GLForm {
/**
diff --git a/app/assets/javascripts/grafana_integration/components/grafana_integration.vue b/app/assets/javascripts/grafana_integration/components/grafana_integration.vue
index 7a991ac2455..7b029c6cf54 100644
--- a/app/assets/javascripts/grafana_integration/components/grafana_integration.vue
+++ b/app/assets/javascripts/grafana_integration/components/grafana_integration.vue
@@ -54,9 +54,9 @@ export default {
<template>
<section id="grafana" class="settings no-animate js-grafana-integration">
<div class="settings-header">
- <h3 class="js-section-header h4">
+ <h4 class="js-section-header">
{{ s__('GrafanaIntegration|Grafana authentication') }}
- </h3>
+ </h4>
<gl-button class="js-settings-toggle">{{ __('Expand') }}</gl-button>
<p class="js-section-sub-header">
{{ s__('GrafanaIntegration|Embed Grafana charts in GitLab issues.') }}
diff --git a/app/assets/javascripts/graphql_shared/fragments/alert_note.fragment.graphql b/app/assets/javascripts/graphql_shared/fragments/alert_note.fragment.graphql
index 74b425717a0..801311301ac 100644
--- a/app/assets/javascripts/graphql_shared/fragments/alert_note.fragment.graphql
+++ b/app/assets/javascripts/graphql_shared/fragments/alert_note.fragment.graphql
@@ -1,4 +1,4 @@
-#import "~/graphql_shared/fragments/author.fragment.graphql"
+#import "./author.fragment.graphql"
fragment AlertNote on Note {
id
diff --git a/app/assets/javascripts/graphql_shared/mutations/update_alert_status.mutation.graphql b/app/assets/javascripts/graphql_shared/mutations/alert_status_update.mutation.graphql
index 42dc388c9d1..ba1e607bc10 100644
--- a/app/assets/javascripts/graphql_shared/mutations/update_alert_status.mutation.graphql
+++ b/app/assets/javascripts/graphql_shared/mutations/alert_status_update.mutation.graphql
@@ -1,4 +1,4 @@
-#import "~/graphql_shared/fragments/alert_note.fragment.graphql"
+#import "../fragments/alert_note.fragment.graphql"
mutation updateAlertStatus($projectPath: ID!, $status: AlertManagementStatus!, $iid: String!) {
updateAlertStatus(input: { iid: $iid, status: $status, projectPath: $projectPath }) {
diff --git a/app/assets/javascripts/graphql_shared/queries/get_alerts.query.graphql b/app/assets/javascripts/graphql_shared/queries/get_alerts.query.graphql
index e94758ef60e..7a676e67f1b 100644
--- a/app/assets/javascripts/graphql_shared/queries/get_alerts.query.graphql
+++ b/app/assets/javascripts/graphql_shared/queries/get_alerts.query.graphql
@@ -10,6 +10,7 @@ query getAlerts(
$nextPageCursor: String = ""
$searchTerm: String = ""
$assigneeUsername: String = ""
+ $domain: AlertManagementDomainFilter = operations
) {
project(fullPath: $projectPath) {
alertManagementAlerts(
@@ -21,6 +22,7 @@ query getAlerts(
last: $lastPageSize
after: $nextPageCursor
before: $prevPageCursor
+ domain: $domain
) {
nodes {
...AlertListItem
diff --git a/app/assets/javascripts/group.js b/app/assets/javascripts/group.js
index 6878635b288..29ddd0c5fef 100644
--- a/app/assets/javascripts/group.js
+++ b/app/assets/javascripts/group.js
@@ -1,7 +1,7 @@
-import { slugify } from './lib/utils/text_utility';
import fetchGroupPathAvailability from '~/pages/groups/new/fetch_group_path_availability';
import { deprecatedCreateFlash as flash } from '~/flash';
import { __ } from '~/locale';
+import { slugify } from './lib/utils/text_utility';
export default class Group {
constructor() {
diff --git a/app/assets/javascripts/group_label_subscription.js b/app/assets/javascripts/group_label_subscription.js
index bfaa54080bd..9e846b88a11 100644
--- a/app/assets/javascripts/group_label_subscription.js
+++ b/app/assets/javascripts/group_label_subscription.js
@@ -1,8 +1,8 @@
import $ from 'jquery';
import { __ } from '~/locale';
+import { fixTitle, hide } from '~/tooltips';
import axios from './lib/utils/axios_utils';
import { deprecatedCreateFlash as flash } from './flash';
-import { fixTitle, hide } from '~/tooltips';
const tooltipTitles = {
group: __('Unsubscribe at group level'),
diff --git a/app/assets/javascripts/groups/components/group_item.vue b/app/assets/javascripts/groups/components/group_item.vue
index d65ad974c73..0997fb3a98c 100644
--- a/app/assets/javascripts/groups/components/group_item.vue
+++ b/app/assets/javascripts/groups/components/group_item.vue
@@ -12,8 +12,6 @@ import itemStats from './item_stats.vue';
import itemStatsValue from './item_stats_value.vue';
import itemActions from './item_actions.vue';
-import { showLearnGitLabGroupItemPopover } from '~/onboarding_issues';
-
export default {
directives: {
GlTooltip: GlTooltipDirective,
@@ -78,11 +76,6 @@ export default {
return this.group.microdata || {};
},
},
- mounted() {
- if (this.group.name === 'Learn GitLab') {
- showLearnGitLabGroupItemPopover(this.group.id);
- }
- },
methods: {
onClickRowGroup(e) {
const NO_EXPAND_CLS = 'no-expand';
@@ -179,7 +172,12 @@ export default {
<div
class="metadata align-items-md-center d-flex flex-grow-1 flex-shrink-0 flex-wrap justify-content-md-between"
>
- <item-actions v-if="isGroup" :group="group" :parent-group="parentGroup" />
+ <item-actions
+ v-if="isGroup"
+ :group="group"
+ :parent-group="parentGroup"
+ :action="action"
+ />
<item-stats :item="group" class="group-stats gl-mt-2 d-none d-md-flex" />
</div>
</div>
diff --git a/app/assets/javascripts/groups/groups_filterable_list.js b/app/assets/javascripts/groups/groups_filterable_list.js
index c33ad8b6ecb..cedf16cd7f1 100644
--- a/app/assets/javascripts/groups/groups_filterable_list.js
+++ b/app/assets/javascripts/groups/groups_filterable_list.js
@@ -1,7 +1,7 @@
import $ from 'jquery';
import FilterableList from '~/filterable_list';
-import eventHub from './event_hub';
import { normalizeHeaders, getParameterByName } from '../lib/utils/common_utils';
+import eventHub from './event_hub';
export default class GroupFilterableList extends FilterableList {
constructor({
diff --git a/app/assets/javascripts/groups/index.js b/app/assets/javascripts/groups/index.js
index e11c3aaf984..4fd2c12c9fe 100644
--- a/app/assets/javascripts/groups/index.js
+++ b/app/assets/javascripts/groups/index.js
@@ -33,8 +33,8 @@ export default (containerId = 'js-groups-tree', endpoint, action = '') => {
dataEl = containerEl.querySelector(CONTENT_LIST_CLASS);
}
- Vue.component('group-folder', groupFolderComponent);
- Vue.component('group-item', groupItemComponent);
+ Vue.component('GroupFolder', groupFolderComponent);
+ Vue.component('GroupItem', groupItemComponent);
Vue.use(GlToast);
diff --git a/app/assets/javascripts/groups/members/constants.js b/app/assets/javascripts/groups/members/constants.js
index 6d71b666d7a..3315712891d 100644
--- a/app/assets/javascripts/groups/members/constants.js
+++ b/app/assets/javascripts/groups/members/constants.js
@@ -1,5 +1 @@
export const GROUP_MEMBER_BASE_PROPERTY_NAME = 'group_member';
-export const GROUP_MEMBER_ACCESS_LEVEL_PROPERTY_NAME = 'access_level';
-
-export const GROUP_LINK_BASE_PROPERTY_NAME = 'group_link';
-export const GROUP_LINK_ACCESS_LEVEL_PROPERTY_NAME = 'group_access';
diff --git a/app/assets/javascripts/groups/members/utils.js b/app/assets/javascripts/groups/members/utils.js
index 4fcf348b69f..71918bfe9f0 100644
--- a/app/assets/javascripts/groups/members/utils.js
+++ b/app/assets/javascripts/groups/members/utils.js
@@ -1,45 +1,8 @@
-import { isUndefined } from 'lodash';
-import { convertObjectPropsToCamelCase, parseBoolean } from '~/lib/utils/common_utils';
-import {
- GROUP_MEMBER_BASE_PROPERTY_NAME,
- GROUP_MEMBER_ACCESS_LEVEL_PROPERTY_NAME,
- GROUP_LINK_BASE_PROPERTY_NAME,
- GROUP_LINK_ACCESS_LEVEL_PROPERTY_NAME,
-} from './constants';
-
-export const parseDataAttributes = (el) => {
- const { members, groupId, memberPath, canManageMembers } = el.dataset;
-
- return {
- members: convertObjectPropsToCamelCase(JSON.parse(members), { deep: true }),
- sourceId: parseInt(groupId, 10),
- memberPath,
- canManageMembers: parseBoolean(canManageMembers),
- };
-};
-
-const baseRequestFormatter = (basePropertyName, accessLevelPropertyName) => ({
- accessLevel,
- ...otherProperties
-}) => {
- const accessLevelProperty = !isUndefined(accessLevel)
- ? { [accessLevelPropertyName]: accessLevel }
- : {};
+import { baseRequestFormatter } from '~/members/utils';
+import { MEMBER_ACCESS_LEVEL_PROPERTY_NAME } from '~/members/constants';
+import { GROUP_MEMBER_BASE_PROPERTY_NAME } from './constants';
- return {
- [basePropertyName]: {
- ...accessLevelProperty,
- ...otherProperties,
- },
- };
-};
-
-export const memberRequestFormatter = baseRequestFormatter(
+export const groupMemberRequestFormatter = baseRequestFormatter(
GROUP_MEMBER_BASE_PROPERTY_NAME,
- GROUP_MEMBER_ACCESS_LEVEL_PROPERTY_NAME,
-);
-
-export const groupLinkRequestFormatter = baseRequestFormatter(
- GROUP_LINK_BASE_PROPERTY_NAME,
- GROUP_LINK_ACCESS_LEVEL_PROPERTY_NAME,
+ MEMBER_ACCESS_LEVEL_PROPERTY_NAME,
);
diff --git a/app/assets/javascripts/groups_select.js b/app/assets/javascripts/groups_select.js
index c65fff432d0..840b3030177 100644
--- a/app/assets/javascripts/groups_select.js
+++ b/app/assets/javascripts/groups_select.js
@@ -1,9 +1,9 @@
import $ from 'jquery';
import { escape } from 'lodash';
+import { __ } from '~/locale';
import axios from './lib/utils/axios_utils';
import Api from './api';
import { normalizeHeaders } from './lib/utils/common_utils';
-import { __ } from '~/locale';
import { loadCSSFile } from './lib/utils/css_utils';
const fetchGroups = (params) => {
diff --git a/app/assets/javascripts/header.js b/app/assets/javascripts/header.js
index 9f9708bf879..bcafb09bc66 100644
--- a/app/assets/javascripts/header.js
+++ b/app/assets/javascripts/header.js
@@ -106,7 +106,5 @@ export function initNavUserDropdownTracking() {
}
}
-document.addEventListener('DOMContentLoaded', () => {
- requestIdleCallback(initStatusTriggers);
- requestIdleCallback(initNavUserDropdownTracking);
-});
+requestIdleCallback(initStatusTriggers);
+requestIdleCallback(initNavUserDropdownTracking);
diff --git a/app/assets/javascripts/ide/components/activity_bar.vue b/app/assets/javascripts/ide/components/activity_bar.vue
index 644808cb83a..edb7e373f6f 100644
--- a/app/assets/javascripts/ide/components/activity_bar.vue
+++ b/app/assets/javascripts/ide/components/activity_bar.vue
@@ -1,6 +1,7 @@
<script>
import { mapActions, mapState } from 'vuex';
import { GlIcon, GlTooltipDirective } from '@gitlab/ui';
+import { BV_HIDE_TOOLTIP } from '~/lib/utils/constants';
import { leftSidebarViews } from '../constants';
export default {
@@ -11,7 +12,7 @@ export default {
GlTooltip: GlTooltipDirective,
},
computed: {
- ...mapState(['currentActivityView']),
+ ...mapState(['currentActivityView', 'stagedFiles']),
},
methods: {
...mapActions(['updateActivityBarView']),
@@ -20,7 +21,7 @@ export default {
this.updateActivityBarView(view);
- this.$root.$emit('bv::hide::tooltip');
+ this.$root.$emit(BV_HIDE_TOOLTIP);
},
},
leftSidebarViews,
@@ -81,6 +82,9 @@ export default {
@click.prevent="changedActivityView($event, $options.leftSidebarViews.commit.name)"
>
<gl-icon name="commit" />
+ <div v-if="stagedFiles.length > 0" class="ide-commit-badge badge badge-pill">
+ {{ stagedFiles.length }}
+ </div>
</button>
</li>
</ul>
diff --git a/app/assets/javascripts/ide/components/commit_sidebar/actions.vue b/app/assets/javascripts/ide/components/commit_sidebar/actions.vue
index b89329c92ec..c7f79b3ace4 100644
--- a/app/assets/javascripts/ide/components/commit_sidebar/actions.vue
+++ b/app/assets/javascripts/ide/components/commit_sidebar/actions.vue
@@ -3,7 +3,10 @@ import { escape } from 'lodash';
import { mapState, mapGetters, createNamespacedHelpers } from 'vuex';
import { GlSprintf } from '@gitlab/ui';
import { s__ } from '~/locale';
-import consts from '../../stores/modules/commit/constants';
+import {
+ COMMIT_TO_CURRENT_BRANCH,
+ COMMIT_TO_NEW_BRANCH,
+} from '../../stores/modules/commit/constants';
import RadioGroup from './radio_group.vue';
import NewMergeRequestOption from './new_merge_request_option.vue';
@@ -53,14 +56,14 @@ export default {
}
if (this.shouldDefaultToCurrentBranch) {
- this.updateCommitAction(consts.COMMIT_TO_CURRENT_BRANCH);
+ this.updateCommitAction(COMMIT_TO_CURRENT_BRANCH);
} else {
- this.updateCommitAction(consts.COMMIT_TO_NEW_BRANCH);
+ this.updateCommitAction(COMMIT_TO_NEW_BRANCH);
}
},
},
- commitToCurrentBranch: consts.COMMIT_TO_CURRENT_BRANCH,
- commitToNewBranch: consts.COMMIT_TO_NEW_BRANCH,
+ commitToCurrentBranch: COMMIT_TO_CURRENT_BRANCH,
+ commitToNewBranch: COMMIT_TO_NEW_BRANCH,
currentBranchPermissionsTooltip: s__(
"IDE|This option is disabled because you don't have write permissions for the current branch.",
),
diff --git a/app/assets/javascripts/ide/components/commit_sidebar/form.vue b/app/assets/javascripts/ide/components/commit_sidebar/form.vue
index 7c3e522a488..dd649c7d46a 100644
--- a/app/assets/javascripts/ide/components/commit_sidebar/form.vue
+++ b/app/assets/javascripts/ide/components/commit_sidebar/form.vue
@@ -1,12 +1,16 @@
<script>
import { mapState, mapActions, mapGetters } from 'vuex';
-import { GlModal, GlSafeHtmlDirective, GlButton } from '@gitlab/ui';
-import { n__, __ } from '~/locale';
+import { GlModal, GlSafeHtmlDirective, GlButton, GlTooltipDirective } from '@gitlab/ui';
+import { n__, s__ } from '~/locale';
+import { leftSidebarViews, MAX_WINDOW_HEIGHT_COMPACT } from '../../constants';
+import { createUnexpectedCommitError } from '../../lib/errors';
import CommitMessageField from './message_field.vue';
import Actions from './actions.vue';
import SuccessMessage from './success_message.vue';
-import { leftSidebarViews, MAX_WINDOW_HEIGHT_COMPACT } from '../../constants';
-import { createUnexpectedCommitError } from '../../lib/errors';
+
+const MSG_CANNOT_PUSH_CODE = s__(
+ 'WebIDE|You need permission to edit files directly in this project.',
+);
export default {
components: {
@@ -18,6 +22,7 @@ export default {
},
directives: {
SafeHtml: GlSafeHtmlDirective,
+ GlTooltip: GlTooltipDirective,
},
data() {
return {
@@ -30,15 +35,21 @@ export default {
computed: {
...mapState(['changedFiles', 'stagedFiles', 'currentActivityView', 'lastCommitMsg']),
...mapState('commit', ['commitMessage', 'submitCommitLoading', 'commitError']),
- ...mapGetters(['someUncommittedChanges']),
+ ...mapGetters(['someUncommittedChanges', 'canPushCode']),
...mapGetters('commit', ['discardDraftButtonDisabled', 'preBuiltCommitMessage']),
+ commitButtonDisabled() {
+ return !this.canPushCode || !this.someUncommittedChanges;
+ },
+ commitButtonTooltip() {
+ if (!this.canPushCode) {
+ return MSG_CANNOT_PUSH_CODE;
+ }
+
+ return '';
+ },
overviewText() {
return n__('%d changed file', '%d changed files', this.stagedFiles.length);
},
- commitButtonText() {
- return this.stagedFiles.length ? __('Commit') : __('Stage & Commit');
- },
-
currentViewIsCommitView() {
return this.currentActivityView === leftSidebarViews.commit.name;
},
@@ -73,6 +84,12 @@ export default {
'updateCommitAction',
]),
commit() {
+ // Even though the submit button will be disabled, we need to disable the submission
+ // since hitting enter on the branch name text input also submits the form.
+ if (!this.canPushCode) {
+ return false;
+ }
+
return this.commitChanges();
},
handleCompactState() {
@@ -113,6 +130,8 @@ export default {
this.componentHeight = null;
},
},
+ // Expose for tests
+ MSG_CANNOT_PUSH_CODE,
};
</script>
@@ -134,17 +153,22 @@ export default {
@after-enter="afterEndTransition"
>
<div v-if="isCompact" ref="compactEl" class="commit-form-compact">
- <gl-button
- :disabled="!someUncommittedChanges"
- category="primary"
- variant="info"
- block
- class="qa-begin-commit-button"
- data-testid="begin-commit-button"
- @click="beginCommit"
+ <div
+ v-gl-tooltip="{ title: commitButtonTooltip }"
+ data-testid="begin-commit-button-tooltip"
>
- {{ __('Commit…') }}
- </gl-button>
+ <gl-button
+ :disabled="commitButtonDisabled"
+ category="primary"
+ variant="info"
+ block
+ class="qa-begin-commit-button"
+ data-testid="begin-commit-button"
+ @click="beginCommit"
+ >
+ {{ __('Commit…') }}
+ </gl-button>
+ </div>
<p class="text-center bold">{{ overviewText }}</p>
</div>
<form v-else ref="formEl" @submit.prevent.stop="commit">
@@ -157,16 +181,29 @@ export default {
/>
<div class="clearfix gl-mt-5">
<actions />
+ <div
+ v-gl-tooltip="{ title: commitButtonTooltip }"
+ class="float-left"
+ data-testid="commit-button-tooltip"
+ >
+ <gl-button
+ :disabled="commitButtonDisabled"
+ :loading="submitCommitLoading"
+ data-testid="commit-button"
+ class="qa-commit-button"
+ category="primary"
+ variant="success"
+ @click="commit"
+ >
+ {{ __('Commit') }}
+ </gl-button>
+ </div>
<gl-button
- :loading="submitCommitLoading"
- class="float-left qa-commit-button"
- category="primary"
- variant="success"
- @click="commit"
+ v-if="!discardDraftButtonDisabled"
+ class="float-right"
+ data-testid="discard-draft"
+ @click="discardDraft"
>
- {{ __('Commit') }}
- </gl-button>
- <gl-button v-if="!discardDraftButtonDisabled" class="float-right" @click="discardDraft">
{{ __('Discard draft') }}
</gl-button>
<gl-button
diff --git a/app/assets/javascripts/ide/components/commit_sidebar/list_item.vue b/app/assets/javascripts/ide/components/commit_sidebar/list_item.vue
index 4192a002486..ab90ab0791a 100644
--- a/app/assets/javascripts/ide/components/commit_sidebar/list_item.vue
+++ b/app/assets/javascripts/ide/components/commit_sidebar/list_item.vue
@@ -85,7 +85,11 @@ export default {
role="button"
@click="openFileInEditor"
>
- <span class="multi-file-commit-list-file-path d-flex align-items-center">
+ <span
+ class="multi-file-commit-list-file-path d-flex align-items-center"
+ data-qa-selector="file_to_commit_content"
+ :data-qa-file-name="file.name"
+ >
<file-icon :file-name="file.name" class="gl-mr-3" />
<template v-if="file.prevName && file.prevName !== file.name">
{{ file.prevName }} &#x2192;
diff --git a/app/assets/javascripts/ide/components/ide.vue b/app/assets/javascripts/ide/components/ide.vue
index aac899fde0d..131a039ef1a 100644
--- a/app/assets/javascripts/ide/components/ide.vue
+++ b/app/assets/javascripts/ide/components/ide.vue
@@ -1,7 +1,7 @@
<script>
import { mapActions, mapGetters, mapState } from 'vuex';
-import { GlButton, GlLoadingIcon } from '@gitlab/ui';
-import { __ } from '~/locale';
+import { GlAlert, GlButton, GlLoadingIcon } from '@gitlab/ui';
+import { __, s__ } from '~/locale';
import {
WEBIDE_MARK_APP_START,
WEBIDE_MARK_FILE_FINISH,
@@ -10,13 +10,12 @@ import {
WEBIDE_MEASURE_BEFORE_VUE,
} from '~/performance/constants';
import { performanceMarkAndMeasure } from '~/performance/utils';
+import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { modalTypes } from '../constants';
import eventHub from '../eventhub';
+import { measurePerformance } from '../utils';
import IdeSidebar from './ide_side_bar.vue';
import RepoEditor from './repo_editor.vue';
-import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
-
-import { measurePerformance } from '../utils';
eventHub.$on(WEBIDE_MEASURE_FILE_AFTER_INTERACTION, () =>
measurePerformance(
@@ -26,10 +25,15 @@ eventHub.$on(WEBIDE_MEASURE_FILE_AFTER_INTERACTION, () =>
),
);
+const MSG_CANNOT_PUSH_CODE = s__(
+ 'WebIDE|You need permission to edit files directly in this project. Fork this project to make your changes and submit a merge request.',
+);
+
export default {
components: {
IdeSidebar,
RepoEditor,
+ GlAlert,
GlButton,
GlLoadingIcon,
ErrorMessage: () => import(/* webpackChunkName: 'ide_runtime' */ './error_message.vue'),
@@ -59,12 +63,14 @@ export default {
'loading',
]),
...mapGetters([
+ 'canPushCode',
'activeFile',
'someUncommittedChanges',
'isCommitModeActive',
'allBlobs',
'emptyRepo',
'currentTree',
+ 'hasCurrentProject',
'editorTheme',
'getUrlForPath',
]),
@@ -110,6 +116,7 @@ export default {
this.loadDeferred = true;
},
},
+ MSG_CANNOT_PUSH_CODE,
};
</script>
@@ -118,6 +125,9 @@ export default {
class="ide position-relative d-flex flex-column align-items-stretch"
:class="{ [`theme-${themeName}`]: themeName }"
>
+ <gl-alert v-if="!canPushCode" :dismissible="false">{{
+ $options.MSG_CANNOT_PUSH_CODE
+ }}</gl-alert>
<error-message v-if="errorMessage" :message="errorMessage" />
<div class="ide-view flex-grow d-flex">
<template v-if="loadDeferred">
diff --git a/app/assets/javascripts/ide/components/ide_review.vue b/app/assets/javascripts/ide/components/ide_review.vue
index 7d2f0acb08c..d75629e9995 100644
--- a/app/assets/javascripts/ide/components/ide_review.vue
+++ b/app/assets/javascripts/ide/components/ide_review.vue
@@ -1,8 +1,8 @@
<script>
import { mapGetters, mapState, mapActions } from 'vuex';
+import { viewerTypes } from '../constants';
import IdeTreeList from './ide_tree_list.vue';
import EditorModeDropdown from './editor_mode_dropdown.vue';
-import { viewerTypes } from '../constants';
export default {
components: {
diff --git a/app/assets/javascripts/ide/components/ide_side_bar.vue b/app/assets/javascripts/ide/components/ide_side_bar.vue
index 135b28685ed..a447dae3f80 100644
--- a/app/assets/javascripts/ide/components/ide_side_bar.vue
+++ b/app/assets/javascripts/ide/components/ide_side_bar.vue
@@ -1,12 +1,12 @@
<script>
import { mapState, mapGetters } from 'vuex';
import { GlDeprecatedSkeletonLoading as GlSkeletonLoading } from '@gitlab/ui';
+import { SIDEBAR_INIT_WIDTH, leftSidebarViews } from '../constants';
import IdeTree from './ide_tree.vue';
import ResizablePanel from './resizable_panel.vue';
import ActivityBar from './activity_bar.vue';
import CommitForm from './commit_sidebar/form.vue';
import IdeProjectHeader from './ide_project_header.vue';
-import { SIDEBAR_INIT_WIDTH, leftSidebarViews } from '../constants';
export default {
components: {
diff --git a/app/assets/javascripts/ide/components/ide_sidebar_nav.vue b/app/assets/javascripts/ide/components/ide_sidebar_nav.vue
index 9dbed0ace40..6bc84eb90c6 100644
--- a/app/assets/javascripts/ide/components/ide_sidebar_nav.vue
+++ b/app/assets/javascripts/ide/components/ide_sidebar_nav.vue
@@ -1,5 +1,6 @@
<script>
import { GlTooltipDirective, GlIcon } from '@gitlab/ui';
+import { BV_HIDE_TOOLTIP } from '~/lib/utils/constants';
import { otherSide } from '../utils';
import { SIDE_RIGHT } from '../constants';
@@ -50,7 +51,7 @@ export default {
},
clickTab(e, tab) {
e.currentTarget.blur();
- this.$root.$emit('bv::hide::tooltip');
+ this.$root.$emit(BV_HIDE_TOOLTIP);
if (this.isActiveTab(tab)) {
this.$emit('close');
diff --git a/app/assets/javascripts/ide/components/ide_status_bar.vue b/app/assets/javascripts/ide/components/ide_status_bar.vue
index ee292190e06..8a47835a252 100644
--- a/app/assets/javascripts/ide/components/ide_status_bar.vue
+++ b/app/assets/javascripts/ide/components/ide_status_bar.vue
@@ -2,12 +2,12 @@
/* eslint-disable @gitlab/vue-require-i18n-strings */
import { mapActions, mapState, mapGetters } from 'vuex';
import { GlIcon, GlTooltipDirective } from '@gitlab/ui';
-import IdeStatusList from './ide_status_list.vue';
-import IdeStatusMr from './ide_status_mr.vue';
import timeAgoMixin from '~/vue_shared/mixins/timeago';
import CiIcon from '../../vue_shared/components/ci_icon.vue';
import userAvatarImage from '../../vue_shared/components/user_avatar/user_avatar_image.vue';
import { rightSidebarViews } from '../constants';
+import IdeStatusMr from './ide_status_mr.vue';
+import IdeStatusList from './ide_status_list.vue';
export default {
components: {
diff --git a/app/assets/javascripts/ide/components/ide_status_list.vue b/app/assets/javascripts/ide/components/ide_status_list.vue
index aa61c0d9b5e..9966d109b55 100644
--- a/app/assets/javascripts/ide/components/ide_status_list.vue
+++ b/app/assets/javascripts/ide/components/ide_status_list.vue
@@ -1,8 +1,8 @@
<script>
import { mapGetters } from 'vuex';
import { GlLink, GlTooltipDirective } from '@gitlab/ui';
-import TerminalSyncStatusSafe from './terminal_sync/terminal_sync_status_safe.vue';
import { isTextFile, getFileEOL } from '~/ide/utils';
+import TerminalSyncStatusSafe from './terminal_sync/terminal_sync_status_safe.vue';
export default {
components: {
diff --git a/app/assets/javascripts/ide/components/ide_tree.vue b/app/assets/javascripts/ide/components/ide_tree.vue
index e563de6659a..d10714a687d 100644
--- a/app/assets/javascripts/ide/components/ide_tree.vue
+++ b/app/assets/javascripts/ide/components/ide_tree.vue
@@ -58,8 +58,9 @@ export default {
<new-entry-button
:label="__('New file')"
:show-label="false"
- class="d-flex border-0 p-0 mr-3 qa-new-file"
+ class="d-flex border-0 p-0 mr-3"
icon="doc-new"
+ data-qa-selector="new_file_button"
@click="createNewFile()"
/>
<upload
@@ -73,6 +74,7 @@ export default {
:show-label="false"
class="d-flex border-0 p-0"
icon="folder-new"
+ data-qa-selector="new_directory_button"
@click="createNewFolder()"
/>
</div>
diff --git a/app/assets/javascripts/ide/components/merge_requests/list.vue b/app/assets/javascripts/ide/components/merge_requests/list.vue
index 4b3c6e61e11..c6711d1ac10 100644
--- a/app/assets/javascripts/ide/components/merge_requests/list.vue
+++ b/app/assets/javascripts/ide/components/merge_requests/list.vue
@@ -3,8 +3,8 @@ import { mapActions, mapState } from 'vuex';
import { debounce } from 'lodash';
import { GlLoadingIcon, GlIcon } from '@gitlab/ui';
import { __ } from '~/locale';
-import Item from './item.vue';
import TokenedInput from '../shared/tokened_input.vue';
+import Item from './item.vue';
const SEARCH_TYPES = [
{ type: 'created', label: __('Created by me') },
diff --git a/app/assets/javascripts/ide/components/new_dropdown/index.vue b/app/assets/javascripts/ide/components/new_dropdown/index.vue
index 692878de5e1..46d21bc14aa 100644
--- a/app/assets/javascripts/ide/components/new_dropdown/index.vue
+++ b/app/assets/javascripts/ide/components/new_dropdown/index.vue
@@ -1,9 +1,9 @@
<script>
import { mapActions } from 'vuex';
import { GlIcon } from '@gitlab/ui';
+import { modalTypes } from '../../constants';
import upload from './upload.vue';
import ItemButton from './button.vue';
-import { modalTypes } from '../../constants';
import NewModal from './modal.vue';
export default {
@@ -108,6 +108,7 @@ export default {
class="d-flex"
icon="remove"
icon-classes="mr-2"
+ data-qa-selector="delete_button"
@click="deleteEntry(path)"
/>
</li>
diff --git a/app/assets/javascripts/ide/components/new_dropdown/upload.vue b/app/assets/javascripts/ide/components/new_dropdown/upload.vue
index 5704129c10f..76d8a0aff3d 100644
--- a/app/assets/javascripts/ide/components/new_dropdown/upload.vue
+++ b/app/assets/javascripts/ide/components/new_dropdown/upload.vue
@@ -1,6 +1,6 @@
<script>
-import ItemButton from './button.vue';
import { isTextFile } from '~/ide/utils';
+import ItemButton from './button.vue';
export default {
components: {
diff --git a/app/assets/javascripts/ide/components/panes/right.vue b/app/assets/javascripts/ide/components/panes/right.vue
index 46ef08a45a9..da484893530 100644
--- a/app/assets/javascripts/ide/components/panes/right.vue
+++ b/app/assets/javascripts/ide/components/panes/right.vue
@@ -1,13 +1,13 @@
<script>
import { mapGetters, mapState } from 'vuex';
import { __ } from '~/locale';
-import CollapsibleSidebar from './collapsible_sidebar.vue';
import ResizablePanel from '../resizable_panel.vue';
import { rightSidebarViews, SIDEBAR_INIT_WIDTH, SIDEBAR_NAV_WIDTH } from '../../constants';
import PipelinesList from '../pipelines/list.vue';
import JobsDetail from '../jobs/detail.vue';
import Clientside from '../preview/clientside.vue';
import TerminalView from '../terminal/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
const WIDTH = SIDEBAR_INIT_WIDTH + SIDEBAR_NAV_WIDTH;
diff --git a/app/assets/javascripts/ide/components/pipelines/list.vue b/app/assets/javascripts/ide/components/pipelines/list.vue
index a4a13389fbf..4c13dfc7a1d 100644
--- a/app/assets/javascripts/ide/components/pipelines/list.vue
+++ b/app/assets/javascripts/ide/components/pipelines/list.vue
@@ -10,13 +10,12 @@ import {
GlBadge,
GlAlert,
} from '@gitlab/ui';
+import IDEServices from '~/ide/services';
import { sprintf, __ } from '../../../locale';
import CiIcon from '../../../vue_shared/components/ci_icon.vue';
import EmptyState from '../../../pipelines/components/pipelines_list/empty_state.vue';
import JobsList from '../jobs/list.vue';
-import IDEServices from '~/ide/services';
-
export default {
components: {
GlIcon,
diff --git a/app/assets/javascripts/ide/components/preview/clientside.vue b/app/assets/javascripts/ide/components/preview/clientside.vue
index 4c2a369226e..3bfb8f918af 100644
--- a/app/assets/javascripts/ide/components/preview/clientside.vue
+++ b/app/assets/javascripts/ide/components/preview/clientside.vue
@@ -4,10 +4,10 @@ import { isEmpty, debounce } from 'lodash';
import { Manager } from 'smooshpack';
import { listen } from 'codesandbox-api';
import { GlLoadingIcon } from '@gitlab/ui';
-import Navigator from './navigator.vue';
import { packageJsonPath, LIVE_PREVIEW_DEBOUNCE } from '../../constants';
import { createPathWithExt } from '../../utils';
import eventHub from '../../eventhub';
+import Navigator from './navigator.vue';
export default {
components: {
@@ -165,7 +165,7 @@ export default {
</p>
<a
:href="links.webIDEHelpPagePath"
- class="btn btn-primary"
+ class="btn gl-button btn-confirm"
target="_blank"
rel="noopener noreferrer"
>
diff --git a/app/assets/javascripts/ide/components/preview/navigator.vue b/app/assets/javascripts/ide/components/preview/navigator.vue
index 8986359427f..ad4e6adf125 100644
--- a/app/assets/javascripts/ide/components/preview/navigator.vue
+++ b/app/assets/javascripts/ide/components/preview/navigator.vue
@@ -78,6 +78,7 @@ export default {
this.visitPath(this.path);
},
visitPath(path) {
+ // eslint-disable-next-line vue/no-mutating-props
this.manager.iframe.src = `${this.manager.bundlerURL}${path}`;
},
},
diff --git a/app/assets/javascripts/ide/components/repo_commit_section.vue b/app/assets/javascripts/ide/components/repo_commit_section.vue
index 8092ef3bce6..c5ff9fbbc01 100644
--- a/app/assets/javascripts/ide/components/repo_commit_section.vue
+++ b/app/assets/javascripts/ide/components/repo_commit_section.vue
@@ -1,8 +1,8 @@
<script>
import { mapState, mapActions, mapGetters } from 'vuex';
+import { stageKeys } from '../constants';
import CommitFilesList from './commit_sidebar/list.vue';
import EmptyState from './commit_sidebar/empty_state.vue';
-import { stageKeys } from '../constants';
export default {
components: {
diff --git a/app/assets/javascripts/ide/components/repo_editor.vue b/app/assets/javascripts/ide/components/repo_editor.vue
index a9c05f2e1ac..9f8bd299d52 100644
--- a/app/assets/javascripts/ide/components/repo_editor.vue
+++ b/app/assets/javascripts/ide/components/repo_editor.vue
@@ -12,21 +12,21 @@ import {
WEBIDE_MEASURE_FILE_AFTER_INTERACTION,
} from '~/performance/constants';
import { performanceMarkAndMeasure } from '~/performance/utils';
-import eventHub from '../eventhub';
+import { __ } from '~/locale';
+import Editor from '../lib/editor';
import {
leftSidebarViews,
viewerTypes,
FILE_VIEW_MODE_EDITOR,
FILE_VIEW_MODE_PREVIEW,
} from '../constants';
-import Editor from '../lib/editor';
-import FileTemplatesBar from './file_templates/bar.vue';
-import { __ } from '~/locale';
+import eventHub from '../eventhub';
import { extractMarkdownImagesFromEntries } from '../stores/utils';
import { getFileEditorOrDefault } from '../stores/modules/editor/utils';
import { getPathParent, readFileAsDataURL, registerSchema, isTextFile } from '../utils';
import { getRulesWithTraversal } from '../lib/editorconfig/parser';
import mapRulesToMonaco from '../lib/editorconfig/rules_mapper';
+import FileTemplatesBar from './file_templates/bar.vue';
export default {
name: 'RepoEditor',
diff --git a/app/assets/javascripts/ide/components/terminal/session.vue b/app/assets/javascripts/ide/components/terminal/session.vue
index 0e67a2ab45f..4089d2e4ba9 100644
--- a/app/assets/javascripts/ide/components/terminal/session.vue
+++ b/app/assets/javascripts/ide/components/terminal/session.vue
@@ -2,8 +2,8 @@
import { mapActions, mapState } from 'vuex';
import { GlButton } from '@gitlab/ui';
import { __ } from '~/locale';
-import Terminal from './terminal.vue';
import { isEndingStatus } from '../../stores/modules/terminal/utils';
+import Terminal from './terminal.vue';
export default {
components: {
diff --git a/app/assets/javascripts/ide/components/terminal/terminal.vue b/app/assets/javascripts/ide/components/terminal/terminal.vue
index 0ee4107f9ab..9cfc5504c83 100644
--- a/app/assets/javascripts/ide/components/terminal/terminal.vue
+++ b/app/assets/javascripts/ide/components/terminal/terminal.vue
@@ -3,9 +3,9 @@ import { mapState } from 'vuex';
import { GlLoadingIcon } from '@gitlab/ui';
import { __ } from '~/locale';
import GLTerminal from '~/terminal/terminal';
-import TerminalControls from './terminal_controls.vue';
import { RUNNING, STOPPING } from '../../stores/modules/terminal/constants';
import { isStartingStatus } from '../../stores/modules/terminal/utils';
+import TerminalControls from './terminal_controls.vue';
export default {
components: {
diff --git a/app/assets/javascripts/ide/constants.js b/app/assets/javascripts/ide/constants.js
index e5618466395..6bd74b143e2 100644
--- a/app/assets/javascripts/ide/constants.js
+++ b/app/assets/javascripts/ide/constants.js
@@ -16,6 +16,13 @@ export const PERMISSION_CREATE_MR = 'createMergeRequestIn';
export const PERMISSION_READ_MR = 'readMergeRequest';
export const PERMISSION_PUSH_CODE = 'pushCode';
+// The default permission object to use when the project data isn't available yet.
+// This helps us encapsulate checks like `canPushCode` without requiring an
+// additional check like `currentProject && canPushCode`.
+export const DEFAULT_PERMISSIONS = {
+ [PERMISSION_PUSH_CODE]: true,
+};
+
export const viewerTypes = {
mr: 'mrdiff',
edit: 'editor',
diff --git a/app/assets/javascripts/ide/index.js b/app/assets/javascripts/ide/index.js
index af408c06556..bf0d6b32850 100644
--- a/app/assets/javascripts/ide/index.js
+++ b/app/assets/javascripts/ide/index.js
@@ -3,11 +3,11 @@ import { mapActions } from 'vuex';
import { identity } from 'lodash';
import Translate from '~/vue_shared/translate';
import PerformancePlugin from '~/performance/vue_performance_plugin';
+import { parseBoolean } from '../lib/utils/common_utils';
+import { resetServiceWorkersPublicPath } from '../lib/utils/webpack';
import ide from './components/ide.vue';
import { createStore } from './stores';
import { createRouter } from './ide_router';
-import { parseBoolean } from '../lib/utils/common_utils';
-import { resetServiceWorkersPublicPath } from '../lib/utils/webpack';
import { DEFAULT_THEME } from './lib/themes';
Vue.use(Translate);
diff --git a/app/assets/javascripts/ide/lib/common/model.js b/app/assets/javascripts/ide/lib/common/model.js
index 4969875439e..46128651547 100644
--- a/app/assets/javascripts/ide/lib/common/model.js
+++ b/app/assets/javascripts/ide/lib/common/model.js
@@ -1,9 +1,9 @@
import { editor as monacoEditor, Uri } from 'monaco-editor';
-import Disposable from './disposable';
+import { insertFinalNewline } from '~/lib/utils/text_utility';
import eventHub from '../../eventhub';
import { trimTrailingWhitespace } from '../../utils';
-import { insertFinalNewline } from '~/lib/utils/text_utility';
import { defaultModelOptions } from '../editor_options';
+import Disposable from './disposable';
export default class Model {
constructor(file, head = null) {
diff --git a/app/assets/javascripts/ide/lib/diff/controller.js b/app/assets/javascripts/ide/lib/diff/controller.js
index 3efe692be13..ee4fdd10c97 100644
--- a/app/assets/javascripts/ide/lib/diff/controller.js
+++ b/app/assets/javascripts/ide/lib/diff/controller.js
@@ -1,7 +1,7 @@
import { Range } from 'monaco-editor';
import { throttle } from 'lodash';
-import DirtyDiffWorker from './diff_worker';
import Disposable from '../common/disposable';
+import DirtyDiffWorker from './diff_worker';
export const getDiffChangeType = (change) => {
if (change.modified) {
diff --git a/app/assets/javascripts/ide/lib/editor.js b/app/assets/javascripts/ide/lib/editor.js
index 4fad0c09ce7..f3c572bfeed 100644
--- a/app/assets/javascripts/ide/lib/editor.js
+++ b/app/assets/javascripts/ide/lib/editor.js
@@ -1,5 +1,7 @@
import { debounce } from 'lodash';
import { editor as monacoEditor, KeyCode, KeyMod, Range } from 'monaco-editor';
+import { clearDomElement } from '~/editor/utils';
+import { registerLanguages } from '../utils';
import DecorationsController from './decorations/controller';
import DirtyDiffController from './diff/controller';
import Disposable from './common/disposable';
@@ -8,8 +10,6 @@ import { editorOptions, defaultEditorOptions, defaultDiffEditorOptions } from '.
import { themes } from './themes';
import languages from './languages';
import keymap from './keymap.json';
-import { clearDomElement } from '~/editor/utils';
-import { registerLanguages } from '../utils';
function setupThemes() {
themes.forEach((theme) => {
diff --git a/app/assets/javascripts/ide/lib/errors.js b/app/assets/javascripts/ide/lib/errors.js
index f975034a872..a8a048e588f 100644
--- a/app/assets/javascripts/ide/lib/errors.js
+++ b/app/assets/javascripts/ide/lib/errors.js
@@ -1,6 +1,6 @@
import { escape } from 'lodash';
import { __ } from '~/locale';
-import consts from '../stores/modules/commit/constants';
+import { COMMIT_TO_NEW_BRANCH } from '../stores/modules/commit/constants';
const CODEOWNERS_REGEX = /Push.*protected branches.*CODEOWNERS/;
const BRANCH_CHANGED_REGEX = /changed.*since.*start.*edit/;
@@ -8,7 +8,7 @@ const BRANCH_ALREADY_EXISTS = /branch.*already.*exists/;
const createNewBranchAndCommit = (store) =>
store
- .dispatch('commit/updateCommitAction', consts.COMMIT_TO_NEW_BRANCH)
+ .dispatch('commit/updateCommitAction', COMMIT_TO_NEW_BRANCH)
.then(() => store.dispatch('commit/commitChanges'));
export const createUnexpectedCommitError = (message) => ({
diff --git a/app/assets/javascripts/ide/lib/mirror.js b/app/assets/javascripts/ide/lib/mirror.js
index 6f9cfec9465..78990953beb 100644
--- a/app/assets/javascripts/ide/lib/mirror.js
+++ b/app/assets/javascripts/ide/lib/mirror.js
@@ -1,6 +1,6 @@
-import createDiff from './create_diff';
import { getWebSocketUrl, mergeUrlParams } from '~/lib/utils/url_utility';
import { __ } from '~/locale';
+import createDiff from './create_diff';
export const SERVICE_NAME = 'webide-file-sync';
export const PROTOCOL = 'webfilesync.gitlab.com';
diff --git a/app/assets/javascripts/ide/stores/actions.js b/app/assets/javascripts/ide/stores/actions.js
index d62dfc35d15..d4ae460d78b 100644
--- a/app/assets/javascripts/ide/stores/actions.js
+++ b/app/assets/javascripts/ide/stores/actions.js
@@ -9,11 +9,11 @@ import {
WEBIDE_MARK_FETCH_BRANCH_DATA_FINISH,
WEBIDE_MEASURE_FETCH_BRANCH_DATA,
} from '~/performance/constants';
-import * as types from './mutation_types';
import { decorateFiles } from '../lib/files';
import { stageKeys, commitActionTypes } from '../constants';
import service from '../services';
import eventHub from '../eventhub';
+import * as types from './mutation_types';
export const redirectToUrl = (self, url) => visitUrl(url);
diff --git a/app/assets/javascripts/ide/stores/getters.js b/app/assets/javascripts/ide/stores/getters.js
index 59e8d37a92a..9600c1a1b8c 100644
--- a/app/assets/javascripts/ide/stores/getters.js
+++ b/app/assets/javascripts/ide/stores/getters.js
@@ -1,13 +1,14 @@
-import { getChangesCountForFiles, filePathMatches } from './utils';
+import { addNumericSuffix } from '~/ide/utils';
+import Api from '~/api';
import {
leftSidebarViews,
packageJsonPath,
+ DEFAULT_PERMISSIONS,
PERMISSION_READ_MR,
PERMISSION_CREATE_MR,
PERMISSION_PUSH_CODE,
} from '../constants';
-import { addNumericSuffix } from '~/ide/utils';
-import Api from '~/api';
+import { getChangesCountForFiles, filePathMatches } from './utils';
export const activeFile = (state) => state.openFiles.find((file) => file.active) || null;
@@ -150,7 +151,7 @@ export const getDiffInfo = (state, getters) => (path) => {
};
export const findProjectPermissions = (state, getters) => (projectId) =>
- getters.findProject(projectId)?.userPermissions || {};
+ getters.findProject(projectId)?.userPermissions || DEFAULT_PERMISSIONS;
export const canReadMergeRequests = (state, getters) =>
Boolean(getters.findProjectPermissions(state.currentProjectId)[PERMISSION_READ_MR]);
diff --git a/app/assets/javascripts/ide/stores/modules/commit/actions.js b/app/assets/javascripts/ide/stores/modules/commit/actions.js
index 29b9a8a9521..57a1e4f133a 100644
--- a/app/assets/javascripts/ide/stores/modules/commit/actions.js
+++ b/app/assets/javascripts/ide/stores/modules/commit/actions.js
@@ -1,14 +1,14 @@
import { sprintf, __ } from '~/locale';
import { deprecatedCreateFlash as flash } from '~/flash';
+import { addNumericSuffix } from '~/ide/utils';
import * as rootTypes from '../../mutation_types';
import { createCommitPayload, createNewMergeRequestUrl } from '../../utils';
import service from '../../../services';
-import * as types from './mutation_types';
-import consts from './constants';
import { leftSidebarViews } from '../../../constants';
import eventHub from '../../../eventhub';
import { parseCommitError } from '../../../lib/errors';
-import { addNumericSuffix } from '~/ide/utils';
+import { COMMIT_TO_CURRENT_BRANCH } from './constants';
+import * as types from './mutation_types';
export const updateCommitMessage = ({ commit }, message) => {
commit(types.UPDATE_COMMIT_MESSAGE, message);
@@ -112,7 +112,7 @@ export const commitChanges = ({ commit, state, getters, dispatch, rootState, roo
// Pull commit options out because they could change
// During some of the pre and post commit processing
const { shouldCreateMR, shouldHideNewMrOption, isCreatingNewBranch, branchName } = getters;
- const newBranch = state.commitAction !== consts.COMMIT_TO_CURRENT_BRANCH;
+ const newBranch = state.commitAction !== COMMIT_TO_CURRENT_BRANCH;
const stageFilesPromise = rootState.stagedFiles.length
? Promise.resolve()
: dispatch('stageAllChanges', null, { root: true });
@@ -206,7 +206,7 @@ export const commitChanges = ({ commit, state, getters, dispatch, rootState, roo
dispatch('updateViewer', 'editor', { root: true });
}
})
- .then(() => dispatch('updateCommitAction', consts.COMMIT_TO_CURRENT_BRANCH))
+ .then(() => dispatch('updateCommitAction', COMMIT_TO_CURRENT_BRANCH))
.then(() => {
if (newBranch) {
const path = rootGetters.activeFile ? rootGetters.activeFile.path : '';
diff --git a/app/assets/javascripts/ide/stores/modules/commit/constants.js b/app/assets/javascripts/ide/stores/modules/commit/constants.js
index c6c3701effe..9f4299e5537 100644
--- a/app/assets/javascripts/ide/stores/modules/commit/constants.js
+++ b/app/assets/javascripts/ide/stores/modules/commit/constants.js
@@ -1,7 +1,2 @@
-const COMMIT_TO_CURRENT_BRANCH = '1';
-const COMMIT_TO_NEW_BRANCH = '2';
-
-export default {
- COMMIT_TO_CURRENT_BRANCH,
- COMMIT_TO_NEW_BRANCH,
-};
+export const COMMIT_TO_CURRENT_BRANCH = '1';
+export const COMMIT_TO_NEW_BRANCH = '2';
diff --git a/app/assets/javascripts/ide/stores/modules/commit/getters.js b/app/assets/javascripts/ide/stores/modules/commit/getters.js
index 2301cf23f9f..f5e367e16f5 100644
--- a/app/assets/javascripts/ide/stores/modules/commit/getters.js
+++ b/app/assets/javascripts/ide/stores/modules/commit/getters.js
@@ -1,5 +1,5 @@
import { sprintf, n__, __ } from '../../../../locale';
-import consts from './constants';
+import { COMMIT_TO_NEW_BRANCH } from './constants';
const BRANCH_SUFFIX_COUNT = 5;
const createTranslatedTextForFiles = (files, text) => {
@@ -48,7 +48,7 @@ export const preBuiltCommitMessage = (state, _, rootState) => {
.join('\n');
};
-export const isCreatingNewBranch = (state) => state.commitAction === consts.COMMIT_TO_NEW_BRANCH;
+export const isCreatingNewBranch = (state) => state.commitAction === COMMIT_TO_NEW_BRANCH;
export const shouldHideNewMrOption = (_state, getters, _rootState, rootGetters) =>
!getters.isCreatingNewBranch &&
diff --git a/app/assets/javascripts/ide/stores/modules/file_templates/actions.js b/app/assets/javascripts/ide/stores/modules/file_templates/actions.js
index 6800f824da0..ad5b01e7040 100644
--- a/app/assets/javascripts/ide/stores/modules/file_templates/actions.js
+++ b/app/assets/javascripts/ide/stores/modules/file_templates/actions.js
@@ -1,8 +1,8 @@
import Api from '~/api';
import { __ } from '~/locale';
import { normalizeHeaders } from '~/lib/utils/common_utils';
-import * as types from './mutation_types';
import eventHub from '../../../eventhub';
+import * as types from './mutation_types';
export const requestTemplateTypes = ({ commit }) => commit(types.REQUEST_TEMPLATE_TYPES);
export const receiveTemplateTypesError = ({ commit, dispatch }) => {
diff --git a/app/assets/javascripts/ide/stores/modules/file_templates/getters.js b/app/assets/javascripts/ide/stores/modules/file_templates/getters.js
index 0613fe9b12b..9708e5e588c 100644
--- a/app/assets/javascripts/ide/stores/modules/file_templates/getters.js
+++ b/app/assets/javascripts/ide/stores/modules/file_templates/getters.js
@@ -1,5 +1,5 @@
-import { leftSidebarViews } from '../../../constants';
import { __ } from '~/locale';
+import { leftSidebarViews } from '../../../constants';
export const templateTypes = () => [
{
diff --git a/app/assets/javascripts/ide/stores/modules/terminal_sync/actions.js b/app/assets/javascripts/ide/stores/modules/terminal_sync/actions.js
index 006800f58c2..a2cb0666a99 100644
--- a/app/assets/javascripts/ide/stores/modules/terminal_sync/actions.js
+++ b/app/assets/javascripts/ide/stores/modules/terminal_sync/actions.js
@@ -1,5 +1,5 @@
-import * as types from './mutation_types';
import mirror, { canConnect } from '../../../lib/mirror';
+import * as types from './mutation_types';
export const upload = ({ rootState, commit }) => {
commit(types.START_LOADING);
diff --git a/app/assets/javascripts/ide/stores/mutations/file.js b/app/assets/javascripts/ide/stores/mutations/file.js
index 4446971d5d6..d678c5e280c 100644
--- a/app/assets/javascripts/ide/stores/mutations/file.js
+++ b/app/assets/javascripts/ide/stores/mutations/file.js
@@ -1,7 +1,7 @@
+import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import * as types from '../mutation_types';
import { sortTree } from '../utils';
import { diffModes } from '../../constants';
-import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
export default {
[types.SET_FILE_ACTIVE](state, { path, active }) {
diff --git a/app/assets/javascripts/ide/stores/utils.js b/app/assets/javascripts/ide/stores/utils.js
index 04eacf271b8..4019703b296 100644
--- a/app/assets/javascripts/ide/stores/utils.js
+++ b/app/assets/javascripts/ide/stores/utils.js
@@ -1,10 +1,10 @@
-import { commitActionTypes } from '../constants';
import {
relativePathToAbsolute,
isAbsolute,
isRootRelative,
isBlobUrl,
} from '~/lib/utils/url_utility';
+import { commitActionTypes } from '../constants';
export const dataStructure = () => ({
id: '',
diff --git a/app/assets/javascripts/ide/utils.js b/app/assets/javascripts/ide/utils.js
index 8eb2d17b876..99d6ccde770 100644
--- a/app/assets/javascripts/ide/utils.js
+++ b/app/assets/javascripts/ide/utils.js
@@ -1,7 +1,7 @@
import { languages } from 'monaco-editor';
import { flatten, isString } from 'lodash';
-import { SIDE_LEFT, SIDE_RIGHT } from './constants';
import { performanceMarkAndMeasure } from '~/performance/utils';
+import { SIDE_LEFT, SIDE_RIGHT } from './constants';
const toLowerCase = (x) => x.toLowerCase();
diff --git a/app/assets/javascripts/image_diff/image_diff.js b/app/assets/javascripts/image_diff/image_diff.js
index 079f4a63f6e..a0dd8e6f894 100644
--- a/app/assets/javascripts/image_diff/image_diff.js
+++ b/app/assets/javascripts/image_diff/image_diff.js
@@ -1,7 +1,7 @@
import $ from 'jquery';
+import { isImageLoaded } from '../lib/utils/image_utility';
import imageDiffHelper from './helpers/index';
import ImageBadge from './image_badge';
-import { isImageLoaded } from '../lib/utils/image_utility';
export default class ImageDiff {
constructor(el, options) {
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 80e2e73f420..c4859d4f60e 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
@@ -1,5 +1,14 @@
<script>
-import { GlLoadingIcon } from '@gitlab/ui';
+import {
+ GlEmptyState,
+ GlIcon,
+ GlLink,
+ GlLoadingIcon,
+ GlSearchBoxByClick,
+ GlSprintf,
+} from '@gitlab/ui';
+import { s__ } from '~/locale';
+import PaginationLinks from '~/vue_shared/components/pagination_links.vue';
import bulkImportSourceGroupsQuery from '../graphql/queries/bulk_import_source_groups.query.graphql';
import availableNamespacesQuery from '../graphql/queries/available_namespaces.query.graphql';
import setTargetNamespaceMutation from '../graphql/mutations/set_target_namespace.mutation.graphql';
@@ -7,72 +16,169 @@ import setNewNameMutation from '../graphql/mutations/set_new_name.mutation.graph
import importGroupMutation from '../graphql/mutations/import_group.mutation.graphql';
import ImportTableRow from './import_table_row.vue';
-const mapApolloMutations = (mutations) =>
- Object.fromEntries(
- Object.entries(mutations).map(([key, mutation]) => [
- key,
- function mutate(config) {
- return this.$apollo.mutate({
- mutation,
- ...config,
- });
- },
- ]),
- );
-
export default {
components: {
+ GlEmptyState,
+ GlIcon,
+ GlLink,
GlLoadingIcon,
+ GlSearchBoxByClick,
+ GlSprintf,
ImportTableRow,
+ PaginationLinks,
+ },
+
+ props: {
+ sourceUrl: {
+ type: String,
+ required: true,
+ },
+ },
+
+ data() {
+ return {
+ filter: '',
+ page: 1,
+ };
},
apollo: {
- bulkImportSourceGroups: bulkImportSourceGroupsQuery,
+ bulkImportSourceGroups: {
+ query: bulkImportSourceGroupsQuery,
+ variables() {
+ return { page: this.page, filter: this.filter };
+ },
+ },
availableNamespaces: availableNamespacesQuery,
},
+ computed: {
+ hasGroups() {
+ return this.bulkImportSourceGroups?.nodes?.length > 0;
+ },
+
+ hasEmptyFilter() {
+ return this.filter.length > 0 && !this.hasGroups;
+ },
+
+ statusMessage() {
+ return this.filter.length === 0
+ ? s__('BulkImport|Showing %{start}-%{end} of %{total} from %{link}')
+ : s__(
+ 'BulkImport|Showing %{start}-%{end} of %{total} matching filter "%{filter}" from %{link}',
+ );
+ },
+
+ paginationInfo() {
+ const { page, perPage, total } = this.bulkImportSourceGroups?.pageInfo ?? {
+ page: 1,
+ perPage: 0,
+ total: 0,
+ };
+ const start = (page - 1) * perPage + 1;
+ const end = start + (this.bulkImportSourceGroups.nodes?.length ?? 0) - 1;
+
+ return { start, end, total };
+ },
+ },
+
+ watch: {
+ filter() {
+ this.page = 1;
+ },
+ },
+
methods: {
- ...mapApolloMutations({
- setTargetNamespace: setTargetNamespaceMutation,
- setNewName: setNewNameMutation,
- importGroup: importGroupMutation,
- }),
+ setPage(page) {
+ this.page = page;
+ },
+
+ updateTargetNamespace(sourceGroupId, targetNamespace) {
+ this.$apollo.mutate({
+ mutation: setTargetNamespaceMutation,
+ variables: { sourceGroupId, targetNamespace },
+ });
+ },
+
+ updateNewName(sourceGroupId, newName) {
+ this.$apollo.mutate({
+ mutation: setNewNameMutation,
+ variables: { sourceGroupId, newName },
+ });
+ },
+
+ importGroup(sourceGroupId) {
+ this.$apollo.mutate({
+ mutation: importGroupMutation,
+ variables: { sourceGroupId },
+ });
+ },
},
};
</script>
<template>
<div>
- <gl-loading-icon v-if="$apollo.loading" size="md" class="gl-mt-5" />
- <div v-else-if="bulkImportSourceGroups.length">
- <table class="gl-w-full">
- <thead class="gl-border-solid gl-border-gray-200 gl-border-0 gl-border-b-1">
- <th class="gl-py-4 import-jobs-from-col">{{ s__('BulkImport|From source group') }}</th>
- <th class="gl-py-4 import-jobs-to-col">{{ s__('BulkImport|To new group') }}</th>
- <th class="gl-py-4 import-jobs-status-col">{{ __('Status') }}</th>
- <th class="gl-py-4 import-jobs-cta-col"></th>
- </thead>
- <tbody>
- <template v-for="group in bulkImportSourceGroups">
- <import-table-row
- :key="group.id"
- :group="group"
- :available-namespaces="availableNamespaces"
- @update-target-namespace="
- setTargetNamespace({
- variables: { sourceGroupId: group.id, targetNamespace: $event },
- })
- "
- @update-new-name="
- setNewName({
- variables: { sourceGroupId: group.id, newName: $event },
- })
- "
- @import-group="importGroup({ variables: { sourceGroupId: group.id } })"
- />
+ <div
+ class="gl-py-5 gl-border-solid gl-border-gray-200 gl-border-0 gl-border-b-1 gl-display-flex gl-align-items-center"
+ >
+ <span>
+ <gl-sprintf v-if="!$apollo.loading && hasGroups" :message="statusMessage">
+ <template #start>
+ <strong>{{ paginationInfo.start }}</strong>
+ </template>
+ <template #end>
+ <strong>{{ paginationInfo.end }}</strong>
+ </template>
+ <template #total>
+ <strong>{{ n__('%d group', '%d groups', paginationInfo.total) }}</strong>
</template>
- </tbody>
- </table>
+ <template #filter>
+ <strong>{{ filter }}</strong>
+ </template>
+ <template #link>
+ <gl-link class="gl-display-inline-block" :href="sourceUrl" target="_blank">
+ {{ sourceUrl }} <gl-icon name="external-link" class="vertical-align-middle" />
+ </gl-link>
+ </template>
+ </gl-sprintf>
+ </span>
+ <gl-search-box-by-click class="gl-ml-auto" @submit="filter = $event" @clear="filter = ''" />
</div>
+ <gl-loading-icon v-if="$apollo.loading" size="md" class="gl-mt-5" />
+ <template v-else>
+ <gl-empty-state v-if="hasEmptyFilter" :title="__('Sorry, your filter produced no results')" />
+ <gl-empty-state
+ v-else-if="!hasGroups"
+ :title="s__('BulkImport|No groups available for import')"
+ />
+ <div v-else class="gl-display-flex gl-flex-direction-column gl-align-items-center">
+ <table class="gl-w-full">
+ <thead class="gl-border-solid gl-border-gray-200 gl-border-0 gl-border-b-1">
+ <th class="gl-py-4 import-jobs-from-col">{{ s__('BulkImport|From source group') }}</th>
+ <th class="gl-py-4 import-jobs-to-col">{{ s__('BulkImport|To new group') }}</th>
+ <th class="gl-py-4 import-jobs-status-col">{{ __('Status') }}</th>
+ <th class="gl-py-4 import-jobs-cta-col"></th>
+ </thead>
+ <tbody>
+ <template v-for="group in bulkImportSourceGroups.nodes">
+ <import-table-row
+ :key="group.id"
+ :group="group"
+ :available-namespaces="availableNamespaces"
+ @update-target-namespace="updateTargetNamespace(group.id, $event)"
+ @update-new-name="updateNewName(group.id, $event)"
+ @import-group="importGroup(group.id)"
+ />
+ </template>
+ </tbody>
+ </table>
+ <pagination-links
+ :change="setPage"
+ :page-info="bulkImportSourceGroups.pageInfo"
+ class="gl-mt-3"
+ />
+ </div>
+ </template>
</div>
</template>
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 8f2d488d661..beb058417e5 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
@@ -1,4 +1,5 @@
import axios from '~/lib/utils/axios_utils';
+import { parseIntPagination, normalizeHeaders } from '~/lib/utils/common_utils';
import createDefaultClient from '~/lib/graphql';
import { s__ } from '~/locale';
import createFlash from '~/flash';
@@ -8,8 +9,10 @@ import { SourceGroupsManager } from './services/source_groups_manager';
import { StatusPoller } from './services/status_poller';
export const clientTypenames = {
+ BulkImportSourceGroupConnection: 'ClientBulkImportSourceGroupConnection',
BulkImportSourceGroup: 'ClientBulkImportSourceGroup',
AvailableNamespace: 'ClientAvailableNamespace',
+ BulkImportPageInfo: 'ClientBulkImportPageInfo',
};
export function createResolvers({ endpoints }) {
@@ -17,22 +20,39 @@ export function createResolvers({ endpoints }) {
return {
Query: {
- async bulkImportSourceGroups(_, __, { client }) {
+ async bulkImportSourceGroups(_, vars, { client }) {
const {
data: { availableNamespaces },
} = await client.query({ query: availableNamespacesQuery });
- return axios.get(endpoints.status).then(({ data }) => {
- return data.importable_data.map((group) => ({
- __typename: clientTypenames.BulkImportSourceGroup,
- ...group,
- status: STATUSES.NONE,
- import_target: {
- new_name: group.full_path,
- target_namespace: availableNamespaces[0].full_path,
+ return axios
+ .get(endpoints.status, {
+ params: {
+ page: vars.page,
+ per_page: vars.perPage,
+ filter: vars.filter,
},
- }));
- });
+ })
+ .then(({ headers, data }) => {
+ const pagination = parseIntPagination(normalizeHeaders(headers));
+
+ return {
+ __typename: clientTypenames.BulkImportSourceGroupConnection,
+ nodes: data.importable_data.map((group) => ({
+ __typename: clientTypenames.BulkImportSourceGroup,
+ ...group,
+ status: STATUSES.NONE,
+ import_target: {
+ new_name: group.full_path,
+ target_namespace: availableNamespaces[0].full_path,
+ },
+ })),
+ pageInfo: {
+ __typename: clientTypenames.BulkImportPageInfo,
+ ...pagination,
+ },
+ };
+ });
},
availableNamespaces: () =>
diff --git a/app/assets/javascripts/import_entities/import_groups/graphql/queries/bulk_import_source_groups.query.graphql b/app/assets/javascripts/import_entities/import_groups/graphql/queries/bulk_import_source_groups.query.graphql
index 8d52d94925c..28dfefdf8a7 100644
--- a/app/assets/javascripts/import_entities/import_groups/graphql/queries/bulk_import_source_groups.query.graphql
+++ b/app/assets/javascripts/import_entities/import_groups/graphql/queries/bulk_import_source_groups.query.graphql
@@ -1,7 +1,15 @@
#import "../fragments/bulk_import_source_group_item.fragment.graphql"
-query bulkImportSourceGroups {
- bulkImportSourceGroups @client {
- ...BulkImportSourceGroupItem
+query bulkImportSourceGroups($page: Int = 1, $perPage: Int = 20, $filter: String = "") {
+ bulkImportSourceGroups(page: $page, filter: $filter, perPage: $perPage) @client {
+ nodes {
+ ...BulkImportSourceGroupItem
+ }
+ pageInfo {
+ page
+ perPage
+ total
+ totalPages
+ }
}
}
diff --git a/app/assets/javascripts/import_entities/import_groups/graphql/services/status_poller.js b/app/assets/javascripts/import_entities/import_groups/graphql/services/status_poller.js
index 41dd25b9150..886cf24081b 100644
--- a/app/assets/javascripts/import_entities/import_groups/graphql/services/status_poller.js
+++ b/app/assets/javascripts/import_entities/import_groups/graphql/services/status_poller.js
@@ -46,7 +46,10 @@ export class StatusPoller {
const { bulkImportSourceGroups } = this.client.readQuery({
query: bulkImportSourceGroupsQuery,
});
- const groupsInProgress = bulkImportSourceGroups.filter((g) => g.status === STATUSES.STARTED);
+
+ const groupsInProgress = bulkImportSourceGroups.nodes.filter(
+ (g) => g.status === STATUSES.STARTED,
+ );
if (groupsInProgress.length) {
const { data: results } = await this.client.query({
query: generateGroupsQuery(groupsInProgress),
diff --git a/app/assets/javascripts/import_entities/import_groups/index.js b/app/assets/javascripts/import_entities/import_groups/index.js
index bf427075564..1ce74bf4f60 100644
--- a/app/assets/javascripts/import_entities/import_groups/index.js
+++ b/app/assets/javascripts/import_entities/import_groups/index.js
@@ -10,7 +10,12 @@ Vue.use(VueApollo);
export function mountImportGroupsApp(mountElement) {
if (!mountElement) return undefined;
- const { statusPath, availableNamespacesPath, createBulkImportPath } = mountElement.dataset;
+ const {
+ statusPath,
+ availableNamespacesPath,
+ createBulkImportPath,
+ sourceUrl,
+ } = mountElement.dataset;
const apolloProvider = new VueApollo({
defaultClient: createApolloClient({
endpoints: {
@@ -25,7 +30,11 @@ export function mountImportGroupsApp(mountElement) {
el: mountElement,
apolloProvider,
render(createElement) {
- return createElement(ImportTable);
+ return createElement(ImportTable, {
+ props: {
+ sourceUrl,
+ },
+ });
},
});
}
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 a8217ff1033..a5d6fa175fc 100644
--- a/app/assets/javascripts/import_entities/import_projects/store/actions.js
+++ b/app/assets/javascripts/import_entities/import_projects/store/actions.js
@@ -1,6 +1,4 @@
import Visibility from 'visibilityjs';
-import * as types from './mutation_types';
-import { isProjectImportable } from '../utils';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import Poll from '~/lib/utils/poll';
import { visitUrl, objectToQuery } from '~/lib/utils/url_utility';
@@ -9,6 +7,8 @@ import { s__, sprintf } from '~/locale';
import axios from '~/lib/utils/axios_utils';
import httpStatusCodes from '~/lib/utils/http_status';
import { capitalizeFirstCharacter } from '~/lib/utils/text_utility';
+import { isProjectImportable } from '../utils';
+import * as types from './mutation_types';
let eTagPoll;
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 1a96508bd48..c5e1922597a 100644
--- a/app/assets/javascripts/import_entities/import_projects/store/mutations.js
+++ b/app/assets/javascripts/import_entities/import_projects/store/mutations.js
@@ -1,6 +1,6 @@
import Vue from 'vue';
-import * as types from './mutation_types';
import { STATUSES } from '../../constants';
+import * as types from './mutation_types';
const makeNewImportedProject = (importedProject) => ({
importSource: {
diff --git a/app/assets/javascripts/incidents/components/incidents_list.vue b/app/assets/javascripts/incidents/components/incidents_list.vue
index 7d44a28b4bb..b848f7adc9d 100644
--- a/app/assets/javascripts/incidents/components/incidents_list.vue
+++ b/app/assets/javascripts/incidents/components/incidents_list.vue
@@ -22,10 +22,10 @@ import {
import { convertToSnakeCase } from '~/lib/utils/text_utility';
import { s__ } from '~/locale';
import { visitUrl, mergeUrlParams, joinPaths } from '~/lib/utils/url_utility';
-import getIncidents from '../graphql/queries/get_incidents.query.graphql';
-import getIncidentsCountByStatus from '../graphql/queries/get_count_by_status.query.graphql';
import SeverityToken from '~/sidebar/components/severity/severity.vue';
import { INCIDENT_SEVERITY } from '~/sidebar/components/severity/constants';
+import getIncidents from '../graphql/queries/get_incidents.query.graphql';
+import getIncidentsCountByStatus from '../graphql/queries/get_count_by_status.query.graphql';
import {
I18N,
INCIDENT_STATUS_TABS,
@@ -102,7 +102,7 @@ export default {
GlIcon,
PublishedCell: () => import('ee_component/incidents/components/published_cell.vue'),
ServiceLevelAgreementCell: () =>
- import('ee_component/incidents/components/service_level_agreement_cell.vue'),
+ import('ee_component/vue_shared/components/incidents/service_level_agreement.vue'),
GlEmptyState,
SeverityToken,
PaginatedTableWithSearchAndTabs,
diff --git a/app/assets/javascripts/incidents_settings/components/incidents_settings_tabs.vue b/app/assets/javascripts/incidents_settings/components/incidents_settings_tabs.vue
index c90ff8079b8..9d5f37dc3b7 100644
--- a/app/assets/javascripts/incidents_settings/components/incidents_settings_tabs.vue
+++ b/app/assets/javascripts/incidents_settings/components/incidents_settings_tabs.vue
@@ -1,8 +1,8 @@
<script>
import { GlButton, GlTabs, GlTab } from '@gitlab/ui';
+import { INTEGRATION_TABS_CONFIG, I18N_INTEGRATION_TABS } from '../constants';
import AlertsSettingsForm from './alerts_form.vue';
import PagerDutySettingsForm from './pagerduty_form.vue';
-import { INTEGRATION_TABS_CONFIG, I18N_INTEGRATION_TABS } from '../constants';
export default {
components: {
@@ -26,7 +26,7 @@ export default {
class="settings no-animate qa-incident-management-settings"
>
<div class="settings-header">
- <h4 ref="sectionHeader" class="gl-my-3! gl-py-1">
+ <h4 ref="sectionHeader">
{{ $options.i18n.headerText }}
</h4>
<gl-button ref="toggleBtn" class="js-settings-toggle">{{
diff --git a/app/assets/javascripts/init_changes_dropdown.js b/app/assets/javascripts/init_changes_dropdown.js
index 22667d8ae88..b42264c870b 100644
--- a/app/assets/javascripts/init_changes_dropdown.js
+++ b/app/assets/javascripts/init_changes_dropdown.js
@@ -1,6 +1,6 @@
import $ from 'jquery';
-import { stickyMonitor } from './lib/utils/sticky';
import initDeprecatedJQueryDropdown from '~/deprecated_jquery_dropdown';
+import { stickyMonitor } from './lib/utils/sticky';
export default (stickyTop) => {
stickyMonitor(document.querySelector('.js-diff-files-changed'), stickyTop);
diff --git a/app/assets/javascripts/init_issuable_sidebar.js b/app/assets/javascripts/init_issuable_sidebar.js
index 1e82ecb05b5..63ef6a01844 100644
--- a/app/assets/javascripts/init_issuable_sidebar.js
+++ b/app/assets/javascripts/init_issuable_sidebar.js
@@ -1,11 +1,11 @@
/* eslint-disable no-new */
+import { mountSidebarLabels, getSidebarOptions } from '~/sidebar/mount_sidebar';
import MilestoneSelect from './milestone_select';
import LabelsSelect from './labels_select';
import IssuableContext from './issuable_context';
import Sidebar from './right_sidebar';
import DueDateSelectors from './due_date_select';
-import { mountSidebarLabels, getSidebarOptions } from '~/sidebar/mount_sidebar';
export default () => {
const sidebarOptEl = document.querySelector('.js-sidebar-options');
diff --git a/app/assets/javascripts/integrations/edit/components/dynamic_field.vue b/app/assets/javascripts/integrations/edit/components/dynamic_field.vue
index f568f7e6d3d..80815972006 100644
--- a/app/assets/javascripts/integrations/edit/components/dynamic_field.vue
+++ b/app/assets/javascripts/integrations/edit/components/dynamic_field.vue
@@ -3,8 +3,8 @@
import { mapGetters } from 'vuex';
import { capitalize, lowerCase, isEmpty } from 'lodash';
import { GlFormGroup, GlFormCheckbox, GlFormInput, GlFormSelect, GlFormTextarea } from '@gitlab/ui';
-import eventHub from '../event_hub';
import { __, sprintf } from '~/locale';
+import eventHub from '../event_hub';
export default {
name: 'DynamicField',
diff --git a/app/assets/javascripts/integrations/edit/components/integration_form.vue b/app/assets/javascripts/integrations/edit/components/integration_form.vue
index ac8a64d5f3b..e5c4368c32d 100644
--- a/app/assets/javascripts/integrations/edit/components/integration_form.vue
+++ b/app/assets/javascripts/integrations/edit/components/integration_form.vue
@@ -1,6 +1,6 @@
<script>
import { mapState, mapActions, mapGetters } from 'vuex';
-import { GlButton, GlModalDirective } from '@gitlab/ui';
+import { GlButton, GlModalDirective, GlSafeHtmlDirective as SafeHtml } from '@gitlab/ui';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import eventHub from '../event_hub';
import { integrationLevels } from '../constants';
@@ -28,9 +28,17 @@ export default {
GlButton,
},
directives: {
- 'gl-modal': GlModalDirective,
+ GlModal: GlModalDirective,
+ SafeHtml,
},
mixins: [glFeatureFlagsMixin()],
+ props: {
+ helpHtml: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ },
computed: {
...mapGetters(['currentKey', 'propsSource', 'isDisabled']),
...mapState([
@@ -80,11 +88,14 @@ export default {
this.fetchResetIntegration();
},
},
+ helpHtmlConfig: {
+ ADD_TAGS: ['use'], // to support icon SVGs
+ },
};
</script>
<template>
- <div>
+ <div class="gl-mb-3">
<override-dropdown
v-if="defaultState !== null"
:inherit-from-id="defaultState.id"
@@ -92,80 +103,92 @@ export default {
:learn-more-path="propsSource.learnMorePath"
@change="setOverride"
/>
- <active-checkbox v-if="propsSource.showActive" :key="`${currentKey}-active-checkbox`" />
- <jira-trigger-fields
- v-if="isJira"
- :key="`${currentKey}-jira-trigger-fields`"
- v-bind="propsSource.triggerFieldsProps"
- />
- <trigger-fields
- v-else-if="propsSource.triggerEvents.length"
- :key="`${currentKey}-trigger-fields`"
- :events="propsSource.triggerEvents"
- :type="propsSource.type"
- />
- <dynamic-field
- v-for="field in propsSource.fields"
- :key="`${currentKey}-${field.name}`"
- v-bind="field"
- />
- <jira-issues-fields
- v-if="showJiraIssuesFields"
- :key="`${currentKey}-jira-issues-fields`"
- v-bind="propsSource.jiraIssuesProps"
- />
- <div v-if="isEditable" class="footer-block row-content-block">
- <template v-if="isInstanceOrGroupLevel">
- <gl-button
- v-gl-modal.confirmSaveIntegration
- category="primary"
- variant="success"
- :loading="isSaving"
- :disabled="isDisabled"
- data-qa-selector="save_changes_button"
- >
- {{ __('Save changes') }}
- </gl-button>
- <confirmation-modal @submit="onSaveClick" />
- </template>
- <gl-button
- v-else
- category="primary"
- variant="success"
- type="submit"
- :loading="isSaving"
- :disabled="isDisabled"
- data-qa-selector="save_changes_button"
- @click.prevent="onSaveClick"
- >
- {{ __('Save changes') }}
- </gl-button>
- <gl-button
- v-if="propsSource.canTest"
- :loading="isTesting"
- :disabled="isDisabled"
- :href="propsSource.testPath"
- @click.prevent="onTestClick"
- >
- {{ __('Test settings') }}
- </gl-button>
+ <div class="row">
+ <div class="col-lg-4"></div>
+
+ <div class="col-lg-8">
+ <!-- helpHtml is trusted input -->
+ <div v-if="helpHtml" v-safe-html:[$options.helpHtmlConfig]="helpHtml"></div>
+
+ <active-checkbox v-if="propsSource.showActive" :key="`${currentKey}-active-checkbox`" />
+ <jira-trigger-fields
+ v-if="isJira"
+ :key="`${currentKey}-jira-trigger-fields`"
+ v-bind="propsSource.triggerFieldsProps"
+ />
+ <trigger-fields
+ v-else-if="propsSource.triggerEvents.length"
+ :key="`${currentKey}-trigger-fields`"
+ :events="propsSource.triggerEvents"
+ :type="propsSource.type"
+ />
+ <dynamic-field
+ v-for="field in propsSource.fields"
+ :key="`${currentKey}-${field.name}`"
+ v-bind="field"
+ />
+ <jira-issues-fields
+ v-if="showJiraIssuesFields"
+ :key="`${currentKey}-jira-issues-fields`"
+ v-bind="propsSource.jiraIssuesProps"
+ />
+ <div v-if="isEditable" class="footer-block row-content-block">
+ <template v-if="isInstanceOrGroupLevel">
+ <gl-button
+ v-gl-modal.confirmSaveIntegration
+ category="primary"
+ variant="success"
+ :loading="isSaving"
+ :disabled="isDisabled"
+ data-qa-selector="save_changes_button"
+ >
+ {{ __('Save changes') }}
+ </gl-button>
+ <confirmation-modal @submit="onSaveClick" />
+ </template>
+ <gl-button
+ v-else
+ category="primary"
+ variant="success"
+ type="submit"
+ :loading="isSaving"
+ :disabled="isDisabled"
+ data-qa-selector="save_changes_button"
+ @click.prevent="onSaveClick"
+ >
+ {{ __('Save changes') }}
+ </gl-button>
+
+ <gl-button
+ v-if="propsSource.canTest"
+ :loading="isTesting"
+ :disabled="isDisabled"
+ :href="propsSource.testPath"
+ @click.prevent="onTestClick"
+ >
+ {{ __('Test settings') }}
+ </gl-button>
- <template v-if="showReset">
- <gl-button
- v-gl-modal.confirmResetIntegration
- category="secondary"
- variant="default"
- :loading="isResetting"
- :disabled="isDisabled"
- data-testid="reset-button"
- >
- {{ __('Reset') }}
- </gl-button>
- <reset-confirmation-modal @reset="onResetClick" />
- </template>
+ <template v-if="showReset">
+ <gl-button
+ v-gl-modal.confirmResetIntegration
+ category="secondary"
+ variant="default"
+ :loading="isResetting"
+ :disabled="isDisabled"
+ data-testid="reset-button"
+ >
+ {{ __('Reset') }}
+ </gl-button>
+ <reset-confirmation-modal @reset="onResetClick" />
+ </template>
- <gl-button class="btn-cancel" :href="propsSource.cancelPath">{{ __('Cancel') }}</gl-button>
+ <gl-button class="btn-cancel" :href="propsSource.cancelPath">{{
+ __('Cancel')
+ }}</gl-button>
+ </div>
+ </div>
</div>
</div>
</template>
diff --git a/app/assets/javascripts/integrations/edit/components/jira_issues_fields.vue b/app/assets/javascripts/integrations/edit/components/jira_issues_fields.vue
index 1baa2b440b0..f8a44f0c2a2 100644
--- a/app/assets/javascripts/integrations/edit/components/jira_issues_fields.vue
+++ b/app/assets/javascripts/integrations/edit/components/jira_issues_fields.vue
@@ -8,6 +8,7 @@ import {
GlButton,
GlCard,
} from '@gitlab/ui';
+import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import eventHub from '../event_hub';
export default {
@@ -20,18 +21,36 @@ export default {
GlLink,
GlButton,
GlCard,
+ JiraIssueCreationVulnerabilities: () =>
+ import('ee_component/integrations/edit/components/jira_issue_creation_vulnerabilities.vue'),
},
+ mixins: [glFeatureFlagsMixin()],
props: {
showJiraIssuesIntegration: {
type: Boolean,
required: false,
default: false,
},
+ showJiraVulnerabilitiesIntegration: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
initialEnableJiraIssues: {
type: Boolean,
required: false,
default: null,
},
+ initialEnableJiraVulnerabilities: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ initialVulnerabilitiesIssuetype: {
+ type: String,
+ required: false,
+ default: '',
+ },
initialProjectKey: {
type: String,
required: false,
@@ -45,12 +64,12 @@ export default {
upgradePlanPath: {
type: String,
required: false,
- default: null,
+ default: '',
},
editProjectPath: {
type: String,
required: false,
- default: null,
+ default: '',
},
},
data() {
@@ -64,6 +83,13 @@ export default {
validProjectKey() {
return !this.enableJiraIssues || Boolean(this.projectKey) || !this.validated;
},
+ showJiraVulnerabilitiesOptions() {
+ return (
+ this.enableJiraIssues &&
+ this.showJiraVulnerabilitiesIntegration &&
+ this.glFeatures.jiraForVulnerabilities
+ );
+ },
},
created() {
eventHub.$on('validateForm', this.validateForm);
@@ -75,6 +101,9 @@ export default {
validateForm() {
this.validated = true;
},
+ getJiraIssueTypes() {
+ eventHub.$emit('getJiraIssueTypes');
+ },
},
};
</script>
@@ -105,6 +134,14 @@ export default {
}}
</template>
</gl-form-checkbox>
+ <jira-issue-creation-vulnerabilities
+ v-if="showJiraVulnerabilitiesOptions"
+ :project-key="projectKey"
+ :initial-is-enabled="initialEnableJiraVulnerabilities"
+ :initial-issue-type-id="initialVulnerabilitiesIssuetype"
+ data-testid="jira-for-vulnerabilities"
+ @request-get-issue-types="getJiraIssueTypes"
+ />
</template>
<gl-card v-else class="gl-mt-7">
<strong>{{ __('This is a Premium feature') }}</strong>
diff --git a/app/assets/javascripts/integrations/edit/index.js b/app/assets/javascripts/integrations/edit/index.js
index 95a53f1beab..deca11d07d9 100644
--- a/app/assets/javascripts/integrations/edit/index.js
+++ b/app/assets/javascripts/integrations/edit/index.js
@@ -1,6 +1,6 @@
import Vue from 'vue';
-import { createStore } from './store';
import { parseBoolean } from '~/lib/utils/common_utils';
+import { createStore } from './store';
import IntegrationForm from './components/integration_form.vue';
function parseBooleanInData(data) {
@@ -27,6 +27,7 @@ function parseDatasetToProps(data) {
cancelPath,
testPath,
resetPath,
+ vulnerabilitiesIssuetype,
...booleanAttributes
} = data;
const {
@@ -38,7 +39,9 @@ function parseDatasetToProps(data) {
mergeRequestEvents,
enableComments,
showJiraIssuesIntegration,
+ showJiraVulnerabilitiesIntegration,
enableJiraIssues,
+ enableJiraVulnerabilities,
gitlabIssuesEnabled,
} = parseBooleanInData(booleanAttributes);
@@ -59,7 +62,10 @@ function parseDatasetToProps(data) {
},
jiraIssuesProps: {
showJiraIssuesIntegration,
+ showJiraVulnerabilitiesIntegration,
initialEnableJiraIssues: enableJiraIssues,
+ initialEnableJiraVulnerabilities: enableJiraVulnerabilities,
+ initialVulnerabilitiesIssuetype: vulnerabilitiesIssuetype,
initialProjectKey: projectKey,
gitlabIssuesEnabled,
upgradePlanPath,
@@ -80,21 +86,29 @@ export default (el, defaultEl) => {
}
const props = parseDatasetToProps(el.dataset);
-
const initialState = {
defaultState: null,
customState: props,
};
-
if (defaultEl) {
initialState.defaultState = Object.freeze(parseDatasetToProps(defaultEl.dataset));
}
+ // Here, we capture the "helpHtml", so we can pass it to the Vue component
+ // to position it where ever it wants.
+ // Because this node is a _child_ of `el`, it will be removed when the Vue component is mounted,
+ // so we don't need to manually remove it.
+ const helpHtml = el.querySelector('.js-integration-help-html')?.innerHTML;
+
return new Vue({
el,
store: createStore(initialState),
render(createElement) {
- return createElement(IntegrationForm);
+ return createElement(IntegrationForm, {
+ props: {
+ helpHtml,
+ },
+ });
},
});
};
diff --git a/app/assets/javascripts/integrations/edit/store/actions.js b/app/assets/javascripts/integrations/edit/store/actions.js
index 421917b720a..400397c050c 100644
--- a/app/assets/javascripts/integrations/edit/store/actions.js
+++ b/app/assets/javascripts/integrations/edit/store/actions.js
@@ -26,3 +26,18 @@ export const fetchResetIntegration = ({ dispatch, getters }) => {
.then(() => dispatch('receiveResetIntegrationSuccess'))
.catch(() => dispatch('receiveResetIntegrationError'));
};
+
+export const requestJiraIssueTypes = ({ commit }) => {
+ commit(types.SET_JIRA_ISSUE_TYPES_ERROR_MESSAGE, '');
+ commit(types.SET_IS_LOADING_JIRA_ISSUE_TYPES, true);
+};
+export const receiveJiraIssueTypesSuccess = ({ commit }, issueTypes = []) => {
+ commit(types.SET_IS_LOADING_JIRA_ISSUE_TYPES, false);
+ commit(types.SET_JIRA_ISSUE_TYPES, issueTypes);
+};
+
+export const receiveJiraIssueTypesError = ({ commit }, errorMessage) => {
+ commit(types.SET_IS_LOADING_JIRA_ISSUE_TYPES, false);
+ commit(types.SET_JIRA_ISSUE_TYPES, []);
+ commit(types.SET_JIRA_ISSUE_TYPES_ERROR_MESSAGE, errorMessage);
+};
diff --git a/app/assets/javascripts/integrations/edit/store/mutation_types.js b/app/assets/javascripts/integrations/edit/store/mutation_types.js
index 54928148b22..c681056a515 100644
--- a/app/assets/javascripts/integrations/edit/store/mutation_types.js
+++ b/app/assets/javascripts/integrations/edit/store/mutation_types.js
@@ -3,5 +3,9 @@ export const SET_IS_SAVING = 'SET_IS_SAVING';
export const SET_IS_TESTING = 'SET_IS_TESTING';
export const SET_IS_RESETTING = 'SET_IS_RESETTING';
+export const SET_IS_LOADING_JIRA_ISSUE_TYPES = 'SET_IS_LOADING_JIRA_ISSUE_TYPES';
+export const SET_JIRA_ISSUE_TYPES_ERROR_MESSAGE = 'SET_JIRA_ISSUE_TYPES_ERROR_MESSAGE';
+export const SET_JIRA_ISSUE_TYPES = 'SET_JIRA_ISSUE_TYPES';
+
export const REQUEST_RESET_INTEGRATION = 'REQUEST_RESET_INTEGRATION';
export const RECEIVE_RESET_INTEGRATION_ERROR = 'RECEIVE_RESET_INTEGRATION_ERROR';
diff --git a/app/assets/javascripts/integrations/edit/store/mutations.js b/app/assets/javascripts/integrations/edit/store/mutations.js
index 826757e665b..279df1b9266 100644
--- a/app/assets/javascripts/integrations/edit/store/mutations.js
+++ b/app/assets/javascripts/integrations/edit/store/mutations.js
@@ -19,4 +19,13 @@ export default {
[types.RECEIVE_RESET_INTEGRATION_ERROR](state) {
state.isResetting = false;
},
+ [types.SET_JIRA_ISSUE_TYPES](state, jiraIssueTypes) {
+ state.jiraIssueTypes = jiraIssueTypes;
+ },
+ [types.SET_IS_LOADING_JIRA_ISSUE_TYPES](state, isLoadingJiraIssueTypes) {
+ state.isLoadingJiraIssueTypes = isLoadingJiraIssueTypes;
+ },
+ [types.SET_JIRA_ISSUE_TYPES_ERROR_MESSAGE](state, errorMessage) {
+ state.loadingJiraIssueTypesErrorMessage = errorMessage;
+ },
};
diff --git a/app/assets/javascripts/integrations/edit/store/state.js b/app/assets/javascripts/integrations/edit/store/state.js
index aae3db1583f..1c0b274e4ef 100644
--- a/app/assets/javascripts/integrations/edit/store/state.js
+++ b/app/assets/javascripts/integrations/edit/store/state.js
@@ -8,5 +8,8 @@ export default ({ defaultState = null, customState = {} } = {}) => {
isSaving: false,
isTesting: false,
isResetting: false,
+ isLoadingJiraIssueTypes: false,
+ loadingJiraIssueTypesErrorMessage: '',
+ jiraIssueTypes: [],
};
};
diff --git a/app/assets/javascripts/integrations/integration_settings_form.js b/app/assets/javascripts/integrations/integration_settings_form.js
index 861655a6a64..801cf3ed27e 100644
--- a/app/assets/javascripts/integrations/integration_settings_form.js
+++ b/app/assets/javascripts/integrations/integration_settings_form.js
@@ -1,8 +1,8 @@
import $ from 'jquery';
import { delay } from 'lodash';
-import axios from '../lib/utils/axios_utils';
import { __, s__ } from '~/locale';
import toast from '~/vue_shared/plugins/global_toast';
+import axios from '../lib/utils/axios_utils';
import initForm from './edit';
import eventHub from './edit/event_hub';
@@ -33,6 +33,12 @@ export default class IntegrationSettingsForm {
eventHub.$on('saveIntegration', () => {
this.saveIntegration();
});
+ eventHub.$on('getJiraIssueTypes', () => {
+ // eslint-disable-next-line no-jquery/no-serialize
+ this.getJiraIssueTypes(this.$form.serialize());
+ });
+
+ eventHub.$emit('formInitialized');
}
saveIntegration() {
@@ -80,15 +86,58 @@ export default class IntegrationSettingsForm {
}
/**
+ * Get a list of Jira issue types for the currently configured project
+ *
+ * @param {string} formData - URL encoded string containing the form data
+ *
+ * @return {Promise}
+ */
+ getJiraIssueTypes(formData) {
+ const {
+ $store: { dispatch },
+ } = this.vue;
+
+ dispatch('requestJiraIssueTypes');
+
+ return this.fetchTestSettings(formData)
+ .then(
+ ({
+ data: {
+ issuetypes,
+ error,
+ message = s__('Integrations|Connection failed. Please check your settings.'),
+ },
+ }) => {
+ if (error || !issuetypes?.length) {
+ eventHub.$emit('validateForm');
+ throw new Error(message);
+ }
+
+ dispatch('receiveJiraIssueTypesSuccess', issuetypes);
+ },
+ )
+ .catch(({ message = __('Something went wrong on our end.') }) => {
+ dispatch('receiveJiraIssueTypesError', message);
+ });
+ }
+
+ /**
+ * Send request to the test endpoint which checks if the current config is valid
+ */
+ fetchTestSettings(formData) {
+ return axios.put(this.testEndPoint, formData);
+ }
+
+ /**
* Test Integration config
*/
testSettings(formData) {
- return axios
- .put(this.testEndPoint, formData)
+ return this.fetchTestSettings(formData)
.then(({ data }) => {
if (data.error) {
toast(`${data.message} ${data.service_response}`);
} else {
+ this.vue.$store.dispatch('receiveJiraIssueTypesSuccess', data.issuetypes);
toast(s__('Integrations|Connection successful.'));
}
})
diff --git a/app/assets/javascripts/invite_member/components/invite_member_modal.vue b/app/assets/javascripts/invite_member/components/invite_member_modal.vue
index 3df99bccdb0..ce79e1b58d2 100644
--- a/app/assets/javascripts/invite_member/components/invite_member_modal.vue
+++ b/app/assets/javascripts/invite_member/components/invite_member_modal.vue
@@ -1,7 +1,8 @@
<script>
import { GlModal, GlLink } from '@gitlab/ui';
-import eventHub from '../event_hub';
import { s__, __ } from '~/locale';
+import { BV_SHOW_MODAL } from '~/lib/utils/constants';
+import eventHub from '../event_hub';
import { OPEN_MODAL, MODAL_ID } from '../constants';
export default {
@@ -38,7 +39,7 @@ export default {
},
methods: {
openModal() {
- this.$root.$emit('bv::show::modal', MODAL_ID);
+ this.$root.$emit(BV_SHOW_MODAL, MODAL_ID);
},
},
};
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 a92289ca8c1..e70bbfe152d 100644
--- a/app/assets/javascripts/invite_members/components/invite_members_modal.vue
+++ b/app/assets/javascripts/invite_members/components/invite_members_modal.vue
@@ -10,10 +10,11 @@ import {
GlFormInput,
} from '@gitlab/ui';
import { partition, isString } from 'lodash';
-import eventHub from '../event_hub';
import { s__, __, sprintf } from '~/locale';
import Api from '~/api';
import MembersTokenSelect from '~/invite_members/components/members_token_select.vue';
+import { BV_SHOW_MODAL, BV_HIDE_MODAL } from '~/lib/utils/constants';
+import eventHub from '../event_hub';
export default {
name: 'InviteMembersModal',
@@ -113,10 +114,10 @@ export default {
];
},
openModal() {
- this.$root.$emit('bv::show::modal', this.modalId);
+ this.$root.$emit(BV_SHOW_MODAL, this.modalId);
},
closeModal() {
- this.$root.$emit('bv::hide::modal', this.modalId);
+ this.$root.$emit(BV_HIDE_MODAL, this.modalId);
},
sendInvite() {
this.submitForm();
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 627d4ab2771..ae27aaddb62 100644
--- a/app/assets/javascripts/invite_members/components/members_token_select.vue
+++ b/app/assets/javascripts/invite_members/components/members_token_select.vue
@@ -2,8 +2,8 @@
import { debounce } from 'lodash';
import { GlTokenSelector, GlAvatar, GlAvatarLabeled, GlSprintf } from '@gitlab/ui';
import { __ } from '~/locale';
-import { USER_SEARCH_DELAY } from '../constants';
import { getUsers } from '~/rest_api';
+import { USER_SEARCH_DELAY } from '../constants';
export default {
components: {
diff --git a/app/assets/javascripts/issuable_bulk_update_actions.js b/app/assets/javascripts/issuable_bulk_update_actions.js
index 8bb76edbd47..b9e8c015d19 100644
--- a/app/assets/javascripts/issuable_bulk_update_actions.js
+++ b/app/assets/javascripts/issuable_bulk_update_actions.js
@@ -50,6 +50,7 @@ export default {
subscription_event: this.form.find('input[name="update[subscription_event]"]').val(),
health_status: this.form.find('input[name="update[health_status]"]').val(),
epic_id: this.form.find('input[name="update[epic_id]"]').val(),
+ sprint_id: this.form.find('input[name="update[iteration_id]"]').val(),
add_label_ids: [],
remove_label_ids: [],
},
diff --git a/app/assets/javascripts/issuable_bulk_update_sidebar.js b/app/assets/javascripts/issuable_bulk_update_sidebar.js
index b9daa16874a..d6fa0f42bd4 100644
--- a/app/assets/javascripts/issuable_bulk_update_sidebar.js
+++ b/app/assets/javascripts/issuable_bulk_update_sidebar.js
@@ -79,6 +79,16 @@ export default class IssuableBulkUpdateSidebar {
})
.catch(() => {});
}
+
+ if (IS_EE) {
+ import('ee/vue_shared/components/sidebar/iterations_dropdown_bundle')
+ .then(({ default: iterationsDropdown }) => {
+ iterationsDropdown();
+ })
+ .catch((e) => {
+ throw e;
+ });
+ }
}
setupBulkUpdateActions() {
diff --git a/app/assets/javascripts/issuable_list/components/issuable_item.vue b/app/assets/javascripts/issuable_list/components/issuable_item.vue
index 583e5cb703d..df303665538 100644
--- a/app/assets/javascripts/issuable_list/components/issuable_item.vue
+++ b/app/assets/javascripts/issuable_list/components/issuable_item.vue
@@ -139,6 +139,12 @@ export default {
<div class="issuable-main-info">
<div data-testid="issuable-title" class="issue-title title">
<span class="issue-title-text" dir="auto">
+ <gl-icon
+ v-if="issuable.confidential"
+ v-gl-tooltip
+ name="eye-slash"
+ :title="__('Confidential')"
+ />
<gl-link :href="issuable.webUrl" v-bind="issuableTitleProps"
>{{ issuable.title
}}<gl-icon v-if="isIssuableUrlExternal" name="external-link" class="gl-ml-2"
@@ -196,7 +202,7 @@ export default {
<li
v-if="showDiscussions"
data-testid="issuable-discussions"
- class="issuable-comments gl-display-none gl-display-sm-block"
+ class="issuable-comments gl-display-none gl-sm-display-block"
>
<gl-link
v-gl-tooltip:tooltipcontainer.top
diff --git a/app/assets/javascripts/issuable_list/components/issuable_list_root.vue b/app/assets/javascripts/issuable_list/components/issuable_list_root.vue
index c5475a34d3c..8a4c487f119 100644
--- a/app/assets/javascripts/issuable_list/components/issuable_list_root.vue
+++ b/app/assets/javascripts/issuable_list/components/issuable_list_root.vue
@@ -5,12 +5,11 @@ import { uniqueId } from 'lodash';
import { updateHistory, setUrlParams } from '~/lib/utils/url_utility';
import FilteredSearchBar from '~/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue';
+import { DEFAULT_SKELETON_COUNT } from '../constants';
import IssuableTabs from './issuable_tabs.vue';
import IssuableItem from './issuable_item.vue';
import IssuableBulkEditSidebar from './issuable_bulk_edit_sidebar.vue';
-import { DEFAULT_SKELETON_COUNT } from '../constants';
-
export default {
components: {
GlSkeletonLoading,
@@ -230,7 +229,7 @@ export default {
:initial-sort-by="initialSortBy"
:show-checkbox="showBulkEditSidebar"
:checkbox-checked="allIssuablesChecked"
- class="gl-flex-grow-1 row-content-block"
+ class="gl-flex-grow-1 gl-border-t-none row-content-block"
@checked-input="handleAllIssuablesCheckedInput"
@onFilter="$emit('filter', $event)"
@onSort="$emit('sort', $event)"
diff --git a/app/assets/javascripts/issuable_list/components/issuable_tabs.vue b/app/assets/javascripts/issuable_list/components/issuable_tabs.vue
index d9aab004077..57da030e22e 100644
--- a/app/assets/javascripts/issuable_list/components/issuable_tabs.vue
+++ b/app/assets/javascripts/issuable_list/components/issuable_tabs.vue
@@ -32,7 +32,10 @@ export default {
<template>
<div class="top-area">
- <gl-tabs class="nav-links mobile-separator issuable-state-filters">
+ <gl-tabs
+ class="gl-display-flex gl-flex-fill-1 gl-p-0 gl-m-0 mobile-separator issuable-state-filters"
+ nav-class="gl-border-b-0"
+ >
<gl-tab
v-for="tab in tabs"
:key="tab.id"
@@ -41,7 +44,7 @@ export default {
>
<template #title>
<span :title="tab.titleTooltip">{{ tab.title }}</span>
- <gl-badge v-if="tabCounts" variant="neutral" size="sm" class="gl-px-2 gl-py-1!">{{
+ <gl-badge v-if="tabCounts" variant="neutral" size="sm" class="gl-tab-counter-badge">{{
tabCounts[tab.name]
}}</gl-badge>
</template>
diff --git a/app/assets/javascripts/issuable_show/components/issuable_header.vue b/app/assets/javascripts/issuable_show/components/issuable_header.vue
index 5404753631d..4c6df31a0f3 100644
--- a/app/assets/javascripts/issuable_show/components/issuable_header.vue
+++ b/app/assets/javascripts/issuable_show/components/issuable_header.vue
@@ -112,7 +112,7 @@ export default {
</div>
<div
data-testid="header-actions"
- class="detail-page-header-actions gl-display-flex gl-display-md-block"
+ class="detail-page-header-actions gl-display-flex gl-md-display-block"
>
<slot name="header-actions"></slot>
</div>
diff --git a/app/assets/javascripts/issuable_suggestions/components/app.vue b/app/assets/javascripts/issuable_suggestions/components/app.vue
index ac5f04147d3..d0642b64e7e 100644
--- a/app/assets/javascripts/issuable_suggestions/components/app.vue
+++ b/app/assets/javascripts/issuable_suggestions/components/app.vue
@@ -1,8 +1,8 @@
<script>
import { GlTooltipDirective, GlIcon } from '@gitlab/ui';
import { __ } from '~/locale';
-import Suggestion from './item.vue';
import query from '../queries/issues.query.graphql';
+import Suggestion from './item.vue';
export default {
components: {
diff --git a/app/assets/javascripts/issue.js b/app/assets/javascripts/issue.js
index 91912c684ad..576bd1f93ed 100644
--- a/app/assets/javascripts/issue.js
+++ b/app/assets/javascripts/issue.js
@@ -1,9 +1,9 @@
import $ from 'jquery';
+import { joinPaths } from '~/lib/utils/url_utility';
import axios from './lib/utils/axios_utils';
import { addDelimiter } from './lib/utils/text_utility';
import { deprecatedCreateFlash as flash } from './flash';
import CreateMergeRequestDropdown from './create_merge_request_dropdown';
-import { joinPaths } from '~/lib/utils/url_utility';
import { __ } from './locale';
export default class Issue {
diff --git a/app/assets/javascripts/issue_show/components/app.vue b/app/assets/javascripts/issue_show/components/app.vue
index d569ad573a2..1720fdf8eec 100644
--- a/app/assets/javascripts/issue_show/components/app.vue
+++ b/app/assets/javascripts/issue_show/components/app.vue
@@ -5,16 +5,16 @@ import { __, s__, sprintf } from '~/locale';
import { deprecatedCreateFlash as createFlash } from '~/flash';
import { visitUrl } from '~/lib/utils/url_utility';
import Poll from '~/lib/utils/poll';
+import recaptchaModalImplementor from '~/vue_shared/mixins/recaptcha_modal_implementor';
import eventHub from '../event_hub';
import Service from '../services/index';
import Store from '../stores';
+import { IssuableStatus, IssuableStatusText, IssuableType } from '../constants';
import titleComponent from './title.vue';
import descriptionComponent from './description.vue';
import editedComponent from './edited.vue';
import formComponent from './form.vue';
import PinnedLinks from './pinned_links.vue';
-import recaptchaModalImplementor from '~/vue_shared/mixins/recaptcha_modal_implementor';
-import { IssuableStatus, IssuableStatusText, IssuableType } from '../constants';
export default {
components: {
diff --git a/app/assets/javascripts/issue_show/components/description.vue b/app/assets/javascripts/issue_show/components/description.vue
index 2a6468c783b..ec454ef9e8d 100644
--- a/app/assets/javascripts/issue_show/components/description.vue
+++ b/app/assets/javascripts/issue_show/components/description.vue
@@ -154,6 +154,7 @@ export default {
}"
class="md"
></div>
+ <!-- eslint-disable vue/no-mutating-props -->
<textarea
v-if="descriptionText"
ref="textarea"
@@ -163,6 +164,7 @@ export default {
dir="auto"
>
</textarea>
+ <!-- eslint-enable vue/no-mutating-props -->
<recaptcha-modal v-show="showRecaptcha" :html="recaptchaHTML" @close="closeRecaptcha" />
</div>
diff --git a/app/assets/javascripts/issue_show/components/fields/description.vue b/app/assets/javascripts/issue_show/components/fields/description.vue
index 8d417e32d62..5476a1ef897 100644
--- a/app/assets/javascripts/issue_show/components/fields/description.vue
+++ b/app/assets/javascripts/issue_show/components/fields/description.vue
@@ -1,7 +1,7 @@
<script>
-import updateMixin from '../../mixins/update';
import markdownField from '~/vue_shared/components/markdown/field.vue';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
+import updateMixin from '../../mixins/update';
export default {
components: {
@@ -49,6 +49,7 @@ export default {
:textarea-value="formState.description"
>
<template #textarea>
+ <!-- eslint-disable vue/no-mutating-props -->
<textarea
id="issue-description"
ref="textarea"
@@ -62,6 +63,7 @@ export default {
@keydown.ctrl.enter="updateIssuable"
>
</textarea>
+ <!-- eslint-enable vue/no-mutating-props -->
</template>
</markdown-field>
</div>
diff --git a/app/assets/javascripts/issue_show/components/fields/description_template.vue b/app/assets/javascripts/issue_show/components/fields/description_template.vue
index 71299381aae..a3669cd40bd 100644
--- a/app/assets/javascripts/issue_show/components/fields/description_template.vue
+++ b/app/assets/javascripts/issue_show/components/fields/description_template.vue
@@ -35,6 +35,7 @@ export default {
// Create the editor for the template
const editor = document.querySelector('.detail-page-description .note-textarea') || {};
editor.setValue = (val) => {
+ // eslint-disable-next-line vue/no-mutating-props
this.formState.description = val;
};
editor.getValue = () => this.formState.description;
diff --git a/app/assets/javascripts/issue_show/components/fields/title.vue b/app/assets/javascripts/issue_show/components/fields/title.vue
index 34eb0451d53..d331fb47077 100644
--- a/app/assets/javascripts/issue_show/components/fields/title.vue
+++ b/app/assets/javascripts/issue_show/components/fields/title.vue
@@ -15,6 +15,7 @@ export default {
<template>
<fieldset>
<label class="sr-only" for="issuable-title">{{ __('Title') }}</label>
+ <!-- eslint-disable vue/no-mutating-props -->
<input
id="issuable-title"
ref="input"
@@ -27,5 +28,6 @@ export default {
@keydown.meta.enter="updateIssuable"
@keydown.ctrl.enter="updateIssuable"
/>
+ <!-- eslint-enable vue/no-mutating-props -->
</fieldset>
</template>
diff --git a/app/assets/javascripts/issue_show/components/form.vue b/app/assets/javascripts/issue_show/components/form.vue
index d48bf1fe7a9..2688089fa89 100644
--- a/app/assets/javascripts/issue_show/components/form.vue
+++ b/app/assets/javascripts/issue_show/components/form.vue
@@ -1,12 +1,12 @@
<script>
import $ from 'jquery';
+import Autosave from '~/autosave';
+import eventHub from '../event_hub';
import lockedWarning from './locked_warning.vue';
import titleField from './fields/title.vue';
import descriptionField from './fields/description.vue';
import editActions from './edit_actions.vue';
import descriptionTemplate from './fields/description_template.vue';
-import Autosave from '~/autosave';
-import eventHub from '../event_hub';
export default {
components: {
diff --git a/app/assets/javascripts/issue_show/components/header_actions.vue b/app/assets/javascripts/issue_show/components/header_actions.vue
index 998f740be0e..9c3988d0469 100644
--- a/app/assets/javascripts/issue_show/components/header_actions.vue
+++ b/app/assets/javascripts/issue_show/components/header_actions.vue
@@ -193,7 +193,7 @@ export default {
<template>
<div class="detail-page-header-actions">
<gl-dropdown
- class="gl-display-block gl-display-sm-none!"
+ class="gl-display-block gl-sm-display-none!"
block
:text="dropdownText"
:loading="isToggleStateButtonLoading"
@@ -222,7 +222,7 @@ export default {
<gl-button
v-if="showToggleIssueStateButton"
- class="gl-display-none gl-display-sm-inline-flex!"
+ class="gl-display-none gl-sm-display-inline-flex!"
category="secondary"
:data-qa-selector="qaSelector"
:loading="isToggleStateButtonLoading"
@@ -233,7 +233,7 @@ export default {
</gl-button>
<gl-dropdown
- class="gl-display-none gl-display-sm-inline-flex!"
+ class="gl-display-none gl-sm-display-inline-flex!"
toggle-class="gl-border-0! gl-shadow-none!"
no-caret
right
diff --git a/app/assets/javascripts/issue_show/components/incidents/incident_tabs.vue b/app/assets/javascripts/issue_show/components/incidents/incident_tabs.vue
index f9f06c3ad5a..9fc90b501eb 100644
--- a/app/assets/javascripts/issue_show/components/incidents/incident_tabs.vue
+++ b/app/assets/javascripts/issue_show/components/incidents/incident_tabs.vue
@@ -1,13 +1,13 @@
<script>
import { GlTab, GlTabs } from '@gitlab/ui';
-import DescriptionComponent from '../description.vue';
-import HighlightBar from './highlight_bar.vue';
import createFlash from '~/flash';
import { s__ } from '~/locale';
import AlertDetailsTable from '~/vue_shared/components/alert_details_table.vue';
import Tracking from '~/tracking';
-import getAlert from './graphql/queries/get_alert.graphql';
import { trackIncidentDetailsViewsOptions } from '~/incidents/constants';
+import DescriptionComponent from '../description.vue';
+import getAlert from './graphql/queries/get_alert.graphql';
+import HighlightBar from './highlight_bar.vue';
export default {
components: {
diff --git a/app/assets/javascripts/issue_show/incident.js b/app/assets/javascripts/issue_show/incident.js
index ccac38811b5..0c81ecdc843 100644
--- a/app/assets/javascripts/issue_show/incident.js
+++ b/app/assets/javascripts/issue_show/incident.js
@@ -1,9 +1,9 @@
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import createDefaultClient from '~/lib/graphql';
+import { parseBoolean } from '~/lib/utils/common_utils';
import issuableApp from './components/app.vue';
import incidentTabs from './components/incidents/incident_tabs.vue';
-import { parseBoolean } from '~/lib/utils/common_utils';
Vue.use(VueApollo);
diff --git a/app/assets/javascripts/issue_status_select.js b/app/assets/javascripts/issue_status_select.js
index 02b10730153..2ede0837930 100644
--- a/app/assets/javascripts/issue_status_select.js
+++ b/app/assets/javascripts/issue_status_select.js
@@ -1,6 +1,6 @@
import $ from 'jquery';
-import { __ } from './locale';
import initDeprecatedJQueryDropdown from '~/deprecated_jquery_dropdown';
+import { __ } from './locale';
export default function issueStatusSelect() {
$('.js-issue-status').each((i, el) => {
diff --git a/app/assets/javascripts/issues_list/components/issuable.vue b/app/assets/javascripts/issues_list/components/issuable.vue
index 3965fd6b0c7..6cadcd2043b 100644
--- a/app/assets/javascripts/issues_list/components/issuable.vue
+++ b/app/assets/javascripts/issues_list/components/issuable.vue
@@ -414,7 +414,7 @@ export default {
v-if="meta.visible"
:key="meta.key"
v-gl-tooltip
- class="gl-display-none gl-display-sm-flex gl-align-items-center gl-ml-3"
+ class="gl-display-none gl-sm-display-flex gl-align-items-center gl-ml-3"
:class="meta.class"
:data-testid="meta.dataTestId"
:title="meta.title"
diff --git a/app/assets/javascripts/issues_list/components/issuables_list_app.vue b/app/assets/javascripts/issues_list/components/issuables_list_app.vue
index eda8bc2b61f..225d56c11f0 100644
--- a/app/assets/javascripts/issues_list/components/issuables_list_app.vue
+++ b/app/assets/javascripts/issues_list/components/issuables_list_app.vue
@@ -16,8 +16,8 @@ import {
} from '~/lib/utils/common_utils';
import { __ } from '~/locale';
import initManualOrdering from '~/manual_ordering';
-import Issuable from './issuable.vue';
import FilteredSearchBar from '~/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue';
+import { setUrlParams } from '~/lib/utils/url_utility';
import {
sortOrderMap,
availableSortOptionsJira,
@@ -26,9 +26,9 @@ import {
PAGE_SIZE_MANUAL,
LOADING_LIST_ITEMS_LENGTH,
} from '../constants';
-import { setUrlParams } from '~/lib/utils/url_utility';
import issueableEventHub from '../eventhub';
import { emptyStateHelper } from '../service_desk_helper';
+import Issuable from './issuable.vue';
export default {
LOADING_LIST_ITEMS_LENGTH,
diff --git a/app/assets/javascripts/issues_list/components/jira_issues_list_root.vue b/app/assets/javascripts/issues_list/components/jira_issues_list_root.vue
index 61781c576c0..534f478cb7e 100644
--- a/app/assets/javascripts/issues_list/components/jira_issues_list_root.vue
+++ b/app/assets/javascripts/issues_list/components/jira_issues_list_root.vue
@@ -2,13 +2,13 @@
import { GlAlert, GlLabel } from '@gitlab/ui';
import { last } from 'lodash';
import { n__ } from '~/locale';
-import getIssuesListDetailsQuery from '../queries/get_issues_list_details.query.graphql';
import {
calculateJiraImportLabel,
isInProgress,
setFinishedAlertHideMap,
shouldShowFinishedAlert,
} from '~/jira_import/utils/jira_import_utils';
+import getIssuesListDetailsQuery from '../queries/get_issues_list_details.query.graphql';
export default {
name: 'JiraIssuesList',
diff --git a/app/assets/javascripts/jira_connect/api.js b/app/assets/javascripts/jira_connect/api.js
index d689a2d1962..d78aba0a3f7 100644
--- a/app/assets/javascripts/jira_connect/api.js
+++ b/app/assets/javascripts/jira_connect/api.js
@@ -1,7 +1,23 @@
import axios from 'axios';
-const getJwt = async () => {
- return AP.context.getToken();
+export const getJwt = () => {
+ return new Promise((resolve) => {
+ AP.context.getToken((token) => {
+ resolve(token);
+ });
+ });
+};
+
+export const getLocation = () => {
+ return new Promise((resolve) => {
+ if (typeof AP.getLocation !== 'function') {
+ resolve();
+ }
+
+ AP.getLocation((location) => {
+ resolve(location);
+ });
+ });
};
export const addSubscription = async (addPath, namespace) => {
diff --git a/app/assets/javascripts/jira_connect/components/app.vue b/app/assets/javascripts/jira_connect/components/app.vue
index f5bf30f4488..ed01c188752 100644
--- a/app/assets/javascripts/jira_connect/components/app.vue
+++ b/app/assets/javascripts/jira_connect/components/app.vue
@@ -3,6 +3,8 @@ import { mapState } from 'vuex';
import { GlAlert, GlButton, GlModal, GlModalDirective } from '@gitlab/ui';
import { __ } from '~/locale';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
+
+import { getLocation } from '~/jira_connect/api';
import GroupsList from './groups_list.vue';
export default {
@@ -17,17 +19,42 @@ export default {
GlModalDirective,
},
mixins: [glFeatureFlagsMixin()],
+ inject: {
+ usersPath: {
+ default: '',
+ },
+ },
+ data() {
+ return {
+ location: '',
+ };
+ },
computed: {
...mapState(['errorMessage']),
showNewUI() {
return this.glFeatures.newJiraConnectUi;
},
+ usersPathWithReturnTo() {
+ if (this.location) {
+ return `${this.usersPath}?return_to=${this.location}`;
+ }
+
+ return this.usersPath;
+ },
},
modal: {
cancelProps: {
text: __('Cancel'),
},
},
+ created() {
+ this.setLocation();
+ },
+ methods: {
+ async setLocation() {
+ this.location = await getLocation();
+ },
+ },
};
</script>
@@ -37,27 +64,40 @@ export default {
{{ errorMessage }}
</gl-alert>
- <h1>GitLab for Jira Configuration</h1>
+ <h2>{{ s__('JiraService|GitLab for Jira Configuration') }}</h2>
<div
v-if="showNewUI"
- class="gl-display-flex gl-justify-content-space-between gl-my-5 gl-pb-4 gl-border-b-solid gl-border-b-1 gl-border-b-gray-200"
+ class="gl-display-flex gl-justify-content-space-between gl-my-7 gl-pb-4 gl-border-b-solid gl-border-b-1 gl-border-b-gray-200"
>
- <h3 data-testid="new-jira-connect-ui-heading">{{ s__('Integrations|Linked namespaces') }}</h3>
+ <h5 class="gl-align-self-center gl-mb-0" data-testid="new-jira-connect-ui-heading">
+ {{ s__('Integrations|Linked namespaces') }}
+ </h5>
<gl-button
- v-gl-modal-directive="'add-namespace-modal'"
+ v-if="usersPath"
category="primary"
variant="info"
class="gl-align-self-center"
- >{{ s__('Integrations|Add namespace') }}</gl-button
- >
- <gl-modal
- modal-id="add-namespace-modal"
- :title="s__('Integrations|Link namespaces')"
- :action-cancel="$options.modal.cancelProps"
+ :href="usersPathWithReturnTo"
+ target="_blank"
+ >{{ s__('Integrations|Sign in to add namespaces') }}</gl-button
>
- <groups-list />
- </gl-modal>
+ <template v-else>
+ <gl-button
+ v-gl-modal-directive="'add-namespace-modal'"
+ category="primary"
+ variant="info"
+ class="gl-align-self-center"
+ >{{ s__('Integrations|Add namespace') }}</gl-button
+ >
+ <gl-modal
+ modal-id="add-namespace-modal"
+ :title="s__('Integrations|Link namespaces')"
+ :action-cancel="$options.modal.cancelProps"
+ >
+ <groups-list />
+ </gl-modal>
+ </template>
</div>
</div>
</template>
diff --git a/app/assets/javascripts/jira_connect/components/groups_list.vue b/app/assets/javascripts/jira_connect/components/groups_list.vue
index eeddd32addc..8671ecaa78a 100644
--- a/app/assets/javascripts/jira_connect/components/groups_list.vue
+++ b/app/assets/javascripts/jira_connect/components/groups_list.vue
@@ -1,5 +1,5 @@
<script>
-import { GlTabs, GlTab, GlLoadingIcon, GlPagination } from '@gitlab/ui';
+import { GlTabs, GlTab, GlLoadingIcon, GlPagination, GlAlert } from '@gitlab/ui';
import { s__ } from '~/locale';
import { parseIntPagination, normalizeHeaders } from '~/lib/utils/common_utils';
import { fetchGroups } from '~/jira_connect/api';
@@ -12,6 +12,7 @@ export default {
GlTab,
GlLoadingIcon,
GlPagination,
+ GlAlert,
GroupsListItem,
},
inject: {
@@ -26,6 +27,7 @@ export default {
page: 1,
perPage: defaultPerPage,
totalItems: 0,
+ errorMessage: null,
};
},
mounted() {
@@ -46,8 +48,7 @@ export default {
this.groups = response.data;
})
.catch(() => {
- // eslint-disable-next-line no-alert
- alert(s__('Integrations|Failed to load namespaces. Please try again.'));
+ this.errorMessage = s__('Integrations|Failed to load namespaces. Please try again.');
})
.finally(() => {
this.isLoading = false;
@@ -58,31 +59,42 @@ export default {
</script>
<template>
- <gl-tabs>
- <gl-tab :title="__('Groups and subgroups')" class="gl-pt-3">
- <gl-loading-icon v-if="isLoading" size="md" />
- <div v-else-if="groups.length === 0" class="gl-text-center">
- <h5>{{ s__('Integrations|No available namespaces.') }}</h5>
- <p class="gl-mt-5">
- {{
- s__('Integrations|You must have owner or maintainer permissions to link namespaces.')
- }}
- </p>
- </div>
- <ul v-else class="gl-list-style-none gl-pl-0">
- <groups-list-item v-for="group in groups" :key="group.id" :group="group" />
- </ul>
+ <div>
+ <gl-alert v-if="errorMessage" class="gl-mb-6" variant="danger" @dismiss="errorMessage = null">
+ {{ errorMessage }}
+ </gl-alert>
- <div class="gl-display-flex gl-justify-content-center gl-mt-5">
- <gl-pagination
- v-if="totalItems > perPage && groups.length > 0"
- v-model="page"
- class="gl-mb-0"
- :per-page="perPage"
- :total-items="totalItems"
- @input="loadGroups"
- />
- </div>
- </gl-tab>
- </gl-tabs>
+ <gl-tabs>
+ <gl-tab :title="__('Groups and subgroups')" class="gl-pt-3">
+ <gl-loading-icon v-if="isLoading" size="md" />
+ <div v-else-if="groups.length === 0" class="gl-text-center">
+ <h5>{{ s__('Integrations|No available namespaces.') }}</h5>
+ <p class="gl-mt-5">
+ {{
+ s__('Integrations|You must have owner or maintainer permissions to link namespaces.')
+ }}
+ </p>
+ </div>
+ <ul v-else class="gl-list-style-none gl-pl-0">
+ <groups-list-item
+ v-for="group in groups"
+ :key="group.id"
+ :group="group"
+ @error="errorMessage = $event"
+ />
+ </ul>
+
+ <div class="gl-display-flex gl-justify-content-center gl-mt-5">
+ <gl-pagination
+ v-if="totalItems > perPage && groups.length > 0"
+ v-model="page"
+ class="gl-mb-0"
+ :per-page="perPage"
+ :total-items="totalItems"
+ @input="loadGroups"
+ />
+ </div>
+ </gl-tab>
+ </gl-tabs>
+ </div>
</template>
diff --git a/app/assets/javascripts/jira_connect/components/groups_list_item.vue b/app/assets/javascripts/jira_connect/components/groups_list_item.vue
index 15e37ab3cb0..305f440707e 100644
--- a/app/assets/javascripts/jira_connect/components/groups_list_item.vue
+++ b/app/assets/javascripts/jira_connect/components/groups_list_item.vue
@@ -1,10 +1,19 @@
<script>
-import { GlIcon, GlAvatar } from '@gitlab/ui';
+import { GlAvatar, GlButton, GlIcon } from '@gitlab/ui';
+import { s__ } from '~/locale';
+
+import { addSubscription } from '~/jira_connect/api';
export default {
components: {
- GlIcon,
GlAvatar,
+ GlButton,
+ GlIcon,
+ },
+ inject: {
+ subscriptionsPath: {
+ default: '',
+ },
},
props: {
group: {
@@ -12,6 +21,31 @@ export default {
required: true,
},
},
+ data() {
+ return {
+ isLoading: false,
+ };
+ },
+ methods: {
+ onClick() {
+ this.isLoading = true;
+
+ addSubscription(this.subscriptionsPath, this.group.full_path)
+ .then(() => {
+ AP.navigator.reload();
+ })
+ .catch((error) => {
+ this.$emit(
+ 'error',
+ error?.response?.data?.error ||
+ s__('Integrations|Failed to link namespace. Please try again.'),
+ );
+ })
+ .finally(() => {
+ this.isLoading = false;
+ });
+ },
+ },
};
</script>
@@ -19,7 +53,7 @@ export default {
<li class="gl-border-b-1 gl-border-b-solid gl-border-b-gray-200">
<div class="gl-display-flex gl-align-items-center gl-py-3">
<gl-icon name="folder-o" class="gl-mr-3" />
- <div class="gl-display-none gl-flex-shrink-0 gl-display-sm-flex gl-mr-3">
+ <div class="gl-display-none gl-flex-shrink-0 gl-sm-display-flex gl-mr-3">
<gl-avatar :size="32" shape="rect" :entity-name="group.name" :src="group.avatar_url" />
</div>
<div class="gl-min-w-0 gl-display-flex gl-flex-grow-1 gl-flex-shrink-1 gl-align-items-center">
@@ -36,6 +70,14 @@ export default {
<p class="gl-mt-2! gl-mb-0 gl-text-gray-600" v-text="group.description"></p>
</div>
</div>
+
+ <gl-button
+ category="secondary"
+ variant="success"
+ :loading="isLoading"
+ @click.prevent="onClick"
+ >{{ __('Link') }}</gl-button
+ >
</div>
</div>
</li>
diff --git a/app/assets/javascripts/jira_connect/index.js b/app/assets/javascripts/jira_connect/index.js
index dc2a77f4e0c..082c74150c5 100644
--- a/app/assets/javascripts/jira_connect/index.js
+++ b/app/assets/javascripts/jira_connect/index.js
@@ -1,70 +1,73 @@
import Vue from 'vue';
-import Vuex from 'vuex';
-import $ from 'jquery';
import setConfigs from '@gitlab/ui/dist/config';
import Translate from '~/vue_shared/translate';
import GlFeatureFlagsPlugin from '~/vue_shared/gl_feature_flags_plugin';
+import { addSubscription, removeSubscription, getLocation } from '~/jira_connect/api';
import JiraConnectApp from './components/app.vue';
-import { addSubscription, removeSubscription } from '~/jira_connect/api';
import createStore from './store';
import { SET_ERROR_MESSAGE } from './store/mutation_types';
-Vue.use(Vuex);
-
const store = createStore();
-/**
- * Initialize form handlers for the Jira Connect app
- */
-const initJiraFormHandlers = () => {
- const reqComplete = () => {
- AP.navigator.reload();
- };
-
- const reqFailed = (res, fallbackErrorMessage) => {
- const { error = fallbackErrorMessage } = res || {};
-
- store.commit(SET_ERROR_MESSAGE, error);
- };
-
- if (typeof AP.getLocation === 'function') {
- AP.getLocation((location) => {
- $('.js-jira-connect-sign-in').each(function updateSignInLink() {
- const updatedLink = `${$(this).attr('href')}?return_to=${location}`;
- $(this).attr('href', updatedLink);
- });
- });
- }
+const reqComplete = () => {
+ AP.navigator.reload();
+};
- $('#add-subscription-form').on('submit', function onAddSubscriptionForm(e) {
- const addPath = $(this).attr('action');
- const namespace = $('#namespace-input').val();
+const reqFailed = (res, fallbackErrorMessage) => {
+ const { error = fallbackErrorMessage } = res || {};
- e.preventDefault();
+ store.commit(SET_ERROR_MESSAGE, error);
+};
- addSubscription(addPath, namespace)
- .then(reqComplete)
- .catch((err) => reqFailed(err.response.data, 'Failed to add namespace. Please try again.'));
+const updateSignInLinks = async () => {
+ const location = await getLocation();
+ Array.from(document.querySelectorAll('.js-jira-connect-sign-in')).forEach((el) => {
+ const updatedLink = `${el.getAttribute('href')}?return_to=${location}`;
+ el.setAttribute('href', updatedLink);
});
+};
- $('.remove-subscription').on('click', function onRemoveSubscriptionClick(e) {
- const removePath = $(this).attr('href');
+const initRemoveSubscriptionButtonHandlers = () => {
+ Array.from(document.querySelectorAll('.js-jira-connect-remove-subscription')).forEach((el) => {
+ el.addEventListener('click', function onRemoveSubscriptionClick(e) {
+ e.preventDefault();
+
+ const removePath = e.target.getAttribute('href');
+ removeSubscription(removePath)
+ .then(reqComplete)
+ .catch((err) =>
+ reqFailed(err.response.data, 'Failed to remove namespace. Please try again.'),
+ );
+ });
+ });
+};
+
+const initAddSubscriptionFormHandler = () => {
+ const formEl = document.querySelector('#add-subscription-form');
+ if (!formEl) {
+ return;
+ }
+
+ formEl.addEventListener('submit', function onAddSubscriptionForm(e) {
e.preventDefault();
- removeSubscription(removePath)
+ const addPath = e.target.getAttribute('action');
+ const namespace = (e.target.querySelector('#namespace-input') || {}).value;
+
+ addSubscription(addPath, namespace)
.then(reqComplete)
- .catch((err) =>
- reqFailed(err.response.data, 'Failed to remove namespace. Please try again.'),
- );
+ .catch((err) => reqFailed(err.response.data, 'Failed to add namespace. Please try again.'));
});
};
-function initJiraConnect() {
- const el = document.querySelector('.js-jira-connect-app');
+export async function initJiraConnect() {
+ initAddSubscriptionFormHandler();
+ initRemoveSubscriptionButtonHandlers();
- initJiraFormHandlers();
+ await updateSignInLinks();
+ const el = document.querySelector('.js-jira-connect-app');
if (!el) {
return null;
}
@@ -73,13 +76,15 @@ function initJiraConnect() {
Vue.use(Translate);
Vue.use(GlFeatureFlagsPlugin);
- const { groupsPath } = el.dataset;
+ const { groupsPath, subscriptionsPath, usersPath } = el.dataset;
return new Vue({
el,
store,
provide: {
groupsPath,
+ subscriptionsPath,
+ usersPath,
},
render(createElement) {
return createElement(JiraConnectApp);
diff --git a/app/assets/javascripts/jira_connect/store/index.js b/app/assets/javascripts/jira_connect/store/index.js
index aa7e14269a4..de830e3891a 100644
--- a/app/assets/javascripts/jira_connect/store/index.js
+++ b/app/assets/javascripts/jira_connect/store/index.js
@@ -1,9 +1,12 @@
+import Vue from 'vue';
import Vuex from 'vuex';
import mutations from './mutations';
import state from './state';
+Vue.use(Vuex);
+
export default () =>
new Vuex.Store({
- state,
mutations,
+ state,
});
diff --git a/app/assets/javascripts/jira_import/components/jira_import_form.vue b/app/assets/javascripts/jira_import/components/jira_import_form.vue
index ab475c3c85a..6f2fb41ca15 100644
--- a/app/assets/javascripts/jira_import/components/jira_import_form.vue
+++ b/app/assets/javascripts/jira_import/components/jira_import_form.vue
@@ -15,10 +15,11 @@ import {
GlTable,
} from '@gitlab/ui';
import { debounce } from 'lodash';
-import axios from '~/lib/utils/axios_utils';
+import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { __ } from '~/locale';
import getJiraUserMappingMutation from '../queries/get_jira_user_mapping.mutation.graphql';
import initiateJiraImportMutation from '../queries/initiate_jira_import.mutation.graphql';
+import searchProjectMembersQuery from '../queries/search_project_members.query.graphql';
import { addInProgressImportToStore } from '../utils/cache_update';
import {
debounceWait,
@@ -155,19 +156,23 @@ export default {
});
},
searchUsers() {
- const params = {
- active: true,
- project_id: this.projectId,
- search: this.searchTerm,
- };
-
this.isFetching = true;
- return axios
- .get('/-/autocomplete/users.json', { params })
+ return this.$apollo
+ .query({
+ query: searchProjectMembersQuery,
+ variables: {
+ fullPath: this.projectPath,
+ search: this.searchTerm,
+ },
+ })
.then(({ data }) => {
- this.users = data;
- return data;
+ this.users =
+ data?.project?.projectMembers?.nodes?.map(({ user }) => ({
+ ...user,
+ id: getIdFromGraphQLId(user.id),
+ })) || [];
+ return this.users;
})
.finally(() => {
this.isFetching = false;
diff --git a/app/assets/javascripts/jira_import/queries/search_project_members.query.graphql b/app/assets/javascripts/jira_import/queries/search_project_members.query.graphql
new file mode 100644
index 00000000000..06f119e75ed
--- /dev/null
+++ b/app/assets/javascripts/jira_import/queries/search_project_members.query.graphql
@@ -0,0 +1,13 @@
+query searchProjectMembers($fullPath: ID!, $search: String) {
+ project(fullPath: $fullPath) {
+ projectMembers(search: $search) {
+ nodes {
+ user {
+ id
+ name
+ username
+ }
+ }
+ }
+ }
+}
diff --git a/app/assets/javascripts/jobs/components/artifacts_block.vue b/app/assets/javascripts/jobs/components/artifacts_block.vue
index 2850a8e86fd..0f34926f689 100644
--- a/app/assets/javascripts/jobs/components/artifacts_block.vue
+++ b/app/assets/javascripts/jobs/components/artifacts_block.vue
@@ -1,13 +1,15 @@
<script>
-import { GlIcon, GlLink } from '@gitlab/ui';
+import { GlButton, GlButtonGroup, GlIcon, GlLink } from '@gitlab/ui';
import TimeagoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
import timeagoMixin from '~/vue_shared/mixins/timeago';
export default {
components: {
- TimeagoTooltip,
+ GlButton,
+ GlButtonGroup,
GlIcon,
GlLink,
+ TimeagoTooltip,
},
mixins: [timeagoMixin],
props: {
@@ -36,7 +38,7 @@ export default {
</script>
<template>
<div class="block">
- <div class="title font-weight-bold">{{ s__('Job|Job artifacts') }}</div>
+ <div class="title gl-font-weight-bold">{{ s__('Job|Job artifacts') }}</div>
<p
v-if="isExpired || willExpire"
class="build-detail-row"
@@ -61,32 +63,29 @@ export default {
)
}}</span>
</p>
- <div class="btn-group d-flex gl-mt-3" role="group">
- <gl-link
+ <gl-button-group class="gl-display-flex gl-mt-3">
+ <gl-button
v-if="artifact.keep_path"
:href="artifact.keep_path"
- class="btn btn-sm btn-default"
data-method="post"
data-testid="keep-artifacts"
- >{{ s__('Job|Keep') }}</gl-link
+ >{{ s__('Job|Keep') }}</gl-button
>
- <gl-link
+ <gl-button
v-if="artifact.download_path"
:href="artifact.download_path"
- class="btn btn-sm btn-default"
- download
rel="nofollow"
data-testid="download-artifacts"
- >{{ s__('Job|Download') }}</gl-link
+ download
+ >{{ s__('Job|Download') }}</gl-button
>
- <gl-link
+ <gl-button
v-if="artifact.browse_path"
:href="artifact.browse_path"
- class="btn btn-sm btn-default"
data-testid="browse-artifacts"
data-qa-selector="browse_artifacts_button"
- >{{ s__('Job|Browse') }}</gl-link
+ >{{ s__('Job|Browse') }}</gl-button
>
- </div>
+ </gl-button-group>
</div>
</template>
diff --git a/app/assets/javascripts/jobs/components/environments_block.vue b/app/assets/javascripts/jobs/components/environments_block.vue
index ec7868d9235..c78d36355e2 100644
--- a/app/assets/javascripts/jobs/components/environments_block.vue
+++ b/app/assets/javascripts/jobs/components/environments_block.vue
@@ -201,9 +201,7 @@ export default {
/>
<template v-else>{{ clusterNameOrLink.name }}</template>
</template>
- <template #kubernetesNamespace>
- <template>{{ kubernetesNamespace }}</template>
- </template>
+ <template #kubernetesNamespace>{{ kubernetesNamespace }}</template>
<template #deploymentLink>
<gl-link
:href="deploymentLink.path"
diff --git a/app/assets/javascripts/jobs/components/erased_block.vue b/app/assets/javascripts/jobs/components/erased_block.vue
index a6d1b41c275..91c36f38447 100644
--- a/app/assets/javascripts/jobs/components/erased_block.vue
+++ b/app/assets/javascripts/jobs/components/erased_block.vue
@@ -1,12 +1,14 @@
<script>
import { isEmpty } from 'lodash';
-import { GlLink } from '@gitlab/ui';
+import { GlAlert, GlLink, GlSprintf } from '@gitlab/ui';
import TimeagoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
export default {
components: {
- TimeagoTooltip,
+ GlAlert,
GlLink,
+ GlSprintf,
+ TimeagoTooltip,
},
props: {
user: {
@@ -27,17 +29,21 @@ export default {
};
</script>
<template>
- <div class="gl-mt-3 js-build-erased">
- <div class="erased alert alert-warning">
+ <div class="gl-mt-3">
+ <gl-alert variant="warning" :dismissible="false">
<template v-if="isErasedByUser">
- {{ s__('Job|Job has been erased by') }}
- <gl-link :href="user.web_url"> {{ user.username }} </gl-link>
+ <gl-sprintf :message="s__('Job|Job has been erased by %{userLink}')">
+ <template #userLink>
+ <gl-link :href="user.web_url" target="_blank">{{ user.username }}</gl-link>
+ </template>
+ </gl-sprintf>
</template>
+
<template v-else>
{{ s__('Job|Job has been erased') }}
</template>
<timeago-tooltip :time="erasedAt" />
- </div>
+ </gl-alert>
</div>
</template>
diff --git a/app/assets/javascripts/jobs/components/job_app.vue b/app/assets/javascripts/jobs/components/job_app.vue
index b0ba6ce52d1..52b60a1effd 100644
--- a/app/assets/javascripts/jobs/components/job_app.vue
+++ b/app/assets/javascripts/jobs/components/job_app.vue
@@ -6,6 +6,8 @@ import { GlBreakpointInstance as bp } from '@gitlab/ui/dist/utils';
import { isScrolledToBottom } from '~/lib/utils/scroll_utils';
import { polyfillSticky } from '~/lib/utils/sticky';
import CiHeader from '~/vue_shared/components/header_ci_component.vue';
+import { sprintf } from '~/locale';
+import delayedJobMixin from '../mixins/delayed_job_mixin';
import EmptyState from './empty_state.vue';
import EnvironmentsBlock from './environments_block.vue';
import ErasedBlock from './erased_block.vue';
@@ -13,8 +15,6 @@ import LogTopBar from './job_log_controllers.vue';
import StuckBlock from './stuck_block.vue';
import UnmetPrerequisitesBlock from './unmet_prerequisites_block.vue';
import Sidebar from './sidebar.vue';
-import { sprintf } from '~/locale';
-import delayedJobMixin from '../mixins/delayed_job_mixin';
import Log from './log/log.vue';
export default {
@@ -54,11 +54,6 @@ export default {
required: false,
default: null,
},
- runnerHelpUrl: {
- type: String,
- required: false,
- default: null,
- },
deploymentHelpUrl: {
type: String,
required: false,
@@ -250,7 +245,6 @@ export default {
v-if="shouldRenderSharedRunnerLimitWarning"
:quota-used="job.runners.quota.used"
:quota-limit="job.runners.quota.limit"
- :runners-path="runnerHelpUrl"
:project-path="projectPath"
:subscriptions-more-minutes-url="subscriptionsMoreMinutesUrl"
/>
@@ -330,7 +324,6 @@ export default {
'right-sidebar-collapsed': !isSidebarOpen,
}"
:artifact-help-url="artifactHelpUrl"
- :runner-help-url="runnerHelpUrl"
data-testid="job-sidebar"
/>
</div>
diff --git a/app/assets/javascripts/jobs/components/log/duration_badge.vue b/app/assets/javascripts/jobs/components/log/duration_badge.vue
index 8e5dcdcc902..54b76fd9edd 100644
--- a/app/assets/javascripts/jobs/components/log/duration_badge.vue
+++ b/app/assets/javascripts/jobs/components/log/duration_badge.vue
@@ -1,5 +1,10 @@
<script>
+import { GlBadge } from '@gitlab/ui';
+
export default {
+ components: {
+ GlBadge,
+ },
props: {
duration: {
type: String,
@@ -9,7 +14,7 @@ export default {
};
</script>
<template>
- <div class="log-duration-badge rounded align-self-start px-2 ml-2 flex-shrink-0 ws-normal">
+ <gl-badge>
{{ duration }}
- </div>
+ </gl-badge>
</template>
diff --git a/app/assets/javascripts/jobs/components/sidebar.vue b/app/assets/javascripts/jobs/components/sidebar.vue
index 0789bb54f0f..83eddc232a1 100644
--- a/app/assets/javascripts/jobs/components/sidebar.vue
+++ b/app/assets/javascripts/jobs/components/sidebar.vue
@@ -3,6 +3,7 @@ import { isEmpty } from 'lodash';
import { mapActions, mapGetters, mapState } from 'vuex';
import { GlButton, GlIcon, GlLink } from '@gitlab/ui';
import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate.vue';
+import { JOB_SIDEBAR } from '../constants';
import ArtifactsBlock from './artifacts_block.vue';
import JobSidebarRetryButton from './job_sidebar_retry_button.vue';
import JobRetryForwardDeploymentModal from './job_retry_forward_deployment_modal.vue';
@@ -11,7 +12,6 @@ import CommitBlock from './commit_block.vue';
import StagesDropdown from './stages_dropdown.vue';
import JobsContainer from './jobs_container.vue';
import JobSidebarDetailsContainer from './sidebar_job_details_container.vue';
-import { JOB_SIDEBAR } from '../constants';
export const forwardDeploymentFailureModalId = 'forward-deployment-failure';
@@ -41,11 +41,6 @@ export default {
required: false,
default: '',
},
- runnerHelpUrl: {
- type: String,
- required: false,
- default: '',
- },
},
computed: {
...mapGetters(['hasForwardDeploymentFailure']),
@@ -110,7 +105,7 @@ export default {
<gl-button
:aria-label="$options.i18n.toggleSidebar"
category="tertiary"
- class="gl-display-md-none gl-ml-2 js-sidebar-build-toggle"
+ class="gl-md-display-none gl-ml-2 js-sidebar-build-toggle"
icon="chevron-double-lg-right"
@click="toggleSidebar"
/>
@@ -135,7 +130,7 @@ export default {
<gl-icon :size="14" name="external-link" />
</gl-link>
</div>
- <job-sidebar-details-container :runner-help-url="runnerHelpUrl" />
+ <job-sidebar-details-container />
<artifacts-block v-if="hasArtifact" :artifact="job.artifact" :help-url="artifactHelpUrl" />
<trigger-block v-if="hasTriggers" :trigger="job.trigger" />
<commit-block
diff --git a/app/assets/javascripts/jobs/components/sidebar_job_details_container.vue b/app/assets/javascripts/jobs/components/sidebar_job_details_container.vue
index 8ad1008278e..84ce6674104 100644
--- a/app/assets/javascripts/jobs/components/sidebar_job_details_container.vue
+++ b/app/assets/javascripts/jobs/components/sidebar_job_details_container.vue
@@ -1,9 +1,10 @@
<script>
import { mapState } from 'vuex';
-import DetailRow from './sidebar_detail_row.vue';
import { __, sprintf } from '~/locale';
import timeagoMixin from '~/vue_shared/mixins/timeago';
import { timeIntervalInWords } from '~/lib/utils/datetime_utility';
+import { helpPagePath } from '~/helpers/help_page_helper';
+import DetailRow from './sidebar_detail_row.vue';
export default {
name: 'JobSidebarDetailsContainer',
@@ -11,13 +12,6 @@ export default {
DetailRow,
},
mixins: [timeagoMixin],
- props: {
- runnerHelpUrl: {
- type: String,
- required: false,
- default: '',
- },
- },
computed: {
...mapState(['job']),
coverage() {
@@ -51,6 +45,11 @@ export default {
queued() {
return timeIntervalInWords(this.job.queued);
},
+ runnerHelpUrl() {
+ return helpPagePath('ci/runners/README.html', {
+ anchor: 'set-maximum-job-timeout-for-a-runner',
+ });
+ },
runnerId() {
return `${this.job.runner.description} (#${this.job.runner.id})`;
},
diff --git a/app/assets/javascripts/jobs/components/stuck_block.vue b/app/assets/javascripts/jobs/components/stuck_block.vue
index 8e8202246a2..abd0c13702a 100644
--- a/app/assets/javascripts/jobs/components/stuck_block.vue
+++ b/app/assets/javascripts/jobs/components/stuck_block.vue
@@ -1,6 +1,6 @@
<script>
import { GlAlert, GlBadge, GlLink } from '@gitlab/ui';
-import { s__ } from '../../locale';
+import { s__ } from '~/locale';
/**
* Renders Stuck Runners block for job's view.
*/
diff --git a/app/assets/javascripts/jobs/components/trigger_block.vue b/app/assets/javascripts/jobs/components/trigger_block.vue
index 1d46dd8cea4..f6b98777011 100644
--- a/app/assets/javascripts/jobs/components/trigger_block.vue
+++ b/app/assets/javascripts/jobs/components/trigger_block.vue
@@ -1,12 +1,31 @@
<script>
-import { GlButton } from '@gitlab/ui';
+import { GlButton, GlTable } from '@gitlab/ui';
import { __ } from '~/locale';
-const HIDDEN_VALUE = '••••••';
+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!';
export default {
+ fields: [
+ {
+ key: 'key',
+ 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: {
GlButton,
+ GlTable,
},
props: {
trigger: {
@@ -21,7 +40,7 @@ export default {
},
computed: {
hasVariables() {
- return this.trigger.variables && this.trigger.variables.length > 0;
+ return this.trigger.variables.length > 0;
},
getToggleButtonText() {
return this.showVariableValues ? __('Hide values') : __('Reveal values');
@@ -35,45 +54,41 @@ export default {
this.showVariableValues = !this.showVariableValues;
},
getDisplayValue(value) {
- return this.showVariableValues ? value : HIDDEN_VALUE;
+ return this.showVariableValues ? value : '••••••';
},
},
};
</script>
<template>
- <div class="build-widget block">
+ <div class="block">
<p
v-if="trigger.short_token"
- class="js-short-token"
:class="{ 'gl-mb-2': hasVariables, 'gl-mb-0': !hasVariables }"
+ data-testid="trigger-short-token"
>
- <span class="font-weight-bold">{{ __('Trigger token:') }}</span> {{ trigger.short_token }}
+ <span class="gl-font-weight-bold">{{ __('Trigger token:') }}</span> {{ trigger.short_token }}
</p>
<template v-if="hasVariables">
- <p class="trigger-variables-btn-container d-flex">
- <span class="font-weight-bold">{{ __('Trigger variables:') }}</span>
+ <p class="gl-display-flex gl-justify-content-space-between gl-align-items-center">
+ <span class="gl-font-weight-bold">{{ __('Trigger variables:') }}</span>
<gl-button
v-if="hasValues"
- class="group js-reveal-variables trigger-variables-btn"
+ class="gl-mt-2"
size="small"
+ data-testid="trigger-reveal-values-button"
@click="toggleValues"
>{{ getToggleButtonText }}</gl-button
>
</p>
- <table class="js-build-variables trigger-build-variables">
- <tr v-for="(variable, index) in trigger.variables" :key="`${variable.key}-${index}`">
- <td class="js-build-variable trigger-build-variable trigger-variables-table-cell">
- {{ variable.key }}
- </td>
- <td class="js-build-value trigger-build-value trigger-variables-table-cell">
- {{ getDisplayValue(variable.value) }}
- </td>
- </tr>
- </table>
+ <gl-table :items="trigger.variables" :fields="$options.fields" small bordered>
+ <template #cell(value)="data">
+ {{ getDisplayValue(data.value) }}
+ </template>
+ </gl-table>
</template>
</div>
</template>
diff --git a/app/assets/javascripts/jobs/index.js b/app/assets/javascripts/jobs/index.js
index 1ad6292a030..3e00056ee81 100644
--- a/app/assets/javascripts/jobs/index.js
+++ b/app/assets/javascripts/jobs/index.js
@@ -13,7 +13,6 @@ export default () => {
const {
artifactHelpUrl,
deploymentHelpUrl,
- runnerHelpUrl,
runnerSettingsUrl,
variablesSettingsUrl,
subscriptionsMoreMinutesUrl,
@@ -39,7 +38,6 @@ export default () => {
props: {
artifactHelpUrl,
deploymentHelpUrl,
- runnerHelpUrl,
runnerSettingsUrl,
variablesSettingsUrl,
subscriptionsMoreMinutesUrl,
diff --git a/app/assets/javascripts/jobs/store/actions.js b/app/assets/javascripts/jobs/store/actions.js
index e76a3693db9..6b0ca8ab986 100644
--- a/app/assets/javascripts/jobs/store/actions.js
+++ b/app/assets/javascripts/jobs/store/actions.js
@@ -1,5 +1,4 @@
import Visibility from 'visibilityjs';
-import * as types from './mutation_types';
import axios from '~/lib/utils/axios_utils';
import Poll from '~/lib/utils/poll';
import { setFaviconOverlay, resetFavicon } from '~/lib/utils/favicon';
@@ -14,6 +13,7 @@ import {
scrollUp,
} from '~/lib/utils/scroll_utils';
import httpStatusCodes from '~/lib/utils/http_status';
+import * as types from './mutation_types';
export const init = ({ dispatch }, { endpoint, logState, pagePath }) => {
dispatch('setJobEndpoint', endpoint);
diff --git a/app/assets/javascripts/jobs/store/getters.js b/app/assets/javascripts/jobs/store/getters.js
index 30a4a247dc4..930a225857d 100644
--- a/app/assets/javascripts/jobs/store/getters.js
+++ b/app/assets/javascripts/jobs/store/getters.js
@@ -1,7 +1,7 @@
import { isEmpty, isString } from 'lodash';
import { isScrolledToBottom } from '~/lib/utils/scroll_utils';
-export const headerTime = (state) => (state.job.started ? state.job.started : state.job.created_at);
+export const headerTime = (state) => state.job.started ?? state.job.created_at;
export const hasForwardDeploymentFailure = (state) =>
state?.job?.failure_reason === 'forward_deployment_failure';
@@ -28,11 +28,9 @@ export const hasEnvironment = (state) => !isEmpty(state.job.deployment_status);
export const hasTrace = (state) =>
state.job.has_trace || (!isEmpty(state.job.status) && state.job.status.group === 'running');
-export const emptyStateIllustration = (state) =>
- (state.job && state.job.status && state.job.status.illustration) || {};
+export const emptyStateIllustration = (state) => state?.job?.status?.illustration || {};
-export const emptyStateAction = (state) =>
- (state.job && state.job.status && state.job.status.action) || null;
+export const emptyStateAction = (state) => state?.job?.status?.action || null;
/**
* Shared runners limit is only rendered when
@@ -48,4 +46,4 @@ export const shouldRenderSharedRunnerLimitWarning = (state) =>
export const isScrollingDown = (state) => isScrolledToBottom() && !state.isTraceComplete;
export const hasRunnersForProject = (state) =>
- state.job.runners.available && !state.job.runners.online;
+ state?.job?.runners?.available && !state?.job?.runners?.online;
diff --git a/app/assets/javascripts/label_manager.js b/app/assets/javascripts/label_manager.js
index 92dffb87e1a..aa197edd449 100644
--- a/app/assets/javascripts/label_manager.js
+++ b/app/assets/javascripts/label_manager.js
@@ -3,10 +3,10 @@
import $ from 'jquery';
import Sortable from 'sortablejs';
+import { hide, dispose } from '~/tooltips';
import { deprecatedCreateFlash as flash } from './flash';
import axios from './lib/utils/axios_utils';
import { __ } from './locale';
-import { hide, dispose } from '~/tooltips';
export default class LabelManager {
constructor({ togglePriorityButton, prioritizedLabels, otherLabels } = {}) {
diff --git a/app/assets/javascripts/labels_select.js b/app/assets/javascripts/labels_select.js
index 337d063b02a..dd5bba45c21 100644
--- a/app/assets/javascripts/labels_select.js
+++ b/app/assets/javascripts/labels_select.js
@@ -4,6 +4,8 @@
import $ from 'jquery';
import { difference, isEqual, escape, sortBy, template, union } from 'lodash';
+import { isScopedLabel } from '~/lib/utils/common_utils';
+import initDeprecatedJQueryDropdown from '~/deprecated_jquery_dropdown';
import { sprintf, __ } from './locale';
import axios from './lib/utils/axios_utils';
import IssuableBulkUpdateActions from './issuable_bulk_update_actions';
@@ -11,8 +13,6 @@ import CreateLabelDropdown from './create_label';
import { deprecatedCreateFlash as flash } from './flash';
import ModalStore from './boards/stores/modal_store';
import boardsStore from './boards/stores/boards_store';
-import { isScopedLabel } from '~/lib/utils/common_utils';
-import initDeprecatedJQueryDropdown from '~/deprecated_jquery_dropdown';
export default class LabelsSelect {
constructor(els, options = {}) {
diff --git a/app/assets/javascripts/lib/graphql.js b/app/assets/javascripts/lib/graphql.js
index 5c4bb5ea01f..d974e36f6f0 100644
--- a/app/assets/javascripts/lib/graphql.js
+++ b/app/assets/javascripts/lib/graphql.js
@@ -35,6 +35,16 @@ export default (resolvers = {}, config = {}) => {
batchMax: config.batchMax || 10,
};
+ const requestCounterLink = new ApolloLink((operation, forward) => {
+ window.pendingApolloRequests = window.pendingApolloRequests || 0;
+ window.pendingApolloRequests += 1;
+
+ return forward(operation).map((response) => {
+ window.pendingApolloRequests -= 1;
+ return response;
+ });
+ });
+
const uploadsLink = ApolloLink.split(
(operation) => operation.getContext().hasUpload || operation.getContext().isSingleRequest,
createUploadLink(httpOptions),
@@ -63,7 +73,12 @@ export default (resolvers = {}, config = {}) => {
return new ApolloClient({
typeDefs: config.typeDefs,
- link: ApolloLink.from([performanceBarLink, new StartupJSLink(), uploadsLink]),
+ link: ApolloLink.from([
+ requestCounterLink,
+ performanceBarLink,
+ new StartupJSLink(),
+ uploadsLink,
+ ]),
cache: new InMemoryCache({
...config.cacheConfig,
freezeResults: config.assumeImmutableResults,
diff --git a/app/assets/javascripts/lib/utils/array_utility.js b/app/assets/javascripts/lib/utils/array_utility.js
new file mode 100644
index 00000000000..197e7790ed7
--- /dev/null
+++ b/app/assets/javascripts/lib/utils/array_utility.js
@@ -0,0 +1,20 @@
+/**
+ * Return a shallow copy of an array with two items swapped.
+ *
+ * @param {Array} array - The source array
+ * @param {Number} leftIndex - Index of the first item
+ * @param {Number} rightIndex - Index of the second item
+ * @returns {Array} new array with the left and right items swapped
+ */
+export const swapArrayItems = (array, leftIndex = 0, rightIndex = 0) => {
+ const copy = array.slice();
+
+ if (leftIndex >= array.length || leftIndex < 0 || rightIndex >= array.length || rightIndex < 0) {
+ return copy;
+ }
+
+ const temp = copy[leftIndex];
+ copy[leftIndex] = copy[rightIndex];
+ copy[rightIndex] = temp;
+ return copy;
+};
diff --git a/app/assets/javascripts/lib/utils/color_utils.js b/app/assets/javascripts/lib/utils/color_utils.js
index a1f56b15631..ff176f11867 100644
--- a/app/assets/javascripts/lib/utils/color_utils.js
+++ b/app/assets/javascripts/lib/utils/color_utils.js
@@ -23,3 +23,23 @@ export const textColorForBackground = (backgroundColor) => {
}
return '#FFFFFF';
};
+
+/**
+ * Check whether a color matches the expected hex format
+ *
+ * This matches any hex (0-9 and A-F) value which is either 3 or 6 characters in length
+ *
+ * An empty string will return `null` which means that this is neither valid nor invalid.
+ * This is useful for forms resetting the validation state
+ *
+ * @param color string = ''
+ *
+ * @returns {null|boolean}
+ */
+export const validateHexColor = (color = '') => {
+ if (!color) {
+ return null;
+ }
+
+ return /^#([0-9A-F]{3}){1,2}$/i.test(color);
+};
diff --git a/app/assets/javascripts/lib/utils/common_utils.js b/app/assets/javascripts/lib/utils/common_utils.js
index 128ef5b335e..d0528204fd5 100644
--- a/app/assets/javascripts/lib/utils/common_utils.js
+++ b/app/assets/javascripts/lib/utils/common_utils.js
@@ -45,6 +45,7 @@ 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 isInEpicPage = () => checkPageAndAction('epics', 'show');
@@ -801,3 +802,12 @@ export const removeCookie = (name) => Cookies.remove(name);
* @returns {Boolean} on/off
*/
export const isFeatureFlagEnabled = (flag) => window.gon.features?.[flag];
+
+/**
+ * This method takes in array with snake_case strings
+ * and returns a new array with camelCase strings
+ *
+ * @param {Array[String]} array - Array to be converted
+ * @returns {Array[String]} Converted array
+ */
+export const convertArrayToCamelCase = (array) => array.map((i) => convertToCamelCase(i));
diff --git a/app/assets/javascripts/lib/utils/constants.js b/app/assets/javascripts/lib/utils/constants.js
index 993d51370ec..b19a4a01a5f 100644
--- a/app/assets/javascripts/lib/utils/constants.js
+++ b/app/assets/javascripts/lib/utils/constants.js
@@ -10,3 +10,9 @@ export const DATETIME_RANGE_TYPES = {
open: 'open',
invalid: 'invalid',
};
+
+export const BV_SHOW_MODAL = 'bv::show::modal';
+export const BV_HIDE_MODAL = 'bv::hide::modal';
+export const BV_HIDE_TOOLTIP = 'bv::hide::tooltip';
+export const BV_DROPDOWN_SHOW = 'bv::dropdown::show';
+export const BV_DROPDOWN_HIDE = 'bv::dropdown::hide';
diff --git a/app/assets/javascripts/lib/utils/datetime_utility.js b/app/assets/javascripts/lib/utils/datetime_utility.js
index 15f7c0c874e..d475dd6b9e6 100644
--- a/app/assets/javascripts/lib/utils/datetime_utility.js
+++ b/app/assets/javascripts/lib/utils/datetime_utility.js
@@ -4,7 +4,9 @@ import * as timeago from 'timeago.js';
import dateFormat from 'dateformat';
import { languageCode, s__, __, n__ } from '../../locale';
-const MILLISECONDS_IN_DAY = 24 * 60 * 60 * 1000;
+const MILLISECONDS_IN_HOUR = 60 * 60 * 1000;
+const MILLISECONDS_IN_DAY = 24 * MILLISECONDS_IN_HOUR;
+const DAYS_IN_WEEK = 7;
window.timeago = timeago;
@@ -674,50 +676,127 @@ export const secondsToHours = (offset) => {
};
/**
- * Returns the date n days after the date provided
+ * Returns the date `n` days after the date provided
*
* @param {Date} date the initial date
* @param {Number} numberOfDays number of days after
- * @return {Date} the date following the date provided
+ * @param {Object} [options={}] Additional options for this calculation
+ * @param {boolean} [options.utc=false] Performs the calculation using UTC dates.
+ * This will cause Daylight Saving Time to be ignored. Defaults to `false`
+ * if not provided, which causes the calculation to be performed in the
+ * user's timezone.
+ *
+ * @return {Date} A `Date` object `n` days after the provided `Date`
*/
-export const nDaysAfter = (date, numberOfDays) =>
- new Date(newDate(date)).setDate(date.getDate() + numberOfDays);
+export const nDaysAfter = (date, numberOfDays, { utc = false } = {}) => {
+ const clone = newDate(date);
+
+ const cloneValue = utc
+ ? clone.setUTCDate(date.getUTCDate() + numberOfDays)
+ : clone.setDate(date.getDate() + numberOfDays);
+
+ return new Date(cloneValue);
+};
/**
- * Returns the date n days before the date provided
+ * Returns the date `n` days before the date provided
*
* @param {Date} date the initial date
* @param {Number} numberOfDays number of days before
- * @return {Date} the date preceding the date provided
+ * @param {Object} [options={}] Additional options for this calculation
+ * @param {boolean} [options.utc=false] Performs the calculation using UTC dates.
+ * This will cause Daylight Saving Time to be ignored. Defaults to `false`
+ * if not provided, which causes the calculation to be performed in the
+ * user's timezone.
+ * @return {Date} A `Date` object `n` days before the provided `Date`
+ */
+export const nDaysBefore = (date, numberOfDays, options) =>
+ nDaysAfter(date, -numberOfDays, options);
+
+/**
+ * Returns the date `n` weeks after the date provided
+ *
+ * @param {Date} date the initial date
+ * @param {Number} numberOfWeeks number of weeks after
+ * @param {Object} [options={}] Additional options for this calculation
+ * @param {boolean} [options.utc=false] Performs the calculation using UTC dates.
+ * This will cause Daylight Saving Time to be ignored. Defaults to `false`
+ * if not provided, which causes the calculation to be performed in the
+ * user's timezone.
+ *
+ * @return {Date} A `Date` object `n` weeks after the provided `Date`
+ */
+export const nWeeksAfter = (date, numberOfWeeks, options) =>
+ nDaysAfter(date, DAYS_IN_WEEK * numberOfWeeks, options);
+
+/**
+ * Returns the date `n` weeks before the date provided
+ *
+ * @param {Date} date the initial date
+ * @param {Number} numberOfWeeks number of weeks before
+ * @param {Object} [options={}] Additional options for this calculation
+ * @param {boolean} [options.utc=false] Performs the calculation using UTC dates.
+ * This will cause Daylight Saving Time to be ignored. Defaults to `false`
+ * if not provided, which causes the calculation to be performed in the
+ * user's timezone.
+ *
+ * @return {Date} A `Date` object `n` weeks before the provided `Date`
*/
-export const nDaysBefore = (date, numberOfDays) => nDaysAfter(date, -numberOfDays);
+export const nWeeksBefore = (date, numberOfWeeks, options) =>
+ nWeeksAfter(date, -numberOfWeeks, options);
/**
- * Returns the date n months after the date provided
+ * Returns the date `n` months after the date provided
*
* @param {Date} date the initial date
* @param {Number} numberOfMonths number of months after
- * @return {Date} the date following the date provided
+ * @param {Object} [options={}] Additional options for this calculation
+ * @param {boolean} [options.utc=false] Performs the calculation using UTC dates.
+ * This will cause Daylight Saving Time to be ignored. Defaults to `false`
+ * if not provided, which causes the calculation to be performed in the
+ * user's timezone.
+ *
+ * @return {Date} A `Date` object `n` months after the provided `Date`
*/
-export const nMonthsAfter = (date, numberOfMonths) =>
- new Date(newDate(date)).setMonth(date.getMonth() + numberOfMonths);
+export const nMonthsAfter = (date, numberOfMonths, { utc = false } = {}) => {
+ const clone = newDate(date);
+
+ const cloneValue = utc
+ ? clone.setUTCMonth(date.getUTCMonth() + numberOfMonths)
+ : clone.setMonth(date.getMonth() + numberOfMonths);
+
+ return new Date(cloneValue);
+};
/**
- * Returns the date n months before the date provided
+ * Returns the date `n` months before the date provided
*
* @param {Date} date the initial date
* @param {Number} numberOfMonths number of months before
- * @return {Date} the date preceding the date provided
+ * @param {Object} [options={}] Additional options for this calculation
+ * @param {boolean} [options.utc=false] Performs the calculation using UTC dates.
+ * This will cause Daylight Saving Time to be ignored. Defaults to `false`
+ * if not provided, which causes the calculation to be performed in the
+ * user's timezone.
+ *
+ * @return {Date} A `Date` object `n` months before the provided `Date`
*/
-export const nMonthsBefore = (date, numberOfMonths) => nMonthsAfter(date, -numberOfMonths);
+export const nMonthsBefore = (date, numberOfMonths, options) =>
+ nMonthsAfter(date, -numberOfMonths, options);
/**
* Returns the date after the date provided
*
* @param {Date} date the initial date
+ * @param {Object} [options={}] Additional options for this calculation
+ * @param {boolean} [options.utc=false] Performs the calculation using UTC dates.
+ * This will cause Daylight Saving Time to be ignored. Defaults to `false`
+ * if not provided, which causes the calculation to be performed in the
+ * user's timezone.
+ *
* @return {Date} the date following the date provided
*/
-export const dayAfter = (date) => new Date(newDate(date).setDate(date.getDate() + 1));
+export const dayAfter = (date, options) => nDaysAfter(date, 1, options);
/**
* Mimics the behaviour of the rails distance_of_time_in_words function
@@ -859,17 +938,17 @@ export const format24HourTimeStringFromInt = (time) => {
*
* @param {Object} givenPeriodLeft - the first period to compare.
* @param {Object} givenPeriodRight - the second period to compare.
- * @returns {Object} { overlap: number of days the overlap is present, overlapStartDate: the start date of the overlap in time format, overlapEndDate: the end date of the overlap in time format }
+ * @returns {Object} { daysOverlap: number of days the overlap is present, hoursOverlap: number of hours the overlap is present, overlapStartDate: the start date of the overlap in time format, overlapEndDate: the end date of the overlap in time format }
* @throws {Error} Uncaught Error: Invalid period
*
* @example
- * getOverlappingDaysInPeriods(
+ * getOverlapDateInPeriods(
* { start: new Date(2021, 0, 11), end: new Date(2021, 0, 13) },
* { start: new Date(2021, 0, 11), end: new Date(2021, 0, 14) }
- * ) => { daysOverlap: 2, overlapStartDate: 1610323200000, overlapEndDate: 1610496000000 }
+ * ) => { daysOverlap: 2, hoursOverlap: 48, overlapStartDate: 1610323200000, overlapEndDate: 1610496000000 }
*
*/
-export const getOverlappingDaysInPeriods = (givenPeriodLeft = {}, givenPeriodRight = {}) => {
+export const getOverlapDateInPeriods = (givenPeriodLeft = {}, givenPeriodRight = {}) => {
const leftStartTime = new Date(givenPeriodLeft.start).getTime();
const leftEndTime = new Date(givenPeriodLeft.end).getTime();
const rightStartTime = new Date(givenPeriodRight.start).getTime();
@@ -890,8 +969,44 @@ export const getOverlappingDaysInPeriods = (givenPeriodLeft = {}, givenPeriodRig
const differenceInMs = overlapEndDate - overlapStartDate;
return {
+ hoursOverlap: Math.ceil(differenceInMs / MILLISECONDS_IN_HOUR),
daysOverlap: Math.ceil(differenceInMs / MILLISECONDS_IN_DAY),
overlapStartDate,
overlapEndDate,
};
};
+
+/**
+ * A utility function that checks that the date is today
+ *
+ * @param {Date} date
+ *
+ * @return {Boolean} true if provided date is today
+ */
+export const isToday = (date) => {
+ const today = new Date();
+ return (
+ date.getDate() === today.getDate() &&
+ date.getMonth() === today.getMonth() &&
+ date.getFullYear() === today.getFullYear()
+ );
+};
+
+/**
+ * Returns the start of the provided day
+ *
+ * @param {Object} [options={}] Additional options for this calculation
+ * @param {boolean} [options.utc=false] Performs the calculation using UTC time.
+ * If `true`, the time returned will be midnight UTC. If `false` (the default)
+ * the time returned will be midnight in the user's local time.
+ *
+ * @returns {Date} A new `Date` object that represents the start of the day
+ * of the provided date
+ */
+export const getStartOfDay = (date, { utc = false } = {}) => {
+ const clone = newDate(date);
+
+ const cloneValue = utc ? clone.setUTCHours(0, 0, 0, 0) : clone.setHours(0, 0, 0, 0);
+
+ return new Date(cloneValue);
+};
diff --git a/app/assets/javascripts/lib/utils/number_utils.js b/app/assets/javascripts/lib/utils/number_utils.js
index d49382733c0..0f29f538b07 100644
--- a/app/assets/javascripts/lib/utils/number_utils.js
+++ b/app/assets/javascripts/lib/utils/number_utils.js
@@ -1,5 +1,5 @@
-import { BYTES_IN_KIB } from './constants';
import { sprintf, __ } from '~/locale';
+import { BYTES_IN_KIB } from './constants';
/**
* Function that allows a number with an X amount of decimals
diff --git a/app/assets/javascripts/lib/utils/unit_format/formatter_factory.js b/app/assets/javascripts/lib/utils/unit_format/formatter_factory.js
index 9d47a1b7132..15f9512fe92 100644
--- a/app/assets/javascripts/lib/utils/unit_format/formatter_factory.js
+++ b/app/assets/javascripts/lib/utils/unit_format/formatter_factory.js
@@ -20,8 +20,9 @@ function formatNumber(
return '';
}
+ const locale = document.documentElement.lang || undefined;
const num = value * valueFactor;
- const formatted = num.toLocaleString(undefined, {
+ const formatted = num.toLocaleString(locale, {
minimumFractionDigits: fractionDigits,
maximumFractionDigits: fractionDigits,
style,
diff --git a/app/assets/javascripts/logs/components/environment_logs.vue b/app/assets/javascripts/logs/components/environment_logs.vue
index a114b3c7d4d..2ce8d9815b5 100644
--- a/app/assets/javascripts/logs/components/environment_logs.vue
+++ b/app/assets/javascripts/logs/components/environment_logs.vue
@@ -11,13 +11,12 @@ import {
GlInfiniteScroll,
} from '@gitlab/ui';
-import LogSimpleFilters from './log_simple_filters.vue';
-import LogAdvancedFilters from './log_advanced_filters.vue';
-import LogControlButtons from './log_control_buttons.vue';
-
import { defaultTimeRange } from '~/vue_shared/constants';
import { timeRangeFromUrl } from '~/monitoring/utils';
import { formatDate } from '../utils';
+import LogSimpleFilters from './log_simple_filters.vue';
+import LogAdvancedFilters from './log_advanced_filters.vue';
+import LogControlButtons from './log_control_buttons.vue';
export default {
components: {
@@ -176,7 +175,7 @@ export default {
id="environments-dropdown"
:text="environments.current || managedApps.current"
:disabled="environments.isLoading"
- class="gl-mr-3 gl-mb-3 gl-display-flex gl-display-md-block js-environments-dropdown"
+ class="gl-mr-3 gl-mb-3 gl-display-flex gl-md-display-block js-environments-dropdown"
>
<gl-dropdown-section-header>
{{ s__('Environments|Environments') }}
diff --git a/app/assets/javascripts/logs/components/log_simple_filters.vue b/app/assets/javascripts/logs/components/log_simple_filters.vue
index ba30d4628c9..6ee1df3c3fe 100644
--- a/app/assets/javascripts/logs/components/log_simple_filters.vue
+++ b/app/assets/javascripts/logs/components/log_simple_filters.vue
@@ -42,7 +42,7 @@ export default {
ref="podsDropdown"
:text="podDropdownText"
:disabled="disabled"
- class="gl-mr-3 gl-mb-3 gl-display-flex gl-display-md-block qa-pods-dropdown"
+ class="gl-mr-3 gl-mb-3 gl-display-flex gl-md-display-block qa-pods-dropdown"
>
<gl-dropdown-section-header>
{{ s__('Environments|Select pod') }}
diff --git a/app/assets/javascripts/logs/stores/mutations.js b/app/assets/javascripts/logs/stores/mutations.js
index 147f562057f..21031838adf 100644
--- a/app/assets/javascripts/logs/stores/mutations.js
+++ b/app/assets/javascripts/logs/stores/mutations.js
@@ -1,5 +1,5 @@
-import * as types from './mutation_types';
import { convertToFixedRange } from '~/lib/utils/datetime_range';
+import * as types from './mutation_types';
const mapLine = ({ timestamp, pod, message }) => ({
timestamp,
diff --git a/app/assets/javascripts/main.js b/app/assets/javascripts/main.js
index ef0fef6085b..f1900ea5f7e 100644
--- a/app/assets/javascripts/main.js
+++ b/app/assets/javascripts/main.js
@@ -12,6 +12,8 @@ import './behaviors';
import applyGitLabUIConfig from '@gitlab/ui/dist/config';
import { GlBreakpointInstance as bp } from '@gitlab/ui/dist/utils';
import { initRails } from '~/lib/utils/rails_ujs';
+import * as tooltips from '~/tooltips';
+import * as popovers from '~/popovers';
import {
handleLocationHash,
addSelectOnFocusBehaviour,
@@ -38,9 +40,6 @@ import initPersistentUserCallouts from './persistent_user_callouts';
import { initUserTracking, initDefaultTrackers } from './tracking';
import { __ } from './locale';
-import * as tooltips from '~/tooltips';
-import * as popovers from '~/popovers';
-
import 'ee_else_ce/main_ee';
applyGitLabUIConfig();
diff --git a/app/assets/javascripts/members/components/action_buttons/access_request_action_buttons.vue b/app/assets/javascripts/members/components/action_buttons/access_request_action_buttons.vue
index fcb70dd45a6..77b41996ab5 100644
--- a/app/assets/javascripts/members/components/action_buttons/access_request_action_buttons.vue
+++ b/app/assets/javascripts/members/components/action_buttons/access_request_action_buttons.vue
@@ -1,8 +1,8 @@
<script>
+import { s__, sprintf } from '~/locale';
import ActionButtonGroup from './action_button_group.vue';
import RemoveMemberButton from './remove_member_button.vue';
import ApproveAccessRequestButton from './approve_access_request_button.vue';
-import { s__, sprintf } from '~/locale';
export default {
name: 'AccessRequestActionButtons',
diff --git a/app/assets/javascripts/members/components/action_buttons/invite_action_buttons.vue b/app/assets/javascripts/members/components/action_buttons/invite_action_buttons.vue
index 9a27348f146..0bcc85157f1 100644
--- a/app/assets/javascripts/members/components/action_buttons/invite_action_buttons.vue
+++ b/app/assets/javascripts/members/components/action_buttons/invite_action_buttons.vue
@@ -1,8 +1,8 @@
<script>
+import { s__, sprintf } from '~/locale';
import ActionButtonGroup from './action_button_group.vue';
import RemoveMemberButton from './remove_member_button.vue';
import ResendInviteButton from './resend_invite_button.vue';
-import { s__, sprintf } from '~/locale';
export default {
name: 'InviteActionButtons',
diff --git a/app/assets/javascripts/members/components/action_buttons/remove_group_link_button.vue b/app/assets/javascripts/members/components/action_buttons/remove_group_link_button.vue
index 9d89cb40676..0120ab08556 100644
--- a/app/assets/javascripts/members/components/action_buttons/remove_group_link_button.vue
+++ b/app/assets/javascripts/members/components/action_buttons/remove_group_link_button.vue
@@ -31,6 +31,7 @@ export default {
:title="$options.i18n.buttonTitle"
:aria-label="$options.i18n.buttonTitle"
icon="remove"
+ data-qa-selector="delete_group_access_link"
@click="showRemoveGroupLinkModal(groupLink)"
/>
</template>
diff --git a/app/assets/javascripts/members/components/action_buttons/user_action_buttons.vue b/app/assets/javascripts/members/components/action_buttons/user_action_buttons.vue
index 0e5df961782..70e7a2a6948 100644
--- a/app/assets/javascripts/members/components/action_buttons/user_action_buttons.vue
+++ b/app/assets/javascripts/members/components/action_buttons/user_action_buttons.vue
@@ -1,8 +1,8 @@
<script>
+import { s__, sprintf } from '~/locale';
import ActionButtonGroup from './action_button_group.vue';
import RemoveMemberButton from './remove_member_button.vue';
import LeaveButton from './leave_button.vue';
-import { s__, sprintf } from '~/locale';
export default {
name: 'UserActionButtons',
diff --git a/app/assets/javascripts/groups/members/components/app.vue b/app/assets/javascripts/members/components/app.vue
index 34a2c67fa9f..0aee50e529d 100644
--- a/app/assets/javascripts/groups/members/components/app.vue
+++ b/app/assets/javascripts/members/components/app.vue
@@ -1,13 +1,13 @@
<script>
import { mapState, mapMutations } from 'vuex';
import { GlAlert } from '@gitlab/ui';
-import MembersTable from '~/members/components/table/members_table.vue';
-import FilterSortContainer from '~/members/components/filter_sort/filter_sort_container.vue';
import { scrollToElement } from '~/lib/utils/common_utils';
-import { HIDE_ERROR } from '~/members/store/mutation_types';
+import { HIDE_ERROR } from '../store/mutation_types';
+import MembersTable from './table/members_table.vue';
+import FilterSortContainer from './filter_sort/filter_sort_container.vue';
export default {
- name: 'GroupMembersApp',
+ name: 'MembersApp',
components: { MembersTable, FilterSortContainer, GlAlert },
computed: {
...mapState(['showError', 'errorMessage']),
diff --git a/app/assets/javascripts/members/components/avatars/user_avatar.vue b/app/assets/javascripts/members/components/avatars/user_avatar.vue
index e2264085e67..991f77cf3da 100644
--- a/app/assets/javascripts/members/components/avatars/user_avatar.vue
+++ b/app/assets/javascripts/members/components/avatars/user_avatar.vue
@@ -7,8 +7,8 @@ import {
} from '@gitlab/ui';
import { generateBadges } from 'ee_else_ce/members/utils';
import { __ } from '~/locale';
-import { AVATAR_SIZE } from '../../constants';
import { glEmojiTag } from '~/emoji';
+import { AVATAR_SIZE } from '../../constants';
export default {
name: 'UserAvatar',
@@ -69,7 +69,10 @@ export default {
>
<template #meta>
<div v-if="statusEmoji" class="gl-p-1">
- <span v-safe-html:[$options.safeHtmlConfig]="glEmojiTag(statusEmoji)"></span>
+ <span
+ v-safe-html:[$options.safeHtmlConfig]="glEmojiTag(statusEmoji)"
+ class="user-status-emoji gl-mr-0"
+ ></span>
</div>
<div v-for="badge in badges" :key="badge.text" class="gl-p-1">
<gl-badge size="sm" :variant="badge.variant">
diff --git a/app/assets/javascripts/members/components/filter_sort/filter_sort_container.vue b/app/assets/javascripts/members/components/filter_sort/filter_sort_container.vue
index f869ecd392f..812a8626949 100644
--- a/app/assets/javascripts/members/components/filter_sort/filter_sort_container.vue
+++ b/app/assets/javascripts/members/components/filter_sort/filter_sort_container.vue
@@ -19,7 +19,7 @@ export default {
</script>
<template>
- <div v-if="showContainer" class="gl-bg-gray-10 gl-p-3 gl-display-md-flex">
+ <div v-if="showContainer" class="gl-bg-gray-10 gl-p-3 gl-md-display-flex">
<members-filtered-search-bar v-if="filteredSearchBar.show" class="gl-p-3 gl-flex-grow-1" />
<sort-dropdown v-if="showSortDropdown" class="gl-p-3 gl-flex-shrink-0" />
</div>
diff --git a/app/assets/javascripts/members/components/modals/remove_group_link_modal.vue b/app/assets/javascripts/members/components/modals/remove_group_link_modal.vue
index 231d014a4ec..2bf61c7795a 100644
--- a/app/assets/javascripts/members/components/modals/remove_group_link_modal.vue
+++ b/app/assets/javascripts/members/components/modals/remove_group_link_modal.vue
@@ -52,6 +52,7 @@ export default {
:action-primary="$options.actionPrimary"
:action-cancel="$options.actionCancel"
size="sm"
+ data-qa-selector="remove_group_link_modal_content"
@primary="handlePrimary"
@hide="hideRemoveGroupLinkModal"
>
diff --git a/app/assets/javascripts/members/components/table/members_table.vue b/app/assets/javascripts/members/components/table/members_table.vue
index 16e0cd5ad4e..cfe9e40cde2 100644
--- a/app/assets/javascripts/members/components/table/members_table.vue
+++ b/app/assets/javascripts/members/components/table/members_table.vue
@@ -3,15 +3,15 @@ import { mapState } from 'vuex';
import { GlTable, GlBadge } from '@gitlab/ui';
import MembersTableCell from 'ee_else_ce/members/components/table/members_table_cell.vue';
import { canOverride, canRemove, canResend, canUpdate } from 'ee_else_ce/members/utils';
-import { FIELDS } from '../../constants';
import initUserPopovers from '~/user_popovers';
+import { FIELDS } from '../../constants';
+import RemoveGroupLinkModal from '../modals/remove_group_link_modal.vue';
import MemberAvatar from './member_avatar.vue';
import MemberSource from './member_source.vue';
import CreatedAt from './created_at.vue';
import ExpiresAt from './expires_at.vue';
import MemberActionButtons from './member_action_buttons.vue';
import RoleDropdown from './role_dropdown.vue';
-import RemoveGroupLinkModal from '../modals/remove_group_link_modal.vue';
import ExpirationDatepicker from './expiration_datepicker.vue';
export default {
@@ -32,7 +32,7 @@ export default {
import('ee_component/members/components/ldap/ldap_override_confirmation_modal.vue'),
},
computed: {
- ...mapState(['members', 'tableFields', 'tableAttrs', 'currentUserId', 'sourceId']),
+ ...mapState(['members', 'tableFields', 'tableAttrs', 'currentUserId']),
filteredFields() {
return FIELDS.filter(
(field) => this.tableFields.includes(field.key) && this.showField(field),
@@ -55,9 +55,9 @@ export default {
methods: {
hasActionButtons(member) {
return (
- canRemove(member, this.sourceId) ||
+ canRemove(member) ||
canResend(member) ||
- canUpdate(member, this.currentUserId, this.sourceId) ||
+ canUpdate(member, this.currentUserId) ||
canOverride(member)
);
},
@@ -80,7 +80,7 @@ export default {
return 'col-actions';
}
- return ['col-actions', 'gl-display-none!', 'gl-display-lg-table-cell!'];
+ return ['col-actions', 'gl-display-none!', 'gl-lg-display-table-cell!'];
},
tbodyTrAttr(member) {
return {
diff --git a/app/assets/javascripts/members/components/table/members_table_cell.vue b/app/assets/javascripts/members/components/table/members_table_cell.vue
index 20aa01b96bc..1f537740f94 100644
--- a/app/assets/javascripts/members/components/table/members_table_cell.vue
+++ b/app/assets/javascripts/members/components/table/members_table_cell.vue
@@ -19,7 +19,7 @@ export default {
},
},
computed: {
- ...mapState(['sourceId', 'currentUserId']),
+ ...mapState(['currentUserId']),
isGroup() {
return isGroup(this.member);
},
@@ -41,19 +41,19 @@ export default {
return MEMBER_TYPES.user;
},
isDirectMember() {
- return isDirectMember(this.member, this.sourceId);
+ return isDirectMember(this.member);
},
isCurrentUser() {
return isCurrentUser(this.member, this.currentUserId);
},
canRemove() {
- return canRemove(this.member, this.sourceId);
+ return canRemove(this.member);
},
canResend() {
return canResend(this.member);
},
canUpdate() {
- return canUpdate(this.member, this.currentUserId, this.sourceId);
+ return canUpdate(this.member, this.currentUserId);
},
},
render() {
diff --git a/app/assets/javascripts/members/constants.js b/app/assets/javascripts/members/constants.js
index 77cb150bff6..f68a8814fee 100644
--- a/app/assets/javascripts/members/constants.js
+++ b/app/assets/javascripts/members/constants.js
@@ -98,3 +98,8 @@ export const REMOVE_GROUP_LINK_MODAL_ID = 'remove-group-link-modal-id';
export const SEARCH_TOKEN_TYPE = 'filtered-search-term';
export const SORT_PARAM = 'sort';
+
+export const MEMBER_ACCESS_LEVEL_PROPERTY_NAME = 'access_level';
+
+export const GROUP_LINK_BASE_PROPERTY_NAME = 'group_link';
+export const GROUP_LINK_ACCESS_LEVEL_PROPERTY_NAME = 'group_access';
diff --git a/app/assets/javascripts/groups/members/index.js b/app/assets/javascripts/members/index.js
index 3ec874b8d36..bd80bb2485b 100644
--- a/app/assets/javascripts/groups/members/index.js
+++ b/app/assets/javascripts/members/index.js
@@ -1,11 +1,11 @@
import Vue from 'vue';
import Vuex from 'vuex';
import { GlToast } from '@gitlab/ui';
-import { parseDataAttributes } from 'ee_else_ce/groups/members/utils';
+import { parseDataAttributes } from 'ee_else_ce/members/utils';
import App from './components/app.vue';
-import membersStore from '~/members/store';
+import membersStore from './store';
-export const initGroupMembersApp = (
+export const initMembersApp = (
el,
{
tableFields = [],
diff --git a/app/assets/javascripts/members/store/actions.js b/app/assets/javascripts/members/store/actions.js
index 4c31b3c9744..7b191dd85d0 100644
--- a/app/assets/javascripts/members/store/actions.js
+++ b/app/assets/javascripts/members/store/actions.js
@@ -1,6 +1,6 @@
-import * as types from './mutation_types';
import axios from '~/lib/utils/axios_utils';
import { formatDate } from '~/lib/utils/datetime_utility';
+import * as types from './mutation_types';
export const updateMemberRole = async ({ state, commit }, { memberId, accessLevel }) => {
try {
@@ -11,7 +11,7 @@ export const updateMemberRole = async ({ state, commit }, { memberId, accessLeve
commit(types.RECEIVE_MEMBER_ROLE_SUCCESS, { memberId, accessLevel });
} catch (error) {
- commit(types.RECEIVE_MEMBER_ROLE_ERROR);
+ commit(types.RECEIVE_MEMBER_ROLE_ERROR, { error });
throw error;
}
@@ -37,7 +37,7 @@ export const updateMemberExpiration = async ({ state, commit }, { memberId, expi
expiresAt: expiresAt ? formatDate(expiresAt, 'isoUtcDateTime') : null,
});
} catch (error) {
- commit(types.RECEIVE_MEMBER_EXPIRATION_ERROR);
+ commit(types.RECEIVE_MEMBER_EXPIRATION_ERROR, { error });
throw error;
}
diff --git a/app/assets/javascripts/members/store/mutations.js b/app/assets/javascripts/members/store/mutations.js
index 2415e744290..f4aac1571d6 100644
--- a/app/assets/javascripts/members/store/mutations.js
+++ b/app/assets/javascripts/members/store/mutations.js
@@ -13,10 +13,10 @@ export default {
Vue.set(member, 'accessLevel', accessLevel);
},
- [types.RECEIVE_MEMBER_ROLE_ERROR](state) {
- state.errorMessage = s__(
- "Members|An error occurred while updating the member's role, please try again.",
- );
+ [types.RECEIVE_MEMBER_ROLE_ERROR](state, { error }) {
+ state.errorMessage =
+ error.response?.data?.message ||
+ s__("Members|An error occurred while updating the member's role, please try again.");
state.showError = true;
},
[types.RECEIVE_MEMBER_EXPIRATION_SUCCESS](state, { memberId, expiresAt }) {
@@ -28,10 +28,12 @@ export default {
Vue.set(member, 'expiresAt', expiresAt);
},
- [types.RECEIVE_MEMBER_EXPIRATION_ERROR](state) {
- state.errorMessage = s__(
- "Members|An error occurred while updating the member's expiration date, please try again.",
- );
+ [types.RECEIVE_MEMBER_EXPIRATION_ERROR](state, { error }) {
+ state.errorMessage =
+ error.response?.data?.message ||
+ s__(
+ "Members|An error occurred while updating the member's expiration date, please try again.",
+ );
state.showError = true;
},
[types.HIDE_ERROR](state) {
diff --git a/app/assets/javascripts/members/utils.js b/app/assets/javascripts/members/utils.js
index 780b5a9df57..bac83533214 100644
--- a/app/assets/javascripts/members/utils.js
+++ b/app/assets/javascripts/members/utils.js
@@ -1,7 +1,17 @@
+import { isUndefined } from 'lodash';
import { __ } from '~/locale';
-import { getParameterByName } from '~/lib/utils/common_utils';
+import {
+ getParameterByName,
+ convertObjectPropsToCamelCase,
+ parseBoolean,
+} from '~/lib/utils/common_utils';
import { setUrlParams } from '~/lib/utils/url_utility';
-import { FIELDS, DEFAULT_SORT } from './constants';
+import {
+ FIELDS,
+ DEFAULT_SORT,
+ GROUP_LINK_BASE_PROPERTY_NAME,
+ GROUP_LINK_ACCESS_LEVEL_PROPERTY_NAME,
+} from './constants';
export const generateBadges = (member, isCurrentUser) => [
{
@@ -25,26 +35,24 @@ export const isGroup = (member) => {
return Boolean(member.sharedWithGroup);
};
-export const isDirectMember = (member, sourceId) => {
- return isGroup(member) || member.source?.id === sourceId;
+export const isDirectMember = (member) => {
+ return isGroup(member) || member.isDirectMember;
};
export const isCurrentUser = (member, currentUserId) => {
return member.user?.id === currentUserId;
};
-export const canRemove = (member, sourceId) => {
- return isDirectMember(member, sourceId) && member.canRemove;
+export const canRemove = (member) => {
+ return isDirectMember(member) && member.canRemove;
};
export const canResend = (member) => {
return Boolean(member.invite?.canResend);
};
-export const canUpdate = (member, currentUserId, sourceId) => {
- return (
- !isCurrentUser(member, currentUserId) && isDirectMember(member, sourceId) && member.canUpdate
- );
+export const canUpdate = (member, currentUserId) => {
+ return !isCurrentUser(member, currentUserId) && isDirectMember(member) && member.canUpdate;
};
export const parseSortParam = (sortableFields) => {
@@ -95,3 +103,35 @@ export const buildSortHref = ({
// Defined in `ee/app/assets/javascripts/vue_shared/components/members/utils.js`
export const canOverride = () => false;
+
+export const parseDataAttributes = (el) => {
+ const { members, sourceId, memberPath, canManageMembers } = el.dataset;
+
+ return {
+ members: convertObjectPropsToCamelCase(JSON.parse(members), { deep: true }),
+ sourceId: parseInt(sourceId, 10),
+ memberPath,
+ canManageMembers: parseBoolean(canManageMembers),
+ };
+};
+
+export const baseRequestFormatter = (basePropertyName, accessLevelPropertyName) => ({
+ accessLevel,
+ ...otherProperties
+}) => {
+ const accessLevelProperty = !isUndefined(accessLevel)
+ ? { [accessLevelPropertyName]: accessLevel }
+ : {};
+
+ return {
+ [basePropertyName]: {
+ ...accessLevelProperty,
+ ...otherProperties,
+ },
+ };
+};
+
+export const groupLinkRequestFormatter = baseRequestFormatter(
+ GROUP_LINK_BASE_PROPERTY_NAME,
+ GROUP_LINK_ACCESS_LEVEL_PROPERTY_NAME,
+);
diff --git a/app/assets/javascripts/merge_conflicts/components/diff_file_editor.js b/app/assets/javascripts/merge_conflicts/components/diff_file_editor.js
index 338fbd9078a..87642c7a698 100644
--- a/app/assets/javascripts/merge_conflicts/components/diff_file_editor.js
+++ b/app/assets/javascripts/merge_conflicts/components/diff_file_editor.js
@@ -1,4 +1,7 @@
-/* eslint-disable no-param-reassign */
+// This is a true violation of @gitlab/no-runtime-template-compiler, as it relies on
+// app/views/projects/merge_requests/conflicts/components/_diff_file_editor.html.haml
+// for its template.
+/* eslint-disable no-param-reassign, @gitlab/no-runtime-template-compiler */
import Vue from 'vue';
import { debounce } from 'lodash';
@@ -90,9 +93,11 @@ import { __ } from '~/locale';
this.saved = true;
// This probably be better placed in the data provider
+ /* eslint-disable vue/no-mutating-props */
this.file.content = this.editor.getValue();
this.file.resolveEditChanged = this.file.content !== this.originalContent;
this.file.promptDiscardConfirmation = false;
+ /* eslint-enable vue/no-mutating-props */
},
resetEditorContent() {
if (this.fileLoaded) {
diff --git a/app/assets/javascripts/merge_conflicts/components/inline_conflict_lines.js b/app/assets/javascripts/merge_conflicts/components/inline_conflict_lines.js
index bc926cb9155..47214e288ae 100644
--- a/app/assets/javascripts/merge_conflicts/components/inline_conflict_lines.js
+++ b/app/assets/javascripts/merge_conflicts/components/inline_conflict_lines.js
@@ -1,4 +1,7 @@
-/* eslint-disable no-param-reassign */
+// This is a true violation of @gitlab/no-runtime-template-compiler, as it relies on
+// app/views/projects/merge_requests/conflicts/components/_inline_conflict_lines.html.haml
+// for its template.
+/* eslint-disable no-param-reassign, @gitlab/no-runtime-template-compiler */
import Vue from 'vue';
import actionsMixin from '../mixins/line_conflict_actions';
diff --git a/app/assets/javascripts/merge_conflicts/components/parallel_conflict_lines.js b/app/assets/javascripts/merge_conflicts/components/parallel_conflict_lines.js
index bb306e74825..1d5946cd78a 100644
--- a/app/assets/javascripts/merge_conflicts/components/parallel_conflict_lines.js
+++ b/app/assets/javascripts/merge_conflicts/components/parallel_conflict_lines.js
@@ -15,6 +15,9 @@ import utilsMixin from '../mixins/line_conflict_utils';
required: true,
},
},
+ // This is a true violation of @gitlab/no-runtime-template-compiler, as it
+ // has a template string.
+ // eslint-disable-next-line @gitlab/no-runtime-template-compiler
template: `
<table class="diff-wrap-lines code js-syntax-highlight">
<tr class="line_holder parallel" v-for="section in file.parallelLines">
diff --git a/app/assets/javascripts/merge_conflicts/merge_conflicts_bundle.js b/app/assets/javascripts/merge_conflicts/merge_conflicts_bundle.js
index 229f6f3e339..96b2e2a2240 100644
--- a/app/assets/javascripts/merge_conflicts/merge_conflicts_bundle.js
+++ b/app/assets/javascripts/merge_conflicts/merge_conflicts_bundle.js
@@ -1,15 +1,19 @@
+// This is a true violation of @gitlab/no-runtime-template-compiler, as it
+// relies on app/views/projects/merge_requests/conflicts/show.html.haml for its
+// template.
+/* eslint-disable @gitlab/no-runtime-template-compiler */
import $ from 'jquery';
import Vue from 'vue';
import FileIcon from '~/vue_shared/components/file_icon.vue';
+import { __ } from '~/locale';
import { deprecatedCreateFlash as createFlash } from '../flash';
import initIssuableSidebar from '../init_issuable_sidebar';
import './merge_conflict_store';
+import syntaxHighlight from '../syntax_highlight';
import MergeConflictsService from './merge_conflict_service';
import './components/diff_file_editor';
import './components/inline_conflict_lines';
import './components/parallel_conflict_lines';
-import syntaxHighlight from '../syntax_highlight';
-import { __ } from '~/locale';
export default function initMergeConflicts() {
const INTERACTIVE_RESOLVE_MODE = 'interactive';
diff --git a/app/assets/javascripts/merge_request.js b/app/assets/javascripts/merge_request.js
index bf9e0a309dd..7bcfe529175 100644
--- a/app/assets/javascripts/merge_request.js
+++ b/app/assets/javascripts/merge_request.js
@@ -1,10 +1,10 @@
/* eslint-disable func-names, no-underscore-dangle, consistent-return */
import $ from 'jquery';
-import axios from './lib/utils/axios_utils';
import { __ } from '~/locale';
import eventHub from '~/vue_merge_request_widget/event_hub';
import { deprecatedCreateFlash as createFlash } from '~/flash';
+import axios from './lib/utils/axios_utils';
import TaskList from './task_list';
import MergeRequestTabs from './merge_request_tabs';
import { addDelimiter } from './lib/utils/text_utility';
diff --git a/app/assets/javascripts/merge_request/components/status_box.vue b/app/assets/javascripts/merge_request/components/status_box.vue
index fd99802caff..5d2660d65e6 100644
--- a/app/assets/javascripts/merge_request/components/status_box.vue
+++ b/app/assets/javascripts/merge_request/components/status_box.vue
@@ -5,12 +5,14 @@ import mrEventHub from '../eventhub';
const CLASSES = {
opened: 'status-box-open',
+ locked: 'status-box-open',
closed: 'status-box-mr-closed',
merged: 'status-box-mr-merged',
};
const STATUS = {
opened: [__('Open'), 'issue-open-m'],
+ locked: [__('Open'), 'issue-open-m'],
closed: [__('Closed'), 'close'],
merged: [__('Merged'), 'git-merge'],
};
@@ -59,10 +61,10 @@ export default {
<div :class="statusBoxClass" class="issuable-status-box status-box">
<gl-icon
:name="statusIconName"
- class="gl-display-block gl-display-sm-none!"
+ class="gl-display-block gl-sm-display-none!"
data-testid="status-icon"
/>
- <span class="gl-display-none gl-display-sm-block">
+ <span class="gl-display-none gl-sm-display-block">
{{ statusHumanName }}
</span>
</div>
diff --git a/app/assets/javascripts/milestone_select.js b/app/assets/javascripts/milestone_select.js
index 921925e15c5..badd87921d4 100644
--- a/app/assets/javascripts/milestone_select.js
+++ b/app/assets/javascripts/milestone_select.js
@@ -95,14 +95,19 @@ export default class MilestoneSelect {
name: m.title,
}))
.sort((mA, mB) => {
+ const dueDateA = mA.due_date ? parsePikadayDate(mA.due_date) : null;
+ const dueDateB = mB.due_date ? parsePikadayDate(mB.due_date) : null;
+
// Move all expired milestones to the bottom.
- if (mA.expired) {
- return 1;
- }
- if (mB.expired) {
- return -1;
- }
- return 0;
+ if (mA.expired) return 1;
+ if (mB.expired) return -1;
+
+ // Move milestones without due dates just above expired milestones.
+ if (!dueDateA) return 1;
+ if (!dueDateB) return -1;
+
+ // Sort by due date in ascending order.
+ return dueDateA - dueDateB;
}),
)
.then((data) => {
diff --git a/app/assets/javascripts/milestones/components/milestone_results_section.vue b/app/assets/javascripts/milestones/components/milestone_results_section.vue
index d53a59e58d4..b866977b974 100644
--- a/app/assets/javascripts/milestones/components/milestone_results_section.vue
+++ b/app/assets/javascripts/milestones/components/milestone_results_section.vue
@@ -77,12 +77,7 @@ export default {
</div>
</template>
<template v-else>
- <gl-dropdown-item
- v-for="{ title } in items"
- :key="title"
- role="milestone option"
- @click="$emit('selected', title)"
- >
+ <gl-dropdown-item v-for="{ title } in items" :key="title" @click="$emit('selected', title)">
<span class="gl-pl-6" :class="{ 'selected-item': isSelectedMilestone(title) }">
{{ title }}
</span>
diff --git a/app/assets/javascripts/mirrors/mirror_repos.js b/app/assets/javascripts/mirrors/mirror_repos.js
index f7200f22471..b4e34021e36 100644
--- a/app/assets/javascripts/mirrors/mirror_repos.js
+++ b/app/assets/javascripts/mirrors/mirror_repos.js
@@ -3,8 +3,8 @@ import { debounce } from 'lodash';
import { __ } from '~/locale';
import { deprecatedCreateFlash as Flash } from '~/flash';
import axios from '~/lib/utils/axios_utils';
-import SSHMirror from './ssh_mirror';
import { hide } from '~/tooltips';
+import SSHMirror from './ssh_mirror';
export default class MirrorRepos {
constructor(container) {
diff --git a/app/assets/javascripts/monitoring/components/alert_widget.vue b/app/assets/javascripts/monitoring/components/alert_widget.vue
index bf31b86561a..0ffbf169941 100644
--- a/app/assets/javascripts/monitoring/components/alert_widget.vue
+++ b/app/assets/javascripts/monitoring/components/alert_widget.vue
@@ -3,10 +3,11 @@ import { GlBadge, GlLoadingIcon, GlModalDirective, GlIcon, GlTooltip, GlSprintf
import { values, get } from 'lodash';
import { s__ } from '~/locale';
import { deprecatedCreateFlash as createFlash } from '~/flash';
-import AlertWidgetForm from './alert_widget_form.vue';
+import { BV_SHOW_MODAL, BV_HIDE_MODAL } from '~/lib/utils/constants';
import AlertsService from '../services/alerts_service';
import { alertsValidator, queriesValidator } from '../validators';
import { OPERATORS } from '../constants';
+import AlertWidgetForm from './alert_widget_form.vue';
export default {
components: {
@@ -165,11 +166,11 @@ export default {
return get(alertQuery, 'result[0].values', []).map((value) => get(value, '[1]', null));
},
showModal() {
- this.$root.$emit('bv::show::modal', this.modalId);
+ this.$root.$emit(BV_SHOW_MODAL, this.modalId);
},
hideModal() {
this.errorMessage = null;
- this.$root.$emit('bv::hide::modal', this.modalId);
+ this.$root.$emit(BV_HIDE_MODAL, this.modalId);
},
handleSetApiAction(apiAction) {
this.apiAction = apiAction;
diff --git a/app/assets/javascripts/monitoring/components/charts/column.vue b/app/assets/javascripts/monitoring/components/charts/column.vue
index ba947c2fa9c..bb1882c90af 100644
--- a/app/assets/javascripts/monitoring/components/charts/column.vue
+++ b/app/assets/javascripts/monitoring/components/charts/column.vue
@@ -2,11 +2,11 @@
import { GlResizeObserverDirective } from '@gitlab/ui';
import { GlColumnChart } from '@gitlab/ui/dist/charts';
import { getSvgIconPathContent } from '~/lib/utils/icon_utils';
-import { chartHeight } from '../../constants';
import { makeDataSeries } from '~/helpers/monitor_helper';
+import { chartHeight } from '../../constants';
import { graphDataValidatorForValues } from '../../utils';
-import { getTimeAxisOptions, getYAxisOptions, getChartGrid } from './options';
import { timezones } from '../../format_date';
+import { getTimeAxisOptions, getYAxisOptions, getChartGrid } from './options';
export default {
components: {
diff --git a/app/assets/javascripts/monitoring/components/charts/gauge.vue b/app/assets/javascripts/monitoring/components/charts/gauge.vue
index 63fa60bbdf0..461ff06be72 100644
--- a/app/assets/javascripts/monitoring/components/charts/gauge.vue
+++ b/app/assets/javascripts/monitoring/components/charts/gauge.vue
@@ -2,9 +2,9 @@
import { GlResizeObserverDirective } from '@gitlab/ui';
import { GlGaugeChart } from '@gitlab/ui/dist/charts';
import { isFinite, isArray, isInteger } from 'lodash';
+import { getFormatter, SUPPORTED_FORMATS } from '~/lib/utils/unit_format';
import { graphDataValidatorForValues } from '../../utils';
import { getValidThresholds } from './options';
-import { getFormatter, SUPPORTED_FORMATS } from '~/lib/utils/unit_format';
export default {
components: {
diff --git a/app/assets/javascripts/monitoring/components/charts/single_stat.vue b/app/assets/javascripts/monitoring/components/charts/single_stat.vue
index a8ab41ebf26..4cf583fed18 100644
--- a/app/assets/javascripts/monitoring/components/charts/single_stat.vue
+++ b/app/assets/javascripts/monitoring/components/charts/single_stat.vue
@@ -45,12 +45,18 @@ export default {
}
if (this.graphData?.maxValue) {
- formatter = getFormatter(SUPPORTED_FORMATS.percent);
- return formatter(this.queryResult / Number(this.graphData.maxValue), defaultPrecision);
+ formatter = getFormatter(SUPPORTED_FORMATS.number);
+ return formatter(
+ (this.queryResult / Number(this.graphData.maxValue)) * 100,
+ defaultPrecision,
+ );
}
formatter = getFormatter(SUPPORTED_FORMATS.number);
- return `${formatter(this.queryResult, defaultPrecision)}${this.queryInfo.unit ?? ''}`;
+ return `${formatter(this.queryResult, defaultPrecision)}`;
+ },
+ unit() {
+ return this.graphData?.maxValue ? '%' : this.queryInfo.unit;
},
graphTitle() {
return this.queryInfo.label;
@@ -60,6 +66,6 @@ export default {
</script>
<template>
<div>
- <gl-single-stat :value="statValue" :title="graphTitle" variant="success" />
+ <gl-single-stat :value="statValue" :title="graphTitle" :unit="unit" variant="success" />
</div>
</template>
diff --git a/app/assets/javascripts/monitoring/components/charts/stacked_column.vue b/app/assets/javascripts/monitoring/components/charts/stacked_column.vue
index b5ae6bcfd13..bfa0bd5333b 100644
--- a/app/assets/javascripts/monitoring/components/charts/stacked_column.vue
+++ b/app/assets/javascripts/monitoring/components/charts/stacked_column.vue
@@ -2,11 +2,11 @@
import { GlResizeObserverDirective } from '@gitlab/ui';
import { GlStackedColumnChart } from '@gitlab/ui/dist/charts';
import { getSvgIconPathContent } from '~/lib/utils/icon_utils';
-import { chartHeight, legendLayoutTypes } from '../../constants';
import { s__ } from '~/locale';
+import { chartHeight, legendLayoutTypes } from '../../constants';
import { graphDataValidatorForValues } from '../../utils';
-import { getTimeAxisOptions, axisTypes } from './options';
import { formats, timezones } from '../../format_date';
+import { getTimeAxisOptions, axisTypes } from './options';
export default {
components: {
diff --git a/app/assets/javascripts/monitoring/components/charts/time_series.vue b/app/assets/javascripts/monitoring/components/charts/time_series.vue
index e9f7b11c977..b720700d36d 100644
--- a/app/assets/javascripts/monitoring/components/charts/time_series.vue
+++ b/app/assets/javascripts/monitoring/components/charts/time_series.vue
@@ -4,12 +4,12 @@ import { GlLink, GlTooltip, GlResizeObserverDirective, GlIcon } from '@gitlab/ui
import { GlAreaChart, GlLineChart, GlChartSeriesLabel } from '@gitlab/ui/dist/charts';
import { s__ } from '~/locale';
import { getSvgIconPathContent } from '~/lib/utils/icon_utils';
-import { panelTypes, chartHeight, lineTypes, lineWidths, legendLayoutTypes } from '../../constants';
-import { getYAxisOptions, getTimeAxisOptions, getChartGrid, getTooltipFormatter } from './options';
-import { annotationsYAxis, generateAnnotationsSeries } from './annotations';
import { makeDataSeries } from '~/helpers/monitor_helper';
+import { panelTypes, chartHeight, lineTypes, lineWidths, legendLayoutTypes } from '../../constants';
import { graphDataValidatorForValues } from '../../utils';
import { formatDate, timezones } from '../../format_date';
+import { getYAxisOptions, getTimeAxisOptions, getChartGrid, getTooltipFormatter } from './options';
+import { annotationsYAxis, generateAnnotationsSeries } from './annotations';
export const timestampToISODate = (timestamp) => new Date(timestamp).toISOString();
diff --git a/app/assets/javascripts/monitoring/components/dashboard.vue b/app/assets/javascripts/monitoring/components/dashboard.vue
index 16c2c87a4b7..5ec87332288 100644
--- a/app/assets/javascripts/monitoring/components/dashboard.vue
+++ b/app/assets/javascripts/monitoring/components/dashboard.vue
@@ -3,21 +3,13 @@ import { mapActions, mapState, mapGetters } from 'vuex';
import VueDraggable from 'vuedraggable';
import Mousetrap from 'mousetrap';
import { GlButton, GlModalDirective, GlTooltipDirective, GlIcon } from '@gitlab/ui';
-import DashboardHeader from './dashboard_header.vue';
-import DashboardPanel from './dashboard_panel.vue';
import { s__ } from '~/locale';
import { deprecatedCreateFlash as createFlash } from '~/flash';
import { ESC_KEY } from '~/lib/utils/keys';
import { mergeUrlParams, updateHistory } from '~/lib/utils/url_utility';
import invalidUrl from '~/lib/utils/invalid_url';
-
-import GraphGroup from './graph_group.vue';
-import EmptyState from './empty_state.vue';
-import GroupEmptyState from './group_empty_state.vue';
-import VariablesSection from './variables_section.vue';
-import LinksSection from './links_section.vue';
-
import TrackEventDirective from '~/vue_shared/directives/track_event';
+import { defaultTimeRange } from '~/vue_shared/constants';
import {
timeRangeFromUrl,
panelToUrl,
@@ -25,7 +17,14 @@ import {
convertVariablesForURL,
} from '../utils';
import { metricStates, keyboardShortcutKeys } from '../constants';
-import { defaultTimeRange } from '~/vue_shared/constants';
+import DashboardHeader from './dashboard_header.vue';
+import DashboardPanel from './dashboard_panel.vue';
+
+import GraphGroup from './graph_group.vue';
+import EmptyState from './empty_state.vue';
+import GroupEmptyState from './group_empty_state.vue';
+import VariablesSection from './variables_section.vue';
+import LinksSection from './links_section.vue';
export default {
components: {
diff --git a/app/assets/javascripts/monitoring/components/dashboard_actions_menu.vue b/app/assets/javascripts/monitoring/components/dashboard_actions_menu.vue
index 9d1926dca54..77d3766d4bc 100644
--- a/app/assets/javascripts/monitoring/components/dashboard_actions_menu.vue
+++ b/app/assets/javascripts/monitoring/components/dashboard_actions_menu.vue
@@ -11,14 +11,14 @@ import {
GlTooltipDirective,
} from '@gitlab/ui';
import CustomMetricsFormFields from '~/custom_metrics/components/custom_metrics_form_fields.vue';
-import { PANEL_NEW_PAGE } from '../router/constants';
-import DuplicateDashboardModal from './duplicate_dashboard_modal.vue';
-import CreateDashboardModal from './create_dashboard_modal.vue';
import { s__ } from '~/locale';
import invalidUrl from '~/lib/utils/invalid_url';
import { redirectTo } from '~/lib/utils/url_utility';
import TrackEventDirective from '~/vue_shared/directives/track_event';
+import { PANEL_NEW_PAGE } from '../router/constants';
import { getAddMetricTrackingOptions } from '../utils';
+import CreateDashboardModal from './create_dashboard_modal.vue';
+import DuplicateDashboardModal from './duplicate_dashboard_modal.vue';
export default {
components: {
diff --git a/app/assets/javascripts/monitoring/components/dashboard_header.vue b/app/assets/javascripts/monitoring/components/dashboard_header.vue
index 0f6a9ce3814..0303c47203f 100644
--- a/app/assets/javascripts/monitoring/components/dashboard_header.vue
+++ b/app/assets/javascripts/monitoring/components/dashboard_header.vue
@@ -16,14 +16,13 @@ import { mergeUrlParams, redirectTo } from '~/lib/utils/url_utility';
import invalidUrl from '~/lib/utils/invalid_url';
import DateTimePicker from '~/vue_shared/components/date_time_picker/date_time_picker.vue';
+import { timeRanges } from '~/vue_shared/constants';
+import { timeRangeToUrl } from '../utils';
+import { timezones } from '../format_date';
import DashboardsDropdown from './dashboards_dropdown.vue';
import RefreshButton from './refresh_button.vue';
import ActionsMenu from './dashboard_actions_menu.vue';
-import { timeRangeToUrl } from '../utils';
-import { timeRanges } from '~/vue_shared/constants';
-import { timezones } from '../format_date';
-
export default {
components: {
GlIcon,
diff --git a/app/assets/javascripts/monitoring/components/dashboard_panel.vue b/app/assets/javascripts/monitoring/components/dashboard_panel.vue
index 2b0c3d03b8d..bf4f52bb4b9 100644
--- a/app/assets/javascripts/monitoring/components/dashboard_panel.vue
+++ b/app/assets/javascripts/monitoring/components/dashboard_panel.vue
@@ -19,8 +19,12 @@ import invalidUrl from '~/lib/utils/invalid_url';
import { convertToFixedRange } from '~/lib/utils/datetime_range';
import { relativePathToAbsolute, getBaseURL, visitUrl, isSafeURL } from '~/lib/utils/url_utility';
import { __, n__ } from '~/locale';
+import { BV_SHOW_MODAL } from '~/lib/utils/constants';
+import TrackEventDirective from '~/vue_shared/directives/track_event';
import { panelTypes } from '../constants';
+import { timeRangeToUrl, downloadCSVOptions, generateLinkToChartOptions } from '../utils';
+import { graphDataToCsv } from '../csv_export';
import MonitorEmptyChart from './charts/empty_chart.vue';
import MonitorTimeSeriesChart from './charts/time_series.vue';
import MonitorAnomalyChart from './charts/anomaly.vue';
@@ -31,10 +35,7 @@ import MonitorColumnChart from './charts/column.vue';
import MonitorBarChart from './charts/bar.vue';
import MonitorStackedColumnChart from './charts/stacked_column.vue';
-import TrackEventDirective from '~/vue_shared/directives/track_event';
import AlertWidget from './alert_widget.vue';
-import { timeRangeToUrl, downloadCSVOptions, generateLinkToChartOptions } from '../utils';
-import { graphDataToCsv } from '../csv_export';
const events = {
timeRangeZoom: 'timerangezoom',
@@ -318,7 +319,7 @@ export default {
return isSafeURL(url) ? url : '#';
},
showAlertModal() {
- this.$root.$emit('bv::show::modal', this.alertModalId);
+ this.$root.$emit(BV_SHOW_MODAL, this.alertModalId);
},
showAlertModalFromKeyboardShortcut() {
if (this.isContextualMenuShown) {
diff --git a/app/assets/javascripts/monitoring/components/links_section.vue b/app/assets/javascripts/monitoring/components/links_section.vue
index ca1e9c4d0d4..1fa36c14242 100644
--- a/app/assets/javascripts/monitoring/components/links_section.vue
+++ b/app/assets/javascripts/monitoring/components/links_section.vue
@@ -15,12 +15,12 @@ export default {
<template>
<div
ref="linksSection"
- class="gl-display-sm-flex gl-flex-sm-wrap gl-mt-5 gl-p-3 gl-bg-gray-10 border gl-rounded-base links-section"
+ class="gl-sm-display-flex gl-flex-sm-wrap gl-mt-5 gl-p-3 gl-bg-gray-10 border gl-rounded-base links-section"
>
<div
v-for="(link, key) in links"
:key="key"
- class="gl-mb-1 gl-mr-5 gl-display-flex gl-display-sm-block gl-hover-text-blue-600-children gl-word-break-all"
+ class="gl-mb-1 gl-mr-5 gl-display-flex gl-sm-display-block gl-hover-text-blue-600-children gl-word-break-all"
>
<gl-link :href="link.url" class="gl-text-gray-900 gl-text-decoration-none!"
><gl-icon name="link" class="gl-text-gray-500 gl-vertical-align-text-bottom gl-mr-2" />{{
diff --git a/app/assets/javascripts/monitoring/components/variables_section.vue b/app/assets/javascripts/monitoring/components/variables_section.vue
index 7c4fb135ec8..a4aa00abbd1 100644
--- a/app/assets/javascripts/monitoring/components/variables_section.vue
+++ b/app/assets/javascripts/monitoring/components/variables_section.vue
@@ -1,9 +1,9 @@
<script>
import { mapState, mapActions } from 'vuex';
-import DropdownField from './variables/dropdown_field.vue';
-import TextField from './variables/text_field.vue';
import { setCustomVariablesFromUrl } from '../utils';
import { VARIABLE_TYPES } from '../constants';
+import DropdownField from './variables/dropdown_field.vue';
+import TextField from './variables/text_field.vue';
export default {
components: {
diff --git a/app/assets/javascripts/monitoring/monitoring_app.js b/app/assets/javascripts/monitoring/monitoring_app.js
index 307154c9a84..2f8c3e2a0c4 100644
--- a/app/assets/javascripts/monitoring/monitoring_app.js
+++ b/app/assets/javascripts/monitoring/monitoring_app.js
@@ -26,7 +26,24 @@ export default (props = {}) => {
dashboardProps: { ...dataProps, ...props },
};
},
- template: `<router-view :dashboardProps="dashboardProps"/>`,
+ render(h) {
+ return h('RouterView', {
+ // This is attrs rather than props because:
+ // 1. RouterView only actually defines one prop: `name`.
+ // 2. The RouterView [throws away other props][1] given to it, in
+ // favour of those configured in the route config/params.
+ // 3. The Vue template compiler itself in general compiles anything
+ // like <some-component :foo="bar" /> into roughly
+ // h('some-component', { attrs: { foo: bar } }). Then later, Vue
+ // [extract props from attrs and merges them with props][2],
+ // matching them up according to the component's definition.
+ // [1]: https://github.com/vuejs/vue-router/blob/v3.4.9/src/components/view.js#L124
+ // [2]: https://github.com/vuejs/vue/blob/v2.6.12/src/core/vdom/helpers/extract-props.js#L12-L50
+ attrs: {
+ dashboardProps: this.dashboardProps,
+ },
+ });
+ },
});
}
};
diff --git a/app/assets/javascripts/monitoring/stores/actions.js b/app/assets/javascripts/monitoring/stores/actions.js
index 44c200cdb54..391cb35882c 100644
--- a/app/assets/javascripts/monitoring/stores/actions.js
+++ b/app/assets/javascripts/monitoring/stores/actions.js
@@ -1,14 +1,7 @@
import * as Sentry from '~/sentry/wrapper';
-import * as types from './mutation_types';
import axios from '~/lib/utils/axios_utils';
import { deprecatedCreateFlash as createFlash } from '~/flash';
import { convertToFixedRange } from '~/lib/utils/datetime_range';
-import {
- gqClient,
- parseEnvironmentsResponse,
- parseAnnotationsResponse,
- removeLeadingSlash,
-} from './utils';
import trackDashboardLoad from '../monitoring_tracking_helper';
import getEnvironments from '../queries/getEnvironments.query.graphql';
import getAnnotations from '../queries/getAnnotations.query.graphql';
@@ -18,6 +11,13 @@ import { s__, sprintf } from '../../locale';
import { getDashboard, getPrometheusQueryData } from '../requests';
import { ENVIRONMENT_AVAILABLE_STATE, OVERVIEW_DASHBOARD_PATH, VARIABLE_TYPES } from '../constants';
+import {
+ gqClient,
+ parseEnvironmentsResponse,
+ parseAnnotationsResponse,
+ removeLeadingSlash,
+} from './utils';
+import * as types from './mutation_types';
const axiosCancelToken = axios.CancelToken;
let cancelTokenSource;
diff --git a/app/assets/javascripts/monitoring/stores/mutations.js b/app/assets/javascripts/monitoring/stores/mutations.js
index 5c5a7d03b97..37febd062ff 100644
--- a/app/assets/javascripts/monitoring/stores/mutations.js
+++ b/app/assets/javascripts/monitoring/stores/mutations.js
@@ -1,10 +1,10 @@
import Vue from 'vue';
import { pick } from 'lodash';
-import * as types from './mutation_types';
-import { mapToDashboardViewModel, mapPanelToViewModel, normalizeQueryResponseData } from './utils';
import httpStatusCodes from '~/lib/utils/http_status';
import { BACKOFF_TIMEOUT } from '~/lib/utils/common_utils';
import { dashboardEmptyStates, endpointKeys, initialStateKeys, metricStates } from '../constants';
+import { mapToDashboardViewModel, mapPanelToViewModel, normalizeQueryResponseData } from './utils';
+import * as types from './mutation_types';
import { optionsFromSeriesData } from './variable_mapping';
/**
diff --git a/app/assets/javascripts/monitoring/stores/state.js b/app/assets/javascripts/monitoring/stores/state.js
index ef8b1adb624..b8ecb15a72f 100644
--- a/app/assets/javascripts/monitoring/stores/state.js
+++ b/app/assets/javascripts/monitoring/stores/state.js
@@ -1,7 +1,7 @@
import invalidUrl from '~/lib/utils/invalid_url';
+import { defaultTimeRange } from '~/vue_shared/constants';
import { timezones } from '../format_date';
import { dashboardEmptyStates } from '../constants';
-import { defaultTimeRange } from '~/vue_shared/constants';
export default () => ({
// API endpoints
diff --git a/app/assets/javascripts/monitoring/stores/utils.js b/app/assets/javascripts/monitoring/stores/utils.js
index 36e5a135d59..305080cc104 100644
--- a/app/assets/javascripts/monitoring/stores/utils.js
+++ b/app/assets/javascripts/monitoring/stores/utils.js
@@ -2,11 +2,11 @@ import { slugify } from '~/lib/utils/text_utility';
import createGqClient, { fetchPolicies } from '~/lib/graphql';
import { SUPPORTED_FORMATS } from '~/lib/utils/unit_format';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
-import { mergeURLVariables, parseTemplatingVariables } from './variable_mapping';
import { DATETIME_RANGE_TYPES } from '~/lib/utils/constants';
import { timeRangeToParams, getRangeType } from '~/lib/utils/datetime_range';
import { isSafeURL, mergeUrlParams } from '~/lib/utils/url_utility';
import { NOT_IN_DB_PREFIX, linkTypes, OUT_OF_THE_BOX_DASHBOARDS_PATH_PREFIX } from '../constants';
+import { mergeURLVariables, parseTemplatingVariables } from './variable_mapping';
export const gqClient = createGqClient(
{},
diff --git a/app/assets/javascripts/mr_notes/index.js b/app/assets/javascripts/mr_notes/index.js
index fb6ef0249bb..e59991e638b 100644
--- a/app/assets/javascripts/mr_notes/index.js
+++ b/app/assets/javascripts/mr_notes/index.js
@@ -1,12 +1,13 @@
import Vue from 'vue';
import store from '~/mr_notes/stores';
-import initNotesApp from './init_notes';
+import initRevertCommitModal from '~/projects/commit/init_revert_commit_modal';
import initDiffsApp from '../diffs';
import discussionCounter from '../notes/components/discussion_counter.vue';
import initDiscussionFilters from '../notes/discussion_filters';
import initSortDiscussions from '../notes/sort_discussions';
import MergeRequest from '../merge_request';
import { resetServiceWorkersPublicPath } from '../lib/utils/webpack';
+import initNotesApp from './init_notes';
export default function initMrNotes() {
resetServiceWorkersPublicPath();
@@ -19,6 +20,10 @@ export default function initMrNotes() {
initNotesApp();
+ document.addEventListener('merged:UpdateActions', () => {
+ initRevertCommitModal();
+ });
+
// eslint-disable-next-line no-new
new Vue({
el: '#js-vue-discussion-counter',
diff --git a/app/assets/javascripts/mr_notes/init_notes.js b/app/assets/javascripts/mr_notes/init_notes.js
index ab88a610469..958ee8e226a 100644
--- a/app/assets/javascripts/mr_notes/init_notes.js
+++ b/app/assets/javascripts/mr_notes/init_notes.js
@@ -2,10 +2,10 @@ import $ from 'jquery';
import Vue from 'vue';
import { mapActions, mapState, mapGetters } from 'vuex';
import store from '~/mr_notes/stores';
+import { parseBoolean } from '~/lib/utils/common_utils';
import notesApp from '../notes/components/notes_app.vue';
import discussionNavigator from '../notes/components/discussion_navigator.vue';
import initWidget from '../vue_merge_request_widget';
-import { parseBoolean } from '~/lib/utils/common_utils';
export default () => {
// eslint-disable-next-line no-new
diff --git a/app/assets/javascripts/mr_popover/index.js b/app/assets/javascripts/mr_popover/index.js
index 03ddfd13d50..714cf67e0bd 100644
--- a/app/assets/javascripts/mr_popover/index.js
+++ b/app/assets/javascripts/mr_popover/index.js
@@ -1,7 +1,7 @@
import Vue from 'vue';
import VueApollo from 'vue-apollo';
-import MRPopover from './components/mr_popover.vue';
import createDefaultClient from '~/lib/graphql';
+import MRPopover from './components/mr_popover.vue';
let renderedPopover;
let renderFn;
diff --git a/app/assets/javascripts/namespace_select.js b/app/assets/javascripts/namespace_select.js
index e668b492ebe..16acf130c9f 100644
--- a/app/assets/javascripts/namespace_select.js
+++ b/app/assets/javascripts/namespace_select.js
@@ -1,9 +1,9 @@
import $ from 'jquery';
+import { parseBoolean } from '~/lib/utils/common_utils';
+import initDeprecatedJQueryDropdown from '~/deprecated_jquery_dropdown';
import Api from './api';
import { mergeUrlParams } from './lib/utils/url_utility';
-import { parseBoolean } from '~/lib/utils/common_utils';
import { __ } from './locale';
-import initDeprecatedJQueryDropdown from '~/deprecated_jquery_dropdown';
export default class NamespaceSelect {
constructor(opts) {
diff --git a/app/assets/javascripts/notes.js b/app/assets/javascripts/notes.js
index 857e5a34db6..78e89f28542 100644
--- a/app/assets/javascripts/notes.js
+++ b/app/assets/javascripts/notes.js
@@ -904,18 +904,7 @@ export default class Notes {
// DiffNote
form.find('#note_position').val(dataHolder.attr('data-position'));
- form
- .prepend(
- `<a href="${escape(
- gon.current_username,
- )}" class="user-avatar-link d-none d-sm-block"><img class="avatar s40" src="${encodeURI(
- gon.current_user_avatar_url || gon.default_avatar_url,
- )}" alt="${escape(gon.current_user_fullname)}" /></a>`,
- )
- .append('</div>')
- .find('.js-close-discussion-note-form')
- .show()
- .removeClass('hide');
+ form.append('</div>').find('.js-close-discussion-note-form').show().removeClass('hide');
form.find('.js-note-target-close').remove();
form.find('.js-note-new-discussion').remove();
this.setupNoteForm(form);
diff --git a/app/assets/javascripts/notes/components/comment_field_layout.vue b/app/assets/javascripts/notes/components/comment_field_layout.vue
index aaf64702ffd..47d14783d5d 100644
--- a/app/assets/javascripts/notes/components/comment_field_layout.vue
+++ b/app/assets/javascripts/notes/components/comment_field_layout.vue
@@ -1,6 +1,6 @@
<script>
-import EmailParticipantsWarning from './email_participants_warning.vue';
import NoteableWarning from '~/vue_shared/components/notes/noteable_warning.vue';
+import EmailParticipantsWarning from './email_participants_warning.vue';
const DEFAULT_NOTEABLE_TYPE = 'Issue';
diff --git a/app/assets/javascripts/notes/components/comment_form.vue b/app/assets/javascripts/notes/components/comment_form.vue
index 111af977ec5..bc2b2d6d5d0 100644
--- a/app/assets/javascripts/notes/components/comment_form.vue
+++ b/app/assets/javascripts/notes/components/comment_form.vue
@@ -15,14 +15,13 @@ import {
slugifyWithUnderscore,
} from '~/lib/utils/text_utility';
import { refreshUserMergeRequestCounts } from '~/commons/nav/user_merge_requests';
-import * as constants from '../constants';
-import eventHub from '../event_hub';
import markdownField from '~/vue_shared/components/markdown/field.vue';
-import userAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
+import * as constants from '../constants';
+import eventHub from '../event_hub';
+import issuableStateMixin from '../mixins/issuable_state';
import noteSignedOutWidget from './note_signed_out_widget.vue';
import discussionLockedWidget from './discussion_locked_widget.vue';
-import issuableStateMixin from '../mixins/issuable_state';
import CommentFieldLayout from './comment_field_layout.vue';
export default {
@@ -31,7 +30,6 @@ export default {
noteSignedOutWidget,
discussionLockedWidget,
markdownField,
- userAvatarLink,
GlButton,
TimelineEntryItem,
GlIcon,
@@ -145,6 +143,9 @@ export default {
trackingLabel() {
return slugifyWithUnderscore(`${this.commentButtonTitle} button`);
},
+ hasCloseAndCommentButton() {
+ return !this.glFeatures.removeCommentCloseReopen;
+ },
},
watch: {
note(newNote) {
@@ -301,15 +302,6 @@ export default {
<ul v-else-if="canCreateNote" class="notes notes-form timeline">
<timeline-entry-item class="note-form">
<div class="flash-container error-alert timeline-content"></div>
- <div class="timeline-icon d-none d-md-block">
- <user-avatar-link
- v-if="author"
- :link-href="author.path"
- :img-src="author.avatar_url"
- :img-alt="author.name"
- :img-size="40"
- />
- </div>
<div class="timeline-content timeline-content-form">
<form ref="commentForm" class="new-note common-note-form gfm-form js-main-target-form">
<comment-field-layout
@@ -384,7 +376,7 @@ export default {
class="btn btn-transparent"
@click.prevent="setNoteType('comment')"
>
- <gl-icon name="check" class="icon" />
+ <gl-icon name="check" class="icon gl-flex-shrink-0" />
<div class="description">
<strong>{{ __('Comment') }}</strong>
<p>
@@ -400,10 +392,12 @@ export default {
<li class="divider droplab-item-ignore"></li>
<li :class="{ 'droplab-item-selected': noteType === 'discussion' }">
<button
+ type="button"
+ class="btn btn-transparent"
data-qa-selector="discussion_menu_item"
@click.prevent="setNoteType('discussion')"
>
- <gl-icon name="check" class="icon" />
+ <gl-icon name="check" class="icon gl-flex-shrink-0" />
<div class="description">
<strong>{{ __('Start thread') }}</strong>
<p>{{ startDiscussionDescription }}</p>
@@ -414,7 +408,7 @@ export default {
</div>
<gl-button
- v-if="canToggleIssueState"
+ v-if="hasCloseAndCommentButton && canToggleIssueState"
:loading="isToggleStateButtonLoading"
category="secondary"
:variant="buttonVariant"
diff --git a/app/assets/javascripts/notes/components/discussion_actions.vue b/app/assets/javascripts/notes/components/discussion_actions.vue
index da4134ab2c4..27408bc3354 100644
--- a/app/assets/javascripts/notes/components/discussion_actions.vue
+++ b/app/assets/javascripts/notes/components/discussion_actions.vue
@@ -1,8 +1,8 @@
<script>
+import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import ReplyPlaceholder from './discussion_reply_placeholder.vue';
import ResolveDiscussionButton from './discussion_resolve_button.vue';
import ResolveWithIssueButton from './discussion_resolve_with_issue_button.vue';
-import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
export default {
name: 'DiscussionActions',
diff --git a/app/assets/javascripts/notes/components/discussion_notes.vue b/app/assets/javascripts/notes/components/discussion_notes.vue
index 8ac915c3c03..fc82f8e6b89 100644
--- a/app/assets/javascripts/notes/components/discussion_notes.vue
+++ b/app/assets/javascripts/notes/components/discussion_notes.vue
@@ -1,15 +1,15 @@
<script>
import { mapGetters, mapActions } from 'vuex';
-import { SYSTEM_NOTE } from '../constants';
import { __ } from '~/locale';
import PlaceholderNote from '~/vue_shared/components/notes/placeholder_note.vue';
import PlaceholderSystemNote from '~/vue_shared/components/notes/placeholder_system_note.vue';
import SystemNote from '~/vue_shared/components/notes/system_note.vue';
+import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
+import { SYSTEM_NOTE } from '../constants';
import NoteableNote from './noteable_note.vue';
import ToggleRepliesWidget from './toggle_replies_widget.vue';
import NoteEditedText from './note_edited_text.vue';
import DiscussionNotesRepliesWrapper from './discussion_notes_replies_wrapper.vue';
-import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
export default {
name: 'DiscussionNotes',
diff --git a/app/assets/javascripts/notes/components/discussion_notes_replies_wrapper.vue b/app/assets/javascripts/notes/components/discussion_notes_replies_wrapper.vue
index 2ddca56ddd5..dfeda4aae7c 100644
--- a/app/assets/javascripts/notes/components/discussion_notes_replies_wrapper.vue
+++ b/app/assets/javascripts/notes/components/discussion_notes_replies_wrapper.vue
@@ -16,9 +16,14 @@ export default {
},
render(h, { props, children }) {
if (props.isDiffDiscussion) {
- return h('li', { class: 'discussion-collapsible bordered-box clearfix' }, [
- h('ul', { class: 'notes' }, children),
- ]);
+ return h(
+ 'li',
+ {
+ class:
+ 'discussion-collapsible gl-border-solid gl-border-gray-100 gl-border-1 gl-rounded-base gl-overflow-hidden clearfix',
+ },
+ [h('ul', { class: 'notes' }, children)],
+ );
}
return children;
diff --git a/app/assets/javascripts/notes/components/note_actions.vue b/app/assets/javascripts/notes/components/note_actions.vue
index b85cfa83e09..bc8e1d3fec6 100644
--- a/app/assets/javascripts/notes/components/note_actions.vue
+++ b/app/assets/javascripts/notes/components/note_actions.vue
@@ -3,11 +3,12 @@ import { mapGetters } from 'vuex';
import { GlTooltipDirective, GlIcon, GlButton, GlDropdownItem } from '@gitlab/ui';
import { __, sprintf } from '~/locale';
import resolvedStatusMixin from '~/batch_comments/mixins/resolved_status';
-import ReplyButton from './note_actions/reply_button.vue';
import eventHub from '~/sidebar/event_hub';
import Api from '~/api';
import { deprecatedCreateFlash as flash } from '~/flash';
+import { BV_HIDE_TOOLTIP } from '~/lib/utils/constants';
import { splitCamelCase } from '../../lib/utils/text_utility';
+import ReplyButton from './note_actions/reply_button.vue';
export default {
name: 'NoteActions',
@@ -193,7 +194,7 @@ export default {
},
closeTooltip() {
this.$nextTick(() => {
- this.$root.$emit('bv::hide::tooltip');
+ this.$root.$emit(BV_HIDE_TOOLTIP);
});
},
handleAssigneeUpdate(assignees) {
diff --git a/app/assets/javascripts/notes/components/note_awards_list.vue b/app/assets/javascripts/notes/components/note_awards_list.vue
index cf3e991986c..ff0bf5bd142 100644
--- a/app/assets/javascripts/notes/components/note_awards_list.vue
+++ b/app/assets/javascripts/notes/components/note_awards_list.vue
@@ -1,8 +1,8 @@
<script>
import { mapActions, mapGetters } from 'vuex';
import AwardsList from '~/vue_shared/components/awards_list.vue';
-import { deprecatedCreateFlash as Flash } from '../../flash';
import { __ } from '~/locale';
+import { deprecatedCreateFlash as Flash } from '../../flash';
export default {
components: {
diff --git a/app/assets/javascripts/notes/components/note_body.vue b/app/assets/javascripts/notes/components/note_body.vue
index 8855ceac3d5..03a8a8f9376 100644
--- a/app/assets/javascripts/notes/components/note_body.vue
+++ b/app/assets/javascripts/notes/components/note_body.vue
@@ -3,12 +3,12 @@
import { mapActions, mapGetters, mapState } from 'vuex';
import $ from 'jquery';
import '~/behaviors/markdown/render_gfm';
+import Suggestions from '~/vue_shared/components/markdown/suggestions.vue';
+import autosave from '../mixins/autosave';
import noteEditedText from './note_edited_text.vue';
import noteAwardsList from './note_awards_list.vue';
import noteAttachment from './note_attachment.vue';
import noteForm from './note_form.vue';
-import autosave from '../mixins/autosave';
-import Suggestions from '~/vue_shared/components/markdown/suggestions.vue';
export default {
components: {
@@ -156,6 +156,7 @@ export default {
@handleFormUpdate="handleFormUpdate"
@cancelForm="formCancelHandler"
/>
+ <!-- eslint-disable vue/no-mutating-props -->
<textarea
v-if="canEdit"
v-model="note.note"
@@ -163,6 +164,7 @@ export default {
class="hidden js-task-list-field"
dir="auto"
></textarea>
+ <!-- eslint-enable vue/no-mutating-props -->
<note-edited-text
v-if="note.last_edited_at"
:edited-at="note.last_edited_at"
diff --git a/app/assets/javascripts/notes/components/note_form.vue b/app/assets/javascripts/notes/components/note_form.vue
index 9acb837c27f..03ee5547fc8 100644
--- a/app/assets/javascripts/notes/components/note_form.vue
+++ b/app/assets/javascripts/notes/components/note_form.vue
@@ -1,14 +1,15 @@
<script>
/* eslint-disable vue/no-v-html */
import { mapGetters, mapActions, mapState } from 'vuex';
+import { GlButton } from '@gitlab/ui';
import { mergeUrlParams } from '~/lib/utils/url_utility';
-import eventHub from '../event_hub';
import markdownField from '~/vue_shared/components/markdown/field.vue';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
-import issuableStateMixin from '../mixins/issuable_state';
-import resolvable from '../mixins/resolvable';
import { __, sprintf } from '~/locale';
import { getDraft, updateDraft } from '~/lib/utils/autosave';
+import issuableStateMixin from '../mixins/issuable_state';
+import resolvable from '../mixins/resolvable';
+import eventHub from '../event_hub';
import CommentFieldLayout from './comment_field_layout.vue';
export default {
@@ -16,6 +17,7 @@ export default {
components: {
markdownField,
CommentFieldLayout,
+ GlButton,
},
mixins: [glFeatureFlagsMixin(), issuableStateMixin, resolvable],
props: {
@@ -378,61 +380,70 @@ export default {
</template>
</label>
</p>
- <div>
- <button
+ <div class="gl-display-sm-flex gl-flex-wrap">
+ <gl-button
:disabled="isDisabled"
- type="button"
- class="btn btn-success"
+ category="primary"
+ variant="success"
+ class="gl-mr-3"
data-qa-selector="start_review_button"
@click="handleAddToReview"
>
<template v-if="hasDrafts">{{ __('Add to review') }}</template>
<template v-else>{{ __('Start a review') }}</template>
- </button>
- <button
+ </gl-button>
+ <gl-button
:disabled="isDisabled"
- type="button"
- class="btn js-comment-button"
+ category="secondary"
+ variant="default"
data-qa-selector="comment_now_button"
+ class="gl-mr-3 js-comment-button"
@click="handleUpdate()"
>
{{ __('Add comment now') }}
- </button>
- <button
- class="btn note-edit-cancel js-close-discussion-note-form"
- type="button"
+ </gl-button>
+ <gl-button
+ class="note-edit-cancel js-close-discussion-note-form"
+ category="secondary"
+ variant="default"
data-testid="cancelBatchCommentsEnabled"
@click="cancelHandler(true)"
>
{{ __('Cancel') }}
- </button>
+ </gl-button>
</div>
</template>
<template v-else>
- <button
- :disabled="isDisabled"
- type="button"
- class="js-vue-issue-save btn btn-success js-comment-button"
- data-qa-selector="reply_comment_button"
- @click="handleUpdate()"
- >
- {{ saveButtonTitle }}
- </button>
- <button
- v-if="discussion.resolvable"
- class="btn btn-default gl-mr-3 js-comment-resolve-button"
- @click.prevent="handleUpdate(true)"
- >
- {{ resolveButtonTitle }}
- </button>
- <button
- class="btn btn-cancel note-edit-cancel js-close-discussion-note-form"
- type="button"
- data-testid="cancel"
- @click="cancelHandler(true)"
- >
- {{ __('Cancel') }}
- </button>
+ <div class="gl-display-sm-flex gl-flex-wrap">
+ <gl-button
+ :disabled="isDisabled"
+ category="primary"
+ variant="success"
+ data-qa-selector="reply_comment_button"
+ class="gl-mr-3 js-vue-issue-save js-comment-button"
+ @click="handleUpdate()"
+ >
+ {{ saveButtonTitle }}
+ </gl-button>
+ <gl-button
+ v-if="discussion.resolvable"
+ category="secondary"
+ variant="default"
+ class="gl-mr-3 js-comment-resolve-button"
+ @click.prevent="handleUpdate(true)"
+ >
+ {{ resolveButtonTitle }}
+ </gl-button>
+ <gl-button
+ class="note-edit-cancel js-close-discussion-note-form"
+ category="secondary"
+ variant="default"
+ data-testid="cancel"
+ @click="cancelHandler(true)"
+ >
+ {{ __('Cancel') }}
+ </gl-button>
+ </div>
</template>
</div>
</form>
diff --git a/app/assets/javascripts/notes/components/noteable_discussion.vue b/app/assets/javascripts/notes/components/noteable_discussion.vue
index 0a9a3da6069..c9722ebb8b6 100644
--- a/app/assets/javascripts/notes/components/noteable_discussion.vue
+++ b/app/assets/javascripts/notes/components/noteable_discussion.vue
@@ -8,13 +8,13 @@ import TimelineEntryItem from '~/vue_shared/components/notes/timeline_entry_item
import DraftNote from '~/batch_comments/components/draft_note.vue';
import { deprecatedCreateFlash as Flash } from '../../flash';
import userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue';
+import noteable from '../mixins/noteable';
+import resolvable from '../mixins/resolvable';
+import eventHub from '../event_hub';
import diffDiscussionHeader from './diff_discussion_header.vue';
import noteSignedOutWidget from './note_signed_out_widget.vue';
import noteForm from './note_form.vue';
import diffWithNote from './diff_with_note.vue';
-import noteable from '../mixins/noteable';
-import resolvable from '../mixins/resolvable';
-import eventHub from '../event_hub';
import DiscussionNotes from './discussion_notes.vue';
import DiscussionActions from './discussion_actions.vue';
@@ -265,16 +265,8 @@ export default {
<div
v-else-if="showReplies"
:class="{ 'is-replying': isReplying }"
- class="discussion-reply-holder clearfix"
+ class="discussion-reply-holder gl-border-t-0! clearfix"
>
- <user-avatar-link
- v-if="!isReplying && userCanReply"
- :link-href="currentUser.path"
- :img-src="currentUser.avatar_url"
- :img-alt="currentUser.name"
- :img-size="40"
- class="d-none d-sm-block"
- />
<discussion-actions
v-if="!isReplying && userCanReply"
:discussion="discussion"
@@ -285,27 +277,18 @@ export default {
@showReplyForm="showReplyForm"
@resolve="resolveHandler"
/>
- <div v-if="isReplying" class="avatar-note-form-holder">
- <user-avatar-link
- v-if="currentUser"
- :link-href="currentUser.path"
- :img-src="currentUser.avatar_url"
- :img-alt="currentUser.name"
- :img-size="40"
- class="d-none d-sm-block"
- />
- <note-form
- ref="noteForm"
- :discussion="discussion"
- :is-editing="false"
- :line="diffLine"
- save-button-title="Comment"
- :autosave-key="autosaveKey"
- @handleFormUpdateAddToReview="addReplyToReview"
- @handleFormUpdate="saveReply"
- @cancelForm="cancelReplyForm"
- />
- </div>
+ <note-form
+ v-if="isReplying"
+ ref="noteForm"
+ :discussion="discussion"
+ :is-editing="false"
+ :line="diffLine"
+ save-button-title="Comment"
+ :autosave-key="autosaveKey"
+ @handleFormUpdateAddToReview="addReplyToReview"
+ @handleFormUpdate="saveReply"
+ @cancelForm="cancelReplyForm"
+ />
<note-signed-out-widget v-if="!isLoggedIn" />
</div>
</template>
diff --git a/app/assets/javascripts/notes/components/noteable_note.vue b/app/assets/javascripts/notes/components/noteable_note.vue
index eaa64cf7c01..a1738b993d7 100644
--- a/app/assets/javascripts/notes/components/noteable_note.vue
+++ b/app/assets/javascripts/notes/components/noteable_note.vue
@@ -6,16 +6,17 @@ import { GlSprintf, GlSafeHtmlDirective as SafeHtml } from '@gitlab/ui';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { truncateSha } from '~/lib/utils/text_utility';
import TimelineEntryItem from '~/vue_shared/components/notes/timeline_entry_item.vue';
+import httpStatusCodes from '~/lib/utils/http_status';
+import { INLINE_DIFF_LINES_KEY } from '~/diffs/constants';
import { __, s__, sprintf } from '../../locale';
import { deprecatedCreateFlash as Flash } from '../../flash';
import userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue';
-import noteHeader from './note_header.vue';
-import noteActions from './note_actions.vue';
-import NoteBody from './note_body.vue';
import eventHub from '../event_hub';
import noteable from '../mixins/noteable';
import resolvable from '../mixins/resolvable';
-import httpStatusCodes from '~/lib/utils/http_status';
+import noteHeader from './note_header.vue';
+import noteActions from './note_actions.vue';
+import NoteBody from './note_body.vue';
import {
getStartLineNumber,
getEndLineNumber,
@@ -23,7 +24,6 @@ import {
commentLineOptions,
formatLineRange,
} from './multiline_comment_utils';
-import { INLINE_DIFF_LINES_KEY } from '~/diffs/constants';
export default {
name: 'NoteableNote',
@@ -289,6 +289,7 @@ export default {
};
this.isRequesting = true;
this.oldContent = this.note.note_html;
+ // eslint-disable-next-line vue/no-mutating-props
this.note.note_html = escape(noteText);
this.updateNote(data)
@@ -321,6 +322,7 @@ export default {
}
this.$refs.noteBody.resetAutoSave();
if (this.oldContent) {
+ // eslint-disable-next-line vue/no-mutating-props
this.note.note_html = this.oldContent;
this.oldContent = null;
}
@@ -330,6 +332,7 @@ export default {
recoverNoteContent(noteText) {
// we need to do this to prevent noteForm inconsistent content warning
// this is something we intentionally do so we need to recover the content
+ // eslint-disable-next-line vue/no-mutating-props
this.note.note = noteText;
const { noteBody } = this.$refs;
if (noteBody) {
diff --git a/app/assets/javascripts/notes/components/notes_app.vue b/app/assets/javascripts/notes/components/notes_app.vue
index e9e687a8743..c0468e5df0f 100644
--- a/app/assets/javascripts/notes/components/notes_app.vue
+++ b/app/assets/javascripts/notes/components/notes_app.vue
@@ -1,21 +1,22 @@
<script>
import { mapGetters, mapActions } from 'vuex';
+import OrderedLayout from '~/vue_shared/components/ordered_layout.vue';
+import highlightCurrentUser from '~/behaviors/markdown/highlight_current_user';
+import { __ } from '~/locale';
+import initUserPopovers from '~/user_popovers';
+import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { getLocationHash, doesHashExistInUrl } from '../../lib/utils/url_utility';
import { deprecatedCreateFlash as Flash } from '../../flash';
import * as constants from '../constants';
import eventHub from '../event_hub';
-import noteableNote from './noteable_note.vue';
-import noteableDiscussion from './noteable_discussion.vue';
-import discussionFilterNote from './discussion_filter_note.vue';
import systemNote from '../../vue_shared/components/notes/system_note.vue';
-import commentForm from './comment_form.vue';
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';
-import OrderedLayout from '~/vue_shared/components/ordered_layout.vue';
-import highlightCurrentUser from '~/behaviors/markdown/highlight_current_user';
-import { __ } from '~/locale';
-import initUserPopovers from '~/user_popovers';
+import noteableNote from './noteable_note.vue';
+import noteableDiscussion from './noteable_discussion.vue';
+import discussionFilterNote from './discussion_filter_note.vue';
+import commentForm from './comment_form.vue';
export default {
name: 'NotesApp',
@@ -30,6 +31,7 @@ export default {
discussionFilterNote,
OrderedLayout,
},
+ mixins: [glFeatureFlagsMixin()],
props: {
noteableData: {
type: Object,
@@ -57,7 +59,6 @@ export default {
},
data() {
return {
- isFetching: false,
currentFilter: null,
};
},
@@ -68,6 +69,7 @@ export default {
'convertedDisscussionIds',
'getNotesDataByProp',
'isLoading',
+ 'isFetching',
'commentsDisabled',
'getNoteableData',
'userCanReply',
@@ -103,6 +105,13 @@ export default {
},
},
watch: {
+ async isFetching() {
+ if (!this.isFetching) {
+ await this.$nextTick();
+ await this.startTaskList();
+ await this.checkLocationHash();
+ }
+ },
shouldShow() {
if (!this.isNotesFetched) {
this.fetchNotes();
@@ -153,6 +162,7 @@ export default {
},
methods: {
...mapActions([
+ 'setFetchingState',
'setLoadingState',
'fetchDiscussions',
'poll',
@@ -183,7 +193,11 @@ export default {
fetchNotes() {
if (this.isFetching) return null;
- this.isFetching = true;
+ this.setFetchingState(true);
+
+ if (this.glFeatures.paginatedNotes) {
+ return this.initPolling();
+ }
return this.fetchDiscussions(this.getFetchDiscussionsConfig())
.then(this.initPolling)
@@ -191,11 +205,8 @@ export default {
this.setLoadingState(false);
this.setNotesFetchedState(true);
eventHub.$emit('fetchedNotesData');
- this.isFetching = false;
+ this.setFetchingState(false);
})
- .then(this.$nextTick)
- .then(this.startTaskList)
- .then(this.checkLocationHash)
.catch(() => {
this.setLoadingState(false);
this.setNotesFetchedState(true);
diff --git a/app/assets/javascripts/notes/components/timeline_toggle.vue b/app/assets/javascripts/notes/components/timeline_toggle.vue
index 8162878f80d..87d22e5b986 100644
--- a/app/assets/javascripts/notes/components/timeline_toggle.vue
+++ b/app/assets/javascripts/notes/components/timeline_toggle.vue
@@ -2,9 +2,9 @@
import { GlButton, GlTooltipDirective } from '@gitlab/ui';
import { mapActions, mapGetters } from 'vuex';
import { s__ } from '~/locale';
+import TrackEventDirective from '~/vue_shared/directives/track_event';
import { COMMENTS_ONLY_FILTER_VALUE, DESC } from '../constants';
import notesEventHub from '../event_hub';
-import TrackEventDirective from '~/vue_shared/directives/track_event';
import { trackToggleTimelineView } from '../utils';
export const timelineEnabledTooltip = s__('Timeline|Turn timeline view off');
diff --git a/app/assets/javascripts/notes/components/toggle_replies_widget.vue b/app/assets/javascripts/notes/components/toggle_replies_widget.vue
index ab7fa793bdc..06de3104a47 100644
--- a/app/assets/javascripts/notes/components/toggle_replies_widget.vue
+++ b/app/assets/javascripts/notes/components/toggle_replies_widget.vue
@@ -39,13 +39,17 @@ export default {
this.$emit('toggle');
},
},
+ ICON_CLASS: 'gl-mr-3 gl-cursor-pointer',
};
</script>
<template>
- <li :class="className" class="replies-toggle js-toggle-replies">
+ <li
+ :class="className"
+ class="replies-toggle js-toggle-replies gl-display-flex! gl-align-items-center gl-flex-wrap"
+ >
<template v-if="collapsed">
- <gl-icon name="chevron-right" @click.native="toggle" />
+ <gl-icon :class="$options.ICON_CLASS" name="chevron-right" @click.native="toggle" />
<div>
<user-avatar-link
v-for="author in uniqueAuthors"
@@ -59,7 +63,7 @@ export default {
/>
</div>
<gl-button
- class="js-replies-text"
+ class="js-replies-text gl-mr-2"
category="tertiary"
variant="link"
data-qa-selector="expand_replies_button"
@@ -68,18 +72,19 @@ export default {
{{ replies.length }} {{ n__('reply', 'replies', replies.length) }}
</gl-button>
{{ __('Last reply by') }}
- <a :href="lastReply.author.path" class="btn btn-link author-link">
+ <a :href="lastReply.author.path" class="btn btn-link author-link gl-mx-2">
{{ lastReply.author.name }}
</a>
<time-ago-tooltip :time="lastReply.created_at" tooltip-placement="bottom" />
</template>
- <span
+ <div
v-else
- class="collapse-replies-btn js-collapse-replies"
+ class="collapse-replies-btn js-collapse-replies gl-display-flex align-items-center"
data-qa-selector="collapse_replies_button"
@click="toggle"
>
- <gl-icon name="chevron-down" /> {{ s__('Notes|Collapse replies') }}
- </span>
+ <gl-icon :class="$options.ICON_CLASS" name="chevron-down" />
+ <span class="gl-cursor-pointer">{{ s__('Notes|Collapse replies') }}</span>
+ </div>
</li>
</template>
diff --git a/app/assets/javascripts/notes/mixins/autosave.js b/app/assets/javascripts/notes/mixins/autosave.js
index b161773f5f1..d670d0bd4c5 100644
--- a/app/assets/javascripts/notes/mixins/autosave.js
+++ b/app/assets/javascripts/notes/mixins/autosave.js
@@ -1,7 +1,7 @@
import $ from 'jquery';
+import { s__ } from '~/locale';
import Autosave from '../../autosave';
import { capitalizeFirstCharacter } from '../../lib/utils/text_utility';
-import { s__ } from '~/locale';
export default {
methods: {
diff --git a/app/assets/javascripts/notes/stores/actions.js b/app/assets/javascripts/notes/stores/actions.js
index c6684efed4d..ddc6c44a4e5 100644
--- a/app/assets/javascripts/notes/stores/actions.js
+++ b/app/assets/javascripts/notes/stores/actions.js
@@ -2,22 +2,23 @@ import Vue from 'vue';
import $ from 'jquery';
import Visibility from 'visibilityjs';
import axios from '~/lib/utils/axios_utils';
+import updateIssueConfidentialMutation from '~/sidebar/components/confidential/mutations/update_issue_confidential.mutation.graphql';
+import updateMergeRequestLockMutation from '~/sidebar/components/lock/mutations/update_merge_request_lock.mutation.graphql';
+import updateIssueLockMutation from '~/sidebar/components/lock/mutations/update_issue_lock.mutation.graphql';
+import { __, sprintf } from '~/locale';
+import Api from '~/api';
import TaskList from '../../task_list';
import { deprecatedCreateFlash as Flash } from '../../flash';
import Poll from '../../lib/utils/poll';
-import * as types from './mutation_types';
-import * as utils from './utils';
import * as constants from '../constants';
import loadAwardsHandler from '../../awards_handler';
import sidebarTimeTrackingEventHub from '../../sidebar/event_hub';
import { isInViewport, scrollToElement, isInMRPage } from '../../lib/utils/common_utils';
import { mergeUrlParams } from '../../lib/utils/url_utility';
+import eventHub from '../event_hub';
import mrWidgetEventHub from '../../vue_merge_request_widget/event_hub';
-import updateIssueConfidentialMutation from '~/sidebar/components/confidential/mutations/update_issue_confidential.mutation.graphql';
-import updateMergeRequestLockMutation from '~/sidebar/components/lock/mutations/update_merge_request_lock.mutation.graphql';
-import updateIssueLockMutation from '~/sidebar/components/lock/mutations/update_issue_lock.mutation.graphql';
-import { __, sprintf } from '~/locale';
-import Api from '~/api';
+import * as utils from './utils';
+import * as types from './mutation_types';
let eTagPoll;
@@ -420,14 +421,25 @@ export const saveNote = ({ commit, dispatch }, noteData) => {
.catch(processErrors);
};
-const pollSuccessCallBack = (resp, commit, state, getters, dispatch) => {
+export const setFetchingState = ({ commit }, fetchingState) =>
+ commit(types.SET_NOTES_FETCHING_STATE, fetchingState);
+
+const pollSuccessCallBack = async (resp, commit, state, getters, dispatch) => {
if (state.isResolvingDiscussion) {
return null;
}
+ if (window.gon?.features?.paginatedNotes && !resp.more && state.isFetching) {
+ eventHub.$emit('fetchedNotesData');
+ dispatch('setFetchingState', false);
+ dispatch('setNotesFetchedState', true);
+ dispatch('setLoadingState', false);
+ }
+
if (resp.notes?.length) {
- dispatch('updateOrCreateNotes', resp.notes);
+ await dispatch('updateOrCreateNotes', resp.notes);
dispatch('startTaskList');
+ dispatch('updateResolvableDiscussionsCounts');
}
commit(types.SET_LAST_FETCHED_AT, resp.last_fetched_at);
diff --git a/app/assets/javascripts/notes/stores/getters.js b/app/assets/javascripts/notes/stores/getters.js
index 5891a2e63e3..43d99937b8d 100644
--- a/app/assets/javascripts/notes/stores/getters.js
+++ b/app/assets/javascripts/notes/stores/getters.js
@@ -48,6 +48,8 @@ export const persistSortOrder = (state) => state.persistSortOrder;
export const timelineEnabled = (state) => state.isTimelineEnabled;
+export const isFetching = (state) => state.isFetching;
+
export const isLoading = (state) => state.isLoading;
export const getNotesDataByProp = (state) => (prop) => state.notesData[prop];
diff --git a/app/assets/javascripts/notes/stores/modules/index.js b/app/assets/javascripts/notes/stores/modules/index.js
index 144a3d7ba90..c1738eb20da 100644
--- a/app/assets/javascripts/notes/stores/modules/index.js
+++ b/app/assets/javascripts/notes/stores/modules/index.js
@@ -47,6 +47,7 @@ export default () => ({
unresolvedDiscussionsCount: 0,
descriptionVersions: {},
isTimelineEnabled: false,
+ isFetching: false,
},
actions,
getters,
diff --git a/app/assets/javascripts/notes/stores/mutation_types.js b/app/assets/javascripts/notes/stores/mutation_types.js
index 5c4f62f4575..2e8b728e013 100644
--- a/app/assets/javascripts/notes/stores/mutation_types.js
+++ b/app/assets/javascripts/notes/stores/mutation_types.js
@@ -14,6 +14,7 @@ export const UPDATE_NOTE = 'UPDATE_NOTE';
export const UPDATE_DISCUSSION = 'UPDATE_DISCUSSION';
export const UPDATE_DISCUSSION_POSITION = 'UPDATE_DISCUSSION_POSITION';
export const SET_DISCUSSION_DIFF_LINES = 'SET_DISCUSSION_DIFF_LINES';
+export const SET_NOTES_FETCHING_STATE = 'SET_NOTES_FETCHING_STATE';
export const SET_NOTES_FETCHED_STATE = 'SET_NOTES_FETCHED_STATE';
export const SET_NOTES_LOADING_STATE = 'SET_NOTES_LOADING_STATE';
export const DISABLE_COMMENTS = 'DISABLE_COMMENTS';
diff --git a/app/assets/javascripts/notes/stores/mutations.js b/app/assets/javascripts/notes/stores/mutations.js
index 2c51ce0d970..536b47667c2 100644
--- a/app/assets/javascripts/notes/stores/mutations.js
+++ b/app/assets/javascripts/notes/stores/mutations.js
@@ -1,7 +1,8 @@
-import * as utils from './utils';
-import * as types from './mutation_types';
+import { isEqual } from 'lodash';
import * as constants from '../constants';
import { isInMRPage } from '../../lib/utils/common_utils';
+import * as utils from './utils';
+import * as types from './mutation_types';
export default {
[types.ADD_NEW_NOTE](state, data) {
@@ -31,7 +32,22 @@ export default {
}
}
- note.base_discussion = undefined; // No point keeping a reference to this
+ if (window.gon?.features?.paginatedNotes && note.base_discussion) {
+ if (discussion.diff_file) {
+ discussion.file_hash = discussion.diff_file.file_hash;
+
+ discussion.truncated_diff_lines = utils.prepareDiffLines(
+ discussion.truncated_diff_lines || [],
+ );
+ }
+
+ discussion.resolvable = note.resolvable;
+ discussion.expanded = note.base_discussion.expanded;
+ discussion.resolved = note.resolved;
+ }
+
+ // note.base_discussion = undefined; // No point keeping a reference to this
+ delete note.base_discussion;
discussion.notes = [note];
state.discussions.push(discussion);
@@ -220,6 +236,11 @@ export default {
[types.UPDATE_NOTE](state, note) {
const noteObj = utils.findNoteObjectById(state.discussions, note.discussion_id);
+ // Disable eslint here so we can delete the property that we no longer need
+ // in the note object
+ // eslint-disable-next-line no-param-reassign
+ delete note.base_discussion;
+
if (noteObj.individual_note) {
if (note.type === constants.DISCUSSION_NOTE) {
noteObj.individual_note = false;
@@ -228,7 +249,10 @@ export default {
noteObj.notes.splice(0, 1, note);
} else {
const comment = utils.findNoteObjectById(noteObj.notes, note.id);
- noteObj.notes.splice(noteObj.notes.indexOf(comment), 1, note);
+
+ if (!isEqual(comment, note)) {
+ noteObj.notes.splice(noteObj.notes.indexOf(comment), 1, note);
+ }
}
},
@@ -313,6 +337,10 @@ export default {
state.isLoading = value;
},
+ [types.SET_NOTES_FETCHING_STATE](state, value) {
+ state.isFetching = value;
+ },
+
[types.SET_DISCUSSION_DIFF_LINES](state, { discussionId, diffLines }) {
const discussion = utils.findNoteObjectById(state.discussions, discussionId);
diff --git a/app/assets/javascripts/notifications/components/notifications_dropdown.vue b/app/assets/javascripts/notifications/components/notifications_dropdown.vue
new file mode 100644
index 00000000000..f6a83d643f9
--- /dev/null
+++ b/app/assets/javascripts/notifications/components/notifications_dropdown.vue
@@ -0,0 +1,180 @@
+<script>
+import {
+ GlButtonGroup,
+ GlButton,
+ GlDropdown,
+ GlDropdownDivider,
+ GlTooltipDirective,
+} from '@gitlab/ui';
+import { sprintf } from '~/locale';
+import Api from '~/api';
+import { CUSTOM_LEVEL, i18n } from '../constants';
+import NotificationsDropdownItem from './notifications_dropdown_item.vue';
+
+export default {
+ name: 'NotificationsDropdown',
+ components: {
+ GlButtonGroup,
+ GlButton,
+ GlDropdown,
+ GlDropdownDivider,
+ NotificationsDropdownItem,
+ },
+ directives: {
+ GlTooltip: GlTooltipDirective,
+ },
+ inject: {
+ containerClass: {
+ default: '',
+ },
+ disabled: {
+ default: false,
+ },
+ dropdownItems: {
+ default: [],
+ },
+ buttonSize: {
+ default: 'medium',
+ },
+ initialNotificationLevel: {
+ default: '',
+ },
+ projectId: {
+ default: null,
+ },
+ groupId: {
+ default: null,
+ },
+ showLabel: {
+ default: false,
+ },
+ },
+ data() {
+ return {
+ selectedNotificationLevel: this.initialNotificationLevel,
+ isLoading: false,
+ };
+ },
+ computed: {
+ notificationLevels() {
+ return this.dropdownItems.map((level) => ({
+ level,
+ title: this.$options.i18n.notificationTitles[level] || '',
+ description: this.$options.i18n.notificationDescriptions[level] || '',
+ }));
+ },
+ isCustomNotification() {
+ return this.selectedNotificationLevel === CUSTOM_LEVEL;
+ },
+ buttonIcon() {
+ if (this.isLoading) {
+ return null;
+ }
+
+ return this.selectedNotificationLevel === 'disabled' ? 'notifications-off' : 'notifications';
+ },
+ buttonText() {
+ return this.showLabel
+ ? this.$options.i18n.notificationTitles[this.selectedNotificationLevel]
+ : null;
+ },
+ buttonTooltip() {
+ const notificationTitle =
+ this.$options.i18n.notificationTitles[this.selectedNotificationLevel] ||
+ this.selectedNotificationLevel;
+
+ return this.disabled
+ ? this.$options.i18n.notificationDescriptions.owner_disabled
+ : sprintf(this.$options.i18n.notificationTooltipTitle, {
+ notification_title: notificationTitle,
+ });
+ },
+ },
+ methods: {
+ selectItem(level) {
+ if (level !== this.selectedNotificationLevel) {
+ this.updateNotificationLevel(level);
+ }
+ },
+ async updateNotificationLevel(level) {
+ this.isLoading = true;
+
+ try {
+ await Api.updateNotificationSettings(this.projectId, this.groupId, { level });
+ this.selectedNotificationLevel = level;
+ } catch (error) {
+ this.$toast.show(this.$options.i18n.updateNotificationLevelErrorMessage, { type: 'error' });
+ } finally {
+ this.isLoading = false;
+ }
+ },
+ },
+ customLevel: CUSTOM_LEVEL,
+ i18n,
+};
+</script>
+
+<template>
+ <div :class="containerClass">
+ <gl-button-group
+ v-if="isCustomNotification"
+ v-gl-tooltip="{ title: buttonTooltip }"
+ data-testid="notificationButton"
+ :size="buttonSize"
+ >
+ <gl-button :size="buttonSize" :icon="buttonIcon" :loading="isLoading" :disabled="disabled">
+ <template v-if="buttonText">{{ buttonText }}</template>
+ </gl-button>
+ <gl-dropdown :size="buttonSize" :disabled="disabled">
+ <notifications-dropdown-item
+ v-for="item in notificationLevels"
+ :key="item.level"
+ :level="item.level"
+ :title="item.title"
+ :description="item.description"
+ :notification-level="selectedNotificationLevel"
+ @item-selected="selectItem"
+ />
+ <gl-dropdown-divider />
+ <notifications-dropdown-item
+ :key="$options.customLevel"
+ :level="$options.customLevel"
+ :title="$options.i18n.notificationTitles.custom"
+ :description="$options.i18n.notificationDescriptions.custom"
+ :notification-level="selectedNotificationLevel"
+ @item-selected="selectItem"
+ />
+ </gl-dropdown>
+ </gl-button-group>
+
+ <gl-dropdown
+ v-else
+ v-gl-tooltip="{ title: buttonTooltip }"
+ data-testid="notificationButton"
+ :text="buttonText"
+ :icon="buttonIcon"
+ :loading="isLoading"
+ :size="buttonSize"
+ :disabled="disabled"
+ >
+ <notifications-dropdown-item
+ v-for="item in notificationLevels"
+ :key="item.level"
+ :level="item.level"
+ :title="item.title"
+ :description="item.description"
+ :notification-level="selectedNotificationLevel"
+ @item-selected="selectItem"
+ />
+ <gl-dropdown-divider />
+ <notifications-dropdown-item
+ :key="$options.customLevel"
+ :level="$options.customLevel"
+ :title="$options.i18n.notificationTitles.custom"
+ :description="$options.i18n.notificationDescriptions.custom"
+ :notification-level="selectedNotificationLevel"
+ @item-selected="selectItem"
+ />
+ </gl-dropdown>
+ </div>
+</template>
diff --git a/app/assets/javascripts/notifications/components/notifications_dropdown_item.vue b/app/assets/javascripts/notifications/components/notifications_dropdown_item.vue
new file mode 100644
index 00000000000..73bb9c1b36f
--- /dev/null
+++ b/app/assets/javascripts/notifications/components/notifications_dropdown_item.vue
@@ -0,0 +1,42 @@
+<script>
+import { GlDropdownItem } from '@gitlab/ui';
+
+export default {
+ name: 'NotificationsDropdownItem',
+ components: {
+ GlDropdownItem,
+ },
+ props: {
+ level: {
+ type: String,
+ required: true,
+ },
+ title: {
+ type: String,
+ required: true,
+ },
+ description: {
+ type: String,
+ required: true,
+ },
+ notificationLevel: {
+ type: String,
+ required: true,
+ },
+ },
+ computed: {
+ isActive() {
+ return this.notificationLevel === this.level;
+ },
+ },
+};
+</script>
+
+<template>
+ <gl-dropdown-item is-check-item :is-checked="isActive" @click="$emit('item-selected', level)">
+ <div class="gl-display-flex gl-flex-direction-column">
+ <span class="gl-font-weight-bold">{{ title }}</span>
+ <span class="gl-text-gray-500">{{ description }}</span>
+ </div>
+ </gl-dropdown-item>
+</template>
diff --git a/app/assets/javascripts/notifications/constants.js b/app/assets/javascripts/notifications/constants.js
new file mode 100644
index 00000000000..f730ee59e84
--- /dev/null
+++ b/app/assets/javascripts/notifications/constants.js
@@ -0,0 +1,27 @@
+import { __, s__ } from '~/locale';
+
+export const CUSTOM_LEVEL = 'custom';
+
+export const i18n = {
+ notificationTitles: {
+ participating: s__('NotificationLevel|Participate'),
+ mention: s__('NotificationLevel|On mention'),
+ watch: s__('NotificationLevel|Watch'),
+ global: s__('NotificationLevel|Global'),
+ disabled: s__('NotificationLevel|Disabled'),
+ custom: s__('NotificationLevel|Custom'),
+ },
+ notificationTooltipTitle: __('Notification setting - %{notification_title}'),
+ notificationDescriptions: {
+ participating: __('You will only receive notifications for threads you have participated in'),
+ mention: __('You will receive notifications only for comments in which you were @mentioned'),
+ watch: __('You will receive notifications for any activity'),
+ disabled: __('You will not get any notifications via email'),
+ global: __('Use your global notification setting'),
+ custom: __('You will only receive notifications for the events you choose'),
+ owner_disabled: __('Notifications have been disabled by the project or group owner'),
+ },
+ updateNotificationLevelErrorMessage: __(
+ 'An error occured while updating the notification settings. Please try again.',
+ ),
+};
diff --git a/app/assets/javascripts/notifications/index.js b/app/assets/javascripts/notifications/index.js
new file mode 100644
index 00000000000..dc5b8e7fbb1
--- /dev/null
+++ b/app/assets/javascripts/notifications/index.js
@@ -0,0 +1,42 @@
+import Vue from 'vue';
+import { GlToast } from '@gitlab/ui';
+import { parseBoolean } from '~/lib/utils/common_utils';
+import NotificationsDropdown from './components/notifications_dropdown.vue';
+
+Vue.use(GlToast);
+
+export default () => {
+ const containers = document.querySelectorAll('.js-vue-notification-dropdown');
+
+ if (!containers.length) return false;
+
+ return containers.forEach((el) => {
+ const {
+ containerClass,
+ buttonSize,
+ disabled,
+ dropdownItems,
+ notificationLevel,
+ projectId,
+ groupId,
+ showLabel,
+ } = el.dataset;
+
+ return new Vue({
+ el,
+ provide: {
+ containerClass,
+ buttonSize,
+ disabled: parseBoolean(disabled),
+ dropdownItems: JSON.parse(dropdownItems),
+ initialNotificationLevel: notificationLevel,
+ projectId,
+ groupId,
+ showLabel: parseBoolean(showLabel),
+ },
+ render(h) {
+ return h(NotificationsDropdown);
+ },
+ });
+ });
+};
diff --git a/app/assets/javascripts/notifications_dropdown.js b/app/assets/javascripts/notifications_dropdown.js
index eaa5ec3a2e4..d61defed14d 100644
--- a/app/assets/javascripts/notifications_dropdown.js
+++ b/app/assets/javascripts/notifications_dropdown.js
@@ -1,7 +1,7 @@
import $ from 'jquery';
import { Rails } from '~/lib/utils/rails_ujs';
-import { deprecatedCreateFlash as Flash } from './flash';
import { __ } from '~/locale';
+import { deprecatedCreateFlash as Flash } from './flash';
export default function notificationsDropdown() {
$(document).on('click', '.update-notification', function updateNotificationCallback(e) {
diff --git a/app/assets/javascripts/onboarding_issues/index.js b/app/assets/javascripts/onboarding_issues/index.js
deleted file mode 100644
index b23a10c9254..00000000000
--- a/app/assets/javascripts/onboarding_issues/index.js
+++ /dev/null
@@ -1,128 +0,0 @@
-import $ from 'jquery';
-import { parseBoolean, getCookie, setCookie, removeCookie } from '~/lib/utils/common_utils';
-import { __, sprintf } from '~/locale';
-import Tracking from '~/tracking';
-
-const COOKIE_NAME = 'onboarding_issues_settings';
-
-const POPOVER_LOCATIONS = {
- GROUPS_SHOW: 'groups#show',
- PROJECTS_SHOW: 'projects#show',
- ISSUES_INDEX: 'issues#index',
-};
-
-const removeLearnGitLabCookie = () => {
- removeCookie(COOKIE_NAME);
-};
-
-function disposePopover(event) {
- event.preventDefault();
- this.popover('dispose');
- removeLearnGitLabCookie();
- Tracking.event('Growth::Conversion::Experiment::OnboardingIssues', 'dismiss_popover');
-}
-
-const showPopover = (el, path, footer, options) => {
- // Cookie value looks like `{ 'groups#show': true, 'projects#show': true, 'issues#index': true }`. When it doesn't exist, don't show the popover.
- const cookie = getCookie(COOKIE_NAME);
- if (!cookie) return;
-
- // When the popover action has already been taken, don't show the popover.
- const settings = JSON.parse(cookie);
- if (!parseBoolean(settings[path])) return;
-
- const defaultOptions = {
- boundary: 'window',
- html: true,
- placement: 'top',
- template: `<div class="popover blue learn-gitlab d-none d-xl-block" role="tooltip">
- <div class="arrow"></div>
- <div class="close cursor-pointer gl-font-base text-white gl-opacity-10 p-2">&#10005</div>
- <div class="popover-body gl-font-base gl-line-height-20 pb-0 px-3"></div>
- <div class="bold text-right text-white p-2">${footer}</div>
- </div>`,
- };
-
- // When one of the popovers is dismissed, remove the cookie.
- const closeButton = () => document.querySelector('.learn-gitlab.popover .close');
-
- // We still have to use jQuery, since Bootstrap's Popover is based on jQuery.
- const jQueryEl = $(el);
- const clickCloseButton = disposePopover.bind(jQueryEl);
-
- jQueryEl
- .popover({ ...defaultOptions, ...options })
- .on('inserted.bs.popover', () => closeButton().addEventListener('click', clickCloseButton))
- .on('hide.bs.dropdown', () => closeButton().removeEventListener('click', clickCloseButton))
- .popover('show');
-
- // The previous popover actions have been taken, don't show those popovers anymore.
- Object.keys(settings).forEach((pathSetting) => {
- if (path !== pathSetting) {
- settings[pathSetting] = false;
- } else {
- setCookie(COOKIE_NAME, settings);
- }
- });
-
- // The final popover action will be taken on click, we then no longer need the cookie.
- if (path === POPOVER_LOCATIONS.ISSUES_INDEX) {
- el.addEventListener('click', removeLearnGitLabCookie);
- }
-};
-
-export const showLearnGitLabGroupItemPopover = (id) => {
- const el = document.querySelector(`#group-${id} .group-text a`);
-
- if (!el) return;
-
- const options = {
- content: __(
- 'Here are all your projects in your group, including the one you just created. To start, let’s take a look at your personalized learning project which will help you learn about GitLab at your own pace.',
- ),
- };
-
- showPopover(el, POPOVER_LOCATIONS.GROUPS_SHOW, '1 / 2', options);
-};
-
-export const showLearnGitLabProjectPopover = () => {
- // Do not show a popover if we are not viewing the 'Learn GitLab' project.
- if (!window.location.pathname.includes('learn-gitlab')) return;
-
- const el = document.querySelector('a.shortcuts-issues');
-
- if (!el) return;
-
- const options = {
- content: sprintf(
- __(
- 'Go to %{strongStart}Issues%{strongEnd} &gt; %{strongStart}Boards%{strongEnd} to access your personalized learning issue board.',
- ),
- { strongStart: '<strong>', strongEnd: '</strong>' },
- false,
- ),
- };
-
- showPopover(el, POPOVER_LOCATIONS.PROJECTS_SHOW, '2 / 2', options);
-};
-
-export const showLearnGitLabIssuesPopover = () => {
- // Do not show a popover if we are not viewing the 'Learn GitLab' project.
- if (!window.location.pathname.includes('learn-gitlab')) return;
-
- const el = document.querySelector('a[data-qa-selector="issue_boards_link"]');
-
- if (!el) return;
-
- const options = {
- content: sprintf(
- __(
- 'Go to %{strongStart}Issues%{strongEnd} &gt; %{strongStart}Boards%{strongEnd} to access your personalized learning issue board.',
- ),
- { strongStart: '<strong>', strongEnd: '</strong>' },
- false,
- ),
- };
-
- showPopover(el, POPOVER_LOCATIONS.ISSUES_INDEX, '2 / 2', options);
-};
diff --git a/app/assets/javascripts/operation_settings/components/metrics_settings.vue b/app/assets/javascripts/operation_settings/components/metrics_settings.vue
index 2e972dd7154..2d2e625d3d3 100644
--- a/app/assets/javascripts/operation_settings/components/metrics_settings.vue
+++ b/app/assets/javascripts/operation_settings/components/metrics_settings.vue
@@ -31,9 +31,9 @@ export default {
<template>
<section class="settings no-animate">
<div class="settings-header">
- <h3 class="js-section-header h4">
+ <h4 class="js-section-header">
{{ s__('MetricsSettings|Metrics dashboard') }}
- </h3>
+ </h4>
<gl-button class="js-settings-toggle">{{ __('Expand') }}</gl-button>
<p class="js-section-sub-header">
{{ s__('MetricsSettings|Manage Metrics Dashboard settings.') }}
diff --git a/app/assets/javascripts/packages/details/components/app.vue b/app/assets/javascripts/packages/details/components/app.vue
index c9f1c8b903c..e8b9ecfd616 100644
--- a/app/assets/javascripts/packages/details/components/app.vue
+++ b/app/assets/javascripts/packages/details/components/app.vue
@@ -15,12 +15,12 @@ import Tracking from '~/tracking';
import { s__ } from '~/locale';
import { objectToQueryString } from '~/lib/utils/common_utils';
import { numberToHumanSize } from '~/lib/utils/number_utils';
-import PackageHistory from './package_history.vue';
-import PackageTitle from './package_title.vue';
import PackagesListLoader from '../../shared/components/packages_list_loader.vue';
import PackageListRow from '../../shared/components/package_list_row.vue';
import { packageTypeToTrackCategory } from '../../shared/utils';
import { PackageType, TrackingActions, SHOW_DELETE_SUCCESS_ALERT } from '../../shared/constants';
+import PackageTitle from './package_title.vue';
+import PackageHistory from './package_history.vue';
import DependencyRow from './dependency_row.vue';
import AdditionalMetadata from './additional_metadata.vue';
import InstallationCommands from './installation_commands.vue';
diff --git a/app/assets/javascripts/packages/details/components/installation_commands.vue b/app/assets/javascripts/packages/details/components/installation_commands.vue
index 138103020a7..ec97ef216f1 100644
--- a/app/assets/javascripts/packages/details/components/installation_commands.vue
+++ b/app/assets/javascripts/packages/details/components/installation_commands.vue
@@ -1,11 +1,11 @@
<script>
+import { PackageType } from '../../shared/constants';
import ConanInstallation from './conan_installation.vue';
import MavenInstallation from './maven_installation.vue';
import NpmInstallation from './npm_installation.vue';
import NugetInstallation from './nuget_installation.vue';
import PypiInstallation from './pypi_installation.vue';
import ComposerInstallation from './composer_installation.vue';
-import { PackageType } from '../../shared/constants';
export default {
name: 'InstallationCommands',
diff --git a/app/assets/javascripts/packages/details/components/package_title.vue b/app/assets/javascripts/packages/details/components/package_title.vue
index 6b7eeacb964..cded8708b0c 100644
--- a/app/assets/javascripts/packages/details/components/package_title.vue
+++ b/app/assets/javascripts/packages/details/components/package_title.vue
@@ -3,12 +3,12 @@
import { mapState, mapGetters } from 'vuex';
import { GlIcon, GlSprintf, GlTooltipDirective, GlBadge } from '@gitlab/ui';
import { GlBreakpointInstance } from '@gitlab/ui/dist/utils';
-import PackageTags from '../../shared/components/package_tags.vue';
import { numberToHumanSize } from '~/lib/utils/number_utils';
import timeagoMixin from '~/vue_shared/mixins/timeago';
import TitleArea from '~/vue_shared/components/registry/title_area.vue';
import MetadataItem from '~/vue_shared/components/registry/metadata_item.vue';
import { __ } from '~/locale';
+import PackageTags from '../../shared/components/package_tags.vue';
export default {
name: 'PackageTitle',
diff --git a/app/assets/javascripts/packages/details/index.js b/app/assets/javascripts/packages/details/index.js
index 233da3e4a99..5b9d58a3860 100644
--- a/app/assets/javascripts/packages/details/index.js
+++ b/app/assets/javascripts/packages/details/index.js
@@ -1,6 +1,6 @@
import Vue from 'vue';
-import PackagesApp from './components/app.vue';
import Translate from '~/vue_shared/translate';
+import PackagesApp from './components/app.vue';
import createStore from './store';
Vue.use(Translate);
diff --git a/app/assets/javascripts/packages/details/store/actions.js b/app/assets/javascripts/packages/details/store/actions.js
index 340f60258a0..87216366c8b 100644
--- a/app/assets/javascripts/packages/details/store/actions.js
+++ b/app/assets/javascripts/packages/details/store/actions.js
@@ -1,7 +1,7 @@
import Api from '~/api';
import { deprecatedCreateFlash as createFlash } from '~/flash';
-import { FETCH_PACKAGE_VERSIONS_ERROR } from '../constants';
import { DELETE_PACKAGE_ERROR_MESSAGE } from '~/packages/shared/constants';
+import { FETCH_PACKAGE_VERSIONS_ERROR } from '../constants';
import * as types from './mutation_types';
export const fetchPackageVersions = ({ commit, state }) => {
diff --git a/app/assets/javascripts/packages/list/components/package_search.vue b/app/assets/javascripts/packages/list/components/package_search.vue
new file mode 100644
index 00000000000..1befc440ac8
--- /dev/null
+++ b/app/assets/javascripts/packages/list/components/package_search.vue
@@ -0,0 +1,97 @@
+<script>
+import { GlSorting, GlSortingItem, GlFilteredSearch } from '@gitlab/ui';
+import { mapState, mapActions } from 'vuex';
+import { __, s__ } from '~/locale';
+import { ASCENDING_ODER, DESCENDING_ORDER } from '../constants';
+import getTableHeaders from '../utils';
+import PackageTypeToken from './tokens/package_type_token.vue';
+
+export default {
+ components: {
+ GlSorting,
+ GlSortingItem,
+ GlFilteredSearch,
+ },
+ computed: {
+ ...mapState({
+ isGroupPage: (state) => state.config.isGroupPage,
+ orderBy: (state) => state.sorting.orderBy,
+ sort: (state) => state.sorting.sort,
+ filter: (state) => state.filter,
+ }),
+ internalFilter: {
+ get() {
+ return this.filter;
+ },
+ set(value) {
+ this.setFilter(value);
+ },
+ },
+ sortText() {
+ const field = this.sortableFields.find((s) => s.orderBy === this.orderBy);
+ return field ? field.label : '';
+ },
+ sortableFields() {
+ return getTableHeaders(this.isGroupPage);
+ },
+ isSortAscending() {
+ return this.sort === ASCENDING_ODER;
+ },
+ tokens() {
+ return [
+ {
+ type: 'type',
+ icon: 'package',
+ title: s__('PackageRegistry|Type'),
+ unique: true,
+ token: PackageTypeToken,
+ operators: [{ value: '=', description: __('is'), default: 'true' }],
+ },
+ ];
+ },
+ },
+ methods: {
+ ...mapActions(['setSorting', 'setFilter']),
+ onDirectionChange() {
+ const sort = this.isSortAscending ? DESCENDING_ORDER : ASCENDING_ODER;
+ this.setSorting({ sort });
+ this.$emit('sort:changed');
+ },
+ onSortItemClick(item) {
+ this.setSorting({ orderBy: item });
+ this.$emit('sort:changed');
+ },
+ clearSearch() {
+ this.setFilter([]);
+ this.$emit('filter:changed');
+ },
+ },
+};
+</script>
+
+<template>
+ <div class="gl-display-flex gl-p-5 gl-bg-gray-10 gl-border-solid gl-border-1 gl-border-gray-100">
+ <gl-filtered-search
+ v-model="internalFilter"
+ class="gl-mr-4 gl-flex-fill-1"
+ :placeholder="__('Filter results')"
+ :available-tokens="tokens"
+ @submit="$emit('filter:changed')"
+ @clear="clearSearch"
+ />
+ <gl-sorting
+ :text="sortText"
+ :is-ascending="isSortAscending"
+ @sortDirectionChange="onDirectionChange"
+ >
+ <gl-sorting-item
+ v-for="item in sortableFields"
+ ref="packageListSortItem"
+ :key="item.orderBy"
+ @click="onSortItemClick(item.orderBy)"
+ >
+ {{ item.label }}
+ </gl-sorting-item>
+ </gl-sorting>
+ </div>
+</template>
diff --git a/app/assets/javascripts/packages/list/components/packages_filter.vue b/app/assets/javascripts/packages/list/components/packages_filter.vue
deleted file mode 100644
index 17398071217..00000000000
--- a/app/assets/javascripts/packages/list/components/packages_filter.vue
+++ /dev/null
@@ -1,21 +0,0 @@
-<script>
-import { GlSearchBoxByClick } from '@gitlab/ui';
-import { mapActions } from 'vuex';
-
-export default {
- components: {
- GlSearchBoxByClick,
- },
- methods: {
- ...mapActions(['setFilter']),
- },
-};
-</script>
-
-<template>
- <gl-search-box-by-click
- :placeholder="s__('PackageRegistry|Filter by name')"
- @submit="$emit('filter')"
- @input="setFilter"
- />
-</template>
diff --git a/app/assets/javascripts/packages/list/components/packages_list_app.vue b/app/assets/javascripts/packages/list/components/packages_list_app.vue
index 2a786b92515..ee93ed2ad89 100644
--- a/app/assets/javascripts/packages/list/components/packages_list_app.vue
+++ b/app/assets/javascripts/packages/list/components/packages_list_app.vue
@@ -1,39 +1,43 @@
<script>
import { mapActions, mapState } from 'vuex';
-import { GlEmptyState, GlTab, GlTabs, GlLink, GlSprintf } from '@gitlab/ui';
-import { s__, sprintf } from '~/locale';
+import { GlEmptyState, GlLink, GlSprintf } from '@gitlab/ui';
+import { s__ } from '~/locale';
import createFlash from '~/flash';
import { historyReplaceState } from '~/lib/utils/common_utils';
import { SHOW_DELETE_SUCCESS_ALERT } from '~/packages/shared/constants';
-import PackageFilter from './packages_filter.vue';
+import { DELETE_PACKAGE_SUCCESS_MESSAGE } from '../constants';
import PackageList from './packages_list.vue';
-import PackageSort from './packages_sort.vue';
-import { PACKAGE_REGISTRY_TABS, DELETE_PACKAGE_SUCCESS_MESSAGE } from '../constants';
import PackageTitle from './package_title.vue';
+import PackageSearch from './package_search.vue';
export default {
components: {
GlEmptyState,
- GlTab,
- GlTabs,
GlLink,
GlSprintf,
- PackageFilter,
PackageList,
- PackageSort,
PackageTitle,
+ PackageSearch,
},
computed: {
...mapState({
emptyListIllustration: (state) => state.config.emptyListIllustration,
emptyListHelpUrl: (state) => state.config.emptyListHelpUrl,
- filterQuery: (state) => state.filterQuery,
+ filter: (state) => state.filter,
selectedType: (state) => state.selectedType,
packageHelpUrl: (state) => state.config.packageHelpUrl,
packagesCount: (state) => state.pagination?.total,
}),
- tabsToRender() {
- return PACKAGE_REGISTRY_TABS;
+ emptySearch() {
+ return (
+ this.filter.filter((f) => f.type !== 'filtered-search-term' || f.value?.data).length === 0
+ );
+ },
+
+ emptyStateTitle() {
+ return this.emptySearch
+ ? s__('PackageRegistry|There are no packages yet')
+ : s__('PackageRegistry|Sorry, your filter produced no results');
},
},
mounted() {
@@ -48,27 +52,6 @@ export default {
onPackageDeleteRequest(item) {
return this.requestDeletePackage(item);
},
- tabChanged(index) {
- const selected = PACKAGE_REGISTRY_TABS[index];
-
- if (selected !== this.selectedType) {
- this.setSelectedType(selected);
- this.requestPackagesList();
- }
- },
- emptyStateTitle({ title, type }) {
- if (this.filterQuery) {
- return s__('PackageRegistry|Sorry, your filter produced no results');
- }
-
- if (type) {
- return sprintf(s__('PackageRegistry|There are no %{packageType} packages yet'), {
- packageType: title,
- });
- }
-
- return s__('PackageRegistry|There are no packages yet');
- },
checkDeleteAlert() {
const urlParams = new URLSearchParams(window.location.search);
const showAlert = urlParams.get(SHOW_DELETE_SUCCESS_ALERT);
@@ -92,33 +75,21 @@ export default {
<template>
<div>
<package-title :package-help-url="packageHelpUrl" :packages-count="packagesCount" />
+ <package-search @sort:changed="requestPackagesList" @filter:changed="requestPackagesList" />
- <gl-tabs @input="tabChanged">
- <template #tabs-end>
- <div
- class="gl-display-flex gl-align-self-center gl-py-2 gl-flex-grow-1 gl-justify-content-end"
- >
- <package-filter class="gl-mr-2" @filter="requestPackagesList" />
- <package-sort @sort:changed="requestPackagesList" />
- </div>
- </template>
-
- <gl-tab v-for="(tab, index) in tabsToRender" :key="index" :title="tab.title">
- <package-list @page:changed="onPageChanged" @package:delete="onPackageDeleteRequest">
- <template #empty-state>
- <gl-empty-state :title="emptyStateTitle(tab)" :svg-path="emptyListIllustration">
- <template #description>
- <gl-sprintf v-if="filterQuery" :message="$options.i18n.widenFilters" />
- <gl-sprintf v-else :message="$options.i18n.noResults">
- <template #noPackagesLink="{ content }">
- <gl-link :href="emptyListHelpUrl" target="_blank">{{ content }}</gl-link>
- </template>
- </gl-sprintf>
+ <package-list @page:changed="onPageChanged" @package:delete="onPackageDeleteRequest">
+ <template #empty-state>
+ <gl-empty-state :title="emptyStateTitle" :svg-path="emptyListIllustration">
+ <template #description>
+ <gl-sprintf v-if="!emptySearch" :message="$options.i18n.widenFilters" />
+ <gl-sprintf v-else :message="$options.i18n.noResults">
+ <template #noPackagesLink="{ content }">
+ <gl-link :href="emptyListHelpUrl" target="_blank">{{ content }}</gl-link>
</template>
- </gl-empty-state>
+ </gl-sprintf>
</template>
- </package-list>
- </gl-tab>
- </gl-tabs>
+ </gl-empty-state>
+ </template>
+ </package-list>
</div>
</template>
diff --git a/app/assets/javascripts/packages/list/components/packages_sort.vue b/app/assets/javascripts/packages/list/components/packages_sort.vue
deleted file mode 100644
index 4b2d9091f8f..00000000000
--- a/app/assets/javascripts/packages/list/components/packages_sort.vue
+++ /dev/null
@@ -1,60 +0,0 @@
-<script>
-import { GlSorting, GlSortingItem } from '@gitlab/ui';
-import { mapState, mapActions } from 'vuex';
-import { ASCENDING_ODER, DESCENDING_ORDER } from '../constants';
-import getTableHeaders from '../utils';
-
-export default {
- name: 'PackageSort',
- components: {
- GlSorting,
- GlSortingItem,
- },
- computed: {
- ...mapState({
- isGroupPage: (state) => state.config.isGroupPage,
- orderBy: (state) => state.sorting.orderBy,
- sort: (state) => state.sorting.sort,
- }),
- sortText() {
- const field = this.sortableFields.find((s) => s.orderBy === this.orderBy);
- return field ? field.label : '';
- },
- sortableFields() {
- return getTableHeaders(this.isGroupPage);
- },
- isSortAscending() {
- return this.sort === ASCENDING_ODER;
- },
- },
- methods: {
- ...mapActions(['setSorting']),
- onDirectionChange() {
- const sort = this.isSortAscending ? DESCENDING_ORDER : ASCENDING_ODER;
- this.setSorting({ sort });
- this.$emit('sort:changed');
- },
- onSortItemClick(item) {
- this.setSorting({ orderBy: item });
- this.$emit('sort:changed');
- },
- },
-};
-</script>
-
-<template>
- <gl-sorting
- :text="sortText"
- :is-ascending="isSortAscending"
- @sortDirectionChange="onDirectionChange"
- >
- <gl-sorting-item
- v-for="item in sortableFields"
- ref="packageListSortItem"
- :key="item.orderBy"
- @click="onSortItemClick(item.orderBy)"
- >
- {{ item.label }}
- </gl-sorting-item>
- </gl-sorting>
-</template>
diff --git a/app/assets/javascripts/packages/list/components/tokens/package_type_token.vue b/app/assets/javascripts/packages/list/components/tokens/package_type_token.vue
new file mode 100644
index 00000000000..74b6774712e
--- /dev/null
+++ b/app/assets/javascripts/packages/list/components/tokens/package_type_token.vue
@@ -0,0 +1,26 @@
+<script>
+import { GlFilteredSearchToken, GlFilteredSearchSuggestion } from '@gitlab/ui';
+import { PACKAGE_TYPES } from '../../constants';
+
+export default {
+ components: {
+ GlFilteredSearchToken,
+ GlFilteredSearchSuggestion,
+ },
+ PACKAGE_TYPES,
+};
+</script>
+
+<template>
+ <gl-filtered-search-token v-bind="{ ...$attrs }" v-on="$listeners">
+ <template #suggestions>
+ <gl-filtered-search-suggestion
+ v-for="(type, index) in $options.PACKAGE_TYPES"
+ :key="index"
+ :value="type.type"
+ >
+ {{ type.title }}
+ </gl-filtered-search-suggestion>
+ </template>
+ </gl-filtered-search-token>
+</template>
diff --git a/app/assets/javascripts/packages/list/constants.js b/app/assets/javascripts/packages/list/constants.js
index e14696e0d1c..9c78778d9f8 100644
--- a/app/assets/javascripts/packages/list/constants.js
+++ b/app/assets/javascripts/packages/list/constants.js
@@ -55,11 +55,7 @@ export const SORT_FIELDS = [
},
];
-export const PACKAGE_REGISTRY_TABS = [
- {
- title: __('All'),
- type: null,
- },
+export const PACKAGE_TYPES = [
{
title: s__('PackageRegistry|Composer'),
type: PackageType.COMPOSER,
diff --git a/app/assets/javascripts/packages/list/packages_list_app_bundle.js b/app/assets/javascripts/packages/list/packages_list_app_bundle.js
index 2f240cff143..73840035728 100644
--- a/app/assets/javascripts/packages/list/packages_list_app_bundle.js
+++ b/app/assets/javascripts/packages/list/packages_list_app_bundle.js
@@ -1,9 +1,9 @@
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import Translate from '~/vue_shared/translate';
+import createDefaultClient from '~/lib/graphql';
import { createStore } from './stores';
import PackagesListApp from './components/packages_list_app.vue';
-import createDefaultClient from '~/lib/graphql';
Vue.use(VueApollo);
Vue.use(Translate);
diff --git a/app/assets/javascripts/packages/list/stores/actions.js b/app/assets/javascripts/packages/list/stores/actions.js
index bbc11e3cf13..b123cbaa531 100644
--- a/app/assets/javascripts/packages/list/stores/actions.js
+++ b/app/assets/javascripts/packages/list/stores/actions.js
@@ -2,7 +2,6 @@ import Api from '~/api';
import axios from '~/lib/utils/axios_utils';
import { deprecatedCreateFlash as createFlash } from '~/flash';
import { DELETE_PACKAGE_ERROR_MESSAGE } from '~/packages/shared/constants';
-import * as types from './mutation_types';
import {
FETCH_PACKAGES_LIST_ERROR_MESSAGE,
DELETE_PACKAGE_SUCCESS_MESSAGE,
@@ -11,11 +10,11 @@ import {
MISSING_DELETE_PATH_ERROR,
} from '../constants';
import { getNewPaginationPage } from '../utils';
+import * as types from './mutation_types';
export const setInitialState = ({ commit }, data) => commit(types.SET_INITIAL_STATE, data);
export const setLoading = ({ commit }, data) => commit(types.SET_MAIN_LOADING, data);
export const setSorting = ({ commit }, data) => commit(types.SET_SORTING, data);
-export const setSelectedType = ({ commit }, data) => commit(types.SET_SELECTED_TYPE, data);
export const setFilter = ({ commit }, data) => commit(types.SET_FILTER, data);
export const receivePackagesListSuccess = ({ commit }, { data, headers }) => {
@@ -29,9 +28,9 @@ export const requestPackagesList = ({ dispatch, state }, params = {}) => {
const { page = DEFAULT_PAGE, per_page = DEFAULT_PAGE_SIZE } = params;
const { sort, orderBy } = state.sorting;
- const type = state.selectedType?.type?.toLowerCase();
- const nameFilter = state.filterQuery?.toLowerCase();
- const packageFilters = { package_type: type, package_name: nameFilter };
+ const type = state.filter.find((f) => f.type === 'type');
+ 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/list/stores/mutation_types.js b/app/assets/javascripts/packages/list/stores/mutation_types.js
index a5a584ccf1f..561ad97f7e3 100644
--- a/app/assets/javascripts/packages/list/stores/mutation_types.js
+++ b/app/assets/javascripts/packages/list/stores/mutation_types.js
@@ -4,5 +4,4 @@ export const SET_PACKAGE_LIST_SUCCESS = 'SET_PACKAGE_LIST_SUCCESS';
export const SET_PAGINATION = 'SET_PAGINATION';
export const SET_MAIN_LOADING = 'SET_MAIN_LOADING';
export const SET_SORTING = 'SET_SORTING';
-export const SET_SELECTED_TYPE = 'SET_SELECTED_TYPE';
export const SET_FILTER = 'SET_FILTER';
diff --git a/app/assets/javascripts/packages/list/stores/mutations.js b/app/assets/javascripts/packages/list/stores/mutations.js
index 2fe7981b3d9..4ce13cfcb29 100644
--- a/app/assets/javascripts/packages/list/stores/mutations.js
+++ b/app/assets/javascripts/packages/list/stores/mutations.js
@@ -1,6 +1,6 @@
-import * as types from './mutation_types';
import { parseIntPagination, normalizeHeaders } from '~/lib/utils/common_utils';
import { GROUP_PAGE_TYPE } from '../constants';
+import * as types from './mutation_types';
export default {
[types.SET_INITIAL_STATE](state, config) {
@@ -28,11 +28,7 @@ export default {
state.sorting = { ...state.sorting, ...sorting };
},
- [types.SET_SELECTED_TYPE](state, type) {
- state.selectedType = type;
- },
-
- [types.SET_FILTER](state, query) {
- state.filterQuery = query;
+ [types.SET_FILTER](state, filter) {
+ state.filter = filter;
},
};
diff --git a/app/assets/javascripts/packages/list/stores/state.js b/app/assets/javascripts/packages/list/stores/state.js
index 18ab2390b87..60f02eddc9f 100644
--- a/app/assets/javascripts/packages/list/stores/state.js
+++ b/app/assets/javascripts/packages/list/stores/state.js
@@ -1,5 +1,3 @@
-import { PACKAGE_REGISTRY_TABS } from '../constants';
-
export default () => ({
/**
* Determine if the component is loading data from the API
@@ -49,9 +47,8 @@ export default () => ({
/**
* The search query that is used to filter packages by name
*/
- filterQuery: '',
+ filter: [],
/**
* The selected TAB of the package types tabs
*/
- selectedType: PACKAGE_REGISTRY_TABS[0],
});
diff --git a/app/assets/javascripts/packages/shared/components/package_list_row.vue b/app/assets/javascripts/packages/shared/components/package_list_row.vue
index d55ca80a7fc..89b217b7d42 100644
--- a/app/assets/javascripts/packages/shared/components/package_list_row.vue
+++ b/app/assets/javascripts/packages/shared/components/package_list_row.vue
@@ -1,11 +1,11 @@
<script>
import { GlButton, GlIcon, GlLink, GlSprintf, GlTooltipDirective, GlTruncate } from '@gitlab/ui';
+import timeagoMixin from '~/vue_shared/mixins/timeago';
+import ListItem from '~/vue_shared/components/registry/list_item.vue';
+import { getPackageTypeLabel } from '../utils';
import PackageTags from './package_tags.vue';
import PackagePath from './package_path.vue';
import PublishMethod from './publish_method.vue';
-import { getPackageTypeLabel } from '../utils';
-import timeagoMixin from '~/vue_shared/mixins/timeago';
-import ListItem from '~/vue_shared/components/registry/list_item.vue';
export default {
name: 'PackageListRow',
@@ -88,7 +88,7 @@ export default {
<div class="gl-display-flex">
<span>{{ packageEntity.version }}</span>
- <div v-if="hasPipeline" class="gl-display-none gl-display-sm-flex gl-ml-2">
+ <div v-if="hasPipeline" class="gl-display-none gl-sm-display-flex gl-ml-2">
<gl-sprintf :message="s__('PackageRegistry|published by %{author}')">
<template #author>{{ packageEntity.pipeline.user.name }}</template>
</gl-sprintf>
diff --git a/app/assets/javascripts/packages/shared/components/package_tags.vue b/app/assets/javascripts/packages/shared/components/package_tags.vue
index 5172b855fc3..5ec950e4d45 100644
--- a/app/assets/javascripts/packages/shared/components/package_tags.vue
+++ b/app/assets/javascripts/packages/shared/components/package_tags.vue
@@ -91,7 +91,7 @@ export default {
variant="muted"
:title="moreTagsTooltip"
size="sm"
- class="gl-display-none gl-display-md-flex gl-ml-2"
+ class="gl-display-none gl-md-display-flex gl-ml-2"
><gl-sprintf :message="__('+%{tags} more')">
<template #tags>
{{ moreTagsDisplay }}
@@ -103,7 +103,7 @@ export default {
v-if="moreTagsDisplay && hideLabel"
data-testid="moreBadge"
variant="muted"
- class="gl-display-md-none gl-ml-2"
+ class="gl-md-display-none gl-ml-2"
>{{ tagsDisplay }}</gl-badge
>
</div>
diff --git a/app/assets/javascripts/packages/shared/components/packages_list_loader.vue b/app/assets/javascripts/packages/shared/components/packages_list_loader.vue
index efd9f8db908..cf555f46f8c 100644
--- a/app/assets/javascripts/packages/shared/components/packages_list_loader.vue
+++ b/app/assets/javascripts/packages/shared/components/packages_list_loader.vue
@@ -21,7 +21,7 @@ export default {
<template>
<div>
- <div class="gl-flex-direction-column gl-display-sm-none" data-testid="mobile-loader">
+ <div class="gl-flex-direction-column gl-sm-display-none" data-testid="mobile-loader">
<gl-skeleton-loader
v-for="index in $options.rowsToRender.mobile"
:key="index"
@@ -37,7 +37,7 @@ export default {
</gl-skeleton-loader>
</div>
<div
- class="gl-display-none gl-display-sm-flex gl-flex-direction-column"
+ class="gl-display-none gl-sm-display-flex gl-flex-direction-column"
data-testid="desktop-loader"
>
<gl-skeleton-loader
diff --git a/app/assets/javascripts/packages_and_registries/settings/group/bundle.js b/app/assets/javascripts/packages_and_registries/settings/group/bundle.js
index a3d507180c6..b2ff75fe1bd 100644
--- a/app/assets/javascripts/packages_and_registries/settings/group/bundle.js
+++ b/app/assets/javascripts/packages_and_registries/settings/group/bundle.js
@@ -1,5 +1,6 @@
import Vue from 'vue';
import Translate from '~/vue_shared/translate';
+import { parseBoolean } from '~/lib/utils/common_utils';
import SettingsApp from './components/group_settings_app.vue';
import { apolloProvider } from './graphql';
@@ -13,6 +14,10 @@ export default () => {
return new Vue({
el,
apolloProvider,
+ provide: {
+ defaultExpanded: parseBoolean(el.dataset.defaultExpanded),
+ groupPath: el.dataset.groupPath,
+ },
render(createElement) {
return createElement(SettingsApp);
},
diff --git a/app/assets/javascripts/packages_and_registries/settings/group/components/group_settings_app.vue b/app/assets/javascripts/packages_and_registries/settings/group/components/group_settings_app.vue
index 6bcecf43a13..31abdc730f8 100644
--- a/app/assets/javascripts/packages_and_registries/settings/group/components/group_settings_app.vue
+++ b/app/assets/javascripts/packages_and_registries/settings/group/components/group_settings_app.vue
@@ -1,9 +1,75 @@
<script>
+import { GlSprintf, GlLink } from '@gitlab/ui';
+import SettingsBlock from '~/vue_shared/components/settings/settings_block.vue';
+
+import {
+ PACKAGE_SETTINGS_HEADER,
+ PACKAGE_SETTINGS_DESCRIPTION,
+ PACKAGES_DOCS_PATH,
+} from '../constants';
+import getGroupPackagesSettingsQuery from '../graphql/queries/get_group_packages_settings.query.graphql';
+
export default {
name: 'GroupSettingsApp',
+ i18n: {
+ PACKAGE_SETTINGS_HEADER,
+ PACKAGE_SETTINGS_DESCRIPTION,
+ },
+ links: {
+ PACKAGES_DOCS_PATH,
+ },
+ components: {
+ GlSprintf,
+ GlLink,
+ SettingsBlock,
+ },
+ inject: {
+ defaultExpanded: {
+ type: Boolean,
+ default: false,
+ required: true,
+ },
+ groupPath: {
+ type: String,
+ required: true,
+ },
+ },
+ apollo: {
+ packageSettings: {
+ query: getGroupPackagesSettingsQuery,
+ variables() {
+ return {
+ fullPath: this.groupPath,
+ };
+ },
+ update(data) {
+ return data.group?.packageSettings;
+ },
+ },
+ },
+ data() {
+ return {
+ packageSettings: {},
+ };
+ },
};
</script>
<template>
- <section></section>
+ <div>
+ <settings-block :default-expanded="defaultExpanded">
+ <template #title> {{ $options.i18n.PACKAGE_SETTINGS_HEADER }}</template>
+ <template #description>
+ <span data-testid="description">
+ <gl-sprintf :message="$options.i18n.PACKAGE_SETTINGS_DESCRIPTION">
+ <template #link="{ content }">
+ <gl-link :href="$options.links.PACKAGES_DOCS_PATH" target="_blank">{{
+ content
+ }}</gl-link>
+ </template>
+ </gl-sprintf>
+ </span>
+ </template>
+ </settings-block>
+ </div>
</template>
diff --git a/app/assets/javascripts/packages_and_registries/settings/group/constants.js b/app/assets/javascripts/packages_and_registries/settings/group/constants.js
new file mode 100644
index 00000000000..b0c4bf821f9
--- /dev/null
+++ b/app/assets/javascripts/packages_and_registries/settings/group/constants.js
@@ -0,0 +1,9 @@
+import { s__ } from '~/locale';
+import { helpPagePath } from '~/helpers/help_page_helper';
+
+export const PACKAGE_SETTINGS_HEADER = s__('PackageRegistry|Package Registry');
+export const PACKAGE_SETTINGS_DESCRIPTION = s__(
+ 'PackageRegistry|GitLab Packages allows organizations to utilize GitLab as a private repository for a variety of common package formats. %{linkStart}More Information%{linkEnd}',
+);
+
+export const PACKAGES_DOCS_PATH = helpPagePath('user/packages');
diff --git a/app/assets/javascripts/packages_and_registries/settings/group/graphql/mutations/update_group_packages_settings.mutation.graphql b/app/assets/javascripts/packages_and_registries/settings/group/graphql/mutations/update_group_packages_settings.mutation.graphql
new file mode 100644
index 00000000000..1fc59bd3496
--- /dev/null
+++ b/app/assets/javascripts/packages_and_registries/settings/group/graphql/mutations/update_group_packages_settings.mutation.graphql
@@ -0,0 +1,9 @@
+mutation updateNamespacePackageSettings($input: UpdateNamespacePackageSettingsInput!) {
+ updateNamespacePackageSettings(input: $input) {
+ packageSettings {
+ mavenDuplicatesAllowed
+ mavenDuplicateExceptionRegex
+ }
+ errors
+ }
+}
diff --git a/app/assets/javascripts/packages_and_registries/settings/group/graphql/queries/get_group_packages_settings.query.graphql b/app/assets/javascripts/packages_and_registries/settings/group/graphql/queries/get_group_packages_settings.query.graphql
new file mode 100644
index 00000000000..2011659887d
--- /dev/null
+++ b/app/assets/javascripts/packages_and_registries/settings/group/graphql/queries/get_group_packages_settings.query.graphql
@@ -0,0 +1,8 @@
+query getGroupPackagesSettings($fullPath: ID!) {
+ group(fullPath: $fullPath) {
+ packageSettings {
+ mavenDuplicatesAllowed
+ mavenDuplicateExceptionRegex
+ }
+ }
+}
diff --git a/app/assets/javascripts/pages/admin/abuse_reports/abuse_reports.js b/app/assets/javascripts/pages/admin/abuse_reports/abuse_reports.js
index da7f81759ea..e78b3f9ec95 100644
--- a/app/assets/javascripts/pages/admin/abuse_reports/abuse_reports.js
+++ b/app/assets/javascripts/pages/admin/abuse_reports/abuse_reports.js
@@ -1,6 +1,6 @@
import $ from 'jquery';
-import { truncate } from '../../../lib/utils/text_utility';
import { parseBoolean } from '~/lib/utils/common_utils';
+import { truncate } from '../../../lib/utils/text_utility';
const MAX_MESSAGE_LENGTH = 500;
const MESSAGE_CELL_SELECTOR = '.abuse-reports .message';
diff --git a/app/assets/javascripts/pages/admin/abuse_reports/index.js b/app/assets/javascripts/pages/admin/abuse_reports/index.js
index d97e24d9e0b..5649c47d7e8 100644
--- a/app/assets/javascripts/pages/admin/abuse_reports/index.js
+++ b/app/assets/javascripts/pages/admin/abuse_reports/index.js
@@ -1,6 +1,6 @@
/* eslint-disable no-new */
-import AbuseReports from './abuse_reports';
import UsersSelect from '~/users_select';
+import AbuseReports from './abuse_reports';
document.addEventListener('DOMContentLoaded', () => {
new AbuseReports();
diff --git a/app/assets/javascripts/pages/admin/application_settings/general/index.js b/app/assets/javascripts/pages/admin/application_settings/general/index.js
index af1595398a8..f7bd32880ff 100644
--- a/app/assets/javascripts/pages/admin/application_settings/general/index.js
+++ b/app/assets/javascripts/pages/admin/application_settings/general/index.js
@@ -1,6 +1,10 @@
+// This is a true violation of @gitlab/no-runtime-template-compiler, as it
+// relies on app/views/admin/application_settings/_gitpod.html.haml for its
+// template.
+/* eslint-disable @gitlab/no-runtime-template-compiler */
import Vue from 'vue';
-import initUserInternalRegexPlaceholder from '../account_and_limits';
import IntegrationHelpText from '~/vue_shared/components/integrations_help_text.vue';
+import initUserInternalRegexPlaceholder from '../account_and_limits';
document.addEventListener('DOMContentLoaded', () => {
initUserInternalRegexPlaceholder();
diff --git a/app/assets/javascripts/pages/admin/cohorts/index.js b/app/assets/javascripts/pages/admin/cohorts/index.js
deleted file mode 100644
index 1cc54df15a1..00000000000
--- a/app/assets/javascripts/pages/admin/cohorts/index.js
+++ /dev/null
@@ -1,22 +0,0 @@
-import Vue from 'vue';
-import UsagePingDisabled from '~/admin/cohorts/components/usage_ping_disabled.vue';
-
-document.addEventListener('DOMContentLoaded', () => {
- const emptyStateContainer = document.getElementById('js-cohorts-empty-state');
-
- if (!emptyStateContainer) return false;
-
- const { emptyStateSvgPath, enableUsagePingLink, docsLink } = emptyStateContainer.dataset;
-
- return new Vue({
- el: emptyStateContainer,
- provide: {
- svgPath: emptyStateSvgPath,
- primaryButtonPath: enableUsagePingLink,
- docsLink,
- },
- render(h) {
- return h(UsagePingDisabled);
- },
- });
-});
diff --git a/app/assets/javascripts/pages/admin/groups/new/index.js b/app/assets/javascripts/pages/admin/groups/new/index.js
index b94c999ed12..94f7cfd55be 100644
--- a/app/assets/javascripts/pages/admin/groups/new/index.js
+++ b/app/assets/javascripts/pages/admin/groups/new/index.js
@@ -1,6 +1,6 @@
+import initFilePickers from '~/file_pickers';
import BindInOut from '../../../../behaviors/bind_in_out';
import Group from '../../../../group';
-import initFilePickers from '~/file_pickers';
document.addEventListener('DOMContentLoaded', () => {
BindInOut.initAll();
diff --git a/app/assets/javascripts/pages/admin/index.js b/app/assets/javascripts/pages/admin/index.js
index 3f4e658fc8d..792a6eda14e 100644
--- a/app/assets/javascripts/pages/admin/index.js
+++ b/app/assets/javascripts/pages/admin/index.js
@@ -1,6 +1,6 @@
-import initAdmin from './admin';
import initAdminStatisticsPanel from '../../admin/statistics_panel/index';
import initVueAlerts from '../../vue_alerts';
+import initAdmin from './admin';
document.addEventListener('DOMContentLoaded', initVueAlerts);
diff --git a/app/assets/javascripts/pages/admin/jobs/index/index.js b/app/assets/javascripts/pages/admin/jobs/index/index.js
index 4df210debb5..9db2868e051 100644
--- a/app/assets/javascripts/pages/admin/jobs/index/index.js
+++ b/app/assets/javascripts/pages/admin/jobs/index/index.js
@@ -1,5 +1,6 @@
import Vue from 'vue';
import Translate from '~/vue_shared/translate';
+import { BV_SHOW_MODAL } from '~/lib/utils/constants';
import stopJobsModal from './components/stop_jobs_modal.vue';
Vue.use(Translate);
@@ -18,7 +19,7 @@ document.addEventListener('DOMContentLoaded', () => {
mounted() {
stopJobsButton.classList.remove('disabled');
stopJobsButton.addEventListener('click', () => {
- this.$root.$emit('bv::show::modal', modalId, `#${buttonId}`);
+ this.$root.$emit(BV_SHOW_MODAL, modalId, `#${buttonId}`);
});
},
render(createElement) {
diff --git a/app/assets/javascripts/pages/admin/projects/index/index.js b/app/assets/javascripts/pages/admin/projects/index/index.js
index bf512ef395d..40f176d5eb2 100644
--- a/app/assets/javascripts/pages/admin/projects/index/index.js
+++ b/app/assets/javascripts/pages/admin/projects/index/index.js
@@ -2,6 +2,7 @@ import Vue from 'vue';
import Translate from '~/vue_shared/translate';
import csrf from '~/lib/utils/csrf';
+import { BV_SHOW_MODAL } from '~/lib/utils/constants';
import deleteProjectModal from './components/delete_project_modal.vue';
@@ -24,7 +25,7 @@ document.addEventListener('DOMContentLoaded', () => {
deleteModal.deleteProjectUrl = buttonProps.deleteProjectUrl;
deleteModal.projectName = buttonProps.projectName;
- this.$root.$emit('bv::show::modal', 'delete-project-modal');
+ this.$root.$emit(BV_SHOW_MODAL, 'delete-project-modal');
});
});
},
diff --git a/app/assets/javascripts/pages/admin/runners/index.js b/app/assets/javascripts/pages/admin/runners/index.js
index e60c6133c7c..0b92f5ef90f 100644
--- a/app/assets/javascripts/pages/admin/runners/index.js
+++ b/app/assets/javascripts/pages/admin/runners/index.js
@@ -1,6 +1,7 @@
import initFilteredSearch from '~/pages/search/init_filtered_search';
import AdminRunnersFilteredSearchTokenKeys from '~/filtered_search/admin_runners_filtered_search_token_keys';
import { FILTERED_SEARCH } from '~/pages/constants';
+import { initInstallRunner } from '~/pages/shared/mount_runner_instructions';
document.addEventListener('DOMContentLoaded', () => {
initFilteredSearch({
@@ -8,4 +9,6 @@ document.addEventListener('DOMContentLoaded', () => {
filteredSearchTokenKeys: AdminRunnersFilteredSearchTokenKeys,
useDefaultState: true,
});
+
+ initInstallRunner();
});
diff --git a/app/assets/javascripts/pages/admin/users/components/delete_user_modal.vue b/app/assets/javascripts/pages/admin/users/components/delete_user_modal.vue
index 9c303cc6445..d2b83f980d7 100644
--- a/app/assets/javascripts/pages/admin/users/components/delete_user_modal.vue
+++ b/app/assets/javascripts/pages/admin/users/components/delete_user_modal.vue
@@ -85,38 +85,36 @@ export default {
<template>
<gl-modal ref="modal" modal-id="delete-user-modal" :title="modalTitle" kind="danger">
- <template>
- <p>
- <gl-sprintf :message="content">
- <template #username>
- <strong>{{ username }}</strong>
- </template>
- <template #strong="props">
- <strong>{{ props.content }}</strong>
- </template>
- </gl-sprintf>
- </p>
+ <p>
+ <gl-sprintf :message="content">
+ <template #username>
+ <strong>{{ username }}</strong>
+ </template>
+ <template #strong="props">
+ <strong>{{ props.content }}</strong>
+ </template>
+ </gl-sprintf>
+ </p>
- <p>
- <gl-sprintf :message="s__('AdminUsers|To confirm, type %{username}')">
- <template #username>
- <code>{{ username }}</code>
- </template>
- </gl-sprintf>
- </p>
+ <p>
+ <gl-sprintf :message="s__('AdminUsers|To confirm, type %{username}')">
+ <template #username>
+ <code>{{ username }}</code>
+ </template>
+ </gl-sprintf>
+ </p>
- <form ref="form" :action="deleteUserUrl" method="post" @submit.prevent>
- <input ref="method" type="hidden" name="_method" value="delete" />
- <input :value="csrfToken" type="hidden" name="authenticity_token" />
- <gl-form-input
- v-model="enteredUsername"
- autofocus
- type="text"
- name="username"
- autocomplete="off"
- />
- </form>
- </template>
+ <form ref="form" :action="deleteUserUrl" method="post" @submit.prevent>
+ <input ref="method" type="hidden" name="_method" value="delete" />
+ <input :value="csrfToken" type="hidden" name="authenticity_token" />
+ <gl-form-input
+ v-model="enteredUsername"
+ autofocus
+ type="text"
+ name="username"
+ autocomplete="off"
+ />
+ </form>
<template #modal-footer>
<gl-button @click="onCancel">{{ s__('Cancel') }}</gl-button>
<gl-button
diff --git a/app/assets/javascripts/pages/admin/users/index.js b/app/assets/javascripts/pages/admin/users/index.js
index 75a8284f5f8..1fd838e704c 100644
--- a/app/assets/javascripts/pages/admin/users/index.js
+++ b/app/assets/javascripts/pages/admin/users/index.js
@@ -1,10 +1,11 @@
import Vue from 'vue';
import Translate from '~/vue_shared/translate';
-import ModalManager from './components/user_modal_manager.vue';
import csrf from '~/lib/utils/csrf';
import initConfirmModal from '~/confirm_modal';
-import initAdminUsersApp from '~/admin/users';
+import { initAdminUsersApp, initCohortsEmptyState } from '~/admin/users';
+import initTabs from '~/admin/users/tabs';
+import ModalManager from './components/user_modal_manager.vue';
const MODAL_TEXTS_CONTAINER_SELECTOR = '#js-modal-texts';
const MODAL_MANAGER_SELECTOR = '#js-delete-user-modal';
@@ -58,4 +59,6 @@ document.addEventListener('DOMContentLoaded', () => {
initConfirmModal();
initAdminUsersApp();
+ initCohortsEmptyState();
+ initTabs();
});
diff --git a/app/assets/javascripts/pages/groups/group_members/index.js b/app/assets/javascripts/pages/groups/group_members/index.js
index 5346e3720e8..516311dd841 100644
--- a/app/assets/javascripts/pages/groups/group_members/index.js
+++ b/app/assets/javascripts/pages/groups/group_members/index.js
@@ -3,10 +3,11 @@ import memberExpirationDate from '~/member_expiration_date';
import UsersSelect from '~/users_select';
import groupsSelect from '~/groups_select';
import RemoveMemberModal from '~/vue_shared/components/remove_member_modal.vue';
-import { initGroupMembersApp } from '~/groups/members';
import initInviteMembersModal from '~/invite_members/init_invite_members_modal';
import initInviteMembersTrigger from '~/invite_members/init_invite_members_trigger';
-import { memberRequestFormatter, groupLinkRequestFormatter } from '~/groups/members/utils';
+import { initMembersApp } from '~/members/index';
+import { groupMemberRequestFormatter } from '~/groups/members/utils';
+import { groupLinkRequestFormatter } from '~/members/utils';
import { s__ } from '~/locale';
function mountRemoveMemberModal() {
@@ -25,11 +26,11 @@ function mountRemoveMemberModal() {
const SHARED_FIELDS = ['account', 'expires', 'maxRole', 'expiration', 'actions'];
-initGroupMembersApp(document.querySelector('.js-group-members-list'), {
+initMembersApp(document.querySelector('.js-group-members-list'), {
tableFields: SHARED_FIELDS.concat(['source', 'granted']),
tableAttrs: { tr: { 'data-qa-selector': 'member_row' } },
tableSortableFields: ['account', 'granted', 'maxRole', 'lastSignIn'],
- requestFormatter: memberRequestFormatter,
+ requestFormatter: groupMemberRequestFormatter,
filteredSearchBar: {
show: true,
tokens: ['two_factor', 'with_inherited_permissions'],
@@ -38,7 +39,8 @@ initGroupMembersApp(document.querySelector('.js-group-members-list'), {
recentSearchesStorageKey: 'group_members',
},
});
-initGroupMembersApp(document.querySelector('.js-group-linked-list'), {
+
+initMembersApp(document.querySelector('.js-group-group-links-list'), {
tableFields: SHARED_FIELDS.concat('granted'),
tableAttrs: {
table: { 'data-qa-selector': 'groups_list' },
@@ -46,9 +48,9 @@ initGroupMembersApp(document.querySelector('.js-group-linked-list'), {
},
requestFormatter: groupLinkRequestFormatter,
});
-initGroupMembersApp(document.querySelector('.js-group-invited-members-list'), {
+initMembersApp(document.querySelector('.js-group-invited-members-list'), {
tableFields: SHARED_FIELDS.concat('invited'),
- requestFormatter: memberRequestFormatter,
+ requestFormatter: groupMemberRequestFormatter,
filteredSearchBar: {
show: true,
tokens: [],
@@ -57,9 +59,9 @@ initGroupMembersApp(document.querySelector('.js-group-invited-members-list'), {
recentSearchesStorageKey: 'group_invited_members',
},
});
-initGroupMembersApp(document.querySelector('.js-group-access-requests-list'), {
+initMembersApp(document.querySelector('.js-group-access-requests-list'), {
tableFields: SHARED_FIELDS.concat('requested'),
- requestFormatter: memberRequestFormatter,
+ requestFormatter: groupMemberRequestFormatter,
});
groupsSelect();
diff --git a/app/assets/javascripts/pages/groups/new/group_path_validator.js b/app/assets/javascripts/pages/groups/new/group_path_validator.js
index 97f3d8cf7f5..7da196a34ab 100644
--- a/app/assets/javascripts/pages/groups/new/group_path_validator.js
+++ b/app/assets/javascripts/pages/groups/new/group_path_validator.js
@@ -1,9 +1,9 @@
import { debounce } from 'lodash';
import InputValidator from '~/validators/input_validator';
-import fetchGroupPathAvailability from './fetch_group_path_availability';
import { deprecatedCreateFlash as flash } from '~/flash';
import { __ } from '~/locale';
+import fetchGroupPathAvailability from './fetch_group_path_availability';
const debounceTimeoutDuration = 1000;
const invalidInputClass = 'gl-field-error-outline';
diff --git a/app/assets/javascripts/pages/groups/new/index.js b/app/assets/javascripts/pages/groups/new/index.js
index 7021473b380..88118d07c9e 100644
--- a/app/assets/javascripts/pages/groups/new/index.js
+++ b/app/assets/javascripts/pages/groups/new/index.js
@@ -1,8 +1,8 @@
import $ from 'jquery';
import BindInOut from '~/behaviors/bind_in_out';
import Group from '~/group';
-import GroupPathValidator from './group_path_validator';
import initFilePickers from '~/file_pickers';
+import GroupPathValidator from './group_path_validator';
const parentId = $('#group_parent_id');
if (!parentId.val()) {
diff --git a/app/assets/javascripts/pages/groups/settings/ci_cd/show/index.js b/app/assets/javascripts/pages/groups/settings/ci_cd/show/index.js
index e8d8c985ade..19d81f61dba 100644
--- a/app/assets/javascripts/pages/groups/settings/ci_cd/show/index.js
+++ b/app/assets/javascripts/pages/groups/settings/ci_cd/show/index.js
@@ -4,6 +4,7 @@ import initFilteredSearch from '~/pages/search/init_filtered_search';
import GroupRunnersFilteredSearchTokenKeys from '~/filtered_search/group_runners_filtered_search_token_keys';
import { FILTERED_SEARCH } from '~/pages/constants';
import initSharedRunnersForm from '~/group_settings/mount_shared_runners';
+import { initInstallRunner } from '~/pages/shared/mount_runner_instructions';
document.addEventListener('DOMContentLoaded', () => {
// Initialize expandable settings panels
@@ -18,4 +19,6 @@ document.addEventListener('DOMContentLoaded', () => {
initSharedRunnersForm();
initVariableList();
+
+ initInstallRunner();
});
diff --git a/app/assets/javascripts/pages/groups/shared/group_details.js b/app/assets/javascripts/pages/groups/shared/group_details.js
index 8d956c694c0..add955ab1f2 100644
--- a/app/assets/javascripts/pages/groups/shared/group_details.js
+++ b/app/assets/javascripts/pages/groups/shared/group_details.js
@@ -6,8 +6,11 @@ import notificationsDropdown from '~/notifications_dropdown';
import NotificationsForm from '~/notifications_form';
import ProjectsList from '~/projects_list';
import ShortcutsNavigation from '~/behaviors/shortcuts/shortcuts_navigation';
-import GroupTabs from './group_tabs';
import initInviteMembersBanner from '~/groups/init_invite_members_banner';
+import initInviteMembersTrigger from '~/invite_members/init_invite_members_trigger';
+import initInviteMembersModal from '~/invite_members/init_invite_members_modal';
+import initNotificationsDropdown from '~/notifications';
+import GroupTabs from './group_tabs';
export default function initGroupDetails(actionName = 'show') {
const loadableActions = [ACTIVE_TAB_SHARED, ACTIVE_TAB_ARCHIVED];
@@ -20,8 +23,16 @@ export default function initGroupDetails(actionName = 'show') {
new GroupTabs({ parentEl: '.groups-listing', action });
new ShortcutsNavigation();
new NotificationsForm();
- notificationsDropdown();
+
+ if (gon.features?.vueNotificationDropdown) {
+ initNotificationsDropdown();
+ } else {
+ notificationsDropdown();
+ }
+
new ProjectsList();
initInviteMembersBanner();
+ initInviteMembersModal();
+ initInviteMembersTrigger();
}
diff --git a/app/assets/javascripts/pages/milestones/shared/delete_milestone_modal_init.js b/app/assets/javascripts/pages/milestones/shared/delete_milestone_modal_init.js
index e8b67891c42..829dfa7bb5e 100644
--- a/app/assets/javascripts/pages/milestones/shared/delete_milestone_modal_init.js
+++ b/app/assets/javascripts/pages/milestones/shared/delete_milestone_modal_init.js
@@ -1,5 +1,6 @@
import Vue from 'vue';
import Translate from '~/vue_shared/translate';
+import { BV_SHOW_MODAL } from '~/lib/utils/constants';
import DeleteMilestoneModal from './components/delete_milestone_modal.vue';
import eventHub from './event_hub';
@@ -47,7 +48,7 @@ export default () => {
deleteMilestoneButtons.forEach((button) => {
button.removeAttribute('disabled');
button.addEventListener('click', () => {
- this.$root.$emit('bv::show::modal', 'delete-milestone-modal');
+ this.$root.$emit(BV_SHOW_MODAL, 'delete-milestone-modal');
eventHub.$once('deleteMilestoneModal.requestStarted', onRequestStarted);
this.setModalProps({
diff --git a/app/assets/javascripts/pages/profiles/notifications/show/index.js b/app/assets/javascripts/pages/profiles/notifications/show/index.js
index 2e24a10fa5c..cd1512df2da 100644
--- a/app/assets/javascripts/pages/profiles/notifications/show/index.js
+++ b/app/assets/javascripts/pages/profiles/notifications/show/index.js
@@ -1,7 +1,9 @@
+import initNotificationsDropdown from '~/notifications';
import NotificationsForm from '../../../../notifications_form';
import notificationsDropdown from '../../../../notifications_dropdown';
document.addEventListener('DOMContentLoaded', () => {
new NotificationsForm(); // eslint-disable-line no-new
notificationsDropdown();
+ initNotificationsDropdown();
});
diff --git a/app/assets/javascripts/pages/profiles/show/index.js b/app/assets/javascripts/pages/profiles/show/index.js
index b78f24ca2fb..f1f0b2c508b 100644
--- a/app/assets/javascripts/pages/profiles/show/index.js
+++ b/app/assets/javascripts/pages/profiles/show/index.js
@@ -1,10 +1,10 @@
import $ from 'jquery';
-import GfmAutoComplete from 'ee_else_ce/gfm_auto_complete';
import emojiRegex from 'emoji-regex';
+import GfmAutoComplete from 'ee_else_ce/gfm_auto_complete';
import { deprecatedCreateFlash as createFlash } from '~/flash';
-import EmojiMenu from './emoji_menu';
import { __ } from '~/locale';
import * as Emoji from '~/emoji';
+import EmojiMenu from './emoji_menu';
const defaultStatusEmoji = 'speech_balloon';
diff --git a/app/assets/javascripts/pages/projects/activity/index.js b/app/assets/javascripts/pages/projects/activity/index.js
index d39ea3d10bf..03fbad0f1ec 100644
--- a/app/assets/javascripts/pages/projects/activity/index.js
+++ b/app/assets/javascripts/pages/projects/activity/index.js
@@ -1,7 +1,5 @@
import Activities from '~/activities';
import ShortcutsNavigation from '~/behaviors/shortcuts/shortcuts_navigation';
-document.addEventListener('DOMContentLoaded', () => {
- new Activities(); // eslint-disable-line no-new
- new ShortcutsNavigation(); // eslint-disable-line no-new
-});
+new Activities(); // eslint-disable-line no-new
+new ShortcutsNavigation(); // eslint-disable-line no-new
diff --git a/app/assets/javascripts/pages/projects/alert_management/details/index.js b/app/assets/javascripts/pages/projects/alert_management/details/index.js
index a20f6713c9d..183e07ca1fc 100644
--- a/app/assets/javascripts/pages/projects/alert_management/details/index.js
+++ b/app/assets/javascripts/pages/projects/alert_management/details/index.js
@@ -1,3 +1,3 @@
-import AlertDetails from '~/alert_management/details';
+import AlertDetails from '~/vue_shared/alert_details';
AlertDetails('#js-alert_details');
diff --git a/app/assets/javascripts/pages/projects/blame/show/index.js b/app/assets/javascripts/pages/projects/blame/show/index.js
index 80d0bff92fa..fa22c11d1d7 100644
--- a/app/assets/javascripts/pages/projects/blame/show/index.js
+++ b/app/assets/javascripts/pages/projects/blame/show/index.js
@@ -1,3 +1,3 @@
import initBlob from '~/pages/projects/init_blob';
-document.addEventListener('DOMContentLoaded', initBlob);
+initBlob();
diff --git a/app/assets/javascripts/pages/projects/clusters/show/index.js b/app/assets/javascripts/pages/projects/clusters/show/index.js
index a05ea8ae845..376268ec3f6 100644
--- a/app/assets/javascripts/pages/projects/clusters/show/index.js
+++ b/app/assets/javascripts/pages/projects/clusters/show/index.js
@@ -1,7 +1,7 @@
import ClustersBundle from '~/clusters/clusters_bundle';
import initGkeNamespace from '~/create_cluster/gke_cluster_namespace';
-import initClusterHealth from './cluster_health';
import initIntegrationForm from '~/clusters/forms/show';
+import initClusterHealth from './cluster_health';
document.addEventListener('DOMContentLoaded', () => {
new ClustersBundle(); // eslint-disable-line no-new
diff --git a/app/assets/javascripts/pages/projects/commit/pipelines/index.js b/app/assets/javascripts/pages/projects/commit/pipelines/index.js
index eaf340f2725..f542014c5b9 100644
--- a/app/assets/javascripts/pages/projects/commit/pipelines/index.js
+++ b/app/assets/javascripts/pages/projects/commit/pipelines/index.js
@@ -1,5 +1,7 @@
import { initCommitBoxInfo } from '~/projects/commit_box/info';
import initPipelines from '~/commit/pipelines/pipelines_bundle';
+import initCommitActions from '~/projects/commit';
initCommitBoxInfo();
initPipelines();
+initCommitActions();
diff --git a/app/assets/javascripts/pages/projects/commit/show/index.js b/app/assets/javascripts/pages/projects/commit/show/index.js
index 5cfdb125e4f..5a3b486fd40 100644
--- a/app/assets/javascripts/pages/projects/commit/show/index.js
+++ b/app/assets/javascripts/pages/projects/commit/show/index.js
@@ -14,8 +14,7 @@ import flash from '~/flash';
import { __ } from '~/locale';
import loadAwardsHandler from '~/awards_handler';
import { initCommitBoxInfo } from '~/projects/commit_box/info';
-import initRevertCommitTrigger from '~/projects/commit/init_revert_commit_trigger';
-import initRevertCommitModal from '~/projects/commit/init_revert_commit_modal';
+import initCommitActions from '~/projects/commit';
const hasPerfBar = document.querySelector('.with-performance-bar');
const performanceHeight = hasPerfBar ? 35 : 0;
@@ -47,5 +46,4 @@ if (filesContainer.length) {
new Diff();
}
loadAwardsHandler();
-initRevertCommitModal();
-initRevertCommitTrigger();
+initCommitActions();
diff --git a/app/assets/javascripts/pages/projects/compare/index/index.js b/app/assets/javascripts/pages/projects/compare/index/index.js
new file mode 100644
index 00000000000..b86c9ec442f
--- /dev/null
+++ b/app/assets/javascripts/pages/projects/compare/index/index.js
@@ -0,0 +1,3 @@
+import initCompareSelector from '~/projects/compare';
+
+initCompareSelector();
diff --git a/app/assets/javascripts/pages/projects/edit/index.js b/app/assets/javascripts/pages/projects/edit/index.js
index 5f1d3edc3ba..413e43c638b 100644
--- a/app/assets/javascripts/pages/projects/edit/index.js
+++ b/app/assets/javascripts/pages/projects/edit/index.js
@@ -5,12 +5,12 @@ import initConfirmDangerModal from '~/confirm_danger_modal';
import mountBadgeSettings from '~/pages/shared/mount_badge_settings';
import dirtySubmitFactory from '~/dirty_submit/dirty_submit_factory';
import initFilePickers from '~/file_pickers';
-import initProjectLoadingSpinner from '../shared/save_project_loader';
-import initProjectPermissionsSettings from '../shared/permissions';
import initProjectDeleteButton from '~/projects/project_delete_button';
import UserCallout from '~/user_callout';
import initServiceDesk from '~/projects/settings_service_desk';
-import mountSearchSettings from './mount_search_settings';
+import initSearchSettings from '~/search_settings';
+import initProjectPermissionsSettings from '../shared/permissions';
+import initProjectLoadingSpinner from '../shared/save_project_loader';
document.addEventListener('DOMContentLoaded', () => {
initFilePickers();
@@ -32,5 +32,5 @@ document.addEventListener('DOMContentLoaded', () => {
),
);
- mountSearchSettings();
+ initSearchSettings();
});
diff --git a/app/assets/javascripts/pages/projects/edit/mount_search_settings.js b/app/assets/javascripts/pages/projects/edit/mount_search_settings.js
deleted file mode 100644
index 6c477dd7e80..00000000000
--- a/app/assets/javascripts/pages/projects/edit/mount_search_settings.js
+++ /dev/null
@@ -1,12 +0,0 @@
-const mountSearchSettings = async () => {
- const el = document.querySelector('.js-search-settings-app');
-
- if (el) {
- const { default: initSearch } = await import(
- /* webpackChunkName: 'search_settings' */ '~/search_settings'
- );
- initSearch({ el });
- }
-};
-
-export default mountSearchSettings;
diff --git a/app/assets/javascripts/pages/projects/find_file/show/index.js b/app/assets/javascripts/pages/projects/find_file/show/index.js
index 388d7d7bdda..7fb009e7dc9 100644
--- a/app/assets/javascripts/pages/projects/find_file/show/index.js
+++ b/app/assets/javascripts/pages/projects/find_file/show/index.js
@@ -2,12 +2,10 @@ import $ from 'jquery';
import ProjectFindFile from '~/project_find_file';
import ShortcutsFindFile from '~/behaviors/shortcuts/shortcuts_find_file';
-document.addEventListener('DOMContentLoaded', () => {
- const findElement = document.querySelector('.js-file-finder');
- const projectFindFile = new ProjectFindFile($('.file-finder-holder'), {
- url: findElement.dataset.fileFindUrl,
- treeUrl: findElement.dataset.findTreeUrl,
- blobUrlTemplate: findElement.dataset.blobUrlTemplate,
- });
- new ShortcutsFindFile(projectFindFile); // eslint-disable-line no-new
+const findElement = document.querySelector('.js-file-finder');
+const projectFindFile = new ProjectFindFile($('.file-finder-holder'), {
+ url: findElement.dataset.fileFindUrl,
+ treeUrl: findElement.dataset.findTreeUrl,
+ blobUrlTemplate: findElement.dataset.blobUrlTemplate,
});
+new ShortcutsFindFile(projectFindFile); // eslint-disable-line no-new
diff --git a/app/assets/javascripts/pages/projects/forks/new/components/fork_groups_list_item.vue b/app/assets/javascripts/pages/projects/forks/new/components/fork_groups_list_item.vue
index 57838050d55..b596c862d8f 100644
--- a/app/assets/javascripts/pages/projects/forks/new/components/fork_groups_list_item.vue
+++ b/app/assets/javascripts/pages/projects/forks/new/components/fork_groups_list_item.vue
@@ -94,7 +94,7 @@ export default {
</div>
<gl-link
:href="group.relative_path"
- class="gl-display-none gl-flex-shrink-0 gl-display-sm-flex gl-mr-3"
+ class="gl-display-none gl-flex-shrink-0 gl-sm-display-flex gl-mr-3"
>
<gl-avatar :size="32" shape="rect" :entity-name="group.name" :src="group.avatarUrl" />
</gl-link>
@@ -113,7 +113,7 @@ export default {
<gl-badge
v-if="isGroupPendingRemoval"
variant="warning"
- class="gl-display-none gl-display-sm-flex gl-mt-3 gl-mr-1"
+ class="gl-display-none gl-sm-display-flex gl-mt-3 gl-mr-1"
>{{ __('pending removal') }}</gl-badge
>
<span v-if="group.permission" class="user-access-role gl-mt-3">
diff --git a/app/assets/javascripts/pages/projects/incidents/show/index.js b/app/assets/javascripts/pages/projects/incidents/show/index.js
index 5b3f03cd57e..850bb8da5e7 100644
--- a/app/assets/javascripts/pages/projects/incidents/show/index.js
+++ b/app/assets/javascripts/pages/projects/incidents/show/index.js
@@ -3,7 +3,5 @@ import initRelatedIssues from '~/related_issues';
import initShow from '../../issues/show';
initShow();
-if (!gon.features?.vueIssuableSidebar) {
- initSidebarBundle();
-}
+initSidebarBundle();
initRelatedIssues();
diff --git a/app/assets/javascripts/pages/projects/index.js b/app/assets/javascripts/pages/projects/index.js
index 3e9962a4e72..45e9643b3f3 100644
--- a/app/assets/javascripts/pages/projects/index.js
+++ b/app/assets/javascripts/pages/projects/index.js
@@ -1,5 +1,5 @@
-import Project from './project';
import ShortcutsNavigation from '../../behaviors/shortcuts/shortcuts_navigation';
+import Project from './project';
new Project(); // eslint-disable-line no-new
new ShortcutsNavigation(); // eslint-disable-line no-new
diff --git a/app/assets/javascripts/pages/projects/issues/index/index.js b/app/assets/javascripts/pages/projects/issues/index/index.js
index f3ccedc47c8..5956933fd99 100644
--- a/app/assets/javascripts/pages/projects/issues/index/index.js
+++ b/app/assets/javascripts/pages/projects/issues/index/index.js
@@ -9,7 +9,6 @@ import { FILTERED_SEARCH } from '~/pages/constants';
import { ISSUABLE_INDEX } from '~/pages/projects/constants';
import initIssuablesList from '~/issues_list';
import initManualOrdering from '~/manual_ordering';
-import { showLearnGitLabIssuesPopover } from '~/onboarding_issues';
IssuableFilteredSearchTokenKeys.addExtraTokensForIssues();
@@ -25,4 +24,3 @@ new UsersSelect();
initManualOrdering();
initIssuablesList();
-showLearnGitLabIssuesPopover();
diff --git a/app/assets/javascripts/pages/projects/issues/service_desk/index.js b/app/assets/javascripts/pages/projects/issues/service_desk/index.js
index 231ee6732e9..5be9f6117dc 100644
--- a/app/assets/javascripts/pages/projects/issues/service_desk/index.js
+++ b/app/assets/javascripts/pages/projects/issues/service_desk/index.js
@@ -1,5 +1,5 @@
-import FilteredSearchServiceDesk from './filtered_search';
import initIssuablesList from '~/issues_list';
+import FilteredSearchServiceDesk from './filtered_search';
const supportBotData = JSON.parse(
document.querySelector('.js-service-desk-issues').dataset.supportBot,
diff --git a/app/assets/javascripts/pages/projects/issues/show/index.js b/app/assets/javascripts/pages/projects/issues/show/index.js
index 630add51a97..64f34633db3 100644
--- a/app/assets/javascripts/pages/projects/issues/show/index.js
+++ b/app/assets/javascripts/pages/projects/issues/show/index.js
@@ -3,7 +3,5 @@ import initRelatedIssues from '~/related_issues';
import initShow from '../show';
initShow();
-if (gon.features && !gon.features.vueIssuableSidebar) {
- initSidebarBundle();
-}
+initSidebarBundle();
initRelatedIssues();
diff --git a/app/assets/javascripts/pages/projects/jobs/index/index.js b/app/assets/javascripts/pages/projects/jobs/index/index.js
index c343a37b292..f66c09cb1ac 100644
--- a/app/assets/javascripts/pages/projects/jobs/index/index.js
+++ b/app/assets/javascripts/pages/projects/jobs/index/index.js
@@ -7,10 +7,13 @@ document.addEventListener('DOMContentLoaded', () => {
remainingTimeElements.forEach(
(el) =>
new Vue({
- ...GlCountdown,
el,
- propsData: {
- endDateString: el.dateTime,
+ render(h) {
+ return h(GlCountdown, {
+ props: {
+ endDateString: el.dateTime,
+ },
+ });
},
}),
);
diff --git a/app/assets/javascripts/pages/projects/labels/index/index.js b/app/assets/javascripts/pages/projects/labels/index/index.js
index 4f5e5c8cceb..6954180c670 100644
--- a/app/assets/javascripts/pages/projects/labels/index/index.js
+++ b/app/assets/javascripts/pages/projects/labels/index/index.js
@@ -1,6 +1,7 @@
import Vue from 'vue';
import Translate from '~/vue_shared/translate';
import initLabels from '~/init_labels';
+import { BV_SHOW_MODAL } from '~/lib/utils/constants';
import eventHub from '../event_hub';
import PromoteLabelModal from '../components/promote_label_modal.vue';
@@ -49,7 +50,7 @@ const initLabelIndex = () => {
promoteLabelButtons.forEach((button) => {
button.removeAttribute('disabled');
button.addEventListener('click', () => {
- this.$root.$emit('bv::show::modal', 'promote-label-modal');
+ this.$root.$emit(BV_SHOW_MODAL, 'promote-label-modal');
eventHub.$once('promoteLabelModal.requestStarted', onRequestStarted);
this.setModalProps({
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 602d749ee07..e0476f181e3 100644
--- a/app/assets/javascripts/pages/projects/merge_requests/show/index.js
+++ b/app/assets/javascripts/pages/projects/merge_requests/show/index.js
@@ -1,14 +1,12 @@
import initMrNotes from '~/mr_notes';
import { initReviewBar } from '~/batch_comments';
import initSidebarBundle from '~/sidebar/sidebar_bundle';
-import initShow from '../init_merge_request_show';
import initIssuableHeaderWarning from '~/vue_shared/components/issuable/init_issuable_header_warning';
import store from '~/mr_notes/stores';
+import initShow from '../init_merge_request_show';
initShow();
-if (gon.features && !gon.features.vueIssuableSidebar) {
- initSidebarBundle();
-}
+initSidebarBundle();
initMrNotes();
initReviewBar();
initIssuableHeaderWarning(store);
diff --git a/app/assets/javascripts/pages/projects/new/index.js b/app/assets/javascripts/pages/projects/new/index.js
index 88f4db3ec08..259e62a4c4f 100644
--- a/app/assets/javascripts/pages/projects/new/index.js
+++ b/app/assets/javascripts/pages/projects/new/index.js
@@ -1,7 +1,7 @@
-import initProjectVisibilitySelector from '../../../project_visibility';
-import initProjectNew from '../../../projects/project_new';
import { __ } from '~/locale';
import { deprecatedCreateFlash as createFlash } from '~/flash';
+import initProjectVisibilitySelector from '../../../project_visibility';
+import initProjectNew from '../../../projects/project_new';
document.addEventListener('DOMContentLoaded', () => {
initProjectVisibilitySelector();
diff --git a/app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/pipeline_schedules_callout.vue b/app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/pipeline_schedules_callout.vue
index 8ee9d481466..e73f78bca78 100644
--- a/app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/pipeline_schedules_callout.vue
+++ b/app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/pipeline_schedules_callout.vue
@@ -2,8 +2,8 @@
import Vue from 'vue';
import Cookies from 'js-cookie';
import { GlButton } from '@gitlab/ui';
-import Translate from '../../../../../vue_shared/translate';
import { parseBoolean } from '~/lib/utils/common_utils';
+import Translate from '../../../../../vue_shared/translate';
Vue.use(Translate);
diff --git a/app/assets/javascripts/pages/projects/pipeline_schedules/shared/init_form.js b/app/assets/javascripts/pages/projects/pipeline_schedules/shared/init_form.js
index 497e2c9c0ae..d944cfbdbc7 100644
--- a/app/assets/javascripts/pages/projects/pipeline_schedules/shared/init_form.js
+++ b/app/assets/javascripts/pages/projects/pipeline_schedules/shared/init_form.js
@@ -2,10 +2,10 @@ import $ from 'jquery';
import Vue from 'vue';
import Translate from '../../../../vue_shared/translate';
import GlFieldErrors from '../../../../gl_field_errors';
+import setupNativeFormVariableList from '../../../../ci_variable_list/native_form_variable_list';
import intervalPatternInput from './components/interval_pattern_input.vue';
import TimezoneDropdown from './components/timezone_dropdown';
import TargetBranchDropdown from './components/target_branch_dropdown';
-import setupNativeFormVariableList from '../../../../ci_variable_list/native_form_variable_list';
Vue.use(Translate);
diff --git a/app/assets/javascripts/pages/projects/project.js b/app/assets/javascripts/pages/projects/project.js
index ef6953db83b..7fd59012e83 100644
--- a/app/assets/javascripts/pages/projects/project.js
+++ b/app/assets/javascripts/pages/projects/project.js
@@ -7,9 +7,9 @@ import { mergeUrlParams } from '~/lib/utils/url_utility';
import { serializeForm } from '~/lib/utils/forms';
import axios from '~/lib/utils/axios_utils';
import { deprecatedCreateFlash as flash } from '~/flash';
-import projectSelect from '../../project_select';
import initDeprecatedJQueryDropdown from '~/deprecated_jquery_dropdown';
import initClonePanel from '~/clone_panel';
+import projectSelect from '../../project_select';
export default class Project {
constructor() {
@@ -126,8 +126,9 @@ export default class Project {
const refs = this.fullData.Branches.concat(this.fullData.Tags);
const currentRef = refs.find((ref) => loc.indexOf(ref) > -1);
if (currentRef) {
- const targetPath = loc.split(currentRef)[1].slice(1);
+ const targetPath = loc.split(currentRef)[1].slice(1).split('#')[0];
selectedUrl.searchParams.set('path', targetPath);
+ selectedUrl.hash = window.location.hash;
}
}
diff --git a/app/assets/javascripts/pages/projects/project_members/index.js b/app/assets/javascripts/pages/projects/project_members/index.js
index 3e0a48ee6a2..f029b26fa78 100644
--- a/app/assets/javascripts/pages/projects/project_members/index.js
+++ b/app/assets/javascripts/pages/projects/project_members/index.js
@@ -6,6 +6,8 @@ import groupsSelect from '~/groups_select';
import RemoveMemberModal from '~/vue_shared/components/remove_member_modal.vue';
import initInviteMembersModal from '~/invite_members/init_invite_members_modal';
import initInviteMembersTrigger from '~/invite_members/init_invite_members_trigger';
+import { __ } from '~/locale';
+import { deprecatedCreateFlash as flash } from '~/flash';
function mountRemoveMemberModal() {
const el = document.querySelector('.js-remove-member-modal');
@@ -32,3 +34,65 @@ document.addEventListener('DOMContentLoaded', () => {
new Members(); // eslint-disable-line no-new
new UsersSelect(); // eslint-disable-line no-new
});
+
+if (window.gon.features.vueProjectMembersList) {
+ const SHARED_FIELDS = ['account', 'expires', 'maxRole', 'expiration', 'actions'];
+
+ Promise.all([
+ import('~/members/index'),
+ import('~/members/utils'),
+ import('~/projects/members/utils'),
+ import('~/locale'),
+ ])
+ .then(
+ ([
+ { initMembersApp },
+ { groupLinkRequestFormatter },
+ { projectMemberRequestFormatter },
+ { s__ },
+ ]) => {
+ initMembersApp(document.querySelector('.js-project-members-list'), {
+ tableFields: SHARED_FIELDS.concat(['source', 'granted']),
+ tableAttrs: { tr: { 'data-qa-selector': 'member_row' } },
+ tableSortableFields: ['account', 'granted', 'maxRole', 'lastSignIn'],
+ requestFormatter: projectMemberRequestFormatter,
+ filteredSearchBar: {
+ show: true,
+ tokens: ['with_inherited_permissions'],
+ searchParam: 'search',
+ placeholder: s__('Members|Filter members'),
+ recentSearchesStorageKey: 'project_members',
+ },
+ });
+
+ initMembersApp(document.querySelector('.js-project-group-links-list'), {
+ tableFields: SHARED_FIELDS.concat('granted'),
+ tableAttrs: {
+ table: { 'data-qa-selector': 'groups_list' },
+ tr: { 'data-qa-selector': 'group_row' },
+ },
+ requestFormatter: groupLinkRequestFormatter,
+ filteredSearchBar: {
+ show: true,
+ tokens: [],
+ searchParam: 'search_groups',
+ placeholder: s__('Members|Search groups'),
+ recentSearchesStorageKey: 'project_group_links',
+ },
+ });
+
+ initMembersApp(document.querySelector('.js-project-invited-members-list'), {
+ tableFields: SHARED_FIELDS.concat('invited'),
+ requestFormatter: projectMemberRequestFormatter,
+ });
+
+ initMembersApp(document.querySelector('.js-project-access-requests-list'), {
+ tableFields: SHARED_FIELDS.concat('requested'),
+ requestFormatter: projectMemberRequestFormatter,
+ });
+ },
+ )
+ .catch(() => {
+ flash(__('An error occurred while loading the members, please try again.'));
+ });
+}
diff --git a/app/assets/javascripts/pages/projects/releases/index/index.js b/app/assets/javascripts/pages/projects/releases/index/index.js
index 24c9cd528b3..caf95ae53c8 100644
--- a/app/assets/javascripts/pages/projects/releases/index/index.js
+++ b/app/assets/javascripts/pages/projects/releases/index/index.js
@@ -1,3 +1,3 @@
import initReleases from '~/releases/mount_index';
-document.addEventListener('DOMContentLoaded', initReleases);
+initReleases();
diff --git a/app/assets/javascripts/pages/projects/settings/ci_cd/show/index.js b/app/assets/javascripts/pages/projects/settings/ci_cd/show/index.js
index 1321155b7ec..f2dd0da4d62 100644
--- a/app/assets/javascripts/pages/projects/settings/ci_cd/show/index.js
+++ b/app/assets/javascripts/pages/projects/settings/ci_cd/show/index.js
@@ -6,6 +6,7 @@ import initDeployFreeze from '~/deploy_freeze';
import initSettingsPipelinesTriggers from '~/ci_settings_pipeline_triggers';
import initSharedRunnersToggle from '~/projects/settings/mount_shared_runners_toggle';
import initArtifactsSettings from '~/artifacts_settings';
+import { initInstallRunner } from '~/pages/shared/mount_runner_instructions';
document.addEventListener('DOMContentLoaded', () => {
// Initialize expandable settings panels
@@ -39,4 +40,6 @@ document.addEventListener('DOMContentLoaded', () => {
if (gon?.features?.vueifySharedRunnersToggle) {
initSharedRunnersToggle();
}
+
+ initInstallRunner();
});
diff --git a/app/assets/javascripts/pages/projects/settings/repository/show/index.js b/app/assets/javascripts/pages/projects/settings/repository/show/index.js
index 1ef4b460263..e90954c14c5 100644
--- a/app/assets/javascripts/pages/projects/settings/repository/show/index.js
+++ b/app/assets/javascripts/pages/projects/settings/repository/show/index.js
@@ -1,5 +1,5 @@
-import initForm from '../form';
import MirrorRepos from '~/mirrors/mirror_repos';
+import initForm from '../form';
document.addEventListener('DOMContentLoaded', () => {
initForm();
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 4af476fbd68..d81c11ddbaf 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
@@ -3,9 +3,8 @@ import { GlIcon, GlSprintf, GlLink, GlFormCheckbox } from '@gitlab/ui';
import settingsMixin from 'ee_else_ce/pages/projects/shared/permissions/mixins/settings_pannel_mixin';
import { s__ } from '~/locale';
-import projectFeatureSetting from './project_feature_setting.vue';
import projectFeatureToggle from '~/vue_shared/components/toggle_button.vue';
-import projectSettingRow from './project_setting_row.vue';
+import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import {
visibilityOptions,
visibilityLevelDescriptions,
@@ -15,7 +14,8 @@ import {
featureAccessLevelNone,
} from '../constants';
import { toggleHiddenClassBySelector } from '../external';
-import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
+import projectFeatureSetting from './project_feature_setting.vue';
+import projectSettingRow from './project_setting_row.vue';
const PAGE_FEATURE_ACCESS_LEVEL = s__('ProjectSettings|Everyone');
@@ -75,6 +75,11 @@ export default {
required: false,
default: false,
},
+ securityAndComplianceAvailable: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
visibilityHelpPath: {
type: String,
required: false,
@@ -141,6 +146,7 @@ export default {
metricsDashboardAccessLevel: featureAccessLevel.PROJECT_MEMBERS,
analyticsAccessLevel: featureAccessLevel.EVERYONE,
requirementsAccessLevel: featureAccessLevel.EVERYONE,
+ securityAndComplianceAccessLevel: featureAccessLevel.PROJECT_MEMBERS,
operationsAccessLevel: featureAccessLevel.EVERYONE,
containerRegistryEnabled: true,
lfsEnabled: true,
@@ -218,11 +224,11 @@ export default {
repositoryHelpText() {
if (this.visibilityLevel === visibilityOptions.PRIVATE) {
- return s__('ProjectSettings|View and edit files in this project');
+ return s__('ProjectSettings|View and edit files in this project.');
}
return s__(
- 'ProjectSettings|View and edit files in this project. Non-project members will only have read access',
+ 'ProjectSettings|View and edit files in this project. Non-project members will only have read access.',
);
},
},
@@ -264,6 +270,10 @@ export default {
featureAccessLevel.PROJECT_MEMBERS,
this.requirementsAccessLevel,
);
+ this.securityAndComplianceAccessLevel = Math.min(
+ featureAccessLevel.PROJECT_MEMBERS,
+ this.securityAndComplianceAccessLevel,
+ );
this.operationsAccessLevel = Math.min(
featureAccessLevel.PROJECT_MEMBERS,
this.operationsAccessLevel,
@@ -390,7 +400,7 @@ export default {
name="project[request_access_enabled]"
/>
<input v-model="requestAccessEnabled" type="checkbox" />
- {{ s__('ProjectSettings|Allow users to request access') }}
+ {{ s__('ProjectSettings|Users can request access') }}
</label>
</project-setting-row>
</div>
@@ -401,7 +411,7 @@ export default {
<project-setting-row
ref="issues-settings"
:label="s__('ProjectSettings|Issues')"
- :help-text="s__('ProjectSettings|Lightweight issue tracking system for this project')"
+ :help-text="s__('ProjectSettings|Lightweight issue tracking system.')"
>
<project-feature-setting
v-model="issuesAccessLevel"
@@ -424,7 +434,7 @@ export default {
<project-setting-row
ref="merge-request-settings"
:label="s__('ProjectSettings|Merge requests')"
- :help-text="s__('ProjectSettings|Submit changes to be merged upstream')"
+ :help-text="s__('ProjectSettings|Submit changes to be merged upstream.')"
>
<project-feature-setting
v-model="mergeRequestsAccessLevel"
@@ -436,9 +446,7 @@ export default {
<project-setting-row
ref="fork-settings"
:label="s__('ProjectSettings|Forks')"
- :help-text="
- s__('ProjectSettings|Allow users to make copies of your repository to a new project')
- "
+ :help-text="s__('ProjectSettings|Users can copy the repository to a new project.')"
>
<project-feature-setting
v-model="forkingAccessLevel"
@@ -450,7 +458,7 @@ export default {
<project-setting-row
ref="pipeline-settings"
:label="s__('ProjectSettings|Pipelines')"
- :help-text="s__('ProjectSettings|Build, test, and deploy your changes')"
+ :help-text="s__('ProjectSettings|Build, test, and deploy your changes.')"
>
<project-feature-setting
v-model="buildsAccessLevel"
@@ -487,7 +495,7 @@ export default {
:help-path="lfsHelpPath"
:label="s__('ProjectSettings|Git Large File Storage (LFS)')"
:help-text="
- s__('ProjectSettings|Manages large files such as audio, video, and graphics files')
+ s__('ProjectSettings|Manages large files such as audio, video, and graphics files.')
"
>
<project-feature-toggle
@@ -499,7 +507,7 @@ export default {
<gl-sprintf
:message="
s__(
- 'ProjectSettings|LFS objects from this repository are still available to forks. %{linkStart}How do I remove them?%{linkEnd}',
+ 'ProjectSettings|LFS objects from this repository are available to forks. %{linkStart}How do I remove them?%{linkEnd}',
)
"
>
@@ -519,7 +527,7 @@ export default {
:help-path="packagesHelpPath"
:label="s__('ProjectSettings|Packages')"
:help-text="
- s__('ProjectSettings|Every project can have its own space to store its packages')
+ s__('ProjectSettings|Every project can have its own space to store its packages.')
"
>
<project-feature-toggle
@@ -532,7 +540,7 @@ export default {
<project-setting-row
ref="analytics-settings"
:label="s__('ProjectSettings|Analytics')"
- :help-text="s__('ProjectSettings|View project analytics')"
+ :help-text="s__('ProjectSettings|View project analytics.')"
>
<project-feature-setting
v-model="analyticsAccessLevel"
@@ -544,7 +552,7 @@ export default {
v-if="requirementsAvailable"
ref="requirements-settings"
:label="s__('ProjectSettings|Requirements')"
- :help-text="s__('ProjectSettings|Requirements management system for this project')"
+ :help-text="s__('ProjectSettings|Requirements management system.')"
>
<project-feature-setting
v-model="requirementsAccessLevel"
@@ -553,9 +561,20 @@ export default {
/>
</project-setting-row>
<project-setting-row
+ v-if="securityAndComplianceAvailable"
+ :label="s__('ProjectSettings|Security & Compliance')"
+ :help-text="s__('ProjectSettings|Security & Compliance for this project')"
+ >
+ <project-feature-setting
+ v-model="securityAndComplianceAccessLevel"
+ :options="featureAccessLevelOptions"
+ name="project[project_feature_attributes][security_and_compliance_access_level]"
+ />
+ </project-setting-row>
+ <project-setting-row
ref="wiki-settings"
:label="s__('ProjectSettings|Wiki')"
- :help-text="s__('ProjectSettings|Pages for project documentation')"
+ :help-text="s__('ProjectSettings|Pages for project documentation.')"
>
<project-feature-setting
v-model="wikiAccessLevel"
@@ -566,7 +585,7 @@ export default {
<project-setting-row
ref="snippet-settings"
:label="s__('ProjectSettings|Snippets')"
- :help-text="s__('ProjectSettings|Share code pastes with others out of Git repository')"
+ :help-text="s__('ProjectSettings|Share code with others outside the project.')"
>
<project-feature-setting
v-model="snippetsAccessLevel"
@@ -580,7 +599,7 @@ export default {
:help-path="pagesHelpPath"
:label="s__('ProjectSettings|Pages')"
:help-text="
- s__('ProjectSettings|With GitLab Pages you can host your static websites on GitLab')
+ s__('ProjectSettings|With GitLab Pages you can host your static websites on GitLab.')
"
>
<project-feature-setting
@@ -592,7 +611,7 @@ export default {
<project-setting-row
ref="operations-settings"
:label="s__('ProjectSettings|Operations')"
- :help-text="s__('ProjectSettings|Environments, logs, cluster management, and more')"
+ :help-text="s__('ProjectSettings|Environments, logs, cluster management, and more.')"
>
<project-feature-setting
v-model="operationsAccessLevel"
@@ -604,11 +623,7 @@ export default {
<project-setting-row
ref="metrics-visibility-settings"
:label="__('Metrics Dashboard')"
- :help-text="
- s__(
- 'ProjectSettings|With Metrics Dashboard you can visualize this project performance metrics',
- )
- "
+ :help-text="s__('ProjectSettings|Visualize the project\'s performance metrics.')"
>
<project-feature-setting
v-model="metricsDashboardAccessLevel"
@@ -626,9 +641,7 @@ export default {
{{ s__('ProjectSettings|Disable email notifications') }}
</label>
<span class="form-text text-muted">{{
- s__(
- 'ProjectSettings|This setting will override user notification preferences for all project members.',
- )
+ s__('ProjectSettings|Override user notification preferences for all project members.')
}}</span>
</project-setting-row>
<project-setting-row class="mb-3">
@@ -644,7 +657,7 @@ export default {
{{ s__('ProjectSettings|Show default award emojis') }}
<template #help>{{
s__(
- 'ProjectSettings|When enabled, issues, merge requests, and snippets will always show thumbs-up and thumbs-down award emoji buttons.',
+ 'ProjectSettings|Always show thumbs-up and thumbs-down award emoji buttons on issues, merge requests, and snippets.',
)
}}</template>
</gl-form-checkbox>
@@ -662,9 +675,7 @@ export default {
<gl-form-checkbox v-model="allowEditingCommitMessages">
{{ s__('ProjectSettings|Allow editing commit messages') }}
<template #help>{{
- s__(
- 'ProjectSettings|When enabled, commit authors will be able to edit commit messages on unprotected branches.',
- )
+ s__('ProjectSettings|Commit authors can edit commit messages on unprotected branches.')
}}</template>
</gl-form-checkbox>
</project-setting-row>
diff --git a/app/assets/javascripts/pages/projects/shared/permissions/mixins/settings_pannel_mixin.js b/app/assets/javascripts/pages/projects/shared/permissions/mixins/settings_pannel_mixin.js
index ae0936417ad..b52e30dae39 100644
--- a/app/assets/javascripts/pages/projects/shared/permissions/mixins/settings_pannel_mixin.js
+++ b/app/assets/javascripts/pages/projects/shared/permissions/mixins/settings_pannel_mixin.js
@@ -3,6 +3,7 @@ export default {
return {
packagesEnabled: false,
requirementsEnabled: false,
+ securityAndComplianceEnabled: false,
};
},
watch: {
diff --git a/app/assets/javascripts/pages/projects/show/index.js b/app/assets/javascripts/pages/projects/show/index.js
index cc676b98e49..5120b6bee27 100644
--- a/app/assets/javascripts/pages/projects/show/index.js
+++ b/app/assets/javascripts/pages/projects/show/index.js
@@ -7,11 +7,11 @@ import BlobViewer from '~/blob/viewer/index';
import Activities from '~/activities';
import initReadMore from '~/read_more';
import leaveByUrl from '~/namespaces/leave_by_url';
-import Star from '../../../star';
-import notificationsDropdown from '../../../notifications_dropdown';
-import { showLearnGitLabProjectPopover } from '~/onboarding_issues';
import initInviteMembersTrigger from '~/invite_members/init_invite_members_trigger';
import initInviteMembersModal from '~/invite_members/init_invite_members_modal';
+import initVueNotificationsDropdown from '~/notifications';
+import notificationsDropdown from '../../../notifications_dropdown';
+import Star from '../../../star';
initReadMore();
new Star(); // eslint-disable-line no-new
@@ -40,9 +40,14 @@ if (document.querySelector('.project-show-activity')) {
leaveByUrl('project');
-showLearnGitLabProjectPopover();
+if (gon.features?.vueNotificationDropdown) {
+ initVueNotificationsDropdown();
+} else {
+ notificationsDropdown();
+}
+
+initVueNotificationsDropdown();
-notificationsDropdown();
new ShortcutsNavigation(); // eslint-disable-line no-new
initInviteMembersTrigger();
diff --git a/app/assets/javascripts/pages/projects/wikis/index.js b/app/assets/javascripts/pages/projects/wikis/index.js
index 9c75531ca40..dead61cf358 100644
--- a/app/assets/javascripts/pages/projects/wikis/index.js
+++ b/app/assets/javascripts/pages/projects/wikis/index.js
@@ -1,3 +1,3 @@
import initWikis from '~/pages/shared/wikis';
-document.addEventListener('DOMContentLoaded', initWikis);
+initWikis();
diff --git a/app/assets/javascripts/pages/search/show/index.js b/app/assets/javascripts/pages/search/show/index.js
index b6171e08e01..a8c288c3663 100644
--- a/app/assets/javascripts/pages/search/show/index.js
+++ b/app/assets/javascripts/pages/search/show/index.js
@@ -1,7 +1,5 @@
-import Search from './search';
import { initSearchApp } from '~/search';
document.addEventListener('DOMContentLoaded', () => {
- initSearchApp(); // Vue Bootstrap
- return new Search(); // Legacy Search Methods
+ initSearchApp();
});
diff --git a/app/assets/javascripts/pages/search/show/search.js b/app/assets/javascripts/pages/search/show/search.js
deleted file mode 100644
index cbef5ab1bbc..00000000000
--- a/app/assets/javascripts/pages/search/show/search.js
+++ /dev/null
@@ -1,54 +0,0 @@
-import $ from 'jquery';
-import setHighlightClass from 'ee_else_ce/search/highlight_blob_search_result';
-import Project from '~/pages/projects/project';
-import { visitUrl } from '~/lib/utils/url_utility';
-import refreshCounts from './refresh_counts';
-
-export default class Search {
- constructor() {
- this.searchInput = '.js-search-input';
- this.searchClear = '.js-search-clear';
-
- setHighlightClass(); // Code Highlighting
- this.eventListeners(); // Search Form Actions
- refreshCounts(); // Other Scope Tab Counts
- Project.initRefSwitcher(); // Code Search Branch Picker
- }
-
- eventListeners() {
- $(document).off('keyup', this.searchInput).on('keyup', this.searchInput, this.searchKeyUp);
- $(document)
- .off('click', this.searchClear)
- .on('click', this.searchClear, this.clearSearchField.bind(this));
-
- $('a.js-search-clear').off('click', this.clearSearchFilter).on('click', this.clearSearchFilter);
- }
-
- static submitSearch() {
- return $('.js-search-form').submit();
- }
-
- searchKeyUp() {
- const $input = $(this);
- if ($input.val() === '') {
- $('.js-search-clear').addClass('hidden');
- } else {
- $('.js-search-clear').removeClass('hidden');
- }
- }
-
- clearSearchField() {
- return $(this.searchInput).val('').trigger('keyup').focus();
- }
-
- // We need to manually follow the link on the anchors
- // that have this event bound, as their `click` default
- // behavior is prevented by the toggle logic.
- /* eslint-disable-next-line class-methods-use-this */
- clearSearchFilter(ev) {
- const $target = $(ev.currentTarget);
-
- visitUrl($target.href);
- ev.stopPropagation();
- }
-}
diff --git a/app/assets/javascripts/pages/sessions/new/index.js b/app/assets/javascripts/pages/sessions/new/index.js
index 55bc93a2b13..84c825d129c 100644
--- a/app/assets/javascripts/pages/sessions/new/index.js
+++ b/app/assets/javascripts/pages/sessions/new/index.js
@@ -1,7 +1,7 @@
import $ from 'jquery';
+import NoEmojiValidator from '../../../emoji/no_emoji_validator';
import LengthValidator from './length_validator';
import UsernameValidator from './username_validator';
-import NoEmojiValidator from '../../../emoji/no_emoji_validator';
import SigninTabsMemoizer from './signin_tabs_memoizer';
import OAuthRememberMe from './oauth_remember_me';
import preserveUrlFragment from './preserve_url_fragment';
diff --git a/app/assets/javascripts/pages/shared/mount_runner_instructions.js b/app/assets/javascripts/pages/shared/mount_runner_instructions.js
new file mode 100644
index 00000000000..51028e585b8
--- /dev/null
+++ b/app/assets/javascripts/pages/shared/mount_runner_instructions.js
@@ -0,0 +1,32 @@
+import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+import createDefaultClient from '~/lib/graphql';
+import InstallRunnerInstructions from '~/vue_shared/components/runner_instructions/runner_instructions.vue';
+
+Vue.use(VueApollo);
+
+export function initInstallRunner(componentId = 'js-install-runner') {
+ const installRunnerEl = document.getElementById(componentId);
+
+ if (installRunnerEl) {
+ const defaultClient = createDefaultClient();
+ const { projectPath, groupPath } = installRunnerEl.dataset;
+
+ const apolloProvider = new VueApollo({
+ defaultClient,
+ });
+
+ // eslint-disable-next-line no-new
+ new Vue({
+ el: installRunnerEl,
+ apolloProvider,
+ provide: {
+ projectPath,
+ groupPath,
+ },
+ render(createElement) {
+ return createElement(InstallRunnerInstructions);
+ },
+ });
+ }
+}
diff --git a/app/assets/javascripts/pages/shared/wikis/index.js b/app/assets/javascripts/pages/shared/wikis/index.js
index 5e23b9bab4e..0679686110d 100644
--- a/app/assets/javascripts/pages/shared/wikis/index.js
+++ b/app/assets/javascripts/pages/shared/wikis/index.js
@@ -3,9 +3,9 @@ import Vue from 'vue';
import Translate from '~/vue_shared/translate';
import csrf from '~/lib/utils/csrf';
import ShortcutsWiki from '~/behaviors/shortcuts/shortcuts_wiki';
-import Wikis from './wikis';
import ZenMode from '../../../zen_mode';
import GLForm from '../../../gl_form';
+import Wikis from './wikis';
import deleteWikiModal from './components/delete_wiki_modal.vue';
export default () => {
diff --git a/app/assets/javascripts/performance_bar/components/add_request.vue b/app/assets/javascripts/performance_bar/components/add_request.vue
index 54bca8a1b67..d48a5acb85c 100644
--- a/app/assets/javascripts/performance_bar/components/add_request.vue
+++ b/app/assets/javascripts/performance_bar/components/add_request.vue
@@ -27,7 +27,7 @@ export default {
<div id="peek-view-add-request" class="view">
<form class="form-inline" @submit.prevent>
<button
- class="btn-blank btn-link bold"
+ class="btn-blank btn-link bold gl-text-blue-300"
type="button"
:title="__(`Add request manually`)"
@click="toggleInput"
diff --git a/app/assets/javascripts/performance_bar/components/detailed_metric.vue b/app/assets/javascripts/performance_bar/components/detailed_metric.vue
index 930c5e50511..6d896a97455 100644
--- a/app/assets/javascripts/performance_bar/components/detailed_metric.vue
+++ b/app/assets/javascripts/performance_bar/components/detailed_metric.vue
@@ -94,9 +94,9 @@ export default {
data-qa-selector="detailed_metric_content"
>
<gl-button v-gl-modal="modalId" class="gl-mr-2" type="button" variant="link">
- {{ metricDetailsLabel }}
+ <span class="gl-text-blue-300 gl-font-weight-bold">{{ metricDetailsLabel }}</span>
</gl-button>
- <gl-modal :modal-id="modalId" :title="header" size="lg" modal-class="gl-mt-7" scrollable>
+ <gl-modal :modal-id="modalId" :title="header" size="lg" footer-class="d-none" scrollable>
<table class="table">
<template v-if="detailsList.length">
<tr v-for="(item, index) in detailsList" :key="index">
@@ -116,7 +116,7 @@ export default {
{{ item[key] }}
<gl-button
v-if="keyIndex == 0 && item.backtrace"
- class="gl-ml-3"
+ class="btn-sm gl-ml-3"
data-testid="backtrace-expand-btn"
type="button"
:aria-label="__('Toggle backtrace')"
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 e38771785b7..85789cd1fdf 100644
--- a/app/assets/javascripts/performance_bar/components/performance_bar_app.vue
+++ b/app/assets/javascripts/performance_bar/components/performance_bar_app.vue
@@ -2,10 +2,10 @@
/* eslint-disable vue/no-v-html */
import { glEmojiTag } from '~/emoji';
+import { s__ } from '~/locale';
import AddRequest from './add_request.vue';
import DetailedMetric from './detailed_metric.vue';
import RequestSelector from './request_selector.vue';
-import { s__ } from '~/locale';
export default {
components: {
@@ -64,6 +64,11 @@ export default {
keys: ['request', 'body'],
},
{
+ metric: 'external-http',
+ header: s__('PerformanceBar|External Http calls'),
+ keys: ['label', 'code', 'proxy', 'error'],
+ },
+ {
metric: 'total',
header: s__('PerformanceBar|Frontend resources'),
keys: ['name', 'size'],
@@ -120,7 +125,7 @@ export default {
<div id="js-peek" :class="env">
<div
v-if="currentRequest"
- class="d-flex container-fluid container-limited"
+ class="d-flex container-fluid container-limited justify-content-center"
data-qa-selector="performance_bar"
>
<div id="peek-view-host" class="view">
@@ -147,11 +152,15 @@ export default {
id="peek-view-trace"
class="view"
>
- <a :href="currentRequest.details.tracing.tracing_url">{{ s__('PerformanceBar|trace') }}</a>
+ <a class="gl-text-blue-300" :href="currentRequest.details.tracing.tracing_url">{{
+ s__('PerformanceBar|trace')
+ }}</a>
</div>
<add-request v-on="$listeners" />
<div v-if="currentRequest.details" id="peek-download" class="view">
- <a :download="downloadName" :href="downloadPath">{{ s__('PerformanceBar|Download') }}</a>
+ <a class="gl-text-blue-300" :download="downloadName" :href="downloadPath">{{
+ s__('PerformanceBar|Download')
+ }}</a>
</div>
<request-selector
v-if="currentRequest"
diff --git a/app/assets/javascripts/performance_bar/performance_bar_log.js b/app/assets/javascripts/performance_bar/performance_bar_log.js
index c61b0cb32e8..aad99e2604e 100644
--- a/app/assets/javascripts/performance_bar/performance_bar_log.js
+++ b/app/assets/javascripts/performance_bar/performance_bar_log.js
@@ -43,7 +43,7 @@ const logUserTimingMetrics = () => {
const initPerformanceBarLog = () => {
console.log(
`%c ${String.fromCodePoint(0x1f98a)} GitLab performance bar`,
- 'width:100%;background-color: #292961; color: #FFFFFF; font-size:24px; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto; padding: 10px;display:block;padding-right: 100px;',
+ 'width:100%; background-color: #292961; color: #FFFFFF; padding: 10px; display:block;',
);
initVitalsLog();
diff --git a/app/assets/javascripts/performance_bar/services/performance_bar_service.js b/app/assets/javascripts/performance_bar/services/performance_bar_service.js
index 38255b3a37d..a614342c858 100644
--- a/app/assets/javascripts/performance_bar/services/performance_bar_service.js
+++ b/app/assets/javascripts/performance_bar/services/performance_bar_service.js
@@ -1,5 +1,5 @@
-import axios from '../../lib/utils/axios_utils';
import { parseBoolean } from '~/lib/utils/common_utils';
+import axios from '../../lib/utils/axios_utils';
export default class PerformanceBarService {
static interceptor = null;
diff --git a/app/assets/javascripts/pipeline_editor/components/commit/commit_section.vue b/app/assets/javascripts/pipeline_editor/components/commit/commit_section.vue
new file mode 100644
index 00000000000..6e701f063a7
--- /dev/null
+++ b/app/assets/javascripts/pipeline_editor/components/commit/commit_section.vue
@@ -0,0 +1,114 @@
+<script>
+import { mergeUrlParams, redirectTo } from '~/lib/utils/url_utility';
+import { __, s__, sprintf } from '~/locale';
+import commitCIFile from '../../graphql/mutations/commit_ci_file.mutation.graphql';
+import getCommitSha from '../../graphql/queries/client/commit_sha.graphql';
+
+import { COMMIT_FAILURE, COMMIT_SUCCESS } from '../../constants';
+import CommitForm from './commit_form.vue';
+
+const MR_SOURCE_BRANCH = 'merge_request[source_branch]';
+const MR_TARGET_BRANCH = 'merge_request[target_branch]';
+
+export default {
+ alertTexts: {
+ [COMMIT_FAILURE]: s__('Pipelines|The GitLab CI configuration could not be updated.'),
+ [COMMIT_SUCCESS]: __('Your changes have been successfully committed.'),
+ },
+ i18n: {
+ defaultCommitMessage: __('Update %{sourcePath} file'),
+ },
+ components: {
+ CommitForm,
+ },
+ inject: ['projectFullPath', 'ciConfigPath', 'defaultBranch', 'newMergeRequestPath'],
+ props: {
+ ciFileContent: {
+ type: String,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ commit: {},
+ isSaving: false,
+ };
+ },
+ apollo: {
+ commitSha: {
+ query: getCommitSha,
+ },
+ },
+ computed: {
+ defaultCommitMessage() {
+ return sprintf(this.$options.i18n.defaultCommitMessage, { sourcePath: this.ciConfigPath });
+ },
+ },
+ methods: {
+ redirectToNewMergeRequest(sourceBranch) {
+ const url = mergeUrlParams(
+ {
+ [MR_SOURCE_BRANCH]: sourceBranch,
+ [MR_TARGET_BRANCH]: this.defaultBranch,
+ },
+ this.newMergeRequestPath,
+ );
+ redirectTo(url);
+ },
+ async onCommitSubmit({ message, branch, openMergeRequest }) {
+ this.isSaving = true;
+
+ try {
+ const {
+ data: {
+ commitCreate: { errors },
+ },
+ } = await this.$apollo.mutate({
+ mutation: commitCIFile,
+ variables: {
+ projectPath: this.projectFullPath,
+ branch,
+ startBranch: this.defaultBranch,
+ message,
+ filePath: this.ciConfigPath,
+ content: this.ciFileContent,
+ lastCommitId: this.commitSha,
+ },
+ update(store, { data }) {
+ const commitSha = data?.commitCreate?.commit?.sha;
+
+ if (commitSha) {
+ store.writeQuery({ query: getCommitSha, data: { commitSha } });
+ }
+ },
+ });
+
+ if (errors?.length) {
+ this.$emit('showError', { type: COMMIT_FAILURE, reasons: errors });
+ } else if (openMergeRequest) {
+ this.redirectToNewMergeRequest(branch);
+ } else {
+ this.$emit('commit', { type: COMMIT_SUCCESS });
+ }
+ } catch (error) {
+ this.$emit('showError', { type: COMMIT_FAILURE, reasons: [error?.message] });
+ } finally {
+ this.isSaving = false;
+ }
+ },
+ onCommitCancel() {
+ this.$emit('resetContent');
+ },
+ },
+};
+</script>
+
+<template>
+ <commit-form
+ :default-branch="defaultBranch"
+ :default-message="defaultCommitMessage"
+ :is-saving="isSaving"
+ @cancel="onCommitCancel"
+ @submit="onCommitSubmit"
+ />
+</template>
diff --git a/app/assets/javascripts/pipeline_editor/components/header/pipeline_editor_header.vue b/app/assets/javascripts/pipeline_editor/components/header/pipeline_editor_header.vue
new file mode 100644
index 00000000000..ab41c0170e9
--- /dev/null
+++ b/app/assets/javascripts/pipeline_editor/components/header/pipeline_editor_header.vue
@@ -0,0 +1,30 @@
+<script>
+import ValidationSegment from './validation_segment.vue';
+
+export default {
+ validationSegmentClasses:
+ 'gl-p-5 gl-bg-gray-10 gl-border-solid gl-border-1 gl-border-gray-100 gl-rounded-base',
+ components: {
+ ValidationSegment,
+ },
+ props: {
+ ciConfigData: {
+ type: Object,
+ required: true,
+ },
+ isCiConfigDataLoading: {
+ type: Boolean,
+ required: true,
+ },
+ },
+};
+</script>
+<template>
+ <div class="gl-mb-5">
+ <validation-segment
+ :class="$options.validationSegmentClasses"
+ :loading="isCiConfigDataLoading"
+ :ci-config="ciConfigData"
+ />
+ </div>
+</template>
diff --git a/app/assets/javascripts/pipeline_editor/components/info/validation_segment.vue b/app/assets/javascripts/pipeline_editor/components/header/validation_segment.vue
index 22f378c571a..94fb3a66fdd 100644
--- a/app/assets/javascripts/pipeline_editor/components/info/validation_segment.vue
+++ b/app/assets/javascripts/pipeline_editor/components/header/validation_segment.vue
@@ -1,8 +1,8 @@
<script>
import { GlIcon, GlLink, GlLoadingIcon } from '@gitlab/ui';
import { __, s__, sprintf } from '~/locale';
-import { CI_CONFIG_STATUS_VALID } from '../../constants';
import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate.vue';
+import { CI_CONFIG_STATUS_VALID } from '../../constants';
export const i18n = {
learnMore: __('Learn more'),
diff --git a/app/assets/javascripts/pipeline_editor/components/lint/ci_lint_results.vue b/app/assets/javascripts/pipeline_editor/components/lint/ci_lint_results.vue
index 58a96c3f725..23f8d3818ab 100644
--- a/app/assets/javascripts/pipeline_editor/components/lint/ci_lint_results.vue
+++ b/app/assets/javascripts/pipeline_editor/components/lint/ci_lint_results.vue
@@ -1,9 +1,9 @@
<script>
import { GlAlert, GlLink, GlSprintf, GlTable } from '@gitlab/ui';
+import { __ } from '~/locale';
import CiLintWarnings from './ci_lint_warnings.vue';
import CiLintResultsValue from './ci_lint_results_value.vue';
import CiLintResultsParam from './ci_lint_results_param.vue';
-import { __ } from '~/locale';
const thBorderColor = 'gl-border-gray-100!';
diff --git a/app/assets/javascripts/pipeline_editor/components/pipeline_editor_tabs.vue b/app/assets/javascripts/pipeline_editor/components/pipeline_editor_tabs.vue
new file mode 100644
index 00000000000..760a8b7e232
--- /dev/null
+++ b/app/assets/javascripts/pipeline_editor/components/pipeline_editor_tabs.vue
@@ -0,0 +1,62 @@
+<script>
+import { GlLoadingIcon, GlTabs, GlTab } from '@gitlab/ui';
+import { s__ } from '~/locale';
+import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
+import PipelineGraph from '~/pipelines/components/pipeline_graph/pipeline_graph.vue';
+import CiLint from './lint/ci_lint.vue';
+import EditorTab from './ui/editor_tab.vue';
+import TextEditor from './text_editor.vue';
+
+export default {
+ i18n: {
+ tabEdit: s__('Pipelines|Write pipeline configuration'),
+ tabGraph: s__('Pipelines|Visualize'),
+ tabLint: s__('Pipelines|Lint'),
+ },
+ components: {
+ CiLint,
+ EditorTab,
+ GlLoadingIcon,
+ GlTab,
+ GlTabs,
+ PipelineGraph,
+ TextEditor,
+ },
+ mixins: [glFeatureFlagsMixin()],
+ props: {
+ ciConfigData: {
+ type: Object,
+ required: true,
+ },
+ ciFileContent: {
+ type: String,
+ required: true,
+ },
+ isCiConfigDataLoading: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ },
+};
+</script>
+<template>
+ <gl-tabs class="file-editor gl-mb-3">
+ <editor-tab :title="$options.i18n.tabEdit" lazy data-testid="editor-tab">
+ <text-editor :value="ciFileContent" v-on="$listeners" />
+ </editor-tab>
+ <gl-tab
+ v-if="glFeatures.ciConfigVisualizationTab"
+ :title="$options.i18n.tabGraph"
+ lazy
+ data-testid="visualization-tab"
+ >
+ <gl-loading-icon v-if="isCiConfigDataLoading" size="lg" class="gl-m-3" />
+ <pipeline-graph v-else :pipeline-data="ciConfigData" />
+ </gl-tab>
+ <editor-tab :title="$options.i18n.tabLint" data-testid="lint-tab">
+ <gl-loading-icon v-if="isCiConfigDataLoading" size="lg" class="gl-m-3" />
+ <ci-lint v-else :ci-config="ciConfigData" />
+ </editor-tab>
+ </gl-tabs>
+</template>
diff --git a/app/assets/javascripts/pipeline_editor/components/text_editor.vue b/app/assets/javascripts/pipeline_editor/components/text_editor.vue
index b8d49d77ea9..4e9ba47727d 100644
--- a/app/assets/javascripts/pipeline_editor/components/text_editor.vue
+++ b/app/assets/javascripts/pipeline_editor/components/text_editor.vue
@@ -1,26 +1,30 @@
<script>
import EditorLite from '~/vue_shared/components/editor_lite.vue';
import { CiSchemaExtension } from '~/editor/extensions/editor_ci_schema_ext';
+import { EDITOR_READY_EVENT } from '~/editor/constants';
+import getCommitSha from '../graphql/queries/client/commit_sha.graphql';
export default {
components: {
EditorLite,
},
- inject: ['projectPath', 'projectNamespace'],
+ inject: ['ciConfigPath', 'projectPath', 'projectNamespace'],
inheritAttrs: false,
- props: {
- ciConfigPath: {
- type: String,
- required: true,
- },
+ data() {
+ return {
+ commitSha: '',
+ };
+ },
+ apollo: {
commitSha: {
- type: String,
- required: false,
- default: null,
+ query: getCommitSha,
},
},
methods: {
- onEditorReady() {
+ onCiConfigUpdate(content) {
+ this.$emit('updateCiConfig', content);
+ },
+ registerCiSchema() {
const editorInstance = this.$refs.editor.getEditor();
editorInstance.use(new CiSchemaExtension());
@@ -31,6 +35,7 @@ export default {
});
},
},
+ readyEvent: EDITOR_READY_EVENT,
};
</script>
<template>
@@ -39,7 +44,8 @@ export default {
ref="editor"
:file-name="ciConfigPath"
v-bind="$attrs"
- @editor-ready="onEditorReady"
+ @[$options.readyEvent]="registerCiSchema"
+ @input="onCiConfigUpdate"
v-on="$listeners"
/>
</div>
diff --git a/app/assets/javascripts/pipeline_editor/components/ui/confirm_unsaved_changes_dialog.vue b/app/assets/javascripts/pipeline_editor/components/ui/confirm_unsaved_changes_dialog.vue
new file mode 100644
index 00000000000..bc076fbe349
--- /dev/null
+++ b/app/assets/javascripts/pipeline_editor/components/ui/confirm_unsaved_changes_dialog.vue
@@ -0,0 +1,26 @@
+<script>
+export default {
+ props: {
+ hasUnsavedChanges: {
+ type: Boolean,
+ required: true,
+ },
+ },
+ created() {
+ window.addEventListener('beforeunload', this.confirmChanges);
+ },
+ destroyed() {
+ window.removeEventListener('beforeunload', this.confirmChanges);
+ },
+ methods: {
+ confirmChanges(e = {}) {
+ if (this.hasUnsavedChanges) {
+ e.preventDefault();
+ // eslint-disable-next-line no-param-reassign
+ e.returnValue = ''; // Chrome requires returnValue to be set
+ }
+ },
+ },
+ render: () => null,
+};
+</script>
diff --git a/app/assets/javascripts/pipeline_editor/constants.js b/app/assets/javascripts/pipeline_editor/constants.js
index 70bab8092c0..f15558363bf 100644
--- a/app/assets/javascripts/pipeline_editor/constants.js
+++ b/app/assets/javascripts/pipeline_editor/constants.js
@@ -1,2 +1,9 @@
export const CI_CONFIG_STATUS_VALID = 'VALID';
export const CI_CONFIG_STATUS_INVALID = 'INVALID';
+
+export const COMMIT_FAILURE = 'COMMIT_FAILURE';
+export const COMMIT_SUCCESS = 'COMMIT_SUCCESS';
+
+export const DEFAULT_FAILURE = 'DEFAULT_FAILURE';
+export const LOAD_FAILURE_NO_FILE = 'LOAD_FAILURE_NO_FILE';
+export const LOAD_FAILURE_UNKNOWN = 'LOAD_FAILURE_UNKNOWN';
diff --git a/app/assets/javascripts/pipeline_editor/graphql/mutations/commit_ci_file.mutation.graphql b/app/assets/javascripts/pipeline_editor/graphql/mutations/commit_ci_file.mutation.graphql
index 0c58749a8b2..2af0cd5f6d4 100644
--- a/app/assets/javascripts/pipeline_editor/graphql/mutations/commit_ci_file.mutation.graphql
+++ b/app/assets/javascripts/pipeline_editor/graphql/mutations/commit_ci_file.mutation.graphql
@@ -1,4 +1,4 @@
-mutation commitCIFileMutation(
+mutation commitCIFile(
$projectPath: ID!
$branch: String!
$startBranch: String
diff --git a/app/assets/javascripts/pipeline_editor/graphql/queries/client/commit_sha.graphql b/app/assets/javascripts/pipeline_editor/graphql/queries/client/commit_sha.graphql
new file mode 100644
index 00000000000..6c7635887ec
--- /dev/null
+++ b/app/assets/javascripts/pipeline_editor/graphql/queries/client/commit_sha.graphql
@@ -0,0 +1,3 @@
+query getCommitSha {
+ commitSha @client
+}
diff --git a/app/assets/javascripts/pipeline_editor/index.js b/app/assets/javascripts/pipeline_editor/index.js
index 583ba555080..003feb16281 100644
--- a/app/assets/javascripts/pipeline_editor/index.js
+++ b/app/assets/javascripts/pipeline_editor/index.js
@@ -15,14 +15,13 @@ export const initPipelineEditor = (selector = '#js-pipeline-editor') => {
}
const {
- // props
- ciConfigPath,
+ // Add to apollo cache as it can be updated by future queries
commitSha,
+ // Add to provide/inject API for static values
+ ciConfigPath,
defaultBranch,
- newMergeRequestPath,
-
- // `provide/inject` data
lintHelpPagePath,
+ newMergeRequestPath,
projectFullPath,
projectPath,
projectNamespace,
@@ -35,25 +34,27 @@ export const initPipelineEditor = (selector = '#js-pipeline-editor') => {
defaultClient: createDefaultClient(resolvers, { typeDefs }),
});
+ apolloProvider.clients.defaultClient.cache.writeData({
+ data: {
+ commitSha,
+ },
+ });
+
return new Vue({
el,
apolloProvider,
provide: {
+ ciConfigPath,
+ defaultBranch,
lintHelpPagePath,
+ newMergeRequestPath,
projectFullPath,
projectPath,
projectNamespace,
ymlHelpPagePath,
},
render(h) {
- return h(PipelineEditorApp, {
- props: {
- ciConfigPath,
- commitSha,
- defaultBranch,
- newMergeRequestPath,
- },
- });
+ return h(PipelineEditorApp);
},
});
};
diff --git a/app/assets/javascripts/pipeline_editor/pipeline_editor_app.vue b/app/assets/javascripts/pipeline_editor/pipeline_editor_app.vue
index 21993e2120a..266439c273a 100644
--- a/app/assets/javascripts/pipeline_editor/pipeline_editor_app.vue
+++ b/app/assets/javascripts/pipeline_editor/pipeline_editor_app.vue
@@ -1,84 +1,55 @@
<script>
-import { GlAlert, GlLoadingIcon, GlTabs, GlTab } from '@gitlab/ui';
+import { GlAlert, GlLoadingIcon } from '@gitlab/ui';
import { __, s__, sprintf } from '~/locale';
-import { mergeUrlParams, redirectTo } from '~/lib/utils/url_utility';
-import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import httpStatusCodes from '~/lib/utils/http_status';
-import PipelineGraph from '~/pipelines/components/pipeline_graph/pipeline_graph.vue';
-import CiLint from './components/lint/ci_lint.vue';
-import CommitForm from './components/commit/commit_form.vue';
-import EditorTab from './components/ui/editor_tab.vue';
-import TextEditor from './components/text_editor.vue';
-import ValidationSegment from './components/info/validation_segment.vue';
-
-import commitCiFileMutation from './graphql/mutations/commit_ci_file.mutation.graphql';
+import { unwrapStagesWithNeeds } from '~/pipelines/components/unwrapping_utils';
+import ConfirmUnsavedChangesDialog from './components/ui/confirm_unsaved_changes_dialog.vue';
+import {
+ COMMIT_FAILURE,
+ COMMIT_SUCCESS,
+ DEFAULT_FAILURE,
+ LOAD_FAILURE_NO_FILE,
+ LOAD_FAILURE_UNKNOWN,
+} from './constants';
import getBlobContent from './graphql/queries/blob_content.graphql';
import getCiConfigData from './graphql/queries/ci_config.graphql';
-import { unwrapStagesWithNeeds } from '~/pipelines/components/unwrapping_utils';
-
-const MR_SOURCE_BRANCH = 'merge_request[source_branch]';
-const MR_TARGET_BRANCH = 'merge_request[target_branch]';
-
-const COMMIT_FAILURE = 'COMMIT_FAILURE';
-const COMMIT_SUCCESS = 'COMMIT_SUCCESS';
-const DEFAULT_FAILURE = 'DEFAULT_FAILURE';
-const LOAD_FAILURE_NO_FILE = 'LOAD_FAILURE_NO_FILE';
-const LOAD_FAILURE_UNKNOWN = 'LOAD_FAILURE_UNKNOWN';
+import PipelineEditorHome from './pipeline_editor_home.vue';
export default {
components: {
- CiLint,
- CommitForm,
- EditorTab,
+ ConfirmUnsavedChangesDialog,
GlAlert,
GlLoadingIcon,
- GlTabs,
- GlTab,
- PipelineGraph,
- TextEditor,
- ValidationSegment,
+ PipelineEditorHome,
},
- mixins: [glFeatureFlagsMixin()],
- inject: ['projectFullPath'],
- props: {
- defaultBranch: {
- type: String,
- required: false,
- default: null,
+ inject: {
+ ciConfigPath: {
+ default: '',
},
- commitSha: {
- type: String,
- required: false,
+ defaultBranch: {
default: null,
},
- ciConfigPath: {
- type: String,
- required: true,
- },
- newMergeRequestPath: {
- type: String,
- required: true,
+ projectFullPath: {
+ default: '',
},
},
data() {
return {
ciConfigData: {},
- content: '',
- contentModel: '',
- lastCommitSha: this.commitSha,
- isSaving: false,
-
// Success and failure state
failureType: null,
- showFailureAlert: false,
failureReasons: [],
- successType: null,
+ initialCiFileContent: '',
+ lastCommittedContent: '',
+ currentCiFileContent: '',
+ showFailureAlert: false,
showSuccessAlert: false,
+ successType: null,
};
},
apollo: {
- content: {
+ initialCiFileContent: {
query: getBlobContent,
variables() {
return {
@@ -91,7 +62,10 @@ export default {
return data?.blobContent?.rawData;
},
result({ data }) {
- this.contentModel = data?.blobContent?.rawData ?? '';
+ const fileContent = data?.blobContent?.rawData ?? '';
+
+ this.lastCommittedContent = fileContent;
+ this.currentCiFileContent = fileContent;
},
error(error) {
this.handleBlobContentError(error);
@@ -100,13 +74,13 @@ export default {
ciConfigData: {
query: getCiConfigData,
// If content is not loaded, we can't lint the data
- skip: ({ contentModel }) => {
- return !contentModel;
+ skip: ({ currentCiFileContent }) => {
+ return !currentCiFileContent;
},
variables() {
return {
projectPath: this.projectFullPath,
- content: this.contentModel,
+ content: this.currentCiFileContent,
};
},
update(data) {
@@ -122,8 +96,11 @@ export default {
},
},
computed: {
+ hasUnsavedChanges() {
+ return this.lastCommittedContent !== this.currentCiFileContent;
+ },
isBlobContentLoading() {
- return this.$apollo.queries.content.loading;
+ return this.$apollo.queries.initialCiFileContent.loading;
},
isBlobContentError() {
return this.failureType === LOAD_FAILURE_NO_FILE;
@@ -131,62 +108,60 @@ export default {
isCiConfigDataLoading() {
return this.$apollo.queries.ciConfigData.loading;
},
- defaultCommitMessage() {
- return sprintf(this.$options.i18n.defaultCommitMessage, { sourcePath: this.ciConfigPath });
- },
- success() {
- switch (this.successType) {
- case COMMIT_SUCCESS:
- return {
- text: this.$options.alertTexts[COMMIT_SUCCESS],
- variant: 'info',
- };
- default:
- return null;
- }
- },
failure() {
switch (this.failureType) {
case LOAD_FAILURE_NO_FILE:
return {
- text: sprintf(this.$options.alertTexts[LOAD_FAILURE_NO_FILE], {
+ text: sprintf(this.$options.errorTexts[LOAD_FAILURE_NO_FILE], {
filePath: this.ciConfigPath,
}),
variant: 'danger',
};
case LOAD_FAILURE_UNKNOWN:
return {
- text: this.$options.alertTexts[LOAD_FAILURE_UNKNOWN],
+ text: this.$options.errorTexts[LOAD_FAILURE_UNKNOWN],
variant: 'danger',
};
case COMMIT_FAILURE:
return {
- text: this.$options.alertTexts[COMMIT_FAILURE],
+ text: this.$options.errorTexts[COMMIT_FAILURE],
variant: 'danger',
};
default:
return {
- text: this.$options.alertTexts[DEFAULT_FAILURE],
+ text: this.$options.errorTexts[DEFAULT_FAILURE],
variant: 'danger',
};
}
},
+ success() {
+ switch (this.successType) {
+ case COMMIT_SUCCESS:
+ return {
+ text: this.$options.successTexts[COMMIT_SUCCESS],
+ variant: 'info',
+ };
+ default:
+ return null;
+ }
+ },
},
i18n: {
- defaultCommitMessage: __('Update %{sourcePath} file'),
tabEdit: s__('Pipelines|Write pipeline configuration'),
tabGraph: s__('Pipelines|Visualize'),
tabLint: s__('Pipelines|Lint'),
},
- alertTexts: {
+ errorTexts: {
[COMMIT_FAILURE]: s__('Pipelines|The GitLab CI configuration could not be updated.'),
- [COMMIT_SUCCESS]: __('Your changes have been successfully committed.'),
[DEFAULT_FAILURE]: __('Something went wrong on our end.'),
[LOAD_FAILURE_NO_FILE]: s__(
'Pipelines|There is no %{filePath} file in this repository, please add one and visit the Pipeline Editor again.',
),
[LOAD_FAILURE_UNKNOWN]: s__('Pipelines|The CI configuration was not loaded, please try again.'),
},
+ successTexts: {
+ [COMMIT_SUCCESS]: __('Your changes have been successfully committed.'),
+ },
methods: {
handleBlobContentError(error = {}) {
const { networkError } = error;
@@ -207,72 +182,32 @@ export default {
dismissFailure() {
this.showFailureAlert = false;
},
+ dismissSuccess() {
+ this.showSuccessAlert = false;
+ },
reportFailure(type, reasons = []) {
this.showFailureAlert = true;
this.failureType = type;
this.failureReasons = reasons;
},
- dismissSuccess() {
- this.showSuccessAlert = false;
- },
reportSuccess(type) {
this.showSuccessAlert = true;
this.successType = type;
},
-
- redirectToNewMergeRequest(sourceBranch) {
- const url = mergeUrlParams(
- {
- [MR_SOURCE_BRANCH]: sourceBranch,
- [MR_TARGET_BRANCH]: this.defaultBranch,
- },
- this.newMergeRequestPath,
- );
- redirectTo(url);
+ resetContent() {
+ this.currentCiFileContent = this.lastCommittedContent;
},
- async onCommitSubmit(event) {
- this.isSaving = true;
- const { message, branch, openMergeRequest } = event;
-
- try {
- const {
- data: {
- commitCreate: { errors, commit },
- },
- } = await this.$apollo.mutate({
- mutation: commitCiFileMutation,
- variables: {
- projectPath: this.projectFullPath,
- branch,
- startBranch: this.defaultBranch,
- message,
- filePath: this.ciConfigPath,
- content: this.contentModel,
- lastCommitId: this.lastCommitSha,
- },
- });
-
- if (errors?.length) {
- this.reportFailure(COMMIT_FAILURE, errors);
- return;
- }
-
- if (openMergeRequest) {
- this.redirectToNewMergeRequest(branch);
- } else {
- this.reportSuccess(COMMIT_SUCCESS);
-
- // Update latest commit
- this.lastCommitSha = commit.sha;
- }
- } catch (error) {
- this.reportFailure(COMMIT_FAILURE, [error?.message]);
- } finally {
- this.isSaving = false;
- }
+ showErrorAlert({ type, reasons = [] }) {
+ this.reportFailure(type, reasons);
},
- onCommitCancel() {
- this.contentModel = this.content;
+ updateCiConfig(ciFileContent) {
+ this.currentCiFileContent = ciFileContent;
+ },
+ updateOnCommit({ type }) {
+ this.reportSuccess(type);
+ // Keep track of the latest commited content to know
+ // if the user has made changes to the file that are unsaved.
+ this.lastCommittedContent = this.currentCiFileContent;
},
},
};
@@ -280,20 +215,10 @@ export default {
<template>
<div class="gl-mt-4">
- <gl-alert
- v-if="showSuccessAlert"
- :variant="success.variant"
- :dismissible="true"
- @dismiss="dismissSuccess"
- >
+ <gl-alert v-if="showSuccessAlert" :variant="success.variant" @dismiss="dismissSuccess">
{{ success.text }}
</gl-alert>
- <gl-alert
- v-if="showFailureAlert"
- :variant="failure.variant"
- :dismissible="true"
- @dismiss="dismissFailure"
- >
+ <gl-alert v-if="showFailureAlert" :variant="failure.variant" @dismiss="dismissFailure">
{{ failure.text }}
<ul v-if="failureReasons.length" class="gl-mb-0">
<li v-for="reason in failureReasons" :key="reason">{{ reason }}</li>
@@ -301,46 +226,16 @@ export default {
</gl-alert>
<gl-loading-icon v-if="isBlobContentLoading" size="lg" class="gl-m-3" />
<div v-else-if="!isBlobContentError" class="gl-mt-4">
- <div class="file-editor gl-mb-3">
- <div class="info-well gl-display-none gl-display-sm-block">
- <validation-segment
- class="well-segment"
- :loading="isCiConfigDataLoading"
- :ci-config="ciConfigData"
- />
- </div>
-
- <gl-tabs>
- <editor-tab :lazy="true" :title="$options.i18n.tabEdit">
- <text-editor
- v-model="contentModel"
- :ci-config-path="ciConfigPath"
- :commit-sha="lastCommitSha"
- />
- </editor-tab>
- <gl-tab
- v-if="glFeatures.ciConfigVisualizationTab"
- :lazy="true"
- :title="$options.i18n.tabGraph"
- data-testid="visualization-tab"
- >
- <gl-loading-icon v-if="isCiConfigDataLoading" size="lg" class="gl-m-3" />
- <pipeline-graph v-else :pipeline-data="ciConfigData" />
- </gl-tab>
-
- <editor-tab :title="$options.i18n.tabLint">
- <gl-loading-icon v-if="isCiConfigDataLoading" size="lg" class="gl-m-3" />
- <ci-lint v-else :ci-config="ciConfigData" />
- </editor-tab>
- </gl-tabs>
- </div>
- <commit-form
- :default-branch="defaultBranch"
- :default-message="defaultCommitMessage"
- :is-saving="isSaving"
- @cancel="onCommitCancel"
- @submit="onCommitSubmit"
+ <pipeline-editor-home
+ :is-ci-config-data-loading="isCiConfigDataLoading"
+ :ci-config-data="ciConfigData"
+ :ci-file-content="currentCiFileContent"
+ @commit="updateOnCommit"
+ @resetContent="resetContent"
+ @showError="showErrorAlert"
+ @updateCiConfig="updateCiConfig"
/>
</div>
+ <confirm-unsaved-changes-dialog :has-unsaved-changes="hasUnsavedChanges" />
</div>
</template>
diff --git a/app/assets/javascripts/pipeline_editor/pipeline_editor_home.vue b/app/assets/javascripts/pipeline_editor/pipeline_editor_home.vue
new file mode 100644
index 00000000000..b7535cc0964
--- /dev/null
+++ b/app/assets/javascripts/pipeline_editor/pipeline_editor_home.vue
@@ -0,0 +1,43 @@
+<script>
+import CommitSection from './components/commit/commit_section.vue';
+import PipelineEditorTabs from './components/pipeline_editor_tabs.vue';
+import PipelineEditorHeader from './components/header/pipeline_editor_header.vue';
+
+export default {
+ components: {
+ CommitSection,
+ PipelineEditorHeader,
+ PipelineEditorTabs,
+ },
+ props: {
+ ciConfigData: {
+ type: Object,
+ required: true,
+ },
+ ciFileContent: {
+ type: String,
+ required: true,
+ },
+ isCiConfigDataLoading: {
+ type: Boolean,
+ required: true,
+ },
+ },
+};
+</script>
+
+<template>
+ <div>
+ <pipeline-editor-header
+ :ci-config-data="ciConfigData"
+ :is-ci-config-data-loading="isCiConfigDataLoading"
+ />
+ <pipeline-editor-tabs
+ :ci-config-data="ciConfigData"
+ :ci-file-content="ciFileContent"
+ :is-ci-config-data-loading="isCiConfigDataLoading"
+ v-on="$listeners"
+ />
+ <commit-section :ci-file-content="ciFileContent" v-on="$listeners" />
+ </div>
+</template>
diff --git a/app/assets/javascripts/pipeline_new/components/pipeline_new_form.vue b/app/assets/javascripts/pipeline_new/components/pipeline_new_form.vue
index 70c5713b216..69b02463235 100644
--- a/app/assets/javascripts/pipeline_new/components/pipeline_new_form.vue
+++ b/app/assets/javascripts/pipeline_new/components/pipeline_new_form.vue
@@ -22,9 +22,9 @@ import * as Sentry from '~/sentry/wrapper';
import { s__, __, n__ } from '~/locale';
import axios from '~/lib/utils/axios_utils';
import { redirectTo } from '~/lib/utils/url_utility';
-import { VARIABLE_TYPE, FILE_TYPE, CONFIG_VARIABLES_TIMEOUT } from '../constants';
import { backOff } from '~/lib/utils/common_utils';
import httpStatusCodes from '~/lib/utils/http_status';
+import { VARIABLE_TYPE, FILE_TYPE, CONFIG_VARIABLES_TIMEOUT } from '../constants';
export default {
typeOptions: [
@@ -116,6 +116,7 @@ export default {
totalWarnings: 0,
isWarningDismissed: false,
isLoading: false,
+ submitted: false,
};
},
computed: {
@@ -251,10 +252,6 @@ export default {
return index < this.variables.length - 1;
},
fetchConfigVariables(refValue) {
- if (!gon?.features?.newPipelineFormPrefilledVars) {
- return Promise.resolve({ params: {}, descriptions: {} });
- }
-
this.isLoading = true;
return backOff((next, stop) => {
@@ -298,6 +295,7 @@ export default {
});
},
createPipeline() {
+ this.submitted = true;
const filteredVariables = this.variables
.filter(({ key, value }) => key !== '' && value !== '')
.map(({ variable_type, key, value }) => ({
@@ -317,8 +315,16 @@ export default {
redirectTo(`${this.pipelinesPath}/${data.id}`);
})
.catch((err) => {
- const { errors, warnings, total_warnings: totalWarnings } = err.response.data;
+ // always re-enable submit button
+ this.submitted = false;
+
+ const {
+ errors = [],
+ warnings = [],
+ total_warnings: totalWarnings = 0,
+ } = err?.response?.data;
const [error] = errors;
+
this.error = error;
this.warnings = warnings;
this.totalWarnings = totalWarnings;
@@ -436,12 +442,12 @@ export default {
category="secondary"
@click="removeVariable(index)"
>
- <gl-icon class="gl-mr-0! gl-display-none gl-display-md-block" name="clear" />
- <span class="gl-display-md-none">{{ s__('CiVariables|Remove variable') }}</span>
+ <gl-icon class="gl-mr-0! gl-display-none gl-md-display-block" name="clear" />
+ <span class="gl-md-display-none">{{ s__('CiVariables|Remove variable') }}</span>
</gl-button>
<gl-button
v-else
- class="gl-md-ml-3 gl-mb-3 gl-display-none gl-display-md-block gl-visibility-hidden"
+ class="gl-md-ml-3 gl-mb-3 gl-display-none gl-md-display-block gl-visibility-hidden"
icon="clear"
/>
</template>
@@ -468,6 +474,8 @@ export default {
variant="success"
class="js-no-auto-disable"
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>
diff --git a/app/assets/javascripts/pipelines/components/dag/dag.vue b/app/assets/javascripts/pipelines/components/dag/dag.vue
index 2482af2c7f0..23e7ce349cf 100644
--- a/app/assets/javascripts/pipelines/components/dag/dag.vue
+++ b/app/assets/javascripts/pipelines/components/dag/dag.vue
@@ -4,11 +4,11 @@ import { isEmpty } from 'lodash';
import { __ } from '~/locale';
import { fetchPolicies } from '~/lib/graphql';
import getDagVisData from '../../graphql/queries/get_dag_vis_data.query.graphql';
+import { parseData } from '../parsing_utils';
+import { DEFAULT, PARSE_FAILURE, LOAD_FAILURE, UNSUPPORTED_DATA } from '../../constants';
import DagGraph from './dag_graph.vue';
import DagAnnotations from './dag_annotations.vue';
import { ADD_NOTE, REMOVE_NOTE, REPLACE_NOTES } from './constants';
-import { parseData } from '../parsing_utils';
-import { DEFAULT, PARSE_FAILURE, LOAD_FAILURE, UNSUPPORTED_DATA } from '../../constants';
export default {
// eslint-disable-next-line @gitlab/require-i18n-strings
diff --git a/app/assets/javascripts/pipelines/components/dag/dag_graph.vue b/app/assets/javascripts/pipelines/components/dag/dag_graph.vue
index 5ba0604fa01..76ccbd74bb6 100644
--- a/app/assets/javascripts/pipelines/components/dag/dag_graph.vue
+++ b/app/assets/javascripts/pipelines/components/dag/dag_graph.vue
@@ -1,6 +1,8 @@
<script>
import * as d3 from 'd3';
import { uniqueId } from 'lodash';
+import { getMaxNodes, removeOrphanNodes } from '../parsing_utils';
+import { PARSE_FAILURE } from '../../constants';
import { LINK_SELECTOR, NODE_SELECTOR, ADD_NOTE, REMOVE_NOTE, REPLACE_NOTES } from './constants';
import {
currentIsLive,
@@ -10,9 +12,7 @@ import {
toggleLinkHighlight,
togglePathHighlights,
} from './interactions';
-import { getMaxNodes, removeOrphanNodes } from '../parsing_utils';
import { calculateClip, createLinkPath, createSankey, labelPosition } from './drawing_utils';
-import { PARSE_FAILURE } from '../../constants';
export default {
viewOptions: {
diff --git a/app/assets/javascripts/pipelines/components/graph/action_component.vue b/app/assets/javascripts/pipelines/components/graph/action_component.vue
index 0ce94d4f02f..949d0d30297 100644
--- a/app/assets/javascripts/pipelines/components/graph/action_component.vue
+++ b/app/assets/javascripts/pipelines/components/graph/action_component.vue
@@ -3,6 +3,7 @@ import { GlTooltipDirective, GlButton, GlLoadingIcon, GlIcon } from '@gitlab/ui'
import axios from '~/lib/utils/axios_utils';
import { dasherize } from '~/lib/utils/text_utility';
import { __ } from '~/locale';
+import { BV_HIDE_TOOLTIP } from '~/lib/utils/constants';
import { deprecatedCreateFlash as createFlash } from '~/flash';
import { reportToSentry } from './utils';
@@ -62,7 +63,7 @@ export default {
*
*/
onClickAction() {
- this.$root.$emit('bv::hide::tooltip', `js-ci-action-${this.link}`);
+ this.$root.$emit(BV_HIDE_TOOLTIP, `js-ci-action-${this.link}`);
this.isDisabled = true;
this.isLoading = true;
diff --git a/app/assets/javascripts/pipelines/components/graph/constants.js b/app/assets/javascripts/pipelines/components/graph/constants.js
index 6f0deccfef6..caa269f5095 100644
--- a/app/assets/javascripts/pipelines/components/graph/constants.js
+++ b/app/assets/javascripts/pipelines/components/graph/constants.js
@@ -2,5 +2,11 @@ export const DOWNSTREAM = 'downstream';
export const MAIN = 'main';
export const UPSTREAM = 'upstream';
+/*
+ this value is based on the gl-pipeline-job-width class
+ plus some extra for the margins
+*/
+export const ONE_COL_WIDTH = 180;
+
export const REST = 'rest';
export const GRAPHQL = 'graphql';
diff --git a/app/assets/javascripts/pipelines/components/graph/graph_component.vue b/app/assets/javascripts/pipelines/components/graph/graph_component.vue
index cd403757fe6..cae26e6ee3f 100644
--- a/app/assets/javascripts/pipelines/components/graph/graph_component.vue
+++ b/app/assets/javascripts/pipelines/components/graph/graph_component.vue
@@ -3,7 +3,7 @@ import LinkedGraphWrapper from '../graph_shared/linked_graph_wrapper.vue';
import LinksLayer from '../graph_shared/links_layer.vue';
import LinkedPipelinesColumn from './linked_pipelines_column.vue';
import StageColumnComponent from './stage_column_component.vue';
-import { DOWNSTREAM, MAIN, UPSTREAM } from './constants';
+import { DOWNSTREAM, MAIN, UPSTREAM, ONE_COL_WIDTH } from './constants';
import { reportToSentry } from './utils';
export default {
@@ -86,11 +86,11 @@ export default {
reportToSentry(this.$options.name, `error: ${err}, info: ${info}`);
},
mounted() {
- this.measurements = this.getMeasurements();
+ this.getMeasurements();
},
methods: {
getMeasurements() {
- return {
+ this.measurements = {
width: this.$refs[this.containerId].scrollWidth,
height: this.$refs[this.containerId].scrollHeight,
};
@@ -101,6 +101,13 @@ export default {
setJob(jobName) {
this.hoveredJobName = jobName;
},
+ slidePipelineContainer() {
+ this.$refs.mainPipelineContainer.scrollBy({
+ left: ONE_COL_WIDTH,
+ top: 0,
+ behavior: 'smooth',
+ });
+ },
togglePipelineExpanded(jobName, expanded) {
this.pipelineExpanded = {
expanded,
@@ -116,8 +123,9 @@ export default {
<template>
<div class="js-pipeline-graph">
<div
- class="gl-display-flex gl-position-relative gl-overflow-auto gl-bg-gray-10 gl-white-space-nowrap"
- :class="{ 'gl-pipeline-min-h gl-py-5': !isLinkedPipeline }"
+ ref="mainPipelineContainer"
+ class="gl-display-flex gl-position-relative gl-bg-gray-10 gl-white-space-nowrap"
+ :class="{ 'gl-pipeline-min-h gl-py-5 gl-overflow-auto': !isLinkedPipeline }"
>
<linked-graph-wrapper>
<template #upstream>
@@ -153,6 +161,7 @@ export default {
:pipeline-id="pipeline.id"
@refreshPipelineGraph="$emit('refreshPipelineGraph')"
@jobHover="setJob"
+ @updateMeasurements="getMeasurements"
/>
</links-layer>
</div>
@@ -160,11 +169,13 @@ export default {
<template #downstream>
<linked-pipelines-column
v-if="showDownstreamPipelines"
+ class="gl-mr-6"
:linked-pipelines="downstreamPipelines"
:column-title="__('Downstream')"
:type="$options.pipelineTypeConstants.DOWNSTREAM"
@downstreamHovered="setJob"
@pipelineExpandToggle="togglePipelineExpanded"
+ @scrollContainer="slidePipelineContainer"
@error="onError"
/>
</template>
diff --git a/app/assets/javascripts/pipelines/components/graph/graph_component_legacy.vue b/app/assets/javascripts/pipelines/components/graph/graph_component_legacy.vue
index 2164dbf4d55..818985a74d1 100644
--- a/app/assets/javascripts/pipelines/components/graph/graph_component_legacy.vue
+++ b/app/assets/javascripts/pipelines/components/graph/graph_component_legacy.vue
@@ -1,9 +1,9 @@
<script>
import { escape, capitalize } from 'lodash';
import { GlLoadingIcon } from '@gitlab/ui';
+import GraphBundleMixin from '../../mixins/graph_pipeline_bundle_mixin';
import StageColumnComponentLegacy from './stage_column_component_legacy.vue';
import LinkedPipelinesColumnLegacy from './linked_pipelines_column_legacy.vue';
-import GraphBundleMixin from '../../mixins/graph_pipeline_bundle_mixin';
import { UPSTREAM, DOWNSTREAM, MAIN } from './constants';
import { reportToSentry } from './utils';
diff --git a/app/assets/javascripts/pipelines/components/graph/job_item.vue b/app/assets/javascripts/pipelines/components/graph/job_item.vue
index 8262d728a24..52848fe3ade 100644
--- a/app/assets/javascripts/pipelines/components/graph/job_item.vue
+++ b/app/assets/javascripts/pipelines/components/graph/job_item.vue
@@ -1,9 +1,10 @@
<script>
import { GlTooltipDirective, GlLink } from '@gitlab/ui';
-import ActionComponent from './action_component.vue';
-import JobNameComponent from './job_name_component.vue';
import { sprintf } from '~/locale';
import delayedJobMixin from '~/jobs/mixins/delayed_job_mixin';
+import { BV_HIDE_TOOLTIP } from '~/lib/utils/constants';
+import ActionComponent from './action_component.vue';
+import JobNameComponent from './job_name_component.vue';
import { accessValue } from './accessors';
import { REST } from './constants';
import { reportToSentry } from './utils';
@@ -144,7 +145,7 @@ export default {
},
methods: {
hideTooltips() {
- this.$root.$emit('bv::hide::tooltip');
+ this.$root.$emit(BV_HIDE_TOOLTIP);
},
pipelineActionRequestComplete() {
this.$emit('pipelineActionRequestComplete');
diff --git a/app/assets/javascripts/pipelines/components/graph/linked_pipeline.vue b/app/assets/javascripts/pipelines/components/graph/linked_pipeline.vue
index d18e604f087..22f9fb72159 100644
--- a/app/assets/javascripts/pipelines/components/graph/linked_pipeline.vue
+++ b/app/assets/javascripts/pipelines/components/graph/linked_pipeline.vue
@@ -2,6 +2,7 @@
import { GlTooltipDirective, GlButton, GlLink, GlLoadingIcon, GlBadge } from '@gitlab/ui';
import CiStatus from '~/vue_shared/components/ci_icon.vue';
import { __, sprintf } from '~/locale';
+import { BV_HIDE_TOOLTIP } from '~/lib/utils/constants';
import { accessValue } from './accessors';
import { DOWNSTREAM, REST, UPSTREAM } from './constants';
import { reportToSentry } from './utils';
@@ -126,7 +127,7 @@ export default {
this.$emit('pipelineExpandToggle', this.sourceJobName, !this.expanded);
},
hideTooltips() {
- this.$root.$emit('bv::hide::tooltip');
+ this.$root.$emit(BV_HIDE_TOOLTIP);
},
onDownstreamHovered() {
this.$emit('downstreamHovered', this.sourceJobName);
diff --git a/app/assets/javascripts/pipelines/components/graph/linked_pipelines_column.vue b/app/assets/javascripts/pipelines/components/graph/linked_pipelines_column.vue
index 40e6a01b88c..079e938ddd4 100644
--- a/app/assets/javascripts/pipelines/components/graph/linked_pipelines_column.vue
+++ b/app/assets/javascripts/pipelines/components/graph/linked_pipelines_column.vue
@@ -1,8 +1,8 @@
<script>
import getPipelineDetails from 'shared_queries/pipelines/get_pipeline_details.query.graphql';
-import LinkedPipeline from './linked_pipeline.vue';
import { LOAD_FAILURE } from '../../constants';
-import { UPSTREAM } from './constants';
+import LinkedPipeline from './linked_pipeline.vue';
+import { ONE_COL_WIDTH, UPSTREAM } from './constants';
import { unwrapPipelineData, toggleQueryPollingByVisibility, reportToSentry } from './utils';
export default {
@@ -39,6 +39,7 @@ export default {
'gl-pl-3',
'gl-mb-5',
],
+ minWidth: `${ONE_COL_WIDTH}px`,
computed: {
columnClass() {
const positionValues = {
@@ -47,12 +48,6 @@ export default {
};
return `graph-position-${this.graphPosition} ${positionValues[this.graphPosition]}`;
},
- graphPosition() {
- return this.isUpstream ? 'left' : 'right';
- },
- isUpstream() {
- return this.type === UPSTREAM;
- },
computedTitleClasses() {
const positionalClasses = this.isUpstream
? ['gl-w-full', 'gl-text-right', 'gl-linked-pipeline-padding']
@@ -60,6 +55,12 @@ export default {
return [...this.$options.titleClasses, ...positionalClasses];
},
+ graphPosition() {
+ return this.isUpstream ? 'left' : 'right';
+ },
+ isUpstream() {
+ return this.type === UPSTREAM;
+ },
},
methods: {
getPipelineData(pipeline) {
@@ -79,6 +80,7 @@ export default {
},
result() {
this.loadingPipelineId = null;
+ this.$emit('scrollContainer');
},
error(err, _vm, _key, type) {
this.$emit('error', LOAD_FAILURE);
@@ -130,6 +132,9 @@ export default {
this.$emit('pipelineExpandToggle', jobName, expanded);
},
+ showDownstreamContainer(id) {
+ return !this.isUpstream && (this.isExpanded(id) || this.isLoadingPipeline(id));
+ },
},
};
</script>
@@ -158,9 +163,13 @@ export default {
@pipelineClicked="onPipelineClick(pipeline)"
@pipelineExpandToggle="onPipelineExpandToggle"
/>
- <div v-if="isExpanded(pipeline.id)" class="gl-display-inline-block">
+ <div
+ v-if="showDownstreamContainer(pipeline.id)"
+ :style="{ minWidth: $options.minWidth }"
+ class="gl-display-inline-block"
+ >
<pipeline-graph
- v-if="currentPipeline"
+ v-if="isExpanded(pipeline.id)"
:type="type"
class="d-inline-block gl-mt-n2"
:pipeline="currentPipeline"
diff --git a/app/assets/javascripts/pipelines/components/graph/stage_column_component.vue b/app/assets/javascripts/pipelines/components/graph/stage_column_component.vue
index 65f8c231885..b258c885abb 100644
--- a/app/assets/javascripts/pipelines/components/graph/stage_column_component.vue
+++ b/app/assets/javascripts/pipelines/components/graph/stage_column_component.vue
@@ -67,6 +67,9 @@ export default {
errorCaptured(err, _vm, info) {
reportToSentry('stage_column_component', `error: ${err}, info: ${info}`);
},
+ mounted() {
+ this.$emit('updateMeasurements');
+ },
methods: {
getGroupId(group) {
return accessValue(GRAPHQL, 'groupId', group);
@@ -75,11 +78,7 @@ export default {
return `ci-badge-${escape(group.name)}`;
},
isFadedOut(jobName) {
- return (
- this.jobHovered &&
- this.highlightedJobs.length > 1 &&
- !this.highlightedJobs.includes(jobName)
- );
+ return this.highlightedJobs.length > 1 && !this.highlightedJobs.includes(jobName);
},
},
};
@@ -123,12 +122,9 @@ export default {
:class="{ 'gl-opacity-3': isFadedOut(group.name) }"
@pipelineActionRequestComplete="$emit('refreshPipelineGraph')"
/>
- <job-group-dropdown
- v-else
- :group="group"
- :pipeline-id="pipelineId"
- :class="{ 'gl-opacity-3': isFadedOut(group.name) }"
- />
+ <div v-else :class="{ 'gl-opacity-3': isFadedOut(group.name) }">
+ <job-group-dropdown :group="group" :pipeline-id="pipelineId" />
+ </div>
</div>
</template>
</main-graph-wrapper>
diff --git a/app/assets/javascripts/pipelines/components/graph_shared/drawing_utils.js b/app/assets/javascripts/pipelines/components/graph_shared/drawing_utils.js
index 65c215be794..202498fb188 100644
--- a/app/assets/javascripts/pipelines/components/graph_shared/drawing_utils.js
+++ b/app/assets/javascripts/pipelines/components/graph_shared/drawing_utils.js
@@ -40,10 +40,10 @@ export const generateLinksData = ({ links }, containerID, modifier = '') => {
// positioned in the center of the job node by adding half the height
// of the job pill.
const paddingLeft = parseFloat(
- window.getComputedStyle(containerEl, null).getPropertyValue('padding-left'),
+ window.getComputedStyle(containerEl, null).getPropertyValue('padding-left') || 0,
);
const paddingTop = parseFloat(
- window.getComputedStyle(containerEl, null).getPropertyValue('padding-top'),
+ window.getComputedStyle(containerEl, null).getPropertyValue('padding-top') || 0,
);
const sourceNodeX = sourceNodeCoordinates.right - containerCoordinates.x - paddingLeft;
diff --git a/app/assets/javascripts/pipelines/components/graph_shared/links_inner.vue b/app/assets/javascripts/pipelines/components/graph_shared/links_inner.vue
index 89444076ae0..289e04e02c5 100644
--- a/app/assets/javascripts/pipelines/components/graph_shared/links_inner.vue
+++ b/app/assets/javascripts/pipelines/components/graph_shared/links_inner.vue
@@ -118,22 +118,20 @@ export default {
<div class="gl-display-flex gl-relative">
<svg
id="link-svg"
- class="gl-absolute"
+ class="gl-absolute gl-pointer-events-none"
:viewBox="viewBox"
:width="`${containerMeasurements.width}px`"
:height="`${containerMeasurements.height}px`"
>
- <template>
- <path
- v-for="link in links"
- :key="link.path"
- :ref="link.ref"
- :d="link.path"
- class="gl-fill-transparent gl-transition-duration-slow gl-transition-timing-function-ease"
- :class="getLinkClasses(link)"
- :stroke-width="$options.STROKE_WIDTH"
- />
- </template>
+ <path
+ v-for="link in links"
+ :key="link.path"
+ :ref="link.ref"
+ :d="link.path"
+ class="gl-fill-transparent gl-transition-duration-slow gl-transition-timing-function-ease"
+ :class="getLinkClasses(link)"
+ :stroke-width="$options.STROKE_WIDTH"
+ />
</svg>
<slot></slot>
</div>
diff --git a/app/assets/javascripts/pipelines/components/graph_shared/links_layer.vue b/app/assets/javascripts/pipelines/components/graph_shared/links_layer.vue
index 0993892a574..1c1bc7ecb2a 100644
--- a/app/assets/javascripts/pipelines/components/graph_shared/links_layer.vue
+++ b/app/assets/javascripts/pipelines/components/graph_shared/links_layer.vue
@@ -74,13 +74,15 @@ export default {
<div v-else>
<gl-alert
v-if="showAlert"
- class="gl-w-max-content gl-ml-4"
+ class="gl-ml-4 gl-mb-4"
:primary-button-text="$options.i18n.showLinksAnyways"
@primaryAction="overrideShowLinks"
@dismiss="dismissAlert"
>
{{ $options.i18n.tooManyJobs }}
</gl-alert>
- <slot></slot>
+ <div class="gl-display-flex gl-relative">
+ <slot></slot>
+ </div>
</div>
</template>
diff --git a/app/assets/javascripts/pipelines/components/legacy_header_component.vue b/app/assets/javascripts/pipelines/components/legacy_header_component.vue
deleted file mode 100644
index c7b72be36ad..00000000000
--- a/app/assets/javascripts/pipelines/components/legacy_header_component.vue
+++ /dev/null
@@ -1,132 +0,0 @@
-<script>
-import { GlLoadingIcon, GlModal, GlModalDirective, GlButton } from '@gitlab/ui';
-import ciHeader from '~/vue_shared/components/header_ci_component.vue';
-import eventHub from '../event_hub';
-import { __ } from '~/locale';
-
-const DELETE_MODAL_ID = 'pipeline-delete-modal';
-
-export default {
- name: 'PipelineHeaderSection',
- components: {
- ciHeader,
- GlLoadingIcon,
- GlModal,
- GlButton,
- },
- directives: {
- GlModal: GlModalDirective,
- },
- props: {
- pipeline: {
- type: Object,
- required: true,
- },
- isLoading: {
- type: Boolean,
- required: true,
- },
- },
- data() {
- return {
- isCanceling: false,
- isRetrying: false,
- isDeleting: false,
- };
- },
-
- computed: {
- status() {
- return this.pipeline.details && this.pipeline.details.status;
- },
- shouldRenderContent() {
- return !this.isLoading && Object.keys(this.pipeline).length;
- },
- deleteModalConfirmationText() {
- return __(
- 'Are you sure you want to delete this pipeline? Doing so will expire all pipeline caches and delete all related objects, such as builds, logs, artifacts, and triggers. This action cannot be undone.',
- );
- },
- },
-
- methods: {
- cancelPipeline() {
- this.isCanceling = true;
- eventHub.$emit('headerPostAction', this.pipeline.cancel_path);
- },
- retryPipeline() {
- this.isRetrying = true;
- eventHub.$emit('headerPostAction', this.pipeline.retry_path);
- },
- deletePipeline() {
- this.isDeleting = true;
- eventHub.$emit('headerDeleteAction', this.pipeline.delete_path);
- },
- },
- DELETE_MODAL_ID,
-};
-</script>
-<template>
- <div class="pipeline-header-container">
- <ci-header
- v-if="shouldRenderContent"
- :status="status"
- :item-id="pipeline.id"
- :time="pipeline.created_at"
- :user="pipeline.user"
- item-name="Pipeline"
- >
- <gl-button
- v-if="pipeline.retry_path"
- :loading="isRetrying"
- :disabled="isRetrying"
- data-testid="retryButton"
- category="secondary"
- variant="info"
- @click="retryPipeline()"
- >
- {{ __('Retry') }}
- </gl-button>
-
- <gl-button
- v-if="pipeline.cancel_path"
- :loading="isCanceling"
- :disabled="isCanceling"
- data-testid="cancelPipeline"
- class="gl-ml-3"
- category="primary"
- variant="danger"
- @click="cancelPipeline()"
- >
- {{ __('Cancel running') }}
- </gl-button>
-
- <gl-button
- v-if="pipeline.delete_path"
- v-gl-modal="$options.DELETE_MODAL_ID"
- :loading="isDeleting"
- :disabled="isDeleting"
- data-testid="deletePipeline"
- class="gl-ml-3"
- category="secondary"
- variant="danger"
- >
- {{ __('Delete') }}
- </gl-button>
- </ci-header>
-
- <gl-loading-icon v-if="isLoading" size="lg" class="gl-mt-3 gl-mb-3" />
-
- <gl-modal
- :modal-id="$options.DELETE_MODAL_ID"
- :title="__('Delete pipeline')"
- :ok-title="__('Delete pipeline')"
- ok-variant="danger"
- @ok="deletePipeline()"
- >
- <p>
- {{ deleteModalConfirmationText }}
- </p>
- </gl-modal>
- </div>
-</template>
diff --git a/app/assets/javascripts/pipelines/components/pipeline_graph/pipeline_graph.vue b/app/assets/javascripts/pipelines/components/pipeline_graph/pipeline_graph.vue
index 8636808b69e..678678b87eb 100644
--- a/app/assets/javascripts/pipelines/components/pipeline_graph/pipeline_graph.vue
+++ b/app/assets/javascripts/pipelines/components/pipeline_graph/pipeline_graph.vue
@@ -1,13 +1,13 @@
<script>
import { GlAlert } from '@gitlab/ui';
import { __ } from '~/locale';
+import { CI_CONFIG_STATUS_INVALID } from '~/pipeline_editor/constants';
import { generateLinksData } from '../graph_shared/drawing_utils';
-import JobPill from './job_pill.vue';
-import StagePill from './stage_pill.vue';
import { parseData } from '../parsing_utils';
import { DRAW_FAILURE, DEFAULT, INVALID_CI_CONFIG, EMPTY_PIPELINE_DATA } from '../../constants';
import { createJobsHash, generateJobNeedsDict } from '../../utils';
-import { CI_CONFIG_STATUS_INVALID } from '~/pipeline_editor/constants';
+import StagePill from './stage_pill.vue';
+import JobPill from './job_pill.vue';
export default {
components: {
@@ -224,17 +224,15 @@ export default {
data-testid="graph-container"
>
<svg :viewBox="viewBox" :width="width" :height="height" class="gl-absolute">
- <template>
- <path
- v-for="link in links"
- :key="link.path"
- :ref="link.ref"
- :d="link.path"
- class="gl-fill-transparent gl-transition-duration-slow gl-transition-timing-function-ease"
- :class="getLinkClasses(link)"
- :stroke-width="$options.STROKE_WIDTH"
- />
- </template>
+ <path
+ v-for="link in links"
+ :key="link.path"
+ :ref="link.ref"
+ :d="link.path"
+ class="gl-fill-transparent gl-transition-duration-slow gl-transition-timing-function-ease"
+ :class="getLinkClasses(link)"
+ :stroke-width="$options.STROKE_WIDTH"
+ />
</svg>
<div
v-for="(stage, index) in pipelineStages"
diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_url.vue b/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_url.vue
index d1bac078642..823ada133d2 100644
--- a/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_url.vue
+++ b/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_url.vue
@@ -77,6 +77,15 @@ export default {
>{{ __('latest') }}</gl-badge
>
<gl-badge
+ v-if="pipeline.flags.merge_train_pipeline"
+ v-gl-tooltip
+ :title="__('This is a merge train pipeline')"
+ variant="info"
+ size="sm"
+ data-testid="pipeline-url-train"
+ >{{ __('train') }}</gl-badge
+ >
+ <gl-badge
v-if="pipeline.flags.yaml_errors"
v-gl-tooltip
:title="pipeline.yaml_errors"
diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/pipelines.vue b/app/assets/javascripts/pipelines/components/pipelines_list/pipelines.vue
index ec7c5764be1..b4eb429748f 100644
--- a/app/assets/javascripts/pipelines/components/pipelines_list/pipelines.vue
+++ b/app/assets/javascripts/pipelines/components/pipelines_list/pipelines.vue
@@ -3,16 +3,16 @@ import { isEqual } from 'lodash';
import { GlIcon } from '@gitlab/ui';
import { __, s__ } from '~/locale';
import { deprecatedCreateFlash as createFlash } from '~/flash';
-import PipelinesService from '../../services/pipelines_service';
-import pipelinesMixin from '../../mixins/pipelines';
import TablePagination from '~/vue_shared/components/pagination/table_pagination.vue';
import NavigationTabs from '~/vue_shared/components/navigation_tabs.vue';
-import NavigationControls from './nav_controls.vue';
import { getParameterByName } from '~/lib/utils/common_utils';
import CIPaginationMixin from '~/vue_shared/mixins/ci_pagination_api_mixin';
-import PipelinesFilteredSearch from './pipelines_filtered_search.vue';
+import pipelinesMixin from '../../mixins/pipelines';
+import PipelinesService from '../../services/pipelines_service';
import { validateParams } from '../../utils';
import { ANY_TRIGGER_AUTHOR, RAW_TEXT_WARNING, FILTER_TAG_IDENTIFIER } from '../../constants';
+import NavigationControls from './nav_controls.vue';
+import PipelinesFilteredSearch from './pipelines_filtered_search.vue';
export default {
components: {
diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_actions.vue b/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_actions.vue
index 1ea71610897..13f314a3a45 100644
--- a/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_actions.vue
+++ b/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_actions.vue
@@ -1,7 +1,7 @@
<script>
-import { GlTooltipDirective, GlButton, GlLoadingIcon, GlIcon } from '@gitlab/ui';
+import { GlDropdown, GlDropdownItem, GlIcon, GlTooltipDirective } from '@gitlab/ui';
import axios from '~/lib/utils/axios_utils';
-import { deprecatedCreateFlash as flash } from '~/flash';
+import createFlash from '~/flash';
import { s__, __, sprintf } from '~/locale';
import GlCountdown from '~/vue_shared/components/gl_countdown.vue';
import eventHub from '../../event_hub';
@@ -11,10 +11,10 @@ export default {
GlTooltip: GlTooltipDirective,
},
components: {
- GlIcon,
GlCountdown,
- GlButton,
- GlLoadingIcon,
+ GlDropdown,
+ GlDropdownItem,
+ GlIcon,
},
props: {
actions: {
@@ -61,7 +61,7 @@ export default {
})
.catch(() => {
this.isLoading = false;
- flash(__('An error occurred while making the request.'));
+ createFlash({ message: __('An error occurred while making the request.') });
});
},
@@ -76,39 +76,27 @@ export default {
};
</script>
<template>
- <div class="btn-group">
- <button
- v-gl-tooltip
- type="button"
- :disabled="isLoading"
- class="dropdown-new btn btn-default js-pipeline-dropdown-manual-actions"
- :title="__('Run manual or delayed jobs')"
- data-toggle="dropdown"
- :aria-label="__('Run manual or delayed jobs')"
+ <gl-dropdown
+ v-gl-tooltip
+ :title="__('Run manual or delayed jobs')"
+ :loading="isLoading"
+ data-testid="pipelines-manual-actions-dropdown"
+ right
+ icon="play"
+ >
+ <gl-dropdown-item
+ v-for="action in actions"
+ :key="action.path"
+ :disabled="isActionDisabled(action)"
+ @click="onClickAction(action)"
>
- <gl-icon name="play" class="icon-play" />
- <gl-icon name="chevron-down" />
- <gl-loading-icon v-if="isLoading" />
- </button>
-
- <ul class="dropdown-menu dropdown-menu-right">
- <li v-for="action in actions" :key="action.path">
- <gl-button
- category="tertiary"
- :class="{ disabled: isActionDisabled(action) }"
- :disabled="isActionDisabled(action)"
- class="js-pipeline-action-link"
- @click="onClickAction(action)"
- >
- <div class="d-flex justify-content-between flex-wrap">
- {{ action.name }}
- <span v-if="action.scheduled_at">
- <gl-icon name="clock" />
- <gl-countdown :end-date-string="action.scheduled_at" />
- </span>
- </div>
- </gl-button>
- </li>
- </ul>
- </div>
+ <div class="gl-display-flex gl-justify-content-space-between gl-flex-wrap">
+ {{ action.name }}
+ <span v-if="action.scheduled_at">
+ <gl-icon name="clock" />
+ <gl-countdown :end-date-string="action.scheduled_at" />
+ </span>
+ </div>
+ </gl-dropdown-item>
+ </gl-dropdown>
</template>
diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_table.vue b/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_table.vue
index 6c60594efca..1b08f883b05 100644
--- a/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_table.vue
+++ b/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_table.vue
@@ -1,8 +1,8 @@
<script>
import { GlTooltipDirective } from '@gitlab/ui';
+import eventHub from '../../event_hub';
import PipelinesTableRowComponent from './pipelines_table_row.vue';
import PipelineStopModal from './pipeline_stop_modal.vue';
-import eventHub from '../../event_hub';
/**
* Pipelines Table Component.
diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_table_row.vue b/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_table_row.vue
index b6c4e617a90..5231fe0b112 100644
--- a/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_table_row.vue
+++ b/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_table_row.vue
@@ -1,16 +1,16 @@
<script>
import { GlButton, GlTooltipDirective, GlModalDirective } from '@gitlab/ui';
-import eventHub from '../../event_hub';
import { __ } from '~/locale';
+import CiBadge from '~/vue_shared/components/ci_badge_link.vue';
+import CommitComponent from '~/vue_shared/components/commit.vue';
+import eventHub from '../../event_hub';
+import { PIPELINES_TABLE } from '../../constants';
import PipelinesActionsComponent from './pipelines_actions.vue';
import PipelinesArtifactsComponent from './pipelines_artifacts.vue';
-import CiBadge from '~/vue_shared/components/ci_badge_link.vue';
import PipelineStage from './stage.vue';
import PipelineUrl from './pipeline_url.vue';
import PipelineTriggerer from './pipeline_triggerer.vue';
import PipelinesTimeago from './time_ago.vue';
-import CommitComponent from '~/vue_shared/components/commit.vue';
-import { PIPELINES_TABLE } from '../../constants';
/**
* Pipeline table row.
diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/stage.vue b/app/assets/javascripts/pipelines/components/pipelines_list/stage.vue
index a9154d93194..460aa427196 100644
--- a/app/assets/javascripts/pipelines/components/pipelines_list/stage.vue
+++ b/app/assets/javascripts/pipelines/components/pipelines_list/stage.vue
@@ -11,11 +11,11 @@
* 3. Merge request widget
* 4. Commit widget
*/
-
import $ from 'jquery';
-import { GlLoadingIcon, GlTooltipDirective, GlIcon } from '@gitlab/ui';
+import { GlDropdown, GlLoadingIcon, GlTooltipDirective, GlIcon } from '@gitlab/ui';
import { __ } from '~/locale';
import { deprecatedCreateFlash as Flash } from '~/flash';
+import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import axios from '~/lib/utils/axios_utils';
import eventHub from '../../event_hub';
import JobItem from '../graph/job_item.vue';
@@ -24,14 +24,14 @@ import { PIPELINES_TABLE } from '../../constants';
export default {
components: {
GlIcon,
- JobItem,
GlLoadingIcon,
+ GlDropdown,
+ JobItem,
},
-
directives: {
GlTooltip: GlTooltipDirective,
},
-
+ mixins: [glFeatureFlagsMixin()],
props: {
stage: {
type: Object,
@@ -50,30 +50,25 @@ export default {
default: '',
},
},
-
data() {
return {
isLoading: false,
- dropdownContent: '',
+ dropdownContent: [],
};
},
-
computed: {
- dropdownClass() {
- return this.dropdownContent.length > 0
- ? 'js-builds-dropdown-container'
- : 'js-builds-dropdown-loading';
+ isCiMiniPipelineGlDropdown() {
+ // Feature flag ci_mini_pipeline_gl_dropdown
+ // See more at https://gitlab.com/gitlab-org/gitlab/-/issues/300400
+ return this.glFeatures?.ciMiniPipelineGlDropdown;
},
-
triggerButtonClass() {
return `ci-status-icon-${this.stage.status.group}`;
},
-
borderlessIcon() {
return `${this.stage.status.icon}_borderless`;
},
},
-
watch: {
updateDropdown() {
if (this.updateDropdown && this.isDropdownOpen() && !this.isLoading) {
@@ -81,14 +76,17 @@ export default {
}
},
},
-
updated() {
- if (this.dropdownContent.length > 0) {
+ if (!this.isCiMiniPipelineGlDropdown && this.dropdownContent.length) {
this.stopDropdownClickPropagation();
}
},
-
methods: {
+ onShowDropdown() {
+ eventHub.$emit('clickedDropdown');
+ this.isLoading = true;
+ this.fetchJobs();
+ },
onClickStage() {
if (!this.isDropdownOpen()) {
eventHub.$emit('clickedDropdown');
@@ -96,7 +94,6 @@ export default {
this.fetchJobs();
}
},
-
fetchJobs() {
axios
.get(this.stage.dropdown_path)
@@ -105,13 +102,16 @@ export default {
this.isLoading = false;
})
.catch(() => {
- this.closeDropdown();
+ if (this.isCiMiniPipelineGlDropdown) {
+ this.$refs.stageGlDropdown.hide();
+ } else {
+ this.closeDropdown();
+ }
this.isLoading = false;
Flash(__('Something went wrong on our end.'));
});
},
-
/**
* When the user right clicks or cmd/ctrl + click in the job name
* the dropdown should not be closed and the link should open in another tab,
@@ -119,6 +119,8 @@ export default {
*
* Since this component is rendered multiple times per page we need to guarantee we only
* target the click event of this component.
+ *
+ * Note: This should be removed once ci_mini_pipeline_gl_dropdown FF is removed as true.
*/
stopDropdownClickPropagation() {
$(
@@ -128,23 +130,24 @@ export default {
e.stopPropagation();
});
},
-
closeDropdown() {
if (this.isDropdownOpen()) {
$(this.$refs.dropdown).dropdown('toggle');
}
},
-
isDropdownOpen() {
return this.$el.classList.contains('show');
},
-
pipelineActionRequestComplete() {
if (this.type === PIPELINES_TABLE) {
// warn the table to update
eventHub.$emit('refreshPipelinesTable');
+ return;
+ }
+ // close the dropdown in mr widget
+ if (this.isCiMiniPipelineGlDropdown) {
+ this.$refs.stageGlDropdown.hide();
} else {
- // close the dropdown in mr widget
$(this.$refs.dropdown).dropdown('toggle');
}
},
@@ -154,31 +157,30 @@ export default {
<template>
<div class="dropdown">
- <button
- id="stageDropdown"
- ref="dropdown"
+ <gl-dropdown
+ v-if="isCiMiniPipelineGlDropdown"
+ ref="stageGlDropdown"
v-gl-tooltip.hover
- :class="triggerButtonClass"
+ data-testid="mini-pipeline-graph-dropdown"
:title="stage.title"
- class="mini-pipeline-graph-dropdown-toggle js-builds-dropdown-button"
- data-toggle="dropdown"
- data-display="static"
- type="button"
- aria-haspopup="true"
- aria-expanded="false"
- @click="onClickStage"
- >
- <span :aria-label="stage.title" aria-hidden="true" class="gl-pointer-events-none">
- <gl-icon :name="borderlessIcon" />
- </span>
- </button>
-
- <div
- class="dropdown-menu mini-pipeline-graph-dropdown-menu js-builds-dropdown-container"
- aria-labelledby="stageDropdown"
+ variant="link"
+ :lazy="true"
+ :popper-opts="{ placement: 'bottom' }"
+ :toggle-class="['mini-pipeline-graph-gl-dropdown-toggle', triggerButtonClass]"
+ menu-class="mini-pipeline-graph-dropdown-menu"
+ @show="onShowDropdown"
>
+ <template #button-content>
+ <span class="gl-pointer-events-none">
+ <gl-icon :name="borderlessIcon" />
+ </span>
+ </template>
<gl-loading-icon v-if="isLoading" />
- <ul v-else class="js-builds-dropdown-list scrollable-menu">
+ <ul
+ v-else
+ class="js-builds-dropdown-list scrollable-menu"
+ data-testid="mini-pipeline-graph-dropdown-menu-list"
+ >
<li v-for="job in dropdownContent" :key="job.id">
<job-item
:dropdown-length="dropdownContent.length"
@@ -188,6 +190,45 @@ export default {
/>
</li>
</ul>
- </div>
+ </gl-dropdown>
+
+ <template v-else>
+ <button
+ id="stageDropdown"
+ ref="dropdown"
+ v-gl-tooltip.hover
+ :class="triggerButtonClass"
+ :title="stage.title"
+ class="mini-pipeline-graph-dropdown-toggle"
+ data-testid="mini-pipeline-graph-dropdown-toggle"
+ data-toggle="dropdown"
+ data-display="static"
+ type="button"
+ aria-haspopup="true"
+ aria-expanded="false"
+ @click="onClickStage"
+ >
+ <span :aria-label="stage.title" aria-hidden="true" class="gl-pointer-events-none">
+ <gl-icon :name="borderlessIcon" />
+ </span>
+ </button>
+
+ <div
+ class="dropdown-menu mini-pipeline-graph-dropdown-menu js-builds-dropdown-container"
+ aria-labelledby="stageDropdown"
+ >
+ <gl-loading-icon v-if="isLoading" />
+ <ul v-else class="js-builds-dropdown-list scrollable-menu">
+ <li v-for="job in dropdownContent" :key="job.id">
+ <job-item
+ :dropdown-length="dropdownContent.length"
+ :job="job"
+ css-class-job-name="mini-pipeline-graph-dropdown-item"
+ @pipelineActionRequestComplete="pipelineActionRequestComplete"
+ />
+ </li>
+ </ul>
+ </div>
+ </template>
</div>
</template>
diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/tokens/pipeline_branch_name_token.vue b/app/assets/javascripts/pipelines/components/pipelines_list/tokens/pipeline_branch_name_token.vue
index 24456574a6f..20a232beb83 100644
--- a/app/assets/javascripts/pipelines/components/pipelines_list/tokens/pipeline_branch_name_token.vue
+++ b/app/assets/javascripts/pipelines/components/pipelines_list/tokens/pipeline_branch_name_token.vue
@@ -2,8 +2,8 @@
import { GlFilteredSearchToken, GlFilteredSearchSuggestion, GlLoadingIcon } from '@gitlab/ui';
import { debounce } from 'lodash';
import Api from '~/api';
-import { FETCH_BRANCH_ERROR_MESSAGE, FILTER_PIPELINES_SEARCH_DELAY } from '../../../constants';
import { deprecatedCreateFlash as createFlash } from '~/flash';
+import { FETCH_BRANCH_ERROR_MESSAGE, FILTER_PIPELINES_SEARCH_DELAY } from '../../../constants';
export default {
components: {
diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/tokens/pipeline_tag_name_token.vue b/app/assets/javascripts/pipelines/components/pipelines_list/tokens/pipeline_tag_name_token.vue
index 1241803c612..4a8d89ebe37 100644
--- a/app/assets/javascripts/pipelines/components/pipelines_list/tokens/pipeline_tag_name_token.vue
+++ b/app/assets/javascripts/pipelines/components/pipelines_list/tokens/pipeline_tag_name_token.vue
@@ -2,8 +2,8 @@
import { GlFilteredSearchToken, GlFilteredSearchSuggestion, GlLoadingIcon } from '@gitlab/ui';
import { debounce } from 'lodash';
import Api from '~/api';
-import { FETCH_TAG_ERROR_MESSAGE, FILTER_PIPELINES_SEARCH_DELAY } from '../../../constants';
import { deprecatedCreateFlash as createFlash } from '~/flash';
+import { FETCH_TAG_ERROR_MESSAGE, FILTER_PIPELINES_SEARCH_DELAY } from '../../../constants';
export default {
components: {
diff --git a/app/assets/javascripts/pipelines/components/test_reports/test_case_details.vue b/app/assets/javascripts/pipelines/components/test_reports/test_case_details.vue
index 504cf138d07..08f296bec12 100644
--- a/app/assets/javascripts/pipelines/components/test_reports/test_case_details.vue
+++ b/app/assets/javascripts/pipelines/components/test_reports/test_case_details.vue
@@ -1,12 +1,13 @@
<script>
-import { GlModal } from '@gitlab/ui';
-import { __ } from '~/locale';
+import { GlBadge, GlModal } from '@gitlab/ui';
+import { __, n__, sprintf } from '~/locale';
import CodeBlock from '~/vue_shared/components/code_block.vue';
export default {
name: 'TestCaseDetails',
components: {
CodeBlock,
+ GlBadge,
GlModal,
},
props: {
@@ -21,9 +22,35 @@ export default {
Boolean(classname) && Boolean(formattedTime) && Boolean(name),
},
},
+ computed: {
+ failureHistoryMessage() {
+ if (!this.hasRecentFailures) {
+ return null;
+ }
+
+ return sprintf(
+ n__(
+ 'Reports|Failed %{count} time in %{baseBranch} in the last 14 days',
+ 'Reports|Failed %{count} times in %{baseBranch} in the last 14 days',
+ this.recentFailures.count,
+ ),
+ {
+ count: this.recentFailures.count,
+ baseBranch: this.recentFailures.base_branch,
+ },
+ );
+ },
+ hasRecentFailures() {
+ return Boolean(this.recentFailures);
+ },
+ recentFailures() {
+ return this.testCase.recent_failures;
+ },
+ },
text: {
name: __('Name'),
duration: __('Execution time'),
+ history: __('History'),
trace: __('System output'),
},
modalCloseButton: {
@@ -53,6 +80,13 @@ export default {
</div>
</div>
+ <div v-if="testCase.recent_failures" class="gl-display-flex gl-flex-wrap gl-mx-n4 gl-my-3">
+ <strong class="gl-text-right col-sm-3">{{ $options.text.history }}</strong>
+ <div class="col-sm-9" data-testid="test-case-recent-failures">
+ <gl-badge variant="warning">{{ failureHistoryMessage }}</gl-badge>
+ </div>
+ </div>
+
<div
v-if="testCase.system_output"
class="gl-display-flex gl-flex-wrap gl-mx-n4 gl-my-3"
diff --git a/app/assets/javascripts/pipelines/components/test_reports/test_suite_table.vue b/app/assets/javascripts/pipelines/components/test_reports/test_suite_table.vue
index 4b4fb6082c6..6b7381883cc 100644
--- a/app/assets/javascripts/pipelines/components/test_reports/test_suite_table.vue
+++ b/app/assets/javascripts/pipelines/components/test_reports/test_suite_table.vue
@@ -114,7 +114,7 @@ export default {
<div role="rowheader" class="table-mobile-header">{{ __('Status') }}</div>
<div class="table-mobile-content text-center">
<div
- class="add-border ci-status-icon d-flex align-items-center justify-content-end justify-content-md-center"
+ class="ci-status-icon d-flex align-items-center justify-content-end justify-content-md-center"
:class="`ci-status-icon-${testCase.status}`"
>
<gl-icon :size="24" :name="testCase.icon" />
diff --git a/app/assets/javascripts/pipelines/pipeline_details_bundle.js b/app/assets/javascripts/pipelines/pipeline_details_bundle.js
index 133608b9801..5ee5b45aac9 100644
--- a/app/assets/javascripts/pipelines/pipeline_details_bundle.js
+++ b/app/assets/javascripts/pipelines/pipeline_details_bundle.js
@@ -2,12 +2,9 @@ import Vue from 'vue';
import { deprecatedCreateFlash as Flash } from '~/flash';
import Translate from '~/vue_shared/translate';
import { __ } from '~/locale';
-import { setUrlFragment, redirectTo } from '~/lib/utils/url_utility';
import PipelineGraphLegacy from './components/graph/graph_component_legacy.vue';
import createDagApp from './pipeline_details_dag';
import GraphBundleMixin from './mixins/graph_pipeline_bundle_mixin';
-import legacyPipelineHeader from './components/legacy_header_component.vue';
-import eventHub from './event_hub';
import TestReports from './components/test_reports/test_reports.vue';
import createTestReportsStore from './stores/test_reports';
import { reportToSentry } from './components/graph/utils';
@@ -59,58 +56,6 @@ const createLegacyPipelinesDetailApp = (mediator) => {
});
};
-const createLegacyPipelineHeaderApp = (mediator) => {
- if (!document.querySelector(SELECTORS.PIPELINE_HEADER)) {
- return;
- }
- // eslint-disable-next-line no-new
- new Vue({
- el: SELECTORS.PIPELINE_HEADER,
- components: {
- legacyPipelineHeader,
- },
- data() {
- return {
- mediator,
- };
- },
- created() {
- eventHub.$on('headerPostAction', this.postAction);
- eventHub.$on('headerDeleteAction', this.deleteAction);
- },
- beforeDestroy() {
- eventHub.$off('headerPostAction', this.postAction);
- eventHub.$off('headerDeleteAction', this.deleteAction);
- },
- errorCaptured(err, _vm, info) {
- reportToSentry('pipeline_details_bundle_legacy', `error: ${err}, info: ${info}`);
- },
- methods: {
- postAction(path) {
- this.mediator.service
- .postAction(path)
- .then(() => this.mediator.refreshPipeline())
- .catch(() => Flash(__('An error occurred while making the request.')));
- },
- deleteAction(path) {
- this.mediator.stopPipelinePoll();
- this.mediator.service
- .deleteAction(path)
- .then(({ request }) => redirectTo(setUrlFragment(request.responseURL, 'delete_success')))
- .catch(() => Flash(__('An error occurred while deleting the pipeline.')));
- },
- },
- render(createElement) {
- return createElement('legacy-pipeline-header', {
- props: {
- isLoading: this.mediator.state.isLoading,
- pipeline: this.mediator.store.state.pipeline,
- },
- });
- },
- });
-};
-
const createTestDetails = () => {
const el = document.querySelector(SELECTORS.PIPELINE_TESTS);
const { summaryEndpoint, suiteEndpoint } = el?.dataset || {};
@@ -136,22 +81,12 @@ export default async function () {
createTestDetails();
createDagApp();
- const { dataset } = document.querySelector(SELECTORS.PIPELINE_DETAILS);
- let mediator;
+ const canShowNewPipelineDetails =
+ gon.features.graphqlPipelineDetails || gon.features.graphqlPipelineDetailsUsers;
- if (!gon.features.graphqlPipelineHeader || !gon.features.graphqlPipelineDetails) {
- try {
- const { default: PipelinesMediator } = await import(
- /* webpackChunkName: 'PipelinesMediator' */ './pipeline_details_mediator'
- );
- mediator = new PipelinesMediator({ endpoint: dataset.endpoint });
- mediator.fetchPipeline();
- } catch {
- Flash(__('An error occurred while loading the pipeline.'));
- }
- }
+ const { dataset } = document.querySelector(SELECTORS.PIPELINE_DETAILS);
- if (gon.features.graphqlPipelineDetails) {
+ if (canShowNewPipelineDetails) {
try {
const { createPipelinesDetailApp } = await import(
/* webpackChunkName: 'createPipelinesDetailApp' */ './pipeline_details_graph'
@@ -163,19 +98,21 @@ export default async function () {
Flash(__('An error occurred while loading the pipeline.'));
}
} else {
+ const { default: PipelinesMediator } = await import(
+ /* webpackChunkName: 'PipelinesMediator' */ './pipeline_details_mediator'
+ );
+ const mediator = new PipelinesMediator({ endpoint: dataset.endpoint });
+ mediator.fetchPipeline();
+
createLegacyPipelinesDetailApp(mediator);
}
- if (gon.features.graphqlPipelineHeader) {
- try {
- const { createPipelineHeaderApp } = await import(
- /* webpackChunkName: 'createPipelineHeaderApp' */ './pipeline_details_header'
- );
- createPipelineHeaderApp(SELECTORS.PIPELINE_HEADER);
- } catch {
- Flash(__('An error occurred while loading a section of this page.'));
- }
- } else {
- createLegacyPipelineHeaderApp(mediator);
+ try {
+ const { createPipelineHeaderApp } = await import(
+ /* webpackChunkName: 'createPipelineHeaderApp' */ './pipeline_details_header'
+ );
+ createPipelineHeaderApp(SELECTORS.PIPELINE_HEADER);
+ } catch {
+ Flash(__('An error occurred while loading a section of this page.'));
}
}
diff --git a/app/assets/javascripts/pipelines/pipeline_details_dag.js b/app/assets/javascripts/pipelines/pipeline_details_dag.js
index d37c72a4f2a..4ee0ad462d2 100644
--- a/app/assets/javascripts/pipelines/pipeline_details_dag.js
+++ b/app/assets/javascripts/pipelines/pipeline_details_dag.js
@@ -12,7 +12,7 @@ const apolloProvider = new VueApollo({
const createDagApp = () => {
const el = document.querySelector('#js-pipeline-dag-vue');
- if (!window.gon?.features?.dagPipelineTab || !el) {
+ if (!el) {
return;
}
diff --git a/app/assets/javascripts/pipelines/pipeline_details_mediator.js b/app/assets/javascripts/pipelines/pipeline_details_mediator.js
index 74c5fc45644..474dc828e5e 100644
--- a/app/assets/javascripts/pipelines/pipeline_details_mediator.js
+++ b/app/assets/javascripts/pipelines/pipeline_details_mediator.js
@@ -1,8 +1,8 @@
import Visibility from 'visibilityjs';
-import PipelineStore from './stores/pipeline_store';
import { deprecatedCreateFlash as Flash } from '../flash';
import Poll from '../lib/utils/poll';
import { __ } from '../locale';
+import PipelineStore from './stores/pipeline_store';
import PipelineService from './services/pipeline_service';
export default class pipelinesMediator {
diff --git a/app/assets/javascripts/pipelines/services/pipelines_service.js b/app/assets/javascripts/pipelines/services/pipelines_service.js
index 0b06bcf243a..523ca13b6c3 100644
--- a/app/assets/javascripts/pipelines/services/pipelines_service.js
+++ b/app/assets/javascripts/pipelines/services/pipelines_service.js
@@ -1,5 +1,5 @@
-import axios from '../../lib/utils/axios_utils';
import Api from '~/api';
+import axios from '../../lib/utils/axios_utils';
import { validateParams } from '../utils';
export default class PipelinesService {
diff --git a/app/assets/javascripts/pipelines/stores/test_reports/actions.js b/app/assets/javascripts/pipelines/stores/test_reports/actions.js
index 3c664457756..3512734e528 100644
--- a/app/assets/javascripts/pipelines/stores/test_reports/actions.js
+++ b/app/assets/javascripts/pipelines/stores/test_reports/actions.js
@@ -1,7 +1,7 @@
import axios from '~/lib/utils/axios_utils';
-import * as types from './mutation_types';
import { deprecatedCreateFlash as createFlash } from '~/flash';
import { s__ } from '~/locale';
+import * as types from './mutation_types';
export const fetchSummary = ({ state, commit, dispatch }) => {
dispatch('toggleLoading');
@@ -28,16 +28,12 @@ export const fetchTestSuite = ({ state, commit, dispatch }, index) => {
dispatch('toggleLoading');
- const { name = '', build_ids = [] } = state.testReports?.test_suites?.[index] || {};
+ const { build_ids = [] } = state.testReports?.test_suites?.[index] || {};
// Replacing `/:suite_name.json` with the name of the suite. Including the extra characters
// to ensure that we replace exactly the template part of the URL string
- const endpoint = state.suiteEndpoint?.replace(
- '/:suite_name.json',
- `/${encodeURIComponent(name)}.json`,
- );
return axios
- .get(endpoint, { params: { build_ids } })
+ .get(state.suiteEndpoint, { params: { build_ids } })
.then(({ data }) => commit(types.SET_SUITE, { suite: data, index }))
.catch(() => {
createFlash(s__('TestReports|There was an error fetching the test suite.'));
diff --git a/app/assets/javascripts/pipelines/stores/test_reports/utils.js b/app/assets/javascripts/pipelines/stores/test_reports/utils.js
index 5c1f27b166a..8d1a941058c 100644
--- a/app/assets/javascripts/pipelines/stores/test_reports/utils.js
+++ b/app/assets/javascripts/pipelines/stores/test_reports/utils.js
@@ -4,16 +4,16 @@ import { TestStatus } from '../../constants';
export function iconForTestStatus(status) {
switch (status) {
case TestStatus.SUCCESS:
- return 'status_success_borderless';
+ return 'status_success';
case TestStatus.FAILED:
- return 'status_failed_borderless';
+ return 'status_failed';
case TestStatus.ERROR:
- return 'status_warning_borderless';
+ return 'status_warning';
case TestStatus.SKIPPED:
- return 'status_skipped_borderless';
+ return 'status_skipped';
case TestStatus.UNKNOWN:
default:
- return 'status_notfound_borderless';
+ return 'status_notfound';
}
}
diff --git a/app/assets/javascripts/pipelines/utils.js b/app/assets/javascripts/pipelines/utils.js
index 50bb23b7e63..23046586de7 100644
--- a/app/assets/javascripts/pipelines/utils.js
+++ b/app/assets/javascripts/pipelines/utils.js
@@ -49,10 +49,10 @@ export const generateJobNeedsDict = (jobs = {}) => {
// to the list of `needs` to ensure we can properly reference it.
const group = jobs[job];
if (group.size > 1) {
- return [job, group.name, ...newNeeds];
+ return [job, group.name, newNeeds];
}
- return [job, ...newNeeds];
+ return [job, newNeeds];
})
.flat(Infinity);
};
diff --git a/app/assets/javascripts/profile/account/index.js b/app/assets/javascripts/profile/account/index.js
index 5e89002b3bc..f6f424db7bd 100644
--- a/app/assets/javascripts/profile/account/index.js
+++ b/app/assets/javascripts/profile/account/index.js
@@ -1,5 +1,6 @@
import Vue from 'vue';
import Translate from '~/vue_shared/translate';
+import { BV_SHOW_MODAL } from '~/lib/utils/constants';
import UpdateUsername from './components/update_username.vue';
import deleteAccountModal from './components/delete_account_modal.vue';
@@ -31,7 +32,7 @@ export default () => {
mounted() {
deleteAccountButton.classList.remove('disabled');
deleteAccountButton.addEventListener('click', () => {
- this.$root.$emit('bv::show::modal', 'delete-account-modal', '#delete-account-button');
+ this.$root.$emit(BV_SHOW_MODAL, 'delete-account-modal', '#delete-account-button');
});
},
render(createElement) {
diff --git a/app/assets/javascripts/profile/profile.js b/app/assets/javascripts/profile/profile.js
index bfeeff47163..e6c41c7615c 100644
--- a/app/assets/javascripts/profile/profile.js
+++ b/app/assets/javascripts/profile/profile.js
@@ -1,11 +1,11 @@
import $ from 'jquery';
import axios from '~/lib/utils/axios_utils';
import { Rails } from '~/lib/utils/rails_ujs';
-import { deprecatedCreateFlash as flash } from '../flash';
import { parseBoolean } from '~/lib/utils/common_utils';
import TimezoneDropdown, {
formatTimezone,
} from '~/pages/projects/pipeline_schedules/shared/components/timezone_dropdown';
+import { deprecatedCreateFlash as flash } from '../flash';
export default class Profile {
constructor({ form } = {}) {
diff --git a/app/assets/javascripts/project_label_subscription.js b/app/assets/javascripts/project_label_subscription.js
index 4fefc2ed569..666f6e15e61 100644
--- a/app/assets/javascripts/project_label_subscription.js
+++ b/app/assets/javascripts/project_label_subscription.js
@@ -1,8 +1,8 @@
import $ from 'jquery';
+import { fixTitle } from '~/tooltips';
import { __ } from './locale';
import axios from './lib/utils/axios_utils';
import { deprecatedCreateFlash as flash } from './flash';
-import { fixTitle } from '~/tooltips';
const tooltipTitles = {
group: {
diff --git a/app/assets/javascripts/projects/commit/components/branches_dropdown.vue b/app/assets/javascripts/projects/commit/components/branches_dropdown.vue
index 3ecc3f1d1d3..36da4128450 100644
--- a/app/assets/javascripts/projects/commit/components/branches_dropdown.vue
+++ b/app/assets/javascripts/projects/commit/components/branches_dropdown.vue
@@ -68,6 +68,7 @@ export default {
autocomplete="off"
:debounce="250"
:placeholder="$options.i18n.searchPlaceholder"
+ data-testid="dropdown-search-box"
@input="searchTermChanged"
/>
<gl-dropdown-item
diff --git a/app/assets/javascripts/projects/commit/components/form_modal.vue b/app/assets/javascripts/projects/commit/components/form_modal.vue
index 6411b1ca921..22f9d04d87c 100644
--- a/app/assets/javascripts/projects/commit/components/form_modal.vue
+++ b/app/assets/javascripts/projects/commit/components/form_modal.vue
@@ -1,8 +1,9 @@
<script>
import { GlModal, GlForm, GlFormCheckbox, GlSprintf, GlFormGroup } from '@gitlab/ui';
import { mapActions, mapState } from 'vuex';
-import eventHub from '../event_hub';
import csrf from '~/lib/utils/csrf';
+import { BV_SHOW_MODAL } from '~/lib/utils/constants';
+import eventHub from '../event_hub';
import BranchesDropdown from './branches_dropdown.vue';
export default {
@@ -67,7 +68,7 @@ export default {
methods: {
...mapActions(['clearModal', 'setBranch', 'setSelectedBranch']),
show() {
- this.$root.$emit('bv::show::modal', this.modalId);
+ this.$root.$emit(BV_SHOW_MODAL, this.modalId);
},
handlePrimary() {
this.$refs.form.$el.submit();
diff --git a/app/assets/javascripts/projects/commit/components/form_trigger.vue b/app/assets/javascripts/projects/commit/components/form_trigger.vue
index e92854c1ac3..3561b5c2473 100644
--- a/app/assets/javascripts/projects/commit/components/form_trigger.vue
+++ b/app/assets/javascripts/projects/commit/components/form_trigger.vue
@@ -10,6 +10,9 @@ export default {
displayText: {
default: '',
},
+ testId: {
+ default: '',
+ },
},
props: {
openModal: {
@@ -26,7 +29,7 @@ export default {
</script>
<template>
- <gl-link data-is-link="true" data-testid="revert-commit-link" @click="showModal">
+ <gl-link data-is-link="true" :data-testid="testId" @click="showModal">
{{ displayText }}
</gl-link>
</template>
diff --git a/app/assets/javascripts/projects/commit/constants.js b/app/assets/javascripts/projects/commit/constants.js
index 233f43d56b9..b47c744e5fb 100644
--- a/app/assets/javascripts/projects/commit/constants.js
+++ b/app/assets/javascripts/projects/commit/constants.js
@@ -2,6 +2,10 @@ import { s__, __ } from '~/locale';
export const OPEN_REVERT_MODAL = 'openRevertModal';
export const REVERT_MODAL_ID = 'revert-commit-modal';
+export const REVERT_LINK_TEST_ID = 'revert-commit-link';
+export const OPEN_CHERRY_PICK_MODAL = 'openCherryPickModal';
+export const CHERRY_PICK_MODAL_ID = 'cherry-pick-commit-modal';
+export const CHERRY_PICK_LINK_TEST_ID = 'cherry-pick-commit-link';
export const I18N_MODAL = {
startMergeRequest: s__('ChangeTypeAction|Start a %{newMergeRequest} with these changes'),
@@ -20,6 +24,11 @@ export const I18N_REVERT_MODAL = {
actionPrimaryText: s__('ChangeTypeAction|Revert'),
};
+export const I18N_CHERRY_PICK_MODAL = {
+ branchLabel: s__('ChangeTypeAction|Pick into branch'),
+ actionPrimaryText: s__('ChangeTypeAction|Cherry-pick'),
+};
+
export const PREPENDED_MODAL_TEXT = s__(
'ChangeTypeAction|This will create a new commit in order to revert the existing changes.',
);
diff --git a/app/assets/javascripts/projects/commit/index.js b/app/assets/javascripts/projects/commit/index.js
new file mode 100644
index 00000000000..d5d7e62ce32
--- /dev/null
+++ b/app/assets/javascripts/projects/commit/index.js
@@ -0,0 +1,11 @@
+import initRevertCommitTrigger from './init_revert_commit_trigger';
+import initRevertCommitModal from './init_revert_commit_modal';
+import initCherryPickCommitTrigger from './init_cherry_pick_commit_trigger';
+import initCherryPickCommitModal from './init_cherry_pick_commit_modal';
+
+export default () => {
+ initRevertCommitModal();
+ initRevertCommitTrigger();
+ initCherryPickCommitModal();
+ initCherryPickCommitTrigger();
+};
diff --git a/app/assets/javascripts/projects/commit/init_cherry_pick_commit_modal.js b/app/assets/javascripts/projects/commit/init_cherry_pick_commit_modal.js
new file mode 100644
index 00000000000..33184d25c9b
--- /dev/null
+++ b/app/assets/javascripts/projects/commit/init_cherry_pick_commit_modal.js
@@ -0,0 +1,51 @@
+import Vue from 'vue';
+import { parseBoolean } from '~/lib/utils/common_utils';
+import CommitFormModal from './components/form_modal.vue';
+import createStore from './store';
+import {
+ I18N_MODAL,
+ I18N_CHERRY_PICK_MODAL,
+ OPEN_CHERRY_PICK_MODAL,
+ CHERRY_PICK_MODAL_ID,
+} from './constants';
+
+export default function initInviteMembersModal() {
+ const el = document.querySelector('.js-cherry-pick-commit-modal');
+ if (!el) {
+ return false;
+ }
+
+ const {
+ title,
+ endpoint,
+ branch,
+ pushCode,
+ branchCollaboration,
+ existingBranch,
+ branchesEndpoint,
+ } = el.dataset;
+
+ const store = createStore({
+ endpoint,
+ branchesEndpoint,
+ branch,
+ pushCode: parseBoolean(pushCode),
+ branchCollaboration: parseBoolean(branchCollaboration),
+ defaultBranch: branch,
+ modalTitle: title,
+ existingBranch,
+ });
+
+ return new Vue({
+ el,
+ store,
+ render: (createElement) =>
+ createElement(CommitFormModal, {
+ props: {
+ i18n: { ...I18N_CHERRY_PICK_MODAL, ...I18N_MODAL },
+ openModal: OPEN_CHERRY_PICK_MODAL,
+ modalId: CHERRY_PICK_MODAL_ID,
+ },
+ }),
+ });
+}
diff --git a/app/assets/javascripts/projects/commit/init_cherry_pick_commit_trigger.js b/app/assets/javascripts/projects/commit/init_cherry_pick_commit_trigger.js
new file mode 100644
index 00000000000..942451dc96a
--- /dev/null
+++ b/app/assets/javascripts/projects/commit/init_cherry_pick_commit_trigger.js
@@ -0,0 +1,20 @@
+import Vue from 'vue';
+import CommitFormTrigger from './components/form_trigger.vue';
+import { OPEN_CHERRY_PICK_MODAL, CHERRY_PICK_LINK_TEST_ID } from './constants';
+
+export default function initInviteMembersTrigger() {
+ const el = document.querySelector('.js-cherry-pick-commit-trigger');
+
+ if (!el) {
+ return false;
+ }
+
+ const { displayText } = el.dataset;
+
+ return new Vue({
+ el,
+ provide: { displayText, testId: CHERRY_PICK_LINK_TEST_ID },
+ render: (createElement) =>
+ createElement(CommitFormTrigger, { props: { openModal: OPEN_CHERRY_PICK_MODAL } }),
+ });
+}
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 ec0600cd25a..26695089e90 100644
--- a/app/assets/javascripts/projects/commit/init_revert_commit_modal.js
+++ b/app/assets/javascripts/projects/commit/init_revert_commit_modal.js
@@ -1,6 +1,6 @@
import Vue from 'vue';
-import CommitFormModal from './components/form_modal.vue';
import { parseBoolean } from '~/lib/utils/common_utils';
+import CommitFormModal from './components/form_modal.vue';
import createStore from './store';
import {
I18N_MODAL,
diff --git a/app/assets/javascripts/projects/commit/init_revert_commit_trigger.js b/app/assets/javascripts/projects/commit/init_revert_commit_trigger.js
index 0bb57f22663..dc5168524ca 100644
--- a/app/assets/javascripts/projects/commit/init_revert_commit_trigger.js
+++ b/app/assets/javascripts/projects/commit/init_revert_commit_trigger.js
@@ -1,6 +1,6 @@
import Vue from 'vue';
-import RevertCommitTrigger from './components/form_trigger.vue';
-import { OPEN_REVERT_MODAL } from './constants';
+import CommitFormTrigger from './components/form_trigger.vue';
+import { OPEN_REVERT_MODAL, REVERT_LINK_TEST_ID } from './constants';
export default function initInviteMembersTrigger() {
const el = document.querySelector('.js-revert-commit-trigger');
@@ -13,8 +13,8 @@ export default function initInviteMembersTrigger() {
return new Vue({
el,
- provide: { displayText },
+ provide: { displayText, testId: REVERT_LINK_TEST_ID },
render: (createElement) =>
- createElement(RevertCommitTrigger, { props: { openModal: OPEN_REVERT_MODAL } }),
+ createElement(CommitFormTrigger, { props: { openModal: OPEN_REVERT_MODAL } }),
});
}
diff --git a/app/assets/javascripts/projects/commit/store/actions.js b/app/assets/javascripts/projects/commit/store/actions.js
index 2ae0370d579..a9d1197e955 100644
--- a/app/assets/javascripts/projects/commit/store/actions.js
+++ b/app/assets/javascripts/projects/commit/store/actions.js
@@ -1,7 +1,7 @@
-import * as types from './mutation_types';
import axios from '~/lib/utils/axios_utils';
import createFlash from '~/flash';
import { PROJECT_BRANCHES_ERROR } from '../constants';
+import * as types from './mutation_types';
export const clearModal = ({ commit }) => {
commit(types.CLEAR_MODAL);
diff --git a/app/assets/javascripts/projects/commit_box/info/index.js b/app/assets/javascripts/projects/commit_box/info/index.js
index 254d178f013..cfb7dc8476d 100644
--- a/app/assets/javascripts/projects/commit_box/info/index.js
+++ b/app/assets/javascripts/projects/commit_box/info/index.js
@@ -1,7 +1,7 @@
-import { loadBranches } from './load_branches';
-import { initDetailsButton } from './init_details_button';
import { fetchCommitMergeRequests } from '~/commit_merge_requests';
import MiniPipelineGraph from '~/mini_pipeline_graph_dropdown';
+import { loadBranches } from './load_branches';
+import { initDetailsButton } from './init_details_button';
export const initCommitBoxInfo = (containerSelector = '.js-commit-box-info') => {
const containerEl = document.querySelector(containerSelector);
diff --git a/app/assets/javascripts/projects/commits/store/actions.js b/app/assets/javascripts/projects/commits/store/actions.js
index 359d81f32f7..832f6b904f4 100644
--- a/app/assets/javascripts/projects/commits/store/actions.js
+++ b/app/assets/javascripts/projects/commits/store/actions.js
@@ -1,9 +1,9 @@
import * as Sentry from '~/sentry/wrapper';
-import * as types from './mutation_types';
import axios from '~/lib/utils/axios_utils';
import { deprecatedCreateFlash as createFlash } from '~/flash';
import { __ } from '~/locale';
import { joinPaths } from '~/lib/utils/url_utility';
+import * as types from './mutation_types';
export default {
setInitialData({ commit }, data) {
diff --git a/app/assets/javascripts/projects/compare/components/app.vue b/app/assets/javascripts/projects/compare/components/app.vue
new file mode 100644
index 00000000000..05bd0f1370b
--- /dev/null
+++ b/app/assets/javascripts/projects/compare/components/app.vue
@@ -0,0 +1,89 @@
+<script>
+import { GlButton } from '@gitlab/ui';
+import csrf from '~/lib/utils/csrf';
+import RevisionDropdown from './revision_dropdown.vue';
+
+export default {
+ csrf,
+ components: {
+ RevisionDropdown,
+ GlButton,
+ },
+ props: {
+ projectCompareIndexPath: {
+ type: String,
+ required: true,
+ },
+ refsProjectPath: {
+ type: String,
+ required: true,
+ },
+ paramsFrom: {
+ type: String,
+ required: false,
+ default: null,
+ },
+ paramsTo: {
+ type: String,
+ required: false,
+ default: null,
+ },
+ projectMergeRequestPath: {
+ type: String,
+ required: true,
+ },
+ createMrPath: {
+ type: String,
+ required: true,
+ },
+ },
+ methods: {
+ onSubmit() {
+ this.$refs.form.submit();
+ },
+ },
+};
+</script>
+
+<template>
+ <form
+ ref="form"
+ class="form-inline js-requires-input js-signature-container"
+ method="POST"
+ :action="projectCompareIndexPath"
+ >
+ <input :value="$options.csrf.token" type="hidden" name="authenticity_token" />
+ <revision-dropdown
+ :refs-project-path="refsProjectPath"
+ revision-text="Source"
+ params-name="to"
+ :params-branch="paramsTo"
+ />
+ <div class="compare-ellipsis gl-display-inline" data-testid="ellipsis">...</div>
+ <revision-dropdown
+ :refs-project-path="refsProjectPath"
+ revision-text="Target"
+ params-name="from"
+ :params-branch="paramsFrom"
+ />
+ <gl-button category="primary" variant="success" class="gl-ml-3" @click="onSubmit">
+ {{ s__('CompareRevisions|Compare') }}
+ </gl-button>
+ <a
+ v-if="projectMergeRequestPath"
+ :href="projectMergeRequestPath"
+ data-testid="projectMrButton"
+ class="btn btn-default gl-button gl-ml-3"
+ >
+ {{ s__('CompareRevisions|View open merge request') }}
+ </a>
+ <a
+ v-else-if="createMrPath"
+ :href="createMrPath"
+ data-testid="createMrButton"
+ class="btn btn-default gl-button gl-ml-3"
+ >
+ {{ s__('CompareRevisions|Create merge request') }}
+ </a>
+ </form>
+</template>
diff --git a/app/assets/javascripts/projects/compare/components/revision_dropdown.vue b/app/assets/javascripts/projects/compare/components/revision_dropdown.vue
new file mode 100644
index 00000000000..f657f36322d
--- /dev/null
+++ b/app/assets/javascripts/projects/compare/components/revision_dropdown.vue
@@ -0,0 +1,145 @@
+<script>
+import { GlDropdown, GlDropdownItem, GlSearchBoxByType, GlDropdownSectionHeader } from '@gitlab/ui';
+import axios from '~/lib/utils/axios_utils';
+import createFlash from '~/flash';
+import { s__ } from '~/locale';
+
+export default {
+ components: {
+ GlDropdown,
+ GlDropdownItem,
+ GlDropdownSectionHeader,
+ GlSearchBoxByType,
+ },
+ props: {
+ refsProjectPath: {
+ type: String,
+ required: true,
+ },
+ revisionText: {
+ type: String,
+ required: true,
+ },
+ paramsName: {
+ type: String,
+ required: true,
+ },
+ paramsBranch: {
+ type: String,
+ required: false,
+ default: null,
+ },
+ },
+ data() {
+ return {
+ branches: [],
+ tags: [],
+ loading: true,
+ searchTerm: '',
+ selectedRevision: this.getDefaultBranch(),
+ };
+ },
+ computed: {
+ filteredBranches() {
+ return this.branches.filter((branch) =>
+ branch.toLowerCase().includes(this.searchTerm.toLowerCase()),
+ );
+ },
+ hasFilteredBranches() {
+ return this.filteredBranches.length;
+ },
+ filteredTags() {
+ return this.tags.filter((tag) => tag.toLowerCase().includes(this.searchTerm.toLowerCase()));
+ },
+ hasFilteredTags() {
+ return this.filteredTags.length;
+ },
+ },
+ mounted() {
+ this.fetchBranchesAndTags();
+ },
+ methods: {
+ fetchBranchesAndTags() {
+ const endpoint = this.refsProjectPath;
+
+ return axios
+ .get(endpoint)
+ .then(({ data }) => {
+ this.branches = data.Branches;
+ this.tags = data.Tags;
+ })
+ .catch(() => {
+ createFlash({
+ message: `${s__(
+ 'CompareRevisions|There was an error while updating the branch/tag list. Please try again.',
+ )}`,
+ });
+ })
+ .finally(() => {
+ this.loading = false;
+ });
+ },
+ getDefaultBranch() {
+ return this.paramsBranch || s__('CompareRevisions|Select branch/tag');
+ },
+ onClick(revision) {
+ this.selectedRevision = revision;
+ },
+ onSearchEnter() {
+ this.selectedRevision = this.searchTerm;
+ },
+ },
+};
+</script>
+
+<template>
+ <div class="form-group compare-form-group" :class="`js-compare-${paramsName}-dropdown`">
+ <div class="input-group inline-input-group">
+ <span class="input-group-prepend">
+ <div class="input-group-text">
+ {{ revisionText }}
+ </div>
+ </span>
+ <input type="hidden" :name="paramsName" :value="selectedRevision" />
+ <gl-dropdown
+ class="gl-flex-grow-1 gl-flex-basis-0 gl-min-w-0 gl-font-monospace"
+ toggle-class="form-control compare-dropdown-toggle js-compare-dropdown gl-min-w-0 gl-rounded-top-left-none! gl-rounded-bottom-left-none!"
+ :text="selectedRevision"
+ header-text="Select Git revision"
+ :loading="loading"
+ >
+ <template #header>
+ <gl-search-box-by-type
+ v-model.trim="searchTerm"
+ :placeholder="s__('CompareRevisions|Filter by Git revision')"
+ @keyup.enter="onSearchEnter"
+ />
+ </template>
+ <gl-dropdown-section-header v-if="hasFilteredBranches">
+ {{ s__('CompareRevisions|Branches') }}
+ </gl-dropdown-section-header>
+ <gl-dropdown-item
+ v-for="(branch, index) in filteredBranches"
+ :key="`branch${index}`"
+ is-check-item
+ :is-checked="selectedRevision === branch"
+ @click="onClick(branch)"
+ >
+ {{ branch }}
+ </gl-dropdown-item>
+ <gl-dropdown-section-header v-if="hasFilteredTags">
+ {{ s__('CompareRevisions|Tags') }}
+ </gl-dropdown-section-header>
+ <gl-dropdown-item
+ v-for="(tag, index) in filteredTags"
+ :key="`tag${index}`"
+ is-check-item
+ :is-checked="selectedRevision === tag"
+ @click="onClick(tag)"
+ >
+ {{ tag }}
+ </gl-dropdown-item>
+ </gl-dropdown>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/projects/compare/index.js b/app/assets/javascripts/projects/compare/index.js
new file mode 100644
index 00000000000..4337eecb667
--- /dev/null
+++ b/app/assets/javascripts/projects/compare/index.js
@@ -0,0 +1,33 @@
+import Vue from 'vue';
+import CompareApp from './components/app.vue';
+
+export default function init() {
+ const el = document.getElementById('js-compare-selector');
+ const {
+ refsProjectPath,
+ paramsFrom,
+ paramsTo,
+ projectCompareIndexPath,
+ projectMergeRequestPath,
+ createMrPath,
+ } = el.dataset;
+
+ return new Vue({
+ el,
+ components: {
+ CompareApp,
+ },
+ render(createElement) {
+ return createElement(CompareApp, {
+ props: {
+ refsProjectPath,
+ paramsFrom,
+ paramsTo,
+ projectCompareIndexPath,
+ projectMergeRequestPath,
+ createMrPath,
+ },
+ });
+ },
+ });
+}
diff --git a/app/assets/javascripts/projects/components/project_delete_button.vue b/app/assets/javascripts/projects/components/project_delete_button.vue
index 5429d51dae0..81d23a563e2 100644
--- a/app/assets/javascripts/projects/components/project_delete_button.vue
+++ b/app/assets/javascripts/projects/components/project_delete_button.vue
@@ -22,10 +22,10 @@ export default {
strings: {
alertTitle: __('You are about to permanently delete this project'),
alertBody: __(
- 'Once a project is permanently deleted it %{strongStart}cannot be recovered%{strongEnd}. Permanently deleting this project will %{strongStart}immediately delete%{strongEnd} its repositories and %{strongStart}all related resources%{strongEnd} including issues, merge requests etc.',
+ 'Once a project is permanently deleted, it %{strongStart}cannot be recovered%{strongEnd}. Permanently deleting this project will %{strongStart}immediately delete%{strongEnd} its repositories and %{strongStart}all related resources%{strongEnd}, including issues, merge requests etc.',
),
modalBody: __(
- "This action cannot be undone. You will lose this project's repository and all content: issues, merge requests, etc.",
+ "This action cannot be undone. You will lose this project's repository and all related resources, including issues, merge requests, etc.",
),
},
};
diff --git a/app/assets/javascripts/projects/experiment_new_project_creation/components/app.vue b/app/assets/javascripts/projects/experiment_new_project_creation/components/app.vue
index b54f7051806..ec208b97986 100644
--- a/app/assets/javascripts/projects/experiment_new_project_creation/components/app.vue
+++ b/app/assets/javascripts/projects/experiment_new_project_creation/components/app.vue
@@ -1,14 +1,14 @@
<script>
/* eslint-disable vue/no-v-html */
import { GlBreadcrumb, GlIcon, GlSafeHtmlDirective as SafeHtml } from '@gitlab/ui';
-import WelcomePage from './welcome.vue';
-import LegacyContainer from './legacy_container.vue';
import { __, s__ } from '~/locale';
import blankProjectIllustration from '../illustrations/blank-project.svg';
import createFromTemplateIllustration from '../illustrations/create-from-template.svg';
import importProjectIllustration from '../illustrations/import-project.svg';
import ciCdProjectIllustration from '../illustrations/ci-cd-project.svg';
+import LegacyContainer from './legacy_container.vue';
+import WelcomePage from './welcome.vue';
const BLANK_PANEL = 'blank_project';
const CI_CD_PANEL = 'cicd_for_external_repo';
diff --git a/app/assets/javascripts/projects/members/constants.js b/app/assets/javascripts/projects/members/constants.js
new file mode 100644
index 00000000000..a69a64fe882
--- /dev/null
+++ b/app/assets/javascripts/projects/members/constants.js
@@ -0,0 +1 @@
+export const PROJECT_MEMBER_BASE_PROPERTY_NAME = 'project_member';
diff --git a/app/assets/javascripts/projects/members/utils.js b/app/assets/javascripts/projects/members/utils.js
new file mode 100644
index 00000000000..cc27a43afa9
--- /dev/null
+++ b/app/assets/javascripts/projects/members/utils.js
@@ -0,0 +1,8 @@
+import { baseRequestFormatter } from '~/members/utils';
+import { MEMBER_ACCESS_LEVEL_PROPERTY_NAME } from '~/members/constants';
+import { PROJECT_MEMBER_BASE_PROPERTY_NAME } from './constants';
+
+export const projectMemberRequestFormatter = baseRequestFormatter(
+ PROJECT_MEMBER_BASE_PROPERTY_NAME,
+ MEMBER_ACCESS_LEVEL_PROPERTY_NAME,
+);
diff --git a/app/assets/javascripts/projects/pipelines/charts/components/app.vue b/app/assets/javascripts/projects/pipelines/charts/components/app.vue
index 7bb62cf4a73..7282ac85c70 100644
--- a/app/assets/javascripts/projects/pipelines/charts/components/app.vue
+++ b/app/assets/javascripts/projects/pipelines/charts/components/app.vue
@@ -1,44 +1,12 @@
<script>
-import { GlAlert, GlTabs, GlTab } from '@gitlab/ui';
-import { s__ } from '~/locale';
-import getPipelineCountByStatus from '../graphql/queries/get_pipeline_count_by_status.query.graphql';
-import getProjectPipelineStatistics from '../graphql/queries/get_project_pipeline_statistics.query.graphql';
+import { GlTabs, GlTab } from '@gitlab/ui';
+import { mergeUrlParams, updateHistory, getParameterValues } from '~/lib/utils/url_utility';
import PipelineCharts from './pipeline_charts.vue';
-import {
- DEFAULT,
- LOAD_ANALYTICS_FAILURE,
- LOAD_PIPELINES_FAILURE,
- PARSE_FAILURE,
- UNSUPPORTED_DATA,
-} from '../constants';
-
-const defaultAnalyticsValues = {
- weekPipelinesTotals: [],
- weekPipelinesLabels: [],
- weekPipelinesSuccessful: [],
- monthPipelinesLabels: [],
- monthPipelinesTotals: [],
- monthPipelinesSuccessful: [],
- yearPipelinesLabels: [],
- yearPipelinesTotals: [],
- yearPipelinesSuccessful: [],
- pipelineTimesLabels: [],
- pipelineTimesValues: [],
-};
-
-const defaultCountValues = {
- totalPipelines: {
- count: 0,
- },
- successfulPipelines: {
- count: 0,
- },
-};
+const charts = ['pipelines', 'deployments'];
export default {
components: {
- GlAlert,
GlTabs,
GlTab,
PipelineCharts,
@@ -50,171 +18,34 @@ export default {
type: Boolean,
default: false,
},
- projectPath: {
- type: String,
- default: '',
- },
},
data() {
+ const [chart] = getParameterValues('chart') || charts;
+ const tab = charts.indexOf(chart);
return {
- showFailureAlert: false,
- failureType: null,
- analytics: { ...defaultAnalyticsValues },
- counts: { ...defaultCountValues },
+ chart,
+ selectedTab: tab >= 0 ? tab : 0,
};
},
- apollo: {
- counts: {
- query: getPipelineCountByStatus,
- variables() {
- return {
- projectPath: this.projectPath,
- };
- },
- update(data) {
- return data?.project;
- },
- error() {
- this.reportFailure(LOAD_PIPELINES_FAILURE);
- },
- },
- analytics: {
- query: getProjectPipelineStatistics,
- variables() {
- return {
- projectPath: this.projectPath,
- };
- },
- update(data) {
- return data?.project?.pipelineAnalytics;
- },
- error() {
- this.reportFailure(LOAD_ANALYTICS_FAILURE);
- },
- },
- },
- computed: {
- failure() {
- switch (this.failureType) {
- case LOAD_ANALYTICS_FAILURE:
- return {
- text: this.$options.errorTexts[LOAD_ANALYTICS_FAILURE],
- variant: 'danger',
- };
- case PARSE_FAILURE:
- return {
- text: this.$options.errorTexts[PARSE_FAILURE],
- variant: 'danger',
- };
- case UNSUPPORTED_DATA:
- return {
- text: this.$options.errorTexts[UNSUPPORTED_DATA],
- variant: 'info',
- };
- default:
- return {
- text: this.$options.errorTexts[DEFAULT],
- variant: 'danger',
- };
- }
- },
- lastWeekChartData() {
- return {
- labels: this.analytics.weekPipelinesLabels,
- totals: this.analytics.weekPipelinesTotals,
- success: this.analytics.weekPipelinesSuccessful,
- };
- },
- lastMonthChartData() {
- return {
- labels: this.analytics.monthPipelinesLabels,
- totals: this.analytics.monthPipelinesTotals,
- success: this.analytics.monthPipelinesSuccessful,
- };
- },
- lastYearChartData() {
- return {
- labels: this.analytics.yearPipelinesLabels,
- totals: this.analytics.yearPipelinesTotals,
- success: this.analytics.yearPipelinesSuccessful,
- };
- },
- timesChartData() {
- return {
- labels: this.analytics.pipelineTimesLabels,
- values: this.analytics.pipelineTimesValues,
- };
- },
- successRatio() {
- const { successfulPipelines, failedPipelines } = this.counts;
- const successfulCount = successfulPipelines?.count;
- const failedCount = failedPipelines?.count;
- const ratio = (successfulCount / (successfulCount + failedCount)) * 100;
-
- return failedCount === 0 ? 100 : ratio;
- },
- formattedCounts() {
- const { totalPipelines, successfulPipelines, failedPipelines } = this.counts;
-
- return {
- total: totalPipelines?.count,
- success: successfulPipelines?.count,
- failed: failedPipelines?.count,
- successRatio: this.successRatio,
- };
- },
- },
methods: {
- hideAlert() {
- this.showFailureAlert = false;
+ onTabChange(index) {
+ this.selectedTab = index;
+ const path = mergeUrlParams({ chart: charts[index] }, window.location.pathname);
+ updateHistory({ url: path });
},
- reportFailure(type) {
- this.showFailureAlert = true;
- this.failureType = type;
- },
- },
- errorTexts: {
- [LOAD_ANALYTICS_FAILURE]: s__(
- 'PipelineCharts|An error has ocurred when retrieving the analytics data',
- ),
- [LOAD_PIPELINES_FAILURE]: s__(
- 'PipelineCharts|An error has ocurred when retrieving the pipelines data',
- ),
- [PARSE_FAILURE]: s__('PipelineCharts|There was an error parsing the data for the charts.'),
- [DEFAULT]: s__('PipelineCharts|An unknown error occurred while processing CI/CD analytics.'),
},
};
</script>
<template>
<div>
- <gl-alert v-if="showFailureAlert" :variant="failure.variant" @dismiss="hideAlert">{{
- failure.text
- }}</gl-alert>
- <gl-tabs v-if="shouldRenderDeploymentFrequencyCharts">
+ <gl-tabs v-if="shouldRenderDeploymentFrequencyCharts" :value="selectedTab" @input="onTabChange">
<gl-tab :title="__('Pipelines')">
- <pipeline-charts
- :counts="formattedCounts"
- :last-week="lastWeekChartData"
- :last-month="lastMonthChartData"
- :last-year="lastYearChartData"
- :times-chart="timesChartData"
- :loading="$apollo.queries.counts.loading"
- @report-failure="reportFailure"
- />
+ <pipeline-charts />
</gl-tab>
<gl-tab :title="__('Deployments')">
<deployment-frequency-charts />
</gl-tab>
</gl-tabs>
- <pipeline-charts
- v-else
- :counts="formattedCounts"
- :last-week="lastWeekChartData"
- :last-month="lastMonthChartData"
- :last-year="lastYearChartData"
- :times-chart="timesChartData"
- :loading="$apollo.queries.counts.loading"
- @report-failure="reportFailure"
- />
+ <pipeline-charts v-else />
</div>
</template>
diff --git a/app/assets/javascripts/projects/pipelines/charts/components/pipeline_charts.vue b/app/assets/javascripts/projects/pipelines/charts/components/pipeline_charts.vue
index bec4ab407f0..86433e1b59f 100644
--- a/app/assets/javascripts/projects/pipelines/charts/components/pipeline_charts.vue
+++ b/app/assets/javascripts/projects/pipelines/charts/components/pipeline_charts.vue
@@ -1,10 +1,13 @@
<script>
import dateFormat from 'dateformat';
import { GlColumnChart } from '@gitlab/ui/dist/charts';
-import { GlSkeletonLoader } from '@gitlab/ui';
+import { GlAlert, GlSkeletonLoader } from '@gitlab/ui';
import { __, s__, sprintf } from '~/locale';
import { getDateInPast } from '~/lib/utils/datetime_utility';
+import getPipelineCountByStatus from '../graphql/queries/get_pipeline_count_by_status.query.graphql';
+import getProjectPipelineStatistics from '../graphql/queries/get_project_pipeline_statistics.query.graphql';
import {
+ DEFAULT,
CHART_CONTAINER_HEIGHT,
CHART_DATE_FORMAT,
INNER_CHART_HEIGHT,
@@ -13,51 +16,167 @@ import {
X_AXIS_LABEL_ROTATION,
X_AXIS_TITLE_OFFSET,
PARSE_FAILURE,
+ LOAD_ANALYTICS_FAILURE,
+ LOAD_PIPELINES_FAILURE,
+ UNSUPPORTED_DATA,
} from '../constants';
import StatisticsList from './statistics_list.vue';
import CiCdAnalyticsAreaChart from './ci_cd_analytics_area_chart.vue';
+const defaultAnalyticsValues = {
+ weekPipelinesTotals: [],
+ weekPipelinesLabels: [],
+ weekPipelinesSuccessful: [],
+ monthPipelinesLabels: [],
+ monthPipelinesTotals: [],
+ monthPipelinesSuccessful: [],
+ yearPipelinesLabels: [],
+ yearPipelinesTotals: [],
+ yearPipelinesSuccessful: [],
+ pipelineTimesLabels: [],
+ pipelineTimesValues: [],
+};
+
+const defaultCountValues = {
+ totalPipelines: {
+ count: 0,
+ },
+ successfulPipelines: {
+ count: 0,
+ },
+};
+
export default {
components: {
+ GlAlert,
GlColumnChart,
GlSkeletonLoader,
StatisticsList,
CiCdAnalyticsAreaChart,
},
- props: {
+ inject: {
+ projectPath: {
+ type: String,
+ default: '',
+ },
+ },
+ data() {
+ return {
+ showFailureAlert: false,
+ failureType: null,
+ analytics: { ...defaultAnalyticsValues },
+ counts: { ...defaultCountValues },
+ };
+ },
+ apollo: {
counts: {
- required: true,
- type: Object,
+ query: getPipelineCountByStatus,
+ variables() {
+ return {
+ projectPath: this.projectPath,
+ };
+ },
+ update(data) {
+ return data?.project;
+ },
+ error() {
+ this.reportFailure(LOAD_PIPELINES_FAILURE);
+ },
},
- loading: {
- required: false,
- default: false,
- type: Boolean,
+ analytics: {
+ query: getProjectPipelineStatistics,
+ variables() {
+ return {
+ projectPath: this.projectPath,
+ };
+ },
+ update(data) {
+ return data?.project?.pipelineAnalytics;
+ },
+ error() {
+ this.reportFailure(LOAD_ANALYTICS_FAILURE);
+ },
},
- lastWeek: {
- required: true,
- type: Object,
+ },
+ computed: {
+ loading() {
+ return this.$apollo.queries.counts.loading;
},
- lastMonth: {
- required: true,
- type: Object,
+ failure() {
+ switch (this.failureType) {
+ case LOAD_ANALYTICS_FAILURE:
+ return {
+ text: this.$options.errorTexts[LOAD_ANALYTICS_FAILURE],
+ variant: 'danger',
+ };
+ case PARSE_FAILURE:
+ return {
+ text: this.$options.errorTexts[PARSE_FAILURE],
+ variant: 'danger',
+ };
+ case UNSUPPORTED_DATA:
+ return {
+ text: this.$options.errorTexts[UNSUPPORTED_DATA],
+ variant: 'info',
+ };
+ default:
+ return {
+ text: this.$options.errorTexts[DEFAULT],
+ variant: 'danger',
+ };
+ }
},
- lastYear: {
- required: true,
- type: Object,
+ lastWeekChartData() {
+ return {
+ labels: this.analytics.weekPipelinesLabels,
+ totals: this.analytics.weekPipelinesTotals,
+ success: this.analytics.weekPipelinesSuccessful,
+ };
},
- timesChart: {
- required: true,
- type: Object,
+ lastMonthChartData() {
+ return {
+ labels: this.analytics.monthPipelinesLabels,
+ totals: this.analytics.monthPipelinesTotals,
+ success: this.analytics.monthPipelinesSuccessful,
+ };
+ },
+ lastYearChartData() {
+ return {
+ labels: this.analytics.yearPipelinesLabels,
+ totals: this.analytics.yearPipelinesTotals,
+ success: this.analytics.yearPipelinesSuccessful,
+ };
+ },
+ timesChartData() {
+ return {
+ labels: this.analytics.pipelineTimesLabels,
+ values: this.analytics.pipelineTimesValues,
+ };
+ },
+ successRatio() {
+ const { successfulPipelines, failedPipelines } = this.counts;
+ const successfulCount = successfulPipelines?.count;
+ const failedCount = failedPipelines?.count;
+ const ratio = (successfulCount / (successfulCount + failedCount)) * 100;
+
+ return failedCount === 0 ? 100 : ratio;
+ },
+ formattedCounts() {
+ const { totalPipelines, successfulPipelines, failedPipelines } = this.counts;
+
+ return {
+ total: totalPipelines?.count,
+ success: successfulPipelines?.count,
+ failed: failedPipelines?.count,
+ successRatio: this.successRatio,
+ };
},
- },
- computed: {
areaCharts() {
const { lastWeek, lastMonth, lastYear } = this.$options.chartTitles;
const charts = [
- { title: lastWeek, data: this.lastWeek },
- { title: lastMonth, data: this.lastMonth },
- { title: lastYear, data: this.lastYear },
+ { title: lastWeek, data: this.lastWeekChartData },
+ { title: lastMonth, data: this.lastMonthChartData },
+ { title: lastYear, data: this.lastYearChartData },
];
let areaChartsData = [];
@@ -65,7 +184,7 @@ export default {
areaChartsData = charts.map(this.buildAreaChartData);
} catch {
areaChartsData = [];
- this.vm.$emit('report-failure', PARSE_FAILURE);
+ this.reportFailure(PARSE_FAILURE);
}
return areaChartsData;
@@ -74,12 +193,19 @@ export default {
return [
{
name: 'full',
- data: this.mergeLabelsAndValues(this.timesChart.labels, this.timesChart.values),
+ data: this.mergeLabelsAndValues(this.timesChartData.labels, this.timesChartData.values),
},
];
},
},
methods: {
+ hideAlert() {
+ this.showFailureAlert = false;
+ },
+ reportFailure(type) {
+ this.showFailureAlert = true;
+ this.failureType = type;
+ },
mergeLabelsAndValues(labels, values) {
return labels.map((label, index) => [label, values[index]]);
},
@@ -118,8 +244,19 @@ export default {
},
yAxis: {
name: s__('Pipeline|Pipelines'),
+ minInterval: 1,
},
},
+ errorTexts: {
+ [LOAD_ANALYTICS_FAILURE]: s__(
+ 'PipelineCharts|An error has ocurred when retrieving the analytics data',
+ ),
+ [LOAD_PIPELINES_FAILURE]: s__(
+ 'PipelineCharts|An error has ocurred when retrieving the pipelines data',
+ ),
+ [PARSE_FAILURE]: s__('PipelineCharts|There was an error parsing the data for the charts.'),
+ [DEFAULT]: s__('PipelineCharts|An unknown error occurred while processing CI/CD analytics.'),
+ },
get chartTitles() {
const today = dateFormat(new Date(), CHART_DATE_FORMAT);
const pastDate = (timeScale) =>
@@ -140,6 +277,9 @@ export default {
</script>
<template>
<div>
+ <gl-alert v-if="showFailureAlert" :variant="failure.variant" @dismiss="hideAlert">{{
+ failure.text
+ }}</gl-alert>
<div class="gl-mb-3">
<h3>{{ s__('PipelineCharts|CI / CD Analytics') }}</h3>
</div>
@@ -147,7 +287,7 @@ export default {
<div class="row">
<div class="col-md-6">
<gl-skeleton-loader v-if="loading" :lines="5" />
- <statistics-list v-else :counts="counts" />
+ <statistics-list v-else :counts="formattedCounts" />
</div>
<div v-if="!loading" class="col-md-6">
<strong>{{ __('Duration for the last 30 commits') }}</strong>
diff --git a/app/assets/javascripts/projects/settings/access_dropdown.js b/app/assets/javascripts/projects/settings/access_dropdown.js
index a62b5d423de..e5946de3efd 100644
--- a/app/assets/javascripts/projects/settings/access_dropdown.js
+++ b/app/assets/javascripts/projects/settings/access_dropdown.js
@@ -3,8 +3,8 @@ import { escape, find, countBy } from 'lodash';
import axios from '~/lib/utils/axios_utils';
import createFlash from '~/flash';
import { n__, s__, __, sprintf } from '~/locale';
-import { LEVEL_TYPES, LEVEL_ID_PROP, ACCESS_LEVELS, ACCESS_LEVEL_NONE } from './constants';
import initDeprecatedJQueryDropdown from '~/deprecated_jquery_dropdown';
+import { LEVEL_TYPES, LEVEL_ID_PROP, ACCESS_LEVELS, ACCESS_LEVEL_NONE } from './constants';
export default class AccessDropdown {
constructor(options) {
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 909f1afd9f6..7d570d01f85 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,61 +1,43 @@
<script>
import { GlAlert } from '@gitlab/ui';
import { __, sprintf } from '~/locale';
+import axios from '~/lib/utils/axios_utils';
import ServiceDeskSetting from './service_desk_setting.vue';
-import ServiceDeskService from '../services/service_desk_service';
-import eventHub from '../event_hub';
export default {
- name: 'ServiceDeskRoot',
components: {
GlAlert,
ServiceDeskSetting,
},
- props: {
+ inject: {
initialIsEnabled: {
- type: Boolean,
- required: true,
+ default: false,
},
endpoint: {
- type: String,
- required: true,
+ default: '',
},
- incomingEmail: {
- type: String,
- required: false,
+ initialIncomingEmail: {
default: '',
},
customEmail: {
- type: String,
- required: false,
default: '',
},
customEmailEnabled: {
- type: Boolean,
- required: false,
+ default: false,
},
selectedTemplate: {
- type: String,
- required: false,
default: '',
},
outgoingName: {
- type: String,
- required: false,
default: '',
},
projectKey: {
- type: String,
- required: false,
default: '',
},
templates: {
- type: Array,
- required: false,
- default: () => [],
+ default: [],
},
},
-
data() {
return {
isEnabled: this.initialIsEnabled,
@@ -63,28 +45,21 @@ export default {
isAlertShowing: false,
alertVariant: 'danger',
alertMessage: '',
+ incomingEmail: this.initialIncomingEmail,
updatedCustomEmail: this.customEmail,
};
},
-
- created() {
- eventHub.$on('serviceDeskEnabledCheckboxToggled', this.onEnableToggled);
- eventHub.$on('serviceDeskTemplateSave', this.onSaveTemplate);
- this.service = new ServiceDeskService(this.endpoint);
- },
-
- beforeDestroy() {
- eventHub.$off('serviceDeskEnabledCheckboxToggled', this.onEnableToggled);
- eventHub.$off('serviceDeskTemplateSave', this.onSaveTemplate);
- },
-
methods: {
onEnableToggled(isChecked) {
this.isEnabled = isChecked;
this.incomingEmail = '';
- this.service
- .toggleServiceDesk(isChecked)
+ const body = {
+ service_desk_enabled: isChecked,
+ };
+
+ return axios
+ .put(this.endpoint, body)
.then(({ data }) => {
const email = data.service_desk_address;
if (isChecked && !email) {
@@ -104,8 +79,16 @@ export default {
onSaveTemplate({ selectedTemplate, outgoingName, projectKey }) {
this.isTemplateSaving = true;
- this.service
- .updateTemplate({ selectedTemplate, outgoingName, projectKey }, this.isEnabled)
+
+ const body = {
+ issue_template_key: selectedTemplate,
+ outgoing_name: outgoingName,
+ project_key: projectKey,
+ service_desk_enabled: this.isEnabled,
+ };
+
+ return axios
+ .put(this.endpoint, body)
.then(({ data }) => {
this.updatedCustomEmail = data?.service_desk_address;
this.showAlert(__('Changes saved.'), 'success');
@@ -150,6 +133,8 @@ export default {
:initial-project-key="projectKey"
:templates="templates"
:is-template-saving="isTemplateSaving"
+ @save="onSaveTemplate"
+ @toggle="onEnableToggled"
/>
</div>
</template>
diff --git a/app/assets/javascripts/projects/settings_service_desk/components/service_desk_setting.vue b/app/assets/javascripts/projects/settings_service_desk/components/service_desk_setting.vue
index a850374fc88..39d9a6a4239 100644
--- a/app/assets/javascripts/projects/settings_service_desk/components/service_desk_setting.vue
+++ b/app/assets/javascripts/projects/settings_service_desk/components/service_desk_setting.vue
@@ -1,12 +1,9 @@
<script>
import { GlButton, GlFormSelect, GlToggle, GlLoadingIcon, GlSprintf } from '@gitlab/ui';
import { __ } from '~/locale';
-import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
-import eventHub from '../event_hub';
export default {
- name: 'ServiceDeskSetting',
components: {
ClipboardButton,
GlButton,
@@ -15,7 +12,6 @@ export default {
GlLoadingIcon,
GlSprintf,
},
- mixins: [glFeatureFlagsMixin()],
props: {
isEnabled: {
type: Boolean,
@@ -84,10 +80,10 @@ export default {
},
methods: {
onCheckboxToggle(isChecked) {
- eventHub.$emit('serviceDeskEnabledCheckboxToggled', isChecked);
+ this.$emit('toggle', isChecked);
},
onSaveTemplate() {
- eventHub.$emit('serviceDeskTemplateSave', {
+ this.$emit('save', {
selectedTemplate: this.selectedTemplate,
outgoingName: this.outgoingName,
projectKey: this.projectKey,
@@ -111,7 +107,11 @@ export default {
</label>
<div v-if="isEnabled" class="row mt-3">
<div class="col-md-9 mb-0">
- <strong id="incoming-email-describer" class="d-block mb-1">
+ <strong
+ id="incoming-email-describer"
+ class="gl-display-block gl-mb-1"
+ data-testid="incoming-email-describer"
+ >
{{ __('Email address to use for Support Desk') }}
</strong>
<template v-if="email">
@@ -128,11 +128,7 @@ export default {
disabled="true"
/>
<div class="input-group-append">
- <clipboard-button
- :title="__('Copy')"
- :text="email"
- css-class="input-group-text qa-clipboard-button"
- />
+ <clipboard-button :title="__('Copy')" :text="email" css-class="input-group-text" />
</div>
</div>
<span v-if="hasCustomEmail" class="form-text text-muted">
diff --git a/app/assets/javascripts/projects/settings_service_desk/event_hub.js b/app/assets/javascripts/projects/settings_service_desk/event_hub.js
deleted file mode 100644
index e31806ad199..00000000000
--- a/app/assets/javascripts/projects/settings_service_desk/event_hub.js
+++ /dev/null
@@ -1,3 +0,0 @@
-import createEventHub from '~/helpers/event_hub_factory';
-
-export default createEventHub();
diff --git a/app/assets/javascripts/projects/settings_service_desk/index.js b/app/assets/javascripts/projects/settings_service_desk/index.js
index 8f9828dd73d..f842ffaaa2b 100644
--- a/app/assets/javascripts/projects/settings_service_desk/index.js
+++ b/app/assets/javascripts/projects/settings_service_desk/index.js
@@ -3,43 +3,37 @@ import { parseBoolean } from '~/lib/utils/common_utils';
import ServiceDeskRoot from './components/service_desk_root.vue';
export default () => {
- const serviceDeskRootElement = document.querySelector('.js-service-desk-setting-root');
- if (serviceDeskRootElement) {
- // eslint-disable-next-line no-new
- new Vue({
- el: serviceDeskRootElement,
- components: {
- ServiceDeskRoot,
- },
- data() {
- const { dataset } = serviceDeskRootElement;
- return {
- initialIsEnabled: parseBoolean(dataset.enabled),
- endpoint: dataset.endpoint,
- incomingEmail: dataset.incomingEmail,
- customEmail: dataset.customEmail,
- customEmailEnabled: parseBoolean(dataset.customEmailEnabled),
- selectedTemplate: dataset.selectedTemplate,
- outgoingName: dataset.outgoingName,
- projectKey: dataset.projectKey,
- templates: JSON.parse(dataset.templates),
- };
- },
- render(createElement) {
- return createElement('service-desk-root', {
- props: {
- initialIsEnabled: this.initialIsEnabled,
- endpoint: this.endpoint,
- incomingEmail: this.incomingEmail,
- customEmail: this.customEmail,
- customEmailEnabled: this.customEmailEnabled,
- selectedTemplate: this.selectedTemplate,
- outgoingName: this.outgoingName,
- projectKey: this.projectKey,
- templates: this.templates,
- },
- });
- },
- });
+ const el = document.querySelector('.js-service-desk-setting-root');
+
+ if (!el) {
+ return false;
}
+
+ const {
+ customEmail,
+ customEmailEnabled,
+ enabled,
+ endpoint,
+ incomingEmail,
+ outgoingName,
+ projectKey,
+ selectedTemplate,
+ templates,
+ } = el.dataset;
+
+ return new Vue({
+ el,
+ provide: {
+ customEmail,
+ customEmailEnabled: parseBoolean(customEmailEnabled),
+ endpoint,
+ initialIncomingEmail: incomingEmail,
+ initialIsEnabled: parseBoolean(enabled),
+ outgoingName,
+ projectKey,
+ selectedTemplate,
+ templates: JSON.parse(templates),
+ },
+ render: (createElement) => createElement(ServiceDeskRoot),
+ });
};
diff --git a/app/assets/javascripts/projects/settings_service_desk/services/service_desk_service.js b/app/assets/javascripts/projects/settings_service_desk/services/service_desk_service.js
deleted file mode 100644
index b68c5bb876f..00000000000
--- a/app/assets/javascripts/projects/settings_service_desk/services/service_desk_service.js
+++ /dev/null
@@ -1,23 +0,0 @@
-import axios from '~/lib/utils/axios_utils';
-
-class ServiceDeskService {
- constructor(endpoint) {
- this.endpoint = endpoint;
- }
-
- toggleServiceDesk(enable) {
- return axios.put(this.endpoint, { service_desk_enabled: enable });
- }
-
- updateTemplate({ selectedTemplate, outgoingName, projectKey = '' }, isEnabled) {
- const body = {
- issue_template_key: selectedTemplate,
- outgoing_name: outgoingName,
- project_key: projectKey,
- service_desk_enabled: isEnabled,
- };
- return axios.put(this.endpoint, body);
- }
-}
-
-export default ServiceDeskService;
diff --git a/app/assets/javascripts/prometheus_metrics/custom_metrics.js b/app/assets/javascripts/prometheus_metrics/custom_metrics.js
index e891b8bf3b6..f829b080224 100644
--- a/app/assets/javascripts/prometheus_metrics/custom_metrics.js
+++ b/app/assets/javascripts/prometheus_metrics/custom_metrics.js
@@ -1,10 +1,10 @@
import $ from 'jquery';
import { escape, sortBy } from 'lodash';
-import PrometheusMetrics from './prometheus_metrics';
-import PANEL_STATE from './constants';
import axios from '~/lib/utils/axios_utils';
import { s__ } from '~/locale';
import { capitalizeFirstCharacter } from '~/lib/utils/text_utility';
+import PANEL_STATE from './constants';
+import PrometheusMetrics from './prometheus_metrics';
export default class CustomMetrics extends PrometheusMetrics {
constructor(wrapperSelector) {
diff --git a/app/assets/javascripts/prometheus_metrics/prometheus_metrics.js b/app/assets/javascripts/prometheus_metrics/prometheus_metrics.js
index 57f9cec9682..821de0560cd 100644
--- a/app/assets/javascripts/prometheus_metrics/prometheus_metrics.js
+++ b/app/assets/javascripts/prometheus_metrics/prometheus_metrics.js
@@ -2,8 +2,8 @@ import $ from 'jquery';
import { escape } from 'lodash';
import { s__, n__, sprintf } from '~/locale';
import axios from '../lib/utils/axios_utils';
-import PANEL_STATE from './constants';
import { backOff } from '../lib/utils/common_utils';
+import PANEL_STATE from './constants';
export default class PrometheusMetrics {
constructor(wrapperSelector) {
diff --git a/app/assets/javascripts/protected_branches/protected_branch_create.js b/app/assets/javascripts/protected_branches/protected_branch_create.js
index a5c7b18f709..a1f79084292 100644
--- a/app/assets/javascripts/protected_branches/protected_branch_create.js
+++ b/app/assets/javascripts/protected_branches/protected_branch_create.js
@@ -4,8 +4,8 @@ import axios from '~/lib/utils/axios_utils';
import AccessorUtilities from '~/lib/utils/accessor';
import { deprecatedCreateFlash as Flash } from '~/flash';
import CreateItemDropdown from '~/create_item_dropdown';
-import { ACCESS_LEVELS, LEVEL_TYPES } from './constants';
import { __ } from '~/locale';
+import { ACCESS_LEVELS, LEVEL_TYPES } from './constants';
export default class ProtectedBranchCreate {
constructor(options) {
diff --git a/app/assets/javascripts/protected_branches/protected_branch_edit.js b/app/assets/javascripts/protected_branches/protected_branch_edit.js
index f5f27b67c71..8316b10d49c 100644
--- a/app/assets/javascripts/protected_branches/protected_branch_edit.js
+++ b/app/assets/javascripts/protected_branches/protected_branch_edit.js
@@ -1,9 +1,9 @@
import { find } from 'lodash';
import AccessDropdown from '~/projects/settings/access_dropdown';
import axios from '~/lib/utils/axios_utils';
-import { ACCESS_LEVELS, LEVEL_TYPES } from './constants';
-import { deprecatedCreateFlash as flash } from '../flash';
import { __ } from '~/locale';
+import { deprecatedCreateFlash as flash } from '../flash';
+import { ACCESS_LEVELS, LEVEL_TYPES } from './constants';
export default class ProtectedBranchEdit {
constructor(options) {
diff --git a/app/assets/javascripts/protected_tags/protected_tag_create.js b/app/assets/javascripts/protected_tags/protected_tag_create.js
index eb44f0c67fd..e3f427b8408 100644
--- a/app/assets/javascripts/protected_tags/protected_tag_create.js
+++ b/app/assets/javascripts/protected_tags/protected_tag_create.js
@@ -1,7 +1,7 @@
import $ from 'jquery';
-import ProtectedTagAccessDropdown from './protected_tag_access_dropdown';
-import CreateItemDropdown from '../create_item_dropdown';
import { __ } from '~/locale';
+import CreateItemDropdown from '../create_item_dropdown';
+import ProtectedTagAccessDropdown from './protected_tag_access_dropdown';
export default class ProtectedTagCreate {
constructor() {
diff --git a/app/assets/javascripts/protected_tags/protected_tag_edit.js b/app/assets/javascripts/protected_tags/protected_tag_edit.js
index 157ac1c7ebd..59aa634872f 100644
--- a/app/assets/javascripts/protected_tags/protected_tag_edit.js
+++ b/app/assets/javascripts/protected_tags/protected_tag_edit.js
@@ -1,7 +1,7 @@
+import { __ } from '~/locale';
import { deprecatedCreateFlash as flash } from '../flash';
import axios from '../lib/utils/axios_utils';
import ProtectedTagAccessDropdown from './protected_tag_access_dropdown';
-import { __ } from '~/locale';
export default class ProtectedTagEdit {
constructor(options) {
diff --git a/app/assets/javascripts/ref/stores/mutations.js b/app/assets/javascripts/ref/stores/mutations.js
index 75026a40175..4dc73dabfe2 100644
--- a/app/assets/javascripts/ref/stores/mutations.js
+++ b/app/assets/javascripts/ref/stores/mutations.js
@@ -1,7 +1,7 @@
-import * as types from './mutation_types';
-import { X_TOTAL_HEADER } from '../constants';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import httpStatusCodes from '~/lib/utils/http_status';
+import { X_TOTAL_HEADER } from '../constants';
+import * as types from './mutation_types';
export default {
[types.SET_PROJECT_ID](state, projectId) {
diff --git a/app/assets/javascripts/registry/explorer/components/delete_image.vue b/app/assets/javascripts/registry/explorer/components/delete_image.vue
new file mode 100644
index 00000000000..62e287429ea
--- /dev/null
+++ b/app/assets/javascripts/registry/explorer/components/delete_image.vue
@@ -0,0 +1,77 @@
+<script>
+import { produce } from 'immer';
+import deleteContainerRepositoryMutation from '../graphql/mutations/delete_container_repository.mutation.graphql';
+import getContainerRepositoryDetailsQuery from '../graphql/queries/get_container_repository_details.query.graphql';
+
+import { GRAPHQL_PAGE_SIZE } from '../constants/index';
+
+export default {
+ props: {
+ id: {
+ type: String,
+ required: false,
+ default: null,
+ },
+ useUpdateFn: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ },
+ methods: {
+ updateImageStatus(store, { data: { destroyContainerRepository } }) {
+ const variables = {
+ id: this.id,
+ first: GRAPHQL_PAGE_SIZE,
+ };
+ const sourceData = store.readQuery({
+ query: getContainerRepositoryDetailsQuery,
+ variables,
+ });
+
+ const data = produce(sourceData, (draftState) => {
+ // eslint-disable-next-line no-param-reassign
+ draftState.containerRepository.status =
+ destroyContainerRepository.containerRepository.status;
+ });
+
+ store.writeQuery({
+ query: getContainerRepositoryDetailsQuery,
+ variables,
+ data,
+ });
+ },
+ doDelete() {
+ this.$emit('start');
+ return this.$apollo
+ .mutate({
+ mutation: deleteContainerRepositoryMutation,
+ variables: {
+ id: this.id,
+ },
+ update: this.useUpdateFn ? this.updateImageStatus : undefined,
+ })
+ .then(({ data }) => {
+ if (data?.destroyContainerRepository?.errors[0]) {
+ this.$emit('error', data?.destroyContainerRepository?.errors);
+ return;
+ }
+ this.$emit('success');
+ })
+ .catch((e) => {
+ // note: we are adding an array to follow the same format of the error raised above
+ this.$emit('error', [e]);
+ })
+ .finally(() => {
+ this.$emit('end');
+ });
+ },
+ },
+ render() {
+ if (this.$scopedSlots?.default) {
+ return this.$scopedSlots.default({ doDelete: this.doDelete });
+ }
+ return null;
+ },
+};
+</script>
diff --git a/app/assets/javascripts/registry/explorer/components/details_page/empty_state.vue b/app/assets/javascripts/registry/explorer/components/details_page/empty_state.vue
new file mode 100644
index 00000000000..a16d95a6b30
--- /dev/null
+++ b/app/assets/javascripts/registry/explorer/components/details_page/empty_state.vue
@@ -0,0 +1,44 @@
+<script>
+import { GlEmptyState } from '@gitlab/ui';
+import {
+ NO_TAGS_TITLE,
+ NO_TAGS_MESSAGE,
+ MISSING_OR_DELETED_IMAGE_TITLE,
+ MISSING_OR_DELETED_IMAGE_MESSAGE,
+} from '../../constants/index';
+
+export default {
+ components: {
+ GlEmptyState,
+ },
+ props: {
+ noContainersImage: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ isEmptyImage: {
+ type: Boolean,
+ default: false,
+ required: false,
+ },
+ },
+ computed: {
+ title() {
+ return this.isEmptyImage ? MISSING_OR_DELETED_IMAGE_TITLE : NO_TAGS_TITLE;
+ },
+ description() {
+ return this.isEmptyImage ? MISSING_OR_DELETED_IMAGE_MESSAGE : NO_TAGS_MESSAGE;
+ },
+ },
+};
+</script>
+
+<template>
+ <gl-empty-state
+ :title="title"
+ :svg-path="noContainersImage"
+ :description="description"
+ class="gl-mx-auto gl-my-0"
+ />
+</template>
diff --git a/app/assets/javascripts/registry/explorer/components/details_page/empty_tags_state.vue b/app/assets/javascripts/registry/explorer/components/details_page/empty_tags_state.vue
deleted file mode 100644
index 0c684d124d5..00000000000
--- a/app/assets/javascripts/registry/explorer/components/details_page/empty_tags_state.vue
+++ /dev/null
@@ -1,33 +0,0 @@
-<script>
-import { GlEmptyState } from '@gitlab/ui';
-import {
- EMPTY_IMAGE_REPOSITORY_TITLE,
- EMPTY_IMAGE_REPOSITORY_MESSAGE,
-} from '../../constants/index';
-
-export default {
- components: {
- GlEmptyState,
- },
- props: {
- noContainersImage: {
- type: String,
- required: false,
- default: '',
- },
- },
- i18n: {
- EMPTY_IMAGE_REPOSITORY_TITLE,
- EMPTY_IMAGE_REPOSITORY_MESSAGE,
- },
-};
-</script>
-
-<template>
- <gl-empty-state
- :title="$options.i18n.EMPTY_IMAGE_REPOSITORY_TITLE"
- :svg-path="noContainersImage"
- :description="$options.i18n.EMPTY_IMAGE_REPOSITORY_MESSAGE"
- class="gl-mx-auto gl-my-0"
- />
-</template>
diff --git a/app/assets/javascripts/registry/explorer/components/details_page/tags_list.vue b/app/assets/javascripts/registry/explorer/components/details_page/tags_list.vue
index 1e0736c4a53..9a4ae41d275 100644
--- a/app/assets/javascripts/registry/explorer/components/details_page/tags_list.vue
+++ b/app/assets/javascripts/registry/explorer/components/details_page/tags_list.vue
@@ -1,7 +1,7 @@
<script>
import { GlButton } from '@gitlab/ui';
-import TagsListRow from './tags_list_row.vue';
import { REMOVE_TAGS_BUTTON_TITLE, TAGS_LIST_TITLE } from '../../constants/index';
+import TagsListRow from './tags_list_row.vue';
export default {
name: 'TagsList',
diff --git a/app/assets/javascripts/registry/explorer/components/details_page/tags_list_row.vue b/app/assets/javascripts/registry/explorer/components/details_page/tags_list_row.vue
index 2e4a489f2cb..ed4fea0c88e 100644
--- a/app/assets/javascripts/registry/explorer/components/details_page/tags_list_row.vue
+++ b/app/assets/javascripts/registry/explorer/components/details_page/tags_list_row.vue
@@ -6,8 +6,8 @@ import { numberToHumanSize } from '~/lib/utils/number_utils';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
import { formatDate } from '~/lib/utils/datetime_utility';
import ListItem from '~/vue_shared/components/registry/list_item.vue';
-import DeleteButton from '../delete_button.vue';
import DetailsRow from '~/vue_shared/components/registry/details_row.vue';
+import DeleteButton from '../delete_button.vue';
import {
REMOVE_TAG_BUTTON_TITLE,
DIGEST_LABEL,
diff --git a/app/assets/javascripts/registry/explorer/constants/details.js b/app/assets/javascripts/registry/explorer/constants/details.js
index b5627352857..bb084e813af 100644
--- a/app/assets/javascripts/registry/explorer/constants/details.js
+++ b/app/assets/javascripts/registry/explorer/constants/details.js
@@ -1,4 +1,5 @@
import { s__, __ } from '~/locale';
+import { helpPagePath } from '~/helpers/help_page_helper';
// Translations strings
export const DETAILS_PAGE_TITLE = s__('ContainerRegistry|%{imageName} tags');
@@ -32,18 +33,30 @@ export const CONFIGURATION_DETAILS_ROW_TEST = s__(
export const REMOVE_TAG_BUTTON_TITLE = s__('ContainerRegistry|Remove tag');
export const REMOVE_TAGS_BUTTON_TITLE = s__('ContainerRegistry|Delete selected');
+
export const REMOVE_TAG_CONFIRMATION_TEXT = s__(
`ContainerRegistry|You are about to remove %{item}. Are you sure?`,
);
export const REMOVE_TAGS_CONFIRMATION_TEXT = s__(
`ContainerRegistry|You are about to remove %{item} tags. Are you sure?`,
);
-export const EMPTY_IMAGE_REPOSITORY_TITLE = s__('ContainerRegistry|This image has no active tags');
-export const EMPTY_IMAGE_REPOSITORY_MESSAGE = s__(
+export const NO_TAGS_TITLE = s__('ContainerRegistry|This image has no active tags');
+export const NO_TAGS_MESSAGE = s__(
`ContainerRegistry|The last tag related to this image was recently removed.
This empty image and any associated data will be automatically removed as part of the regular Garbage Collection process.
If you have any questions, contact your administrator.`,
);
+
+export const MISSING_OR_DELETED_IMAGE_TITLE = s__(
+ 'ContainerRegistry|The image repository could not be found.',
+);
+export const MISSING_OR_DELETED_IMAGE_MESSAGE = s__(
+ 'ContainerRegistry|The requested image repository does not exist or has been deleted. If you think this is an error, try refreshing the page.',
+);
+export const MISSING_OR_DELETE_IMAGE_BREADCRUMB = s__(
+ 'ContainerRegistry|Image repository not found',
+);
+
export const ADMIN_GARBAGE_COLLECTION_TIP = s__(
'ContainerRegistry|Remember to run %{docLinkStart}garbage collection%{docLinkEnd} to remove the stale data from storage.',
);
@@ -76,6 +89,29 @@ export const CLEANUP_DISABLED_TOOLTIP = s__(
'ContainerRegistry|Cleanup is disabled for this project',
);
+export const DETAILS_DELETE_IMAGE_ERROR_MESSAGE = s__(
+ 'ContainerRegistry|Something went wrong while scheduling the image for deletion.',
+);
+
+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.',
+);
+
+export const SCHEDULED_FOR_DELETION_STATUS_TITLE = s__(
+ 'ContainerRegistry|Image repository will be deleted',
+);
+export const SCHEDULED_FOR_DELETION_STATUS_MESSAGE = s__(
+ 'ContainerRegistry|This image repository will be deleted. %{linkStart}Learn more.%{linkEnd}',
+);
+
+export const FAILED_DELETION_STATUS_TITLE = s__(
+ 'ContainerRegistry|Image repository deletion failed',
+);
+export const FAILED_DELETION_STATUS_MESSAGE = s__(
+ 'ContainerRegistry|This image repository has failed to be deleted',
+);
+
// Parameters
export const DEFAULT_PAGE = 1;
@@ -85,15 +121,39 @@ export const ALERT_SUCCESS_TAG = 'success_tag';
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 DELETE_SCHEDULED = 'DELETE_SCHEDULED';
+export const DELETE_FAILED = 'DELETE_FAILED';
export const ALERT_MESSAGES = {
[ALERT_SUCCESS_TAG]: DELETE_TAG_SUCCESS_MESSAGE,
[ALERT_DANGER_TAG]: DELETE_TAG_ERROR_MESSAGE,
[ALERT_SUCCESS_TAGS]: DELETE_TAGS_SUCCESS_MESSAGE,
[ALERT_DANGER_TAGS]: DELETE_TAGS_ERROR_MESSAGE,
+ [ALERT_DANGER_IMAGE]: DETAILS_DELETE_IMAGE_ERROR_MESSAGE,
};
export const UNFINISHED_STATUS = 'UNFINISHED';
export const UNSCHEDULED_STATUS = 'UNSCHEDULED';
export const SCHEDULED_STATUS = 'SCHEDULED';
export const ONGOING_STATUS = 'ONGOING';
+
+export const IMAGE_STATUS_TITLES = {
+ [DELETE_SCHEDULED]: SCHEDULED_FOR_DELETION_STATUS_TITLE,
+ [DELETE_FAILED]: FAILED_DELETION_STATUS_TITLE,
+};
+
+export const IMAGE_STATUS_MESSAGES = {
+ [DELETE_SCHEDULED]: SCHEDULED_FOR_DELETION_STATUS_MESSAGE,
+ [DELETE_FAILED]: FAILED_DELETION_STATUS_MESSAGE,
+};
+
+export const IMAGE_STATUS_ALERT_TYPE = {
+ [DELETE_SCHEDULED]: 'info',
+ [DELETE_FAILED]: 'warning',
+};
+
+export const PACKAGE_DELETE_HELP_PAGE_PATH = helpPagePath('user/packages/container_registry', {
+ anchor: 'delete-images',
+});
diff --git a/app/assets/javascripts/registry/explorer/index.js b/app/assets/javascripts/registry/explorer/index.js
index a3890ab5c42..0ddbe50441b 100644
--- a/app/assets/javascripts/registry/explorer/index.js
+++ b/app/assets/javascripts/registry/explorer/index.js
@@ -29,7 +29,14 @@ export default () => {
return null;
}
- const { endpoint, expirationPolicy, isGroupPage, isAdmin, ...config } = el.dataset;
+ const {
+ endpoint,
+ expirationPolicy,
+ isGroupPage,
+ isAdmin,
+ showUnfinishedTagCleanupCallout,
+ ...config
+ } = el.dataset;
// This is a mini state to help the breadcrumb have the correct name in the details page
const breadCrumbState = Vue.observable({
@@ -57,6 +64,7 @@ export default () => {
expirationPolicy: expirationPolicy ? JSON.parse(expirationPolicy) : undefined,
isGroupPage: parseBoolean(isGroupPage),
isAdmin: parseBoolean(isAdmin),
+ showUnfinishedTagCleanupCallout: parseBoolean(showUnfinishedTagCleanupCallout),
},
/* eslint-disable @gitlab/require-i18n-strings */
dockerBuildCommand: `docker build -t ${config.repositoryUrl} .`,
diff --git a/app/assets/javascripts/registry/explorer/pages/details.vue b/app/assets/javascripts/registry/explorer/pages/details.vue
index 0894fd6fcfa..0cf83d9c62e 100644
--- a/app/assets/javascripts/registry/explorer/pages/details.vue
+++ b/app/assets/javascripts/registry/explorer/pages/details.vue
@@ -3,6 +3,7 @@ import { GlKeysetPagination, GlResizeObserverDirective } from '@gitlab/ui';
import { GlBreakpointInstance } from '@gitlab/ui/dist/utils';
import createFlash from '~/flash';
import Tracking from '~/tracking';
+import axios from '~/lib/utils/axios_utils';
import { joinPaths } from '~/lib/utils/url_utility';
import DeleteAlert from '../components/details_page/delete_alert.vue';
import PartialCleanupAlert from '../components/details_page/partial_cleanup_alert.vue';
@@ -10,7 +11,7 @@ import DeleteModal from '../components/details_page/delete_modal.vue';
import DetailsHeader from '../components/details_page/details_header.vue';
import TagsList from '../components/details_page/tags_list.vue';
import TagsLoader from '../components/details_page/tags_loader.vue';
-import EmptyTagsState from '../components/details_page/empty_tags_state.vue';
+import EmptyState from '../components/details_page/empty_state.vue';
import getContainerRepositoryDetailsQuery from '../graphql/queries/get_container_repository_details.query.graphql';
import deleteContainerRepositoryTagsMutation from '../graphql/mutations/delete_container_repository_tags.mutation.graphql';
@@ -23,6 +24,7 @@ import {
GRAPHQL_PAGE_SIZE,
FETCH_IMAGES_LIST_ERROR_MESSAGE,
UNFINISHED_STATUS,
+ MISSING_OR_DELETE_IMAGE_BREADCRUMB,
} from '../constants/index';
export default {
@@ -35,7 +37,7 @@ export default {
DeleteModal,
TagsList,
TagsLoader,
- EmptyTagsState,
+ EmptyState,
},
directives: {
GlResizeObserver: GlResizeObserverDirective,
@@ -53,7 +55,7 @@ export default {
},
result({ data }) {
this.tagsPageInfo = data.containerRepository?.tags?.pageInfo;
- this.breadCrumbState.updateName(data.containerRepository?.name);
+ this.updateBreadcrumb();
},
error() {
createFlash({ message: FETCH_IMAGES_LIST_ERROR_MESSAGE });
@@ -68,7 +70,7 @@ export default {
isMobile: false,
mutationLoading: false,
deleteAlertType: null,
- dismissPartialCleanupWarning: false,
+ hidePartialCleanupWarning: false,
};
},
computed: {
@@ -86,8 +88,9 @@ export default {
},
showPartialCleanupWarning() {
return (
+ this.config.showUnfinishedTagCleanupCallout &&
this.image?.expirationPolicyCleanupStatus === UNFINISHED_STATUS &&
- !this.dismissPartialCleanupWarning
+ !this.hidePartialCleanupWarning
);
},
tracking() {
@@ -99,8 +102,15 @@ export default {
showPagination() {
return this.tagsPageInfo.hasPreviousPage || this.tagsPageInfo.hasNextPage;
},
+ hasNoTags() {
+ return this.tags.length === 0;
+ },
},
methods: {
+ updateBreadcrumb() {
+ const name = this.image?.name || MISSING_OR_DELETE_IMAGE_BREADCRUMB;
+ this.breadCrumbState.updateName(name);
+ },
deleteTags(toBeDeleted) {
this.itemsToBeDeleted = this.tags.filter((tag) => toBeDeleted[tag.name]);
this.track('click_button');
@@ -168,51 +178,60 @@ export default {
});
}
},
+ dismissPartialCleanupWarning() {
+ this.hidePartialCleanupWarning = true;
+ axios.post(this.config.userCalloutsPath, {
+ feature_name: this.config.userCalloutId,
+ });
+ },
},
};
</script>
<template>
<div v-gl-resize-observer="handleResize" class="gl-my-3">
- <delete-alert
- v-model="deleteAlertType"
- :garbage-collection-help-page-path="config.garbageCollectionHelpPagePath"
- :is-admin="config.isAdmin"
- class="gl-my-2"
- />
+ <template v-if="image">
+ <delete-alert
+ v-model="deleteAlertType"
+ :garbage-collection-help-page-path="config.garbageCollectionHelpPagePath"
+ :is-admin="config.isAdmin"
+ class="gl-my-2"
+ />
- <partial-cleanup-alert
- v-if="showPartialCleanupWarning"
- :run-cleanup-policies-help-page-path="config.runCleanupPoliciesHelpPagePath"
- :cleanup-policies-help-page-path="config.cleanupPoliciesHelpPagePath"
- @dismiss="dismissPartialCleanupWarning = true"
- />
+ <partial-cleanup-alert
+ v-if="showPartialCleanupWarning"
+ :run-cleanup-policies-help-page-path="config.runCleanupPoliciesHelpPagePath"
+ :cleanup-policies-help-page-path="config.cleanupPoliciesHelpPagePath"
+ @dismiss="dismissPartialCleanupWarning"
+ />
- <details-header :image="image" :metadata-loading="isLoading" />
+ <details-header :image="image" :metadata-loading="isLoading" />
- <tags-loader v-if="isLoading" />
- <template v-else>
- <empty-tags-state v-if="tags.length === 0" :no-containers-image="config.noContainersImage" />
+ <tags-loader v-if="isLoading" />
<template v-else>
- <tags-list :tags="tags" :is-mobile="isMobile" @delete="deleteTags" />
- <div class="gl-display-flex gl-justify-content-center">
- <gl-keyset-pagination
- v-if="showPagination"
- :has-next-page="tagsPageInfo.hasNextPage"
- :has-previous-page="tagsPageInfo.hasPreviousPage"
- class="gl-mt-3"
- @prev="fetchPreviousPage"
- @next="fetchNextPage"
- />
- </div>
+ <empty-state v-if="hasNoTags" :no-containers-image="config.noContainersImage" />
+ <template v-else>
+ <tags-list :tags="tags" :is-mobile="isMobile" @delete="deleteTags" />
+ <div class="gl-display-flex gl-justify-content-center">
+ <gl-keyset-pagination
+ v-if="showPagination"
+ :has-next-page="tagsPageInfo.hasNextPage"
+ :has-previous-page="tagsPageInfo.hasPreviousPage"
+ class="gl-mt-3"
+ @prev="fetchPreviousPage"
+ @next="fetchNextPage"
+ />
+ </div>
+ </template>
</template>
- </template>
- <delete-modal
- ref="deleteModal"
- :items-to-be-deleted="itemsToBeDeleted"
- @confirmDelete="handleDelete"
- @cancel="track('cancel_delete')"
- />
+ <delete-modal
+ ref="deleteModal"
+ :items-to-be-deleted="itemsToBeDeleted"
+ @confirmDelete="handleDelete"
+ @cancel="track('cancel_delete')"
+ />
+ </template>
+ <empty-state v-else is-empty-image :no-containers-image="config.noContainersImage" />
</div>
</template>
diff --git a/app/assets/javascripts/registry/explorer/pages/list.vue b/app/assets/javascripts/registry/explorer/pages/list.vue
index 336a997d629..d362b79789b 100644
--- a/app/assets/javascripts/registry/explorer/pages/list.vue
+++ b/app/assets/javascripts/registry/explorer/pages/list.vue
@@ -14,9 +14,9 @@ import getContainerRepositoriesQuery from 'shared_queries/container_registry/get
import Tracking from '~/tracking';
import createFlash from '~/flash';
import RegistryHeader from '../components/list_page/registry_header.vue';
+import DeleteImage from '../components/delete_image.vue';
import getContainerRepositoriesDetails from '../graphql/queries/get_container_repositories_details.query.graphql';
-import deleteContainerRepositoryMutation from '../graphql/mutations/delete_container_repository.mutation.graphql';
import {
DELETE_IMAGE_SUCCESS_MESSAGE,
@@ -60,6 +60,7 @@ export default {
GlSkeletonLoader,
GlSearchBoxByClick,
RegistryHeader,
+ DeleteImage,
},
directives: {
GlTooltip: GlTooltipDirective,
@@ -179,30 +180,6 @@ export default {
this.itemToDelete = item;
this.$refs.deleteModal.show();
},
- handleDeleteImage() {
- this.track('confirm_delete');
- this.mutationLoading = true;
- return this.$apollo
- .mutate({
- mutation: deleteContainerRepositoryMutation,
- variables: {
- id: this.itemToDelete.id,
- },
- })
- .then(({ data }) => {
- if (data?.destroyContainerRepository?.errors[0]) {
- this.deleteAlertType = 'danger';
- } else {
- this.deleteAlertType = 'success';
- }
- })
- .catch(() => {
- this.deleteAlertType = 'danger';
- })
- .finally(() => {
- this.mutationLoading = false;
- });
- },
dismissDeleteAlert() {
this.deleteAlertType = null;
this.itemToDelete = {};
@@ -250,6 +227,10 @@ export default {
});
}
},
+ startDelete() {
+ this.track('confirm_delete');
+ this.mutationLoading = true;
+ },
},
};
</script>
@@ -358,23 +339,32 @@ export default {
</template>
</template>
- <gl-modal
- ref="deleteModal"
- modal-id="delete-image-modal"
- ok-variant="danger"
- @ok="handleDeleteImage"
- @cancel="track('cancel_delete')"
+ <delete-image
+ :id="itemToDelete.id"
+ @start="startDelete"
+ @error="deleteAlertType = 'danger'"
+ @success="deleteAlertType = 'success'"
+ @end="mutationLoading = false"
>
- <template #modal-title>{{ $options.i18n.REMOVE_REPOSITORY_LABEL }}</template>
- <p>
- <gl-sprintf :message="$options.i18n.REMOVE_REPOSITORY_MODAL_TEXT">
- <template #title>
- <b>{{ itemToDelete.path }}</b>
- </template>
- </gl-sprintf>
- </p>
- <template #modal-ok>{{ __('Remove') }}</template>
- </gl-modal>
+ <template #default="{ doDelete }">
+ <gl-modal
+ ref="deleteModal"
+ modal-id="delete-image-modal"
+ :action-primary="{ text: __('Remove'), attributes: { variant: 'danger' } }"
+ @primary="doDelete"
+ @cancel="track('cancel_delete')"
+ >
+ <template #modal-title>{{ $options.i18n.REMOVE_REPOSITORY_LABEL }}</template>
+ <p>
+ <gl-sprintf :message="$options.i18n.REMOVE_REPOSITORY_MODAL_TEXT">
+ <template #title>
+ <b>{{ itemToDelete.path }}</b>
+ </template>
+ </gl-sprintf>
+ </p>
+ </gl-modal>
+ </template>
+ </delete-image>
</template>
</div>
</template>
diff --git a/app/assets/javascripts/related_issues/components/add_issuable_form.vue b/app/assets/javascripts/related_issues/components/add_issuable_form.vue
index 6fbae95094a..8d412e14b47 100644
--- a/app/assets/javascripts/related_issues/components/add_issuable_form.vue
+++ b/app/assets/javascripts/related_issues/components/add_issuable_form.vue
@@ -1,7 +1,6 @@
<script>
import { GlFormGroup, GlFormRadioGroup, GlButton } from '@gitlab/ui';
import { __ } from '~/locale';
-import RelatedIssuableInput from './related_issuable_input.vue';
import { mergeUrlParams } from '~/lib/utils/url_utility';
import {
@@ -11,6 +10,7 @@ import {
addRelatedIssueErrorMap,
addRelatedItemErrorMap,
} from '../constants';
+import RelatedIssuableInput from './related_issuable_input.vue';
export default {
name: 'AddIssuableForm',
diff --git a/app/assets/javascripts/related_issues/components/related_issuable_input.vue b/app/assets/javascripts/related_issues/components/related_issuable_input.vue
index a124b055e19..2dc56c3110b 100644
--- a/app/assets/javascripts/related_issues/components/related_issuable_input.vue
+++ b/app/assets/javascripts/related_issues/components/related_issuable_input.vue
@@ -1,13 +1,13 @@
<script>
import $ from 'jquery';
import GfmAutoComplete from 'ee_else_ce/gfm_auto_complete';
-import issueToken from './issue_token.vue';
import {
autoCompleteTextMap,
inputPlaceholderConfidentialTextMap,
inputPlaceholderTextMap,
issuableTypesMap,
} from '../constants';
+import issueToken from './issue_token.vue';
const SPACE_FACTOR = 1;
diff --git a/app/assets/javascripts/related_issues/components/related_issues_block.vue b/app/assets/javascripts/related_issues/components/related_issues_block.vue
index 2591e3e7f48..c042f0eef5f 100644
--- a/app/assets/javascripts/related_issues/components/related_issues_block.vue
+++ b/app/assets/javascripts/related_issues/components/related_issues_block.vue
@@ -1,13 +1,13 @@
<script>
import { GlLink, GlIcon, GlButton } from '@gitlab/ui';
-import AddIssuableForm from './add_issuable_form.vue';
-import RelatedIssuesList from './related_issues_list.vue';
import {
issuableIconMap,
issuableQaClassMap,
linkedIssueTypesMap,
linkedIssueTypesTextMap,
} from '../constants';
+import AddIssuableForm from './add_issuable_form.vue';
+import RelatedIssuesList from './related_issues_list.vue';
export default {
name: 'RelatedIssuesBlock',
@@ -146,7 +146,7 @@ export default {
class="gl-display-flex gl-align-items-center gl-ml-2 gl-text-gray-500"
:aria-label="__('Read more about related issues')"
>
- <gl-icon name="question" :size="12" role="text" />
+ <gl-icon name="question" :size="12" />
</gl-link>
<div class="gl-display-inline-flex">
diff --git a/app/assets/javascripts/related_issues/components/related_issues_root.vue b/app/assets/javascripts/related_issues/components/related_issues_root.vue
index a81edcf141c..1ad9d8b7986 100644
--- a/app/assets/javascripts/related_issues/components/related_issues_root.vue
+++ b/app/assets/javascripts/related_issues/components/related_issues_root.vue
@@ -25,7 +25,6 @@ Your caret can stop touching a `rawReference` can happen in a variety of ways:
*/
import { deprecatedCreateFlash as Flash } from '~/flash';
import { __ } from '~/locale';
-import RelatedIssuesBlock from './related_issues_block.vue';
import RelatedIssuesStore from '../stores/related_issues_store';
import RelatedIssuesService from '../services/related_issues_service';
import {
@@ -35,6 +34,7 @@ import {
issuableTypesMap,
PathIdSeparator,
} from '../constants';
+import RelatedIssuesBlock from './related_issues_block.vue';
export default {
name: 'RelatedIssuesRoot',
diff --git a/app/assets/javascripts/releases/components/app_edit_new.vue b/app/assets/javascripts/releases/components/app_edit_new.vue
index 8d1bc44cba0..6e159939103 100644
--- a/app/assets/javascripts/releases/components/app_edit_new.vue
+++ b/app/assets/javascripts/releases/components/app_edit_new.vue
@@ -5,8 +5,8 @@ import { __ } from '~/locale';
import MarkdownField from '~/vue_shared/components/markdown/field.vue';
import { BACK_URL_PARAM } from '~/releases/constants';
import { getParameterByName } from '~/lib/utils/common_utils';
-import AssetLinksForm from './asset_links_form.vue';
import MilestoneCombobox from '~/milestones/components/milestone_combobox.vue';
+import AssetLinksForm from './asset_links_form.vue';
import TagField from './tag_field.vue';
export default {
diff --git a/app/assets/javascripts/releases/components/asset_links_form.vue b/app/assets/javascripts/releases/components/asset_links_form.vue
index 331cc8ade6c..b6595ea78be 100644
--- a/app/assets/javascripts/releases/components/asset_links_form.vue
+++ b/app/assets/javascripts/releases/components/asset_links_form.vue
@@ -10,8 +10,8 @@ import {
GlFormInput,
GlFormSelect,
} from '@gitlab/ui';
-import { DEFAULT_ASSET_LINK_TYPE, ASSET_LINK_TYPE } from '../constants';
import { s__ } from '~/locale';
+import { DEFAULT_ASSET_LINK_TYPE, ASSET_LINK_TYPE } from '../constants';
export default {
name: 'AssetLinksForm',
diff --git a/app/assets/javascripts/releases/components/release_block_assets.vue b/app/assets/javascripts/releases/components/release_block_assets.vue
index 36929f559b5..1761f4360d1 100644
--- a/app/assets/javascripts/releases/components/release_block_assets.vue
+++ b/app/assets/javascripts/releases/components/release_block_assets.vue
@@ -1,8 +1,8 @@
<script>
import { GlTooltipDirective, GlLink, GlButton, GlCollapse, GlIcon, GlBadge } from '@gitlab/ui';
import { difference, get } from 'lodash';
-import { ASSET_LINK_TYPE } from '../constants';
import { __, s__, sprintf } from '~/locale';
+import { ASSET_LINK_TYPE } from '../constants';
export default {
name: 'ReleaseBlockAssets',
diff --git a/app/assets/javascripts/releases/stores/modules/detail/actions.js b/app/assets/javascripts/releases/stores/modules/detail/actions.js
index 127646826a6..54b08ac0dbc 100644
--- a/app/assets/javascripts/releases/stores/modules/detail/actions.js
+++ b/app/assets/javascripts/releases/stores/modules/detail/actions.js
@@ -1,4 +1,3 @@
-import * as types from './mutation_types';
import api from '~/api';
import { deprecatedCreateFlash as createFlash } from '~/flash';
import { s__ } from '~/locale';
@@ -10,6 +9,7 @@ import {
convertOneReleaseGraphQLResponse,
} from '~/releases/util';
import oneReleaseQuery from '~/releases/queries/one_release.query.graphql';
+import * as types from './mutation_types';
export const initializeRelease = ({ commit, dispatch, getters }) => {
if (getters.isExistingRelease) {
diff --git a/app/assets/javascripts/releases/stores/modules/detail/mutations.js b/app/assets/javascripts/releases/stores/modules/detail/mutations.js
index 8f4bfbc9b86..cf282f9ab2c 100644
--- a/app/assets/javascripts/releases/stores/modules/detail/mutations.js
+++ b/app/assets/javascripts/releases/stores/modules/detail/mutations.js
@@ -1,6 +1,6 @@
import { uniqueId, cloneDeep } from 'lodash';
-import * as types from './mutation_types';
import { DEFAULT_ASSET_LINK_TYPE } from '../../../constants';
+import * as types from './mutation_types';
const findReleaseLink = (release, id) => {
return release.assets.links.find((l) => l.id === id);
diff --git a/app/assets/javascripts/releases/stores/modules/list/actions.js b/app/assets/javascripts/releases/stores/modules/list/actions.js
index 4c4f6e19a93..9d56323b3c7 100644
--- a/app/assets/javascripts/releases/stores/modules/list/actions.js
+++ b/app/assets/javascripts/releases/stores/modules/list/actions.js
@@ -1,4 +1,3 @@
-import * as types from './mutation_types';
import { deprecatedCreateFlash as createFlash } from '~/flash';
import { __ } from '~/locale';
import api from '~/api';
@@ -10,6 +9,7 @@ import {
import allReleasesQuery from '~/releases/queries/all_releases.query.graphql';
import { gqClient, convertAllReleasesGraphQLResponse } from '../../../util';
import { PAGE_SIZE } from '../../../constants';
+import * as types from './mutation_types';
/**
* Gets a paginated list of releases from the server
diff --git a/app/assets/javascripts/reports/accessibility_report/store/getters.js b/app/assets/javascripts/reports/accessibility_report/store/getters.js
index 8f8eec11c7f..20506b1bfd1 100644
--- a/app/assets/javascripts/reports/accessibility_report/store/getters.js
+++ b/app/assets/javascripts/reports/accessibility_report/store/getters.js
@@ -1,5 +1,5 @@
-import { LOADING, ERROR, SUCCESS, STATUS_FAILED } from '../../constants';
import { s__, n__ } from '~/locale';
+import { LOADING, ERROR, SUCCESS, STATUS_FAILED } from '../../constants';
export const groupedSummaryText = (state) => {
if (state.isLoading) {
diff --git a/app/assets/javascripts/reports/codequality_report/grouped_codequality_reports_app.vue b/app/assets/javascripts/reports/codequality_report/grouped_codequality_reports_app.vue
index 5c8f31d7da0..c1de1ab25c1 100644
--- a/app/assets/javascripts/reports/codequality_report/grouped_codequality_reports_app.vue
+++ b/app/assets/javascripts/reports/codequality_report/grouped_codequality_reports_app.vue
@@ -2,6 +2,7 @@
import { mapState, mapActions, mapGetters } from 'vuex';
import { componentNames } from '~/reports/components/issue_body';
import { s__, sprintf } from '~/locale';
+import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import ReportSection from '~/reports/components/report_section.vue';
import createStore from './store';
@@ -11,6 +12,7 @@ export default {
components: {
ReportSection,
},
+ mixins: [glFeatureFlagsMixin()],
props: {
headPath: {
type: String,
@@ -30,6 +32,11 @@ export default {
required: false,
default: null,
},
+ codequalityReportsPath: {
+ type: String,
+ required: false,
+ default: '',
+ },
codequalityHelpPath: {
type: String,
required: true,
@@ -37,7 +44,7 @@ export default {
},
componentNames,
computed: {
- ...mapState(['newIssues', 'resolvedIssues']),
+ ...mapState(['newIssues', 'resolvedIssues', 'hasError', 'statusReason']),
...mapGetters([
'hasCodequalityIssues',
'codequalityStatus',
@@ -51,10 +58,11 @@ export default {
headPath: this.headPath,
baseBlobPath: this.baseBlobPath,
headBlobPath: this.headBlobPath,
+ reportsPath: this.codequalityReportsPath,
helpPath: this.codequalityHelpPath,
});
- this.fetchReports();
+ this.fetchReports(this.glFeatures.codequalityBackendComparison);
},
methods: {
...mapActions(['fetchReports', 'setPaths']),
@@ -80,5 +88,7 @@ export default {
:popover-options="codequalityPopover"
:show-report-section-status-icon="false"
class="js-codequality-widget mr-widget-border-top mr-report"
- />
+ >
+ <template v-if="hasError" #sub-heading>{{ statusReason }}</template>
+ </report-section>
</template>
diff --git a/app/assets/javascripts/reports/codequality_report/store/actions.js b/app/assets/javascripts/reports/codequality_report/store/actions.js
index e5fb5caca2e..ddd1747899f 100644
--- a/app/assets/javascripts/reports/codequality_report/store/actions.js
+++ b/app/assets/javascripts/reports/codequality_report/store/actions.js
@@ -4,9 +4,20 @@ import { parseCodeclimateMetrics, doCodeClimateComparison } from './utils/codequ
export const setPaths = ({ commit }, paths) => commit(types.SET_PATHS, paths);
-export const fetchReports = ({ state, dispatch, commit }) => {
+export const fetchReports = ({ state, dispatch, commit }, diffFeatureFlagEnabled) => {
commit(types.REQUEST_REPORTS);
+ if (diffFeatureFlagEnabled) {
+ return axios
+ .get(state.reportsPath)
+ .then(({ data }) => {
+ return dispatch('receiveReportsSuccess', {
+ newIssues: parseCodeclimateMetrics(data.new_errors, state.headBlobPath),
+ resolvedIssues: parseCodeclimateMetrics(data.resolved_errors, state.baseBlobPath),
+ });
+ })
+ .catch((error) => dispatch('receiveReportsError', error));
+ }
if (!state.basePath) {
return dispatch('receiveReportsError');
}
@@ -18,13 +29,13 @@ export const fetchReports = ({ state, dispatch, commit }) => {
),
)
.then((data) => dispatch('receiveReportsSuccess', data))
- .catch(() => dispatch('receiveReportsError'));
+ .catch((error) => dispatch('receiveReportsError', error));
};
export const receiveReportsSuccess = ({ commit }, data) => {
commit(types.RECEIVE_REPORTS_SUCCESS, data);
};
-export const receiveReportsError = ({ commit }) => {
- commit(types.RECEIVE_REPORTS_ERROR);
+export const receiveReportsError = ({ commit }, error) => {
+ commit(types.RECEIVE_REPORTS_ERROR, error);
};
diff --git a/app/assets/javascripts/reports/codequality_report/store/getters.js b/app/assets/javascripts/reports/codequality_report/store/getters.js
index e017bab976c..f26a41d94c0 100644
--- a/app/assets/javascripts/reports/codequality_report/store/getters.js
+++ b/app/assets/javascripts/reports/codequality_report/store/getters.js
@@ -1,6 +1,6 @@
-import { LOADING, ERROR, SUCCESS } from '../../constants';
import { sprintf, __, s__, n__ } from '~/locale';
import { spriteIcon } from '~/lib/utils/common_utils';
+import { LOADING, ERROR, SUCCESS } from '../../constants';
export const hasCodequalityIssues = (state) =>
Boolean(state.newIssues?.length || state.resolvedIssues?.length);
diff --git a/app/assets/javascripts/reports/codequality_report/store/mutations.js b/app/assets/javascripts/reports/codequality_report/store/mutations.js
index 7ef4f3ce2db..095e6637966 100644
--- a/app/assets/javascripts/reports/codequality_report/store/mutations.js
+++ b/app/assets/javascripts/reports/codequality_report/store/mutations.js
@@ -6,6 +6,7 @@ export default {
state.headPath = paths.headPath;
state.baseBlobPath = paths.baseBlobPath;
state.headBlobPath = paths.headBlobPath;
+ state.reportsPath = paths.reportsPath;
state.helpPath = paths.helpPath;
},
[types.REQUEST_REPORTS](state) {
@@ -13,12 +14,14 @@ export default {
},
[types.RECEIVE_REPORTS_SUCCESS](state, data) {
state.hasError = false;
+ state.statusReason = '';
state.isLoading = false;
state.newIssues = data.newIssues;
state.resolvedIssues = data.resolvedIssues;
},
- [types.RECEIVE_REPORTS_ERROR](state) {
+ [types.RECEIVE_REPORTS_ERROR](state, error) {
state.isLoading = false;
state.hasError = true;
+ state.statusReason = error?.response?.data?.status_reason;
},
};
diff --git a/app/assets/javascripts/reports/codequality_report/store/state.js b/app/assets/javascripts/reports/codequality_report/store/state.js
index 38ab53b432e..b39ff4f9d66 100644
--- a/app/assets/javascripts/reports/codequality_report/store/state.js
+++ b/app/assets/javascripts/reports/codequality_report/store/state.js
@@ -1,12 +1,14 @@
export default () => ({
basePath: null,
headPath: null,
+ reportsPath: null,
baseBlobPath: null,
headBlobPath: null,
isLoading: false,
hasError: false,
+ statusReason: '',
newIssues: [],
resolvedIssues: [],
diff --git a/app/assets/javascripts/reports/codequality_report/store/utils/codequality_comparison.js b/app/assets/javascripts/reports/codequality_report/store/utils/codequality_comparison.js
index fd775f52f7d..b252c8c9817 100644
--- a/app/assets/javascripts/reports/codequality_report/store/utils/codequality_comparison.js
+++ b/app/assets/javascripts/reports/codequality_report/store/utils/codequality_comparison.js
@@ -3,8 +3,10 @@ import CodeQualityComparisonWorker from '../../workers/codequality_comparison_wo
export const parseCodeclimateMetrics = (issues = [], path = '') => {
return issues.map((issue) => {
const parsedIssue = {
- ...issue,
name: issue.description,
+ path: issue.file_path,
+ urlPath: `${path}/${issue.file_path}#L${issue.line}`,
+ ...issue,
};
if (issue?.location?.path) {
diff --git a/app/assets/javascripts/reports/components/grouped_test_reports_app.vue b/app/assets/javascripts/reports/components/grouped_test_reports_app.vue
index bf1868d427e..dc09c3d175e 100644
--- a/app/assets/javascripts/reports/components/grouped_test_reports_app.vue
+++ b/app/assets/javascripts/reports/components/grouped_test_reports_app.vue
@@ -3,19 +3,19 @@ import { mapActions, mapGetters, mapState } from 'vuex';
import { once } from 'lodash';
import { GlButton } from '@gitlab/ui';
import { sprintf, s__ } from '~/locale';
-import { componentNames } from './issue_body';
-import ReportSection from './report_section.vue';
-import SummaryRow from './summary_row.vue';
-import IssuesList from './issues_list.vue';
-import Modal from './modal.vue';
-import createStore from '../store';
import Tracking from '~/tracking';
+import createStore from '../store';
import {
summaryTextBuilder,
reportTextBuilder,
statusIcon,
recentFailuresTextBuilder,
} from '../store/utils';
+import { componentNames } from './issue_body';
+import ReportSection from './report_section.vue';
+import SummaryRow from './summary_row.vue';
+import IssuesList from './issues_list.vue';
+import Modal from './modal.vue';
export default {
name: 'GroupedTestReportsApp',
diff --git a/app/assets/javascripts/reports/components/issue_body.js b/app/assets/javascripts/reports/components/issue_body.js
index 1e6dc4f8b78..a0349506b69 100644
--- a/app/assets/javascripts/reports/components/issue_body.js
+++ b/app/assets/javascripts/reports/components/issue_body.js
@@ -1,6 +1,6 @@
-import TestIssueBody from './test_issue_body.vue';
import AccessibilityIssueBody from '../accessibility_report/components/accessibility_issue_body.vue';
import CodequalityIssueBody from '../codequality_report/components/codequality_issue_body.vue';
+import TestIssueBody from './test_issue_body.vue';
export const components = {
AccessibilityIssueBody,
diff --git a/app/assets/javascripts/reports/components/report_section.vue b/app/assets/javascripts/reports/components/report_section.vue
index 0e9975ea81f..9d0631fbc01 100644
--- a/app/assets/javascripts/reports/components/report_section.vue
+++ b/app/assets/javascripts/reports/components/report_section.vue
@@ -2,8 +2,8 @@
import { __ } from '~/locale';
import StatusIcon from '~/vue_merge_request_widget/components/mr_widget_status_icon.vue';
import Popover from '~/vue_shared/components/help_popover.vue';
-import IssuesList from './issues_list.vue';
import { status, SLOT_SUCCESS, SLOT_LOADING, SLOT_ERROR } from '../constants';
+import IssuesList from './issues_list.vue';
export default {
name: 'ReportSection',
diff --git a/app/assets/javascripts/reports/store/actions.js b/app/assets/javascripts/reports/store/actions.js
index 301fdce7989..a872cfa7e26 100644
--- a/app/assets/javascripts/reports/store/actions.js
+++ b/app/assets/javascripts/reports/store/actions.js
@@ -1,8 +1,8 @@
import Visibility from 'visibilityjs';
import axios from '../../lib/utils/axios_utils';
import Poll from '../../lib/utils/poll';
-import * as types from './mutation_types';
import httpStatusCodes from '../../lib/utils/http_status';
+import * as types from './mutation_types';
export const setEndpoint = ({ commit }, endpoint) => commit(types.SET_ENDPOINT, endpoint);
diff --git a/app/assets/javascripts/repository/components/tree_content.vue b/app/assets/javascripts/repository/components/tree_content.vue
index 7fe6863d006..336237abd8a 100644
--- a/app/assets/javascripts/repository/components/tree_content.vue
+++ b/app/assets/javascripts/repository/components/tree_content.vue
@@ -2,11 +2,11 @@
import filesQuery from 'shared_queries/repository/files.query.graphql';
import { deprecatedCreateFlash as createFlash } from '~/flash';
import { __ } from '../../locale';
-import FileTable from './table/index.vue';
import getRefMixin from '../mixins/get_ref';
import projectPathQuery from '../queries/project_path.query.graphql';
-import FilePreview from './preview/index.vue';
import { readmeFile } from '../utils/readme';
+import FilePreview from './preview/index.vue';
+import FileTable from './table/index.vue';
const LIMIT = 1000;
const PAGE_SIZE = 100;
diff --git a/app/assets/javascripts/repository/index.js b/app/assets/javascripts/repository/index.js
index f56b141fe5c..aec3d90a6d7 100644
--- a/app/assets/javascripts/repository/index.js
+++ b/app/assets/javascripts/repository/index.js
@@ -1,17 +1,17 @@
import Vue from 'vue';
+import initWebIdeLink from '~/pages/projects/shared/web_ide_link';
import { escapeFileUrl } from '../lib/utils/url_utility';
+import { parseBoolean } from '../lib/utils/common_utils';
+import { __ } from '../locale';
import createRouter from './router';
import App from './components/app.vue';
import Breadcrumbs from './components/breadcrumbs.vue';
import LastCommit from './components/last_commit.vue';
import TreeActionLink from './components/tree_action_link.vue';
-import initWebIdeLink from '~/pages/projects/shared/web_ide_link';
import DirectoryDownloadLinks from './components/directory_download_links.vue';
import apolloProvider from './graphql';
import { setTitle } from './utils/title';
import { updateFormAction } from './utils/dom';
-import { parseBoolean } from '../lib/utils/common_utils';
-import { __ } from '../locale';
export default function setupVueRepositoryList() {
const el = document.getElementById('js-tree-list');
diff --git a/app/assets/javascripts/repository/mixins/preload.js b/app/assets/javascripts/repository/mixins/preload.js
index c1607866941..ffc260ec84f 100644
--- a/app/assets/javascripts/repository/mixins/preload.js
+++ b/app/assets/javascripts/repository/mixins/preload.js
@@ -1,6 +1,6 @@
import filesQuery from 'shared_queries/repository/files.query.graphql';
-import getRefMixin from './get_ref';
import projectPathQuery from '../queries/project_path.query.graphql';
+import getRefMixin from './get_ref';
export default {
mixins: [getRefMixin],
diff --git a/app/assets/javascripts/repository/pages/index.vue b/app/assets/javascripts/repository/pages/index.vue
index 29786bf4ec8..0e53235779c 100644
--- a/app/assets/javascripts/repository/pages/index.vue
+++ b/app/assets/javascripts/repository/pages/index.vue
@@ -1,6 +1,6 @@
<script>
-import TreePage from './tree.vue';
import { updateElementsVisibility } from '../utils/dom';
+import TreePage from './tree.vue';
export default {
components: {
diff --git a/app/assets/javascripts/right_sidebar.js b/app/assets/javascripts/right_sidebar.js
index b9bc799fb0b..6d46decf978 100644
--- a/app/assets/javascripts/right_sidebar.js
+++ b/app/assets/javascripts/right_sidebar.js
@@ -2,10 +2,10 @@
import $ from 'jquery';
import Cookies from 'js-cookie';
+import { fixTitle, hide } from '~/tooltips';
import { deprecatedCreateFlash as flash } from './flash';
import axios from './lib/utils/axios_utils';
import { sprintf, s__, __ } from './locale';
-import { fixTitle, hide } from '~/tooltips';
function Sidebar() {
this.toggleTodo = this.toggleTodo.bind(this);
diff --git a/app/assets/javascripts/search/highlight_blob_search_result.js b/app/assets/javascripts/search/highlight_blob_search_result.js
index 3c3ac3582d0..c553d5b14a0 100644
--- a/app/assets/javascripts/search/highlight_blob_search_result.js
+++ b/app/assets/javascripts/search/highlight_blob_search_result.js
@@ -1,7 +1,7 @@
-export default () => {
+export default (search = '') => {
const highlightLineClass = 'hll';
const contentBody = document.getElementById('content-body');
- const searchTerm = contentBody.querySelector('.js-search-input').value.toLowerCase();
+ const searchTerm = search.toLowerCase();
const blobs = contentBody.querySelectorAll('.blob-result');
blobs.forEach((blob) => {
diff --git a/app/assets/javascripts/search/index.js b/app/assets/javascripts/search/index.js
index d2bb1ccfc44..3050b628cd5 100644
--- a/app/assets/javascripts/search/index.js
+++ b/app/assets/javascripts/search/index.js
@@ -1,14 +1,25 @@
+import setHighlightClass from 'ee_else_ce/search/highlight_blob_search_result';
+import Project from '~/pages/projects/project';
+import refreshCounts from '~/pages/search/show/refresh_counts';
import { queryToObject } from '~/lib/utils/url_utility';
import createStore from './store';
import { initTopbar } from './topbar';
import { initSidebar } from './sidebar';
+import { initSearchSort } from './sort';
export const initSearchApp = () => {
// Similar to url_utility.decodeUrlParameter
// Our query treats + as %20. This replaces the query + symbols with %20.
const sanitizedSearch = window.location.search.replace(/\+/g, '%20');
- const store = createStore({ query: queryToObject(sanitizedSearch) });
+ const query = queryToObject(sanitizedSearch);
+
+ const store = createStore({ query });
initTopbar(store);
initSidebar(store);
+ initSearchSort(store);
+
+ setHighlightClass(query.search); // Code Highlighting
+ refreshCounts(); // Other Scope Tab Counts
+ Project.initRefSwitcher(); // Code Search Branch Picker
};
diff --git a/app/assets/javascripts/search/sort/components/app.vue b/app/assets/javascripts/search/sort/components/app.vue
new file mode 100644
index 00000000000..6ce76467c1e
--- /dev/null
+++ b/app/assets/javascripts/search/sort/components/app.vue
@@ -0,0 +1,103 @@
+<script>
+import { mapState, mapActions } from 'vuex';
+import {
+ GlButtonGroup,
+ GlButton,
+ GlDropdown,
+ GlDropdownItem,
+ GlTooltipDirective,
+} from '@gitlab/ui';
+import { SORT_DIRECTION_UI } from '../constants';
+
+export default {
+ name: 'GlobalSearchSort',
+ components: {
+ GlButtonGroup,
+ GlButton,
+ GlDropdown,
+ GlDropdownItem,
+ },
+ directives: {
+ GlTooltip: GlTooltipDirective,
+ },
+ props: {
+ searchSortOptions: {
+ type: Array,
+ required: true,
+ },
+ },
+ computed: {
+ ...mapState(['query']),
+ selectedSortOption: {
+ get() {
+ const { sort } = this.query;
+
+ if (!sort) {
+ return this.searchSortOptions[0];
+ }
+
+ const sortOption = this.searchSortOptions.find((option) => {
+ if (!option.sortable) {
+ return option.sortParam === sort;
+ }
+
+ return Object.values(option.sortParam).indexOf(sort) !== -1;
+ });
+
+ // Handle invalid sort param
+ return sortOption || this.searchSortOptions[0];
+ },
+ set(value) {
+ this.setQuery({ key: 'sort', value });
+ this.applyQuery();
+ },
+ },
+ sortDirectionData() {
+ if (!this.selectedSortOption.sortable) {
+ return SORT_DIRECTION_UI.disabled;
+ }
+
+ return this.query?.sort?.includes('asc') ? SORT_DIRECTION_UI.asc : SORT_DIRECTION_UI.desc;
+ },
+ },
+ methods: {
+ ...mapActions(['applyQuery', 'setQuery']),
+ handleSortChange(option) {
+ if (!option.sortable) {
+ this.selectedSortOption = option.sortParam;
+ } else {
+ // Default new sort options to desc
+ this.selectedSortOption = option.sortParam.desc;
+ }
+ },
+ handleSortDirectionChange() {
+ this.selectedSortOption =
+ this.sortDirectionData.direction === 'desc'
+ ? this.selectedSortOption.sortParam.asc
+ : this.selectedSortOption.sortParam.desc;
+ },
+ },
+};
+</script>
+
+<template>
+ <gl-button-group>
+ <gl-dropdown :text="selectedSortOption.title" :right="true" class="w-100">
+ <gl-dropdown-item
+ v-for="sortOption in searchSortOptions"
+ :key="sortOption.title"
+ is-check-item
+ :is-checked="sortOption.title === selectedSortOption.title"
+ @click="handleSortChange(sortOption)"
+ >{{ sortOption.title }}</gl-dropdown-item
+ >
+ </gl-dropdown>
+ <gl-button
+ v-gl-tooltip
+ :disabled="!selectedSortOption.sortable"
+ :title="sortDirectionData.tooltip"
+ :icon="sortDirectionData.icon"
+ @click="handleSortDirectionChange"
+ />
+ </gl-button-group>
+</template>
diff --git a/app/assets/javascripts/search/sort/constants.js b/app/assets/javascripts/search/sort/constants.js
new file mode 100644
index 00000000000..575fba5873b
--- /dev/null
+++ b/app/assets/javascripts/search/sort/constants.js
@@ -0,0 +1,19 @@
+import { __ } from '~/locale';
+
+export const SORT_DIRECTION_UI = {
+ disabled: {
+ direction: null,
+ tooltip: '',
+ icon: 'sort-highest',
+ },
+ desc: {
+ direction: 'desc',
+ tooltip: __('Sort direction: Descending'),
+ icon: 'sort-highest',
+ },
+ asc: {
+ direction: 'asc',
+ tooltip: __('Sort direction: Ascending'),
+ icon: 'sort-lowest',
+ },
+};
diff --git a/app/assets/javascripts/search/sort/index.js b/app/assets/javascripts/search/sort/index.js
new file mode 100644
index 00000000000..84bb5175b1d
--- /dev/null
+++ b/app/assets/javascripts/search/sort/index.js
@@ -0,0 +1,27 @@
+import Vue from 'vue';
+import Translate from '~/vue_shared/translate';
+import GlobalSearchSort from './components/app.vue';
+
+Vue.use(Translate);
+
+export const initSearchSort = (store) => {
+ const el = document.getElementById('js-search-sort');
+
+ if (!el) return false;
+
+ let { searchSortOptions } = el.dataset;
+
+ searchSortOptions = JSON.parse(searchSortOptions);
+
+ return new Vue({
+ el,
+ store,
+ render(createElement) {
+ return createElement(GlobalSearchSort, {
+ props: {
+ searchSortOptions,
+ },
+ });
+ },
+ });
+};
diff --git a/app/assets/javascripts/search/topbar/components/app.vue b/app/assets/javascripts/search/topbar/components/app.vue
new file mode 100644
index 00000000000..639cff591c3
--- /dev/null
+++ b/app/assets/javascripts/search/topbar/components/app.vue
@@ -0,0 +1,73 @@
+<script>
+import { mapState, mapActions } from 'vuex';
+import { GlForm, GlSearchBoxByType, GlButton } from '@gitlab/ui';
+import GroupFilter from './group_filter.vue';
+import ProjectFilter from './project_filter.vue';
+
+export default {
+ name: 'GlobalSearchTopbar',
+ components: {
+ GlForm,
+ GlSearchBoxByType,
+ GroupFilter,
+ ProjectFilter,
+ GlButton,
+ },
+ props: {
+ groupInitialData: {
+ type: Object,
+ required: false,
+ default: () => ({}),
+ },
+ projectInitialData: {
+ type: Object,
+ required: false,
+ default: () => ({}),
+ },
+ },
+ computed: {
+ ...mapState(['query']),
+ search: {
+ get() {
+ return this.query ? this.query.search : '';
+ },
+ set(value) {
+ this.setQuery({ key: 'search', value });
+ },
+ },
+ showFilters() {
+ return !this.query.snippets || this.query.snippets === 'false';
+ },
+ },
+ methods: {
+ ...mapActions(['applyQuery', 'setQuery']),
+ },
+};
+</script>
+
+<template>
+ <gl-form class="search-page-form" @submit.prevent="applyQuery">
+ <section class="gl-lg-display-flex gl-align-items-flex-end">
+ <div class="gl-flex-fill-1 gl-mb-4 gl-lg-mb-0 gl-lg-mr-2">
+ <label>{{ __('What are you searching for?') }}</label>
+ <gl-search-box-by-type
+ id="dashboard_search"
+ v-model="search"
+ name="search"
+ :placeholder="__(`Search for projects, issues, etc.`)"
+ />
+ </div>
+ <div v-if="showFilters" class="gl-mb-4 gl-lg-mb-0 gl-lg-mx-2">
+ <label class="gl-display-block">{{ __('Group') }}</label>
+ <group-filter :initial-data="groupInitialData" />
+ </div>
+ <div v-if="showFilters" class="gl-mb-4 gl-lg-mb-0 gl-lg-mx-2">
+ <label class="gl-display-block">{{ __('Project') }}</label>
+ <project-filter :initial-data="projectInitialData" />
+ </div>
+ <gl-button class="btn-search gl-lg-ml-2" variant="success" type="submit">{{
+ __('Search')
+ }}</gl-button>
+ </section>
+ </gl-form>
+</template>
diff --git a/app/assets/javascripts/search/topbar/components/group_filter.vue b/app/assets/javascripts/search/topbar/components/group_filter.vue
index fce9ec17d23..39555ce4fd3 100644
--- a/app/assets/javascripts/search/topbar/components/group_filter.vue
+++ b/app/assets/javascripts/search/topbar/components/group_filter.vue
@@ -2,8 +2,8 @@
import { mapState, mapActions } from 'vuex';
import { isEmpty } from 'lodash';
import { visitUrl, setUrlParams } from '~/lib/utils/url_utility';
-import SearchableDropdown from './searchable_dropdown.vue';
import { ANY_OPTION, GROUP_DATA, PROJECT_DATA } from '../constants';
+import SearchableDropdown from './searchable_dropdown.vue';
export default {
name: 'GroupFilter',
@@ -37,6 +37,7 @@ export default {
<template>
<searchable-dropdown
+ data-testid="group-filter"
:header-text="$options.GROUP_DATA.headerText"
:selected-display-value="$options.GROUP_DATA.selectedDisplayValue"
:items-display-value="$options.GROUP_DATA.itemsDisplayValue"
diff --git a/app/assets/javascripts/search/topbar/components/project_filter.vue b/app/assets/javascripts/search/topbar/components/project_filter.vue
index 3f1f3848ac7..b2dd79fcfa3 100644
--- a/app/assets/javascripts/search/topbar/components/project_filter.vue
+++ b/app/assets/javascripts/search/topbar/components/project_filter.vue
@@ -1,8 +1,8 @@
<script>
import { mapState, mapActions } from 'vuex';
import { visitUrl, setUrlParams } from '~/lib/utils/url_utility';
-import SearchableDropdown from './searchable_dropdown.vue';
import { ANY_OPTION, GROUP_DATA, PROJECT_DATA } from '../constants';
+import SearchableDropdown from './searchable_dropdown.vue';
export default {
name: 'ProjectFilter',
@@ -27,7 +27,7 @@ export default {
handleProjectChange(project) {
// This determines if we need to update the group filter or not
const queryParams = {
- ...(project.namespace_id && { [GROUP_DATA.queryParam]: project.namespace_id }),
+ ...(project.namespace?.id && { [GROUP_DATA.queryParam]: project.namespace.id }),
[PROJECT_DATA.queryParam]: project.id,
};
@@ -40,6 +40,7 @@ export default {
<template>
<searchable-dropdown
+ data-testid="project-filter"
:header-text="$options.PROJECT_DATA.headerText"
:selected-display-value="$options.PROJECT_DATA.selectedDisplayValue"
:items-display-value="$options.PROJECT_DATA.itemsDisplayValue"
diff --git a/app/assets/javascripts/search/topbar/components/searchable_dropdown.vue b/app/assets/javascripts/search/topbar/components/searchable_dropdown.vue
index 14577fd7d7a..5fb7217db74 100644
--- a/app/assets/javascripts/search/topbar/components/searchable_dropdown.vue
+++ b/app/assets/javascripts/search/topbar/components/searchable_dropdown.vue
@@ -101,7 +101,7 @@ export default {
@keydown.enter.stop="resetDropdown"
@click.stop="resetDropdown"
>
- <gl-icon name="clear" class="gl-text-gray-200! gl-hover-text-blue-800!" />
+ <gl-icon name="clear" />
</gl-button>
<gl-icon name="chevron-down" />
</template>
diff --git a/app/assets/javascripts/search/topbar/index.js b/app/assets/javascripts/search/topbar/index.js
index f0308109b32..87316e10e8d 100644
--- a/app/assets/javascripts/search/topbar/index.js
+++ b/app/assets/javascripts/search/topbar/index.js
@@ -1,44 +1,31 @@
import Vue from 'vue';
import Translate from '~/vue_shared/translate';
-import GroupFilter from './components/group_filter.vue';
-import ProjectFilter from './components/project_filter.vue';
+import GlobalSearchTopbar from './components/app.vue';
Vue.use(Translate);
-const mountSearchableDropdown = (store, { id, component }) => {
- const el = document.getElementById(id);
+export const initTopbar = (store) => {
+ const el = document.getElementById('js-search-topbar');
if (!el) {
return false;
}
- let { initialData } = el.dataset;
+ let { groupInitialData, projectInitialData } = el.dataset;
- initialData = JSON.parse(initialData);
+ groupInitialData = JSON.parse(groupInitialData);
+ projectInitialData = JSON.parse(projectInitialData);
return new Vue({
el,
store,
render(createElement) {
- return createElement(component, {
+ return createElement(GlobalSearchTopbar, {
props: {
- initialData,
+ groupInitialData,
+ projectInitialData,
},
});
},
});
};
-
-const searchableDropdowns = [
- {
- id: 'js-search-group-dropdown',
- component: GroupFilter,
- },
- {
- id: 'js-search-project-dropdown',
- component: ProjectFilter,
- },
-];
-
-export const initTopbar = (store) =>
- searchableDropdowns.map((dropdown) => mountSearchableDropdown(store, dropdown));
diff --git a/app/assets/javascripts/search_autocomplete.js b/app/assets/javascripts/search_autocomplete.js
index b8a5836e2d4..7ccec640c20 100644
--- a/app/assets/javascripts/search_autocomplete.js
+++ b/app/assets/javascripts/search_autocomplete.js
@@ -4,6 +4,8 @@ import $ from 'jquery';
import { escape, throttle } from 'lodash';
import { s__, __, sprintf } from '~/locale';
import { getIdenticonBackgroundClass, getIdenticonTitle } from '~/helpers/avatar_helper';
+import Tracking from '~/tracking';
+import initDeprecatedJQueryDropdown from '~/deprecated_jquery_dropdown';
import axios from './lib/utils/axios_utils';
import {
isInGroupsPage,
@@ -12,8 +14,6 @@ import {
getProjectSlug,
spriteIcon,
} from './lib/utils/common_utils';
-import Tracking from '~/tracking';
-import initDeprecatedJQueryDropdown from '~/deprecated_jquery_dropdown';
/**
* Search input in top navigation bar.
diff --git a/app/assets/javascripts/search_settings/components/search_settings.vue b/app/assets/javascripts/search_settings/components/search_settings.vue
index 820055dc656..de8ec57e255 100644
--- a/app/assets/javascripts/search_settings/components/search_settings.vue
+++ b/app/assets/javascripts/search_settings/components/search_settings.vue
@@ -118,12 +118,10 @@ export default {
};
</script>
<template>
- <div class="gl-mt-5">
- <gl-search-box-by-type
- :value="searchTerm"
- :debounce="$options.TYPING_DELAY"
- :placeholder="__('Search settings')"
- @input="search"
- />
- </div>
+ <gl-search-box-by-type
+ :value="searchTerm"
+ :debounce="$options.TYPING_DELAY"
+ :placeholder="__('Search settings')"
+ @input="search"
+ />
</template>
diff --git a/app/assets/javascripts/search_settings/index.js b/app/assets/javascripts/search_settings/index.js
index 1fb1a378ffb..676c43c5631 100644
--- a/app/assets/javascripts/search_settings/index.js
+++ b/app/assets/javascripts/search_settings/index.js
@@ -1,23 +1,10 @@
-import Vue from 'vue';
-import $ from 'jquery';
-import { expandSection, closeSection } from '~/settings_panels';
-import SearchSettings from '~/search_settings/components/search_settings.vue';
+const initSearch = async () => {
+ const el = document.querySelector('.js-search-settings-app');
-const initSearch = ({ el }) =>
- new Vue({
- el,
- render: (h) =>
- h(SearchSettings, {
- ref: 'searchSettings',
- props: {
- searchRoot: document.querySelector('#content-body'),
- sectionSelector: 'section.settings',
- },
- on: {
- collapse: (section) => closeSection($(section)),
- expand: (section) => expandSection($(section)),
- },
- }),
- });
+ if (el) {
+ const { default: mount } = await import(/* webpackChunkName: 'search_settings' */ './mount');
+ mount({ el });
+ }
+};
export default initSearch;
diff --git a/app/assets/javascripts/search_settings/mount.js b/app/assets/javascripts/search_settings/mount.js
new file mode 100644
index 00000000000..85ad3b28d59
--- /dev/null
+++ b/app/assets/javascripts/search_settings/mount.js
@@ -0,0 +1,23 @@
+import Vue from 'vue';
+import $ from 'jquery';
+import { expandSection, closeSection } from '~/settings_panels';
+import SearchSettings from '~/search_settings/components/search_settings.vue';
+
+const mountSearch = ({ el }) =>
+ new Vue({
+ el,
+ render: (h) =>
+ h(SearchSettings, {
+ ref: 'searchSettings',
+ props: {
+ searchRoot: document.querySelector('#content-body'),
+ sectionSelector: 'section.settings',
+ },
+ on: {
+ collapse: (section) => closeSection($(section)),
+ expand: (section) => expandSection($(section)),
+ },
+ }),
+ });
+
+export default mountSearch;
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 6776a9ebb22..f63fb8228d4 100644
--- a/app/assets/javascripts/self_monitor/components/self_monitor_form.vue
+++ b/app/assets/javascripts/self_monitor/components/self_monitor_form.vue
@@ -5,6 +5,7 @@ import { GlFormGroup, GlButton, GlModal, GlToast, GlToggle } from '@gitlab/ui';
import { mapState, mapActions } from 'vuex';
import { __, s__, sprintf } from '~/locale';
import { visitUrl, getBaseURL } from '~/lib/utils/url_utility';
+import { BV_SHOW_MODAL, BV_HIDE_MODAL } from '~/lib/utils/constants';
Vue.use(GlToast);
@@ -98,11 +99,11 @@ export default {
'resetAlert',
]),
hideSelfMonitorModal() {
- this.$root.$emit('bv::hide::modal', this.modalId);
+ this.$root.$emit(BV_HIDE_MODAL, this.modalId);
this.setSelfMonitor(true);
},
showSelfMonitorModal() {
- this.$root.$emit('bv::show::modal', this.modalId);
+ this.$root.$emit(BV_SHOW_MODAL, this.modalId);
},
saveChangesSelfMonitorProject() {
if (this.projectCreated && !this.projectEnabled) {
diff --git a/app/assets/javascripts/sentry_error_stack_trace/index.js b/app/assets/javascripts/sentry_error_stack_trace/index.js
index 80fa0988f0a..8e9ee25e7a8 100644
--- a/app/assets/javascripts/sentry_error_stack_trace/index.js
+++ b/app/assets/javascripts/sentry_error_stack_trace/index.js
@@ -1,6 +1,6 @@
import Vue from 'vue';
-import SentryErrorStackTrace from './components/sentry_error_stack_trace.vue';
import store from '~/error_tracking/store';
+import SentryErrorStackTrace from './components/sentry_error_stack_trace.vue';
export default function initSentryErrorStacktrace() {
const sentryErrorStackTraceEl = document.querySelector('#js-sentry-error-stack-trace');
diff --git a/app/assets/javascripts/serverless/components/area.vue b/app/assets/javascripts/serverless/components/area.vue
index 056b342cf39..a9584c070fe 100644
--- a/app/assets/javascripts/serverless/components/area.vue
+++ b/app/assets/javascripts/serverless/components/area.vue
@@ -2,9 +2,9 @@
import { GlAreaChart } from '@gitlab/ui/dist/charts';
import dateFormat from 'dateformat';
import { debounceByAnimationFrame } from '~/lib/utils/common_utils';
+import { __ } from '~/locale';
import { X_INTERVAL } from '../constants';
import { validateGraphData } from '../utils';
-import { __ } from '~/locale';
let debouncedResize;
diff --git a/app/assets/javascripts/serverless/components/environment_row.vue b/app/assets/javascripts/serverless/components/environment_row.vue
index c46dfb66afe..01030172ea8 100644
--- a/app/assets/javascripts/serverless/components/environment_row.vue
+++ b/app/assets/javascripts/serverless/components/environment_row.vue
@@ -1,6 +1,6 @@
<script>
-import FunctionRow from './function_row.vue';
import ItemCaret from '~/groups/components/item_caret.vue';
+import FunctionRow from './function_row.vue';
export default {
components: {
diff --git a/app/assets/javascripts/serverless/components/function_row.vue b/app/assets/javascripts/serverless/components/function_row.vue
index aac7c46a295..aea38b54062 100644
--- a/app/assets/javascripts/serverless/components/function_row.vue
+++ b/app/assets/javascripts/serverless/components/function_row.vue
@@ -1,8 +1,8 @@
<script>
import { isString } from 'lodash';
import Timeago from '~/vue_shared/components/time_ago_tooltip.vue';
-import Url from './url.vue';
import { visitUrl } from '~/lib/utils/url_utility';
+import Url from './url.vue';
export default {
components: {
diff --git a/app/assets/javascripts/serverless/components/functions.vue b/app/assets/javascripts/serverless/components/functions.vue
index d662cc7b802..19a3c3decd2 100644
--- a/app/assets/javascripts/serverless/components/functions.vue
+++ b/app/assets/javascripts/serverless/components/functions.vue
@@ -2,9 +2,9 @@
import { mapState, mapActions, mapGetters } from 'vuex';
import { GlLink, GlLoadingIcon, GlSafeHtmlDirective as SafeHtml } from '@gitlab/ui';
import { sprintf, s__ } from '~/locale';
+import { CHECKING_INSTALLED } from '../constants';
import EnvironmentRow from './environment_row.vue';
import EmptyState from './empty_state.vue';
-import { CHECKING_INSTALLED } from '../constants';
export default {
components: {
@@ -69,18 +69,16 @@ export default {
<div v-else-if="isInstalled">
<div v-if="hasFunctionData">
- <template>
- <div class="groups-list-tree-container js-functions-wrapper">
- <ul class="content-list group-list-tree">
- <environment-row
- v-for="(env, index) in getFunctions"
- :key="index"
- :env="env"
- :env-name="index"
- />
- </ul>
- </div>
- </template>
+ <div class="groups-list-tree-container js-functions-wrapper">
+ <ul class="content-list group-list-tree">
+ <environment-row
+ v-for="(env, index) in getFunctions"
+ :key="index"
+ :env="env"
+ :env-name="index"
+ />
+ </ul>
+ </div>
<gl-loading-icon v-if="isLoading" size="lg" class="gl-mt-3 gl-mb-3 js-functions-loader" />
</div>
<div v-else class="empty-state js-empty-state">
diff --git a/app/assets/javascripts/serverless/store/actions.js b/app/assets/javascripts/serverless/store/actions.js
index acd7020f70f..27d476dc53e 100644
--- a/app/assets/javascripts/serverless/store/actions.js
+++ b/app/assets/javascripts/serverless/store/actions.js
@@ -1,10 +1,10 @@
-import * as types from './mutation_types';
import axios from '~/lib/utils/axios_utils';
import statusCodes from '~/lib/utils/http_status';
import { backOff } from '~/lib/utils/common_utils';
import { deprecatedCreateFlash as createFlash } from '~/flash';
import { __ } from '~/locale';
import { MAX_REQUESTS, CHECKING_INSTALLED, TIMEOUT } from '../constants';
+import * as types from './mutation_types';
export const requestFunctionsLoading = ({ commit }) => commit(types.REQUEST_FUNCTIONS_LOADING);
export const receiveFunctionsSuccess = ({ commit }, data) =>
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 c8efbd73b48..a2b2fbddde7 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
@@ -2,14 +2,15 @@
/* eslint-disable vue/no-v-html */
import $ from 'jquery';
import Vue from 'vue';
-import GfmAutoComplete from 'ee_else_ce/gfm_auto_complete';
import { GlToast, GlModal, GlTooltipDirective, GlIcon, GlFormCheckbox } from '@gitlab/ui';
+import GfmAutoComplete from 'ee_else_ce/gfm_auto_complete';
import { deprecatedCreateFlash as createFlash } from '~/flash';
import { __, s__ } from '~/locale';
import { updateUserStatus } from '~/rest_api';
+import * as Emoji from '~/emoji';
+import { BV_SHOW_MODAL, BV_HIDE_MODAL } from '~/lib/utils/constants';
import EmojiMenuInModal from './emoji_menu_in_modal';
import { isUserBusy, isValidAvailibility } from './utils';
-import * as Emoji from '~/emoji';
const emojiMenuClass = 'js-modal-status-emoji-menu';
export const AVAILABILITY_STATUS = {
@@ -76,14 +77,14 @@ export default {
},
},
mounted() {
- this.$root.$emit('bv::show::modal', this.modalId);
+ this.$root.$emit(BV_SHOW_MODAL, this.modalId);
},
beforeDestroy() {
this.emojiMenu.destroy();
},
methods: {
closeModal() {
- this.$root.$emit('bv::hide::modal', this.modalId);
+ this.$root.$emit(BV_HIDE_MODAL, this.modalId);
},
setupEmojiListAndAutocomplete() {
const toggleEmojiMenuButtonSelector = '#set-user-status-modal .js-toggle-emoji-menu';
diff --git a/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.vue b/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.vue
index b9f268629fb..1fd7514b513 100644
--- a/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.vue
+++ b/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.vue
@@ -4,10 +4,10 @@ import eventHub from '~/sidebar/event_hub';
import Store from '~/sidebar/stores/sidebar_store';
import { refreshUserMergeRequestCounts } from '~/commons/nav/user_merge_requests';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
+import { __ } from '~/locale';
import AssigneeTitle from './assignee_title.vue';
import Assignees from './assignees.vue';
import AssigneesRealtime from './assignees_realtime.vue';
-import { __ } from '~/locale';
export default {
name: 'SidebarAssignees',
diff --git a/app/assets/javascripts/sidebar/components/confidential/edit_form.vue b/app/assets/javascripts/sidebar/components/confidential/edit_form.vue
index 17e44cf0e1d..057224d5918 100644
--- a/app/assets/javascripts/sidebar/components/confidential/edit_form.vue
+++ b/app/assets/javascripts/sidebar/components/confidential/edit_form.vue
@@ -1,7 +1,7 @@
<script>
import { GlSprintf } from '@gitlab/ui';
-import editFormButtons from './edit_form_buttons.vue';
import { __ } from '../../../locale';
+import editFormButtons from './edit_form_buttons.vue';
export default {
components: {
diff --git a/app/assets/javascripts/sidebar/components/lock/edit_form_buttons.vue b/app/assets/javascripts/sidebar/components/lock/edit_form_buttons.vue
index 26a7c8e4a80..6439536a6b4 100644
--- a/app/assets/javascripts/sidebar/components/lock/edit_form_buttons.vue
+++ b/app/assets/javascripts/sidebar/components/lock/edit_form_buttons.vue
@@ -2,8 +2,8 @@
import $ from 'jquery';
import { GlButton } from '@gitlab/ui';
import { mapActions } from 'vuex';
-import { __, sprintf } from '../../../locale';
import { deprecatedCreateFlash as Flash } from '~/flash';
+import { __, sprintf } from '../../../locale';
import eventHub from '../../event_hub';
export default {
diff --git a/app/assets/javascripts/sidebar/components/reviewers/reviewer_avatar_link.vue b/app/assets/javascripts/sidebar/components/reviewers/reviewer_avatar_link.vue
index b1b04564a62..87780888c2f 100644
--- a/app/assets/javascripts/sidebar/components/reviewers/reviewer_avatar_link.vue
+++ b/app/assets/javascripts/sidebar/components/reviewers/reviewer_avatar_link.vue
@@ -76,8 +76,8 @@ export default {
class="d-inline-block"
>
<!-- use d-flex so that slot can be appropriately styled -->
- <span class="d-flex">
- <reviewer-avatar :user="user" :img-size="32" :issuable-type="issuableType" />
+ <span class="gl-display-flex gl-align-items-center">
+ <reviewer-avatar :user="user" :img-size="24" :issuable-type="issuableType" />
<slot :user="user"></slot>
</span>
</gl-link>
diff --git a/app/assets/javascripts/sidebar/components/reviewers/reviewers.vue b/app/assets/javascripts/sidebar/components/reviewers/reviewers.vue
index cd62fe5be0f..00a2456c6b6 100644
--- a/app/assets/javascripts/sidebar/components/reviewers/reviewers.vue
+++ b/app/assets/javascripts/sidebar/components/reviewers/reviewers.vue
@@ -46,6 +46,9 @@ export default {
assignSelf() {
this.$emit('assign-self');
},
+ requestReview(data) {
+ this.$emit('request-review', data);
+ },
},
};
</script>
@@ -66,6 +69,7 @@ export default {
:users="sortedReviewers"
:root-path="rootPath"
:issuable-type="issuableType"
+ @request-review="requestReview"
/>
</div>
</div>
diff --git a/app/assets/javascripts/sidebar/components/reviewers/sidebar_reviewers.vue b/app/assets/javascripts/sidebar/components/reviewers/sidebar_reviewers.vue
index 1a2473e5f6c..4bfae2dfffa 100644
--- a/app/assets/javascripts/sidebar/components/reviewers/sidebar_reviewers.vue
+++ b/app/assets/javascripts/sidebar/components/reviewers/sidebar_reviewers.vue
@@ -6,9 +6,9 @@ import { refreshUserMergeRequestCounts } from '~/commons/nav/user_merge_requests
import eventHub from '~/sidebar/event_hub';
import Store from '~/sidebar/stores/sidebar_store';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
+import { __ } from '~/locale';
import ReviewerTitle from './reviewer_title.vue';
import Reviewers from './reviewers.vue';
-import { __ } from '~/locale';
export default {
name: 'SidebarReviewers',
@@ -83,6 +83,9 @@ export default {
return new Flash(__('Error occurred when saving reviewers'));
});
},
+ requestReview(data) {
+ this.mediator.requestReview(data);
+ },
},
};
</script>
@@ -101,6 +104,7 @@ export default {
:editable="store.editable"
:issuable-type="issuableType"
class="value"
+ @request-review="requestReview"
/>
</div>
</template>
diff --git a/app/assets/javascripts/sidebar/components/reviewers/uncollapsed_reviewer_list.vue b/app/assets/javascripts/sidebar/components/reviewers/uncollapsed_reviewer_list.vue
index e82a271d007..be5fd93f77c 100644
--- a/app/assets/javascripts/sidebar/components/reviewers/uncollapsed_reviewer_list.vue
+++ b/app/assets/javascripts/sidebar/components/reviewers/uncollapsed_reviewer_list.vue
@@ -1,6 +1,7 @@
<script>
// NOTE! For the first iteration, we are simply copying the implementation of Assignees
// It will soon be overhauled in Issue https://gitlab.com/gitlab-org/gitlab/-/issues/233736
+import { GlButton, GlTooltipDirective, GlIcon } from '@gitlab/ui';
import { __, sprintf } from '~/locale';
import ReviewerAvatarLink from './reviewer_avatar_link.vue';
@@ -8,8 +9,13 @@ const DEFAULT_RENDER_COUNT = 5;
export default {
components: {
+ GlButton,
+ GlIcon,
ReviewerAvatarLink,
},
+ directives: {
+ GlTooltip: GlTooltipDirective,
+ },
props: {
users: {
type: Array,
@@ -28,6 +34,8 @@ export default {
data() {
return {
showLess: true,
+ loading: false,
+ requestedReviewSuccess: false,
};
},
computed: {
@@ -61,43 +69,53 @@ export default {
toggleShowLess() {
this.showLess = !this.showLess;
},
+ reRequestReview(userId) {
+ this.loading = true;
+ this.$emit('request-review', { userId, callback: this.requestReviewComplete });
+ },
+ requestReviewComplete(success) {
+ if (success) {
+ this.requestedReviewSuccess = true;
+
+ setTimeout(() => {
+ this.requestedReviewSuccess = false;
+ }, 1500);
+ }
+
+ this.loading = false;
+ },
},
};
</script>
<template>
- <reviewer-avatar-link
- v-if="hasOneUser"
- #default="{ user }"
- tooltip-placement="left"
- :tooltip-has-name="false"
- :user="firstUser"
- :root-path="rootPath"
- :issuable-type="issuableType"
- >
- <div class="gl-ml-3 gl-line-height-normal">
- <div class="author">{{ user.name }}</div>
- <div class="username">{{ username }}</div>
- </div>
- </reviewer-avatar-link>
- <div v-else>
- <div class="user-list">
- <div v-for="user in uncollapsedUsers" :key="user.id" class="user-item">
- <reviewer-avatar-link :user="user" :root-path="rootPath" :issuable-type="issuableType" />
- </div>
- </div>
- <div v-if="renderShowMoreSection" class="user-list-more">
- <button
- type="button"
- class="btn-link"
- data-qa-selector="more_reviewers_link"
- @click="toggleShowLess"
- >
- <template v-if="showLess">
- {{ hiddenReviewersLabel }}
- </template>
- <template v-else>{{ __('- show less') }}</template>
- </button>
+ <div>
+ <div
+ v-for="(user, index) in users"
+ :key="user.id"
+ :class="{ 'gl-mb-3': index !== users.length - 1 }"
+ data-testid="reviewer"
+ >
+ <reviewer-avatar-link :user="user" :root-path="rootPath" :issuable-type="issuableType">
+ <div class="gl-ml-3">@{{ user.username }}</div>
+ </reviewer-avatar-link>
+ <gl-icon
+ v-if="requestedReviewSuccess"
+ :size="24"
+ name="check"
+ class="float-right gl-text-green-500"
+ />
+ <gl-button
+ v-else-if="user.can_update_merge_request && user.reviewed"
+ v-gl-tooltip.left
+ :title="__('Re-request review')"
+ :loading="loading"
+ class="float-right gl-text-gray-500!"
+ size="small"
+ icon="redo"
+ variant="link"
+ @click="reRequestReview(user.id)"
+ />
</div>
</div>
</template>
diff --git a/app/assets/javascripts/sidebar/components/severity/sidebar_severity.vue b/app/assets/javascripts/sidebar/components/severity/sidebar_severity.vue
index 0cf11e83349..6a6300dcde0 100644
--- a/app/assets/javascripts/sidebar/components/severity/sidebar_severity.vue
+++ b/app/assets/javascripts/sidebar/components/severity/sidebar_severity.vue
@@ -7,10 +7,10 @@ import {
GlSprintf,
GlLink,
} from '@gitlab/ui';
+import createFlash from '~/flash';
import { INCIDENT_SEVERITY, ISSUABLE_TYPES, I18N } from './constants';
import updateIssuableSeverity from './graphql/mutations/update_issuable_severity.mutation.graphql';
import SeverityToken from './severity.vue';
-import createFlash from '~/flash';
export default {
i18n: I18N,
diff --git a/app/assets/javascripts/sidebar/components/subscriptions/subscriptions.vue b/app/assets/javascripts/sidebar/components/subscriptions/subscriptions.vue
index 6d21936791c..9b06c20a6f3 100644
--- a/app/assets/javascripts/sidebar/components/subscriptions/subscriptions.vue
+++ b/app/assets/javascripts/sidebar/components/subscriptions/subscriptions.vue
@@ -1,8 +1,7 @@
<script>
-import { GlIcon, GlTooltipDirective } from '@gitlab/ui';
+import { GlIcon, GlToggle, GlTooltipDirective } from '@gitlab/ui';
import { __ } from '~/locale';
import Tracking from '~/tracking';
-import toggleButton from '~/vue_shared/components/toggle_button.vue';
import eventHub from '../../event_hub';
const ICON_ON = 'notifications';
@@ -16,7 +15,7 @@ export default {
},
components: {
GlIcon,
- toggleButton,
+ GlToggle,
},
mixins: [Tracking.mixin({ label: 'right_sidebar' })],
props: {
@@ -106,7 +105,7 @@ export default {
</script>
<template>
- <div>
+ <div class="gl-display-flex gl-justify-content-space-between">
<span
ref="tooltip"
v-gl-tooltip.viewport.left
@@ -116,13 +115,13 @@ export default {
>
<gl-icon :name="notificationIcon" :size="16" class="sidebar-item-icon is-active" />
</span>
- <span class="issuable-header-text hide-collapsed float-left"> {{ notificationText }} </span>
- <toggle-button
+ <span class="hide-collapsed" data-testid="subscription-title"> {{ notificationText }} </span>
+ <gl-toggle
v-if="!projectEmailsDisabled"
- ref="toggleButton"
:is-loading="showLoadingState"
:value="subscribed"
- class="float-right hide-collapsed js-issuable-subscribe-button"
+ class="hide-collapsed"
+ data-testid="subscription-toggle"
@change="toggleSubscription"
/>
</div>
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 8bc828091c0..e0f60b9af08 100644
--- a/app/assets/javascripts/sidebar/components/time_tracking/help_state.vue
+++ b/app/assets/javascripts/sidebar/components/time_tracking/help_state.vue
@@ -1,7 +1,7 @@
<script>
/* eslint-disable vue/no-v-html */
-import { sprintf, s__ } from '../../../locale';
import { joinPaths } from '~/lib/utils/url_utility';
+import { sprintf, s__ } from '../../../locale';
export default {
name: 'TimeTrackingHelpState',
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 26e0a0da860..f66c2540a9e 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
@@ -4,11 +4,10 @@ import { intersection } from 'lodash';
import '~/smart_interval';
-import IssuableTimeTracker from './time_tracker.vue';
-
import Store from '../../stores/sidebar_store';
import Mediator from '../../sidebar_mediator';
import eventHub from '../../event_hub';
+import IssuableTimeTracker from './time_tracker.vue';
export default {
components: {
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 26b8e087512..6d36d16965f 100644
--- a/app/assets/javascripts/sidebar/components/time_tracking/time_tracker.vue
+++ b/app/assets/javascripts/sidebar/components/time_tracking/time_tracker.vue
@@ -1,13 +1,12 @@
<script>
import { GlIcon } from '@gitlab/ui';
import { s__, __ } from '~/locale';
+import eventHub from '../../event_hub';
import TimeTrackingHelpState from './help_state.vue';
import TimeTrackingCollapsedState from './collapsed_state.vue';
import TimeTrackingSpentOnlyPane from './spent_only_pane.vue';
import TimeTrackingComparisonPane from './comparison_pane.vue';
-import eventHub from '../../event_hub';
-
export default {
name: 'IssuableTimeTracker',
i18n: {
@@ -48,11 +47,11 @@ export default {
/*
In issue list, "time-tracking-collapsed-state" is always rendered even if the sidebar isn't collapsed.
The actual hiding is controlled with css classes:
- Hide "time-tracking-collapsed-state"
+ Hide "time-tracking-collapsed-state"
if .right-sidebar .right-sidebar-collapsed .sidebar-collapsed-icon
Show "time-tracking-collapsed-state"
if .right-sidebar .right-sidebar-expanded .sidebar-collapsed-icon
-
+
In Swimlanes sidebar, we do not use collapsed state at all.
*/
showCollapsed: {
@@ -99,10 +98,12 @@ export default {
update(data) {
const { timeEstimate, timeSpent, humanTimeEstimate, humanTimeSpent } = data;
+ /* eslint-disable vue/no-mutating-props */
this.timeEstimate = timeEstimate;
this.timeSpent = timeSpent;
this.humanTimeEstimate = humanTimeEstimate;
this.humanTimeSpent = humanTimeSpent;
+ /* eslint-enable vue/no-mutating-props */
},
},
};
diff --git a/app/assets/javascripts/sidebar/components/todo_toggle/todo.vue b/app/assets/javascripts/sidebar/components/todo_toggle/todo.vue
index 1e3e870ec83..f589e7555b3 100644
--- a/app/assets/javascripts/sidebar/components/todo_toggle/todo.vue
+++ b/app/assets/javascripts/sidebar/components/todo_toggle/todo.vue
@@ -3,7 +3,7 @@ import { GlLoadingIcon, GlIcon, GlTooltipDirective } from '@gitlab/ui';
import { __ } from '~/locale';
const MARK_TEXT = __('Mark as done');
-const TODO_TEXT = __('Add a To-Do');
+const TODO_TEXT = __('Add a to do');
export default {
components: {
@@ -42,7 +42,7 @@ export default {
buttonClasses() {
return this.collapsed
? 'btn-blank btn-todo sidebar-collapsed-icon dont-change-state'
- : 'btn btn-default btn-todo issuable-header-btn float-right';
+ : 'gl-button btn btn-default btn-todo issuable-header-btn float-right';
},
buttonLabel() {
return this.isTodo ? MARK_TEXT : TODO_TEXT;
diff --git a/app/assets/javascripts/sidebar/mount_milestone_sidebar.js b/app/assets/javascripts/sidebar/mount_milestone_sidebar.js
index 4d9e99941d1..b11c8f76a6d 100644
--- a/app/assets/javascripts/sidebar/mount_milestone_sidebar.js
+++ b/app/assets/javascripts/sidebar/mount_milestone_sidebar.js
@@ -1,6 +1,6 @@
import Vue from 'vue';
-import timeTracker from './components/time_tracking/time_tracker.vue';
import { parseBoolean } from '~/lib/utils/common_utils';
+import timeTracker from './components/time_tracking/time_tracker.vue';
export default class SidebarMilestone {
constructor() {
diff --git a/app/assets/javascripts/sidebar/mount_sidebar.js b/app/assets/javascripts/sidebar/mount_sidebar.js
index 2760bf431ea..1af52529be9 100644
--- a/app/assets/javascripts/sidebar/mount_sidebar.js
+++ b/app/assets/javascripts/sidebar/mount_sidebar.js
@@ -1,6 +1,16 @@
import $ from 'jquery';
import Vue from 'vue';
import VueApollo from 'vue-apollo';
+import createDefaultClient from '~/lib/graphql';
+import {
+ isInIssuePage,
+ isInDesignPage,
+ isInIncidentPage,
+ parseBoolean,
+} from '~/lib/utils/common_utils';
+import createFlash from '~/flash';
+import { __ } from '~/locale';
+import Translate from '../vue_shared/translate';
import SidebarTimeTracking from './components/time_tracking/sidebar_time_tracking.vue';
import SidebarAssignees from './components/assignees/sidebar_assignees.vue';
import SidebarLabels from './components/labels/sidebar_labels.vue';
@@ -11,12 +21,7 @@ import IssuableLockForm from './components/lock/issuable_lock_form.vue';
import sidebarParticipants from './components/participants/sidebar_participants.vue';
import sidebarSubscriptions from './components/subscriptions/sidebar_subscriptions.vue';
import SidebarSeverity from './components/severity/sidebar_severity.vue';
-import Translate from '../vue_shared/translate';
import CopyEmailToClipboard from './components/copy_email_to_clipboard.vue';
-import createDefaultClient from '~/lib/graphql';
-import { isInIssuePage, isInIncidentPage, parseBoolean } from '~/lib/utils/common_utils';
-import createFlash from '~/flash';
-import { __ } from '~/locale';
Vue.use(Translate);
Vue.use(VueApollo);
@@ -49,7 +54,8 @@ function mountAssigneesComponent(mediator) {
projectPath: fullPath,
field: el.dataset.field,
signedIn: el.hasAttribute('data-signed-in'),
- issuableType: isInIssuePage() || isInIncidentPage() ? 'issue' : 'merge_request',
+ issuableType:
+ isInIssuePage() || isInIncidentPage() || isInDesignPage() ? 'issue' : 'merge_request',
},
}),
});
@@ -78,7 +84,7 @@ function mountReviewersComponent(mediator) {
issuableIid: String(iid),
projectPath: fullPath,
field: el.dataset.field,
- issuableType: isInIssuePage() ? 'issue' : 'merge_request',
+ issuableType: isInIssuePage() || isInDesignPage() ? 'issue' : 'merge_request',
},
}),
});
diff --git a/app/assets/javascripts/sidebar/queries/reviewer_rereview.mutation.graphql b/app/assets/javascripts/sidebar/queries/reviewer_rereview.mutation.graphql
new file mode 100644
index 00000000000..73765e7d77b
--- /dev/null
+++ b/app/assets/javascripts/sidebar/queries/reviewer_rereview.mutation.graphql
@@ -0,0 +1,5 @@
+mutation mergeRequestRequestRereview($projectPath: ID!, $iid: String!, $userId: ID!) {
+ mergeRequestReviewerRereview(input: { projectPath: $projectPath, iid: $iid, userId: $userId }) {
+ errors
+ }
+}
diff --git a/app/assets/javascripts/sidebar/services/sidebar_service.js b/app/assets/javascripts/sidebar/services/sidebar_service.js
index a61af631661..eef9a22cce5 100644
--- a/app/assets/javascripts/sidebar/services/sidebar_service.js
+++ b/app/assets/javascripts/sidebar/services/sidebar_service.js
@@ -1,6 +1,8 @@
import sidebarDetailsQuery from 'ee_else_ce/sidebar/queries/sidebarDetails.query.graphql';
import axios from '~/lib/utils/axios_utils';
import createGqClient, { fetchPolicies } from '~/lib/graphql';
+import { convertToGraphQLId } from '~/graphql_shared/utils';
+import reviewerRereviewMutation from '../queries/reviewer_rereview.mutation.graphql';
export const gqClient = createGqClient(
{},
@@ -70,4 +72,15 @@ export default class SidebarService {
move_to_project_id: moveToProjectId,
});
}
+
+ requestReview(userId) {
+ return gqClient.mutate({
+ mutation: reviewerRereviewMutation,
+ variables: {
+ userId: convertToGraphQLId('User', `${userId}`), // eslint-disable-line @gitlab/require-i18n-strings
+ projectPath: this.fullPath,
+ iid: this.iid.toString(),
+ },
+ });
+ }
}
diff --git a/app/assets/javascripts/sidebar/sidebar_mediator.js b/app/assets/javascripts/sidebar/sidebar_mediator.js
index d143283653b..b23788f81fe 100644
--- a/app/assets/javascripts/sidebar/sidebar_mediator.js
+++ b/app/assets/javascripts/sidebar/sidebar_mediator.js
@@ -1,8 +1,9 @@
import Store from 'ee_else_ce/sidebar/stores/sidebar_store';
+import toast from '~/vue_shared/plugins/global_toast';
+import { __ } from '~/locale';
import { visitUrl } from '../lib/utils/url_utility';
import { deprecatedCreateFlash as Flash } from '../flash';
import Service from './services/sidebar_service';
-import { __ } from '~/locale';
export default class SidebarMediator {
constructor(options) {
@@ -51,6 +52,17 @@ export default class SidebarMediator {
return this.service.update(field, data);
}
+ requestReview({ userId, callback }) {
+ return this.service
+ .requestReview(userId)
+ .then(() => {
+ this.store.updateReviewer(userId);
+ toast(__('Requested review'));
+ callback(true);
+ })
+ .catch(() => callback(false));
+ }
+
setMoveToProjectId(projectId) {
this.store.setMoveToProjectId(projectId);
}
diff --git a/app/assets/javascripts/sidebar/stores/sidebar_store.js b/app/assets/javascripts/sidebar/stores/sidebar_store.js
index d53393052eb..3c108b06eab 100644
--- a/app/assets/javascripts/sidebar/stores/sidebar_store.js
+++ b/app/assets/javascripts/sidebar/stores/sidebar_store.js
@@ -96,6 +96,14 @@ export default class SidebarStore {
}
}
+ updateReviewer(id) {
+ const reviewer = this.findReviewer({ id });
+
+ if (reviewer) {
+ reviewer.reviewed = false;
+ }
+ }
+
findAssignee(findAssignee) {
return this.assignees.find(({ id }) => id === findAssignee.id);
}
diff --git a/app/assets/javascripts/single_file_diff.js b/app/assets/javascripts/single_file_diff.js
index 192eb0784d4..5b40140a232 100644
--- a/app/assets/javascripts/single_file_diff.js
+++ b/app/assets/javascripts/single_file_diff.js
@@ -1,13 +1,13 @@
/* eslint-disable consistent-return */
import $ from 'jquery';
+import { spriteIcon } from '~/lib/utils/common_utils';
import { __ } from './locale';
import axios from './lib/utils/axios_utils';
import { deprecatedCreateFlash as createFlash } from './flash';
import FilesCommentButton from './files_comment_button';
import initImageDiffHelper from './image_diff/helpers/init_image_diff';
import syntaxHighlight from './syntax_highlight';
-import { spriteIcon } from '~/lib/utils/common_utils';
const WRAPPER = '<div class="diff-content"></div>';
const LOADING_HTML = '<span class="spinner"></span>';
diff --git a/app/assets/javascripts/snippets/components/show.vue b/app/assets/javascripts/snippets/components/show.vue
index a3e5535c5fa..63257785e9d 100644
--- a/app/assets/javascripts/snippets/components/show.vue
+++ b/app/assets/javascripts/snippets/components/show.vue
@@ -1,9 +1,5 @@
<script>
import { GlLoadingIcon } from '@gitlab/ui';
-import EmbedDropdown from './embed_dropdown.vue';
-import SnippetHeader from './snippet_header.vue';
-import SnippetTitle from './snippet_title.vue';
-import SnippetBlob from './snippet_blob_view.vue';
import CloneDropdownButton from '~/vue_shared/components/clone_dropdown.vue';
import { SNIPPET_VISIBILITY_PUBLIC } from '~/snippets/constants';
import {
@@ -15,6 +11,10 @@ import eventHub from '~/blob/components/eventhub';
import { getSnippetMixin } from '../mixins/snippets';
import { markBlobPerformance } from '../utils/blob';
+import SnippetBlob from './snippet_blob_view.vue';
+import SnippetTitle from './snippet_title.vue';
+import SnippetHeader from './snippet_header.vue';
+import EmbedDropdown from './embed_dropdown.vue';
eventHub.$on(SNIPPET_MEASURE_BLOBS_CONTENT, markBlobPerformance);
diff --git a/app/assets/javascripts/snippets/components/snippet_blob_actions_edit.vue b/app/assets/javascripts/snippets/components/snippet_blob_actions_edit.vue
index ff27c90a84d..d221195ddc7 100644
--- a/app/assets/javascripts/snippets/components/snippet_blob_actions_edit.vue
+++ b/app/assets/javascripts/snippets/components/snippet_blob_actions_edit.vue
@@ -2,9 +2,9 @@
import { GlButton } from '@gitlab/ui';
import { cloneDeep } from 'lodash';
import { s__, sprintf } from '~/locale';
-import SnippetBlobEdit from './snippet_blob_edit.vue';
import { SNIPPET_MAX_BLOBS } from '../constants';
import { createBlob, decorateBlob, diffAll } from '../utils/blob';
+import SnippetBlobEdit from './snippet_blob_edit.vue';
export default {
components: {
diff --git a/app/assets/javascripts/snippets/components/snippet_blob_view.vue b/app/assets/javascripts/snippets/components/snippet_blob_view.vue
index 4326c3c3159..816a7842dda 100644
--- a/app/assets/javascripts/snippets/components/snippet_blob_view.vue
+++ b/app/assets/javascripts/snippets/components/snippet_blob_view.vue
@@ -31,8 +31,10 @@ export default {
},
result() {
if (this.activeViewerType === RICH_BLOB_VIEWER) {
+ // eslint-disable-next-line vue/no-mutating-props
this.blob.richViewer.renderError = null;
} else {
+ // eslint-disable-next-line vue/no-mutating-props
this.blob.simpleViewer.renderError = null;
}
},
diff --git a/app/assets/javascripts/snippets/components/snippet_header.vue b/app/assets/javascripts/snippets/components/snippet_header.vue
index 5ba62908b43..65df2464dc5 100644
--- a/app/assets/javascripts/snippets/components/snippet_header.vue
+++ b/app/assets/javascripts/snippets/components/snippet_header.vue
@@ -16,9 +16,9 @@ import CanCreateProjectSnippet from 'shared_queries/snippet/project_permissions.
import { __ } from '~/locale';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
-import DeleteSnippetMutation from '../mutations/deleteSnippet.mutation.graphql';
import { joinPaths } from '~/lib/utils/url_utility';
import { fetchPolicies } from '~/lib/graphql';
+import DeleteSnippetMutation from '../mutations/deleteSnippet.mutation.graphql';
export default {
components: {
diff --git a/app/assets/javascripts/snippets/components/snippet_visibility_edit.vue b/app/assets/javascripts/snippets/components/snippet_visibility_edit.vue
index ee5076835ab..18a7d4ad218 100644
--- a/app/assets/javascripts/snippets/components/snippet_visibility_edit.vue
+++ b/app/assets/javascripts/snippets/components/snippet_visibility_edit.vue
@@ -1,7 +1,7 @@
<script>
import { GlIcon, GlFormGroup, GlFormRadio, GlFormRadioGroup, GlLink } from '@gitlab/ui';
-import { defaultSnippetVisibilityLevels } from '../utils/blob';
import { SNIPPET_LEVELS_RESTRICTED, SNIPPET_LEVELS_DISABLED } from '~/snippets/constants';
+import { defaultSnippetVisibilityLevels } from '../utils/blob';
export default {
components: {
diff --git a/app/assets/javascripts/snippets/utils/blob.js b/app/assets/javascripts/snippets/utils/blob.js
index a47418323f2..49ebd79845f 100644
--- a/app/assets/javascripts/snippets/utils/blob.js
+++ b/app/assets/javascripts/snippets/utils/blob.js
@@ -1,4 +1,6 @@
import { uniqueId } from 'lodash';
+import { performanceMarkAndMeasure } from '~/performance/utils';
+import { SNIPPET_MARK_BLOBS_CONTENT, SNIPPET_MEASURE_BLOBS_CONTENT } from '~/performance/constants';
import {
SNIPPET_BLOB_ACTION_CREATE,
SNIPPET_BLOB_ACTION_UPDATE,
@@ -7,8 +9,6 @@ import {
SNIPPET_LEVELS_MAP,
SNIPPET_VISIBILITY,
} from '../constants';
-import { performanceMarkAndMeasure } from '~/performance/utils';
-import { SNIPPET_MARK_BLOBS_CONTENT, SNIPPET_MEASURE_BLOBS_CONTENT } from '~/performance/constants';
const createLocalId = () => uniqueId('blob_local_');
diff --git a/app/assets/javascripts/static_site_editor/components/edit_area.vue b/app/assets/javascripts/static_site_editor/components/edit_area.vue
index b47126cdeb3..3eed05a5915 100644
--- a/app/assets/javascripts/static_site_editor/components/edit_area.vue
+++ b/app/assets/javascripts/static_site_editor/components/edit_area.vue
@@ -1,15 +1,15 @@
<script>
import RichContentEditor from '~/vue_shared/components/rich_content_editor/rich_content_editor.vue';
-import PublishToolbar from './publish_toolbar.vue';
-import EditHeader from './edit_header.vue';
-import EditDrawer from './edit_drawer.vue';
-import UnsavedChangesConfirmDialog from './unsaved_changes_confirm_dialog.vue';
import parseSourceFile from '~/static_site_editor/services/parse_source_file';
import { EDITOR_TYPES } from '~/vue_shared/components/rich_content_editor/constants';
import imageRepository from '../image_repository';
import formatter from '../services/formatter';
import templater from '../services/templater';
import renderImage from '../services/renderers/render_image';
+import UnsavedChangesConfirmDialog from './unsaved_changes_confirm_dialog.vue';
+import EditDrawer from './edit_drawer.vue';
+import EditHeader from './edit_header.vue';
+import PublishToolbar from './publish_toolbar.vue';
export default {
components: {
diff --git a/app/assets/javascripts/static_site_editor/components/edit_drawer.vue b/app/assets/javascripts/static_site_editor/components/edit_drawer.vue
index 0484d38dde0..0685dfdb1d1 100644
--- a/app/assets/javascripts/static_site_editor/components/edit_drawer.vue
+++ b/app/assets/javascripts/static_site_editor/components/edit_drawer.vue
@@ -22,11 +22,6 @@ export default {
<template>
<gl-drawer class="gl-pt-8" :open="isOpen" @close="$emit('close')">
<template #header>{{ __('Page settings') }}</template>
- <template>
- <front-matter-controls
- :settings="settings"
- @updateSettings="$emit('updateSettings', $event)"
- />
- </template>
+ <front-matter-controls :settings="settings" @updateSettings="$emit('updateSettings', $event)" />
</gl-drawer>
</template>
diff --git a/app/assets/javascripts/static_site_editor/components/edit_meta_modal.vue b/app/assets/javascripts/static_site_editor/components/edit_meta_modal.vue
index f583d2049af..5ecb242f96a 100644
--- a/app/assets/javascripts/static_site_editor/components/edit_meta_modal.vue
+++ b/app/assets/javascripts/static_site_editor/components/edit_meta_modal.vue
@@ -4,9 +4,8 @@ import { __, s__, sprintf } from '~/locale';
import Api from '~/api';
import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue';
-import EditMetaControls from './edit_meta_controls.vue';
-
import { ISSUABLE_TYPE, MR_META_LOCAL_STORAGE_KEY } from '../constants';
+import EditMetaControls from './edit_meta_controls.vue';
export default {
components: {
diff --git a/app/assets/javascripts/subscription_select.js b/app/assets/javascripts/subscription_select.js
index 354ee00a977..4a688d819b0 100644
--- a/app/assets/javascripts/subscription_select.js
+++ b/app/assets/javascripts/subscription_select.js
@@ -1,6 +1,6 @@
import $ from 'jquery';
-import { __ } from './locale';
import initDeprecatedJQueryDropdown from '~/deprecated_jquery_dropdown';
+import { __ } from './locale';
export default function subscriptionSelect() {
$('.js-subscription-event').each((i, element) => {
diff --git a/app/assets/javascripts/templates/issuable_template_selector.js b/app/assets/javascripts/templates/issuable_template_selector.js
index 22bbd083a5d..af9979b2502 100644
--- a/app/assets/javascripts/templates/issuable_template_selector.js
+++ b/app/assets/javascripts/templates/issuable_template_selector.js
@@ -1,9 +1,9 @@
/* eslint-disable no-useless-return */
import $ from 'jquery';
+import { __ } from '~/locale';
import Api from '../api';
import TemplateSelector from '../blob/template_selector';
-import { __ } from '~/locale';
export default class IssuableTemplateSelector extends TemplateSelector {
constructor(...args) {
diff --git a/app/assets/javascripts/terraform/components/states_table.vue b/app/assets/javascripts/terraform/components/states_table.vue
index d0d49233334..ab039608ef2 100644
--- a/app/assets/javascripts/terraform/components/states_table.vue
+++ b/app/assets/javascripts/terraform/components/states_table.vue
@@ -1,15 +1,16 @@
<script>
-import { GlBadge, GlIcon, GlLink, GlSprintf, GlTable, GlTooltip } from '@gitlab/ui';
+import { GlAlert, GlBadge, GlIcon, GlLink, GlSprintf, GlTable, GlTooltip } from '@gitlab/ui';
import { s__ } from '~/locale';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import CiBadge from '~/vue_shared/components/ci_badge_link.vue';
-import StateActions from './states_table_actions.vue';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
import timeagoMixin from '~/vue_shared/mixins/timeago';
+import StateActions from './states_table_actions.vue';
export default {
components: {
CiBadge,
+ GlAlert,
GlBadge,
GlIcon,
GlLink,
@@ -105,6 +106,7 @@ export default {
:items="states"
:fields="fields"
data-testid="terraform-states-table"
+ details-td-class="gl-p-0!"
fixed
stacked="md"
>
@@ -189,5 +191,21 @@ export default {
<template v-if="terraformAdmin" #cell(actions)="{ item }">
<state-actions :state="item" />
</template>
+
+ <template #row-details="row">
+ <gl-alert
+ data-testid="terraform-states-table-error"
+ variant="danger"
+ @dismiss="row.toggleDetails"
+ >
+ <span
+ v-for="errorMessage in row.item.errorMessages"
+ :key="errorMessage"
+ class="gl-display-flex gl-justify-content-start"
+ >
+ {{ errorMessage }}
+ </span>
+ </gl-alert>
+ </template>
</gl-table>
</template>
diff --git a/app/assets/javascripts/terraform/components/states_table_actions.vue b/app/assets/javascripts/terraform/components/states_table_actions.vue
index 44b0713e544..ba064825034 100644
--- a/app/assets/javascripts/terraform/components/states_table_actions.vue
+++ b/app/assets/javascripts/terraform/components/states_table_actions.vue
@@ -10,6 +10,7 @@ import {
GlSprintf,
} from '@gitlab/ui';
import { s__ } from '~/locale';
+import addDataToState from '../graphql/mutations/add_data_to_state.mutation.graphql';
import lockState from '../graphql/mutations/lock_state.mutation.graphql';
import unlockState from '../graphql/mutations/unlock_state.mutation.graphql';
import removeState from '../graphql/mutations/remove_state.mutation.graphql';
@@ -33,13 +34,13 @@ export default {
},
data() {
return {
- loading: false,
showRemoveModal: false,
removeConfirmText: '',
};
},
i18n: {
downloadJSON: s__('Terraform|Download JSON'),
+ errorUpdate: s__('Terraform|An error occurred while changing the state file'),
lock: s__('Terraform|Lock'),
modalBody: s__(
'Terraform|You are about to remove the State file %{name}. This will permanently delete all the State versions and history. The infrastructure provisioned previously will remain intact, only the state file with all its versions are to be removed. This action is non-revertible.',
@@ -76,19 +77,37 @@ export default {
this.removeConfirmText = '';
},
lock() {
- this.stateMutation(lockState);
+ this.stateActionMutation(lockState);
},
unlock() {
- this.stateMutation(unlockState);
+ this.stateActionMutation(unlockState);
+ },
+ updateStateCache(newData) {
+ this.$apollo.mutate({
+ mutation: addDataToState,
+ variables: {
+ terraformState: {
+ ...this.state,
+ ...newData,
+ },
+ },
+ });
},
remove() {
if (!this.disableModalSubmit) {
this.hideModal();
- this.stateMutation(removeState);
+ this.stateActionMutation(removeState);
}
},
- stateMutation(mutation) {
- this.loading = true;
+ stateActionMutation(mutation) {
+ let errorMessages = [];
+
+ this.updateStateCache({
+ _showDetails: false,
+ errorMessages,
+ loadingActions: true,
+ });
+
this.$apollo
.mutate({
mutation,
@@ -99,9 +118,22 @@ export default {
awaitRefetchQueries: true,
notifyOnNetworkStatusChange: true,
})
- .catch(() => {})
+ .then(({ data }) => {
+ errorMessages =
+ data?.terraformStateDelete?.errors ||
+ data?.terraformStateLock?.errors ||
+ data?.terraformStateUnlock?.errors ||
+ [];
+ })
+ .catch(() => {
+ errorMessages = [this.$options.i18n.errorUpdate];
+ })
.finally(() => {
- this.loading = false;
+ this.updateStateCache({
+ _showDetails: Boolean(errorMessages.length),
+ errorMessages,
+ loadingActions: false,
+ });
});
},
},
@@ -114,7 +146,7 @@ export default {
icon="ellipsis_v"
right
:data-testid="`terraform-state-actions-${state.name}`"
- :disabled="loading"
+ :disabled="state.loadingActions"
toggle-class="gl-px-3! gl-shadow-none!"
>
<template #button-content>
diff --git a/app/assets/javascripts/terraform/components/terraform_list.vue b/app/assets/javascripts/terraform/components/terraform_list.vue
index b71133d8813..ea533967ed9 100644
--- a/app/assets/javascripts/terraform/components/terraform_list.vue
+++ b/app/assets/javascripts/terraform/components/terraform_list.vue
@@ -1,9 +1,9 @@
<script>
import { GlAlert, GlBadge, GlKeysetPagination, GlLoadingIcon, GlTab, GlTabs } from '@gitlab/ui';
import getStatesQuery from '../graphql/queries/get_states.query.graphql';
+import { MAX_LIST_COUNT } from '../constants';
import EmptyState from './empty_state.vue';
import StatesTable from './states_table.vue';
-import { MAX_LIST_COUNT } from '../constants';
export default {
apollo: {
diff --git a/app/assets/javascripts/terraform/graphql/fragments/state.fragment.graphql b/app/assets/javascripts/terraform/graphql/fragments/state.fragment.graphql
index 49f9ae3dd97..f876fea64ac 100644
--- a/app/assets/javascripts/terraform/graphql/fragments/state.fragment.graphql
+++ b/app/assets/javascripts/terraform/graphql/fragments/state.fragment.graphql
@@ -2,6 +2,10 @@
#import "./state_version.fragment.graphql"
fragment State on TerraformState {
+ _showDetails @client
+ errorMessages @client
+ loadingActions @client
+
id
name
lockedAt
diff --git a/app/assets/javascripts/terraform/graphql/mutations/add_data_to_state.mutation.graphql b/app/assets/javascripts/terraform/graphql/mutations/add_data_to_state.mutation.graphql
new file mode 100644
index 00000000000..645b9766e2b
--- /dev/null
+++ b/app/assets/javascripts/terraform/graphql/mutations/add_data_to_state.mutation.graphql
@@ -0,0 +1,3 @@
+mutation addDataToTerraformState($terraformState: State!) {
+ addDataToTerraformState(terraformState: $terraformState) @client
+}
diff --git a/app/assets/javascripts/terraform/graphql/resolvers.js b/app/assets/javascripts/terraform/graphql/resolvers.js
new file mode 100644
index 00000000000..2845a1e5279
--- /dev/null
+++ b/app/assets/javascripts/terraform/graphql/resolvers.js
@@ -0,0 +1,41 @@
+import TerraformState from './fragments/state.fragment.graphql';
+
+export default {
+ TerraformState: {
+ _showDetails: (state) => {
+ // eslint-disable-next-line no-underscore-dangle
+ return state._showDetails || false;
+ },
+ errorMessages: (state) => {
+ return state.errorMessages || [];
+ },
+ loadingActions: (state) => {
+ return state.loadingActions || false;
+ },
+ },
+ Mutation: {
+ addDataToTerraformState: (_, { terraformState }, { client }) => {
+ const fragmentData = {
+ id: terraformState.id,
+ fragment: TerraformState,
+ // eslint-disable-next-line @gitlab/require-i18n-strings
+ fragmentName: 'State',
+ };
+
+ const previousTerraformState = client.readFragment(fragmentData);
+
+ if (previousTerraformState) {
+ client.writeFragment({
+ ...fragmentData,
+ data: {
+ ...previousTerraformState,
+ // eslint-disable-next-line no-underscore-dangle
+ _showDetails: terraformState._showDetails,
+ errorMessages: terraformState.errorMessages,
+ loadingActions: terraformState.loadingActions,
+ },
+ });
+ }
+ },
+ },
+};
diff --git a/app/assets/javascripts/terraform/index.js b/app/assets/javascripts/terraform/index.js
index e27a29433f3..f0a924d8a58 100644
--- a/app/assets/javascripts/terraform/index.js
+++ b/app/assets/javascripts/terraform/index.js
@@ -1,7 +1,9 @@
+import { defaultDataIdFromObject } from 'apollo-cache-inmemory';
import Vue from 'vue';
import VueApollo from 'vue-apollo';
-import TerraformList from './components/terraform_list.vue';
import createDefaultClient from '~/lib/graphql';
+import TerraformList from './components/terraform_list.vue';
+import resolvers from './graphql/resolvers';
Vue.use(VueApollo);
@@ -12,7 +14,13 @@ export default () => {
return null;
}
- const defaultClient = createDefaultClient();
+ const defaultClient = createDefaultClient(resolvers, {
+ cacheConfig: {
+ dataIdFromObject: (object) => {
+ return object.id || defaultDataIdFromObject(object);
+ },
+ },
+ });
const { emptyStateImage, projectPath } = el.dataset;
diff --git a/app/assets/javascripts/ui_development_kit.js b/app/assets/javascripts/ui_development_kit.js
index 028b047d9f5..1a3fd6c77ed 100644
--- a/app/assets/javascripts/ui_development_kit.js
+++ b/app/assets/javascripts/ui_development_kit.js
@@ -1,6 +1,6 @@
import $ from 'jquery';
-import Api from './api';
import initDeprecatedJQueryDropdown from '~/deprecated_jquery_dropdown';
+import Api from './api';
export default () => {
initDeprecatedJQueryDropdown($('#js-project-dropdown'), {
diff --git a/app/assets/javascripts/user_lists/store/show/mutations.js b/app/assets/javascripts/user_lists/store/show/mutations.js
index 3cf3b2d8371..bb5f9abe79e 100644
--- a/app/assets/javascripts/user_lists/store/show/mutations.js
+++ b/app/assets/javascripts/user_lists/store/show/mutations.js
@@ -1,6 +1,6 @@
import { states } from '../../constants/show';
-import * as types from './mutation_types';
import { parseUserIds } from '../utils';
+import * as types from './mutation_types';
export default {
[types.REQUEST_USER_LIST](state) {
diff --git a/app/assets/javascripts/users_select/index.js b/app/assets/javascripts/users_select/index.js
index 79dc20fd498..cab30a85e2a 100644
--- a/app/assets/javascripts/users_select/index.js
+++ b/app/assets/javascripts/users_select/index.js
@@ -8,14 +8,14 @@ import {
AJAX_USERS_SELECT_OPTIONS_MAP,
AJAX_USERS_SELECT_PARAMS_MAP,
} from 'ee_else_ce/users_select/constants';
+import initDeprecatedJQueryDropdown from '~/deprecated_jquery_dropdown';
+import { fixTitle, dispose } from '~/tooltips';
import axios from '../lib/utils/axios_utils';
import { s__, __, sprintf } from '../locale';
import ModalStore from '../boards/stores/modal_store';
import { parseBoolean, spriteIcon } from '../lib/utils/common_utils';
-import { getAjaxUsersSelectOptions, getAjaxUsersSelectParams } from './utils';
-import initDeprecatedJQueryDropdown from '~/deprecated_jquery_dropdown';
-import { fixTitle, dispose } from '~/tooltips';
import { loadCSSFile } from '../lib/utils/css_utils';
+import { getAjaxUsersSelectOptions, getAjaxUsersSelectParams } from './utils';
// TODO: remove eventHub hack after code splitting refactor
window.emitSidebarEvent = window.emitSidebarEvent || $.noop;
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 9b822657184..951dc108c51 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
@@ -2,6 +2,7 @@
import { GlButton } from '@gitlab/ui';
import { deprecatedCreateFlash as createFlash } from '~/flash';
import { s__ } from '~/locale';
+import { BV_SHOW_MODAL } from '~/lib/utils/constants';
import eventHub from '../../event_hub';
import approvalsMixin from '../../mixins/approvals';
import MrWidgetContainer from '../mr_widget_container.vue';
@@ -124,7 +125,7 @@ export default {
methods: {
approve() {
if (this.requirePasswordToApprove) {
- this.$root.$emit('bv::show::modal', this.modalId);
+ this.$root.$emit(BV_SHOW_MODAL, this.modalId);
return;
}
@@ -158,6 +159,7 @@ export default {
.then((data) => {
this.mr.setApprovals(data);
eventHub.$emit('MRWidgetUpdateRequested');
+ eventHub.$emit('ApprovalUpdated');
this.$emit('updated');
})
.catch(errFn)
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/artifacts_list_app.vue b/app/assets/javascripts/vue_merge_request_widget/components/artifacts_list_app.vue
index 730e67761be..ebf42fa0be0 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/artifacts_list_app.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/artifacts_list_app.vue
@@ -1,8 +1,8 @@
<script>
import { mapActions, mapState, mapGetters } from 'vuex';
+import createStore from '../stores/artifacts_list';
import ArtifactsList from './artifacts_list.vue';
import MrCollapsibleExtension from './mr_collapsible_extension.vue';
-import createStore from '../stores/artifacts_list';
export default {
store: createStore(),
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.vue
index 6628ab7be83..26693a8f51c 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.vue
@@ -133,14 +133,12 @@ export default {
v-gl-tooltip
:title="ideButtonTitle"
class="gl-display-none d-md-inline-block gl-mr-3"
- :tabindex="!mr.canPushToSourceBranch ? 0 : null"
+ :tabindex="ideButtonTitle ? 0 : null"
>
<gl-button
:href="webIdePath"
:disabled="!mr.canPushToSourceBranch"
class="js-web-ide"
- tabindex="0"
- role="button"
data-qa-selector="open_in_web_ide_button"
>
{{ s__('mrWidget|Open in Web IDE') }}
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 4c130945487..8084ad59f42 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
@@ -139,45 +139,38 @@ export default {
<div class="ci-widget media">
<template v-if="hasCIError">
<gl-icon name="status_failed" class="gl-text-red-500" :size="24" />
- <div
- class="gl-flex-fill-1 gl-ml-5"
- tabindex="0"
- role="text"
- :aria-label="$options.errorText"
- data-testid="ci-error-message"
- >
+ <p class="gl-flex-fill-1 gl-ml-5 gl-mb-0" data-testid="ci-error-message">
<gl-sprintf :message="$options.errorText">
<template #link="{ content }">
<gl-link :href="mrTroubleshootingDocsPath">{{ content }}</gl-link>
</template>
</gl-sprintf>
- </div>
+ </p>
</template>
<template v-else-if="!hasPipeline">
<gl-loading-icon size="md" />
- <div class="gl-flex-fill-1 gl-display-flex gl-ml-5" data-testid="monitoring-pipeline-message">
- <span tabindex="0" role="text" :aria-label="$options.monitoringPipelineText">
- <gl-sprintf :message="$options.monitoringPipelineText" />
- </span>
+ <p
+ class="gl-flex-fill-1 gl-display-flex gl-ml-5 gl-mb-0"
+ data-testid="monitoring-pipeline-message"
+ >
+ {{ $options.monitoringPipelineText }}
<gl-link
+ v-gl-tooltip
:href="ciTroubleshootingDocsPath"
target="_blank"
+ :title="__('About this feature')"
class="gl-display-flex gl-align-items-center gl-ml-2"
- tabindex="0"
>
<gl-icon
name="question"
- :size="12"
- tabindex="0"
- role="text"
:aria-label="__('Link to go to GitLab pipeline documentation')"
/>
</gl-link>
- </div>
+ </p>
</template>
<template v-else-if="hasPipeline">
<a :href="status.details_path" class="align-self-start gl-mr-3">
- <ci-icon :status="status" :size="24" :borderless="true" class="add-border" />
+ <ci-icon :status="status" :size="24" />
</a>
<div class="ci-widget-container d-flex">
<div class="ci-widget-content">
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline_container.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline_container.vue
index 99b55c0f9ee..2bf86c1863a 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline_container.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline_container.vue
@@ -1,10 +1,10 @@
<script>
import { isNumber } from 'lodash';
import { sanitize } from '~/lib/dompurify';
+import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import ArtifactsApp from './artifacts_list_app.vue';
import MrWidgetContainer from './mr_widget_container.vue';
import MrWidgetPipeline from './mr_widget_pipeline.vue';
-import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
/**
* Renders the pipeline and related deployments from the store.
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_status_icon.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_status_icon.vue
index bc23ca6b1fc..677c50ed930 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_status_icon.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_status_icon.vue
@@ -43,10 +43,9 @@ export default {
<gl-button
v-if="showDisabledButton"
- type="button"
category="primary"
variant="success"
- class="js-disabled-merge-button"
+ data-testid="disabled-merge-button"
:disabled="true"
>
{{ s__('mrWidget|Merge') }}
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_suggest_pipeline.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_suggest_pipeline.vue
index 7acdd695cc2..d2581f57837 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_suggest_pipeline.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_suggest_pipeline.vue
@@ -1,6 +1,5 @@
<script>
import { GlLink, GlSprintf, GlButton } from '@gitlab/ui';
-import MrWidgetIcon from './mr_widget_icon.vue';
import Tracking from '~/tracking';
import DismissibleContainer from '~/vue_shared/components/dismissible_container.vue';
import {
@@ -13,6 +12,7 @@ import {
SP_HELP_URL,
SP_ICON_NAME,
} from '../constants';
+import MrWidgetIcon from './mr_widget_icon.vue';
const trackingMixin = Tracking.mixin();
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_auto_merge_enabled.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_auto_merge_enabled.vue
index 20ac8f5a467..9c16bf78c93 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_auto_merge_enabled.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_auto_merge_enabled.vue
@@ -3,12 +3,13 @@ import { GlLoadingIcon, GlSkeletonLoader } from '@gitlab/ui';
import autoMergeMixin from 'ee_else_ce/vue_merge_request_widget/mixins/auto_merge';
import autoMergeEnabledQuery from 'ee_else_ce/vue_merge_request_widget/queries/states/auto_merge_enabled.query.graphql';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
+import { __ } from '~/locale';
+import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { deprecatedCreateFlash as Flash } from '../../../flash';
import statusIcon from '../mr_widget_status_icon.vue';
import MrWidgetAuthor from '../mr_widget_author.vue';
import eventHub from '../../event_hub';
import { AUTO_MERGE_STRATEGIES } from '../../constants';
-import { __ } from '~/locale';
import mergeRequestQueryVariablesMixin from '../../mixins/merge_request_query_variables';
export default {
@@ -53,7 +54,11 @@ export default {
},
computed: {
loading() {
- return this.glFeatures.mergeRequestWidgetGraphql && this.$apollo.queries.state.loading;
+ return (
+ this.glFeatures.mergeRequestWidgetGraphql &&
+ this.$apollo.queries.state.loading &&
+ Object.keys(this.state).length === 0
+ );
},
mergeUser() {
if (this.glFeatures.mergeRequestWidgetGraphql) {
@@ -78,7 +83,7 @@ export default {
canRemoveSourceBranch() {
const { currentUserId } = this.mr;
const mergeUserId = this.glFeatures.mergeRequestWidgetGraphql
- ? this.state.mergeUser?.id
+ ? getIdFromGraphQLId(this.state.mergeUser?.id)
: this.mr.mergeUserId;
const canRemoveSourceBranch = this.glFeatures.mergeRequestWidgetGraphql
? this.state.userPermissions.removeSourceBranch
@@ -96,7 +101,11 @@ export default {
.cancelAutomaticMerge()
.then((res) => res.data)
.then((data) => {
- eventHub.$emit('UpdateWidgetData', data);
+ if (this.glFeatures.mergeRequestWidgetGraphql) {
+ eventHub.$emit('MRWidgetUpdateRequested');
+ } else {
+ eventHub.$emit('UpdateWidgetData', data);
+ }
})
.catch(() => {
this.isCancellingAutoMerge = false;
@@ -119,6 +128,11 @@ export default {
eventHub.$emit('MRWidgetUpdateRequested');
}
})
+ .then(() => {
+ if (this.glFeatures.mergeRequestWidgetGraphql) {
+ this.$apollo.queries.state.refetch();
+ }
+ })
.catch(() => {
this.isRemovingSourceBranch = false;
Flash(__('Something went wrong. Please try again.'));
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_failed_to_merge.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_failed_to_merge.vue
index a5ec095b8ec..f411f91245d 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_failed_to_merge.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_failed_to_merge.vue
@@ -1,6 +1,6 @@
<script>
import { GlButton } from '@gitlab/ui';
-import { n__ } from '~/locale';
+import { sprintf, s__, n__ } from '~/locale';
import { stripHtml } from '~/lib/utils/text_utility';
import statusIcon from '../mr_widget_status_icon.vue';
import eventHub from '../../event_hub';
@@ -31,7 +31,15 @@ export default {
computed: {
mergeError() {
- return this.mr.mergeError ? stripHtml(this.mr.mergeError, ' ').trim() : '';
+ const mergeError = this.mr.mergeError ? stripHtml(this.mr.mergeError, ' ').trim() : '';
+
+ return sprintf(
+ s__('mrWidget|%{mergeError}.'),
+ {
+ mergeError,
+ },
+ false,
+ );
},
timerText() {
return n__(
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.vue
index 9d646dbfb3e..31302904b2d 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.vue
@@ -4,6 +4,8 @@ import { GlLoadingIcon, GlButton, GlTooltipDirective } from '@gitlab/ui';
import { deprecatedCreateFlash as Flash } from '~/flash';
import { s__, __ } from '~/locale';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
+import modalEventHub from '~/projects/commit/event_hub';
+import { OPEN_REVERT_MODAL } from '~/projects/commit/constants';
import MrWidgetAuthorTime from '../mr_widget_author_time.vue';
import statusIcon from '../mr_widget_status_icon.vue';
import eventHub from '../../event_hub';
@@ -77,6 +79,9 @@ export default {
return s__('mrWidget|Cherry-pick');
},
},
+ mounted() {
+ document.dispatchEvent(new CustomEvent('merged:UpdateActions'));
+ },
methods: {
removeSourceBranch() {
this.isMakingRequest = true;
@@ -98,6 +103,9 @@ export default {
Flash(__('Something went wrong. Please try again.'));
});
},
+ openRevertModal() {
+ modalEventHub.$emit(OPEN_REVERT_MODAL);
+ },
},
};
</script>
@@ -119,9 +127,7 @@ export default {
size="small"
category="secondary"
variant="warning"
- href="#modal-revert-commit"
- data-toggle="modal"
- data-container="body"
+ @click="openRevertModal"
>
{{ revertLabel }}
</gl-button>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_missing_branch.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_missing_branch.vue
index 3f68979bc0e..0a87edb0c89 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_missing_branch.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_missing_branch.vue
@@ -1,8 +1,8 @@
<script>
import { GlIcon, GlTooltipDirective } from '@gitlab/ui';
import { sprintf, s__ } from '~/locale';
-import statusIcon from '../mr_widget_status_icon.vue';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
+import statusIcon from '../mr_widget_status_icon.vue';
import mergeRequestQueryVariablesMixin from '../../mixins/merge_request_query_variables';
import missingBranchQuery from '../../queries/states/missing_branch.query.graphql';
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_rebase.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_rebase.vue
index bf86e0d8b07..5127ab3d400 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_rebase.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_rebase.vue
@@ -7,7 +7,7 @@ import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import simplePoll from '../../../lib/utils/simple_poll';
import eventHub from '../../event_hub';
import statusIcon from '../mr_widget_status_icon.vue';
-import rebaseQuery from '../../queries/states/ready_to_merge.query.graphql';
+import rebaseQuery from '../../queries/states/rebase.query.graphql';
import mergeRequestQueryVariablesMixin from '../../mixins/merge_request_query_variables';
import { deprecatedCreateFlash as Flash } from '../../../flash';
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 14a29483d3c..f0259a975db 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,9 +1,13 @@
<script>
/* eslint-disable vue/no-v-html */
+import { GlButton } from '@gitlab/ui';
import emptyStateSVG from 'icons/_mr_widget_empty_state.svg';
export default {
name: 'MRWidgetNothingToMerge',
+ components: {
+ GlButton,
+ },
props: {
mr: {
type: Object,
@@ -25,11 +29,13 @@ export default {
<span v-html="emptyStateSVG"></span>
</div>
<div class="text col-md-7 order-md-first col-12">
- <span>{{
- s__(
- 'mrWidgetNothingToMerge|Merge requests are a place to propose changes you have made to a project and discuss those changes with others.',
- )
- }}</span>
+ <p class="highlight">
+ {{
+ s__(
+ 'mrWidgetNothingToMerge|Merge requests are a place to propose changes you have made to a project and discuss those changes with others.',
+ )
+ }}
+ </p>
<p>
{{
s__(
@@ -45,9 +51,14 @@ export default {
}}
</p>
<div>
- <a v-if="mr.newBlobPath" :href="mr.newBlobPath" class="btn btn-inverted btn-success">{{
- __('Create file')
- }}</a>
+ <gl-button
+ v-if="mr.newBlobPath"
+ :href="mr.newBlobPath"
+ category="secondary"
+ variant="success"
+ >
+ {{ __('Create file') }}
+ </gl-button>
</div>
</div>
</div>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/pipeline_failed.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/pipeline_failed.vue
index 5f56157cb89..89c7a27b1bc 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/pipeline_failed.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/pipeline_failed.vue
@@ -1,11 +1,26 @@
<script>
+import { GlLink, GlSprintf } from '@gitlab/ui';
+import { s__ } from '~/locale';
+import { helpPagePath } from '~/helpers/help_page_helper';
import statusIcon from '../mr_widget_status_icon.vue';
export default {
name: 'PipelineFailed',
components: {
+ GlLink,
+ GlSprintf,
statusIcon,
},
+ computed: {
+ troubleshootingDocsPath() {
+ return helpPagePath('ci/troubleshooting', { anchor: 'merge-request-status-messages' });
+ },
+ },
+ i18n: {
+ failedMessage: s__(
+ `mrWidget|The pipeline for this merge request did not complete. Push a new commit to fix the failure or check the %{linkStart}troubleshooting documentation%{linkEnd} to see other possible actions.`,
+ ),
+ },
};
</script>
@@ -14,10 +29,13 @@ export default {
<status-icon :show-disabled-button="true" status="warning" />
<div class="media-body space-children">
<span class="bold">
- {{
- s__(`mrWidget|The pipeline for this merge request failed.
-Please retry the job or push a new commit to fix the failure`)
- }}
+ <gl-sprintf :message="$options.i18n.failedMessage">
+ <template #link="{ content }">
+ <gl-link :href="troubleshootingDocsPath" target="_blank">
+ {{ content }}
+ </gl-link>
+ </template>
+ </gl-sprintf>
</span>
</div>
</div>
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 a890b176df0..58337ea8f67 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
@@ -15,19 +15,19 @@ import readyToMergeMixin from 'ee_else_ce/vue_merge_request_widget/mixins/ready_
import readyToMergeQuery from 'ee_else_ce/vue_merge_request_widget/queries/states/ready_to_merge.query.graphql';
import simplePoll from '~/lib/utils/simple_poll';
import { __ } from '~/locale';
-import MergeRequest from '../../../merge_request';
import { refreshUserMergeRequestCounts } from '~/commons/nav/user_merge_requests';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
+import MergeRequest from '../../../merge_request';
import mergeRequestQueryVariablesMixin from '../../mixins/merge_request_query_variables';
import { deprecatedCreateFlash as Flash } from '../../../flash';
import MergeRequestStore from '../../stores/mr_widget_store';
import statusIcon from '../mr_widget_status_icon.vue';
import eventHub from '../../event_hub';
+import { AUTO_MERGE_STRATEGIES, DANGER, INFO, WARNING } from '../../constants';
import SquashBeforeMerge from './squash_before_merge.vue';
import CommitsHeader from './commits_header.vue';
import CommitEdit from './commit_edit.vue';
import CommitMessageDropdown from './commit_message_dropdown.vue';
-import { AUTO_MERGE_STRATEGIES, DANGER, INFO, WARNING } from '../../constants';
const PIPELINE_RUNNING_STATE = 'running';
const PIPELINE_FAILED_STATE = 'failed';
@@ -53,8 +53,8 @@ export default {
result({ data }) {
this.state = {
...data.project.mergeRequest,
- mergeRequestsFfOnlyEnabled: data.mergeRequestsFfOnlyEnabled,
- onlyAllowMergeIfPipelineSucceeds: data.onlyAllowMergeIfPipelineSucceeds,
+ mergeRequestsFfOnlyEnabled: data.project.mergeRequestsFfOnlyEnabled,
+ onlyAllowMergeIfPipelineSucceeds: data.project.onlyAllowMergeIfPipelineSucceeds,
};
this.removeSourceBranch = data.project.mergeRequest.shouldRemoveSourceBranch;
this.commitMessage = data.project.mergeRequest.defaultMergeCommitMessage;
@@ -277,7 +277,20 @@ export default {
return this.mr.mergeRequestDiffsPath;
},
},
+ mounted() {
+ if (this.glFeatures.mergeRequestWidgetGraphql) {
+ eventHub.$on('ApprovalUpdated', this.updateGraphqlState);
+ }
+ },
+ beforeDestroy() {
+ if (this.glFeatures.mergeRequestWidgetGraphql) {
+ eventHub.$off('ApprovalUpdated', this.updateGraphqlState);
+ }
+ },
methods: {
+ updateGraphqlState() {
+ return this.$apollo.queries.state.refetch();
+ },
updateMergeCommitMessage(includeDescription) {
const commitMessage = this.glFeatures.mergeRequestWidgetGraphql
? this.state.defaultMergeCommitMessage
@@ -326,6 +339,10 @@ export default {
} else if (hasError) {
eventHub.$emit('FailedToMerge', data.merge_error);
}
+
+ if (this.glFeatures.mergeRequestWidgetGraphql) {
+ this.updateGraphqlState();
+ }
})
.catch(() => {
this.isMakingRequest = false;
@@ -532,7 +549,7 @@ export default {
</div>
<merge-train-helper-text
v-if="shouldRenderMergeTrainHelperText"
- :pipeline-id="pipeline.id"
+ :pipeline-id="pipelineId"
:pipeline-link="pipeline.path"
:merge-train-length="stateData.mergeTrainsCount"
:merge-train-when-pipeline-succeeds-docs-path="mr.mergeTrainWhenPipelineSucceedsDocsPath"
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 78e69b9ff9b..329964d009a 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
@@ -1,7 +1,7 @@
<script>
import { GlButton } from '@gitlab/ui';
-import statusIcon from '../mr_widget_status_icon.vue';
import notesEventHub from '~/notes/event_hub';
+import statusIcon from '../mr_widget_status_icon.vue';
export default {
name: 'UnresolvedDiscussions',
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/terraform/mr_widget_terraform_container.vue b/app/assets/javascripts/vue_merge_request_widget/components/terraform/mr_widget_terraform_container.vue
index 180db7828a8..ce04fa1cef5 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/terraform/mr_widget_terraform_container.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/terraform/mr_widget_terraform_container.vue
@@ -2,8 +2,8 @@
import { GlDeprecatedSkeletonLoading as GlSkeletonLoading, GlSprintf } from '@gitlab/ui';
import { n__ } from '~/locale';
import axios from '~/lib/utils/axios_utils';
-import MrWidgetExpanableSection from '../mr_widget_expandable_section.vue';
import Poll from '~/lib/utils/poll';
+import MrWidgetExpanableSection from '../mr_widget_expandable_section.vue';
import TerraformPlan from './terraform_plan.vue';
export default {
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/terraform/terraform_plan.vue b/app/assets/javascripts/vue_merge_request_widget/components/terraform/terraform_plan.vue
index b74e036d9d9..5f65d1fa49a 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/terraform/terraform_plan.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/terraform/terraform_plan.vue
@@ -15,6 +15,16 @@ export default {
type: Object,
},
},
+ i18n: {
+ changes: s__(
+ 'Terraform|Reported Resource Changes: %{addNum} to add, %{changeNum} to change, %{deleteNum} to delete',
+ ),
+ generationErrored: s__('Terraform|Generating the report caused an error.'),
+ namedReportFailed: s__('Terraform|The report %{name} failed to generate.'),
+ namedReportGenerated: s__('Terraform|The report %{name} was generated in your pipelines.'),
+ reportFailed: s__('Terraform|A report failed to generate.'),
+ reportGenerated: s__('Terraform|A report was generated in your pipelines.'),
+ },
computed: {
addNum() {
return Number(this.plan.create);
@@ -30,23 +40,21 @@ export default {
},
reportChangeText() {
if (this.validPlanValues) {
- return s__(
- 'Terraform|Reported Resource Changes: %{addNum} to add, %{changeNum} to change, %{deleteNum} to delete',
- );
+ return this.$options.i18n.changes;
}
- return s__('Terraform|Generating the report caused an error.');
+ return this.$options.i18n.generationErrored;
},
reportHeaderText() {
if (this.validPlanValues) {
return this.plan.job_name
- ? s__('Terraform|The Terraform report %{name} was generated in your pipelines.')
- : s__('Terraform|A Terraform report was generated in your pipelines.');
+ ? this.$options.i18n.namedReportGenerated
+ : this.$options.i18n.reportGenerated;
}
return this.plan.job_name
- ? s__('Terraform|The Terraform report %{name} failed to generate.')
- : s__('Terraform|A Terraform report failed to generate.');
+ ? this.$options.i18n.namedReportFailed
+ : this.$options.i18n.reportFailed;
},
validPlanValues() {
return this.addNum + this.changeNum + this.deleteNum >= 0;
diff --git a/app/assets/javascripts/vue_merge_request_widget/index.js b/app/assets/javascripts/vue_merge_request_widget/index.js
index d512877a20d..c1c491f6fe0 100644
--- a/app/assets/javascripts/vue_merge_request_widget/index.js
+++ b/app/assets/javascripts/vue_merge_request_widget/index.js
@@ -1,8 +1,12 @@
+// This is a false violation of @gitlab/no-runtime-template-compiler, since it
+// creates a new Vue instance by spreading a _valid_ Vue component definition
+// into the Vue constructor.
+/* eslint-disable @gitlab/no-runtime-template-compiler */
import Vue from 'vue';
-import MrWidgetOptions from 'ee_else_ce/vue_merge_request_widget/mr_widget_options.vue';
import VueApollo from 'vue-apollo';
-import Translate from '../vue_shared/translate';
+import MrWidgetOptions from 'ee_else_ce/vue_merge_request_widget/mr_widget_options.vue';
import createDefaultClient from '~/lib/graphql';
+import Translate from '../vue_shared/translate';
import { registerExtension } from './components/extensions';
import issueExtension from './extensions/issues';
diff --git a/app/assets/javascripts/vue_merge_request_widget/mixins/ready_to_merge.js b/app/assets/javascripts/vue_merge_request_widget/mixins/ready_to_merge.js
index fe512d68ea2..23215982e6e 100644
--- a/app/assets/javascripts/vue_merge_request_widget/mixins/ready_to_merge.js
+++ b/app/assets/javascripts/vue_merge_request_widget/mixins/ready_to_merge.js
@@ -35,5 +35,8 @@ export default {
shouldRenderMergeTrainHelperText() {
return false;
},
+ pipelineId() {
+ return this.pipeline.id;
+ },
},
};
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 519576d9fe6..83370901cc3 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,15 +1,20 @@
<script>
import { isEmpty } from 'lodash';
+import { GlSafeHtmlDirective } from '@gitlab/ui';
import MRWidgetStore from 'ee_else_ce/vue_merge_request_widget/stores/mr_widget_store';
import MRWidgetService from 'ee_else_ce/vue_merge_request_widget/services/mr_widget_service';
import MrWidgetApprovals from 'ee_else_ce/vue_merge_request_widget/components/approvals/approvals.vue';
import stateMaps from 'ee_else_ce/vue_merge_request_widget/stores/state_maps';
-import { GlSafeHtmlDirective } from '@gitlab/ui';
import { sprintf, s__, __ } from '~/locale';
import Project from '~/pages/projects/project';
import SmartInterval from '~/smart_interval';
import { secondsToMilliseconds } from '~/lib/utils/datetime_utility';
+import notify from '~/lib/utils/notify';
import { deprecatedCreateFlash as createFlash } from '../flash';
+import GroupedCodequalityReportsApp from '../reports/codequality_report/grouped_codequality_reports_app.vue';
+import GroupedTestReportsApp from '../reports/components/grouped_test_reports_app.vue';
+import { setFaviconOverlay } from '../lib/utils/favicon';
+import GroupedAccessibilityReportsApp from '../reports/accessibility_report/grouped_accessibility_reports_app.vue';
import mergeRequestQueryVariablesMixin from './mixins/merge_request_query_variables';
import Loading from './components/loading.vue';
import WidgetHeader from './components/mr_widget_header.vue';
@@ -38,13 +43,8 @@ import AutoMergeFailed from './components/states/mr_widget_auto_merge_failed.vue
import CheckingState from './components/states/mr_widget_checking.vue';
// import ExtensionsContainer from './components/extensions/container';
import eventHub from './event_hub';
-import notify from '~/lib/utils/notify';
import SourceBranchRemovalStatus from './components/source_branch_removal_status.vue';
import TerraformPlan from './components/terraform/mr_widget_terraform_container.vue';
-import GroupedCodequalityReportsApp from '../reports/codequality_report/grouped_codequality_reports_app.vue';
-import GroupedTestReportsApp from '../reports/components/grouped_test_reports_app.vue';
-import { setFaviconOverlay } from '../lib/utils/favicon';
-import GroupedAccessibilityReportsApp from '../reports/accessibility_report/grouped_accessibility_reports_app.vue';
import getStateQuery from './queries/get_state.query.graphql';
export default {
@@ -94,7 +94,6 @@ export default {
state: {
query: getStateQuery,
manual: true,
- pollInterval: 10 * 1000,
skip() {
return !this.mr || !window.gon?.features?.mergeRequestWidgetGraphql;
},
@@ -181,7 +180,7 @@ export default {
);
},
shouldRenderSecurityReport() {
- return Boolean(window.gon?.features?.coreSecurityMrWidget && this.mr.pipeline.id);
+ return Boolean(this.mr.pipeline.id);
},
mergeError() {
let { mergeError } = this.mr;
@@ -191,7 +190,7 @@ export default {
}
return sprintf(
- s__('mrWidget|Merge failed: %{mergeError}. Please try again.'),
+ s__('mrWidget|%{mergeError}. Try again.'),
{
mergeError,
},
@@ -286,6 +285,10 @@ export default {
return new MRWidgetService(this.getServiceEndpoints(store));
},
checkStatus(cb, isRebased) {
+ if (window.gon?.features?.mergeRequestWidgetGraphql) {
+ this.$apollo.queries.state.refetch();
+ }
+
return this.service
.checkStatus()
.then(({ data }) => {
@@ -365,6 +368,7 @@ export default {
const el = document.createElement('div');
el.innerHTML = res.data;
document.body.appendChild(el);
+ document.dispatchEvent(new CustomEvent('merged:UpdateActions'));
Project.initRefSwitcher();
}
})
@@ -464,6 +468,7 @@ export default {
:head-path="mr.codeclimate.head_path"
:head-blob-path="mr.headBlobPath"
:base-blob-path="mr.baseBlobPath"
+ :codequality-reports-path="mr.codequalityReportsPath"
:codequality-help-path="mr.codequalityHelpPath"
/>
diff --git a/app/assets/javascripts/vue_merge_request_widget/queries/get_state.query.graphql b/app/assets/javascripts/vue_merge_request_widget/queries/get_state.query.graphql
index 44fc1cc7f23..b284bb23969 100644
--- a/app/assets/javascripts/vue_merge_request_widget/queries/get_state.query.graphql
+++ b/app/assets/javascripts/vue_merge_request_widget/queries/get_state.query.graphql
@@ -18,6 +18,7 @@ query getState($projectPath: ID!, $iid: String!) {
}
shouldBeRebased
sourceBranchExists
+ state
targetBranchExists
userPermissions {
canMerge
diff --git a/app/assets/javascripts/vue_merge_request_widget/queries/states/auto_merge_enabled.fragment.graphql b/app/assets/javascripts/vue_merge_request_widget/queries/states/auto_merge_enabled.fragment.graphql
index 64cd70fcf42..ad715599eb1 100644
--- a/app/assets/javascripts/vue_merge_request_widget/queries/states/auto_merge_enabled.fragment.graphql
+++ b/app/assets/javascripts/vue_merge_request_widget/queries/states/auto_merge_enabled.fragment.graphql
@@ -1,6 +1,7 @@
fragment autoMergeEnabled on MergeRequest {
autoMergeStrategy
mergeUser {
+ id
name
username
webUrl
diff --git a/app/assets/javascripts/vue_merge_request_widget/queries/states/auto_merge_enabled.query.graphql b/app/assets/javascripts/vue_merge_request_widget/queries/states/auto_merge_enabled.query.graphql
index bdcb7a8206b..daf21e75b3b 100644
--- a/app/assets/javascripts/vue_merge_request_widget/queries/states/auto_merge_enabled.query.graphql
+++ b/app/assets/javascripts/vue_merge_request_widget/queries/states/auto_merge_enabled.query.graphql
@@ -4,7 +4,6 @@ query autoMergeEnabledQuery($projectPath: ID!, $iid: String!) {
project(fullPath: $projectPath) {
mergeRequest(iid: $iid) {
...autoMergeEnabled
- mergeTrainsCount
}
}
}
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 a6bbab47a06..78a17493d31 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
@@ -1,9 +1,9 @@
import { format } from 'timeago.js';
import getStateKey from 'ee_else_ce/vue_merge_request_widget/stores/get_state_key';
import mrEventHub from '~/merge_request/eventhub';
-import { stateKey } from './state_maps';
import { formatDate } from '../../lib/utils/datetime_utility';
import { MTWPS_MERGE_STRATEGY, MT_MERGE_STRATEGY, MWPS_MERGE_STRATEGY } from '../constants';
+import { stateKey } from './state_maps';
export default class MergeRequestStore {
constructor(data) {
@@ -156,9 +156,9 @@ export default class MergeRequestStore {
this.setState();
- mrEventHub.$emit('mr.state.updated', {
- state: this.mergeRequestState,
- });
+ if (!window.gon?.features?.mergeRequestWidgetGraphql) {
+ this.emitUpdatedState();
+ }
}
setGraphqlData(project) {
@@ -182,7 +182,9 @@ export default class MergeRequestStore {
this.isSHAMismatch = this.sha !== mergeRequest.diffHeadSha;
this.shouldBeRebased = mergeRequest.shouldBeRebased;
this.workInProgress = mergeRequest.workInProgress;
+ this.mergeRequestState = mergeRequest.state;
+ this.emitUpdatedState();
this.setState();
}
@@ -208,6 +210,12 @@ export default class MergeRequestStore {
}
}
+ emitUpdatedState() {
+ mrEventHub.$emit('mr.state.updated', {
+ state: this.mergeRequestState,
+ });
+ }
+
setPaths(data) {
// Paths are set on the first load of the page and not auto-refreshed
this.squashBeforeMergeHelpPath = data.squash_before_merge_help_path;
@@ -241,10 +249,11 @@ export default class MergeRequestStore {
this.isDismissedSuggestPipeline = data.is_dismissed_suggest_pipeline;
this.securityReportsDocsPath = data.security_reports_docs_path;
- // codeclimate
+ // code quality
const blobPath = data.blob_path || {};
this.headBlobPath = blobPath.head_path || '';
this.baseBlobPath = blobPath.base_path || '';
+ this.codequalityReportsPath = data.codequality_reports_path;
this.codequalityHelpPath = data.codequality_help_path;
this.codeclimate = data.codeclimate;
diff --git a/app/assets/javascripts/alert_management/components/alert_details.vue b/app/assets/javascripts/vue_shared/alert_details/components/alert_details.vue
index 895c6e76019..673c6f4a1eb 100644
--- a/app/assets/javascripts/alert_management/components/alert_details.vue
+++ b/app/assets/javascripts/vue_shared/alert_details/components/alert_details.vue
@@ -13,22 +13,22 @@ import {
} from '@gitlab/ui';
import * as Sentry from '~/sentry/wrapper';
import { s__ } from '~/locale';
-import alertQuery from '../graphql/queries/details.query.graphql';
-import sidebarStatusQuery from '../graphql/queries/sidebar_status.query.graphql';
import { fetchPolicies } from '~/lib/graphql';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
import highlightCurrentUser from '~/behaviors/markdown/highlight_current_user';
import initUserPopovers from '~/user_popovers';
-import { ALERTS_SEVERITY_LABELS, trackAlertsDetailsViewsOptions } from '../constants';
-import createIssueMutation from '../graphql/mutations/create_issue_from_alert.mutation.graphql';
-import toggleSidebarStatusMutation from '../graphql/mutations/toggle_sidebar_status.mutation.graphql';
import { visitUrl, joinPaths } from '~/lib/utils/url_utility';
import Tracking from '~/tracking';
import { toggleContainerClasses } from '~/lib/utils/dom_utils';
+import AlertDetailsTable from '~/vue_shared/components/alert_details_table.vue';
+import alertQuery from '../graphql/queries/alert_details.query.graphql';
+import sidebarStatusQuery from '../graphql/queries/alert_sidebar_status.query.graphql';
+import { SEVERITY_LEVELS } from '../constants';
+import createIssueMutation from '../graphql/mutations/alert_issue_create.mutation.graphql';
+import toggleSidebarStatusMutation from '../graphql/mutations/alert_sidebar_status.mutation.graphql';
import SystemNote from './system_notes/system_note.vue';
import AlertSidebar from './alert_sidebar.vue';
import AlertMetrics from './alert_metrics.vue';
-import AlertDetailsTable from '~/vue_shared/components/alert_details_table.vue';
import AlertSummaryRow from './alert_summary_row.vue';
const containerEl = document.querySelector('.page-with-contextual-sidebar');
@@ -44,7 +44,7 @@ export default {
directives: {
SafeHtml: GlSafeHtmlDirective,
},
- severityLabels: ALERTS_SEVERITY_LABELS,
+ severityLabels: SEVERITY_LEVELS,
tabsConfig: [
{
id: 'overview',
@@ -89,6 +89,9 @@ export default {
projectIssuesPath: {
default: '',
},
+ trackAlertsDetailsViewsOptions: {
+ default: null,
+ },
},
apollo: {
alert: {
@@ -155,7 +158,9 @@ export default {
},
},
mounted() {
- this.trackPageViews();
+ if (this.trackAlertsDetailsViewsOptions) {
+ this.trackPageViews();
+ }
toggleContainerClasses(containerEl, {
'issuable-bulk-update-sidebar': true,
'right-sidebar-expanded': true,
@@ -217,7 +222,7 @@ export default {
return joinPaths(this.projectIssuesPath, issueId);
},
trackPageViews() {
- const { category, action } = trackAlertsDetailsViewsOptions;
+ const { category, action } = this.trackAlertsDetailsViewsOptions;
Tracking.event(category, action);
},
},
diff --git a/app/assets/javascripts/alert_management/components/alert_metrics.vue b/app/assets/javascripts/vue_shared/alert_details/components/alert_metrics.vue
index dd4faa03c00..dd4faa03c00 100644
--- a/app/assets/javascripts/alert_management/components/alert_metrics.vue
+++ b/app/assets/javascripts/vue_shared/alert_details/components/alert_metrics.vue
diff --git a/app/assets/javascripts/alert_management/components/alert_sidebar.vue b/app/assets/javascripts/vue_shared/alert_details/components/alert_sidebar.vue
index 41d77716592..12c58b582c5 100644
--- a/app/assets/javascripts/alert_management/components/alert_sidebar.vue
+++ b/app/assets/javascripts/vue_shared/alert_details/components/alert_sidebar.vue
@@ -1,11 +1,10 @@
<script>
+import sidebarStatusQuery from '../graphql/queries/alert_sidebar_status.query.graphql';
import SidebarHeader from './sidebar/sidebar_header.vue';
import SidebarTodo from './sidebar/sidebar_todo.vue';
import SidebarStatus from './sidebar/sidebar_status.vue';
import SidebarAssignees from './sidebar/sidebar_assignees.vue';
-import sidebarStatusQuery from '../graphql/queries/sidebar_status.query.graphql';
-
export default {
components: {
SidebarAssignees,
diff --git a/app/assets/javascripts/alert_management/components/alert_status.vue b/app/assets/javascripts/vue_shared/alert_details/components/alert_status.vue
index 2afdeb8b6fd..5520f7ff045 100644
--- a/app/assets/javascripts/alert_management/components/alert_status.vue
+++ b/app/assets/javascripts/vue_shared/alert_details/components/alert_status.vue
@@ -2,8 +2,7 @@
import { GlDropdown, GlDropdownItem } from '@gitlab/ui';
import { s__ } from '~/locale';
import Tracking from '~/tracking';
-import { trackAlertStatusUpdateOptions } from '../constants';
-import updateAlertStatusMutation from '~/graphql_shared/mutations/update_alert_status.mutation.graphql';
+import updateAlertStatusMutation from '~/graphql_shared/mutations/alert_status_update.mutation.graphql';
export default {
i18n: {
@@ -21,6 +20,11 @@ export default {
GlDropdown,
GlDropdownItem,
},
+ inject: {
+ trackAlertStatusUpdateOptions: {
+ default: null,
+ },
+ },
props: {
projectPath: {
type: String,
@@ -58,7 +62,9 @@ export default {
},
})
.then((resp) => {
- this.trackStatusUpdate(status);
+ if (this.trackAlertStatusUpdateOptions) {
+ this.trackStatusUpdate(status);
+ }
const errors = resp.data?.updateAlertStatus?.errors || [];
if (errors[0]) {
@@ -81,7 +87,7 @@ export default {
});
},
trackStatusUpdate(status) {
- const { category, action, label } = trackAlertStatusUpdateOptions;
+ const { category, action, label } = this.trackAlertStatusUpdateOptions;
Tracking.event(category, action, { label, property: status });
},
},
diff --git a/app/assets/javascripts/alert_management/components/alert_summary_row.vue b/app/assets/javascripts/vue_shared/alert_details/components/alert_summary_row.vue
index 13835b7e2fa..13835b7e2fa 100644
--- a/app/assets/javascripts/alert_management/components/alert_summary_row.vue
+++ b/app/assets/javascripts/vue_shared/alert_details/components/alert_summary_row.vue
diff --git a/app/assets/javascripts/alert_management/components/sidebar/sidebar_assignee.vue b/app/assets/javascripts/vue_shared/alert_details/components/sidebar/sidebar_assignee.vue
index c39a72a45b9..c39a72a45b9 100644
--- a/app/assets/javascripts/alert_management/components/sidebar/sidebar_assignee.vue
+++ b/app/assets/javascripts/vue_shared/alert_details/components/sidebar/sidebar_assignee.vue
diff --git a/app/assets/javascripts/alert_management/components/sidebar/sidebar_assignees.vue b/app/assets/javascripts/vue_shared/alert_details/components/sidebar/sidebar_assignees.vue
index 2a999b908f9..2a999b908f9 100644
--- a/app/assets/javascripts/alert_management/components/sidebar/sidebar_assignees.vue
+++ b/app/assets/javascripts/vue_shared/alert_details/components/sidebar/sidebar_assignees.vue
diff --git a/app/assets/javascripts/alert_management/components/sidebar/sidebar_header.vue b/app/assets/javascripts/vue_shared/alert_details/components/sidebar/sidebar_header.vue
index 70902a204f8..fd40b5d9f65 100644
--- a/app/assets/javascripts/alert_management/components/sidebar/sidebar_header.vue
+++ b/app/assets/javascripts/vue_shared/alert_details/components/sidebar/sidebar_header.vue
@@ -27,7 +27,7 @@ export default {
<template>
<div class="block gl-display-flex gl-justify-content-space-between">
<span class="issuable-header-text hide-collapsed">
- {{ __('To-Do') }}
+ {{ __('To Do') }}
</span>
<sidebar-todo
v-if="!sidebarCollapsed"
diff --git a/app/assets/javascripts/alert_management/components/sidebar/sidebar_status.vue b/app/assets/javascripts/vue_shared/alert_details/components/sidebar/sidebar_status.vue
index 0a2bad5510b..0a2bad5510b 100644
--- a/app/assets/javascripts/alert_management/components/sidebar/sidebar_status.vue
+++ b/app/assets/javascripts/vue_shared/alert_details/components/sidebar/sidebar_status.vue
diff --git a/app/assets/javascripts/alert_management/components/sidebar/sidebar_todo.vue b/app/assets/javascripts/vue_shared/alert_details/components/sidebar/sidebar_todo.vue
index 485395bcac2..216b429fcbe 100644
--- a/app/assets/javascripts/alert_management/components/sidebar/sidebar_todo.vue
+++ b/app/assets/javascripts/vue_shared/alert_details/components/sidebar/sidebar_todo.vue
@@ -2,14 +2,14 @@
import produce from 'immer';
import { s__ } from '~/locale';
import Todo from '~/sidebar/components/todo_toggle/todo.vue';
-import createAlertTodoMutation from '../../graphql/mutations/alert_todo_create.mutation.graphql';
import todoMarkDoneMutation from '~/graphql_shared/mutations/todo_mark_done.mutation.graphql';
-import alertQuery from '../../graphql/queries/details.query.graphql';
+import createAlertTodoMutation from '../../graphql/mutations/alert_todo_create.mutation.graphql';
+import alertQuery from '../../graphql/queries/alert_details.query.graphql';
export default {
i18n: {
UPDATE_ALERT_TODO_ERROR: s__(
- 'AlertManagement|There was an error while updating the To-Do of the alert.',
+ 'AlertManagement|There was an error while updating the to-do item of the alert.',
),
},
components: {
diff --git a/app/assets/javascripts/alert_management/components/system_notes/system_note.vue b/app/assets/javascripts/vue_shared/alert_details/components/system_notes/system_note.vue
index 3705e36a579..3705e36a579 100644
--- a/app/assets/javascripts/alert_management/components/system_notes/system_note.vue
+++ b/app/assets/javascripts/vue_shared/alert_details/components/system_notes/system_note.vue
diff --git a/app/assets/javascripts/vue_shared/alert_details/constants.js b/app/assets/javascripts/vue_shared/alert_details/constants.js
new file mode 100644
index 00000000000..56f79410064
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/alert_details/constants.js
@@ -0,0 +1,29 @@
+import { s__ } from '~/locale';
+
+export const SEVERITY_LEVELS = {
+ CRITICAL: s__('severity|Critical'),
+ HIGH: s__('severity|High'),
+ MEDIUM: s__('severity|Medium'),
+ LOW: s__('severity|Low'),
+ INFO: s__('severity|Info'),
+ UNKNOWN: s__('severity|Unknown'),
+};
+
+export const DEFAULT_PAGE = 'OPERATIONS';
+
+/* eslint-disable @gitlab/require-i18n-strings */
+export const PAGE_CONFIG = {
+ OPERATIONS: {
+ // Tracks snowplow event when user views alert details
+ TRACK_ALERTS_DETAILS_VIEWS_OPTIONS: {
+ category: 'Alert Management',
+ action: 'view_alert_details',
+ },
+ // Tracks snowplow event when alert status is updated
+ TRACK_ALERT_STATUS_UPDATE_OPTIONS: {
+ category: 'Alert Management',
+ action: 'update_alert_status',
+ label: 'Status',
+ },
+ },
+};
diff --git a/app/assets/javascripts/alert_management/graphql/fragments/detail_item.fragment.graphql b/app/assets/javascripts/vue_shared/alert_details/graphql/fragments/alert_detail_item.fragment.graphql
index 9a9ae369519..9a9ae369519 100644
--- a/app/assets/javascripts/alert_management/graphql/fragments/detail_item.fragment.graphql
+++ b/app/assets/javascripts/vue_shared/alert_details/graphql/fragments/alert_detail_item.fragment.graphql
diff --git a/app/assets/javascripts/alert_management/graphql/mutations/create_issue_from_alert.mutation.graphql b/app/assets/javascripts/vue_shared/alert_details/graphql/mutations/alert_issue_create.mutation.graphql
index bc4d91a51d1..bc4d91a51d1 100644
--- a/app/assets/javascripts/alert_management/graphql/mutations/create_issue_from_alert.mutation.graphql
+++ b/app/assets/javascripts/vue_shared/alert_details/graphql/mutations/alert_issue_create.mutation.graphql
diff --git a/app/assets/javascripts/alert_management/graphql/mutations/alert_set_assignees.mutation.graphql b/app/assets/javascripts/vue_shared/alert_details/graphql/mutations/alert_set_assignees.mutation.graphql
index 63d952a4857..63d952a4857 100644
--- a/app/assets/javascripts/alert_management/graphql/mutations/alert_set_assignees.mutation.graphql
+++ b/app/assets/javascripts/vue_shared/alert_details/graphql/mutations/alert_set_assignees.mutation.graphql
diff --git a/app/assets/javascripts/alert_management/graphql/mutations/toggle_sidebar_status.mutation.graphql b/app/assets/javascripts/vue_shared/alert_details/graphql/mutations/alert_sidebar_status.mutation.graphql
index f666fcd6782..f666fcd6782 100644
--- a/app/assets/javascripts/alert_management/graphql/mutations/toggle_sidebar_status.mutation.graphql
+++ b/app/assets/javascripts/vue_shared/alert_details/graphql/mutations/alert_sidebar_status.mutation.graphql
diff --git a/app/assets/javascripts/alert_management/graphql/mutations/alert_todo_create.mutation.graphql b/app/assets/javascripts/vue_shared/alert_details/graphql/mutations/alert_todo_create.mutation.graphql
index ac9858c104f..dc961b5eb90 100644
--- a/app/assets/javascripts/alert_management/graphql/mutations/alert_todo_create.mutation.graphql
+++ b/app/assets/javascripts/vue_shared/alert_details/graphql/mutations/alert_todo_create.mutation.graphql
@@ -1,4 +1,4 @@
-#import "../fragments/detail_item.fragment.graphql"
+#import "../fragments/alert_detail_item.fragment.graphql"
mutation alertTodoCreate($projectPath: ID!, $iid: String!) {
alertTodoCreate(input: { iid: $iid, projectPath: $projectPath }) {
diff --git a/app/assets/javascripts/alert_management/graphql/queries/details.query.graphql b/app/assets/javascripts/vue_shared/alert_details/graphql/queries/alert_details.query.graphql
index 8881f49b689..5ee2cf7ca44 100644
--- a/app/assets/javascripts/alert_management/graphql/queries/details.query.graphql
+++ b/app/assets/javascripts/vue_shared/alert_details/graphql/queries/alert_details.query.graphql
@@ -1,4 +1,4 @@
-#import "../fragments/detail_item.fragment.graphql"
+#import "../fragments/alert_detail_item.fragment.graphql"
query alertDetails($fullPath: ID!, $alertId: String) {
project(fullPath: $fullPath) {
diff --git a/app/assets/javascripts/alert_management/graphql/queries/sidebar_status.query.graphql b/app/assets/javascripts/vue_shared/alert_details/graphql/queries/alert_sidebar_status.query.graphql
index 61c570c5cd0..61c570c5cd0 100644
--- a/app/assets/javascripts/alert_management/graphql/queries/sidebar_status.query.graphql
+++ b/app/assets/javascripts/vue_shared/alert_details/graphql/queries/alert_sidebar_status.query.graphql
diff --git a/app/assets/javascripts/alert_management/details.js b/app/assets/javascripts/vue_shared/alert_details/index.js
index 4217b702d0a..643d6b3a3fe 100644
--- a/app/assets/javascripts/alert_management/details.js
+++ b/app/assets/javascripts/vue_shared/alert_details/index.js
@@ -4,14 +4,15 @@ import Vue from 'vue';
import VueApollo from 'vue-apollo';
import createDefaultClient from '~/lib/graphql';
import AlertDetails from './components/alert_details.vue';
-import sidebarStatusQuery from './graphql/queries/sidebar_status.query.graphql';
+import sidebarStatusQuery from './graphql/queries/alert_sidebar_status.query.graphql';
import createRouter from './router';
+import { DEFAULT_PAGE, PAGE_CONFIG } from './constants';
Vue.use(VueApollo);
export default (selector) => {
const domEl = document.querySelector(selector);
- const { alertId, projectPath, projectIssuesPath, projectId } = domEl.dataset;
+ const { alertId, projectPath, projectIssuesPath, projectId, page = DEFAULT_PAGE } = domEl.dataset;
const router = createRouter();
const resolvers = {
@@ -48,18 +49,28 @@ export default (selector) => {
},
});
+ const provide = {
+ projectPath,
+ alertId,
+ projectIssuesPath,
+ projectId,
+ };
+
+ if (page === DEFAULT_PAGE) {
+ const { TRACK_ALERTS_DETAILS_VIEWS_OPTIONS, TRACK_ALERT_STATUS_UPDATE_OPTIONS } = PAGE_CONFIG[
+ page
+ ];
+ provide.trackAlertsDetailsViewsOptions = TRACK_ALERTS_DETAILS_VIEWS_OPTIONS;
+ provide.trackAlertStatusUpdateOptions = TRACK_ALERT_STATUS_UPDATE_OPTIONS;
+ }
+
// eslint-disable-next-line no-new
new Vue({
el: selector,
components: {
AlertDetails,
},
- provide: {
- projectPath,
- alertId,
- projectIssuesPath,
- projectId,
- },
+ provide,
apolloProvider,
router,
render(createElement) {
diff --git a/app/assets/javascripts/alert_management/router.js b/app/assets/javascripts/vue_shared/alert_details/router.js
index 5687fe4e0f5..5687fe4e0f5 100644
--- a/app/assets/javascripts/alert_management/router.js
+++ b/app/assets/javascripts/vue_shared/alert_details/router.js
diff --git a/app/assets/javascripts/vue_shared/components/awards_list.vue b/app/assets/javascripts/vue_shared/components/awards_list.vue
index c1da2b8c305..4fefce47da5 100644
--- a/app/assets/javascripts/vue_shared/components/awards_list.vue
+++ b/app/assets/javascripts/vue_shared/components/awards_list.vue
@@ -2,8 +2,8 @@
/* eslint-disable vue/no-v-html */
import { groupBy } from 'lodash';
import { GlIcon, GlButton, GlTooltipDirective } from '@gitlab/ui';
-import { glEmojiTag } from '../../emoji';
import { __, sprintf } from '~/locale';
+import { glEmojiTag } from '../../emoji';
// Internal constant, specific to this component, used when no `currentUserId` is given
const NO_USER_ID = -1;
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 d0f5570db6b..98e6fdead3a 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,8 +1,8 @@
<script>
import { GlSafeHtmlDirective as SafeHtml } from '@gitlab/ui';
import MarkdownFieldView from '~/vue_shared/components/markdown/field_view.vue';
-import ViewerMixin from './mixins';
import { handleBlobRichViewer } from '~/blob/viewer';
+import ViewerMixin from './mixins';
export default {
components: {
diff --git a/app/assets/javascripts/vue_shared/components/color_picker/color_picker.vue b/app/assets/javascripts/vue_shared/components/color_picker/color_picker.vue
index 6977692e30c..0ff33e462b4 100644
--- a/app/assets/javascripts/vue_shared/components/color_picker/color_picker.vue
+++ b/app/assets/javascripts/vue_shared/components/color_picker/color_picker.vue
@@ -3,12 +3,16 @@
* Renders a color picker input with preset colors to choose from
*
* @example
- * <color-picker :label="__('Background color')" set-color="#FF0000" />
+ * <color-picker
+ :invalid-feedback="__('Please enter a valid hex (#RRGGBB or #RGB) color value')"
+ :label="__('Background color')"
+ :value="#FF0000"
+ state="isValidColor"
+ />
*/
import { GlFormGroup, GlFormInput, GlFormInputGroup, GlLink, GlTooltipDirective } from '@gitlab/ui';
import { __ } from '~/locale';
-const VALID_RGB_HEX_COLOR = /^#([0-9A-F]{3}){1,2}$/i;
const PREVIEW_COLOR_DEFAULT_CLASSES =
'gl-relative gl-w-7 gl-bg-gray-10 gl-rounded-top-left-base gl-rounded-bottom-left-base';
@@ -24,21 +28,26 @@ export default {
GlTooltip: GlTooltipDirective,
},
props: {
+ invalidFeedback: {
+ type: String,
+ required: false,
+ default: __('Please enter a valid hex (#RRGGBB or #RGB) color value'),
+ },
label: {
type: String,
required: false,
default: '',
},
- setColor: {
+ value: {
type: String,
required: false,
default: '',
},
- },
- data() {
- return {
- selectedColor: this.setColor.trim() || '',
- };
+ state: {
+ type: Boolean,
+ required: false,
+ default: null,
+ },
},
computed: {
description() {
@@ -50,46 +59,30 @@ export default {
return gon.suggested_label_colors;
},
previewColor() {
- if (this.isValidColor) {
- return { backgroundColor: this.selectedColor };
+ if (this.state) {
+ return { backgroundColor: this.value };
}
return {};
},
previewColorClasses() {
- const borderStyle = this.isInvalidColor
- ? 'gl-inset-border-1-red-500'
- : 'gl-inset-border-1-gray-400';
+ const borderStyle =
+ this.state === false ? 'gl-inset-border-1-red-500' : 'gl-inset-border-1-gray-400';
return `${PREVIEW_COLOR_DEFAULT_CLASSES} ${borderStyle}`;
},
hasSuggestedColors() {
return Object.keys(this.suggestedColors).length;
},
- isInvalidColor() {
- return this.isValidColor === false;
- },
- isValidColor() {
- if (this.selectedColor === '') {
- return null;
- }
-
- return VALID_RGB_HEX_COLOR.test(this.selectedColor);
- },
},
methods: {
handleColorChange(color) {
- this.selectedColor = color.trim();
-
- if (this.isValidColor) {
- this.$emit('input', this.selectedColor);
- }
+ this.$emit('input', color.trim());
},
},
i18n: {
fullDescription: __('Choose any color. Or you can choose one of the suggested colors below'),
shortDescription: __('Choose any color'),
- invalid: __('Please enter a valid hex (#RRGGBB or #RGB) color value'),
},
};
</script>
@@ -100,17 +93,17 @@ export default {
:label="label"
label-for="color-picker"
:description="description"
- :invalid-feedback="this.$options.i18n.invalid"
- :state="isValidColor"
+ :invalid-feedback="invalidFeedback"
+ :state="state"
:class="{ 'gl-mb-3!': hasSuggestedColors }"
>
<gl-form-input-group
id="color-picker"
- :state="isValidColor"
max-length="7"
type="text"
class="gl-align-center gl-rounded-0 gl-rounded-top-right-base gl-rounded-bottom-right-base"
- :value="selectedColor"
+ :value="value"
+ :state="state"
@input="handleColorChange"
>
<template #prepend>
@@ -119,7 +112,7 @@ export default {
type="color"
class="gl-absolute gl-top-0 gl-left-0 gl-h-full! gl-p-0! gl-m-0! gl-cursor-pointer gl-opacity-0"
tabindex="-1"
- :value="selectedColor"
+ :value="value"
@input="handleColorChange"
/>
</div>
diff --git a/app/assets/javascripts/vue_shared/components/diff_viewer/viewers/image_diff_viewer.vue b/app/assets/javascripts/vue_shared/components/diff_viewer/viewers/image_diff_viewer.vue
index 00033145603..31dbe6c4c1a 100644
--- a/app/assets/javascripts/vue_shared/components/diff_viewer/viewers/image_diff_viewer.vue
+++ b/app/assets/javascripts/vue_shared/components/diff_viewer/viewers/image_diff_viewer.vue
@@ -1,9 +1,9 @@
<script>
import ImageViewer from '../../content_viewer/viewers/image_viewer.vue';
+import { diffModes, imageViewMode } from '../constants';
import TwoUpViewer from './image_diff/two_up_viewer.vue';
import SwipeViewer from './image_diff/swipe_viewer.vue';
import OnionSkinViewer from './image_diff/onion_skin_viewer.vue';
-import { diffModes, imageViewMode } from '../constants';
export default {
components: {
diff --git a/app/assets/javascripts/vue_shared/components/dropdown/dropdown_button.vue b/app/assets/javascripts/vue_shared/components/dropdown/dropdown_button.vue
index 7e82d8f3f9c..eb8400e81c7 100644
--- a/app/assets/javascripts/vue_shared/components/dropdown/dropdown_button.vue
+++ b/app/assets/javascripts/vue_shared/components/dropdown/dropdown_button.vue
@@ -36,10 +36,8 @@ export default {
aria-expanded="false"
>
<gl-loading-icon v-show="isLoading" :inline="true" />
- <template>
- <slot v-if="$slots.default"></slot>
- <span v-else class="dropdown-toggle-text"> {{ toggleText }} </span>
- </template>
+ <slot v-if="$slots.default"></slot>
+ <span v-else class="dropdown-toggle-text"> {{ toggleText }} </span>
<gl-icon
v-show="!isLoading"
class="gl-absolute gl-top-3 gl-right-3 gl-text-gray-500"
diff --git a/app/assets/javascripts/vue_shared/components/editor_lite.vue b/app/assets/javascripts/vue_shared/components/editor_lite.vue
index 7218b84cf8a..b5b5330973c 100644
--- a/app/assets/javascripts/vue_shared/components/editor_lite.vue
+++ b/app/assets/javascripts/vue_shared/components/editor_lite.vue
@@ -1,7 +1,7 @@
<script>
import { debounce } from 'lodash';
import Editor from '~/editor/editor_lite';
-import { CONTENT_UPDATE_DEBOUNCE } from '~/editor/constants';
+import { CONTENT_UPDATE_DEBOUNCE, EDITOR_READY_EVENT } from '~/editor/constants';
function initEditorLite({ el, ...args }) {
const editor = new Editor({
@@ -88,6 +88,7 @@ export default {
return this.editor;
},
},
+ readyEvent: EDITOR_READY_EVENT,
};
</script>
<template>
@@ -95,7 +96,7 @@ export default {
:id="`editor-lite-${fileGlobalId}`"
ref="editor"
data-editor-loading
- @editor-ready="$emit('editor-ready')"
+ @[$options.readyEvent]="$emit($options.readyEvent)"
>
<pre class="editor-loading-content">{{ value }}</pre>
</div>
diff --git a/app/assets/javascripts/vue_shared/components/file_finder/index.vue b/app/assets/javascripts/vue_shared/components/file_finder/index.vue
index 27933f87929..c3cebf079ea 100644
--- a/app/assets/javascripts/vue_shared/components/file_finder/index.vue
+++ b/app/assets/javascripts/vue_shared/components/file_finder/index.vue
@@ -3,8 +3,8 @@ import fuzzaldrinPlus from 'fuzzaldrin-plus';
import Mousetrap from 'mousetrap';
import VirtualList from 'vue-virtual-scroll-list';
import { GlIcon } from '@gitlab/ui';
-import Item from './item.vue';
import { UP_KEY_CODE, DOWN_KEY_CODE, ENTER_KEY_CODE, ESC_KEY_CODE } from '~/lib/utils/keycodes';
+import Item from './item.vue';
export const MAX_FILE_FINDER_RESULTS = 40;
export const FILE_FINDER_ROW_HEIGHT = 55;
diff --git a/app/assets/javascripts/vue_shared/components/file_icon.vue b/app/assets/javascripts/vue_shared/components/file_icon.vue
index 6190b07962d..8ac8a3beb7d 100644
--- a/app/assets/javascripts/vue_shared/components/file_icon.vue
+++ b/app/assets/javascripts/vue_shared/components/file_icon.vue
@@ -1,7 +1,7 @@
<script>
import { GlLoadingIcon, GlIcon } from '@gitlab/ui';
-import getIconForFile from './file_icon/file_icon_map';
import { FILE_SYMLINK_MODE } from '../constants';
+import getIconForFile from './file_icon/file_icon_map';
/* This is a re-usable vue component for rendering a svg sprite
icon
@@ -90,6 +90,12 @@ export default {
<svg v-else-if="!folder" :key="spriteHref" :class="[iconSizeClass, cssClasses]">
<use v-bind="{ 'xlink:href': spriteHref }" />
</svg>
- <gl-icon v-else :name="folderIconName" :size="size" class="folder-icon" />
+ <gl-icon
+ v-else
+ :name="folderIconName"
+ :size="size"
+ class="folder-icon"
+ data-qa-selector="folder_icon_content"
+ />
</span>
</template>
diff --git a/app/assets/javascripts/vue_shared/components/file_row.vue b/app/assets/javascripts/vue_shared/components/file_row.vue
index 96567111bbc..17ebdce232a 100644
--- a/app/assets/javascripts/vue_shared/components/file_row.vue
+++ b/app/assets/javascripts/vue_shared/components/file_row.vue
@@ -147,6 +147,7 @@ export default {
:style="levelIndentation"
class="file-row-name"
data-qa-selector="file_name_content"
+ :data-qa-file-name="file.name"
data-testid="file-row-name-container"
:class="[fileClasses, { 'str-truncated': !truncateMiddle, 'gl-min-w-0': truncateMiddle }]"
>
diff --git a/app/assets/javascripts/vue_shared/components/gfm_autocomplete/utils.js b/app/assets/javascripts/vue_shared/components/gfm_autocomplete/utils.js
index 809932b0f29..44c3fc34ba6 100644
--- a/app/assets/javascripts/vue_shared/components/gfm_autocomplete/utils.js
+++ b/app/assets/javascripts/vue_shared/components/gfm_autocomplete/utils.js
@@ -84,7 +84,7 @@ export const tributeConfig = {
value.type === groupType ? last(value.name.split(' / ')) : `${value.name}${value.username}`,
menuItemLimit: memberLimit,
menuItemTemplate: ({ original }) => {
- const commonClasses = 'gl-avatar gl-avatar-s24 gl-flex-shrink-0';
+ const commonClasses = 'gl-avatar gl-avatar-s32 gl-flex-shrink-0';
const noAvatarClasses = `${commonClasses} gl-rounded-small
gl-display-flex gl-align-items-center gl-justify-content-center`;
@@ -111,7 +111,7 @@ export const tributeConfig = {
return `
<div class="gl-display-flex gl-align-items-center">
${avatar}
- <div class="gl-font-sm gl-line-height-normal gl-ml-3">
+ <div class="gl-line-height-normal gl-ml-4">
<div>${escape(displayName)}${count}</div>
<div class="gl-text-gray-700">${escape(parentGroupOrUsername)}</div>
</div>
diff --git a/app/assets/javascripts/vue_shared/components/gl_modal_vuex.vue b/app/assets/javascripts/vue_shared/components/gl_modal_vuex.vue
index e14f6a04d3c..c9e5f433c3c 100644
--- a/app/assets/javascripts/vue_shared/components/gl_modal_vuex.vue
+++ b/app/assets/javascripts/vue_shared/components/gl_modal_vuex.vue
@@ -1,6 +1,7 @@
<script>
import { mapState, mapActions } from 'vuex';
import { GlModal } from '@gitlab/ui';
+import { BV_SHOW_MODAL, BV_HIDE_MODAL } from '~/lib/utils/constants';
/**
* This component keeps the GlModal's visibility in sync with the given vuex module.
@@ -46,11 +47,11 @@ export default {
},
}),
bsShow() {
- this.$root.$emit('bv::show::modal', this.modalId);
+ this.$root.$emit(BV_SHOW_MODAL, this.modalId);
},
bsHide() {
// $root.$emit is a workaround because other b-modal approaches don't work yet with gl-modal
- this.$root.$emit('bv::hide::modal', this.modalId);
+ this.$root.$emit(BV_HIDE_MODAL, this.modalId);
},
cancel() {
this.$emit('cancel');
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 79d9ba6df57..c882e894e7a 100644
--- a/app/assets/javascripts/vue_shared/components/header_ci_component.vue
+++ b/app/assets/javascripts/vue_shared/components/header_ci_component.vue
@@ -1,11 +1,11 @@
<script>
/* eslint-disable vue/no-v-html */
import { GlTooltipDirective, GlLink, GlButton, GlTooltip } from '@gitlab/ui';
+import { glEmojiTag } from '../../emoji';
+import { __, sprintf } from '../../locale';
import CiIconBadge from './ci_badge_link.vue';
import TimeagoTooltip from './time_ago_tooltip.vue';
import UserAvatarImage from './user_avatar/user_avatar_image.vue';
-import { glEmojiTag } from '../../emoji';
-import { __, sprintf } from '../../locale';
/**
* Renders header component for job and pipeline page based on UI mockups
@@ -148,7 +148,7 @@ export default {
<gl-button
v-if="hasSidebarButton"
class="d-sm-none js-sidebar-build-toggle gl-ml-auto"
- icon="angle-double-left"
+ icon="chevron-double-lg-left"
:aria-label="__('Toggle sidebar')"
@click="onClickSidebarButton"
/>
diff --git a/app/assets/javascripts/vue_shared/components/help_popover.vue b/app/assets/javascripts/vue_shared/components/help_popover.vue
index 821ae6cec52..051c65bae70 100644
--- a/app/assets/javascripts/vue_shared/components/help_popover.vue
+++ b/app/assets/javascripts/vue_shared/components/help_popover.vue
@@ -1,8 +1,5 @@
<script>
-import $ from 'jquery';
-import { GlButton } from '@gitlab/ui';
-import { inserted } from '~/feature_highlight/feature_highlight_helper';
-import { mouseenter, debouncedMouseleave, togglePopover } from '~/shared/popover';
+import { GlButton, GlPopover } from '@gitlab/ui';
/**
* Render a button with a question mark icon
@@ -12,6 +9,7 @@ export default {
name: 'HelpPopover',
components: {
GlButton,
+ GlPopover,
},
props: {
options: {
@@ -20,28 +18,20 @@ export default {
default: () => ({}),
},
},
- mounted() {
- const $el = $(this.$el);
-
- $el
- .popover({
- html: true,
- trigger: 'focus',
- container: 'body',
- placement: 'top',
- template:
- '<div class="popover" role="tooltip"><div class="arrow"></div><p class="popover-header"></p><div class="popover-body"></div></div>',
- ...this.options,
- })
- .on('mouseenter', mouseenter)
- .on('mouseleave', debouncedMouseleave(300))
- .on('inserted.bs.popover', inserted)
- .on('show.bs.popover', () => {
- window.addEventListener('scroll', togglePopover.bind($el, false), { once: true });
- });
- },
};
</script>
<template>
- <gl-button variant="link" icon="question" tabindex="0" />
+ <span>
+ <gl-button ref="popoverTrigger" variant="link" icon="question" tabindex="0" />
+ <gl-popover triggers="hover focus" :target="() => $refs.popoverTrigger.$el" v-bind="options">
+ <template #title>
+ <!-- eslint-disable-next-line vue/no-v-html -->
+ <span v-html="options.title"></span>
+ </template>
+ <template #default>
+ <!-- eslint-disable-next-line vue/no-v-html -->
+ <div v-html="options.content"></div>
+ </template>
+ </gl-popover>
+ </span>
</template>
diff --git a/app/assets/javascripts/vue_shared/components/issue/related_issuable_item.vue b/app/assets/javascripts/vue_shared/components/issue/related_issuable_item.vue
index 2ff4033a07e..d5a8e10f5c0 100644
--- a/app/assets/javascripts/vue_shared/components/issue/related_issuable_item.vue
+++ b/app/assets/javascripts/vue_shared/components/issue/related_issuable_item.vue
@@ -3,11 +3,11 @@
import '~/commons/bootstrap';
import { GlIcon, GlTooltip, GlTooltipDirective, GlButton } from '@gitlab/ui';
import { sprintf } from '~/locale';
-import IssueMilestone from './issue_milestone.vue';
-import IssueAssignees from './issue_assignees.vue';
import IssueDueDate from '~/boards/components/issue_due_date.vue';
import relatedIssuableMixin from '../../mixins/related_issuable_mixin';
import CiIcon from '../ci_icon.vue';
+import IssueAssignees from './issue_assignees.vue';
+import IssueMilestone from './issue_milestone.vue';
export default {
name: 'IssueItem',
diff --git a/app/assets/javascripts/vue_shared/components/markdown/field.vue b/app/assets/javascripts/vue_shared/components/markdown/field.vue
index b6e167524aa..d75bff04e32 100644
--- a/app/assets/javascripts/vue_shared/components/markdown/field.vue
+++ b/app/assets/javascripts/vue_shared/components/markdown/field.vue
@@ -8,12 +8,12 @@ import { __, sprintf } from '~/locale';
import { stripHtml } from '~/lib/utils/text_utility';
import { deprecatedCreateFlash as Flash } from '~/flash';
import GLForm from '~/gl_form';
-import MarkdownHeader from './header.vue';
-import MarkdownToolbar from './toolbar.vue';
import GfmAutocomplete from '~/vue_shared/components/gfm_autocomplete/gfm_autocomplete.vue';
import Suggestions from '~/vue_shared/components/markdown/suggestions.vue';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import axios from '~/lib/utils/axios_utils';
+import MarkdownToolbar from './toolbar.vue';
+import MarkdownHeader from './header.vue';
export default {
components: {
@@ -110,11 +110,6 @@ export default {
return this.referencedUsers.length >= referencedUsersThreshold;
},
lineContent() {
- const [firstSuggestion] = this.suggestions;
- if (firstSuggestion) {
- return firstSuggestion.from_content;
- }
-
if (this.line) {
const { rich_text: richText, text } = this.line;
diff --git a/app/assets/javascripts/vue_shared/components/markdown/header.vue b/app/assets/javascripts/vue_shared/components/markdown/header.vue
index 173d192dab0..7ea1a2e5ee8 100644
--- a/app/assets/javascripts/vue_shared/components/markdown/header.vue
+++ b/app/assets/javascripts/vue_shared/components/markdown/header.vue
@@ -172,6 +172,7 @@ export default {
:cursor-offset="4"
:tag-content="lineContent"
icon="doc-code"
+ data-qa-selector="suggestion_button"
class="js-suggestion-btn"
@click="handleSuggestDismissed"
/>
diff --git a/app/assets/javascripts/vue_shared/components/markdown/suggestion_diff.vue b/app/assets/javascripts/vue_shared/components/markdown/suggestion_diff.vue
index 93a270b8a97..bcd8c02e968 100644
--- a/app/assets/javascripts/vue_shared/components/markdown/suggestion_diff.vue
+++ b/app/assets/javascripts/vue_shared/components/markdown/suggestion_diff.vue
@@ -1,7 +1,7 @@
<script>
+import { selectDiffLines } from '../lib/utils/diff_utils';
import SuggestionDiffHeader from './suggestion_diff_header.vue';
import SuggestionDiffRow from './suggestion_diff_row.vue';
-import { selectDiffLines } from '../lib/utils/diff_utils';
export default {
components: {
diff --git a/app/assets/javascripts/vue_shared/components/markdown/suggestion_diff_header.vue b/app/assets/javascripts/vue_shared/components/markdown/suggestion_diff_header.vue
index 63341b433e0..4c6fa71398d 100644
--- a/app/assets/javascripts/vue_shared/components/markdown/suggestion_diff_header.vue
+++ b/app/assets/javascripts/vue_shared/components/markdown/suggestion_diff_header.vue
@@ -88,7 +88,12 @@ export default {
applySuggestion(message) {
if (!this.canApply) return;
this.isApplyingSingle = true;
- this.$emit('apply', this.applySuggestionCallback, message);
+
+ this.$emit(
+ 'apply',
+ this.applySuggestionCallback,
+ gon.features?.suggestionsCustomCommit ? message : undefined,
+ );
},
applySuggestionCallback() {
this.isApplyingSingle = false;
@@ -131,6 +136,7 @@ export default {
<gl-button
v-gl-tooltip.viewport="__('This also resolves all related threads')"
class="btn-inverted js-apply-batch-btn btn-grouped"
+ data-qa-selector="apply_suggestions_batch_button"
:disabled="isApplying"
variant="success"
@click="applySuggestionBatch"
@@ -145,6 +151,7 @@ export default {
<gl-button
v-if="suggestionsCount > 1 && canBeBatched && !isDisableButton"
class="btn-inverted js-add-to-batch-btn btn-grouped"
+ data-qa-selector="add_suggestion_batch_button"
:disabled="isDisableButton"
@click="addSuggestionToBatch"
>
diff --git a/app/assets/javascripts/vue_shared/components/markdown/suggestions.vue b/app/assets/javascripts/vue_shared/components/markdown/suggestions.vue
index 5ee51764555..c86f16e0254 100644
--- a/app/assets/javascripts/vue_shared/components/markdown/suggestions.vue
+++ b/app/assets/javascripts/vue_shared/components/markdown/suggestions.vue
@@ -2,8 +2,8 @@
import Vue from 'vue';
import { GlSafeHtmlDirective as SafeHtml } from '@gitlab/ui';
import { __ } from '~/locale';
-import SuggestionDiff from './suggestion_diff.vue';
import { deprecatedCreateFlash as Flash } from '~/flash';
+import SuggestionDiff from './suggestion_diff.vue';
export default {
directives: {
@@ -64,6 +64,11 @@ export default {
mounted() {
this.renderSuggestions();
},
+ beforeDestroy() {
+ if (this.suggestionsWatch) {
+ this.suggestionsWatch();
+ }
+ },
methods: {
renderSuggestions() {
// swaps out suggestion(s) markdown with rich diff components
@@ -108,6 +113,13 @@ export default {
},
});
+ // We're using `$watch` as `suggestionsCount` updates do not
+ // propagate to this component for some unknown reason while
+ // using a traditional prop watcher.
+ this.suggestionsWatch = this.$watch('suggestionsCount', () => {
+ suggestionDiff.suggestionsCount = this.suggestionsCount;
+ });
+
suggestionDiff.$on('apply', ({ suggestionId, callback, message }) => {
this.$emit('apply', { suggestionId, callback, flashContainer: this.$el, message });
});
diff --git a/app/assets/javascripts/vue_shared/components/markdown/toolbar.vue b/app/assets/javascripts/vue_shared/components/markdown/toolbar.vue
index 15c5b9d6733..387b100a04f 100644
--- a/app/assets/javascripts/vue_shared/components/markdown/toolbar.vue
+++ b/app/assets/javascripts/vue_shared/components/markdown/toolbar.vue
@@ -60,9 +60,7 @@ export default {
</div>
<span v-if="canAttachFile" class="uploading-container">
<span class="uploading-progress-container hide">
- <template>
- <gl-icon name="media" />
- </template>
+ <gl-icon name="media" />
<span class="attaching-file-message"></span>
<!-- eslint-disable-next-line @gitlab/vue-require-i18n-strings -->
<span class="uploading-progress">0%</span>
diff --git a/app/assets/javascripts/vue_shared/components/modal_copy_button.vue b/app/assets/javascripts/vue_shared/components/modal_copy_button.vue
index e3a7f144321..7b36d57dfbf 100644
--- a/app/assets/javascripts/vue_shared/components/modal_copy_button.vue
+++ b/app/assets/javascripts/vue_shared/components/modal_copy_button.vue
@@ -2,6 +2,7 @@
import { GlButton, GlTooltipDirective } from '@gitlab/ui';
import Clipboard from 'clipboard';
import { uniqueId } from 'lodash';
+import { BV_HIDE_TOOLTIP } from '~/lib/utils/constants';
export default {
components: {
@@ -76,7 +77,7 @@ export default {
});
this.clipboard
.on('success', (e) => {
- this.$root.$emit('bv::hide::tooltip', this.id);
+ this.$root.$emit(BV_HIDE_TOOLTIP, this.id);
this.$emit('success', e);
// Clear the selection and blur the trigger so it loses its border
e.clearSelection();
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 cc1203f83f0..c66b1112bf4 100644
--- a/app/assets/javascripts/vue_shared/components/notes/system_note.vue
+++ b/app/assets/javascripts/vue_shared/components/notes/system_note.vue
@@ -30,9 +30,9 @@ import {
import descriptionVersionHistoryMixin from 'ee_else_ce/notes/mixins/description_version_history';
import noteHeader from '~/notes/components/note_header.vue';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
-import TimelineEntryItem from './timeline_entry_item.vue';
-import { spriteIcon } from '../../../lib/utils/common_utils';
import initMRPopovers from '~/mr_popover/';
+import { spriteIcon } from '../../../lib/utils/common_utils';
+import TimelineEntryItem from './timeline_entry_item.vue';
const MAX_VISIBLE_COMMIT_LIST_COUNT = 3;
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 d03987bbbe0..7b766e2f818 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
@@ -4,10 +4,10 @@ import Api from '~/api';
import Tracking from '~/tracking';
import { __ } from '~/locale';
import { updateHistory, setUrlParams } from '~/lib/utils/url_utility';
-import { initialPaginationState, defaultI18n, defaultPageSize } from './constants';
-import { isAny } from './utils';
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 { initialPaginationState, defaultI18n, defaultPageSize } from './constants';
+import { isAny } from './utils';
export default {
defaultI18n,
diff --git a/app/assets/javascripts/vue_shared/components/registry/list_item.vue b/app/assets/javascripts/vue_shared/components/registry/list_item.vue
index 8965dba3e83..9db5d6953d7 100644
--- a/app/assets/javascripts/vue_shared/components/registry/list_item.vue
+++ b/app/assets/javascripts/vue_shared/components/registry/list_item.vue
@@ -54,19 +54,17 @@ export default {
class="gl-display-flex gl-flex-direction-column gl-border-b-solid gl-border-t-solid gl-border-t-1 gl-border-b-1"
:class="optionalClasses"
>
- <div class="gl-display-flex gl-align-items-center gl-py-5">
+ <div class="gl-display-flex gl-align-items-center gl-py-3">
<div
v-if="$slots['left-action']"
- class="gl-w-7 gl-display-none gl-display-sm-flex gl-justify-content-start gl-pl-2"
+ class="gl-w-7 gl-display-none gl-sm-display-flex gl-justify-content-start gl-pl-2"
>
<slot name="left-action"></slot>
</div>
<div
class="gl-display-flex gl-xs-flex-direction-column gl-justify-content-space-between gl-align-items-stretch gl-flex-fill-1"
>
- <div
- class="gl-display-flex gl-flex-direction-column gl-justify-content-space-between gl-xs-mb-3 gl-min-w-0 gl-flex-grow-1"
- >
+ <div class="gl-display-flex gl-flex-direction-column gl-xs-mb-3 gl-min-w-0 gl-flex-grow-1">
<div
v-if="$slots['left-primary']"
class="gl-display-flex gl-align-items-center gl-text-body gl-font-weight-bold gl-min-h-6 gl-min-w-0"
@@ -77,13 +75,13 @@ export default {
:selected="isDetailsShown"
icon="ellipsis_h"
size="small"
- class="gl-ml-2 gl-display-none gl-display-sm-block"
+ class="gl-ml-2 gl-display-none gl-sm-display-block"
@click="toggleDetails"
/>
</div>
<div
v-if="$slots['left-secondary']"
- class="gl-display-flex gl-align-items-center gl-text-gray-500 gl-mt-1 gl-min-h-6 gl-min-w-0 gl-flex-fill-1"
+ class="gl-display-flex gl-align-items-center gl-text-gray-500 gl-min-h-6 gl-min-w-0 gl-flex-fill-1"
>
<slot name="left-secondary"></slot>
</div>
@@ -99,7 +97,7 @@ export default {
</div>
<div
v-if="$slots['right-secondary']"
- class="gl-display-flex gl-align-items-center gl-mt-1 gl-min-h-6"
+ class="gl-display-flex gl-align-items-center gl-min-h-6"
>
<slot name="right-secondary"></slot>
</div>
@@ -107,7 +105,7 @@ export default {
</div>
<div
v-if="$slots['right-action']"
- class="gl-w-9 gl-display-none gl-display-sm-flex gl-justify-content-end gl-pr-1"
+ class="gl-w-9 gl-display-none gl-sm-display-flex gl-justify-content-end gl-pr-1"
>
<slot name="right-action"></slot>
</div>
diff --git a/app/assets/javascripts/vue_shared/components/rich_content_editor/services/editor_service.js b/app/assets/javascripts/vue_shared/components/rich_content_editor/services/editor_service.js
index be78651d38d..f7bd49c7def 100644
--- a/app/assets/javascripts/vue_shared/components/rich_content_editor/services/editor_service.js
+++ b/app/assets/javascripts/vue_shared/components/rich_content_editor/services/editor_service.js
@@ -1,9 +1,9 @@
import Vue from 'vue';
import { defaults } from 'lodash';
import ToolbarItem from '../toolbar_item.vue';
+import { TOOLBAR_ITEM_CONFIGS, VIDEO_ATTRIBUTES } from '../constants';
import buildHtmlToMarkdownRenderer from './build_html_to_markdown_renderer';
import buildCustomHTMLRenderer from './build_custom_renderer';
-import { TOOLBAR_ITEM_CONFIGS, VIDEO_ATTRIBUTES } from '../constants';
import sanitizeHTML from './sanitize_html';
const buildWrapper = (propsData) => {
diff --git a/app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/render_html_block.js b/app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/render_html_block.js
index 30012c1123f..710b807275b 100644
--- a/app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/render_html_block.js
+++ b/app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/render_html_block.js
@@ -1,6 +1,6 @@
-import { buildUneditableHtmlAsTextTokens } from './build_uneditable_token';
-import { ALLOWED_VIDEO_ORIGINS } from '../../constants';
import { getURLOrigin } from '~/lib/utils/url_utility';
+import { ALLOWED_VIDEO_ORIGINS } from '../../constants';
+import { buildUneditableHtmlAsTextTokens } from './build_uneditable_token';
const isVideoFrame = (html) => {
const parser = new DOMParser();
diff --git a/app/assets/javascripts/vue_shared/components/rich_content_editor/services/sanitize_html.js b/app/assets/javascripts/vue_shared/components/rich_content_editor/services/sanitize_html.js
index cb0f1d51cb1..486d88466b7 100644
--- a/app/assets/javascripts/vue_shared/components/rich_content_editor/services/sanitize_html.js
+++ b/app/assets/javascripts/vue_shared/components/rich_content_editor/services/sanitize_html.js
@@ -1,6 +1,6 @@
import createSanitizer from 'dompurify';
-import { ALLOWED_VIDEO_ORIGINS } from '../constants';
import { getURLOrigin } from '~/lib/utils/url_utility';
+import { ALLOWED_VIDEO_ORIGINS } from '../constants';
const sanitizer = createSanitizer(window);
const ADD_TAGS = ['iframe'];
diff --git a/app/assets/javascripts/vue_shared/components/runner_instructions/constants.js b/app/assets/javascripts/vue_shared/components/runner_instructions/constants.js
new file mode 100644
index 00000000000..facace0d809
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/runner_instructions/constants.js
@@ -0,0 +1,18 @@
+import { s__ } from '~/locale';
+
+export const PLATFORMS_WITHOUT_ARCHITECTURES = ['docker', 'kubernetes'];
+
+export const INSTRUCTIONS_PLATFORMS_WITHOUT_ARCHITECTURES = {
+ docker: {
+ instructions: s__(
+ 'Runners|To install Runner in a container follow the instructions described in the GitLab documentation',
+ ),
+ link: 'https://docs.gitlab.com/runner/install/docker.html',
+ },
+ kubernetes: {
+ instructions: s__(
+ 'Runners|To install Runner in Kubernetes follow the instructions described in the GitLab documentation.',
+ ),
+ link: 'https://docs.gitlab.com/runner/install/kubernetes.html',
+ },
+};
diff --git a/app/assets/javascripts/vue_shared/components/runner_instructions/graphql/queries/get_runner_platforms.query.graphql b/app/assets/javascripts/vue_shared/components/runner_instructions/graphql/queries/get_runner_platforms.query.graphql
new file mode 100644
index 00000000000..ff0626167a9
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/runner_instructions/graphql/queries/get_runner_platforms.query.graphql
@@ -0,0 +1,20 @@
+query getRunnerPlatforms($projectPath: ID!, $groupPath: ID!) {
+ runnerPlatforms {
+ nodes {
+ name
+ humanReadableName
+ architectures {
+ nodes {
+ name
+ downloadLocation
+ }
+ }
+ }
+ }
+ project(fullPath: $projectPath) {
+ id
+ }
+ group(fullPath: $groupPath) {
+ id
+ }
+}
diff --git a/app/assets/javascripts/vue_shared/components/runner_instructions/graphql/queries/get_runner_setup.query.graphql b/app/assets/javascripts/vue_shared/components/runner_instructions/graphql/queries/get_runner_setup.query.graphql
new file mode 100644
index 00000000000..643c1991807
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/runner_instructions/graphql/queries/get_runner_setup.query.graphql
@@ -0,0 +1,16 @@
+query runnerSetupInstructions(
+ $platform: String!
+ $architecture: String!
+ $projectId: ID!
+ $groupId: ID!
+) {
+ runnerSetup(
+ platform: $platform
+ architecture: $architecture
+ projectId: $projectId
+ groupId: $groupId
+ ) {
+ installInstructions
+ registerInstructions
+ }
+}
diff --git a/app/assets/javascripts/vue_shared/components/runner_instructions/runner_instructions.vue b/app/assets/javascripts/vue_shared/components/runner_instructions/runner_instructions.vue
new file mode 100644
index 00000000000..1d6db576942
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/runner_instructions/runner_instructions.vue
@@ -0,0 +1,261 @@
+<script>
+import {
+ GlAlert,
+ GlButton,
+ GlModal,
+ GlModalDirective,
+ GlButtonGroup,
+ GlDropdown,
+ GlDropdownItem,
+ GlIcon,
+} from '@gitlab/ui';
+import { __, s__ } from '~/locale';
+import ModalCopyButton from '~/vue_shared/components/modal_copy_button.vue';
+import {
+ PLATFORMS_WITHOUT_ARCHITECTURES,
+ INSTRUCTIONS_PLATFORMS_WITHOUT_ARCHITECTURES,
+} from './constants';
+import getRunnerPlatforms from './graphql/queries/get_runner_platforms.query.graphql';
+import getRunnerSetupInstructions from './graphql/queries/get_runner_setup.query.graphql';
+
+export default {
+ components: {
+ GlAlert,
+ GlButton,
+ GlButtonGroup,
+ GlDropdown,
+ GlDropdownItem,
+ GlModal,
+ GlIcon,
+ ModalCopyButton,
+ },
+ directives: {
+ GlModalDirective,
+ },
+ inject: {
+ projectPath: {
+ default: '',
+ },
+ groupPath: {
+ default: '',
+ },
+ },
+ apollo: {
+ runnerPlatforms: {
+ query: getRunnerPlatforms,
+ variables() {
+ return {
+ projectPath: this.projectPath,
+ groupPath: this.groupPath,
+ };
+ },
+ error() {
+ this.showAlert = true;
+ },
+ result({ data }) {
+ this.project = data?.project;
+ this.group = data?.group;
+
+ this.selectPlatform(this.platforms[0].name);
+ },
+ },
+ },
+ data() {
+ return {
+ showAlert: false,
+ selectedPlatformArchitectures: [],
+ selectedPlatform: {
+ name: '',
+ },
+ selectedArchitecture: {},
+ runnerPlatforms: {},
+ instructions: {},
+ project: {},
+ group: {},
+ };
+ },
+ computed: {
+ isPlatformSelected() {
+ return Object.keys(this.selectedPlatform).length > 0;
+ },
+ instructionsEmpty() {
+ return Object.keys(this.instructions).length === 0;
+ },
+ groupId() {
+ return this.group?.id ?? '';
+ },
+ projectId() {
+ return this.project?.id ?? '';
+ },
+ platforms() {
+ return this.runnerPlatforms?.nodes;
+ },
+ hasArchitecureList() {
+ return !PLATFORMS_WITHOUT_ARCHITECTURES.includes(this.selectedPlatform?.name);
+ },
+ instructionsWithoutArchitecture() {
+ return INSTRUCTIONS_PLATFORMS_WITHOUT_ARCHITECTURES[this.selectedPlatform.name]?.instructions;
+ },
+ runnerInstallationLink() {
+ return INSTRUCTIONS_PLATFORMS_WITHOUT_ARCHITECTURES[this.selectedPlatform.name]?.link;
+ },
+ },
+ methods: {
+ selectPlatform(name) {
+ this.selectedPlatform = this.platforms.find((platform) => platform.name === name);
+ if (this.hasArchitecureList) {
+ this.selectedPlatformArchitectures = this.selectedPlatform?.architectures?.nodes;
+ [this.selectedArchitecture] = this.selectedPlatformArchitectures;
+ this.selectArchitecture(this.selectedArchitecture);
+ }
+ },
+ selectArchitecture(architecture) {
+ this.selectedArchitecture = architecture;
+
+ this.$apollo.addSmartQuery('instructions', {
+ variables() {
+ return {
+ platform: this.selectedPlatform.name,
+ architecture: this.selectedArchitecture.name,
+ projectId: this.projectId,
+ groupId: this.groupId,
+ };
+ },
+ query: getRunnerSetupInstructions,
+ update(data) {
+ return data?.runnerSetup;
+ },
+ error() {
+ this.showAlert = true;
+ },
+ });
+ },
+ toggleAlert(state) {
+ this.showAlert = state;
+ },
+ },
+ modalId: 'installation-instructions-modal',
+ i18n: {
+ installARunner: s__('Runners|Install a Runner'),
+ architecture: s__('Runners|Architecture'),
+ downloadInstallBinary: s__('Runners|Download and Install Binary'),
+ downloadLatestBinary: s__('Runners|Download Latest Binary'),
+ registerRunner: s__('Runners|Register Runner'),
+ method: __('Method'),
+ fetchError: s__('Runners|An error has occurred fetching instructions'),
+ instructions: s__('Runners|Show Runner installation instructions'),
+ copyInstructions: s__('Runners|Copy instructions'),
+ },
+ closeButton: {
+ text: __('Close'),
+ attributes: [{ variant: 'default' }],
+ },
+};
+</script>
+<template>
+ <div>
+ <gl-button
+ v-gl-modal-directive="$options.modalId"
+ class="gl-mt-4"
+ data-testid="show-modal-button"
+ >
+ {{ $options.i18n.instructions }}
+ </gl-button>
+ <gl-modal
+ :modal-id="$options.modalId"
+ :title="$options.i18n.installARunner"
+ :action-secondary="$options.closeButton"
+ >
+ <gl-alert v-if="showAlert" variant="danger" @dismiss="toggleAlert(false)">
+ {{ $options.i18n.fetchError }}
+ </gl-alert>
+ <h5>{{ __('Environment') }}</h5>
+ <gl-button-group class="gl-mb-5">
+ <gl-button
+ v-for="platform in platforms"
+ :key="platform.name"
+ data-testid="platform-button"
+ @click="selectPlatform(platform.name)"
+ >
+ {{ platform.humanReadableName }}
+ </gl-button>
+ </gl-button-group>
+ <template v-if="hasArchitecureList">
+ <template v-if="isPlatformSelected">
+ <h5>
+ {{ $options.i18n.architecture }}
+ </h5>
+ <gl-dropdown class="gl-mb-5" :text="selectedArchitecture.name">
+ <gl-dropdown-item
+ v-for="architecture in selectedPlatformArchitectures"
+ :key="architecture.name"
+ data-testid="architecture-dropdown-item"
+ @click="selectArchitecture(architecture)"
+ >
+ {{ architecture.name }}
+ </gl-dropdown-item>
+ </gl-dropdown>
+ <div class="gl-display-flex gl-align-items-center gl-mb-5">
+ <h5>{{ $options.i18n.downloadInstallBinary }}</h5>
+ <gl-button
+ class="gl-ml-auto"
+ :href="selectedArchitecture.downloadLocation"
+ download
+ data-testid="binary-download-button"
+ >
+ {{ $options.i18n.downloadLatestBinary }}
+ </gl-button>
+ </div>
+ </template>
+ <template v-if="!instructionsEmpty">
+ <div class="gl-display-flex">
+ <pre
+ class="gl-bg-gray gl-flex-fill-1 gl-white-space-pre-line"
+ data-testid="binary-instructions"
+ >
+
+ {{ instructions.installInstructions }}
+ </pre
+ >
+ <modal-copy-button
+ :title="$options.i18n.copyInstructions"
+ :text="instructions.installInstructions"
+ :modal-id="$options.modalId"
+ css-classes="gl-align-self-start gl-ml-2 gl-mt-2"
+ category="tertiary"
+ />
+ </div>
+
+ <hr />
+ <h5 class="gl-mb-5">{{ $options.i18n.registerRunner }}</h5>
+ <h5 class="gl-mb-5">{{ $options.i18n.method }}</h5>
+ <div class="gl-display-flex">
+ <pre
+ class="gl-bg-gray gl-flex-fill-1 gl-white-space-pre-line"
+ data-testid="runner-instructions"
+ >
+ {{ instructions.registerInstructions }}
+ </pre
+ >
+ <modal-copy-button
+ :title="$options.i18n.copyInstructions"
+ :text="instructions.registerInstructions"
+ :modal-id="$options.modalId"
+ css-classes="gl-align-self-start gl-ml-2 gl-mt-2"
+ category="tertiary"
+ />
+ </div>
+ </template>
+ </template>
+ <template v-else>
+ <div>
+ <p>{{ instructionsWithoutArchitecture }}</p>
+ <gl-button :href="runnerInstallationLink">
+ <gl-icon name="external-link" />
+ {{ s__('Runners|View installation instructions') }}
+ </gl-button>
+ </div>
+ </template>
+ </gl-modal>
+ </div>
+</template>
diff --git a/app/assets/javascripts/vue_shared/components/settings/settings_block.vue b/app/assets/javascripts/vue_shared/components/settings/settings_block.vue
new file mode 100644
index 00000000000..31094b985a2
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/settings/settings_block.vue
@@ -0,0 +1,45 @@
+<script>
+import { GlButton } from '@gitlab/ui';
+import { __ } from '~/locale';
+
+export default {
+ components: { GlButton },
+ props: {
+ defaultExpanded: {
+ type: Boolean,
+ default: false,
+ required: false,
+ },
+ },
+ data() {
+ return {
+ sectionExpanded: false,
+ };
+ },
+ computed: {
+ expanded() {
+ return this.defaultExpanded || this.sectionExpanded;
+ },
+ toggleText() {
+ return this.expanded ? __('Collapse') : __('Expand');
+ },
+ },
+};
+</script>
+
+<template>
+ <section class="settings no-animate" :class="{ expanded }">
+ <div class="settings-header">
+ <h4><slot name="title"></slot></h4>
+ <gl-button @click="sectionExpanded = !sectionExpanded">
+ {{ toggleText }}
+ </gl-button>
+ <p>
+ <slot name="description"></slot>
+ </p>
+ </div>
+ <div class="settings-content">
+ <slot></slot>
+ </div>
+ </section>
+</template>
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/date_picker.vue b/app/assets/javascripts/vue_shared/components/sidebar/date_picker.vue
index 6caf8bc92c2..30daabc1e24 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/date_picker.vue
+++ b/app/assets/javascripts/vue_shared/components/sidebar/date_picker.vue
@@ -1,10 +1,10 @@
<script>
import { GlLoadingIcon } from '@gitlab/ui';
+import { __ } from '~/locale';
import datePicker from '../pikaday.vue';
+import { dateInWords } from '../../../lib/utils/datetime_utility';
import toggleSidebar from './toggle_sidebar.vue';
import collapsedCalendarIcon from './collapsed_calendar_icon.vue';
-import { dateInWords } from '../../../lib/utils/datetime_utility';
-import { __ } from '~/locale';
export default {
name: 'SidebarDatePicker',
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select/base.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/base.vue
index 22d86ee25d1..d014139504f 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select/base.vue
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/base.vue
@@ -5,6 +5,7 @@ import { __ } from '~/locale';
import LabelsSelect from '~/labels_select';
import DropdownHiddenInput from '~/vue_shared/components/dropdown/dropdown_hidden_input.vue';
+import { DropdownVariant } from '../labels_select_vue/constants';
import DropdownTitle from './dropdown_title.vue';
import DropdownValue from './dropdown_value.vue';
import DropdownValueCollapsed from './dropdown_value_collapsed.vue';
@@ -14,8 +15,6 @@ import DropdownSearchInput from './dropdown_search_input.vue';
import DropdownFooter from './dropdown_footer.vue';
import DropdownCreateLabel from './dropdown_create_label.vue';
-import { DropdownVariant } from '../labels_select_vue/constants';
-
export default {
DropdownVariant,
components: {
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/labels_select_root.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/labels_select_root.vue
index 683889b8611..9efc17908a2 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/labels_select_root.vue
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/labels_select_root.vue
@@ -35,11 +35,13 @@ export default {
},
allowLabelEdit: {
type: Boolean,
- required: true,
+ required: false,
+ default: false,
},
allowLabelCreate: {
type: Boolean,
- required: true,
+ required: false,
+ default: false,
},
allowMultiselect: {
type: Boolean,
@@ -48,7 +50,8 @@ export default {
},
allowScopedLabels: {
type: Boolean,
- required: true,
+ required: false,
+ default: false,
},
variant: {
type: String,
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/store/mutations.js b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/store/mutations.js
index 6de436ffd13..55716e1105e 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/store/mutations.js
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/store/mutations.js
@@ -1,5 +1,5 @@
-import * as types from './mutation_types';
import { DropdownVariant } from '../constants';
+import * as types from './mutation_types';
export default {
[types.SET_INITIAL_STATE](state, props) {
diff --git a/app/assets/javascripts/vue_shared/components/todo_button.vue b/app/assets/javascripts/vue_shared/components/todo_button.vue
index a9d4f8403fa..935d222a1a9 100644
--- a/app/assets/javascripts/vue_shared/components/todo_button.vue
+++ b/app/assets/javascripts/vue_shared/components/todo_button.vue
@@ -15,7 +15,7 @@ export default {
},
computed: {
buttonLabel() {
- return this.isTodo ? __('Mark as done') : __('Add a To Do');
+ return this.isTodo ? __('Mark as done') : __('Add a to do');
},
},
};
diff --git a/app/assets/javascripts/vue_shared/components/tooltip_on_truncate.vue b/app/assets/javascripts/vue_shared/components/tooltip_on_truncate.vue
index b48dfa8b452..8aa6e29adf1 100644
--- a/app/assets/javascripts/vue_shared/components/tooltip_on_truncate.vue
+++ b/app/assets/javascripts/vue_shared/components/tooltip_on_truncate.vue
@@ -1,7 +1,7 @@
<script>
import { isFunction } from 'lodash';
-import tooltip from '../directives/tooltip';
import { hasHorizontalOverflow } from '~/lib/utils/dom_utils';
+import tooltip from '../directives/tooltip';
export default {
directives: {
diff --git a/app/assets/javascripts/vue_shared/mixins/ci_pagination_api_mixin.js b/app/assets/javascripts/vue_shared/mixins/ci_pagination_api_mixin.js
index be04ff158e7..85a1ab6092b 100644
--- a/app/assets/javascripts/vue_shared/mixins/ci_pagination_api_mixin.js
+++ b/app/assets/javascripts/vue_shared/mixins/ci_pagination_api_mixin.js
@@ -4,8 +4,8 @@
*
* Components need to have `scope`, `page` and `requestData`
*/
-import { historyPushState, buildUrlWithCurrentLocation } from '../../lib/utils/common_utils';
import { validateParams } from '~/pipelines/utils';
+import { historyPushState, buildUrlWithCurrentLocation } from '../../lib/utils/common_utils';
export default {
methods: {
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 a6c7b59aa71..b6b32167ed6 100644
--- a/app/assets/javascripts/vue_shared/security_reports/security_reports_app.vue
+++ b/app/assets/javascripts/vue_shared/security_reports/security_reports_app.vue
@@ -1,13 +1,10 @@
<script>
import { mapActions, mapGetters } from 'vuex';
-import { GlLink, GlSprintf } from '@gitlab/ui';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import ReportSection from '~/reports/components/report_section.vue';
-import { LOADING, ERROR, SLOT_SUCCESS, SLOT_LOADING, SLOT_ERROR } from '~/reports/constants';
+import { ERROR, SLOT_SUCCESS, SLOT_LOADING, SLOT_ERROR } from '~/reports/constants';
import { s__ } from '~/locale';
-import { normalizeHeaders, parseIntPagination } from '~/lib/utils/common_utils';
import createFlash from '~/flash';
-import Api from '~/api';
import HelpIcon from './components/help_icon.vue';
import SecurityReportDownloadDropdown from './components/security_report_download_dropdown.vue';
import SecuritySummary from './components/security_summary.vue';
@@ -24,8 +21,6 @@ import { extractSecurityReportArtifacts } from './utils';
export default {
store,
components: {
- GlLink,
- GlSprintf,
ReportSection,
HelpIcon,
SecurityReportDownloadDropdown,
@@ -101,9 +96,6 @@ export default {
),
};
},
- skip() {
- return !this.canShowDownloads;
- },
update(data) {
return extractSecurityReportArtifacts(this.$options.reportTypes, data);
},
@@ -124,9 +116,6 @@ export default {
},
computed: {
...mapGetters(['groupedSummaryText', 'summaryStatus']),
- canShowDownloads() {
- return this.glFeatures.coreSecurityMrWidgetDownloads;
- },
hasSecurityReports() {
return this.availableSecurityReports.length > 0;
},
@@ -139,23 +128,6 @@ export default {
isLoadingReportArtifacts() {
return this.$apollo.queries.reportArtifacts.loading;
},
- shouldShowDownloadGuidance() {
- return !this.canShowDownloads && this.summaryStatus !== LOADING;
- },
- scansHaveRunMessage() {
- return this.canShowDownloads
- ? this.$options.i18n.scansHaveRun
- : this.$options.i18n.scansHaveRunWithDownloadGuidance;
- },
- },
- created() {
- if (!this.canShowDownloads) {
- this.checkAvailableSecurityReports(this.$options.reportTypes)
- .then((availableSecurityReports) => {
- this.onCheckingAvailableSecurityReports(Array.from(availableSecurityReports));
- })
- .catch(this.showError);
- }
},
methods: {
...mapActions(MODULE_SAST, {
@@ -166,36 +138,6 @@ export default {
setSecretDetectionDiffEndpoint: 'setDiffEndpoint',
fetchSecretDetectionDiff: 'fetchDiff',
}),
- async checkAvailableSecurityReports(reportTypes) {
- const reportTypesSet = new Set(reportTypes);
- const availableReportTypes = new Set();
-
- let page = 1;
- while (page) {
- // eslint-disable-next-line no-await-in-loop
- const { data: jobs, headers } = await Api.pipelineJobs(this.projectId, this.pipelineId, {
- per_page: 100,
- page,
- });
-
- jobs.forEach(({ artifacts = [] }) => {
- artifacts.forEach(({ file_type }) => {
- if (reportTypesSet.has(file_type)) {
- availableReportTypes.add(file_type);
- }
- });
- });
-
- // If we've found artifacts for all the report types, stop looking!
- if (availableReportTypes.size === reportTypesSet.size) {
- return availableReportTypes;
- }
-
- page = parseIntPagination(normalizeHeaders(headers)).nextPage;
- }
-
- return availableReportTypes;
- },
fetchCounts() {
if (!this.glFeatures.coreSecurityMrWidgetCounts) {
return;
@@ -213,11 +155,6 @@ export default {
this.canShowCounts = true;
}
},
- activatePipelinesTab() {
- if (window.mrTabs) {
- window.mrTabs.tabShown('pipelines');
- }
- },
onCheckingAvailableSecurityReports(availableSecurityReports) {
this.availableSecurityReports = availableSecurityReports;
this.fetchCounts();
@@ -236,12 +173,6 @@ export default {
'SecurityReports|Failed to get security report information. Please reload the page or try again later.',
),
scansHaveRun: s__('SecurityReports|Security scans have run'),
- scansHaveRunWithDownloadGuidance: s__(
- 'SecurityReports|Security scans have run. Go to the %{linkStart}pipelines tab%{linkEnd} to download the security reports',
- ),
- downloadFromPipelineTab: s__(
- 'SecurityReports|Go to the %{linkStart}pipelines tab%{linkEnd} to download the security reports',
- ),
},
summarySlots: [SLOT_SUCCESS, SLOT_LOADING, SLOT_ERROR],
};
@@ -265,22 +196,7 @@ export default {
</span>
</template>
- <template v-if="shouldShowDownloadGuidance" #sub-heading>
- <span class="gl-font-sm">
- <gl-sprintf :message="$options.i18n.downloadFromPipelineTab">
- <template #link="{ content }">
- <gl-link
- class="gl-font-sm"
- data-testid="show-pipelines"
- @click="activatePipelinesTab"
- >{{ content }}</gl-link
- >
- </template>
- </gl-sprintf>
- </span>
- </template>
-
- <template v-if="canShowDownloads" #action-buttons>
+ <template #action-buttons>
<security-report-download-dropdown
:artifacts="reportArtifacts"
:loading="isLoadingReportArtifacts"
@@ -298,13 +214,7 @@ export default {
data-testid="security-mr-widget"
>
<template #error>
- <gl-sprintf :message="scansHaveRunMessage">
- <template #link="{ content }">
- <gl-link data-testid="show-pipelines" @click="activatePipelinesTab">{{
- content
- }}</gl-link>
- </template>
- </gl-sprintf>
+ {{ $options.i18n.scansHaveRun }}
<help-icon
:help-path="securityReportsDocsPath"
@@ -312,7 +222,7 @@ export default {
/>
</template>
- <template v-if="canShowDownloads" #action-buttons>
+ <template #action-buttons>
<security-report-download-dropdown
:artifacts="reportArtifacts"
:loading="isLoadingReportArtifacts"
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 443255b0e6a..819b0a4af31 100644
--- a/app/assets/javascripts/vue_shared/security_reports/store/getters.js
+++ b/app/assets/javascripts/vue_shared/security_reports/store/getters.js
@@ -1,6 +1,6 @@
import { s__, sprintf } from '~/locale';
-import { countVulnerabilities, groupedTextBuilder } from './utils';
import { LOADING, ERROR, SUCCESS } from '~/reports/constants';
+import { countVulnerabilities, groupedTextBuilder } from './utils';
import { TRANSLATION_IS_LOADING } from './messages';
export const summaryCounts = (state) =>
diff --git a/app/assets/javascripts/vue_shared/security_reports/store/modules/sast/actions.js b/app/assets/javascripts/vue_shared/security_reports/store/modules/sast/actions.js
index 0f26e3c30ef..4f92e181f9f 100644
--- a/app/assets/javascripts/vue_shared/security_reports/store/modules/sast/actions.js
+++ b/app/assets/javascripts/vue_shared/security_reports/store/modules/sast/actions.js
@@ -1,5 +1,5 @@
-import * as types from './mutation_types';
import { fetchDiffData } from '../../utils';
+import * as types from './mutation_types';
export const setDiffEndpoint = ({ commit }, path) => commit(types.SET_DIFF_ENDPOINT, path);
diff --git a/app/assets/javascripts/vue_shared/security_reports/store/modules/sast/mutations.js b/app/assets/javascripts/vue_shared/security_reports/store/modules/sast/mutations.js
index 5f6153ca3b1..11aa71d2b6b 100644
--- a/app/assets/javascripts/vue_shared/security_reports/store/modules/sast/mutations.js
+++ b/app/assets/javascripts/vue_shared/security_reports/store/modules/sast/mutations.js
@@ -1,6 +1,6 @@
import Vue from 'vue';
-import * as types from './mutation_types';
import { parseDiff } from '../../utils';
+import * as types from './mutation_types';
export default {
[types.SET_DIFF_ENDPOINT](state, path) {
diff --git a/app/assets/javascripts/webpack_non_compiled_placeholder.js b/app/assets/javascripts/webpack_non_compiled_placeholder.js
new file mode 100644
index 00000000000..8cd1d2eb2ca
--- /dev/null
+++ b/app/assets/javascripts/webpack_non_compiled_placeholder.js
@@ -0,0 +1,24 @@
+const div = document.createElement('div');
+
+Object.assign(div.style, {
+ width: '100vw',
+ height: '100vh',
+ position: 'fixed',
+ top: 0,
+ left: 0,
+ 'z-index': 100000,
+ background: 'rgba(0,0,0,0.9)',
+ 'font-size': '25px',
+ 'font-family': 'monospace',
+ color: 'white',
+ padding: '2.5em',
+ 'text-align': 'center',
+});
+
+div.innerHTML = `
+<h1 style="color:white">🧙 Webpack is doing its magic 🧙</h1>
+<p>If you use Hot Module reloading, the page will reload in a few seconds.</p>
+<p>If you do not use Hot Module reloading, please <a href="">reload the page manually in a few seconds</a></p>
+`;
+
+document.body.append(div);
diff --git a/app/assets/javascripts/whats_new/components/app.vue b/app/assets/javascripts/whats_new/components/app.vue
index 0a81f172fe9..a5c8aad01be 100644
--- a/app/assets/javascripts/whats_new/components/app.vue
+++ b/app/assets/javascripts/whats_new/components/app.vue
@@ -9,10 +9,10 @@ import {
GlBadge,
GlLoadingIcon,
} from '@gitlab/ui';
-import SkeletonLoader from './skeleton_loader.vue';
-import Feature from './feature.vue';
import Tracking from '~/tracking';
import { getDrawerBodyHeight } from '../utils/get_drawer_body_height';
+import SkeletonLoader from './skeleton_loader.vue';
+import Feature from './feature.vue';
const trackingMixin = Tracking.mixin();
diff --git a/app/assets/javascripts/whats_new/store/actions.js b/app/assets/javascripts/whats_new/store/actions.js
index 0e5eeda742a..4b3cfa55977 100644
--- a/app/assets/javascripts/whats_new/store/actions.js
+++ b/app/assets/javascripts/whats_new/store/actions.js
@@ -1,6 +1,6 @@
-import * as types from './mutation_types';
import axios from '~/lib/utils/axios_utils';
import { parseIntPagination, normalizeHeaders } from '~/lib/utils/common_utils';
+import * as types from './mutation_types';
export default {
closeDrawer({ commit }) {