summaryrefslogtreecommitdiff
path: root/app/assets/javascripts
diff options
context:
space:
mode:
Diffstat (limited to 'app/assets/javascripts')
-rw-r--r--app/assets/javascripts/access_tokens/components/token.vue55
-rw-r--r--app/assets/javascripts/access_tokens/components/tokens_app.vue111
-rw-r--r--app/assets/javascripts/access_tokens/constants.js4
-rw-r--r--app/assets/javascripts/access_tokens/index.js30
-rw-r--r--app/assets/javascripts/admin/deploy_keys/components/table.vue213
-rw-r--r--app/assets/javascripts/admin/users/components/actions/activate.vue21
-rw-r--r--app/assets/javascripts/admin/users/components/actions/approve.vue20
-rw-r--r--app/assets/javascripts/admin/users/components/actions/ban.vue19
-rw-r--r--app/assets/javascripts/admin/users/components/actions/block.vue19
-rw-r--r--app/assets/javascripts/admin/users/components/actions/deactivate.vue19
-rw-r--r--app/assets/javascripts/admin/users/components/actions/reject.vue19
-rw-r--r--app/assets/javascripts/admin/users/components/actions/unban.vue19
-rw-r--r--app/assets/javascripts/admin/users/components/actions/unblock.vue20
-rw-r--r--app/assets/javascripts/admin/users/components/actions/unlock.vue19
-rw-r--r--app/assets/javascripts/admin/users/components/modals/delete_user_modal.vue13
-rw-r--r--app/assets/javascripts/admin/users/components/user_actions.vue2
-rw-r--r--app/assets/javascripts/alert_management/graphql/queries/get_count_by_status.query.graphql1
-rw-r--r--app/assets/javascripts/alerts_settings/graphql/mutations/create_http_integration.mutation.graphql2
-rw-r--r--app/assets/javascripts/alerts_settings/graphql/mutations/destroy_http_integration.mutation.graphql2
-rw-r--r--app/assets/javascripts/alerts_settings/graphql/mutations/reset_http_token.mutation.graphql2
-rw-r--r--app/assets/javascripts/alerts_settings/graphql/mutations/update_http_integration.mutation.graphql2
-rw-r--r--app/assets/javascripts/alerts_settings/graphql/queries/get_http_integration.query.graphql1
-rw-r--r--app/assets/javascripts/alerts_settings/graphql/queries/get_integrations.query.graphql1
-rw-r--r--app/assets/javascripts/alerts_settings/graphql/queries/parse_sample_payload.query.graphql1
-rw-r--r--app/assets/javascripts/analytics/devops_reports/components/devops_score.vue8
-rw-r--r--app/assets/javascripts/analytics/shared/graphql/projects.query.graphql1
-rw-r--r--app/assets/javascripts/api.js7
-rw-r--r--app/assets/javascripts/api/packages_api.js32
-rw-r--r--app/assets/javascripts/artifacts_settings/graphql/queries/get_keep_latest_artifact_project_setting.query.graphql1
-rw-r--r--app/assets/javascripts/batch_comments/components/draft_note.vue2
-rw-r--r--app/assets/javascripts/behaviors/copy_code.js66
-rw-r--r--app/assets/javascripts/behaviors/gl_emoji.js2
-rw-r--r--app/assets/javascripts/behaviors/index.js1
-rw-r--r--app/assets/javascripts/behaviors/shortcuts/shortcuts_issuable.js9
-rw-r--r--app/assets/javascripts/blob/components/blob_content.vue2
-rw-r--r--app/assets/javascripts/blob/pdf/pdf_viewer.vue8
-rw-r--r--app/assets/javascripts/blob/pipeline_tour_success_modal.vue2
-rw-r--r--app/assets/javascripts/blob_edit/edit_blob.js43
-rw-r--r--app/assets/javascripts/boards/boards_util.js12
-rw-r--r--app/assets/javascripts/boards/components/board_card_inner.vue2
-rw-r--r--app/assets/javascripts/boards/components/board_content_sidebar.vue4
-rw-r--r--app/assets/javascripts/boards/components/board_filtered_search.vue103
-rw-r--r--app/assets/javascripts/boards/components/board_list.vue16
-rw-r--r--app/assets/javascripts/boards/components/board_list_header.vue39
-rw-r--r--app/assets/javascripts/boards/components/boards_selector.vue3
-rw-r--r--app/assets/javascripts/boards/components/issue_board_filtered_search.vue85
-rw-r--r--app/assets/javascripts/boards/components/sidebar/board_sidebar_labels_select.vue173
-rw-r--r--app/assets/javascripts/boards/components/sidebar/board_sidebar_subscription.vue75
-rw-r--r--app/assets/javascripts/boards/constants.js14
-rw-r--r--app/assets/javascripts/boards/graphql/board_labels.query.graphql2
-rw-r--r--app/assets/javascripts/boards/graphql/board_list_create.mutation.graphql2
-rw-r--r--app/assets/javascripts/boards/graphql/board_list_shared.fragment.graphql1
-rw-r--r--app/assets/javascripts/boards/graphql/board_list_update.mutation.graphql2
-rw-r--r--app/assets/javascripts/boards/graphql/board_lists.query.graphql8
-rw-r--r--app/assets/javascripts/boards/graphql/board_lists_deferred.query.graphql6
-rw-r--r--app/assets/javascripts/boards/graphql/group_board.query.graphql1
-rw-r--r--app/assets/javascripts/boards/graphql/group_board_members.query.graphql1
-rw-r--r--app/assets/javascripts/boards/graphql/group_board_milestones.query.graphql1
-rw-r--r--app/assets/javascripts/boards/graphql/group_boards.query.graphql1
-rw-r--r--app/assets/javascripts/boards/graphql/group_projects.query.graphql1
-rw-r--r--app/assets/javascripts/boards/graphql/issue_set_labels.mutation.graphql11
-rw-r--r--app/assets/javascripts/boards/graphql/issue_set_subscription.mutation.graphql1
-rw-r--r--app/assets/javascripts/boards/graphql/issue_set_title.mutation.graphql1
-rw-r--r--app/assets/javascripts/boards/graphql/lists_issues.query.graphql6
-rw-r--r--app/assets/javascripts/boards/graphql/project_board.query.graphql1
-rw-r--r--app/assets/javascripts/boards/graphql/project_board_members.query.graphql1
-rw-r--r--app/assets/javascripts/boards/graphql/project_board_milestones.query.graphql1
-rw-r--r--app/assets/javascripts/boards/graphql/project_boards.query.graphql1
-rw-r--r--app/assets/javascripts/boards/graphql/project_milestones.query.graphql1
-rw-r--r--app/assets/javascripts/boards/index.js3
-rw-r--r--app/assets/javascripts/boards/mount_filtered_search_issue_boards.js3
-rw-r--r--app/assets/javascripts/boards/stores/actions.js133
-rw-r--r--app/assets/javascripts/ci_lint/components/ci_lint.vue2
-rw-r--r--app/assets/javascripts/ci_settings_pipeline_triggers/components/triggers_list.vue2
-rw-r--r--app/assets/javascripts/ci_variable_list/components/ci_variable_modal.vue42
-rw-r--r--app/assets/javascripts/ci_variable_list/components/ci_variable_table.vue48
-rw-r--r--app/assets/javascripts/ci_variable_list/constants.js3
-rw-r--r--app/assets/javascripts/clusters/agents/components/activity_events_list.vue176
-rw-r--r--app/assets/javascripts/clusters/agents/components/activity_history_item.vue79
-rw-r--r--app/assets/javascripts/clusters/agents/components/show.vue16
-rw-r--r--app/assets/javascripts/clusters/agents/components/token_table.vue13
-rw-r--r--app/assets/javascripts/clusters/agents/constants.js37
-rw-r--r--app/assets/javascripts/clusters/agents/graphql/fragments/cluster_agent_token.fragment.graphql2
-rw-r--r--app/assets/javascripts/clusters/agents/graphql/queries/get_agent_activity_events.query.graphql25
-rw-r--r--app/assets/javascripts/clusters/agents/graphql/queries/get_cluster_agent.query.graphql2
-rw-r--r--app/assets/javascripts/clusters/agents/index.js3
-rw-r--r--app/assets/javascripts/clusters_list/components/agent_empty_state.vue67
-rw-r--r--app/assets/javascripts/clusters_list/components/agents.vue9
-rw-r--r--app/assets/javascripts/clusters_list/components/available_agents_dropdown.vue30
-rw-r--r--app/assets/javascripts/clusters_list/components/clusters_empty_state.vue89
-rw-r--r--app/assets/javascripts/clusters_list/components/clusters_main_view.vue19
-rw-r--r--app/assets/javascripts/clusters_list/components/clusters_view_all.vue33
-rw-r--r--app/assets/javascripts/clusters_list/components/install_agent_modal.vue325
-rw-r--r--app/assets/javascripts/clusters_list/constants.js147
-rw-r--r--app/assets/javascripts/clusters_list/graphql/cache_update.js84
-rw-r--r--app/assets/javascripts/clusters_list/graphql/fragments/cluster_agent.fragment.graphql1
-rw-r--r--app/assets/javascripts/clusters_list/graphql/queries/agent_configurations.query.graphql2
-rw-r--r--app/assets/javascripts/clusters_list/graphql/queries/get_agents.query.graphql2
-rw-r--r--app/assets/javascripts/code_navigation/components/doc_line.vue1
-rw-r--r--app/assets/javascripts/content_editor/extensions/attachment.js8
-rw-r--r--app/assets/javascripts/content_editor/extensions/audio.js8
-rw-r--r--app/assets/javascripts/content_editor/extensions/code_block_highlight.js4
-rw-r--r--app/assets/javascripts/content_editor/extensions/division.js14
-rw-r--r--app/assets/javascripts/content_editor/extensions/footnote_definition.js21
-rw-r--r--app/assets/javascripts/content_editor/extensions/footnote_reference.js37
-rw-r--r--app/assets/javascripts/content_editor/extensions/footnotes_section.js19
-rw-r--r--app/assets/javascripts/content_editor/extensions/html_marks.js9
-rw-r--r--app/assets/javascripts/content_editor/extensions/image.js8
-rw-r--r--app/assets/javascripts/content_editor/extensions/inline_diff.js6
-rw-r--r--app/assets/javascripts/content_editor/extensions/link.js9
-rw-r--r--app/assets/javascripts/content_editor/extensions/task_item.js8
-rw-r--r--app/assets/javascripts/content_editor/extensions/video.js10
-rw-r--r--app/assets/javascripts/content_editor/extensions/word_break.js10
-rw-r--r--app/assets/javascripts/content_editor/services/create_content_editor.js6
-rw-r--r--app/assets/javascripts/content_editor/services/markdown_serializer.js23
-rw-r--r--app/assets/javascripts/crm/components/contact_form.vue224
-rw-r--r--app/assets/javascripts/crm/components/contacts_root.vue129
-rw-r--r--app/assets/javascripts/crm/components/new_organization_form.vue164
-rw-r--r--app/assets/javascripts/crm/components/organizations_root.vue91
-rw-r--r--app/assets/javascripts/crm/components/queries/create_contact.mutation.graphql10
-rw-r--r--app/assets/javascripts/crm/components/queries/create_organization.mutation.graphql10
-rw-r--r--app/assets/javascripts/crm/components/queries/crm_contact_fields.fragment.graphql14
-rw-r--r--app/assets/javascripts/crm/components/queries/crm_organization_fields.fragment.graphql7
-rw-r--r--app/assets/javascripts/crm/components/queries/get_group_contacts.query.graphql15
-rw-r--r--app/assets/javascripts/crm/components/queries/get_group_organizations.query.graphql8
-rw-r--r--app/assets/javascripts/crm/components/queries/update_contact.mutation.graphql10
-rw-r--r--app/assets/javascripts/crm/constants.js3
-rw-r--r--app/assets/javascripts/crm/contacts_bundle.js16
-rw-r--r--app/assets/javascripts/crm/organizations_bundle.js16
-rw-r--r--app/assets/javascripts/crm/routes.js16
-rw-r--r--app/assets/javascripts/delete_label_modal.js16
-rw-r--r--app/assets/javascripts/design_management/components/design_notes/design_discussion.vue12
-rw-r--r--app/assets/javascripts/design_management/components/design_overlay.vue8
-rw-r--r--app/assets/javascripts/design_management/graphql/fragments/discussion_resolved_status.fragment.graphql2
-rw-r--r--app/assets/javascripts/design_management/graphql/mutations/upload_design.mutation.graphql2
-rw-r--r--app/assets/javascripts/design_management/graphql/queries/get_design.query.graphql2
-rw-r--r--app/assets/javascripts/diffs/components/app.vue4
-rw-r--r--app/assets/javascripts/diffs/components/diff_file.vue2
-rw-r--r--app/assets/javascripts/diffs/components/diff_row.vue10
-rw-r--r--app/assets/javascripts/diffs/components/diff_view.vue3
-rw-r--r--app/assets/javascripts/diffs/store/modules/diff_state.js1
-rw-r--r--app/assets/javascripts/diffs/store/mutations.js2
-rw-r--r--app/assets/javascripts/diffs/utils/discussions.js76
-rw-r--r--app/assets/javascripts/dropzone_input.js8
-rw-r--r--app/assets/javascripts/editor/constants.js4
-rw-r--r--app/assets/javascripts/editor/extensions/example_source_editor_extension.js14
-rw-r--r--app/assets/javascripts/editor/extensions/source_editor_ci_schema_ext.js47
-rw-r--r--app/assets/javascripts/editor/extensions/source_editor_extension_base.js111
-rw-r--r--app/assets/javascripts/editor/extensions/source_editor_file_template_ext.js16
-rw-r--r--app/assets/javascripts/editor/extensions/source_editor_markdown_ext.js330
-rw-r--r--app/assets/javascripts/editor/extensions/source_editor_markdown_livepreview_ext.js167
-rw-r--r--app/assets/javascripts/editor/extensions/source_editor_webide_ext.js272
-rw-r--r--app/assets/javascripts/editor/extensions/source_editor_yaml_ext.js279
-rw-r--r--app/assets/javascripts/editor/source_editor.js179
-rw-r--r--app/assets/javascripts/editor/source_editor_extension.js2
-rw-r--r--app/assets/javascripts/editor/source_editor_instance.js76
-rw-r--r--app/assets/javascripts/emoji/constants.js3
-rw-r--r--app/assets/javascripts/emoji/index.js29
-rw-r--r--app/assets/javascripts/environments/components/confirm_rollback_modal.vue41
-rw-r--r--app/assets/javascripts/environments/components/delete_environment_modal.vue31
-rw-r--r--app/assets/javascripts/environments/components/enable_review_app_modal.vue12
-rw-r--r--app/assets/javascripts/environments/components/environment_delete.vue23
-rw-r--r--app/assets/javascripts/environments/components/environment_item.vue2
-rw-r--r--app/assets/javascripts/environments/components/environment_rollback.vue34
-rw-r--r--app/assets/javascripts/environments/components/new_environment_folder.vue27
-rw-r--r--app/assets/javascripts/environments/components/new_environments_app.vue180
-rw-r--r--app/assets/javascripts/environments/graphql/client.js16
-rw-r--r--app/assets/javascripts/environments/graphql/mutations/set_environment_to_delete.mutation.graphql3
-rw-r--r--app/assets/javascripts/environments/graphql/mutations/set_environment_to_rollback.mutation.graphql3
-rw-r--r--app/assets/javascripts/environments/graphql/queries/environmentApp.query.graphql8
-rw-r--r--app/assets/javascripts/environments/graphql/queries/environment_app.query.graphql9
-rw-r--r--app/assets/javascripts/environments/graphql/queries/environment_to_delete.query.graphql7
-rw-r--r--app/assets/javascripts/environments/graphql/queries/environment_to_rollback.query.graphql7
-rw-r--r--app/assets/javascripts/environments/graphql/queries/page_info.query.graphql8
-rw-r--r--app/assets/javascripts/environments/graphql/queries/poll_interval.query.graphql3
-rw-r--r--app/assets/javascripts/environments/graphql/resolvers.js123
-rw-r--r--app/assets/javascripts/environments/graphql/typedefs.graphql46
-rw-r--r--app/assets/javascripts/error_tracking/components/error_details.vue2
-rw-r--r--app/assets/javascripts/error_tracking/queries/details.query.graphql1
-rw-r--r--app/assets/javascripts/experimentation/utils.js27
-rw-r--r--app/assets/javascripts/feature_flags/components/form.vue7
-rw-r--r--app/assets/javascripts/filtered_search/available_dropdown_mappings.js2
-rw-r--r--app/assets/javascripts/filtered_search/constants.js7
-rw-r--r--app/assets/javascripts/flash.js8
-rw-r--r--app/assets/javascripts/google_cloud/components/app.vue55
-rw-r--r--app/assets/javascripts/google_cloud/components/errors/gcp_error.vue29
-rw-r--r--app/assets/javascripts/google_cloud/components/errors/no_gcp_projects.vue26
-rw-r--r--app/assets/javascripts/google_cloud/components/home.vue41
-rw-r--r--app/assets/javascripts/google_cloud/components/service_accounts_form.vue70
-rw-r--r--app/assets/javascripts/google_cloud/components/service_accounts_list.vue (renamed from app/assets/javascripts/google_cloud/components/service_accounts.vue)0
-rw-r--r--app/assets/javascripts/google_cloud/index.js13
-rw-r--r--app/assets/javascripts/graphql_shared/constants.js6
-rw-r--r--app/assets/javascripts/graphql_shared/fragments/alert.fragment.graphql1
-rw-r--r--app/assets/javascripts/graphql_shared/fragments/alert_detail_item.fragment.graphql1
-rw-r--r--app/assets/javascripts/graphql_shared/fragments/issuable_timelogs.fragment.graphql2
-rw-r--r--app/assets/javascripts/graphql_shared/fragments/user_availability.fragment.graphql1
-rw-r--r--app/assets/javascripts/graphql_shared/mutations/create_merge_request.mutation.graphql1
-rw-r--r--app/assets/javascripts/graphql_shared/queries/alert_details.query.graphql1
-rw-r--r--app/assets/javascripts/graphql_shared/queries/get_alerts.query.graphql1
-rw-r--r--app/assets/javascripts/graphql_shared/queries/group_users_search.query.graphql1
-rw-r--r--app/assets/javascripts/graphql_shared/queries/project_user_members_search.query.graphql1
-rw-r--r--app/assets/javascripts/graphql_shared/queries/users_search.query.graphql2
-rw-r--r--app/assets/javascripts/header_search/components/app.vue138
-rw-r--r--app/assets/javascripts/header_search/components/header_search_autocomplete_items.vue31
-rw-r--r--app/assets/javascripts/header_search/components/header_search_default_items.vue24
-rw-r--r--app/assets/javascripts/header_search/components/header_search_scoped_items.vue38
-rw-r--r--app/assets/javascripts/header_search/constants.js26
-rw-r--r--app/assets/javascripts/header_search/store/actions.js4
-rw-r--r--app/assets/javascripts/header_search/store/getters.js107
-rw-r--r--app/assets/javascripts/header_search/store/mutation_types.js1
-rw-r--r--app/assets/javascripts/header_search/store/mutations.js7
-rw-r--r--app/assets/javascripts/ide/components/activity_bar.vue2
-rw-r--r--app/assets/javascripts/ide/components/ide_tree_list.vue14
-rw-r--r--app/assets/javascripts/ide/components/new_dropdown/index.vue2
-rw-r--r--app/assets/javascripts/ide/components/pipelines/empty_state.vue35
-rw-r--r--app/assets/javascripts/ide/components/pipelines/list.vue28
-rw-r--r--app/assets/javascripts/ide/components/repo_editor.vue39
-rw-r--r--app/assets/javascripts/ide/constants.js6
-rw-r--r--app/assets/javascripts/ide/ide_router.js69
-rw-r--r--app/assets/javascripts/ide/index.js11
-rw-r--r--app/assets/javascripts/ide/lib/themes/monokai.js2
-rw-r--r--app/assets/javascripts/ide/lib/themes/none.js1
-rw-r--r--app/assets/javascripts/ide/lib/themes/solarized_dark.js2
-rw-r--r--app/assets/javascripts/ide/lib/themes/solarized_light.js2
-rw-r--r--app/assets/javascripts/ide/lib/themes/white.js1
-rw-r--r--app/assets/javascripts/ide/queries/ide_project.fragment.graphql1
-rw-r--r--app/assets/javascripts/ide/services/index.js34
-rw-r--r--app/assets/javascripts/ide/stores/actions/project.js57
-rw-r--r--app/assets/javascripts/ide/stores/mutation_types.js1
-rw-r--r--app/assets/javascripts/ide/stores/mutations/project.js12
-rw-r--r--app/assets/javascripts/import_entities/import_groups/components/import_actions_cell.vue2
-rw-r--r--app/assets/javascripts/import_entities/import_groups/components/import_table.vue135
-rw-r--r--app/assets/javascripts/import_entities/import_groups/components/import_target_cell.vue126
-rw-r--r--app/assets/javascripts/import_entities/import_groups/constants.js4
-rw-r--r--app/assets/javascripts/import_entities/import_groups/graphql/client_factory.js31
-rw-r--r--app/assets/javascripts/import_entities/import_groups/graphql/fragments/bulk_import_source_group_progress.fragment.graphql1
-rw-r--r--app/assets/javascripts/import_entities/import_groups/graphql/mutations/import_groups.mutation.graphql1
-rw-r--r--app/assets/javascripts/import_entities/import_groups/graphql/queries/bulk_import_source_groups.query.graphql9
-rw-r--r--app/assets/javascripts/import_entities/import_groups/graphql/services/local_storage_cache.js9
-rw-r--r--app/assets/javascripts/import_entities/import_groups/graphql/typedefs.graphql16
-rw-r--r--app/assets/javascripts/incidents/graphql/fragments/incident_fields.fragment.graphql1
-rw-r--r--app/assets/javascripts/incidents/graphql/queries/get_count_by_status.query.graphql1
-rw-r--r--app/assets/javascripts/incidents/graphql/queries/get_incidents.query.graphql4
-rw-r--r--app/assets/javascripts/init_confirm_danger.js2
-rw-r--r--app/assets/javascripts/init_issuable_sidebar.js16
-rw-r--r--app/assets/javascripts/init_labels.js19
-rw-r--r--app/assets/javascripts/integrations/constants.js4
-rw-r--r--app/assets/javascripts/integrations/edit/api.js9
-rw-r--r--app/assets/javascripts/integrations/edit/components/active_checkbox.vue11
-rw-r--r--app/assets/javascripts/integrations/edit/components/confirmation_modal.vue9
-rw-r--r--app/assets/javascripts/integrations/edit/components/integration_form.vue129
-rw-r--r--app/assets/javascripts/integrations/edit/components/jira_issues_fields.vue17
-rw-r--r--app/assets/javascripts/integrations/edit/components/reset_confirmation_modal.vue8
-rw-r--r--app/assets/javascripts/integrations/edit/index.js22
-rw-r--r--app/assets/javascripts/integrations/edit/store/actions.js29
-rw-r--r--app/assets/javascripts/integrations/edit/store/getters.js2
-rw-r--r--app/assets/javascripts/integrations/edit/store/mutation_types.js2
-rw-r--r--app/assets/javascripts/integrations/edit/store/mutations.js6
-rw-r--r--app/assets/javascripts/integrations/edit/store/state.js1
-rw-r--r--app/assets/javascripts/integrations/integration_settings_form.js151
-rw-r--r--app/assets/javascripts/integrations/overrides/components/integration_overrides.vue40
-rw-r--r--app/assets/javascripts/invite_members/components/invite_members_modal.vue69
-rw-r--r--app/assets/javascripts/invite_members/components/invite_members_trigger.vue15
-rw-r--r--app/assets/javascripts/invite_members/constants.js12
-rw-r--r--app/assets/javascripts/invite_members/init_invite_members_modal.js2
-rw-r--r--app/assets/javascripts/issuable/bulk_update_sidebar/components/status_select.vue (renamed from app/assets/javascripts/issuable_bulk_update_sidebar/components/status_select.vue)0
-rw-r--r--app/assets/javascripts/issuable/bulk_update_sidebar/constants.js (renamed from app/assets/javascripts/issuable_bulk_update_sidebar/constants.js)0
-rw-r--r--app/assets/javascripts/issuable/bulk_update_sidebar/init_issue_status_select.js (renamed from app/assets/javascripts/issuable_bulk_update_sidebar/init_issue_status_select.js)0
-rw-r--r--app/assets/javascripts/issuable/bulk_update_sidebar/issuable_bulk_update_actions.js (renamed from app/assets/javascripts/issuable_bulk_update_sidebar/issuable_bulk_update_actions.js)2
-rw-r--r--app/assets/javascripts/issuable/bulk_update_sidebar/issuable_bulk_update_sidebar.js (renamed from app/assets/javascripts/issuable_bulk_update_sidebar/issuable_bulk_update_sidebar.js)12
-rw-r--r--app/assets/javascripts/issuable/bulk_update_sidebar/issuable_init_bulk_update_sidebar.js (renamed from app/assets/javascripts/issuable_bulk_update_sidebar/issuable_init_bulk_update_sidebar.js)0
-rw-r--r--app/assets/javascripts/issuable/bulk_update_sidebar/subscription_select.js (renamed from app/assets/javascripts/issuable_bulk_update_sidebar/subscription_select.js)0
-rw-r--r--app/assets/javascripts/issuable/components/issuable_by_email.vue3
-rw-r--r--app/assets/javascripts/issuable/components/issuable_header_warnings.vue (renamed from app/assets/javascripts/vue_shared/components/issuable/issuable_header_warnings.vue)0
-rw-r--r--app/assets/javascripts/issuable/components/issue_assignees.vue (renamed from app/assets/javascripts/vue_shared/components/issue/issue_assignees.vue)0
-rw-r--r--app/assets/javascripts/issuable/components/issue_milestone.vue (renamed from app/assets/javascripts/vue_shared/components/issue/issue_milestone.vue)0
-rw-r--r--app/assets/javascripts/issuable/components/related_issuable_item.vue (renamed from app/assets/javascripts/vue_shared/components/issue/related_issuable_item.vue)4
-rw-r--r--app/assets/javascripts/issuable/constants.js5
-rw-r--r--app/assets/javascripts/issuable/index.js116
-rw-r--r--app/assets/javascripts/issuable/init_csv_import_export_buttons.js48
-rw-r--r--app/assets/javascripts/issuable/init_issuable_by_email.js35
-rw-r--r--app/assets/javascripts/issuable/issuable_context.js (renamed from app/assets/javascripts/issuable_context.js)4
-rw-r--r--app/assets/javascripts/issuable/issuable_form.js (renamed from app/assets/javascripts/issuable_form.js)16
-rw-r--r--app/assets/javascripts/issuable/issuable_template_selector.js (renamed from app/assets/javascripts/templates/issuable_template_selector.js)6
-rw-r--r--app/assets/javascripts/issuable/issuable_template_selectors.js (renamed from app/assets/javascripts/templates/issuable_template_selectors.js)5
-rw-r--r--app/assets/javascripts/issuable/mixins/related_issuable_mixin.js (renamed from app/assets/javascripts/vue_shared/mixins/related_issuable_mixin.js)0
-rw-r--r--app/assets/javascripts/issuable_index.js7
-rw-r--r--app/assets/javascripts/issuable_type_selector/index.js16
-rw-r--r--app/assets/javascripts/issues/constants.js25
-rw-r--r--app/assets/javascripts/issues/filtered_search_service_desk.js (renamed from app/assets/javascripts/pages/projects/issues/service_desk/filtered_search.js)0
-rw-r--r--app/assets/javascripts/issues/form.js (renamed from app/assets/javascripts/pages/projects/issues/form.js)15
-rw-r--r--app/assets/javascripts/issues/init_filtered_search_service_desk.js11
-rw-r--r--app/assets/javascripts/issues/issue.js (renamed from app/assets/javascripts/issue.js)12
-rw-r--r--app/assets/javascripts/issues/manual_ordering.js (renamed from app/assets/javascripts/manual_ordering.js)0
-rw-r--r--app/assets/javascripts/issues/new/components/title_suggestions.vue (renamed from app/assets/javascripts/issuable_suggestions/components/app.vue)8
-rw-r--r--app/assets/javascripts/issues/new/components/title_suggestions_item.vue (renamed from app/assets/javascripts/issuable_suggestions/components/item.vue)0
-rw-r--r--app/assets/javascripts/issues/new/components/type_popover.vue (renamed from app/assets/javascripts/issuable_type_selector/components/info_popover.vue)4
-rw-r--r--app/assets/javascripts/issues/new/index.js (renamed from app/assets/javascripts/issuable_suggestions/index.js)32
-rw-r--r--app/assets/javascripts/issues/new/queries/issues.query.graphql (renamed from app/assets/javascripts/issuable_suggestions/queries/issues.query.graphql)3
-rw-r--r--app/assets/javascripts/issues/related_merge_requests/components/related_merge_requests.vue (renamed from app/assets/javascripts/related_merge_requests/components/related_merge_requests.vue)4
-rw-r--r--app/assets/javascripts/issues/related_merge_requests/index.js (renamed from app/assets/javascripts/related_merge_requests/index.js)0
-rw-r--r--app/assets/javascripts/issues/related_merge_requests/store/actions.js (renamed from app/assets/javascripts/related_merge_requests/store/actions.js)0
-rw-r--r--app/assets/javascripts/issues/related_merge_requests/store/index.js (renamed from app/assets/javascripts/related_merge_requests/store/index.js)0
-rw-r--r--app/assets/javascripts/issues/related_merge_requests/store/mutation_types.js (renamed from app/assets/javascripts/related_merge_requests/store/mutation_types.js)0
-rw-r--r--app/assets/javascripts/issues/related_merge_requests/store/mutations.js (renamed from app/assets/javascripts/related_merge_requests/store/mutations.js)0
-rw-r--r--app/assets/javascripts/issues/related_merge_requests/store/state.js (renamed from app/assets/javascripts/related_merge_requests/store/state.js)0
-rw-r--r--app/assets/javascripts/issues/sentry_error_stack_trace/components/sentry_error_stack_trace.vue (renamed from app/assets/javascripts/sentry_error_stack_trace/components/sentry_error_stack_trace.vue)0
-rw-r--r--app/assets/javascripts/issues/sentry_error_stack_trace/index.js (renamed from app/assets/javascripts/sentry_error_stack_trace/index.js)0
-rw-r--r--app/assets/javascripts/issues/show.js (renamed from app/assets/javascripts/pages/projects/issues/show.js)19
-rw-r--r--app/assets/javascripts/issues/show/components/app.vue (renamed from app/assets/javascripts/issue_show/components/app.vue)33
-rw-r--r--app/assets/javascripts/issues/show/components/delete_issue_modal.vue71
-rw-r--r--app/assets/javascripts/issues/show/components/description.vue (renamed from app/assets/javascripts/issue_show/components/description.vue)4
-rw-r--r--app/assets/javascripts/issues/show/components/edit_actions.vue (renamed from app/assets/javascripts/issue_show/components/edit_actions.vue)53
-rw-r--r--app/assets/javascripts/issues/show/components/edited.vue (renamed from app/assets/javascripts/issue_show/components/edited.vue)2
-rw-r--r--app/assets/javascripts/issues/show/components/fields/description.vue (renamed from app/assets/javascripts/issue_show/components/fields/description.vue)0
-rw-r--r--app/assets/javascripts/issues/show/components/fields/description_template.vue (renamed from app/assets/javascripts/issue_show/components/fields/description_template.vue)2
-rw-r--r--app/assets/javascripts/issues/show/components/fields/title.vue (renamed from app/assets/javascripts/issue_show/components/fields/title.vue)0
-rw-r--r--app/assets/javascripts/issues/show/components/fields/type.vue (renamed from app/assets/javascripts/issue_show/components/fields/type.vue)0
-rw-r--r--app/assets/javascripts/issues/show/components/form.vue (renamed from app/assets/javascripts/issue_show/components/form.vue)7
-rw-r--r--app/assets/javascripts/issues/show/components/header_actions.vue (renamed from app/assets/javascripts/issue_show/components/header_actions.vue)96
-rw-r--r--app/assets/javascripts/issues/show/components/incidents/graphql/queries/get_alert.graphql (renamed from app/assets/javascripts/issue_show/components/incidents/graphql/queries/get_alert.graphql)1
-rw-r--r--app/assets/javascripts/issues/show/components/incidents/highlight_bar.vue (renamed from app/assets/javascripts/issue_show/components/incidents/highlight_bar.vue)2
-rw-r--r--app/assets/javascripts/issues/show/components/incidents/incident_tabs.vue (renamed from app/assets/javascripts/issue_show/components/incidents/incident_tabs.vue)2
-rw-r--r--app/assets/javascripts/issues/show/components/locked_warning.vue (renamed from app/assets/javascripts/issue_show/components/locked_warning.vue)0
-rw-r--r--app/assets/javascripts/issues/show/components/pinned_links.vue (renamed from app/assets/javascripts/issue_show/components/pinned_links.vue)0
-rw-r--r--app/assets/javascripts/issues/show/components/title.vue (renamed from app/assets/javascripts/issue_show/components/title.vue)0
-rw-r--r--app/assets/javascripts/issues/show/constants.js (renamed from app/assets/javascripts/issue_show/constants.js)24
-rw-r--r--app/assets/javascripts/issues/show/event_hub.js (renamed from app/assets/javascripts/issuable_show/event_hub.js)0
-rw-r--r--app/assets/javascripts/issues/show/graphql.js (renamed from app/assets/javascripts/issue_show/graphql.js)0
-rw-r--r--app/assets/javascripts/issues/show/incident.js (renamed from app/assets/javascripts/issue_show/incident.js)2
-rw-r--r--app/assets/javascripts/issues/show/issue.js (renamed from app/assets/javascripts/issue_show/issue.js)3
-rw-r--r--app/assets/javascripts/issues/show/mixins/animate.js (renamed from app/assets/javascripts/issue_show/mixins/animate.js)0
-rw-r--r--app/assets/javascripts/issues/show/mixins/update.js (renamed from app/assets/javascripts/issue_show/mixins/update.js)0
-rw-r--r--app/assets/javascripts/issues/show/queries/get_issue_state.query.graphql (renamed from app/assets/javascripts/issue_show/queries/get_issue_state.query.graphql)0
-rw-r--r--app/assets/javascripts/issues/show/queries/promote_to_epic.mutation.graphql (renamed from app/assets/javascripts/issue_show/queries/promote_to_epic.mutation.graphql)1
-rw-r--r--app/assets/javascripts/issues/show/queries/update_issue.mutation.graphql (renamed from app/assets/javascripts/issue_show/queries/update_issue.mutation.graphql)0
-rw-r--r--app/assets/javascripts/issues/show/queries/update_issue_state.mutation.graphql (renamed from app/assets/javascripts/issue_show/queries/update_issue_state.mutation.graphql)0
-rw-r--r--app/assets/javascripts/issues/show/services/index.js (renamed from app/assets/javascripts/issue_show/services/index.js)2
-rw-r--r--app/assets/javascripts/issues/show/stores/index.js (renamed from app/assets/javascripts/issue_show/stores/index.js)0
-rw-r--r--app/assets/javascripts/issues/show/utils/parse_data.js (renamed from app/assets/javascripts/issue_show/utils/parse_data.js)0
-rw-r--r--app/assets/javascripts/issues/show/utils/update_description.js (renamed from app/assets/javascripts/issue_show/utils/update_description.js)0
-rw-r--r--app/assets/javascripts/issues_list/components/issuable.vue2
-rw-r--r--app/assets/javascripts/issues_list/components/issuables_list_app.vue12
-rw-r--r--app/assets/javascripts/issues_list/components/issue_card_time_info.vue18
-rw-r--r--app/assets/javascripts/issues_list/components/issues_list_app.vue171
-rw-r--r--app/assets/javascripts/issues_list/constants.js59
-rw-r--r--app/assets/javascripts/issues_list/index.js6
-rw-r--r--app/assets/javascripts/issues_list/queries/get_issues.query.graphql2
-rw-r--r--app/assets/javascripts/issues_list/queries/get_issues_counts.query.graphql2
-rw-r--r--app/assets/javascripts/issues_list/queries/get_issues_list_details.query.graphql3
-rw-r--r--app/assets/javascripts/issues_list/queries/issue.fragment.graphql1
-rw-r--r--app/assets/javascripts/issues_list/queries/iteration.fragment.graphql10
-rw-r--r--app/assets/javascripts/issues_list/queries/search_iterations.query.graphql18
-rw-r--r--app/assets/javascripts/issues_list/queries/search_labels.query.graphql2
-rw-r--r--app/assets/javascripts/issues_list/queries/search_milestones.query.graphql2
-rw-r--r--app/assets/javascripts/issues_list/queries/search_projects.query.graphql1
-rw-r--r--app/assets/javascripts/issues_list/queries/search_users.query.graphql4
-rw-r--r--app/assets/javascripts/issues_list/utils.js3
-rw-r--r--app/assets/javascripts/jira_connect/branches/graphql/queries/get_project.query.graphql1
-rw-r--r--app/assets/javascripts/jira_connect/branches/index.js2
-rw-r--r--app/assets/javascripts/jira_connect/subscriptions/components/app.vue4
-rw-r--r--app/assets/javascripts/jira_connect/subscriptions/components/user_link.vue67
-rw-r--r--app/assets/javascripts/jira_connect/subscriptions/index.js21
-rw-r--r--app/assets/javascripts/jira_import/queries/get_jira_import_details.query.graphql1
-rw-r--r--app/assets/javascripts/jira_import/queries/jira_import.fragment.graphql1
-rw-r--r--app/assets/javascripts/jira_import/queries/search_project_members.query.graphql2
-rw-r--r--app/assets/javascripts/jobs/bridge/app.vue20
-rw-r--r--app/assets/javascripts/jobs/bridge/components/constants.js1
-rw-r--r--app/assets/javascripts/jobs/bridge/components/empty_state.vue45
-rw-r--r--app/assets/javascripts/jobs/bridge/components/sidebar.vue98
-rw-r--r--app/assets/javascripts/jobs/components/job_log_controllers.vue2
-rw-r--r--app/assets/javascripts/jobs/components/sidebar.vue2
-rw-r--r--app/assets/javascripts/jobs/components/sidebar_job_details_container.vue5
-rw-r--r--app/assets/javascripts/jobs/components/table/cells/actions_cell.vue16
-rw-r--r--app/assets/javascripts/jobs/components/table/cells/pipeline_cell.vue10
-rw-r--r--app/assets/javascripts/jobs/components/table/constants.js67
-rw-r--r--app/assets/javascripts/jobs/components/table/graphql/queries/get_jobs.query.graphql7
-rw-r--r--app/assets/javascripts/jobs/components/table/jobs_table.vue69
-rw-r--r--app/assets/javascripts/jobs/index.js39
-rw-r--r--app/assets/javascripts/labels/components/delete_label_modal.vue (renamed from app/assets/javascripts/vue_shared/components/delete_label_modal.vue)0
-rw-r--r--app/assets/javascripts/labels/components/promote_label_modal.vue (renamed from app/assets/javascripts/pages/projects/labels/components/promote_label_modal.vue)0
-rw-r--r--app/assets/javascripts/labels/create_label_dropdown.js (renamed from app/assets/javascripts/create_label.js)4
-rw-r--r--app/assets/javascripts/labels/event_hub.js (renamed from app/assets/javascripts/issue_show/event_hub.js)0
-rw-r--r--app/assets/javascripts/labels/group_label_subscription.js (renamed from app/assets/javascripts/group_label_subscription.js)4
-rw-r--r--app/assets/javascripts/labels/index.js137
-rw-r--r--app/assets/javascripts/labels/label_manager.js (renamed from app/assets/javascripts/label_manager.js)6
-rw-r--r--app/assets/javascripts/labels/labels.js (renamed from app/assets/javascripts/labels.js)0
-rw-r--r--app/assets/javascripts/labels/labels_select.js (renamed from app/assets/javascripts/labels_select.js)12
-rw-r--r--app/assets/javascripts/labels/project_label_subscription.js (renamed from app/assets/javascripts/project_label_subscription.js)6
-rw-r--r--app/assets/javascripts/lib/dompurify.js2
-rw-r--r--app/assets/javascripts/lib/utils/common_utils.js11
-rw-r--r--app/assets/javascripts/lib/utils/constants.js1
-rw-r--r--app/assets/javascripts/lib/utils/dom_utils.js14
-rw-r--r--app/assets/javascripts/lib/utils/intersection_observer.js28
-rw-r--r--app/assets/javascripts/lib/utils/navigation_utility.js39
-rw-r--r--app/assets/javascripts/lib/utils/url_utility.js2
-rw-r--r--app/assets/javascripts/main.js40
-rw-r--r--app/assets/javascripts/members/components/action_buttons/access_request_action_buttons.vue1
-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_member_button.vue16
-rw-r--r--app/assets/javascripts/members/components/action_buttons/user_action_buttons.vue18
-rw-r--r--app/assets/javascripts/members/components/table/member_action_buttons.vue5
-rw-r--r--app/assets/javascripts/members/components/table/members_table.vue5
-rw-r--r--app/assets/javascripts/merge_conflicts/merge_conflicts_bundle.js2
-rw-r--r--app/assets/javascripts/milestone.js49
-rw-r--r--app/assets/javascripts/milestones/components/delete_milestone_modal.vue (renamed from app/assets/javascripts/pages/milestones/shared/components/delete_milestone_modal.vue)0
-rw-r--r--app/assets/javascripts/milestones/components/promote_milestone_modal.vue (renamed from app/assets/javascripts/pages/milestones/shared/components/promote_milestone_modal.vue)0
-rw-r--r--app/assets/javascripts/milestones/event_hub.js (renamed from app/assets/javascripts/pages/milestones/shared/event_hub.js)0
-rw-r--r--app/assets/javascripts/milestones/index.js (renamed from app/assets/javascripts/pages/milestones/shared/delete_milestone_modal_init.js)52
-rw-r--r--app/assets/javascripts/milestones/milestone.js49
-rw-r--r--app/assets/javascripts/milestones/milestone_select.js (renamed from app/assets/javascripts/milestone_select.js)6
-rw-r--r--app/assets/javascripts/milestones/utils.js (renamed from app/assets/javascripts/milestones/milestone_utils.js)0
-rw-r--r--app/assets/javascripts/monitoring/queries/getDashboardValidationWarnings.query.graphql1
-rw-r--r--app/assets/javascripts/mr_popover/components/mr_popover.vue11
-rw-r--r--app/assets/javascripts/mr_popover/queries/merge_request.query.graphql5
-rw-r--r--app/assets/javascripts/network/branch_graph.js10
-rw-r--r--app/assets/javascripts/notes/components/comment_form.vue26
-rw-r--r--app/assets/javascripts/notes/components/diff_with_note.vue14
-rw-r--r--app/assets/javascripts/notes/components/discussion_filter.vue3
-rw-r--r--app/assets/javascripts/notes/components/discussion_locked_widget.vue12
-rw-r--r--app/assets/javascripts/notes/components/discussion_notes.vue90
-rw-r--r--app/assets/javascripts/notes/components/note_body.vue2
-rw-r--r--app/assets/javascripts/notes/components/noteable_discussion.vue18
-rw-r--r--app/assets/javascripts/notes/components/noteable_note.vue15
-rw-r--r--app/assets/javascripts/notes/components/notes_app.vue8
-rw-r--r--app/assets/javascripts/notes/components/sidebar_subscription.vue2
-rw-r--r--app/assets/javascripts/notes/i18n.js16
-rw-r--r--app/assets/javascripts/notes/stores/actions.js3
-rw-r--r--app/assets/javascripts/notes/stores/mutation_types.js1
-rw-r--r--app/assets/javascripts/notes/stores/mutations.js4
-rw-r--r--app/assets/javascripts/packages/list/packages_list_app_bundle.js23
-rw-r--r--app/assets/javascripts/packages/shared/constants.js49
-rw-r--r--app/assets/javascripts/packages/shared/utils.js43
-rw-r--r--app/assets/javascripts/packages_and_registries/container_registry/explorer/components/details_page/delete_modal.vue1
-rw-r--r--app/assets/javascripts/packages_and_registries/container_registry/explorer/components/details_page/details_header.vue1
-rw-r--r--app/assets/javascripts/packages_and_registries/container_registry/explorer/components/details_page/tags_list.vue86
-rw-r--r--app/assets/javascripts/packages_and_registries/container_registry/explorer/graphql/queries/get_container_repositories_details.query.graphql2
-rw-r--r--app/assets/javascripts/packages_and_registries/container_registry/explorer/graphql/queries/get_container_repository_details.query.graphql1
-rw-r--r--app/assets/javascripts/packages_and_registries/container_registry/explorer/graphql/queries/get_container_repository_tags.query.graphql1
-rw-r--r--app/assets/javascripts/packages_and_registries/container_registry/explorer/pages/details.vue6
-rw-r--r--app/assets/javascripts/packages_and_registries/container_registry/explorer/pages/list.vue1
-rw-r--r--app/assets/javascripts/packages_and_registries/dependency_proxy/app.vue17
-rw-r--r--app/assets/javascripts/packages_and_registries/dependency_proxy/graphql/queries/get_dependency_proxy_details.query.graphql2
-rw-r--r--app/assets/javascripts/packages_and_registries/infrastructure_registry/details/components/app.vue23
-rw-r--r--app/assets/javascripts/packages_and_registries/infrastructure_registry/details/store/actions.js2
-rw-r--r--app/assets/javascripts/packages_and_registries/infrastructure_registry/list/components/infrastructure_search.vue (renamed from app/assets/javascripts/packages_and_registries/infrastructure_registry/components/infrastructure_search.vue)4
-rw-r--r--app/assets/javascripts/packages_and_registries/infrastructure_registry/list/components/infrastructure_title.vue (renamed from app/assets/javascripts/packages_and_registries/infrastructure_registry/components/infrastructure_title.vue)0
-rw-r--r--app/assets/javascripts/packages_and_registries/infrastructure_registry/list/components/packages_list.vue (renamed from app/assets/javascripts/packages/list/components/packages_list.vue)20
-rw-r--r--app/assets/javascripts/packages_and_registries/infrastructure_registry/list/components/packages_list_app.vue (renamed from app/assets/javascripts/packages/list/components/packages_list_app.vue)15
-rw-r--r--app/assets/javascripts/packages_and_registries/infrastructure_registry/list/constants.js (renamed from app/assets/javascripts/packages/list/constants.js)50
-rw-r--r--app/assets/javascripts/packages_and_registries/infrastructure_registry/list/stores/actions.js (renamed from app/assets/javascripts/packages/list/stores/actions.js)2
-rw-r--r--app/assets/javascripts/packages_and_registries/infrastructure_registry/list/stores/getters.js (renamed from app/assets/javascripts/packages/list/stores/getters.js)2
-rw-r--r--app/assets/javascripts/packages_and_registries/infrastructure_registry/list/stores/index.js (renamed from app/assets/javascripts/packages/list/stores/index.js)0
-rw-r--r--app/assets/javascripts/packages_and_registries/infrastructure_registry/list/stores/mutation_types.js (renamed from app/assets/javascripts/packages/list/stores/mutation_types.js)0
-rw-r--r--app/assets/javascripts/packages_and_registries/infrastructure_registry/list/stores/mutations.js (renamed from app/assets/javascripts/packages/list/stores/mutations.js)0
-rw-r--r--app/assets/javascripts/packages_and_registries/infrastructure_registry/list/stores/state.js (renamed from app/assets/javascripts/packages/list/stores/state.js)0
-rw-r--r--app/assets/javascripts/packages_and_registries/infrastructure_registry/list/utils.js (renamed from app/assets/javascripts/packages/list/utils.js)0
-rw-r--r--app/assets/javascripts/packages_and_registries/infrastructure_registry/list_app_bundle.js7
-rw-r--r--app/assets/javascripts/packages_and_registries/infrastructure_registry/shared/constants.js1
-rw-r--r--app/assets/javascripts/packages_and_registries/infrastructure_registry/shared/infrastructure_icon_and_name.vue (renamed from app/assets/javascripts/packages_and_registries/infrastructure_registry/components/infrastructure_icon_and_name.vue)0
-rw-r--r--app/assets/javascripts/packages_and_registries/infrastructure_registry/shared/package_list_row.vue (renamed from app/assets/javascripts/packages/shared/components/package_list_row.vue)33
-rw-r--r--app/assets/javascripts/packages_and_registries/package_registry/components/details/app.vue4
-rw-r--r--app/assets/javascripts/packages_and_registries/package_registry/components/details/package_title.vue2
-rw-r--r--app/assets/javascripts/packages_and_registries/package_registry/components/details/version_row.vue4
-rw-r--r--app/assets/javascripts/packages_and_registries/package_registry/components/list/package_list_row.vue17
-rw-r--r--app/assets/javascripts/packages_and_registries/package_registry/components/list/packages_list.vue3
-rw-r--r--app/assets/javascripts/packages_and_registries/package_registry/constants.js24
-rw-r--r--app/assets/javascripts/packages_and_registries/package_registry/graphql/fragments/package_data.fragment.graphql6
-rw-r--r--app/assets/javascripts/packages_and_registries/package_registry/graphql/queries/get_package_details.query.graphql7
-rw-r--r--app/assets/javascripts/packages_and_registries/package_registry/graphql/queries/get_packages.query.graphql2
-rw-r--r--app/assets/javascripts/packages_and_registries/package_registry/index.js30
-rw-r--r--app/assets/javascripts/packages_and_registries/package_registry/pages/index.vue5
-rw-r--r--app/assets/javascripts/packages_and_registries/package_registry/pages/list.js24
-rw-r--r--app/assets/javascripts/packages_and_registries/package_registry/pages/list.vue (renamed from app/assets/javascripts/packages_and_registries/package_registry/components/list/app.vue)28
-rw-r--r--app/assets/javascripts/packages_and_registries/package_registry/router.js21
-rw-r--r--app/assets/javascripts/packages_and_registries/settings/group/bundle.js3
-rw-r--r--app/assets/javascripts/packages_and_registries/settings/group/components/dependency_proxy_settings.vue123
-rw-r--r--app/assets/javascripts/packages_and_registries/settings/group/components/group_settings_app.vue4
-rw-r--r--app/assets/javascripts/packages_and_registries/settings/group/components/settings_titles.vue7
-rw-r--r--app/assets/javascripts/packages_and_registries/settings/group/graphql/mutations/update_dependency_proxy_image_ttl_group_policy.mutation.graphql11
-rw-r--r--app/assets/javascripts/packages_and_registries/settings/group/graphql/queries/get_group_packages_settings.query.graphql5
-rw-r--r--app/assets/javascripts/packages_and_registries/settings/group/graphql/utils/cache_update.js5
-rw-r--r--app/assets/javascripts/packages_and_registries/settings/group/graphql/utils/optimistic_responses.js12
-rw-r--r--app/assets/javascripts/packages_and_registries/settings/project/graphql/queries/get_expiration_policy.query.graphql1
-rw-r--r--app/assets/javascripts/packages_and_registries/shared/components/package_icon_and_name.vue (renamed from app/assets/javascripts/packages/shared/components/package_icon_and_name.vue)0
-rw-r--r--app/assets/javascripts/packages_and_registries/shared/components/package_path.vue (renamed from app/assets/javascripts/packages/shared/components/package_path.vue)0
-rw-r--r--app/assets/javascripts/packages_and_registries/shared/components/package_tags.vue (renamed from app/assets/javascripts/packages/shared/components/package_tags.vue)0
-rw-r--r--app/assets/javascripts/packages_and_registries/shared/components/packages_list_loader.vue (renamed from app/assets/javascripts/packages/shared/components/packages_list_loader.vue)0
-rw-r--r--app/assets/javascripts/packages_and_registries/shared/components/publish_method.vue (renamed from app/assets/javascripts/packages/shared/components/publish_method.vue)0
-rw-r--r--app/assets/javascripts/packages_and_registries/shared/components/registry_list.vue124
-rw-r--r--app/assets/javascripts/packages_and_registries/shared/constants.js36
-rw-r--r--app/assets/javascripts/packages_and_registries/shared/utils.js10
-rw-r--r--app/assets/javascripts/pages/admin/integrations/edit/index.js18
-rw-r--r--app/assets/javascripts/pages/admin/labels/edit/index.js2
-rw-r--r--app/assets/javascripts/pages/admin/labels/index/index.js24
-rw-r--r--app/assets/javascripts/pages/admin/labels/new/index.js2
-rw-r--r--app/assets/javascripts/pages/admin/services/edit/index.js4
-rw-r--r--app/assets/javascripts/pages/admin/services/index/index.js4
-rw-r--r--app/assets/javascripts/pages/constants.js6
-rw-r--r--app/assets/javascripts/pages/dashboard/issues/index.js4
-rw-r--r--app/assets/javascripts/pages/dashboard/merge_requests/index.js2
-rw-r--r--app/assets/javascripts/pages/dashboard/milestones/show/index.js2
-rw-r--r--app/assets/javascripts/pages/dashboard/projects/index/components/customize_homepage_banner.vue102
-rw-r--r--app/assets/javascripts/pages/dashboard/projects/index/index.js2
-rw-r--r--app/assets/javascripts/pages/dashboard/projects/index/init_customize_homepage_banner.js16
-rw-r--r--app/assets/javascripts/pages/groups/issues/index.js6
-rw-r--r--app/assets/javascripts/pages/groups/labels/edit/index.js2
-rw-r--r--app/assets/javascripts/pages/groups/labels/index/index.js3
-rw-r--r--app/assets/javascripts/pages/groups/labels/new/index.js2
-rw-r--r--app/assets/javascripts/pages/groups/merge_requests/index.js4
-rw-r--r--app/assets/javascripts/pages/groups/milestones/edit/index.js2
-rw-r--r--app/assets/javascripts/pages/groups/milestones/new/index.js2
-rw-r--r--app/assets/javascripts/pages/groups/milestones/show/index.js5
-rw-r--r--app/assets/javascripts/pages/groups/packages/index/index.js4
-rw-r--r--app/assets/javascripts/pages/groups/settings/ci_cd/show/index.js2
-rw-r--r--app/assets/javascripts/pages/groups/settings/integrations/edit/index.js10
-rw-r--r--app/assets/javascripts/pages/help/ui/index.js3
-rw-r--r--app/assets/javascripts/pages/import/bulk_imports/history/components/bulk_imports_history_app.vue3
-rw-r--r--app/assets/javascripts/pages/milestones/shared/index.js7
-rw-r--r--app/assets/javascripts/pages/milestones/shared/init_milestones_show.js11
-rw-r--r--app/assets/javascripts/pages/milestones/shared/promote_milestone_modal_init.js19
-rw-r--r--app/assets/javascripts/pages/profiles/personal_access_tokens/index.js3
-rw-r--r--app/assets/javascripts/pages/projects/constants.js4
-rw-r--r--app/assets/javascripts/pages/projects/edit/index.js6
-rw-r--r--app/assets/javascripts/pages/projects/incidents/show/index.js2
-rw-r--r--app/assets/javascripts/pages/projects/issues/edit/index.js2
-rw-r--r--app/assets/javascripts/pages/projects/issues/index/index.js13
-rw-r--r--app/assets/javascripts/pages/projects/issues/new/index.js2
-rw-r--r--app/assets/javascripts/pages/projects/issues/service_desk/index.js11
-rw-r--r--app/assets/javascripts/pages/projects/issues/show/index.js2
-rw-r--r--app/assets/javascripts/pages/projects/labels/edit/index.js2
-rw-r--r--app/assets/javascripts/pages/projects/labels/index/index.js82
-rw-r--r--app/assets/javascripts/pages/projects/labels/new/index.js2
-rw-r--r--app/assets/javascripts/pages/projects/learn_gitlab/components/learn_gitlab.vue53
-rw-r--r--app/assets/javascripts/pages/projects/learn_gitlab/components/learn_gitlab_section_link.vue49
-rw-r--r--app/assets/javascripts/pages/projects/learn_gitlab/index/index.js5
-rw-r--r--app/assets/javascripts/pages/projects/merge_requests/index/index.js12
-rw-r--r--app/assets/javascripts/pages/projects/merge_requests/init_merge_request.js8
-rw-r--r--app/assets/javascripts/pages/projects/merge_requests/init_merge_request_show.js2
-rw-r--r--app/assets/javascripts/pages/projects/merge_requests/queries/get_state.query.graphql2
-rw-r--r--app/assets/javascripts/pages/projects/merge_requests/show/index.js4
-rw-r--r--app/assets/javascripts/pages/projects/milestones/edit/index.js2
-rw-r--r--app/assets/javascripts/pages/projects/milestones/index/index.js5
-rw-r--r--app/assets/javascripts/pages/projects/milestones/new/index.js2
-rw-r--r--app/assets/javascripts/pages/projects/milestones/show/index.js8
-rw-r--r--app/assets/javascripts/pages/projects/packages/packages/index/index.js4
-rw-r--r--app/assets/javascripts/pages/projects/path_locks/index.js3
-rw-r--r--app/assets/javascripts/pages/projects/services/edit/index.js5
-rw-r--r--app/assets/javascripts/pages/projects/usage_quotas/index.js23
-rw-r--r--app/assets/javascripts/pages/shared/wikis/components/wiki_form.vue70
-rw-r--r--app/assets/javascripts/pdf/index.vue2
-rw-r--r--app/assets/javascripts/pipeline_editor/components/commit/commit_form.vue6
-rw-r--r--app/assets/javascripts/pipeline_editor/components/commit/commit_section.vue30
-rw-r--r--app/assets/javascripts/pipeline_editor/components/drawer/pipeline_editor_drawer.vue10
-rw-r--r--app/assets/javascripts/pipeline_editor/components/editor/text_editor.vue2
-rw-r--r--app/assets/javascripts/pipeline_editor/components/file_nav/branch_switcher.vue41
-rw-r--r--app/assets/javascripts/pipeline_editor/components/header/pipeline_status.vue28
-rw-r--r--app/assets/javascripts/pipeline_editor/components/header/validation_segment.vue7
-rw-r--r--app/assets/javascripts/pipeline_editor/components/lint/ci_lint_results.vue8
-rw-r--r--app/assets/javascripts/pipeline_editor/components/pipeline_editor_tabs.vue5
-rw-r--r--app/assets/javascripts/pipeline_editor/graphql/mutations/client/lint_ci.mutation.graphql (renamed from app/assets/javascripts/pipeline_editor/graphql/mutations/lint_ci.mutation.graphql)0
-rw-r--r--app/assets/javascripts/pipeline_editor/graphql/mutations/client/update_app_status.mutation.graphql (renamed from app/assets/javascripts/pipeline_editor/graphql/mutations/update_app_status.mutation.graphql)0
-rw-r--r--app/assets/javascripts/pipeline_editor/graphql/mutations/client/update_current_branch.mutation.graphql (renamed from app/assets/javascripts/pipeline_editor/graphql/mutations/update_current_branch.mutation.graphql)0
-rw-r--r--app/assets/javascripts/pipeline_editor/graphql/mutations/client/update_last_commit_branch.mutation.graphql (renamed from app/assets/javascripts/pipeline_editor/graphql/mutations/update_last_commit_branch.mutation.graphql)0
-rw-r--r--app/assets/javascripts/pipeline_editor/graphql/mutations/client/update_pipeline_etag.mutation.graphql (renamed from app/assets/javascripts/pipeline_editor/graphql/mutations/update_pipeline_etag.mutation.graphql)0
-rw-r--r--app/assets/javascripts/pipeline_editor/graphql/mutations/commit_ci_file.mutation.graphql3
-rw-r--r--app/assets/javascripts/pipeline_editor/graphql/queries/available_branches.query.graphql (renamed from app/assets/javascripts/pipeline_editor/graphql/queries/available_branches.graphql)1
-rw-r--r--app/assets/javascripts/pipeline_editor/graphql/queries/blob_content.query.graphql (renamed from app/assets/javascripts/pipeline_editor/graphql/queries/blob_content.graphql)2
-rw-r--r--app/assets/javascripts/pipeline_editor/graphql/queries/ci_config.query.graphql (renamed from app/assets/javascripts/pipeline_editor/graphql/queries/ci_config.graphql)0
-rw-r--r--app/assets/javascripts/pipeline_editor/graphql/queries/client/app_status.graphql3
-rw-r--r--app/assets/javascripts/pipeline_editor/graphql/queries/client/app_status.query.graphql5
-rw-r--r--app/assets/javascripts/pipeline_editor/graphql/queries/client/current_branch.graphql3
-rw-r--r--app/assets/javascripts/pipeline_editor/graphql/queries/client/current_branch.query.graphql7
-rw-r--r--app/assets/javascripts/pipeline_editor/graphql/queries/client/last_commit_branch.query.graphql6
-rw-r--r--app/assets/javascripts/pipeline_editor/graphql/queries/client/pipeline_etag.graphql3
-rw-r--r--app/assets/javascripts/pipeline_editor/graphql/queries/client/pipeline_etag.query.graphql5
-rw-r--r--app/assets/javascripts/pipeline_editor/graphql/queries/get_starter_template.query.graphql1
-rw-r--r--app/assets/javascripts/pipeline_editor/graphql/queries/latest_commit_sha.query.graphql2
-rw-r--r--app/assets/javascripts/pipeline_editor/graphql/queries/pipeline.query.graphql (renamed from app/assets/javascripts/pipeline_editor/graphql/queries/client/pipeline.graphql)3
-rw-r--r--app/assets/javascripts/pipeline_editor/graphql/resolvers.js46
-rw-r--r--app/assets/javascripts/pipeline_editor/graphql/typedefs.graphql22
-rw-r--r--app/assets/javascripts/pipeline_editor/index.js38
-rw-r--r--app/assets/javascripts/pipeline_editor/pipeline_editor_app.vue67
-rw-r--r--app/assets/javascripts/pipelines/components/graph/graph_view_selector.vue39
-rw-r--r--app/assets/javascripts/pipelines/components/graph/job_group_dropdown.vue2
-rw-r--r--app/assets/javascripts/pipelines/components/graph/job_item.vue8
-rw-r--r--app/assets/javascripts/pipelines/components/graph/linked_pipeline.vue8
-rw-r--r--app/assets/javascripts/pipelines/components/jobs/jobs_app.vue121
-rw-r--r--app/assets/javascripts/pipelines/components/pipeline_graph/job_pill.vue2
-rw-r--r--app/assets/javascripts/pipelines/components/pipeline_graph/pipeline_graph.vue2
-rw-r--r--app/assets/javascripts/pipelines/components/pipeline_graph/stage_name.vue2
-rw-r--r--app/assets/javascripts/pipelines/components/pipelines_list/pipeline_stage.vue8
-rw-r--r--app/assets/javascripts/pipelines/graphql/queries/get_dag_vis_data.query.graphql5
-rw-r--r--app/assets/javascripts/pipelines/graphql/queries/get_pipeline_header_data.query.graphql2
-rw-r--r--app/assets/javascripts/pipelines/graphql/queries/get_pipeline_jobs.query.graphql70
-rw-r--r--app/assets/javascripts/pipelines/pipeline_details_bundle.js12
-rw-r--r--app/assets/javascripts/pipelines/pipeline_details_jobs.js34
-rw-r--r--app/assets/javascripts/projects/commit/constants.js2
-rw-r--r--app/assets/javascripts/projects/commit_box/info/graphql/queries/get_linked_pipelines.query.graphql6
-rw-r--r--app/assets/javascripts/projects/new/components/new_project_url_select.vue34
-rw-r--r--app/assets/javascripts/projects/new/queries/search_namespaces_where_user_can_create_projects.query.graphql1
-rw-r--r--app/assets/javascripts/projects/pipelines/charts/components/app.vue25
-rw-r--r--app/assets/javascripts/projects/pipelines/charts/components/statistics_list.vue17
-rw-r--r--app/assets/javascripts/projects/pipelines/charts/graphql/queries/get_pipeline_count_by_status.query.graphql1
-rw-r--r--app/assets/javascripts/projects/pipelines/charts/graphql/queries/get_project_pipeline_statistics.query.graphql1
-rw-r--r--app/assets/javascripts/projects/pipelines/charts/index.js5
-rw-r--r--app/assets/javascripts/projects/settings/components/transfer_project_form.vue63
-rw-r--r--app/assets/javascripts/projects/settings/init_transfer_project_form.js53
-rw-r--r--app/assets/javascripts/projects/settings_service_desk/components/service_desk_setting.vue201
-rw-r--r--app/assets/javascripts/projects/storage_counter/components/app.vue106
-rw-r--r--app/assets/javascripts/projects/storage_counter/components/storage_table.vue88
-rw-r--r--app/assets/javascripts/projects/storage_counter/components/storage_type_icon.vue35
-rw-r--r--app/assets/javascripts/projects/storage_counter/constants.js61
-rw-r--r--app/assets/javascripts/projects/storage_counter/index.js51
-rw-r--r--app/assets/javascripts/projects/storage_counter/queries/project_storage.query.graphql16
-rw-r--r--app/assets/javascripts/projects/storage_counter/utils.js36
-rw-r--r--app/assets/javascripts/related_issues/components/issue_token.vue2
-rw-r--r--app/assets/javascripts/related_issues/components/related_issues_list.vue2
-rw-r--r--app/assets/javascripts/releases/graphql/fragments/release.fragment.graphql3
-rw-r--r--app/assets/javascripts/releases/graphql/fragments/release_for_editing.fragment.graphql1
-rw-r--r--app/assets/javascripts/releases/graphql/queries/all_releases.query.graphql1
-rw-r--r--app/assets/javascripts/releases/graphql/queries/one_release.query.graphql1
-rw-r--r--app/assets/javascripts/releases/graphql/queries/one_release_for_editing.query.graphql1
-rw-r--r--app/assets/javascripts/repository/commits_service.js11
-rw-r--r--app/assets/javascripts/repository/components/blob_button_group.vue9
-rw-r--r--app/assets/javascripts/repository/components/blob_content_viewer.vue15
-rw-r--r--app/assets/javascripts/repository/components/blob_viewers/index.js16
-rw-r--r--app/assets/javascripts/repository/components/blob_viewers/pdf_viewer.vue50
-rw-r--r--app/assets/javascripts/repository/components/blob_viewers/text_viewer.vue25
-rw-r--r--app/assets/javascripts/repository/components/delete_blob_modal.vue14
-rw-r--r--app/assets/javascripts/repository/components/last_commit.vue2
-rw-r--r--app/assets/javascripts/repository/components/table/row.vue16
-rw-r--r--app/assets/javascripts/repository/components/tree_content.vue21
-rw-r--r--app/assets/javascripts/repository/components/upload_blob_modal.vue4
-rw-r--r--app/assets/javascripts/repository/constants.js7
-rw-r--r--app/assets/javascripts/repository/index.js2
-rw-r--r--app/assets/javascripts/repository/mutations/lock_path.mutation.graphql1
-rw-r--r--app/assets/javascripts/repository/pages/tree.vue2
-rw-r--r--app/assets/javascripts/repository/queries/blob_info.query.graphql7
-rw-r--r--app/assets/javascripts/right_sidebar.js9
-rw-r--r--app/assets/javascripts/runner/admin_runners/admin_runners_app.vue18
-rw-r--r--app/assets/javascripts/runner/components/cells/runner_actions_cell.vue83
-rw-r--r--app/assets/javascripts/runner/components/cells/runner_status_cell.vue12
-rw-r--r--app/assets/javascripts/runner/components/cells/runner_summary_cell.vue2
-rw-r--r--app/assets/javascripts/runner/components/runner_delete_modal.vue51
-rw-r--r--app/assets/javascripts/runner/components/runner_filtered_search_bar.vue32
-rw-r--r--app/assets/javascripts/runner/components/runner_list.vue36
-rw-r--r--app/assets/javascripts/runner/components/runner_status_badge.vue (renamed from app/assets/javascripts/runner/components/runner_contacted_state_badge.vue)35
-rw-r--r--app/assets/javascripts/runner/components/search_tokens/status_token_config.js2
-rw-r--r--app/assets/javascripts/runner/components/search_tokens/tag_token.vue1
-rw-r--r--app/assets/javascripts/runner/components/stat/runner_online_stat.vue17
-rw-r--r--app/assets/javascripts/runner/constants.js17
-rw-r--r--app/assets/javascripts/runner/graphql/get_group_runners.query.graphql1
-rw-r--r--app/assets/javascripts/runner/graphql/get_runner.query.graphql2
-rw-r--r--app/assets/javascripts/runner/graphql/runner_node.fragment.graphql3
-rw-r--r--app/assets/javascripts/runner/graphql/runner_update.mutation.graphql2
-rw-r--r--app/assets/javascripts/runner/group_runners/group_runners_app.vue10
-rw-r--r--app/assets/javascripts/security_configuration/components/app.vue17
-rw-r--r--app/assets/javascripts/security_configuration/components/constants.js71
-rw-r--r--app/assets/javascripts/security_configuration/components/training_provider_list.vue61
-rw-r--r--app/assets/javascripts/security_configuration/graphql/security_training_providers.query.graphql9
-rw-r--r--app/assets/javascripts/security_configuration/index.js31
-rw-r--r--app/assets/javascripts/security_configuration/utils.js11
-rw-r--r--app/assets/javascripts/set_status_modal/set_status_modal_wrapper.vue11
-rw-r--r--app/assets/javascripts/shared/milestones/form.js22
-rw-r--r--app/assets/javascripts/sidebar/components/assignees/assignees_realtime.vue2
-rw-r--r--app/assets/javascripts/sidebar/components/assignees/collapsed_assignee_list.vue2
-rw-r--r--app/assets/javascripts/sidebar/components/assignees/sidebar_assignees_widget.vue2
-rw-r--r--app/assets/javascripts/sidebar/components/assignees/uncollapsed_assignee_list.vue2
-rw-r--r--app/assets/javascripts/sidebar/components/attention_requested_toggle.vue2
-rw-r--r--app/assets/javascripts/sidebar/components/confidential/sidebar_confidentiality_form.vue2
-rw-r--r--app/assets/javascripts/sidebar/components/crm_contacts/crm_contacts.vue131
-rw-r--r--app/assets/javascripts/sidebar/components/crm_contacts/queries/get_issue_crm_contacts.query.graphql7
-rw-r--r--app/assets/javascripts/sidebar/components/crm_contacts/queries/issue_crm_contacts.fragment.graphql17
-rw-r--r--app/assets/javascripts/sidebar/components/crm_contacts/queries/issue_crm_contacts.subscription.graphql9
-rw-r--r--app/assets/javascripts/sidebar/components/date/sidebar_date_widget.vue13
-rw-r--r--app/assets/javascripts/sidebar/components/date/sidebar_inherit_date.vue4
-rw-r--r--app/assets/javascripts/sidebar/components/labels/sidebar_labels.vue192
-rw-r--r--app/assets/javascripts/sidebar/components/lock/mutations/update_issue_lock.mutation.graphql1
-rw-r--r--app/assets/javascripts/sidebar/components/lock/mutations/update_merge_request_lock.mutation.graphql1
-rw-r--r--app/assets/javascripts/sidebar/components/reviewers/collapsed_reviewer_list.vue2
-rw-r--r--app/assets/javascripts/sidebar/components/severity/graphql/mutations/update_issuable_severity.mutation.graphql1
-rw-r--r--app/assets/javascripts/sidebar/components/severity/sidebar_severity.vue14
-rw-r--r--app/assets/javascripts/sidebar/components/sidebar_dropdown_widget.vue71
-rw-r--r--app/assets/javascripts/sidebar/components/subscriptions/sidebar_subscriptions_widget.vue2
-rw-r--r--app/assets/javascripts/sidebar/components/time_tracking/time_tracker.vue2
-rw-r--r--app/assets/javascripts/sidebar/constants.js35
-rw-r--r--app/assets/javascripts/sidebar/graphql.js2
-rw-r--r--app/assets/javascripts/sidebar/mount_milestone_sidebar.js2
-rw-r--r--app/assets/javascripts/sidebar/mount_sidebar.js70
-rw-r--r--app/assets/javascripts/sidebar/queries/epic_confidential.query.graphql1
-rw-r--r--app/assets/javascripts/sidebar/queries/epic_due_date.query.graphql1
-rw-r--r--app/assets/javascripts/sidebar/queries/epic_participants.query.graphql1
-rw-r--r--app/assets/javascripts/sidebar/queries/epic_reference.query.graphql1
-rw-r--r--app/assets/javascripts/sidebar/queries/epic_start_date.query.graphql1
-rw-r--r--app/assets/javascripts/sidebar/queries/epic_subscribed.query.graphql1
-rw-r--r--app/assets/javascripts/sidebar/queries/epic_todo.query.graphql1
-rw-r--r--app/assets/javascripts/sidebar/queries/issuable_assignees.subscription.graphql1
-rw-r--r--app/assets/javascripts/sidebar/queries/issue_confidential.query.graphql1
-rw-r--r--app/assets/javascripts/sidebar/queries/issue_due_date.query.graphql1
-rw-r--r--app/assets/javascripts/sidebar/queries/issue_reference.query.graphql1
-rw-r--r--app/assets/javascripts/sidebar/queries/issue_subscribed.query.graphql1
-rw-r--r--app/assets/javascripts/sidebar/queries/issue_time_tracking.query.graphql1
-rw-r--r--app/assets/javascripts/sidebar/queries/issue_todo.query.graphql1
-rw-r--r--app/assets/javascripts/sidebar/queries/merge_request_milestone.query.graphql1
-rw-r--r--app/assets/javascripts/sidebar/queries/merge_request_reference.query.graphql1
-rw-r--r--app/assets/javascripts/sidebar/queries/merge_request_subscribed.query.graphql1
-rw-r--r--app/assets/javascripts/sidebar/queries/merge_request_time_tracking.query.graphql1
-rw-r--r--app/assets/javascripts/sidebar/queries/merge_request_todo.query.graphql1
-rw-r--r--app/assets/javascripts/sidebar/queries/project_issue_milestone.query.graphql1
-rw-r--r--app/assets/javascripts/sidebar/queries/project_milestones.query.graphql1
-rw-r--r--app/assets/javascripts/sidebar/queries/sidebarDetails.query.graphql2
-rw-r--r--app/assets/javascripts/sidebar/queries/sidebarDetailsMR.query.graphql2
-rw-r--r--app/assets/javascripts/sidebar/queries/update_epic_title.mutation.graphql1
-rw-r--r--app/assets/javascripts/sidebar/queries/update_merge_request_labels.mutation.graphql4
-rw-r--r--app/assets/javascripts/sidebar/sidebar_mediator.js14
-rw-r--r--app/assets/javascripts/snippets/components/edit.vue2
-rw-r--r--app/assets/javascripts/snippets/components/snippet_blob_view.vue4
-rw-r--r--app/assets/javascripts/snippets/components/snippet_header.vue2
-rw-r--r--app/assets/javascripts/snippets/fragments/project.fragment.graphql6
-rw-r--r--app/assets/javascripts/snippets/mutations/createSnippet.mutation.graphql1
-rw-r--r--app/assets/javascripts/snippets/mutations/updateSnippet.mutation.graphql1
-rw-r--r--app/assets/javascripts/static_site_editor/graphql/queries/source_content.query.graphql1
-rw-r--r--app/assets/javascripts/tabs/constants.js20
-rw-r--r--app/assets/javascripts/tabs/index.js239
-rw-r--r--app/assets/javascripts/terraform/graphql/fragments/state_version.fragment.graphql6
-rw-r--r--app/assets/javascripts/terraform/graphql/queries/get_states.query.graphql3
-rw-r--r--app/assets/javascripts/test_utils/simulate_drag.js42
-rw-r--r--app/assets/javascripts/token_access/graphql/queries/get_ci_job_token_scope.query.graphql1
-rw-r--r--app/assets/javascripts/token_access/graphql/queries/get_projects_with_ci_job_token_scope.query.graphql2
-rw-r--r--app/assets/javascripts/ui_development_kit.js28
-rw-r--r--app/assets/javascripts/user_lists/components/add_user_modal.vue2
-rw-r--r--app/assets/javascripts/user_lists/components/user_list.vue2
-rw-r--r--app/assets/javascripts/vue_alerts.js28
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/deployment/deployment_info.vue2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/extensions/base.vue171
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/extensions/utils.js62
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.vue2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/mr_widget_related_links.vue8
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/commit_edit.vue1
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/merge_checks_failed.vue2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_archived.vue2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_conflicts.vue7
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_rebase.vue3
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue87
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/work_in_progress.vue9
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/terraform/terraform_plan.vue4
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/constants.js15
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/extensions/issues.js18
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/extensions/issues.query.graphql1
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/extensions/issues_collapsed.query.graphql1
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/mixins/ready_to_merge.js4
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/queries/get_state.query.graphql2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/queries/permissions.query.graphql2
-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/queries/states/auto_merge_failed.query.graphql2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/queries/states/conflicts.query.graphql2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/queries/states/draft.query.graphql2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/queries/states/missing_branch.query.graphql2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/queries/states/new_ready_to_merge.query.graphql2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/queries/states/ready_to_merge.fragment.graphql3
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/queries/states/rebase.query.graphql2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/queries/toggle_draft.mutation.graphql1
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js1
-rw-r--r--app/assets/javascripts/vue_shared/alert_details/components/sidebar/sidebar_assignees.vue9
-rw-r--r--app/assets/javascripts/vue_shared/alert_details/components/sidebar/sidebar_status.vue3
-rw-r--r--app/assets/javascripts/vue_shared/alert_details/graphql/mutations/alert_issue_create.mutation.graphql1
-rw-r--r--app/assets/javascripts/vue_shared/alert_details/graphql/queries/alert_sidebar_details.query.graphql1
-rw-r--r--app/assets/javascripts/vue_shared/components/chronic_duration_input.vue133
-rw-r--r--app/assets/javascripts/vue_shared/components/clipboard_button.vue6
-rw-r--r--app/assets/javascripts/vue_shared/components/commit.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/confirm_danger/confirm_danger.vue7
-rw-r--r--app/assets/javascripts/vue_shared/components/confirm_danger/confirm_danger_modal.vue4
-rw-r--r--app/assets/javascripts/vue_shared/components/confirm_modal.vue72
-rw-r--r--app/assets/javascripts/vue_shared/components/confirm_modal_eventhub.js5
-rw-r--r--app/assets/javascripts/vue_shared/components/date_time_picker/date_time_picker.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/design_management/design_note_pin.vue (renamed from app/assets/javascripts/design_management/components/design_note_pin.vue)22
-rw-r--r--app/assets/javascripts/vue_shared/components/dismissible_alert.vue1
-rw-r--r--app/assets/javascripts/vue_shared/components/dom_element_listener.vue28
-rw-r--r--app/assets/javascripts/vue_shared/components/file_icon/file_icon_map.js113
-rw-r--r--app/assets/javascripts/vue_shared/components/file_row.vue1
-rw-r--r--app/assets/javascripts/vue_shared/components/filtered_search_bar/constants.js15
-rw-r--r--app/assets/javascripts/vue_shared/components/filtered_search_bar/queries/epic.fragment.graphql15
-rw-r--r--app/assets/javascripts/vue_shared/components/filtered_search_bar/queries/search_epics.query.graphql16
-rw-r--r--app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/author_token.vue1
-rw-r--r--app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/base_token.vue42
-rw-r--r--app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/epic_token.vue129
-rw-r--r--app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/iteration_token.vue138
-rw-r--r--app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/label_token.vue1
-rw-r--r--app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/milestone_token.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/weight_token.vue66
-rw-r--r--app/assets/javascripts/vue_shared/components/form/input_copy_toggle_visibility.stories.js27
-rw-r--r--app/assets/javascripts/vue_shared/components/form/input_copy_toggle_visibility.vue127
-rw-r--r--app/assets/javascripts/vue_shared/components/header_ci_component.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/issuable/init_issuable_header_warning.js22
-rw-r--r--app/assets/javascripts/vue_shared/components/line_numbers.vue57
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/suggestions.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/toolbar.vue18
-rw-r--r--app/assets/javascripts/vue_shared/components/namespace_select/namespace_select.vue93
-rw-r--r--app/assets/javascripts/vue_shared/components/notes/placeholder_note.vue10
-rw-r--r--app/assets/javascripts/vue_shared/components/notes/system_note.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/pagination_bar/pagination_bar.stories.js40
-rw-r--r--app/assets/javascripts/vue_shared/components/pagination_bar/pagination_bar.vue (renamed from app/assets/javascripts/import_entities/components/pagination_bar.vue)29
-rw-r--r--app/assets/javascripts/vue_shared/components/registry/list_item.vue6
-rw-r--r--app/assets/javascripts/vue_shared/components/registry/metadata_item.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/collapsed_grouped_date_picker.vue101
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/labels_select_root.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents.vue12
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_header.vue14
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_value.vue45
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_value_collapsed.vue55
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/graphql/epic_labels.query.graphql1
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/graphql/epic_update_labels.mutation.graphql4
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/graphql/issue_labels.query.graphql1
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/graphql/merge_request_labels.query.graphql1
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/labels_select_root.vue21
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/queries/get_alert_assignees.query.graphql1
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/queries/get_issue_assignees.query.graphql1
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/queries/get_issue_participants.query.graphql1
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/queries/get_mr_assignees.query.graphql1
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/queries/get_mr_participants.query.graphql1
-rw-r--r--app/assets/javascripts/vue_shared/components/source_editor.vue1
-rw-r--r--app/assets/javascripts/vue_shared/components/source_viewer.vue88
-rw-r--r--app/assets/javascripts/vue_shared/components/storage_counter/usage_graph.stories.js38
-rw-r--r--app/assets/javascripts/vue_shared/components/storage_counter/usage_graph.vue148
-rw-r--r--app/assets/javascripts/vue_shared/components/tooltip_on_truncate.vue69
-rw-r--r--app/assets/javascripts/vue_shared/components/tooltip_on_truncate/tooltip_on_truncate.stories.js88
-rw-r--r--app/assets/javascripts/vue_shared/components/tooltip_on_truncate/tooltip_on_truncate.vue85
-rw-r--r--app/assets/javascripts/vue_shared/issuable/create/components/issuable_create_root.vue (renamed from app/assets/javascripts/issuable_create/components/issuable_create_root.vue)0
-rw-r--r--app/assets/javascripts/vue_shared/issuable/create/components/issuable_form.vue (renamed from app/assets/javascripts/issuable_create/components/issuable_form.vue)0
-rw-r--r--app/assets/javascripts/vue_shared/issuable/list/components/issuable_bulk_edit_sidebar.vue (renamed from app/assets/javascripts/issuable_list/components/issuable_bulk_edit_sidebar.vue)0
-rw-r--r--app/assets/javascripts/vue_shared/issuable/list/components/issuable_item.vue (renamed from app/assets/javascripts/issuable_list/components/issuable_item.vue)12
-rw-r--r--app/assets/javascripts/vue_shared/issuable/list/components/issuable_list_root.vue (renamed from app/assets/javascripts/issuable_list/components/issuable_list_root.vue)12
-rw-r--r--app/assets/javascripts/vue_shared/issuable/list/components/issuable_tabs.vue (renamed from app/assets/javascripts/issuable_list/components/issuable_tabs.vue)4
-rw-r--r--app/assets/javascripts/vue_shared/issuable/list/constants.js (renamed from app/assets/javascripts/issuable_list/constants.js)0
-rw-r--r--app/assets/javascripts/vue_shared/issuable/show/components/issuable_body.vue (renamed from app/assets/javascripts/issuable_show/components/issuable_body.vue)0
-rw-r--r--app/assets/javascripts/vue_shared/issuable/show/components/issuable_description.vue (renamed from app/assets/javascripts/issuable_show/components/issuable_description.vue)0
-rw-r--r--app/assets/javascripts/vue_shared/issuable/show/components/issuable_discussion.vue (renamed from app/assets/javascripts/issuable_show/components/issuable_discussion.vue)0
-rw-r--r--app/assets/javascripts/vue_shared/issuable/show/components/issuable_edit_form.vue (renamed from app/assets/javascripts/issuable_show/components/issuable_edit_form.vue)0
-rw-r--r--app/assets/javascripts/vue_shared/issuable/show/components/issuable_header.vue (renamed from app/assets/javascripts/issuable_show/components/issuable_header.vue)0
-rw-r--r--app/assets/javascripts/vue_shared/issuable/show/components/issuable_show_root.vue (renamed from app/assets/javascripts/issuable_show/components/issuable_show_root.vue)4
-rw-r--r--app/assets/javascripts/vue_shared/issuable/show/components/issuable_title.vue (renamed from app/assets/javascripts/issuable_show/components/issuable_title.vue)0
-rw-r--r--app/assets/javascripts/vue_shared/issuable/show/constants.js (renamed from app/assets/javascripts/issuable_show/constants.js)0
-rw-r--r--app/assets/javascripts/vue_shared/issuable/show/event_hub.js (renamed from app/assets/javascripts/pages/projects/labels/event_hub.js)0
-rw-r--r--app/assets/javascripts/vue_shared/issuable/sidebar/components/issuable_sidebar_root.vue (renamed from app/assets/javascripts/issuable_sidebar/components/issuable_sidebar_root.vue)0
-rw-r--r--app/assets/javascripts/vue_shared/issuable/sidebar/constants.js (renamed from app/assets/javascripts/issuable_sidebar/constants.js)0
-rw-r--r--app/assets/javascripts/vue_shared/mixins/issuable.js14
-rw-r--r--app/assets/javascripts/vue_shared/security_configuration/components/manage_via_mr.vue2
-rw-r--r--app/assets/javascripts/vue_shared/security_reports/graphql/fragments/job_artifacts.fragment.graphql2
-rw-r--r--app/assets/javascripts/vue_shared/security_reports/graphql/queries/security_report_merge_request_download_paths.query.graphql3
-rw-r--r--app/assets/javascripts/vue_shared/security_reports/graphql/queries/security_report_pipeline_download_paths.query.graphql2
-rw-r--r--app/assets/javascripts/work_items/components/item_title.vue71
-rw-r--r--app/assets/javascripts/work_items/graphql/create_work_item.mutation.graphql18
-rw-r--r--app/assets/javascripts/work_items/graphql/fragmentTypes.json2
-rw-r--r--app/assets/javascripts/work_items/graphql/provider.js20
-rw-r--r--app/assets/javascripts/work_items/graphql/resolvers.js58
-rw-r--r--app/assets/javascripts/work_items/graphql/typedefs.graphql52
-rw-r--r--app/assets/javascripts/work_items/graphql/update_work_item.mutation.graphql18
-rw-r--r--app/assets/javascripts/work_items/graphql/widget.fragment.graphql2
-rw-r--r--app/assets/javascripts/work_items/graphql/work_item.query.graphql2
-rw-r--r--app/assets/javascripts/work_items/pages/create_work_item.vue71
-rw-r--r--app/assets/javascripts/work_items/pages/work_item_root.vue38
-rw-r--r--app/assets/javascripts/work_items/router/routes.js7
865 files changed, 11324 insertions, 6557 deletions
diff --git a/app/assets/javascripts/access_tokens/components/token.vue b/app/assets/javascripts/access_tokens/components/token.vue
new file mode 100644
index 00000000000..3954e541fe0
--- /dev/null
+++ b/app/assets/javascripts/access_tokens/components/token.vue
@@ -0,0 +1,55 @@
+<script>
+import InputCopyToggleVisibility from '~/vue_shared/components/form/input_copy_toggle_visibility.vue';
+
+export default {
+ components: { InputCopyToggleVisibility },
+ props: {
+ token: {
+ type: String,
+ required: true,
+ },
+ inputId: {
+ type: String,
+ required: true,
+ },
+ inputLabel: {
+ type: String,
+ required: true,
+ },
+ copyButtonTitle: {
+ type: String,
+ required: true,
+ },
+ },
+ computed: {
+ formInputGroupProps() {
+ return { id: this.inputId };
+ },
+ },
+};
+</script>
+
+<template>
+ <div class="row">
+ <div class="col-lg-12">
+ <hr />
+ </div>
+ <div class="col-lg-4">
+ <h4 class="gl-mt-0"><slot name="title"></slot></h4>
+ <slot name="description"></slot>
+ </div>
+ <div class="col-lg-8">
+ <input-copy-toggle-visibility
+ :label="inputLabel"
+ :label-for="inputId"
+ :form-input-group-props="formInputGroupProps"
+ :value="token"
+ :copy-button-title="copyButtonTitle"
+ >
+ <template #description>
+ <slot name="input-description"></slot>
+ </template>
+ </input-copy-toggle-visibility>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/access_tokens/components/tokens_app.vue b/app/assets/javascripts/access_tokens/components/tokens_app.vue
new file mode 100644
index 00000000000..755991f64e0
--- /dev/null
+++ b/app/assets/javascripts/access_tokens/components/tokens_app.vue
@@ -0,0 +1,111 @@
+<script>
+import { GlSprintf, GlLink } from '@gitlab/ui';
+import { pickBy } from 'lodash';
+
+import { s__ } from '~/locale';
+
+import { FEED_TOKEN, INCOMING_EMAIL_TOKEN, STATIC_OBJECT_TOKEN } from '../constants';
+import Token from './token.vue';
+
+export default {
+ i18n: {
+ canNotAccessOtherData: s__('AccessTokens|It cannot be used to access any other data.'),
+ [FEED_TOKEN]: {
+ label: s__('AccessTokens|Feed token'),
+ copyButtonTitle: s__('AccessTokens|Copy feed token'),
+ description: s__(
+ 'AccessTokens|Your feed token authenticates you when your RSS reader loads a personalized RSS feed or when your calendar application loads a personalized calendar. It is visible in those feed URLs.',
+ ),
+ inputDescription: s__(
+ 'AccessTokens|Keep this token secret. Anyone who has it can read activity and issue RSS feeds or your calendar feed as if they were you. If that happens, %{linkStart}reset this token%{linkEnd}.',
+ ),
+ resetConfirmMessage: s__(
+ 'AccessTokens|Are you sure? Any RSS or calendar URLs currently in use will stop working.',
+ ),
+ },
+ [INCOMING_EMAIL_TOKEN]: {
+ label: s__('AccessTokens|Incoming email token'),
+ copyButtonTitle: s__('AccessTokens|Copy incoming email token'),
+ description: s__(
+ 'AccessTokens|Your incoming email token authenticates you when you create a new issue by email, and is included in your personal project-specific email addresses.',
+ ),
+ inputDescription: s__(
+ 'AccessTokens|Keep this token secret. Anyone who has it can create issues as if they were you. If that happens, %{linkStart}reset this token%{linkEnd}.',
+ ),
+ resetConfirmMessage: s__(
+ 'AccessTokens|Are you sure? Any issue email addresses currently in use will stop working.',
+ ),
+ },
+ [STATIC_OBJECT_TOKEN]: {
+ label: s__('AccessTokens|Static object token'),
+ copyButtonTitle: s__('AccessTokens|Copy static object token'),
+ description: s__(
+ 'AccessTokens|Your static object token authenticates you when repository static objects (such as archives or blobs) are served from an external storage.',
+ ),
+ inputDescription: s__(
+ 'AccessTokens|Keep this token secret. Anyone who has it can access repository static objects as if they were you. If that ever happens, %{linkStart}reset this token%{linkEnd}.',
+ ),
+ resetConfirmMessage: s__('AccessTokens|Are you sure?'),
+ },
+ },
+ htmlAttributes: {
+ [FEED_TOKEN]: {
+ inputId: 'feed_token',
+ containerTestId: 'feed-token-container',
+ },
+ [INCOMING_EMAIL_TOKEN]: {
+ inputId: 'incoming_email_token',
+ containerTestId: 'incoming-email-token-container',
+ },
+ [STATIC_OBJECT_TOKEN]: {
+ inputId: 'static_object_token',
+ containerTestId: 'static-object-token-container',
+ },
+ },
+ components: { Token, GlSprintf, GlLink },
+ inject: ['tokenTypes'],
+ computed: {
+ enabledTokenTypes() {
+ return pickBy(this.tokenTypes, (tokenData, tokenType) => {
+ return (
+ tokenData?.enabled &&
+ this.$options.i18n[tokenType] &&
+ this.$options.htmlAttributes[tokenType]
+ );
+ });
+ },
+ },
+};
+</script>
+
+<template>
+ <div>
+ <token
+ v-for="(tokenData, tokenType) in enabledTokenTypes"
+ :key="tokenType"
+ :token="tokenData.token"
+ :input-id="$options.htmlAttributes[tokenType].inputId"
+ :input-label="$options.i18n[tokenType].label"
+ :copy-button-title="$options.i18n[tokenType].copyButtonTitle"
+ :data-testid="$options.htmlAttributes[tokenType].containerTestId"
+ >
+ <template #title>{{ $options.i18n[tokenType].label }}</template>
+ <template #description>
+ <p>{{ $options.i18n[tokenType].description }}</p>
+ <p>{{ $options.i18n.canNotAccessOtherData }}</p>
+ </template>
+ <template #input-description>
+ <gl-sprintf :message="$options.i18n[tokenType].inputDescription">
+ <template #link="{ content }">
+ <gl-link
+ :href="tokenData.resetPath"
+ :data-confirm="$options.i18n[tokenType].resetConfirmMessage"
+ data-method="put"
+ >{{ content }}</gl-link
+ >
+ </template>
+ </gl-sprintf>
+ </template>
+ </token>
+ </div>
+</template>
diff --git a/app/assets/javascripts/access_tokens/constants.js b/app/assets/javascripts/access_tokens/constants.js
new file mode 100644
index 00000000000..6188c6d1bb5
--- /dev/null
+++ b/app/assets/javascripts/access_tokens/constants.js
@@ -0,0 +1,4 @@
+// Token types
+export const FEED_TOKEN = 'feedToken';
+export const INCOMING_EMAIL_TOKEN = 'incomingEmailToken';
+export const STATIC_OBJECT_TOKEN = 'staticObjectToken';
diff --git a/app/assets/javascripts/access_tokens/index.js b/app/assets/javascripts/access_tokens/index.js
index 7f5f0403de6..9a1e7d877f8 100644
--- a/app/assets/javascripts/access_tokens/index.js
+++ b/app/assets/javascripts/access_tokens/index.js
@@ -1,9 +1,13 @@
import Vue from 'vue';
+
import createFlash from '~/flash';
+import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import { parseRailsFormFields } from '~/lib/utils/forms';
import { __ } from '~/locale';
import ExpiresAtField from './components/expires_at_field.vue';
+import TokensApp from './components/tokens_app.vue';
+import { FEED_TOKEN, INCOMING_EMAIL_TOKEN, STATIC_OBJECT_TOKEN } from './constants';
export const initExpiresAtField = () => {
const el = document.querySelector('.js-access-tokens-expires-at');
@@ -81,3 +85,29 @@ export const initProjectsField = () => {
return null;
};
+
+export const initTokensApp = () => {
+ const el = document.getElementById('js-tokens-app');
+
+ if (!el) return false;
+
+ const tokensData = convertObjectPropsToCamelCase(JSON.parse(el.dataset.tokensData), {
+ deep: true,
+ });
+
+ const tokenTypes = {
+ [FEED_TOKEN]: tokensData[FEED_TOKEN],
+ [INCOMING_EMAIL_TOKEN]: tokensData[INCOMING_EMAIL_TOKEN],
+ [STATIC_OBJECT_TOKEN]: tokensData[STATIC_OBJECT_TOKEN],
+ };
+
+ return new Vue({
+ el,
+ provide: {
+ tokenTypes,
+ },
+ render(createElement) {
+ return createElement(TokensApp);
+ },
+ });
+};
diff --git a/app/assets/javascripts/admin/deploy_keys/components/table.vue b/app/assets/javascripts/admin/deploy_keys/components/table.vue
index 97a5a2f2f32..29e8b9a724e 100644
--- a/app/assets/javascripts/admin/deploy_keys/components/table.vue
+++ b/app/assets/javascripts/admin/deploy_keys/components/table.vue
@@ -1,13 +1,33 @@
<script>
-import { GlTable, GlButton } from '@gitlab/ui';
+import { GlTable, GlButton, GlPagination, GlLoadingIcon, GlEmptyState, GlModal } from '@gitlab/ui';
import { __ } from '~/locale';
+import Api, { DEFAULT_PER_PAGE } from '~/api';
+import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
+import { cleanLeadingSeparator } from '~/lib/utils/url_utility';
+import createFlash from '~/flash';
+import csrf from '~/lib/utils/csrf';
export default {
name: 'DeployKeysTable',
i18n: {
pageTitle: __('Public deploy keys'),
newDeployKeyButtonText: __('New deploy key'),
+ emptyStateTitle: __('No public deploy keys'),
+ emptyStateDescription: __(
+ 'Deploy keys grant read/write access to all repositories in your instance',
+ ),
+ delete: __('Delete deploy key'),
+ edit: __('Edit deploy key'),
+ pagination: {
+ next: __('Next'),
+ prev: __('Prev'),
+ },
+ modal: {
+ title: __('Are you sure?'),
+ body: __('Are you sure you want to delete this deploy key?'),
+ },
+ apiErrorMessage: __('An error occurred fetching the public deploy keys. Please try again.'),
},
fields: [
{
@@ -29,13 +49,118 @@ export default {
{
key: 'actions',
label: __('Actions'),
+ tdClass: 'gl-lg-w-1px gl-white-space-nowrap',
+ thClass: 'gl-lg-w-1px gl-white-space-nowrap',
},
],
+ modal: {
+ id: 'delete-deploy-key-modal',
+ actionPrimary: {
+ text: __('Delete'),
+ attributes: {
+ variant: 'danger',
+ },
+ },
+ actionSecondary: {
+ text: __('Cancel'),
+ attributes: {
+ variant: 'default',
+ },
+ },
+ },
+ csrf,
+ DEFAULT_PER_PAGE,
components: {
GlTable,
GlButton,
+ GlPagination,
+ TimeAgoTooltip,
+ GlLoadingIcon,
+ GlEmptyState,
+ GlModal,
},
inject: ['editPath', 'deletePath', 'createPath', 'emptyStateSvgPath'],
+ data() {
+ return {
+ page: 1,
+ totalItems: 0,
+ loading: false,
+ items: [],
+ deployKeyToDelete: null,
+ };
+ },
+ computed: {
+ shouldShowTable() {
+ return this.totalItems !== 0 || this.loading;
+ },
+ isModalVisible() {
+ return this.deployKeyToDelete !== null;
+ },
+ deleteAction() {
+ return this.deployKeyToDelete === null
+ ? null
+ : this.deletePath.replace(':id', this.deployKeyToDelete);
+ },
+ },
+ watch: {
+ page(newPage) {
+ this.fetchDeployKeys(newPage);
+ },
+ },
+ mounted() {
+ this.fetchDeployKeys();
+ },
+ methods: {
+ editHref(id) {
+ return this.editPath.replace(':id', id);
+ },
+ projectHref(project) {
+ return `/${cleanLeadingSeparator(project.path_with_namespace)}`;
+ },
+ async fetchDeployKeys(page) {
+ this.loading = true;
+ try {
+ const { headers, data: items } = await Api.deployKeys({
+ page,
+ public: true,
+ });
+
+ if (this.totalItems === 0) {
+ this.totalItems = parseInt(headers?.['x-total'], 10) || 0;
+ }
+
+ this.items = items.map(
+ ({ id, title, fingerprint, projects_with_write_access, created_at }) => ({
+ id,
+ title,
+ fingerprint,
+ projects: projects_with_write_access,
+ created: created_at,
+ }),
+ );
+ } catch (error) {
+ createFlash({
+ message: this.$options.i18n.apiErrorMessage,
+ captureError: true,
+ error,
+ });
+
+ this.totalItems = 0;
+
+ this.items = [];
+ }
+ this.loading = false;
+ },
+ handleDeleteClick(id) {
+ this.deployKeyToDelete = id;
+ },
+ handleModalHide() {
+ this.deployKeyToDelete = null;
+ },
+ handleModalPrimary() {
+ this.$refs.modalForm.submit();
+ },
+ },
};
</script>
@@ -45,10 +170,92 @@ export default {
<h4 class="gl-m-0">
{{ $options.i18n.pageTitle }}
</h4>
- <gl-button variant="confirm" :href="createPath">{{
+ <gl-button variant="confirm" :href="createPath" data-testid="new-deploy-key-button">{{
$options.i18n.newDeployKeyButtonText
}}</gl-button>
</div>
- <gl-table :fields="$options.fields" data-testid="deploy-keys-list" />
+ <template v-if="shouldShowTable">
+ <gl-table
+ :busy="loading"
+ :items="items"
+ :fields="$options.fields"
+ stacked="lg"
+ data-testid="deploy-keys-list"
+ >
+ <template #table-busy>
+ <gl-loading-icon size="lg" class="gl-my-5" />
+ </template>
+
+ <template #cell(projects)="{ item: { projects } }">
+ <a
+ v-for="project in projects"
+ :key="project.id"
+ :href="projectHref(project)"
+ class="gl-display-block"
+ >{{ project.name_with_namespace }}</a
+ >
+ </template>
+
+ <template #cell(fingerprint)="{ item: { fingerprint } }">
+ <code>{{ fingerprint }}</code>
+ </template>
+
+ <template #cell(created)="{ item: { created } }">
+ <time-ago-tooltip :time="created" />
+ </template>
+
+ <template #head(actions)="{ label }">
+ <span class="gl-sr-only">{{ label }}</span>
+ </template>
+
+ <template #cell(actions)="{ item: { id } }">
+ <gl-button
+ icon="pencil"
+ :aria-label="$options.i18n.edit"
+ :href="editHref(id)"
+ class="gl-mr-2"
+ />
+ <gl-button
+ variant="danger"
+ icon="remove"
+ :aria-label="$options.i18n.delete"
+ @click="handleDeleteClick(id)"
+ />
+ </template>
+ </gl-table>
+ <gl-pagination
+ v-if="!loading"
+ v-model="page"
+ :per-page="$options.DEFAULT_PER_PAGE"
+ :total-items="totalItems"
+ :next-text="$options.i18n.pagination.next"
+ :prev-text="$options.i18n.pagination.prev"
+ align="center"
+ />
+ </template>
+ <gl-empty-state
+ v-else
+ :svg-path="emptyStateSvgPath"
+ :title="$options.i18n.emptyStateTitle"
+ :description="$options.i18n.emptyStateDescription"
+ :primary-button-text="$options.i18n.newDeployKeyButtonText"
+ :primary-button-link="createPath"
+ />
+ <gl-modal
+ :modal-id="$options.modal.id"
+ :visible="isModalVisible"
+ :title="$options.i18n.modal.title"
+ :action-primary="$options.modal.actionPrimary"
+ :action-secondary="$options.modal.actionSecondary"
+ size="sm"
+ @hide="handleModalHide"
+ @primary="handleModalPrimary"
+ >
+ <form ref="modalForm" :action="deleteAction" method="post">
+ <input type="hidden" name="_method" value="delete" />
+ <input type="hidden" name="authenticity_token" :value="$options.csrf.token" />
+ </form>
+ {{ $options.i18n.modal.body }}
+ </gl-modal>
</div>
</template>
diff --git a/app/assets/javascripts/admin/users/components/actions/activate.vue b/app/assets/javascripts/admin/users/components/actions/activate.vue
index 74e9c60a57b..3a54035c587 100644
--- a/app/assets/javascripts/admin/users/components/actions/activate.vue
+++ b/app/assets/javascripts/admin/users/components/actions/activate.vue
@@ -1,6 +1,7 @@
<script>
import { GlDropdownItem } from '@gitlab/ui';
import { sprintf, s__, __ } from '~/locale';
+import eventHub, { EVENT_OPEN_CONFIRM_MODAL } from '~/vue_shared/components/confirm_modal_eventhub';
import { I18N_USER_ACTIONS } from '../../constants';
// TODO: To be replaced with <template> content in https://gitlab.com/gitlab-org/gitlab/-/issues/320922
@@ -26,16 +27,15 @@ export default {
required: true,
},
},
- computed: {
- modalAttributes() {
- return {
- 'data-path': this.path,
- 'data-method': 'put',
- 'data-modal-attributes': JSON.stringify({
+ methods: {
+ onClick() {
+ eventHub.$emit(EVENT_OPEN_CONFIRM_MODAL, {
+ path: this.path,
+ method: 'put',
+ modalAttributes: {
title: sprintf(s__('AdminUsers|Activate user %{username}?'), {
username: this.username,
}),
- messageHtml,
actionCancel: {
text: __('Cancel'),
},
@@ -43,15 +43,16 @@ export default {
text: I18N_USER_ACTIONS.activate,
attributes: [{ variant: 'confirm' }],
},
- }),
- };
+ messageHtml,
+ },
+ });
},
},
};
</script>
<template>
- <gl-dropdown-item button-class="js-confirm-modal-button" v-bind="{ ...modalAttributes }">
+ <gl-dropdown-item @click="onClick">
<slot></slot>
</gl-dropdown-item>
</template>
diff --git a/app/assets/javascripts/admin/users/components/actions/approve.vue b/app/assets/javascripts/admin/users/components/actions/approve.vue
index 77a9be8eec2..5a8c675822d 100644
--- a/app/assets/javascripts/admin/users/components/actions/approve.vue
+++ b/app/assets/javascripts/admin/users/components/actions/approve.vue
@@ -1,6 +1,7 @@
<script>
import { GlDropdownItem } from '@gitlab/ui';
import { sprintf, s__, __ } from '~/locale';
+import eventHub, { EVENT_OPEN_CONFIRM_MODAL } from '~/vue_shared/components/confirm_modal_eventhub';
import { I18N_USER_ACTIONS } from '../../constants';
// TODO: To be replaced with <template> content in https://gitlab.com/gitlab-org/gitlab/-/issues/320922
@@ -28,12 +29,12 @@ export default {
required: true,
},
},
- computed: {
- attributes() {
- return {
- 'data-path': this.path,
- 'data-method': 'put',
- 'data-modal-attributes': JSON.stringify({
+ methods: {
+ onClick() {
+ eventHub.$emit(EVENT_OPEN_CONFIRM_MODAL, {
+ path: this.path,
+ method: 'put',
+ modalAttributes: {
title: sprintf(s__('AdminUsers|Approve user %{username}?'), {
username: this.username,
}),
@@ -45,16 +46,15 @@ export default {
attributes: [{ variant: 'confirm', 'data-qa-selector': 'approve_user_confirm_button' }],
},
messageHtml,
- }),
- 'data-qa-selector': 'approve_user_button',
- };
+ },
+ });
},
},
};
</script>
<template>
- <gl-dropdown-item button-class="js-confirm-modal-button" v-bind="{ ...attributes }">
+ <gl-dropdown-item data-qa-selector="approve_user_button" @click="onClick">
<slot></slot>
</gl-dropdown-item>
</template>
diff --git a/app/assets/javascripts/admin/users/components/actions/ban.vue b/app/assets/javascripts/admin/users/components/actions/ban.vue
index e5ab0f9123f..55938832dce 100644
--- a/app/assets/javascripts/admin/users/components/actions/ban.vue
+++ b/app/assets/javascripts/admin/users/components/actions/ban.vue
@@ -2,6 +2,7 @@
import { GlDropdownItem } from '@gitlab/ui';
import { helpPagePath } from '~/helpers/help_page_helper';
import { sprintf, s__, __ } from '~/locale';
+import eventHub, { EVENT_OPEN_CONFIRM_MODAL } from '~/vue_shared/components/confirm_modal_eventhub';
import { I18N_USER_ACTIONS } from '../../constants';
// TODO: To be replaced with <template> content in https://gitlab.com/gitlab-org/gitlab/-/issues/320922
@@ -39,12 +40,12 @@ export default {
required: true,
},
},
- computed: {
- modalAttributes() {
- return {
- 'data-path': this.path,
- 'data-method': 'put',
- 'data-modal-attributes': JSON.stringify({
+ methods: {
+ onClick() {
+ eventHub.$emit(EVENT_OPEN_CONFIRM_MODAL, {
+ path: this.path,
+ method: 'put',
+ modalAttributes: {
title: sprintf(s__('AdminUsers|Ban user %{username}?'), {
username: this.username,
}),
@@ -56,15 +57,15 @@ export default {
attributes: [{ variant: 'confirm' }],
},
messageHtml,
- }),
- };
+ },
+ });
},
},
};
</script>
<template>
- <gl-dropdown-item button-class="js-confirm-modal-button" v-bind="{ ...modalAttributes }">
+ <gl-dropdown-item @click="onClick">
<slot></slot>
</gl-dropdown-item>
</template>
diff --git a/app/assets/javascripts/admin/users/components/actions/block.vue b/app/assets/javascripts/admin/users/components/actions/block.vue
index 03557008a89..d25dd400f9b 100644
--- a/app/assets/javascripts/admin/users/components/actions/block.vue
+++ b/app/assets/javascripts/admin/users/components/actions/block.vue
@@ -1,6 +1,7 @@
<script>
import { GlDropdownItem } from '@gitlab/ui';
import { sprintf, s__, __ } from '~/locale';
+import eventHub, { EVENT_OPEN_CONFIRM_MODAL } from '~/vue_shared/components/confirm_modal_eventhub';
import { I18N_USER_ACTIONS } from '../../constants';
// TODO: To be replaced with <template> content in https://gitlab.com/gitlab-org/gitlab/-/issues/320922
@@ -29,12 +30,12 @@ export default {
required: true,
},
},
- computed: {
- modalAttributes() {
- return {
- 'data-path': this.path,
- 'data-method': 'put',
- 'data-modal-attributes': JSON.stringify({
+ methods: {
+ onClick() {
+ eventHub.$emit(EVENT_OPEN_CONFIRM_MODAL, {
+ path: this.path,
+ method: 'put',
+ modalAttributes: {
title: sprintf(s__('AdminUsers|Block user %{username}?'), { username: this.username }),
actionCancel: {
text: __('Cancel'),
@@ -44,15 +45,15 @@ export default {
attributes: [{ variant: 'confirm' }],
},
messageHtml,
- }),
- };
+ },
+ });
},
},
};
</script>
<template>
- <gl-dropdown-item button-class="js-confirm-modal-button" v-bind="{ ...modalAttributes }">
+ <gl-dropdown-item @click="onClick">
<slot></slot>
</gl-dropdown-item>
</template>
diff --git a/app/assets/javascripts/admin/users/components/actions/deactivate.vue b/app/assets/javascripts/admin/users/components/actions/deactivate.vue
index 640c8fefc20..c85f3f01675 100644
--- a/app/assets/javascripts/admin/users/components/actions/deactivate.vue
+++ b/app/assets/javascripts/admin/users/components/actions/deactivate.vue
@@ -1,6 +1,7 @@
<script>
import { GlDropdownItem } from '@gitlab/ui';
import { sprintf, s__, __ } from '~/locale';
+import eventHub, { EVENT_OPEN_CONFIRM_MODAL } from '~/vue_shared/components/confirm_modal_eventhub';
import { I18N_USER_ACTIONS } from '../../constants';
// TODO: To be replaced with <template> content in https://gitlab.com/gitlab-org/gitlab/-/issues/320922
@@ -36,12 +37,12 @@ export default {
required: true,
},
},
- computed: {
- modalAttributes() {
- return {
- 'data-path': this.path,
- 'data-method': 'put',
- 'data-modal-attributes': JSON.stringify({
+ methods: {
+ onClick() {
+ eventHub.$emit(EVENT_OPEN_CONFIRM_MODAL, {
+ path: this.path,
+ method: 'put',
+ modalAttributes: {
title: sprintf(s__('AdminUsers|Deactivate user %{username}?'), {
username: this.username,
}),
@@ -53,15 +54,15 @@ export default {
attributes: [{ variant: 'confirm' }],
},
messageHtml,
- }),
- };
+ },
+ });
},
},
};
</script>
<template>
- <gl-dropdown-item button-class="js-confirm-modal-button" v-bind="{ ...modalAttributes }">
+ <gl-dropdown-item @click="onClick">
<slot></slot>
</gl-dropdown-item>
</template>
diff --git a/app/assets/javascripts/admin/users/components/actions/reject.vue b/app/assets/javascripts/admin/users/components/actions/reject.vue
index 901306455fa..bac08de1d5e 100644
--- a/app/assets/javascripts/admin/users/components/actions/reject.vue
+++ b/app/assets/javascripts/admin/users/components/actions/reject.vue
@@ -2,6 +2,7 @@
import { GlDropdownItem } from '@gitlab/ui';
import { helpPagePath } from '~/helpers/help_page_helper';
import { sprintf, s__, __ } from '~/locale';
+import eventHub, { EVENT_OPEN_CONFIRM_MODAL } from '~/vue_shared/components/confirm_modal_eventhub';
import { I18N_USER_ACTIONS } from '../../constants';
// TODO: To be replaced with <template> content in https://gitlab.com/gitlab-org/gitlab/-/issues/320922
@@ -39,12 +40,12 @@ export default {
required: true,
},
},
- computed: {
- modalAttributes() {
- return {
- 'data-path': this.path,
- 'data-method': 'delete',
- 'data-modal-attributes': JSON.stringify({
+ methods: {
+ onClick() {
+ eventHub.$emit(EVENT_OPEN_CONFIRM_MODAL, {
+ path: this.path,
+ method: 'delete',
+ modalAttributes: {
title: sprintf(s__('AdminUsers|Reject user %{username}?'), {
username: this.username,
}),
@@ -56,15 +57,15 @@ export default {
attributes: [{ variant: 'danger' }],
},
messageHtml,
- }),
- };
+ },
+ });
},
},
};
</script>
<template>
- <gl-dropdown-item button-class="js-confirm-modal-button" v-bind="{ ...modalAttributes }">
+ <gl-dropdown-item @click="onClick">
<slot></slot>
</gl-dropdown-item>
</template>
diff --git a/app/assets/javascripts/admin/users/components/actions/unban.vue b/app/assets/javascripts/admin/users/components/actions/unban.vue
index 8083e26177e..beede2d37d7 100644
--- a/app/assets/javascripts/admin/users/components/actions/unban.vue
+++ b/app/assets/javascripts/admin/users/components/actions/unban.vue
@@ -1,6 +1,7 @@
<script>
import { GlDropdownItem } from '@gitlab/ui';
import { sprintf, s__, __ } from '~/locale';
+import eventHub, { EVENT_OPEN_CONFIRM_MODAL } from '~/vue_shared/components/confirm_modal_eventhub';
import { I18N_USER_ACTIONS } from '../../constants';
// TODO: To be replaced with <template> content in https://gitlab.com/gitlab-org/gitlab/-/issues/320922
@@ -22,12 +23,12 @@ export default {
required: true,
},
},
- computed: {
- modalAttributes() {
- return {
- 'data-path': this.path,
- 'data-method': 'put',
- 'data-modal-attributes': JSON.stringify({
+ methods: {
+ onClick() {
+ eventHub.$emit(EVENT_OPEN_CONFIRM_MODAL, {
+ path: this.path,
+ method: 'put',
+ modalAttributes: {
title: sprintf(s__('AdminUsers|Unban user %{username}?'), {
username: this.username,
}),
@@ -39,15 +40,15 @@ export default {
attributes: [{ variant: 'confirm' }],
},
messageHtml,
- }),
- };
+ },
+ });
},
},
};
</script>
<template>
- <gl-dropdown-item button-class="js-confirm-modal-button" v-bind="{ ...modalAttributes }">
+ <gl-dropdown-item @click="onClick">
<slot></slot>
</gl-dropdown-item>
</template>
diff --git a/app/assets/javascripts/admin/users/components/actions/unblock.vue b/app/assets/javascripts/admin/users/components/actions/unblock.vue
index 7de6653e0cd..720f2efd932 100644
--- a/app/assets/javascripts/admin/users/components/actions/unblock.vue
+++ b/app/assets/javascripts/admin/users/components/actions/unblock.vue
@@ -1,6 +1,7 @@
<script>
import { GlDropdownItem } from '@gitlab/ui';
import { sprintf, s__, __ } from '~/locale';
+import eventHub, { EVENT_OPEN_CONFIRM_MODAL } from '~/vue_shared/components/confirm_modal_eventhub';
import { I18N_USER_ACTIONS } from '../../constants';
export default {
@@ -17,12 +18,13 @@ export default {
required: true,
},
},
- computed: {
- modalAttributes() {
- return {
- 'data-path': this.path,
- 'data-method': 'put',
- 'data-modal-attributes': JSON.stringify({
+
+ methods: {
+ onClick() {
+ eventHub.$emit(EVENT_OPEN_CONFIRM_MODAL, {
+ path: this.path,
+ method: 'put',
+ modalAttributes: {
title: sprintf(s__('AdminUsers|Unblock user %{username}?'), { username: this.username }),
message: s__('AdminUsers|You can always block their account again if needed.'),
actionCancel: {
@@ -32,15 +34,15 @@ export default {
text: I18N_USER_ACTIONS.unblock,
attributes: [{ variant: 'confirm' }],
},
- }),
- };
+ },
+ });
},
},
};
</script>
<template>
- <gl-dropdown-item button-class="js-confirm-modal-button" v-bind="{ ...modalAttributes }">
+ <gl-dropdown-item @click="onClick">
<slot></slot>
</gl-dropdown-item>
</template>
diff --git a/app/assets/javascripts/admin/users/components/actions/unlock.vue b/app/assets/javascripts/admin/users/components/actions/unlock.vue
index 10d4fb06d61..55ea3e0aba7 100644
--- a/app/assets/javascripts/admin/users/components/actions/unlock.vue
+++ b/app/assets/javascripts/admin/users/components/actions/unlock.vue
@@ -1,6 +1,7 @@
<script>
import { GlDropdownItem } from '@gitlab/ui';
import { sprintf, s__, __ } from '~/locale';
+import eventHub, { EVENT_OPEN_CONFIRM_MODAL } from '~/vue_shared/components/confirm_modal_eventhub';
import { I18N_USER_ACTIONS } from '../../constants';
export default {
@@ -17,12 +18,12 @@ export default {
required: true,
},
},
- computed: {
- modalAttributes() {
- return {
- 'data-path': this.path,
- 'data-method': 'put',
- 'data-modal-attributes': JSON.stringify({
+ methods: {
+ onClick() {
+ eventHub.$emit(EVENT_OPEN_CONFIRM_MODAL, {
+ path: this.path,
+ method: 'put',
+ modalAttributes: {
title: sprintf(s__('AdminUsers|Unlock user %{username}?'), { username: this.username }),
message: __('Are you sure?'),
actionCancel: {
@@ -32,15 +33,15 @@ export default {
text: I18N_USER_ACTIONS.unlock,
attributes: [{ variant: 'confirm' }],
},
- }),
- };
+ },
+ });
},
},
};
</script>
<template>
- <gl-dropdown-item button-class="js-confirm-modal-button" v-bind="{ ...modalAttributes }">
+ <gl-dropdown-item @click="onClick">
<slot></slot>
</gl-dropdown-item>
</template>
diff --git a/app/assets/javascripts/admin/users/components/modals/delete_user_modal.vue b/app/assets/javascripts/admin/users/components/modals/delete_user_modal.vue
index e949498c55b..d7c08096376 100644
--- a/app/assets/javascripts/admin/users/components/modals/delete_user_modal.vue
+++ b/app/assets/javascripts/admin/users/components/modals/delete_user_modal.vue
@@ -57,14 +57,17 @@ export default {
};
},
computed: {
+ trimmedUsername() {
+ return this.username.trim();
+ },
modalTitle() {
- return sprintf(this.title, { username: this.username }, false);
+ return sprintf(this.title, { username: this.trimmedUsername }, false);
},
secondaryButtonLabel() {
return s__('AdminUsers|Block user');
},
canSubmit() {
- return this.enteredUsername === this.username;
+ return this.enteredUsername === this.trimmedUsername;
},
obstacles() {
try {
@@ -104,7 +107,7 @@ export default {
<p>
<gl-sprintf :message="content">
<template #username>
- <strong>{{ username }}</strong>
+ <strong>{{ trimmedUsername }}</strong>
</template>
<template #strong="props">
<strong>{{ props.content }}</strong>
@@ -115,13 +118,13 @@ export default {
<user-deletion-obstacles-list
v-if="obstacles.length"
:obstacles="obstacles"
- :user-name="username"
+ :user-name="trimmedUsername"
/>
<p>
<gl-sprintf :message="s__('AdminUsers|To confirm, type %{username}')">
<template #username>
- <code class="gl-white-space-pre-wrap">{{ username }}</code>
+ <code class="gl-white-space-pre-wrap">{{ trimmedUsername }}</code>
</template>
</gl-sprintf>
</p>
diff --git a/app/assets/javascripts/admin/users/components/user_actions.vue b/app/assets/javascripts/admin/users/components/user_actions.vue
index 4f4e2947341..567d7151847 100644
--- a/app/assets/javascripts/admin/users/components/user_actions.vue
+++ b/app/assets/javascripts/admin/users/components/user_actions.vue
@@ -112,7 +112,7 @@ export default {
right
:text="$options.i18n.userAdministration"
:text-sr-only="!showButtonLabels"
- icon="settings"
+ icon="ellipsis_h"
data-qa-selector="user_actions_dropdown_toggle"
:data-qa-username="user.username"
>
diff --git a/app/assets/javascripts/alert_management/graphql/queries/get_count_by_status.query.graphql b/app/assets/javascripts/alert_management/graphql/queries/get_count_by_status.query.graphql
index 40ec4c56171..0f9075c58bf 100644
--- a/app/assets/javascripts/alert_management/graphql/queries/get_count_by_status.query.graphql
+++ b/app/assets/javascripts/alert_management/graphql/queries/get_count_by_status.query.graphql
@@ -1,5 +1,6 @@
query getAlertsCount($searchTerm: String, $projectPath: ID!, $assigneeUsername: String = "") {
project(fullPath: $projectPath) {
+ id
alertManagementAlertStatusCounts(search: $searchTerm, assigneeUsername: $assigneeUsername) {
all
open
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 babcdea935d..d4f4f244759 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
@@ -3,6 +3,8 @@
mutation createHttpIntegration($projectPath: ID!, $name: String!, $active: Boolean!) {
httpIntegrationCreate(input: { projectPath: $projectPath, name: $name, active: $active }) {
errors
+ # We have ID in a deeply nested fragment
+ # eslint-disable-next-line @graphql-eslint/require-id-when-available
integration {
...HttpIntegrationItem
}
diff --git a/app/assets/javascripts/alerts_settings/graphql/mutations/destroy_http_integration.mutation.graphql b/app/assets/javascripts/alerts_settings/graphql/mutations/destroy_http_integration.mutation.graphql
index a3a50651fd0..caa258e0848 100644
--- a/app/assets/javascripts/alerts_settings/graphql/mutations/destroy_http_integration.mutation.graphql
+++ b/app/assets/javascripts/alerts_settings/graphql/mutations/destroy_http_integration.mutation.graphql
@@ -3,6 +3,8 @@
mutation destroyHttpIntegration($id: ID!) {
httpIntegrationDestroy(input: { id: $id }) {
errors
+ # We have ID in a deeply nested fragment
+ # eslint-disable-next-line @graphql-eslint/require-id-when-available
integration {
...HttpIntegrationItem
}
diff --git a/app/assets/javascripts/alerts_settings/graphql/mutations/reset_http_token.mutation.graphql b/app/assets/javascripts/alerts_settings/graphql/mutations/reset_http_token.mutation.graphql
index c0754d8e32b..2f30f9abb5c 100644
--- a/app/assets/javascripts/alerts_settings/graphql/mutations/reset_http_token.mutation.graphql
+++ b/app/assets/javascripts/alerts_settings/graphql/mutations/reset_http_token.mutation.graphql
@@ -3,6 +3,8 @@
mutation resetHttpIntegrationToken($id: ID!) {
httpIntegrationResetToken(input: { id: $id }) {
errors
+ # We have ID in a deeply nested fragment
+ # eslint-disable-next-line @graphql-eslint/require-id-when-available
integration {
...HttpIntegrationItem
}
diff --git a/app/assets/javascripts/alerts_settings/graphql/mutations/update_http_integration.mutation.graphql b/app/assets/javascripts/alerts_settings/graphql/mutations/update_http_integration.mutation.graphql
index 37df9ec25eb..2cf56613673 100644
--- a/app/assets/javascripts/alerts_settings/graphql/mutations/update_http_integration.mutation.graphql
+++ b/app/assets/javascripts/alerts_settings/graphql/mutations/update_http_integration.mutation.graphql
@@ -3,6 +3,8 @@
mutation updateHttpIntegration($id: ID!, $name: String!, $active: Boolean!) {
httpIntegrationUpdate(input: { id: $id, name: $name, active: $active }) {
errors
+ # We have ID in a deeply nested fragment
+ # eslint-disable-next-line @graphql-eslint/require-id-when-available
integration {
...HttpIntegrationItem
}
diff --git a/app/assets/javascripts/alerts_settings/graphql/queries/get_http_integration.query.graphql b/app/assets/javascripts/alerts_settings/graphql/queries/get_http_integration.query.graphql
index d20a8b8334b..7299e6836d4 100644
--- a/app/assets/javascripts/alerts_settings/graphql/queries/get_http_integration.query.graphql
+++ b/app/assets/javascripts/alerts_settings/graphql/queries/get_http_integration.query.graphql
@@ -2,6 +2,7 @@
query getHttpIntegration($projectPath: ID!, $id: ID) {
project(fullPath: $projectPath) {
+ id
alertManagementHttpIntegrations(id: $id) {
nodes {
...HttpIntegrationPayloadData
diff --git a/app/assets/javascripts/alerts_settings/graphql/queries/get_integrations.query.graphql b/app/assets/javascripts/alerts_settings/graphql/queries/get_integrations.query.graphql
index 228dd5fb176..3cd3f2d92f8 100644
--- a/app/assets/javascripts/alerts_settings/graphql/queries/get_integrations.query.graphql
+++ b/app/assets/javascripts/alerts_settings/graphql/queries/get_integrations.query.graphql
@@ -2,6 +2,7 @@
query getIntegrations($projectPath: ID!) {
project(fullPath: $projectPath) {
+ id
alertManagementIntegrations {
nodes {
...IntegrationItem
diff --git a/app/assets/javascripts/alerts_settings/graphql/queries/parse_sample_payload.query.graphql b/app/assets/javascripts/alerts_settings/graphql/queries/parse_sample_payload.query.graphql
index 159b2661f0b..15df4a08cc2 100644
--- a/app/assets/javascripts/alerts_settings/graphql/queries/parse_sample_payload.query.graphql
+++ b/app/assets/javascripts/alerts_settings/graphql/queries/parse_sample_payload.query.graphql
@@ -1,5 +1,6 @@
query parsePayloadFields($projectPath: ID!, $payload: String!) {
project(fullPath: $projectPath) {
+ id
alertManagementPayloadFields(payloadExample: $payload) {
path
label
diff --git a/app/assets/javascripts/analytics/devops_reports/components/devops_score.vue b/app/assets/javascripts/analytics/devops_reports/components/devops_score.vue
index 238081cc3c0..5a394059931 100644
--- a/app/assets/javascripts/analytics/devops_reports/components/devops_score.vue
+++ b/app/assets/javascripts/analytics/devops_reports/components/devops_score.vue
@@ -1,5 +1,5 @@
<script>
-import { GlBadge, GlTable, GlLink, GlEmptyState } from '@gitlab/ui';
+import { GlBadge, GlTableLite, GlLink, GlEmptyState } from '@gitlab/ui';
import { GlSingleStat } from '@gitlab/ui/dist/charts';
import { helpPagePath } from '~/helpers/help_page_helper';
import { sprintf, s__ } from '~/locale';
@@ -13,7 +13,7 @@ const defaultHeaderAttrs = {
export default {
components: {
GlBadge,
- GlTable,
+ GlTableLite,
GlSingleStat,
GlLink,
GlEmptyState,
@@ -94,7 +94,7 @@ export default {
:meta-text="devopsScoreMetrics.averageScore.scoreLevel.label"
:variant="devopsScoreMetrics.averageScore.scoreLevel.variant"
/>
- <gl-table
+ <gl-table-lite
:fields="$options.tableHeaderFields"
:items="devopsScoreMetrics.cards"
thead-class="gl-border-t-0 gl-border-b-solid gl-border-b-1 gl-border-b-gray-100"
@@ -108,7 +108,7 @@ export default {
}}</gl-badge>
</div>
</template>
- </gl-table>
+ </gl-table-lite>
</div>
</div>
</template>
diff --git a/app/assets/javascripts/analytics/shared/graphql/projects.query.graphql b/app/assets/javascripts/analytics/shared/graphql/projects.query.graphql
index b870ed4dcbf..ea2f911fb54 100644
--- a/app/assets/javascripts/analytics/shared/graphql/projects.query.graphql
+++ b/app/assets/javascripts/analytics/shared/graphql/projects.query.graphql
@@ -5,6 +5,7 @@ query analyticsGetGroupProjects(
$includeSubgroups: Boolean = false
) {
group(fullPath: $groupFullPath) {
+ id
projects(
search: $search
first: $first
diff --git a/app/assets/javascripts/api.js b/app/assets/javascripts/api.js
index adf3e122a64..8c996b448aa 100644
--- a/app/assets/javascripts/api.js
+++ b/app/assets/javascripts/api.js
@@ -91,6 +91,7 @@ const Api = {
projectNotificationSettingsPath: '/api/:version/projects/:id/notification_settings',
groupNotificationSettingsPath: '/api/:version/groups/:id/notification_settings',
notificationSettingsPath: '/api/:version/notification_settings',
+ deployKeysPath: '/api/:version/deploy_keys',
group(groupId, callback = () => {}) {
const url = Api.buildUrl(Api.groupPath).replace(':id', groupId);
@@ -950,6 +951,12 @@ const Api = {
return axios.delete(url);
},
+ deployKeys(params = {}) {
+ const url = Api.buildUrl(this.deployKeysPath);
+
+ return axios.get(url, { params: { per_page: DEFAULT_PER_PAGE, ...params } });
+ },
+
async updateNotificationSettings(projectId, groupId, data = {}) {
let url = Api.buildUrl(this.notificationSettingsPath);
diff --git a/app/assets/javascripts/api/packages_api.js b/app/assets/javascripts/api/packages_api.js
new file mode 100644
index 00000000000..47f51c7e80e
--- /dev/null
+++ b/app/assets/javascripts/api/packages_api.js
@@ -0,0 +1,32 @@
+import axios from '../lib/utils/axios_utils';
+import { buildApiUrl } from './api_utils';
+
+const PUBLISH_PACKAGE_PATH =
+ '/api/:version/projects/:id/packages/generic/:package_name/:package_version/:file_name';
+
+export function publishPackage(
+ { projectPath, name, version, fileName, files },
+ options,
+ axiosOptions = {},
+) {
+ const url = buildApiUrl(PUBLISH_PACKAGE_PATH)
+ .replace(':id', encodeURIComponent(projectPath))
+ .replace(':package_name', name)
+ .replace(':package_version', version)
+ .replace(':file_name', fileName);
+
+ const defaults = {
+ status: 'default',
+ };
+
+ const formData = new FormData();
+ formData.append('file', files[0]);
+
+ return axios.put(url, formData, {
+ headers: {
+ 'Content-Type': 'multipart/form-data',
+ },
+ params: Object.assign(defaults, options),
+ ...axiosOptions,
+ });
+}
diff --git a/app/assets/javascripts/artifacts_settings/graphql/queries/get_keep_latest_artifact_project_setting.query.graphql b/app/assets/javascripts/artifacts_settings/graphql/queries/get_keep_latest_artifact_project_setting.query.graphql
index 7486512c57c..91fa468fc8c 100644
--- a/app/assets/javascripts/artifacts_settings/graphql/queries/get_keep_latest_artifact_project_setting.query.graphql
+++ b/app/assets/javascripts/artifacts_settings/graphql/queries/get_keep_latest_artifact_project_setting.query.graphql
@@ -1,5 +1,6 @@
query getKeepLatestArtifactProjectSetting($fullPath: ID!) {
project(fullPath: $fullPath) {
+ id
ciCdSettings {
keepLatestArtifact
}
diff --git a/app/assets/javascripts/batch_comments/components/draft_note.vue b/app/assets/javascripts/batch_comments/components/draft_note.vue
index 918519f386b..a218624f2d4 100644
--- a/app/assets/javascripts/batch_comments/components/draft_note.vue
+++ b/app/assets/javascripts/batch_comments/components/draft_note.vue
@@ -76,7 +76,7 @@ export default {
},
},
safeHtmlConfig: {
- ADD_TAGS: ['use', 'gl-emoji'],
+ ADD_TAGS: ['use', 'gl-emoji', 'copy-code'],
},
};
</script>
diff --git a/app/assets/javascripts/behaviors/copy_code.js b/app/assets/javascripts/behaviors/copy_code.js
new file mode 100644
index 00000000000..a6e203ea5a2
--- /dev/null
+++ b/app/assets/javascripts/behaviors/copy_code.js
@@ -0,0 +1,66 @@
+import { uniqueId } from 'lodash';
+import { __ } from '~/locale';
+import { spriteIcon } from '~/lib/utils/common_utils';
+import { setAttributes } from '~/lib/utils/dom_utils';
+
+class CopyCodeButton extends HTMLElement {
+ connectedCallback() {
+ this.for = uniqueId('code-');
+
+ this.parentNode.querySelector('pre').setAttribute('id', this.for);
+
+ this.appendChild(this.createButton());
+ }
+
+ createButton() {
+ const button = document.createElement('button');
+
+ setAttributes(button, {
+ type: 'button',
+ class: 'btn btn-default btn-md gl-button btn-icon has-tooltip',
+ 'data-title': __('Copy to clipboard'),
+ 'data-clipboard-target': `pre#${this.for}`,
+ });
+
+ button.innerHTML = spriteIcon('copy-to-clipboard');
+
+ return button;
+ }
+}
+
+function addCodeButton() {
+ [...document.querySelectorAll('pre.code.js-syntax-highlight')]
+ .filter((el) => !el.closest('.js-markdown-code'))
+ .forEach((el) => {
+ const copyCodeEl = document.createElement('copy-code');
+ copyCodeEl.setAttribute('for', uniqueId('code-'));
+
+ const wrapper = document.createElement('div');
+ wrapper.className = 'gl-relative markdown-code-block js-markdown-code';
+ wrapper.appendChild(el.cloneNode(true));
+ wrapper.appendChild(copyCodeEl);
+
+ el.parentNode.insertBefore(wrapper, el);
+
+ el.remove();
+ });
+}
+
+export const initCopyCodeButton = (selector = '#content-body') => {
+ if (!customElements.get('copy-code')) {
+ customElements.define('copy-code', CopyCodeButton);
+ }
+
+ const el = document.querySelector(selector);
+
+ if (!el) return () => {};
+
+ const observer = new MutationObserver(() => addCodeButton());
+
+ observer.observe(document.querySelector(selector), {
+ childList: true,
+ subtree: true,
+ });
+
+ return () => observer.disconnect();
+};
diff --git a/app/assets/javascripts/behaviors/gl_emoji.js b/app/assets/javascripts/behaviors/gl_emoji.js
index ef445548e6e..8fe90b6bb15 100644
--- a/app/assets/javascripts/behaviors/gl_emoji.js
+++ b/app/assets/javascripts/behaviors/gl_emoji.js
@@ -33,7 +33,7 @@ class GlEmoji extends HTMLElement {
this.dataset.unicodeVersion = unicodeVersion;
emojiUnicode = emojiInfo.e;
- this.innerHTML = emojiInfo.e;
+ this.textContent = emojiInfo.e;
this.title = emojiInfo.d;
}
diff --git a/app/assets/javascripts/behaviors/index.js b/app/assets/javascripts/behaviors/index.js
index bfd025e8dab..30160248a77 100644
--- a/app/assets/javascripts/behaviors/index.js
+++ b/app/assets/javascripts/behaviors/index.js
@@ -1,6 +1,5 @@
import $ from 'jquery';
import './autosize';
-import './bind_in_out';
import './markdown/render_gfm';
import initCollapseSidebarOnWindowResize from './collapse_sidebar_on_window_resize';
import initCopyToClipboard from './copy_to_clipboard';
diff --git a/app/assets/javascripts/behaviors/shortcuts/shortcuts_issuable.js b/app/assets/javascripts/behaviors/shortcuts/shortcuts_issuable.js
index c2908133fd0..e58c51104c5 100644
--- a/app/assets/javascripts/behaviors/shortcuts/shortcuts_issuable.js
+++ b/app/assets/javascripts/behaviors/shortcuts/shortcuts_issuable.js
@@ -3,6 +3,7 @@ import Mousetrap from 'mousetrap';
import { clickCopyToClipboardButton } from '~/behaviors/copy_to_clipboard';
import { getSelectedFragment } from '~/lib/utils/common_utils';
import { isElementVisible } from '~/lib/utils/dom_utils';
+import { DEBOUNCE_DROPDOWN_DELAY } from '~/vue_shared/components/sidebar/labels_select_widget/constants';
import Sidebar from '../../right_sidebar';
import { CopyAsGFM } from '../markdown/copy_as_gfm';
import {
@@ -114,6 +115,14 @@ export default class ShortcutsIssuable extends Shortcuts {
static openSidebarDropdown(name) {
Sidebar.instance.openDropdown(name);
+ // Wait for the sidebar to trigger('click') open
+ // so it doesn't cause our dropdown to close preemptively
+ setTimeout(() => {
+ const editBtn =
+ document.querySelector(`.block.${name} .shortcut-sidebar-dropdown-toggle`) ||
+ document.querySelector(`.block.${name} .edit-link`);
+ editBtn.click();
+ }, DEBOUNCE_DROPDOWN_DELAY);
return false;
}
diff --git a/app/assets/javascripts/blob/components/blob_content.vue b/app/assets/javascripts/blob/components/blob_content.vue
index e3e43ea3a0e..9832ebbea5c 100644
--- a/app/assets/javascripts/blob/components/blob_content.vue
+++ b/app/assets/javascripts/blob/components/blob_content.vue
@@ -86,7 +86,7 @@ export default {
:file-name="blob.name"
:type="activeViewer.fileType"
:hide-line-numbers="hideLineNumbers"
- data-qa-selector="file_content"
+ data-qa-selector="blob_viewer_file_content"
/>
</template>
</div>
diff --git a/app/assets/javascripts/blob/pdf/pdf_viewer.vue b/app/assets/javascripts/blob/pdf/pdf_viewer.vue
index 96d6f500960..a1a62abeb6f 100644
--- a/app/assets/javascripts/blob/pdf/pdf_viewer.vue
+++ b/app/assets/javascripts/blob/pdf/pdf_viewer.vue
@@ -38,7 +38,13 @@ export default {
<div v-if="loading && !error" class="text-center loading">
<gl-loading-icon class="mt-5" size="lg" />
</div>
- <pdf-lab v-if="!loadError" :pdf="pdf" @pdflabload="onLoad" @pdflaberror="onError" />
+ <pdf-lab
+ v-if="!loadError"
+ :pdf="pdf"
+ @pdflabload="onLoad"
+ @pdflaberror="onError"
+ v-on="$listeners"
+ />
<p v-if="error" class="text-center">
<span v-if="loadError" ref="loadError">
{{ __('An error occurred while loading the file. Please try again later.') }}
diff --git a/app/assets/javascripts/blob/pipeline_tour_success_modal.vue b/app/assets/javascripts/blob/pipeline_tour_success_modal.vue
index e75aa523ed0..47a0c4ba2d1 100644
--- a/app/assets/javascripts/blob/pipeline_tour_success_modal.vue
+++ b/app/assets/javascripts/blob/pipeline_tour_success_modal.vue
@@ -71,7 +71,7 @@ export default {
i18n: {
modalTitle: __("That's it, well done!"),
pipelinesButton: s__('MR widget|See your pipeline in action'),
- mergeRequestButton: s__('MR widget|Back to the Merge request'),
+ mergeRequestButton: s__('MR widget|Back to the merge request'),
bodyMessage: s__(
`MR widget|The pipeline will test your code on every commit. A %{codeQualityLinkStart}code quality report%{codeQualityLinkEnd} will appear in your merge requests to warn you about potential code degradations.`,
),
diff --git a/app/assets/javascripts/blob_edit/edit_blob.js b/app/assets/javascripts/blob_edit/edit_blob.js
index 118cef59d5a..ee2f6cfb46c 100644
--- a/app/assets/javascripts/blob_edit/edit_blob.js
+++ b/app/assets/javascripts/blob_edit/edit_blob.js
@@ -1,4 +1,5 @@
import $ from 'jquery';
+import { SourceEditorExtension } from '~/editor/extensions/source_editor_extension_base';
import { FileTemplateExtension } from '~/editor/extensions/source_editor_file_template_ext';
import SourceEditor from '~/editor/source_editor';
import { getBlobLanguage } from '~/editor/utils';
@@ -26,23 +27,29 @@ export default class EditBlob {
this.editor.focus();
}
- fetchMarkdownExtension() {
- import('~/editor/extensions/source_editor_markdown_ext')
- .then(({ EditorMarkdownExtension: MarkdownExtension } = {}) => {
- this.editor.use(
- new MarkdownExtension({
- instance: this.editor,
- previewMarkdownPath: this.options.previewMarkdownPath,
- }),
- );
- this.hasMarkdownExtension = true;
- addEditorMarkdownListeners(this.editor);
- })
- .catch((e) =>
- createFlash({
- message: `${BLOB_EDITOR_ERROR}: ${e}`,
- }),
- );
+ async fetchMarkdownExtension() {
+ try {
+ const [
+ { EditorMarkdownExtension: MarkdownExtension },
+ { EditorMarkdownPreviewExtension: MarkdownLivePreview },
+ ] = await Promise.all([
+ import('~/editor/extensions/source_editor_markdown_ext'),
+ import('~/editor/extensions/source_editor_markdown_livepreview_ext'),
+ ]);
+ this.editor.use([
+ { definition: MarkdownExtension },
+ {
+ definition: MarkdownLivePreview,
+ setupOptions: { previewMarkdownPath: this.options.previewMarkdownPath },
+ },
+ ]);
+ } catch (e) {
+ createFlash({
+ message: `${BLOB_EDITOR_ERROR}: ${e}`,
+ });
+ }
+ this.hasMarkdownExtension = true;
+ addEditorMarkdownListeners(this.editor);
}
configureMonacoEditor() {
@@ -60,7 +67,7 @@ export default class EditBlob {
blobPath: fileNameEl.value,
blobContent: editorEl.innerText,
});
- this.editor.use(new FileTemplateExtension({ instance: this.editor }));
+ this.editor.use([{ definition: SourceEditorExtension }, { definition: FileTemplateExtension }]);
fileNameEl.addEventListener('change', () => {
this.editor.updateModelLanguage(fileNameEl.value);
diff --git a/app/assets/javascripts/boards/boards_util.js b/app/assets/javascripts/boards/boards_util.js
index e6c91c7ac1f..7e4d3ebb686 100644
--- a/app/assets/javascripts/boards/boards_util.js
+++ b/app/assets/javascripts/boards/boards_util.js
@@ -1,6 +1,6 @@
import { sortBy, cloneDeep } from 'lodash';
import { isGid } from '~/graphql_shared/utils';
-import { ListType, MilestoneIDs } from './constants';
+import { ListType, MilestoneIDs, AssigneeFilterType, MilestoneFilterType } from './constants';
export function getMilestone() {
return null;
@@ -186,6 +186,7 @@ export function isListDraggable(list) {
export const FiltersInfo = {
assigneeUsername: {
negatedSupport: true,
+ remap: (k, v) => (v === AssigneeFilterType.any ? 'assigneeWildcardId' : k),
},
assigneeId: {
// assigneeId should be renamed to assigneeWildcardId.
@@ -204,6 +205,11 @@ export const FiltersInfo = {
},
milestoneTitle: {
negatedSupport: true,
+ remap: (k, v) => (Object.values(MilestoneFilterType).includes(v) ? 'milestoneWildcardId' : k),
+ },
+ milestoneWildcardId: {
+ negatedSupport: true,
+ transform: (val) => val.toUpperCase(),
},
myReactionEmoji: {
negatedSupport: true,
@@ -214,6 +220,10 @@ export const FiltersInfo = {
types: {
negatedSupport: true,
},
+ confidential: {
+ negatedSupport: false,
+ transform: (val) => val === 'yes',
+ },
search: {
negatedSupport: false,
},
diff --git a/app/assets/javascripts/boards/components/board_card_inner.vue b/app/assets/javascripts/boards/components/board_card_inner.vue
index b6ccc6a00fe..ea80496c3f5 100644
--- a/app/assets/javascripts/boards/components/board_card_inner.vue
+++ b/app/assets/javascripts/boards/components/board_card_inner.vue
@@ -13,7 +13,7 @@ import boardCardInner from 'ee_else_ce/boards/mixins/board_card_inner';
import { isScopedLabel } from '~/lib/utils/common_utils';
import { updateHistory } from '~/lib/utils/url_utility';
import { sprintf, __, n__ } from '~/locale';
-import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate.vue';
+import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate/tooltip_on_truncate.vue';
import UserAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue';
import { ListType } from '../constants';
import eventHub from '../eventhub';
diff --git a/app/assets/javascripts/boards/components/board_content_sidebar.vue b/app/assets/javascripts/boards/components/board_content_sidebar.vue
index 54668c9e88e..f89f8e5feb8 100644
--- a/app/assets/javascripts/boards/components/board_content_sidebar.vue
+++ b/app/assets/javascripts/boards/components/board_content_sidebar.vue
@@ -4,7 +4,6 @@ import { MountingPortal } from 'portal-vue';
import { mapState, mapActions, mapGetters } from 'vuex';
import SidebarDropdownWidget from 'ee_else_ce/sidebar/components/sidebar_dropdown_widget.vue';
import { __, sprintf } from '~/locale';
-import BoardSidebarLabelsSelect from '~/boards/components/sidebar/board_sidebar_labels_select.vue';
import BoardSidebarTimeTracker from '~/boards/components/sidebar/board_sidebar_time_tracker.vue';
import BoardSidebarTitle from '~/boards/components/sidebar/board_sidebar_title.vue';
import { ISSUABLE } from '~/boards/constants';
@@ -26,7 +25,6 @@ export default {
SidebarDateWidget,
SidebarConfidentialityWidget,
BoardSidebarTimeTracker,
- BoardSidebarLabelsSelect,
SidebarLabelsWidget,
SidebarSubscriptionsWidget,
SidebarDropdownWidget,
@@ -210,7 +208,6 @@ export default {
data-testid="sidebar-due-date"
/>
<sidebar-labels-widget
- v-if="glFeatures.labelsWidget"
class="block labels"
data-testid="sidebar-labels"
:iid="activeBoardItem.iid"
@@ -230,7 +227,6 @@ export default {
>
{{ __('None') }}
</sidebar-labels-widget>
- <board-sidebar-labels-select v-else class="block labels" />
<sidebar-weight-widget
v-if="weightFeatureAvailable"
:iid="activeBoardItem.iid"
diff --git a/app/assets/javascripts/boards/components/board_filtered_search.vue b/app/assets/javascripts/boards/components/board_filtered_search.vue
index 6e6ada2d109..09ec385bbba 100644
--- a/app/assets/javascripts/boards/components/board_filtered_search.vue
+++ b/app/assets/javascripts/boards/components/board_filtered_search.vue
@@ -1,7 +1,7 @@
<script>
import { pickBy, isEmpty } from 'lodash';
import { mapActions } from 'vuex';
-import { getIdFromGraphQLId } from '~/graphql_shared/utils';
+import { getIdFromGraphQLId, isGid } from '~/graphql_shared/utils';
import { updateHistory, setUrlParams } from '~/lib/utils/url_utility';
import { __ } from '~/locale';
import { FILTERED_SEARCH_TERM } from '~/vue_shared/components/filtered_search_bar/constants';
@@ -39,30 +39,33 @@ export default {
assigneeUsername,
search,
milestoneTitle,
+ iterationId,
types,
weight,
epicId,
myReactionEmoji,
+ releaseTag,
+ confidential,
} = this.filterParams;
const filteredSearchValue = [];
if (authorUsername) {
filteredSearchValue.push({
- type: 'author_username',
+ type: 'author',
value: { data: authorUsername, operator: '=' },
});
}
if (assigneeUsername) {
filteredSearchValue.push({
- type: 'assignee_username',
+ type: 'assignee',
value: { data: assigneeUsername, operator: '=' },
});
}
if (types) {
filteredSearchValue.push({
- type: 'types',
+ type: 'type',
value: { data: types, operator: '=' },
});
}
@@ -70,7 +73,7 @@ export default {
if (labelName?.length) {
filteredSearchValue.push(
...labelName.map((label) => ({
- type: 'label_name',
+ type: 'label',
value: { data: label, operator: '=' },
})),
);
@@ -78,11 +81,18 @@ export default {
if (milestoneTitle) {
filteredSearchValue.push({
- type: 'milestone_title',
+ type: 'milestone',
value: { data: milestoneTitle, operator: '=' },
});
}
+ if (iterationId) {
+ filteredSearchValue.push({
+ type: 'iteration',
+ value: { data: iterationId, operator: '=' },
+ });
+ }
+
if (weight) {
filteredSearchValue.push({
type: 'weight',
@@ -92,32 +102,53 @@ export default {
if (myReactionEmoji) {
filteredSearchValue.push({
- type: 'my_reaction_emoji',
+ type: 'my-reaction',
value: { data: myReactionEmoji, operator: '=' },
});
}
+ if (releaseTag) {
+ filteredSearchValue.push({
+ type: 'release',
+ value: { data: releaseTag, operator: '=' },
+ });
+ }
+
+ if (confidential !== undefined) {
+ filteredSearchValue.push({
+ type: 'confidential',
+ value: { data: confidential },
+ });
+ }
+
if (epicId) {
filteredSearchValue.push({
- type: 'epic_id',
+ type: 'epic',
value: { data: epicId, operator: '=' },
});
}
if (this.filterParams['not[authorUsername]']) {
filteredSearchValue.push({
- type: 'author_username',
+ type: 'author',
value: { data: this.filterParams['not[authorUsername]'], operator: '!=' },
});
}
if (this.filterParams['not[milestoneTitle]']) {
filteredSearchValue.push({
- type: 'milestone_title',
+ type: 'milestone',
value: { data: this.filterParams['not[milestoneTitle]'], operator: '!=' },
});
}
+ if (this.filterParams['not[iteration_id]']) {
+ filteredSearchValue.push({
+ type: 'iteration_id',
+ value: { data: this.filterParams['not[iteration_id]'], operator: '!=' },
+ });
+ }
+
if (this.filterParams['not[weight]']) {
filteredSearchValue.push({
type: 'weight',
@@ -127,7 +158,7 @@ export default {
if (this.filterParams['not[assigneeUsername]']) {
filteredSearchValue.push({
- type: 'assignee_username',
+ type: 'assignee',
value: { data: this.filterParams['not[assigneeUsername]'], operator: '!=' },
});
}
@@ -135,7 +166,7 @@ export default {
if (this.filterParams['not[labelName]']) {
filteredSearchValue.push(
...this.filterParams['not[labelName]'].map((label) => ({
- type: 'label_name',
+ type: 'label',
value: { data: label, operator: '!=' },
})),
);
@@ -143,25 +174,32 @@ export default {
if (this.filterParams['not[types]']) {
filteredSearchValue.push({
- type: 'types',
+ type: 'type',
value: { data: this.filterParams['not[types]'], operator: '!=' },
});
}
if (this.filterParams['not[epicId]']) {
filteredSearchValue.push({
- type: 'epic_id',
+ type: 'epic',
value: { data: this.filterParams['not[epicId]'], operator: '!=' },
});
}
if (this.filterParams['not[myReactionEmoji]']) {
filteredSearchValue.push({
- type: 'my_reaction_emoji',
+ type: 'my-reaction',
value: { data: this.filterParams['not[myReactionEmoji]'], operator: '!=' },
});
}
+ if (this.filterParams['not[releaseTag]']) {
+ filteredSearchValue.push({
+ type: 'release',
+ value: { data: this.filterParams['not[releaseTag]'], operator: '!=' },
+ });
+ }
+
if (search) {
filteredSearchValue.push(search);
}
@@ -179,8 +217,10 @@ export default {
weight,
epicId,
myReactionEmoji,
+ iterationId,
+ releaseTag,
+ confidential,
} = this.filterParams;
-
let notParams = {};
if (Object.prototype.hasOwnProperty.call(this.filterParams, 'not')) {
@@ -194,6 +234,8 @@ export default {
'not[weight]': this.filterParams.not.weight,
'not[epic_id]': this.filterParams.not.epicId,
'not[my_reaction_emoji]': this.filterParams.not.myReactionEmoji,
+ 'not[iteration_id]': this.filterParams.not.iterationId,
+ 'not[release_tag]': this.filterParams.not.releaseTag,
},
undefined,
);
@@ -205,11 +247,14 @@ export default {
'label_name[]': labelName,
assignee_username: assigneeUsername,
milestone_title: milestoneTitle,
+ iteration_id: iterationId,
search,
types,
weight,
- epic_id: getIdFromGraphQLId(epicId),
+ epic_id: isGid(epicId) ? getIdFromGraphQLId(epicId) : epicId,
my_reaction_emoji: myReactionEmoji,
+ release_tag: releaseTag,
+ confidential,
};
},
},
@@ -246,30 +291,39 @@ export default {
filters.forEach((filter) => {
switch (filter.type) {
- case 'author_username':
+ case 'author':
filterParams.authorUsername = filter.value.data;
break;
- case 'assignee_username':
+ case 'assignee':
filterParams.assigneeUsername = filter.value.data;
break;
- case 'types':
+ case 'type':
filterParams.types = filter.value.data;
break;
- case 'label_name':
+ case 'label':
labels.push(filter.value.data);
break;
- case 'milestone_title':
+ case 'milestone':
filterParams.milestoneTitle = filter.value.data;
break;
+ case 'iteration':
+ filterParams.iterationId = filter.value.data;
+ break;
case 'weight':
filterParams.weight = filter.value.data;
break;
- case 'epic_id':
+ case 'epic':
filterParams.epicId = filter.value.data;
break;
- case 'my_reaction_emoji':
+ case 'my-reaction':
filterParams.myReactionEmoji = filter.value.data;
break;
+ case 'release':
+ filterParams.releaseTag = filter.value.data;
+ break;
+ case 'confidential':
+ filterParams.confidential = filter.value.data;
+ break;
case 'filtered-search-term':
if (filter.value.data) plainText.push(filter.value.data);
break;
@@ -285,6 +339,7 @@ export default {
if (plainText.length) {
filterParams.search = plainText.join(' ');
}
+
return filterParams;
},
},
diff --git a/app/assets/javascripts/boards/components/board_list.vue b/app/assets/javascripts/boards/components/board_list.vue
index 47dffc985aa..e4c3c3206a8 100644
--- a/app/assets/javascripts/boards/components/board_list.vue
+++ b/app/assets/javascripts/boards/components/board_list.vue
@@ -6,6 +6,7 @@ import { sortableStart, sortableEnd } from '~/boards/mixins/sortable_default_opt
import { sprintf, __ } from '~/locale';
import defaultSortableConfig from '~/sortable/sortable_config';
import Tracking from '~/tracking';
+import listQuery from 'ee_else_ce/boards/graphql/board_lists_deferred.query.graphql';
import { toggleFormEventPrefix, DraggableItemTypes } from '../constants';
import eventHub from '../eventhub';
import BoardCard from './board_card.vue';
@@ -50,11 +51,22 @@ export default {
showEpicForm: false,
};
},
+ apollo: {
+ boardList: {
+ query: listQuery,
+ variables() {
+ return {
+ id: this.list.id,
+ filters: this.filterParams,
+ };
+ },
+ },
+ },
computed: {
- ...mapState(['pageInfoByListId', 'listsFlags']),
+ ...mapState(['pageInfoByListId', 'listsFlags', 'filterParams']),
...mapGetters(['isEpicBoard']),
listItemsCount() {
- return this.isEpicBoard ? this.list.epicsCount : this.list.issuesCount;
+ return this.isEpicBoard ? this.list.epicsCount : this.boardList?.issuesCount;
},
paginatedIssueText() {
return sprintf(__('Showing %{pageSize} of %{total} %{issuableType}'), {
diff --git a/app/assets/javascripts/boards/components/board_list_header.vue b/app/assets/javascripts/boards/components/board_list_header.vue
index e985a368e64..19004518edf 100644
--- a/app/assets/javascripts/boards/components/board_list_header.vue
+++ b/app/assets/javascripts/boards/components/board_list_header.vue
@@ -17,6 +17,7 @@ import sidebarEventHub from '~/sidebar/event_hub';
import Tracking from '~/tracking';
import { formatDate } from '~/lib/utils/datetime_utility';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
+import listQuery from 'ee_else_ce/boards/graphql/board_lists_deferred.query.graphql';
import AccessorUtilities from '../../lib/utils/accessor';
import { inactiveId, LIST, ListType, toggleFormEventPrefix } from '../constants';
import eventHub from '../eventhub';
@@ -74,7 +75,7 @@ export default {
},
},
computed: {
- ...mapState(['activeId']),
+ ...mapState(['activeId', 'filterParams']),
...mapGetters(['isEpicBoard', 'isSwimlanesOn']),
isLoggedIn() {
return Boolean(this.currentUserId);
@@ -119,14 +120,11 @@ export default {
}
return false;
},
- itemsCount() {
- return this.list.issuesCount;
- },
countIcon() {
return 'issues';
},
itemsTooltipLabel() {
- return n__(`%d issue`, `%d issues`, this.itemsCount);
+ return n__(`%d issue`, `%d issues`, this.boardLists?.issuesCount);
},
chevronTooltip() {
return this.list.collapsed ? this.$options.i18n.expand : this.$options.i18n.collapse;
@@ -158,6 +156,23 @@ export default {
userCanDrag() {
return !this.disabled && isListDraggable(this.list);
},
+ isLoading() {
+ return this.$apollo.queries.boardList.loading;
+ },
+ },
+ apollo: {
+ boardList: {
+ query: listQuery,
+ variables() {
+ return {
+ id: this.list.id,
+ filters: this.filterParams,
+ };
+ },
+ skip() {
+ return this.isEpicBoard;
+ },
+ },
},
created() {
const localCollapsed = parseBoolean(localStorage.getItem(`${this.uniqueKey}.collapsed`));
@@ -375,10 +390,10 @@ export default {
</gl-sprintf>
</div>
<div v-else>• {{ itemsTooltipLabel }}</div>
- <div v-if="weightFeatureAvailable">
+ <div v-if="weightFeatureAvailable && !isLoading">
<gl-sprintf :message="__('%{totalWeight} total weight')">
- <template #totalWeight>{{ list.totalWeight }}</template>
+ <template #totalWeight>{{ boardList.totalWeight }}</template>
</gl-sprintf>
</div>
</gl-tooltip>
@@ -396,14 +411,18 @@ export default {
<gl-tooltip :target="() => $refs.itemCount" :title="itemsTooltipLabel" />
<span ref="itemCount" class="gl-display-inline-flex gl-align-items-center">
<gl-icon class="gl-mr-2" :name="countIcon" />
- <item-count :items-size="itemsCount" :max-issue-count="list.maxIssueCount" />
+ <item-count
+ v-if="!isLoading"
+ :items-size="isEpicBoard ? list.epicsCount : boardList.issuesCount"
+ :max-issue-count="list.maxIssueCount"
+ />
</span>
<!-- EE start -->
- <template v-if="weightFeatureAvailable && !isEpicBoard">
+ <template v-if="weightFeatureAvailable && !isEpicBoard && !isLoading">
<gl-tooltip :target="() => $refs.weightTooltip" :title="weightCountToolTip" />
<span ref="weightTooltip" class="gl-display-inline-flex gl-ml-3">
<gl-icon class="gl-mr-2" name="weight" />
- {{ list.totalWeight }}
+ {{ boardList.totalWeight }}
</span>
</template>
<!-- EE end -->
diff --git a/app/assets/javascripts/boards/components/boards_selector.vue b/app/assets/javascripts/boards/components/boards_selector.vue
index 71facba1378..69343cd78d8 100644
--- a/app/assets/javascripts/boards/components/boards_selector.vue
+++ b/app/assets/javascripts/boards/components/boards_selector.vue
@@ -349,6 +349,9 @@ export default {
v-if="showCreate"
v-gl-modal-directive="'board-config-modal'"
data-qa-selector="create_new_board_button"
+ data-track-action="click_button"
+ data-track-label="create_new_board"
+ data-track-property="dropdown"
@click.prevent="showPage('new')"
>
{{ s__('IssueBoards|Create new board') }}
diff --git a/app/assets/javascripts/boards/components/issue_board_filtered_search.vue b/app/assets/javascripts/boards/components/issue_board_filtered_search.vue
index bdb9c2be836..7fc87f9f672 100644
--- a/app/assets/javascripts/boards/components/issue_board_filtered_search.vue
+++ b/app/assets/javascripts/boards/components/issue_board_filtered_search.vue
@@ -2,22 +2,25 @@
import { GlFilteredSearchToken } from '@gitlab/ui';
import fuzzaldrinPlus from 'fuzzaldrin-plus';
import { mapActions } from 'vuex';
+import { orderBy } from 'lodash';
import BoardFilteredSearch from 'ee_else_ce/boards/components/board_filtered_search.vue';
import { BoardType } from '~/boards/constants';
import axios from '~/lib/utils/axios_utils';
+import { joinPaths } from '~/lib/utils/url_utility';
import issueBoardFilters from '~/boards/issue_board_filters';
import { TYPE_USER } from '~/graphql_shared/constants';
import { convertToGraphQLId } from '~/graphql_shared/utils';
import { __ } from '~/locale';
import {
- DEFAULT_MILESTONES_GRAPHQL,
TOKEN_TITLE_MY_REACTION,
+ OPERATOR_IS_AND_IS_NOT,
+ OPERATOR_IS_ONLY,
} from '~/vue_shared/components/filtered_search_bar/constants';
import AuthorToken from '~/vue_shared/components/filtered_search_bar/tokens/author_token.vue';
import EmojiToken from '~/vue_shared/components/filtered_search_bar/tokens/emoji_token.vue';
import LabelToken from '~/vue_shared/components/filtered_search_bar/tokens/label_token.vue';
import MilestoneToken from '~/vue_shared/components/filtered_search_bar/tokens/milestone_token.vue';
-import WeightToken from '~/vue_shared/components/filtered_search_bar/tokens/weight_token.vue';
+import ReleaseToken from '~/vue_shared/components/filtered_search_bar/tokens/release_token.vue';
export default {
types: {
@@ -34,12 +37,11 @@ export default {
incident: __('Incident'),
issue: __('Issue'),
milestone: __('Milestone'),
- weight: __('Weight'),
- is: __('is'),
- isNot: __('is not'),
+ release: __('Release'),
+ confidential: __('Confidential'),
},
components: { BoardFilteredSearch },
- inject: ['isSignedIn'],
+ inject: ['isSignedIn', 'releasesFetchPath'],
props: {
fullPath: {
type: String,
@@ -62,15 +64,14 @@ export default {
tokensCE() {
const {
label,
- is,
- isNot,
author,
assignee,
issue,
incident,
type,
milestone,
- weight,
+ release,
+ confidential,
} = this.$options.i18n;
const { types } = this.$options;
const { fetchAuthors, fetchLabels } = issueBoardFilters(
@@ -79,15 +80,12 @@ export default {
this.boardType,
);
- return [
+ const tokens = [
{
icon: 'user',
title: assignee,
- type: 'assignee_username',
- operators: [
- { value: '=', description: is },
- { value: '!=', description: isNot },
- ],
+ type: 'assignee',
+ operators: OPERATOR_IS_AND_IS_NOT,
token: AuthorToken,
unique: true,
fetchAuthors,
@@ -96,11 +94,8 @@ export default {
{
icon: 'pencil',
title: author,
- type: 'author_username',
- operators: [
- { value: '=', description: is },
- { value: '!=', description: isNot },
- ],
+ type: 'author',
+ operators: OPERATOR_IS_AND_IS_NOT,
symbol: '@',
token: AuthorToken,
unique: true,
@@ -110,11 +105,8 @@ export default {
{
icon: 'labels',
title: label,
- type: 'label_name',
- operators: [
- { value: '=', description: is },
- { value: '!=', description: isNot },
- ],
+ type: 'label',
+ operators: OPERATOR_IS_AND_IS_NOT,
token: LabelToken,
unique: false,
symbol: '~',
@@ -123,7 +115,7 @@ export default {
...(this.isSignedIn
? [
{
- type: 'my_reaction_emoji',
+ type: 'my-reaction',
title: TOKEN_TITLE_MY_REACTION,
icon: 'thumb-up',
token: EmojiToken,
@@ -144,22 +136,33 @@ export default {
});
},
},
+ {
+ type: 'confidential',
+ icon: 'eye-slash',
+ title: confidential,
+ unique: true,
+ token: GlFilteredSearchToken,
+ operators: OPERATOR_IS_ONLY,
+ options: [
+ { icon: 'eye-slash', value: 'yes', title: __('Yes') },
+ { icon: 'eye', value: 'no', title: __('No') },
+ ],
+ },
]
: []),
{
- type: 'milestone_title',
+ type: 'milestone',
title: milestone,
icon: 'clock',
symbol: '%',
token: MilestoneToken,
unique: true,
- defaultMilestones: DEFAULT_MILESTONES_GRAPHQL,
fetchMilestones: this.fetchMilestones,
},
{
icon: 'issues',
title: type,
- type: 'types',
+ type: 'type',
token: GlFilteredSearchToken,
unique: true,
options: [
@@ -168,13 +171,27 @@ export default {
],
},
{
- type: 'weight',
- title: weight,
- icon: 'weight',
- token: WeightToken,
- unique: true,
+ type: 'release',
+ title: release,
+ icon: 'rocket',
+ token: ReleaseToken,
+ fetchReleases: (search) => {
+ // TODO: Switch to GraphQL query when backend is ready: https://gitlab.com/gitlab-org/gitlab/-/issues/337686
+ return axios
+ .get(joinPaths(gon.relative_url_root, this.releasesFetchPath))
+ .then(({ data }) => {
+ if (search) {
+ return fuzzaldrinPlus.filter(data, search, {
+ key: ['tag'],
+ });
+ }
+ return data;
+ });
+ },
},
];
+
+ return orderBy(tokens, ['title']);
},
tokens() {
return this.tokensCE;
diff --git a/app/assets/javascripts/boards/components/sidebar/board_sidebar_labels_select.vue b/app/assets/javascripts/boards/components/sidebar/board_sidebar_labels_select.vue
deleted file mode 100644
index ec53947fd5f..00000000000
--- a/app/assets/javascripts/boards/components/sidebar/board_sidebar_labels_select.vue
+++ /dev/null
@@ -1,173 +0,0 @@
-<script>
-import { GlLabel } from '@gitlab/ui';
-import { mapGetters, mapActions } from 'vuex';
-import Api from '~/api';
-import BoardEditableItem from '~/boards/components/sidebar/board_editable_item.vue';
-import { getIdFromGraphQLId } from '~/graphql_shared/utils';
-import { isScopedLabel } from '~/lib/utils/common_utils';
-import { mergeUrlParams } from '~/lib/utils/url_utility';
-import { __ } from '~/locale';
-import LabelsSelect from '~/vue_shared/components/sidebar/labels_select_vue/labels_select_root.vue';
-
-export default {
- components: {
- BoardEditableItem,
- LabelsSelect,
- GlLabel,
- },
- inject: {
- labelsFetchPath: {
- default: null,
- },
- labelsManagePath: {},
- labelsFilterBasePath: {},
- },
- data() {
- return {
- loading: false,
- oldIid: null,
- isEditing: false,
- };
- },
- computed: {
- ...mapGetters(['activeBoardItem', 'projectPathForActiveIssue']),
- selectedLabels() {
- const { labels = [] } = this.activeBoardItem;
-
- return labels.map((label) => ({
- ...label,
- id: getIdFromGraphQLId(label.id),
- }));
- },
- issueLabels() {
- const { labels = [] } = this.activeBoardItem;
-
- return labels.map((label) => ({
- ...label,
- scoped: isScopedLabel(label),
- }));
- },
- fetchPath() {
- /*
- Labels fetched in epic boards are always group-level labels
- and the correct path are passed from the backend (injected through labelsFetchPath)
-
- For issue boards, we should always include project-level labels and use a different endpoint.
- (it requires knowing the project path of a selected issue.)
-
- Note 1. that we will be using GraphQL to fetch labels when we create a labels select widget.
- And this component will be removed _wholesale_ https://gitlab.com/gitlab-org/gitlab/-/issues/300653.
-
- Note 2. Moreover, 'fetchPath' needs to be used as a key for 'labels-select' component to force updates.
- 'labels-select' has its own vuex store and initializes the passed props as states
- and these states aren't reactively bound to the passed props.
- */
-
- const projectLabelsFetchPath = mergeUrlParams(
- { include_ancestor_groups: true },
- Api.buildUrl(Api.projectLabelsPath).replace(
- ':namespace_path/:project_path',
- this.projectPathForActiveIssue,
- ),
- );
-
- return this.labelsFetchPath || projectLabelsFetchPath;
- },
- },
- watch: {
- activeBoardItem(_, oldVal) {
- if (this.isEditing) {
- this.oldIid = oldVal.iid;
- } else {
- this.oldIid = null;
- }
- },
- },
- methods: {
- ...mapActions(['setActiveBoardItemLabels', 'setError']),
- async setLabels(payload) {
- this.loading = true;
- this.$refs.sidebarItem.collapse();
-
- try {
- const addLabelIds = payload.filter((label) => label.set).map((label) => label.id);
- const removeLabelIds = payload.filter((label) => !label.set).map((label) => label.id);
-
- const input = {
- addLabelIds,
- removeLabelIds,
- projectPath: this.projectPathForActiveIssue,
- iid: this.oldIid,
- };
- await this.setActiveBoardItemLabels(input);
- this.oldIid = null;
- } catch (e) {
- this.setError({ error: e, message: __('An error occurred while updating labels.') });
- } finally {
- this.loading = false;
- }
- },
- async removeLabel(id) {
- this.loading = true;
-
- try {
- const removeLabelIds = [getIdFromGraphQLId(id)];
- const input = { removeLabelIds, projectPath: this.projectPathForActiveIssue };
- await this.setActiveBoardItemLabels(input);
- } catch (e) {
- this.setError({ error: e, message: __('An error occurred when removing the label.') });
- } finally {
- this.loading = false;
- }
- },
- },
-};
-</script>
-
-<template>
- <board-editable-item
- ref="sidebarItem"
- :title="__('Labels')"
- :loading="loading"
- data-testid="sidebar-labels"
- @open="isEditing = true"
- @close="isEditing = false"
- >
- <template #collapsed>
- <gl-label
- v-for="label in issueLabels"
- :key="label.id"
- :background-color="label.color"
- :title="label.title"
- :description="label.description"
- :scoped="label.scoped"
- :show-close-button="true"
- :disabled="loading"
- class="gl-mr-2 gl-mb-2"
- @close="removeLabel(label.id)"
- />
- </template>
- <template #default="{ edit }">
- <labels-select
- ref="labelsSelect"
- :key="fetchPath"
- :allow-label-edit="false"
- :allow-label-create="false"
- :allow-multiselect="true"
- :allow-scoped-labels="true"
- :selected-labels="selectedLabels"
- :labels-fetch-path="fetchPath"
- :labels-manage-path="labelsManagePath"
- :labels-filter-base-path="labelsFilterBasePath"
- :labels-list-title="__('Select label')"
- :dropdown-button-text="__('Choose labels')"
- :is-editing="edit"
- variant="sidebar"
- class="gl-display-block labels gl-w-full"
- @updateSelectedLabels="setLabels"
- >
- {{ __('None') }}
- </labels-select>
- </template>
- </board-editable-item>
-</template>
diff --git a/app/assets/javascripts/boards/components/sidebar/board_sidebar_subscription.vue b/app/assets/javascripts/boards/components/sidebar/board_sidebar_subscription.vue
deleted file mode 100644
index 4f5c55d0c5d..00000000000
--- a/app/assets/javascripts/boards/components/sidebar/board_sidebar_subscription.vue
+++ /dev/null
@@ -1,75 +0,0 @@
-<script>
-import { GlToggle } from '@gitlab/ui';
-import { mapGetters, mapActions } from 'vuex';
-import { __, s__ } from '~/locale';
-
-export default {
- i18n: {
- header: {
- title: __('Notifications'),
- /* Any change to subscribeDisabledDescription
- must be reflected in app/helpers/notifications_helper.rb */
- subscribeDisabledDescription: __(
- 'Notifications have been disabled by the project or group owner',
- ),
- },
- updateSubscribedErrorMessage: s__(
- 'IssueBoards|An error occurred while setting notifications status. Please try again.',
- ),
- },
- components: {
- GlToggle,
- },
- inject: ['emailsDisabled'],
- data() {
- return {
- loading: false,
- };
- },
- computed: {
- ...mapGetters(['activeBoardItem', 'projectPathForActiveIssue', 'isEpicBoard']),
- isEmailsDisabled() {
- return this.isEpicBoard ? this.emailsDisabled : this.activeBoardItem.emailsDisabled;
- },
- notificationText() {
- return this.isEmailsDisabled
- ? this.$options.i18n.header.subscribeDisabledDescription
- : this.$options.i18n.header.title;
- },
- },
- methods: {
- ...mapActions(['setActiveItemSubscribed', 'setError']),
- async handleToggleSubscription() {
- this.loading = true;
- try {
- await this.setActiveItemSubscribed({
- subscribed: !this.activeBoardItem.subscribed,
- projectPath: this.projectPathForActiveIssue,
- });
- } catch (error) {
- this.setError({ error, message: this.$options.i18n.updateSubscribedErrorMessage });
- } finally {
- this.loading = false;
- }
- },
- },
-};
-</script>
-
-<template>
- <div
- class="gl-display-flex gl-align-items-center gl-justify-content-space-between"
- data-testid="sidebar-notifications"
- >
- <span data-testid="notification-header-text"> {{ notificationText }} </span>
- <gl-toggle
- v-if="!isEmailsDisabled"
- :value="activeBoardItem.subscribed"
- :is-loading="loading"
- :label="$options.i18n.header.title"
- label-position="hidden"
- data-testid="notification-subscribe-toggle"
- @change="handleToggleSubscription"
- />
- </div>
-</template>
diff --git a/app/assets/javascripts/boards/constants.js b/app/assets/javascripts/boards/constants.js
index 391e0d1fb0a..851b5eca40d 100644
--- a/app/assets/javascripts/boards/constants.js
+++ b/app/assets/javascripts/boards/constants.js
@@ -104,8 +104,10 @@ export const FilterFields = {
'assigneeUsername',
'assigneeWildcardId',
'authorUsername',
+ 'confidential',
'labelName',
'milestoneTitle',
+ 'milestoneWildcardId',
'myReactionEmoji',
'releaseTag',
'search',
@@ -114,6 +116,18 @@ export const FilterFields = {
],
};
+/* eslint-disable @gitlab/require-i18n-strings */
+export const AssigneeFilterType = {
+ any: 'Any',
+};
+
+export const MilestoneFilterType = {
+ any: 'Any',
+ none: 'None',
+ started: 'Started',
+ upcoming: 'Upcoming',
+};
+
export const DraggableItemTypes = {
card: 'card',
list: 'list',
diff --git a/app/assets/javascripts/boards/graphql/board_labels.query.graphql b/app/assets/javascripts/boards/graphql/board_labels.query.graphql
index b19a24e8808..525a4863379 100644
--- a/app/assets/javascripts/boards/graphql/board_labels.query.graphql
+++ b/app/assets/javascripts/boards/graphql/board_labels.query.graphql
@@ -7,6 +7,7 @@ query BoardLabels(
$isProject: Boolean = false
) {
group(fullPath: $fullPath) @include(if: $isGroup) {
+ id
labels(searchTerm: $searchTerm, onlyGroupLabels: true, includeAncestorGroups: true) {
nodes {
...Label
@@ -14,6 +15,7 @@ query BoardLabels(
}
}
project(fullPath: $fullPath) @include(if: $isProject) {
+ id
labels(searchTerm: $searchTerm, includeAncestorGroups: true) {
nodes {
...Label
diff --git a/app/assets/javascripts/boards/graphql/board_list_create.mutation.graphql b/app/assets/javascripts/boards/graphql/board_list_create.mutation.graphql
index 0e1d11727cf..81cc7b4d246 100644
--- a/app/assets/javascripts/boards/graphql/board_list_create.mutation.graphql
+++ b/app/assets/javascripts/boards/graphql/board_list_create.mutation.graphql
@@ -2,6 +2,8 @@
mutation createBoardList($boardId: BoardID!, $backlog: Boolean, $labelId: LabelID) {
boardListCreate(input: { boardId: $boardId, backlog: $backlog, labelId: $labelId }) {
+ # We have ID in a deeply nested fragment
+ # eslint-disable-next-line @graphql-eslint/require-id-when-available
list {
...BoardListFragment
}
diff --git a/app/assets/javascripts/boards/graphql/board_list_shared.fragment.graphql b/app/assets/javascripts/boards/graphql/board_list_shared.fragment.graphql
index d85b736720b..5b532906f6a 100644
--- a/app/assets/javascripts/boards/graphql/board_list_shared.fragment.graphql
+++ b/app/assets/javascripts/boards/graphql/board_list_shared.fragment.graphql
@@ -4,7 +4,6 @@ fragment BoardListShared on BoardList {
position
listType
collapsed
- issuesCount
label {
id
title
diff --git a/app/assets/javascripts/boards/graphql/board_list_update.mutation.graphql b/app/assets/javascripts/boards/graphql/board_list_update.mutation.graphql
index b474c9acb93..7ea0e2f915a 100644
--- a/app/assets/javascripts/boards/graphql/board_list_update.mutation.graphql
+++ b/app/assets/javascripts/boards/graphql/board_list_update.mutation.graphql
@@ -2,6 +2,8 @@
mutation UpdateBoardList($listId: ID!, $position: Int, $collapsed: Boolean) {
updateBoardList(input: { listId: $listId, position: $position, collapsed: $collapsed }) {
+ # We have ID in a deeply nested fragment
+ # eslint-disable-next-line @graphql-eslint/require-id-when-available
list {
...BoardListFragment
}
diff --git a/app/assets/javascripts/boards/graphql/board_lists.query.graphql b/app/assets/javascripts/boards/graphql/board_lists.query.graphql
index 47e87907d76..e6e98864aad 100644
--- a/app/assets/javascripts/boards/graphql/board_lists.query.graphql
+++ b/app/assets/javascripts/boards/graphql/board_lists.query.graphql
@@ -8,9 +8,13 @@ query BoardLists(
$isProject: Boolean = false
) {
group(fullPath: $fullPath) @include(if: $isGroup) {
+ id
board(id: $boardId) {
+ id
hideBacklogList
lists(issueFilters: $filters) {
+ # We have ID in a deeply nested fragment
+ # eslint-disable-next-line @graphql-eslint/require-id-when-available
nodes {
...BoardListFragment
}
@@ -18,9 +22,13 @@ query BoardLists(
}
}
project(fullPath: $fullPath) @include(if: $isProject) {
+ id
board(id: $boardId) {
+ id
hideBacklogList
lists(issueFilters: $filters) {
+ # We have ID in a deeply nested fragment
+ # eslint-disable-next-line @graphql-eslint/require-id-when-available
nodes {
...BoardListFragment
}
diff --git a/app/assets/javascripts/boards/graphql/board_lists_deferred.query.graphql b/app/assets/javascripts/boards/graphql/board_lists_deferred.query.graphql
new file mode 100644
index 00000000000..bae3220dfad
--- /dev/null
+++ b/app/assets/javascripts/boards/graphql/board_lists_deferred.query.graphql
@@ -0,0 +1,6 @@
+query BoardList($id: ID!, $filters: BoardIssueInput) {
+ boardList(id: $id, issueFilters: $filters) {
+ id
+ issuesCount
+ }
+}
diff --git a/app/assets/javascripts/boards/graphql/group_board.query.graphql b/app/assets/javascripts/boards/graphql/group_board.query.graphql
index 77c8e0378f0..8d87b83da96 100644
--- a/app/assets/javascripts/boards/graphql/group_board.query.graphql
+++ b/app/assets/javascripts/boards/graphql/group_board.query.graphql
@@ -2,6 +2,7 @@
query GroupBoard($fullPath: ID!, $boardId: ID!) {
workspace: group(fullPath: $fullPath) {
+ id
board(id: $boardId) {
...BoardScopeFragment
}
diff --git a/app/assets/javascripts/boards/graphql/group_board_members.query.graphql b/app/assets/javascripts/boards/graphql/group_board_members.query.graphql
index d3251c2aa12..aec674eb006 100644
--- a/app/assets/javascripts/boards/graphql/group_board_members.query.graphql
+++ b/app/assets/javascripts/boards/graphql/group_board_members.query.graphql
@@ -3,6 +3,7 @@
query GroupBoardMembers($fullPath: ID!, $search: String) {
workspace: group(fullPath: $fullPath) {
__typename
+ id
assignees: groupMembers(search: $search, relations: [DIRECT, DESCENDANTS, INHERITED]) {
__typename
nodes {
diff --git a/app/assets/javascripts/boards/graphql/group_board_milestones.query.graphql b/app/assets/javascripts/boards/graphql/group_board_milestones.query.graphql
index 73aa9137dec..0963b3fbfaa 100644
--- a/app/assets/javascripts/boards/graphql/group_board_milestones.query.graphql
+++ b/app/assets/javascripts/boards/graphql/group_board_milestones.query.graphql
@@ -1,5 +1,6 @@
query GroupBoardMilestones($fullPath: ID!, $searchTerm: String) {
group(fullPath: $fullPath) {
+ id
milestones(includeAncestors: true, searchTitle: $searchTerm) {
nodes {
id
diff --git a/app/assets/javascripts/boards/graphql/group_boards.query.graphql b/app/assets/javascripts/boards/graphql/group_boards.query.graphql
index feafd6ae10d..0823c4f5a83 100644
--- a/app/assets/javascripts/boards/graphql/group_boards.query.graphql
+++ b/app/assets/javascripts/boards/graphql/group_boards.query.graphql
@@ -2,6 +2,7 @@
query group_boards($fullPath: ID!) {
group(fullPath: $fullPath) {
+ id
boards {
edges {
node {
diff --git a/app/assets/javascripts/boards/graphql/group_projects.query.graphql b/app/assets/javascripts/boards/graphql/group_projects.query.graphql
index c5732bbaff3..0da14d0b872 100644
--- a/app/assets/javascripts/boards/graphql/group_projects.query.graphql
+++ b/app/assets/javascripts/boards/graphql/group_projects.query.graphql
@@ -2,6 +2,7 @@
query boardsGetGroupProjects($fullPath: ID!, $search: String, $after: String) {
group(fullPath: $fullPath) {
+ id
projects(search: $search, after: $after, first: 100, includeSubgroups: true) {
nodes {
id
diff --git a/app/assets/javascripts/boards/graphql/issue_set_labels.mutation.graphql b/app/assets/javascripts/boards/graphql/issue_set_labels.mutation.graphql
index 70eb1dfbf7e..c9c5d744371 100644
--- a/app/assets/javascripts/boards/graphql/issue_set_labels.mutation.graphql
+++ b/app/assets/javascripts/boards/graphql/issue_set_labels.mutation.graphql
@@ -1,13 +1,12 @@
+#import "~/graphql_shared/fragments/label.fragment.graphql"
+
mutation issueSetLabels($input: UpdateIssueInput!) {
- updateIssue(input: $input) {
- issue {
+ updateIssuableLabels: updateIssue(input: $input) {
+ issuable: issue {
id
labels {
nodes {
- id
- title
- color
- description
+ ...Label
}
}
}
diff --git a/app/assets/javascripts/boards/graphql/issue_set_subscription.mutation.graphql b/app/assets/javascripts/boards/graphql/issue_set_subscription.mutation.graphql
index bfb87758e17..c130a64cac4 100644
--- a/app/assets/javascripts/boards/graphql/issue_set_subscription.mutation.graphql
+++ b/app/assets/javascripts/boards/graphql/issue_set_subscription.mutation.graphql
@@ -1,6 +1,7 @@
mutation issueSetSubscription($input: IssueSetSubscriptionInput!) {
updateIssuableSubscription: issueSetSubscription(input: $input) {
issue {
+ id
subscribed
}
errors
diff --git a/app/assets/javascripts/boards/graphql/issue_set_title.mutation.graphql b/app/assets/javascripts/boards/graphql/issue_set_title.mutation.graphql
index 6ad12d982e0..147cf040a85 100644
--- a/app/assets/javascripts/boards/graphql/issue_set_title.mutation.graphql
+++ b/app/assets/javascripts/boards/graphql/issue_set_title.mutation.graphql
@@ -1,6 +1,7 @@
mutation issueSetTitle($input: UpdateIssueInput!) {
updateIssuableTitle: updateIssue(input: $input) {
issue {
+ id
title
}
errors
diff --git a/app/assets/javascripts/boards/graphql/lists_issues.query.graphql b/app/assets/javascripts/boards/graphql/lists_issues.query.graphql
index 9f93bc6d5bf..105f2931caa 100644
--- a/app/assets/javascripts/boards/graphql/lists_issues.query.graphql
+++ b/app/assets/javascripts/boards/graphql/lists_issues.query.graphql
@@ -1,6 +1,6 @@
#import "ee_else_ce/boards/graphql/issue.fragment.graphql"
-query BoardListEE(
+query BoardListsEE(
$fullPath: ID!
$boardId: ID!
$id: ID
@@ -11,7 +11,9 @@ query BoardListEE(
$first: Int
) {
group(fullPath: $fullPath) @include(if: $isGroup) {
+ id
board(id: $boardId) {
+ id
lists(id: $id, issueFilters: $filters) {
nodes {
id
@@ -33,7 +35,9 @@ query BoardListEE(
}
}
project(fullPath: $fullPath) @include(if: $isProject) {
+ id
board(id: $boardId) {
+ id
lists(id: $id, issueFilters: $filters) {
nodes {
id
diff --git a/app/assets/javascripts/boards/graphql/project_board.query.graphql b/app/assets/javascripts/boards/graphql/project_board.query.graphql
index 6e4cd6bed57..8246d615a6a 100644
--- a/app/assets/javascripts/boards/graphql/project_board.query.graphql
+++ b/app/assets/javascripts/boards/graphql/project_board.query.graphql
@@ -2,6 +2,7 @@
query ProjectBoard($fullPath: ID!, $boardId: ID!) {
workspace: project(fullPath: $fullPath) {
+ id
board(id: $boardId) {
...BoardScopeFragment
}
diff --git a/app/assets/javascripts/boards/graphql/project_board_members.query.graphql b/app/assets/javascripts/boards/graphql/project_board_members.query.graphql
index fc6cc6b832c..45bec5e574b 100644
--- a/app/assets/javascripts/boards/graphql/project_board_members.query.graphql
+++ b/app/assets/javascripts/boards/graphql/project_board_members.query.graphql
@@ -3,6 +3,7 @@
query ProjectBoardMembers($fullPath: ID!, $search: String) {
workspace: project(fullPath: $fullPath) {
__typename
+ id
assignees: projectMembers(search: $search) {
__typename
nodes {
diff --git a/app/assets/javascripts/boards/graphql/project_board_milestones.query.graphql b/app/assets/javascripts/boards/graphql/project_board_milestones.query.graphql
index 8dd4d256caa..e456823d78a 100644
--- a/app/assets/javascripts/boards/graphql/project_board_milestones.query.graphql
+++ b/app/assets/javascripts/boards/graphql/project_board_milestones.query.graphql
@@ -1,5 +1,6 @@
query ProjectBoardMilestones($fullPath: ID!, $searchTerm: String) {
project(fullPath: $fullPath) {
+ id
milestones(searchTitle: $searchTerm, includeAncestors: true) {
nodes {
id
diff --git a/app/assets/javascripts/boards/graphql/project_boards.query.graphql b/app/assets/javascripts/boards/graphql/project_boards.query.graphql
index f98d25ba671..b8879bc260c 100644
--- a/app/assets/javascripts/boards/graphql/project_boards.query.graphql
+++ b/app/assets/javascripts/boards/graphql/project_boards.query.graphql
@@ -2,6 +2,7 @@
query project_boards($fullPath: ID!) {
project(fullPath: $fullPath) {
+ id
boards {
edges {
node {
diff --git a/app/assets/javascripts/boards/graphql/project_milestones.query.graphql b/app/assets/javascripts/boards/graphql/project_milestones.query.graphql
index 61c9ddded9b..4c952096d76 100644
--- a/app/assets/javascripts/boards/graphql/project_milestones.query.graphql
+++ b/app/assets/javascripts/boards/graphql/project_milestones.query.graphql
@@ -5,6 +5,7 @@ query boardProjectMilestones(
$searchTitle: String
) {
project(fullPath: $fullPath) {
+ id
milestones(state: $state, includeAncestors: $includeAncestors, searchTitle: $searchTitle) {
edges {
node {
diff --git a/app/assets/javascripts/boards/index.js b/app/assets/javascripts/boards/index.js
index 6fa8dd63245..ded3bfded86 100644
--- a/app/assets/javascripts/boards/index.js
+++ b/app/assets/javascripts/boards/index.js
@@ -110,7 +110,8 @@ export default () => {
});
if (gon?.features?.issueBoardsFilteredSearch) {
- initBoardsFilteredSearch(apolloProvider, isLoggedIn());
+ const { releasesFetchPath } = $boardApp.dataset;
+ initBoardsFilteredSearch(apolloProvider, isLoggedIn(), releasesFetchPath);
}
mountBoardApp($boardApp);
diff --git a/app/assets/javascripts/boards/mount_filtered_search_issue_boards.js b/app/assets/javascripts/boards/mount_filtered_search_issue_boards.js
index 1ea74d5685c..a8ade58e316 100644
--- a/app/assets/javascripts/boards/mount_filtered_search_issue_boards.js
+++ b/app/assets/javascripts/boards/mount_filtered_search_issue_boards.js
@@ -4,7 +4,7 @@ import store from '~/boards/stores';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import { queryToObject } from '~/lib/utils/url_utility';
-export default (apolloProvider, isSignedIn) => {
+export default (apolloProvider, isSignedIn, releasesFetchPath) => {
const el = document.getElementById('js-issue-board-filtered-search');
const rawFilterParams = queryToObject(window.location.search, { gatherArrays: true });
@@ -21,6 +21,7 @@ export default (apolloProvider, isSignedIn) => {
provide: {
initialFilterParams,
isSignedIn,
+ releasesFetchPath,
},
store, // TODO: https://gitlab.com/gitlab-org/gitlab/-/issues/324094
apolloProvider,
diff --git a/app/assets/javascripts/boards/stores/actions.js b/app/assets/javascripts/boards/stores/actions.js
index 3a96e535cf7..1ebfcfc331b 100644
--- a/app/assets/javascripts/boards/stores/actions.js
+++ b/app/assets/javascripts/boards/stores/actions.js
@@ -16,30 +16,30 @@ import {
ListTypeTitles,
DraggableItemTypes,
} from 'ee_else_ce/boards/constants';
-import createBoardListMutation from 'ee_else_ce/boards/graphql/board_list_create.mutation.graphql';
-import issueMoveListMutation from 'ee_else_ce/boards/graphql/issue_move_list.mutation.graphql';
-import { getIdFromGraphQLId } from '~/graphql_shared/utils';
-import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
-import { queryToObject } from '~/lib/utils/url_utility';
-import { s__ } from '~/locale';
import {
+ formatIssueInput,
formatBoardLists,
formatListIssues,
formatListsPageInfo,
formatIssue,
- formatIssueInput,
updateListPosition,
moveItemListHelper,
getMoveData,
FiltersInfo,
filterVariables,
-} from '../boards_util';
+} from 'ee_else_ce/boards/boards_util';
+import createBoardListMutation from 'ee_else_ce/boards/graphql/board_list_create.mutation.graphql';
+import issueMoveListMutation from 'ee_else_ce/boards/graphql/issue_move_list.mutation.graphql';
+import totalCountAndWeightQuery from 'ee_else_ce/boards/graphql/board_lists_deferred.query.graphql';
+import { getIdFromGraphQLId } from '~/graphql_shared/utils';
+import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
+import { queryToObject } from '~/lib/utils/url_utility';
+import { s__ } from '~/locale';
import { gqlClient } from '../graphql';
import boardLabelsQuery from '../graphql/board_labels.query.graphql';
import groupBoardMilestonesQuery from '../graphql/group_board_milestones.query.graphql';
import groupProjectsQuery from '../graphql/group_projects.query.graphql';
import issueCreateMutation from '../graphql/issue_create.mutation.graphql';
-import issueSetLabelsMutation from '../graphql/issue_set_labels.mutation.graphql';
import listsIssuesQuery from '../graphql/lists_issues.query.graphql';
import projectBoardMilestonesQuery from '../graphql/project_board_milestones.query.graphql';
@@ -373,7 +373,6 @@ export default {
commit(types.REQUEST_ITEMS_FOR_LIST, { listId, fetchNext });
const { fullPath, fullBoardId, boardType, filterParams } = state;
-
const variables = {
fullPath,
boardId: fullBoardId,
@@ -503,9 +502,10 @@ export default {
updateIssueOrder: async ({ commit, dispatch, state }, { moveData, mutationVariables = {} }) => {
try {
- const { itemId, fromListId, toListId, moveBeforeId, moveAfterId } = moveData;
+ const { itemId, fromListId, toListId, moveBeforeId, moveAfterId, itemNotInToList } = moveData;
const {
fullBoardId,
+ filterParams,
boardItems: {
[itemId]: { iid, referencePath },
},
@@ -524,6 +524,67 @@ export default {
// 'mutationVariables' allows EE code to pass in extra parameters.
...mutationVariables,
},
+ update(
+ cache,
+ {
+ data: {
+ issueMoveList: {
+ issue: { weight },
+ },
+ },
+ },
+ ) {
+ if (fromListId === toListId) return;
+
+ const updateFromList = () => {
+ const fromList = cache.readQuery({
+ query: totalCountAndWeightQuery,
+ variables: { id: fromListId, filters: filterParams },
+ });
+
+ const updatedFromList = {
+ boardList: {
+ __typename: 'BoardList',
+ id: fromList.boardList.id,
+ issuesCount: fromList.boardList.issuesCount - 1,
+ totalWeight: fromList.boardList.totalWeight - Number(weight),
+ },
+ };
+
+ cache.writeQuery({
+ query: totalCountAndWeightQuery,
+ variables: { id: fromListId, filters: filterParams },
+ data: updatedFromList,
+ });
+ };
+
+ const updateToList = () => {
+ if (!itemNotInToList) return;
+
+ const toList = cache.readQuery({
+ query: totalCountAndWeightQuery,
+ variables: { id: toListId, filters: filterParams },
+ });
+
+ const updatedToList = {
+ boardList: {
+ __typename: 'BoardList',
+ id: toList.boardList.id,
+ issuesCount: toList.boardList.issuesCount + 1,
+ totalWeight: toList.boardList.totalWeight + Number(weight),
+ },
+ };
+
+ cache.writeQuery({
+ query: totalCountAndWeightQuery,
+ variables: { id: toListId, filters: filterParams },
+ data: updatedToList,
+ });
+ };
+
+ updateFromList();
+ updateToList();
+ },
});
if (data?.issueMoveList?.errors.length || !data.issueMoveList) {
@@ -567,7 +628,7 @@ export default {
},
addListNewIssue: (
- { state: { boardConfig, boardType, fullPath }, dispatch, commit },
+ { state: { boardConfig, boardType, fullPath, filterParams }, dispatch, commit },
{ issueInput, list, placeholderId = `tmp-${new Date().getTime()}` },
) => {
const input = formatIssueInput(issueInput, boardConfig);
@@ -583,6 +644,27 @@ export default {
.mutate({
mutation: issueCreateMutation,
variables: { input },
+ update(cache) {
+ const fromList = cache.readQuery({
+ query: totalCountAndWeightQuery,
+ variables: { id: list.id, filters: filterParams },
+ });
+
+ const updatedList = {
+ boardList: {
+ __typename: 'BoardList',
+ id: fromList.boardList.id,
+ issuesCount: fromList.boardList.issuesCount + 1,
+ totalWeight: fromList.boardList.totalWeight,
+ },
+ };
+
+ cache.writeQuery({
+ query: totalCountAndWeightQuery,
+ variables: { id: list.id, filters: filterParams },
+ data: updatedList,
+ });
+ },
})
.then(({ data }) => {
if (data.createIssue.errors.length) {
@@ -610,33 +692,6 @@ export default {
setActiveIssueLabels: async ({ commit, getters }, input) => {
const { activeBoardItem } = getters;
- if (!gon.features?.labelsWidget) {
- const { data } = await gqlClient.mutate({
- mutation: issueSetLabelsMutation,
- variables: {
- input: {
- iid: input.iid || String(activeBoardItem.iid),
- labelIds: input.labelsId ?? undefined,
- addLabelIds: input.addLabelIds ?? [],
- removeLabelIds: input.removeLabelIds ?? [],
- projectPath: input.projectPath,
- },
- },
- });
-
- if (data.updateIssue?.errors?.length > 0) {
- throw new Error(data.updateIssue.errors);
- }
-
- commit(types.UPDATE_BOARD_ITEM_BY_ID, {
- itemId: data.updateIssue?.issue?.id || activeBoardItem.id,
- prop: 'labels',
- value: data.updateIssue?.issue?.labels.nodes,
- });
-
- return;
- }
-
let labels = input?.labels || [];
if (input.removeLabelIds) {
labels = activeBoardItem.labels.filter(
diff --git a/app/assets/javascripts/ci_lint/components/ci_lint.vue b/app/assets/javascripts/ci_lint/components/ci_lint.vue
index bc8a1f05ef5..d541e89756a 100644
--- a/app/assets/javascripts/ci_lint/components/ci_lint.vue
+++ b/app/assets/javascripts/ci_lint/components/ci_lint.vue
@@ -1,7 +1,7 @@
<script>
import { GlButton, GlFormCheckbox, GlIcon, GlLink, GlAlert } from '@gitlab/ui';
import CiLintResults from '~/pipeline_editor/components/lint/ci_lint_results.vue';
-import lintCiMutation from '~/pipeline_editor/graphql/mutations/lint_ci.mutation.graphql';
+import lintCiMutation from '~/pipeline_editor/graphql/mutations/client/lint_ci.mutation.graphql';
import SourceEditor from '~/vue_shared/components/source_editor.vue';
export default {
diff --git a/app/assets/javascripts/ci_settings_pipeline_triggers/components/triggers_list.vue b/app/assets/javascripts/ci_settings_pipeline_triggers/components/triggers_list.vue
index 77ec1f1af47..4ab9b36058d 100644
--- a/app/assets/javascripts/ci_settings_pipeline_triggers/components/triggers_list.vue
+++ b/app/assets/javascripts/ci_settings_pipeline_triggers/components/triggers_list.vue
@@ -3,7 +3,7 @@ import { GlTable, GlButton, GlBadge, GlTooltipDirective } from '@gitlab/ui';
import { s__ } from '~/locale';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
-import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate.vue';
+import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate/tooltip_on_truncate.vue';
import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue';
export default {
diff --git a/app/assets/javascripts/ci_variable_list/components/ci_variable_modal.vue b/app/assets/javascripts/ci_variable_list/components/ci_variable_modal.vue
index 8e527e2bff6..e630ce71bd3 100644
--- a/app/assets/javascripts/ci_variable_list/components/ci_variable_modal.vue
+++ b/app/assets/javascripts/ci_variable_list/components/ci_variable_modal.vue
@@ -17,6 +17,7 @@ import {
import Cookies from 'js-cookie';
import { mapActions, mapState } from 'vuex';
import { __ } from '~/locale';
+import Tracking from '~/tracking';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { mapComputed } from '~/vuex_shared/bindings';
import {
@@ -25,10 +26,14 @@ import {
AWS_TIP_DISMISSED_COOKIE_NAME,
AWS_TIP_MESSAGE,
CONTAINS_VARIABLE_REFERENCE_MESSAGE,
+ EVENT_LABEL,
+ EVENT_ACTION,
} from '../constants';
import CiEnvironmentsDropdown from './ci_environments_dropdown.vue';
import { awsTokens, awsTokenList } from './ci_variable_autocomplete_tokens';
+const trackingMixin = Tracking.mixin({ label: EVENT_LABEL });
+
export default {
modalId: ADD_CI_VARIABLE_MODAL_ID,
tokens: awsTokens,
@@ -51,10 +56,11 @@ export default {
GlModal,
GlSprintf,
},
- mixins: [glFeatureFlagsMixin()],
+ mixins: [glFeatureFlagsMixin(), trackingMixin],
data() {
return {
isTipDismissed: Cookies.get(AWS_TIP_DISMISSED_COOKIE_NAME) === 'true',
+ validationErrorEventProperty: '',
};
},
computed: {
@@ -147,6 +153,14 @@ export default {
return this.variable.secret_value === '' || (this.tokenValidationState && this.maskedState);
},
},
+ watch: {
+ variable: {
+ handler() {
+ this.trackVariableValidationErrors();
+ },
+ deep: true,
+ },
+ },
methods: {
...mapActions([
'addVariable',
@@ -179,6 +193,7 @@ export default {
this.clearModal();
this.resetSelectedEnvironment();
+ this.resetValidationErrorEvents();
},
updateOrAddVariable() {
if (this.variableBeingEdited) {
@@ -193,6 +208,31 @@ export default {
this.setVariableProtected();
}
},
+ trackVariableValidationErrors() {
+ const property = this.getTrackingErrorProperty();
+ if (!this.validationErrorEventProperty && property) {
+ this.track(EVENT_ACTION, { property });
+ this.validationErrorEventProperty = property;
+ }
+ },
+ getTrackingErrorProperty() {
+ let property;
+ if (this.variable.secret_value?.length && !property) {
+ if (this.displayMaskedError && this.maskableRegex?.length) {
+ const supportedChars = this.maskableRegex.replace('^', '').replace(/{(\d,)}\$/, '');
+ const regex = new RegExp(supportedChars, 'g');
+ property = this.variable.secret_value.replace(regex, '');
+ }
+ if (this.containsVariableReference) {
+ property = '$';
+ }
+ }
+
+ return property;
+ },
+ resetValidationErrorEvents() {
+ this.validationErrorEventProperty = '';
+ },
},
};
</script>
diff --git a/app/assets/javascripts/ci_variable_list/components/ci_variable_table.vue b/app/assets/javascripts/ci_variable_list/components/ci_variable_table.vue
index b959d97daea..9c0ffab7f6b 100644
--- a/app/assets/javascripts/ci_variable_list/components/ci_variable_table.vue
+++ b/app/assets/javascripts/ci_variable_list/components/ci_variable_table.vue
@@ -1,5 +1,5 @@
<script>
-import { GlTable, GlButton, GlModalDirective, GlIcon } from '@gitlab/ui';
+import { GlTable, GlButton, GlModalDirective, GlIcon, GlTooltipDirective } from '@gitlab/ui';
import { mapState, mapActions } from 'vuex';
import { s__, __ } from '~/locale';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
@@ -59,6 +59,7 @@ export default {
},
directives: {
GlModalDirective,
+ GlTooltip: GlTooltipDirective,
},
mixins: [glFeatureFlagsMixin()],
computed: {
@@ -102,27 +103,38 @@ export default {
<col v-for="field in scope.fields" :key="field.key" :style="field.customStyle" />
</template>
<template #cell(key)="{ item }">
- <div class="d-flex truncated-container">
- <span :id="`ci-variable-key-${item.id}`" class="d-inline-block mw-100 text-truncate">{{
- item.key
- }}</span>
- <ci-variable-popover
- :target="`ci-variable-key-${item.id}`"
- :value="item.key"
- :tooltip-text="__('Copy key')"
+ <div class="gl-display-flex truncated-container gl-align-items-center">
+ <span
+ :id="`ci-variable-key-${item.id}`"
+ class="gl-display-inline-block gl-max-w-full gl-text-truncate"
+ >{{ item.key }}</span
+ >
+ <gl-button
+ v-gl-tooltip
+ category="tertiary"
+ icon="copy-to-clipboard"
+ :title="__('Copy key')"
+ :data-clipboard-text="item.key"
+ :aria-label="__('Copy to clipboard')"
/>
</div>
</template>
<template #cell(value)="{ item }">
- <span v-if="valuesHidden">*********************</span>
- <div v-else class="d-flex truncated-container">
- <span :id="`ci-variable-value-${item.id}`" class="d-inline-block mw-100 text-truncate">{{
- item.value
- }}</span>
- <ci-variable-popover
- :target="`ci-variable-value-${item.id}`"
- :value="item.value"
- :tooltip-text="__('Copy value')"
+ <div class="gl-display-flex gl-align-items-center truncated-container">
+ <span v-if="valuesHidden">*********************</span>
+ <span
+ v-else
+ :id="`ci-variable-value-${item.id}`"
+ class="gl-display-inline-block gl-max-w-full gl-text-truncate"
+ >{{ item.value }}</span
+ >
+ <gl-button
+ v-gl-tooltip
+ category="tertiary"
+ icon="copy-to-clipboard"
+ :title="__('Copy value')"
+ :data-clipboard-text="item.value"
+ :aria-label="__('Copy to clipboard')"
/>
</div>
</template>
diff --git a/app/assets/javascripts/ci_variable_list/constants.js b/app/assets/javascripts/ci_variable_list/constants.js
index 4ebbf05814b..663a912883b 100644
--- a/app/assets/javascripts/ci_variable_list/constants.js
+++ b/app/assets/javascripts/ci_variable_list/constants.js
@@ -19,6 +19,9 @@ export const AWS_TIP_MESSAGE = __(
'%{deployLinkStart}Use a template to deploy to ECS%{deployLinkEnd}, or use a docker image to %{commandsLinkStart}run AWS commands in GitLab CI/CD%{commandsLinkEnd}.',
);
+export const EVENT_LABEL = 'ci_variable_modal';
+export const EVENT_ACTION = 'validation_error';
+
// AWS TOKEN CONSTANTS
export const AWS_ACCESS_KEY_ID = 'AWS_ACCESS_KEY_ID';
export const AWS_DEFAULT_REGION = 'AWS_DEFAULT_REGION';
diff --git a/app/assets/javascripts/clusters/agents/components/activity_events_list.vue b/app/assets/javascripts/clusters/agents/components/activity_events_list.vue
new file mode 100644
index 00000000000..6567ce203bc
--- /dev/null
+++ b/app/assets/javascripts/clusters/agents/components/activity_events_list.vue
@@ -0,0 +1,176 @@
+<script>
+import {
+ GlLoadingIcon,
+ GlEmptyState,
+ GlLink,
+ GlIcon,
+ GlAlert,
+ GlTooltipDirective,
+} from '@gitlab/ui';
+import { helpPagePath } from '~/helpers/help_page_helper';
+import { n__, s__, __ } from '~/locale';
+import { formatDate, getDayDifference, isToday } from '~/lib/utils/datetime_utility';
+import { EVENTS_STORED_DAYS } from '../constants';
+import getAgentActivityEventsQuery from '../graphql/queries/get_agent_activity_events.query.graphql';
+import ActivityHistoryItem from './activity_history_item.vue';
+
+export default {
+ components: {
+ GlLoadingIcon,
+ GlEmptyState,
+ GlAlert,
+ GlLink,
+ GlIcon,
+ ActivityHistoryItem,
+ },
+ directives: {
+ GlTooltip: GlTooltipDirective,
+ },
+ i18n: {
+ emptyText: s__(
+ 'ClusterAgents|See Agent activity updates such as tokens created or revoked and clusters connected or not connected.',
+ ),
+ emptyTooltip: s__('ClusterAgents|What is GitLab Agent activity?'),
+ error: s__(
+ 'ClusterAgents|An error occurred while retrieving GitLab Agent activity. Reload the page to try again.',
+ ),
+ today: __('Today'),
+ yesterday: __('Yesterday'),
+ },
+ emptyHelpLink: helpPagePath('user/clusters/agent/install/index', {
+ anchor: 'view-agent-activity',
+ }),
+ borderClasses: 'gl-border-b-1 gl-border-b-solid gl-border-b-gray-100',
+ apollo: {
+ agentEvents: {
+ query: getAgentActivityEventsQuery,
+ variables() {
+ return {
+ agentName: this.agentName,
+ projectPath: this.projectPath,
+ };
+ },
+ update: (data) => data?.project?.clusterAgent?.activityEvents?.nodes,
+ error() {
+ this.isError = true;
+ },
+ },
+ },
+ inject: ['agentName', 'projectPath', 'activityEmptyStateImage'],
+ data() {
+ return {
+ isError: false,
+ };
+ },
+ computed: {
+ isLoading() {
+ return this.$apollo.queries.agentEvents?.loading;
+ },
+ emptyStateTitle() {
+ return n__(
+ "ClusterAgents|There's no activity from the past day",
+ "ClusterAgents|There's no activity from the past %d days",
+ EVENTS_STORED_DAYS,
+ );
+ },
+ eventsList() {
+ const list = this.agentEvents;
+ const listByDates = {};
+
+ if (!list?.length) {
+ return listByDates;
+ }
+
+ list.forEach((event) => {
+ const dateName = this.getFormattedDate(event.recordedAt);
+ if (!listByDates[dateName]) {
+ listByDates[dateName] = [];
+ }
+ listByDates[dateName].push(event);
+ });
+
+ return listByDates;
+ },
+ hasEvents() {
+ return Object.keys(this.eventsList).length;
+ },
+ },
+ methods: {
+ isYesterday(date) {
+ const today = new Date();
+ return getDayDifference(today, date) === -1;
+ },
+ getFormattedDate(dateString) {
+ const date = new Date(dateString);
+ let dateName;
+ if (isToday(date)) {
+ dateName = this.$options.i18n.today;
+ } else if (this.isYesterday(date)) {
+ dateName = this.$options.i18n.yesterday;
+ } else {
+ dateName = formatDate(date, 'yyyy-mm-dd');
+ }
+ return dateName;
+ },
+ isLast(dateEvents, idx) {
+ return idx === dateEvents.length - 1;
+ },
+ getBodyClasses(dateEvents, idx) {
+ return !this.isLast(dateEvents, idx) ? this.$options.borderClasses : '';
+ },
+ },
+};
+</script>
+
+<template>
+ <div>
+ <gl-loading-icon v-if="isLoading" size="md" />
+
+ <div v-else-if="hasEvents">
+ <div
+ v-for="(dateEvents, key) in eventsList"
+ :key="key"
+ class="agent-activity-list issuable-discussion"
+ >
+ <h4
+ class="gl-pb-4 gl-ml-5"
+ :class="$options.borderClasses"
+ data-testid="activity-section-title"
+ >
+ {{ key }}
+ </h4>
+
+ <ul class="notes main-notes-list timeline">
+ <activity-history-item
+ v-for="(event, idx) in dateEvents"
+ :key="idx"
+ :event="event"
+ :body-class="getBodyClasses(dateEvents, idx)"
+ />
+ </ul>
+ </div>
+ </div>
+
+ <gl-alert v-else-if="isError" variant="danger" :dismissible="false" class="gl-mt-3">
+ {{ $options.i18n.error }}
+ </gl-alert>
+
+ <gl-empty-state
+ v-else
+ :title="emptyStateTitle"
+ :svg-path="activityEmptyStateImage"
+ :svg-height="150"
+ >
+ <template #description
+ >{{ $options.i18n.emptyText }}
+ <gl-link
+ v-gl-tooltip
+ :href="$options.emptyHelpLink"
+ :title="$options.i18n.emptyTooltip"
+ :aria-label="$options.i18n.emptyTooltip"
+ ><gl-icon name="question" :size="14"
+ /></gl-link>
+ </template>
+ </gl-empty-state>
+ </div>
+</template>
diff --git a/app/assets/javascripts/clusters/agents/components/activity_history_item.vue b/app/assets/javascripts/clusters/agents/components/activity_history_item.vue
new file mode 100644
index 00000000000..7792d89a575
--- /dev/null
+++ b/app/assets/javascripts/clusters/agents/components/activity_history_item.vue
@@ -0,0 +1,79 @@
+<script>
+import { GlLink, GlIcon, GlSprintf } from '@gitlab/ui';
+import { s__ } from '~/locale';
+import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
+import HistoryItem from '~/vue_shared/components/registry/history_item.vue';
+import { EVENT_DETAILS, DEFAULT_ICON } from '../constants';
+
+export default {
+ i18n: {
+ defaultBodyText: s__('ClusterAgents|Event occurred'),
+ },
+ components: {
+ GlLink,
+ GlIcon,
+ GlSprintf,
+ TimeAgoTooltip,
+ HistoryItem,
+ },
+ props: {
+ event: {
+ required: true,
+ type: Object,
+ },
+ bodyClass: {
+ required: false,
+ default: '',
+ type: String,
+ },
+ },
+ computed: {
+ eventDetails() {
+ const defaultEvent = {
+ eventTypeIcon: DEFAULT_ICON,
+ title: this.event.kind,
+ body: this.$options.i18n.defaultBodyText,
+ };
+
+ const eventDetails = EVENT_DETAILS[this.event.kind] || defaultEvent;
+ const { eventTypeIcon, title, body, titleIcon } = eventDetails;
+ const resultEvent = { ...this.event, eventTypeIcon, title, body, titleIcon };
+
+ return resultEvent;
+ },
+ },
+};
+</script>
+<template>
+ <history-item :icon="eventDetails.eventTypeIcon" class="gl-my-0! gl-pr-0!">
+ <strong>
+ <gl-sprintf :message="eventDetails.title"
+ ><template v-if="eventDetails.titleIcon" #titleIcon
+ ><gl-icon
+ class="gl-mr-2"
+ :name="eventDetails.titleIcon.name"
+ :size="12"
+ :class="eventDetails.titleIcon.class"
+ />
+ </template>
+ <template #tokenName>{{ eventDetails.agentToken.name }}</template></gl-sprintf
+ >
+ </strong>
+
+ <template #body>
+ <p class="gl-mt-2 gl-mb-0 gl-pb-2" :class="bodyClass">
+ <gl-sprintf :message="eventDetails.body">
+ <template #userName>
+ <span class="gl-font-weight-bold">{{ eventDetails.user.name }}</span>
+ <gl-link :href="eventDetails.user.webUrl">@{{ eventDetails.user.username }}</gl-link>
+ </template>
+
+ <template #strong="{ content }">
+ <span class="gl-font-weight-bold"> {{ content }} </span>
+ </template>
+ </gl-sprintf>
+ <time-ago-tooltip :time="eventDetails.recordedAt" />
+ </p>
+ </template>
+ </history-item>
+</template>
diff --git a/app/assets/javascripts/clusters/agents/components/show.vue b/app/assets/javascripts/clusters/agents/components/show.vue
index afbba9d1f7c..9109c010500 100644
--- a/app/assets/javascripts/clusters/agents/components/show.vue
+++ b/app/assets/javascripts/clusters/agents/components/show.vue
@@ -8,11 +8,12 @@ import {
GlTab,
GlTabs,
} from '@gitlab/ui';
-import { s__ } from '~/locale';
+import { s__, __ } from '~/locale';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
import { MAX_LIST_COUNT } from '../constants';
import getClusterAgentQuery from '../graphql/queries/get_cluster_agent.query.graphql';
import TokenTable from './token_table.vue';
+import ActivityEvents from './activity_events_list.vue';
export default {
i18n: {
@@ -20,6 +21,7 @@ export default {
loadingError: s__('ClusterAgents|An error occurred while loading your agent'),
tokens: s__('ClusterAgents|Access tokens'),
unknownUser: s__('ClusterAgents|Unknown user'),
+ activity: __('Activity'),
},
apollo: {
clusterAgent: {
@@ -47,6 +49,7 @@ export default {
GlTabs,
TimeAgoTooltip,
TokenTable,
+ ActivityEvents,
},
props: {
agentName: {
@@ -127,9 +130,14 @@ export default {
</gl-sprintf>
</p>
- <gl-tabs>
+ <gl-tabs sync-active-tab-with-query-params lazy>
+ <gl-tab :title="$options.i18n.activity" query-param-value="activity">
+ <activity-events :agent-name="agentName" :project-path="projectPath" />
+ </gl-tab>
+
<slot name="ee-security-tab"></slot>
- <gl-tab>
+
+ <gl-tab query-param-value="tokens">
<template #title>
<span data-testid="cluster-agent-token-count">
{{ $options.i18n.tokens }}
@@ -143,7 +151,7 @@ export default {
<gl-loading-icon v-if="isLoading" size="md" class="gl-m-3" />
<div v-else>
- <TokenTable :tokens="tokens" />
+ <token-table :tokens="tokens" />
<div v-if="showPagination" class="gl-display-flex gl-justify-content-center gl-mt-5">
<gl-keyset-pagination v-bind="tokenPageInfo" @prev="prevPage" @next="nextPage" />
diff --git a/app/assets/javascripts/clusters/agents/components/token_table.vue b/app/assets/javascripts/clusters/agents/components/token_table.vue
index 70ed2566134..019fac531d1 100644
--- a/app/assets/javascripts/clusters/agents/components/token_table.vue
+++ b/app/assets/javascripts/clusters/agents/components/token_table.vue
@@ -62,8 +62,8 @@ export default {
];
},
learnMoreUrl() {
- return helpPagePath('user/clusters/agent/index.md', {
- anchor: 'create-an-agent-record-in-gitlab',
+ return helpPagePath('user/clusters/agent/install/index', {
+ anchor: 'register-an-agent-with-gitlab',
});
},
},
@@ -83,7 +83,14 @@ export default {
</gl-link>
</div>
- <gl-table :items="tokens" :fields="fields" fixed stacked="md">
+ <gl-table
+ :items="tokens"
+ :fields="fields"
+ fixed
+ stacked="md"
+ head-variant="white"
+ thead-class="gl-border-b-solid gl-border-b-2 gl-border-b-gray-100"
+ >
<template #cell(lastUsed)="{ item }">
<time-ago-tooltip v-if="item.lastUsedAt" :time="item.lastUsedAt" />
<span v-else>{{ $options.i18n.neverUsed }}</span>
diff --git a/app/assets/javascripts/clusters/agents/constants.js b/app/assets/javascripts/clusters/agents/constants.js
index bbc4630f83b..315c7662755 100644
--- a/app/assets/javascripts/clusters/agents/constants.js
+++ b/app/assets/javascripts/clusters/agents/constants.js
@@ -1 +1,38 @@
+import { s__ } from '~/locale';
+
export const MAX_LIST_COUNT = 25;
+
+export const EVENTS_STORED_DAYS = 7;
+
+export const EVENT_DETAILS = {
+ token_created: {
+ eventTypeIcon: 'token',
+ title: s__('ClusterAgents|%{tokenName} created'),
+ body: s__('ClusterAgents|Token created by %{userName}'),
+ },
+ token_revoked: {
+ eventTypeIcon: 'token',
+ title: s__('ClusterAgents|%{tokenName} revoked'),
+ body: s__('ClusterAgents|Token revoked by %{userName}'),
+ },
+ agent_connected: {
+ eventTypeIcon: 'connected',
+ title: s__('ClusterAgents|%{titleIcon}Connected'),
+ body: s__('ClusterAgents|Agent %{strongStart}connected%{strongEnd}'),
+ titleIcon: {
+ name: 'status-success',
+ class: 'text-success-500',
+ },
+ },
+ agent_disconnected: {
+ eventTypeIcon: 'connected',
+ title: s__('ClusterAgents|%{titleIcon}Not connected'),
+ body: s__('ClusterAgents|Agent %{strongStart}disconnected%{strongEnd}'),
+ titleIcon: {
+ name: 'severity-critical',
+ class: 'text-danger-800',
+ },
+ },
+};
+
+export const DEFAULT_ICON = 'token';
diff --git a/app/assets/javascripts/clusters/agents/graphql/fragments/cluster_agent_token.fragment.graphql b/app/assets/javascripts/clusters/agents/graphql/fragments/cluster_agent_token.fragment.graphql
index 1e9187e8ad1..7deb057ede9 100644
--- a/app/assets/javascripts/clusters/agents/graphql/fragments/cluster_agent_token.fragment.graphql
+++ b/app/assets/javascripts/clusters/agents/graphql/fragments/cluster_agent_token.fragment.graphql
@@ -4,8 +4,8 @@ fragment Token on ClusterAgentToken {
description
lastUsedAt
name
-
createdByUser {
+ id
name
}
}
diff --git a/app/assets/javascripts/clusters/agents/graphql/queries/get_agent_activity_events.query.graphql b/app/assets/javascripts/clusters/agents/graphql/queries/get_agent_activity_events.query.graphql
new file mode 100644
index 00000000000..0d7ff029387
--- /dev/null
+++ b/app/assets/javascripts/clusters/agents/graphql/queries/get_agent_activity_events.query.graphql
@@ -0,0 +1,25 @@
+query getAgentActivityEvents($projectPath: ID!, $agentName: String!) {
+ project(fullPath: $projectPath) {
+ id
+ clusterAgent(name: $agentName) {
+ id
+ activityEvents {
+ nodes {
+ kind
+ level
+ recordedAt
+ agentToken {
+ id
+ name
+ }
+ user {
+ id
+ name
+ username
+ webUrl
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/app/assets/javascripts/clusters/agents/graphql/queries/get_cluster_agent.query.graphql b/app/assets/javascripts/clusters/agents/graphql/queries/get_cluster_agent.query.graphql
index d01db8f0a6a..3662e925261 100644
--- a/app/assets/javascripts/clusters/agents/graphql/queries/get_cluster_agent.query.graphql
+++ b/app/assets/javascripts/clusters/agents/graphql/queries/get_cluster_agent.query.graphql
@@ -10,11 +10,13 @@ query getClusterAgent(
$beforeToken: String
) {
project(fullPath: $projectPath) {
+ id
clusterAgent(name: $agentName) {
id
createdAt
createdByUser {
+ id
name
}
diff --git a/app/assets/javascripts/clusters/agents/index.js b/app/assets/javascripts/clusters/agents/index.js
index 426d8d83847..5796c9e308d 100644
--- a/app/assets/javascripts/clusters/agents/index.js
+++ b/app/assets/javascripts/clusters/agents/index.js
@@ -13,11 +13,12 @@ export default () => {
}
const defaultClient = createDefaultClient();
- const { agentName, projectPath } = el.dataset;
+ const { agentName, projectPath, activityEmptyStateImage } = el.dataset;
return new Vue({
el,
apolloProvider: new VueApollo({ defaultClient }),
+ provide: { agentName, projectPath, activityEmptyStateImage },
render(createElement) {
return createElement(AgentShowPage, {
props: {
diff --git a/app/assets/javascripts/clusters_list/components/agent_empty_state.vue b/app/assets/javascripts/clusters_list/components/agent_empty_state.vue
index af44a23b4b3..f54f7b11414 100644
--- a/app/assets/javascripts/clusters_list/components/agent_empty_state.vue
+++ b/app/assets/javascripts/clusters_list/components/agent_empty_state.vue
@@ -1,107 +1,54 @@
<script>
-import { GlButton, GlEmptyState, GlLink, GlSprintf, GlAlert, GlModalDirective } from '@gitlab/ui';
+import { GlButton, GlEmptyState, GlLink, GlSprintf, GlModalDirective } from '@gitlab/ui';
import { helpPagePath } from '~/helpers/help_page_helper';
import { INSTALL_AGENT_MODAL_ID, I18N_AGENTS_EMPTY_STATE } from '../constants';
export default {
i18n: I18N_AGENTS_EMPTY_STATE,
modalId: INSTALL_AGENT_MODAL_ID,
- multipleClustersDocsUrl: helpPagePath('user/project/clusters/multiple_kubernetes_clusters'),
- installDocsUrl: helpPagePath('administration/clusters/kas'),
- getStartedDocsUrl: helpPagePath('user/clusters/agent/index', {
- anchor: 'define-a-configuration-repository',
- }),
+ agentDocsUrl: helpPagePath('user/clusters/agent/index'),
components: {
GlButton,
GlEmptyState,
GlLink,
GlSprintf,
- GlAlert,
},
directives: {
GlModalDirective,
},
- inject: ['emptyStateImage', 'projectPath'],
+ inject: ['emptyStateImage'],
props: {
- hasConfigurations: {
- type: Boolean,
- required: true,
- },
isChildComponent: {
default: false,
required: false,
type: Boolean,
},
},
- computed: {
- repositoryPath() {
- return `/${this.projectPath}`;
- },
- },
};
</script>
<template>
<gl-empty-state :svg-path="emptyStateImage" title="" class="agents-empty-state">
<template #description>
- <p class="mw-460 gl-mx-auto gl-text-left">
- {{ $options.i18n.introText }}
- </p>
- <p class="mw-460 gl-mx-auto gl-text-left">
- <gl-sprintf :message="$options.i18n.multipleClustersText">
+ <p class="gl-text-left">
+ <gl-sprintf :message="$options.i18n.introText">
<template #link="{ content }">
- <gl-link
- :href="$options.multipleClustersDocsUrl"
- target="_blank"
- data-testid="multiple-clusters-docs-link"
- >
+ <gl-link :href="$options.agentDocsUrl">
{{ content }}
</gl-link>
</template>
</gl-sprintf>
</p>
-
- <p class="mw-460 gl-mx-auto">
- <gl-link :href="$options.installDocsUrl" target="_blank" data-testid="install-docs-link">
- {{ $options.i18n.learnMoreText }}
- </gl-link>
- </p>
-
- <gl-alert
- v-if="!hasConfigurations"
- variant="warning"
- class="gl-mb-5 text-left"
- :dismissible="false"
- >
- {{ $options.i18n.warningText }}
-
- <template #actions>
- <gl-button
- category="primary"
- variant="info"
- :href="$options.getStartedDocsUrl"
- target="_blank"
- class="gl-ml-0!"
- >
- {{ $options.i18n.readMoreText }}
- </gl-button>
- <gl-button category="secondary" variant="info" :href="repositoryPath">
- {{ $options.i18n.repositoryButtonText }}
- </gl-button>
- </template>
- </gl-alert>
</template>
<template #actions>
<gl-button
v-if="!isChildComponent"
v-gl-modal-directive="$options.modalId"
- :disabled="!hasConfigurations"
- data-testid="integration-primary-button"
category="primary"
variant="confirm"
>
- {{ $options.i18n.primaryButtonText }}
+ {{ $options.i18n.buttonText }}
</gl-button>
</template>
</gl-empty-state>
diff --git a/app/assets/javascripts/clusters_list/components/agents.vue b/app/assets/javascripts/clusters_list/components/agents.vue
index fb5cf7d1206..45108a28e37 100644
--- a/app/assets/javascripts/clusters_list/components/agents.vue
+++ b/app/assets/javascripts/clusters_list/components/agents.vue
@@ -86,9 +86,6 @@ export default {
treePageInfo() {
return this.agents?.project?.repository?.tree?.trees?.pageInfo || {};
},
- hasConfigurations() {
- return Boolean(this.agents?.project?.repository?.tree?.trees?.nodes?.length);
- },
},
methods: {
reloadAgents() {
@@ -161,11 +158,7 @@ export default {
</div>
</div>
- <agent-empty-state
- v-else
- :has-configurations="hasConfigurations"
- :is-child-component="isChildComponent"
- />
+ <agent-empty-state v-else :is-child-component="isChildComponent" />
</section>
<gl-alert v-else variant="danger" :dismissible="false">
diff --git a/app/assets/javascripts/clusters_list/components/available_agents_dropdown.vue b/app/assets/javascripts/clusters_list/components/available_agents_dropdown.vue
index 9fb020d2f4f..1630d0d5c92 100644
--- a/app/assets/javascripts/clusters_list/components/available_agents_dropdown.vue
+++ b/app/assets/javascripts/clusters_list/components/available_agents_dropdown.vue
@@ -1,7 +1,6 @@
<script>
import { GlDropdown, GlDropdownItem } from '@gitlab/ui';
import { I18N_AVAILABLE_AGENTS_DROPDOWN } from '../constants';
-import agentConfigurations from '../graphql/queries/agent_configurations.query.graphql';
export default {
name: 'AvailableAgentsDropdown',
@@ -10,36 +9,22 @@ export default {
GlDropdown,
GlDropdownItem,
},
- inject: ['projectPath'],
props: {
isRegistering: {
required: true,
type: Boolean,
},
- },
- apollo: {
- agents: {
- query: agentConfigurations,
- variables() {
- return {
- projectPath: this.projectPath,
- };
- },
- update(data) {
- this.populateAvailableAgents(data);
- },
+ availableAgents: {
+ required: true,
+ type: Array,
},
},
data() {
return {
- availableAgents: [],
selectedAgent: null,
};
},
computed: {
- isLoading() {
- return this.$apollo.queries.agents.loading;
- },
dropdownText() {
if (this.isRegistering) {
return this.$options.i18n.registeringAgent;
@@ -58,18 +43,11 @@ export default {
isSelected(agent) {
return this.selectedAgent === agent;
},
- populateAvailableAgents(data) {
- const installedAgents = data?.project?.clusterAgents?.nodes.map((agent) => agent.name) ?? [];
- const configuredAgents =
- data?.project?.agentConfigurations?.nodes.map((config) => config.agentName) ?? [];
-
- this.availableAgents = configuredAgents.filter((agent) => !installedAgents.includes(agent));
- },
},
};
</script>
<template>
- <gl-dropdown :text="dropdownText" :loading="isLoading || isRegistering">
+ <gl-dropdown :text="dropdownText" :loading="isRegistering">
<gl-dropdown-item
v-for="agent in availableAgents"
:key="agent"
diff --git a/app/assets/javascripts/clusters_list/components/clusters_empty_state.vue b/app/assets/javascripts/clusters_list/components/clusters_empty_state.vue
index 3879af6e9cb..ce601de57bd 100644
--- a/app/assets/javascripts/clusters_list/components/clusters_empty_state.vue
+++ b/app/assets/javascripts/clusters_list/components/clusters_empty_state.vue
@@ -1,5 +1,5 @@
<script>
-import { GlEmptyState, GlButton, GlLink, GlSprintf } from '@gitlab/ui';
+import { GlEmptyState, GlButton, GlLink, GlSprintf, GlAlert } from '@gitlab/ui';
import { mapState } from 'vuex';
import { helpPagePath } from '~/helpers/help_page_helper';
import { I18N_CLUSTERS_EMPTY_STATE } from '../constants';
@@ -11,6 +11,7 @@ export default {
GlButton,
GlLink,
GlSprintf,
+ GlAlert,
},
inject: ['emptyStateHelpText', 'clustersEmptyStateImage', 'newClusterPath'],
props: {
@@ -20,8 +21,11 @@ export default {
type: Boolean,
},
},
- learnMoreHelpUrl: helpPagePath('user/project/clusters/index'),
- multipleClustersHelpUrl: helpPagePath('user/project/clusters/multiple_kubernetes_clusters'),
+ clustersHelpUrl: helpPagePath('user/infrastructure/clusters/index', {
+ anchor: 'certificate-based-kubernetes-integration-deprecated',
+ }),
+ blogPostUrl:
+ 'https://about.gitlab.com/blog/2021/11/15/deprecating-the-cert-based-kubernetes-integration/',
computed: {
...mapState(['canAddCluster']),
},
@@ -29,48 +33,45 @@ export default {
</script>
<template>
- <gl-empty-state :svg-path="clustersEmptyStateImage" title="">
- <template #description>
- <p class="gl-text-left">
- {{ $options.i18n.description }}
- </p>
- <p class="gl-text-left">
- <gl-sprintf :message="$options.i18n.multipleClustersText">
- <template #link="{ content }">
- <gl-link
- :href="$options.multipleClustersHelpUrl"
- target="_blank"
- data-testid="multiple-clusters-docs-link"
- >
- {{ content }}
- </gl-link>
- </template>
- </gl-sprintf>
- </p>
+ <div>
+ <gl-empty-state :svg-path="clustersEmptyStateImage" title="">
+ <template #description>
+ <p class="gl-text-left">
+ <gl-sprintf :message="$options.i18n.introText">
+ <template #link="{ content }">
+ <gl-link :href="$options.clustersHelpUrl">{{ content }}</gl-link>
+ </template>
+ </gl-sprintf>
+ </p>
- <p v-if="emptyStateHelpText" data-testid="clusters-empty-state-text">
- {{ emptyStateHelpText }}
- </p>
+ <p v-if="emptyStateHelpText" data-testid="clusters-empty-state-text">
+ {{ emptyStateHelpText }}
+ </p>
+ </template>
- <p>
- <gl-link :href="$options.learnMoreHelpUrl" target="_blank" data-testid="clusters-docs-link">
- {{ $options.i18n.learnMoreLinkText }}
- </gl-link>
- </p>
- </template>
+ <template #actions>
+ <gl-button
+ v-if="!isChildComponent"
+ data-testid="integration-primary-button"
+ data-qa-selector="add_kubernetes_cluster_link"
+ category="primary"
+ variant="confirm"
+ :disabled="!canAddCluster"
+ :href="newClusterPath"
+ >
+ {{ $options.i18n.buttonText }}
+ </gl-button>
+ </template>
+ </gl-empty-state>
- <template #actions>
- <gl-button
- v-if="!isChildComponent"
- data-testid="integration-primary-button"
- data-qa-selector="add_kubernetes_cluster_link"
- category="primary"
- variant="confirm"
- :disabled="!canAddCluster"
- :href="newClusterPath"
- >
- {{ $options.i18n.buttonText }}
- </gl-button>
- </template>
- </gl-empty-state>
+ <gl-alert variant="warning" :dismissible="false">
+ <gl-sprintf :message="$options.i18n.alertText">
+ <template #link="{ content }">
+ <gl-link :href="$options.blogPostUrl" target="_blank">
+ {{ content }}
+ </gl-link>
+ </template>
+ </gl-sprintf>
+ </gl-alert>
+ </div>
</template>
diff --git a/app/assets/javascripts/clusters_list/components/clusters_main_view.vue b/app/assets/javascripts/clusters_list/components/clusters_main_view.vue
index 9e03093aa67..7dd5ece9b8e 100644
--- a/app/assets/javascripts/clusters_list/components/clusters_main_view.vue
+++ b/app/assets/javascripts/clusters_list/components/clusters_main_view.vue
@@ -1,12 +1,22 @@
<script>
import { GlTabs, GlTab } from '@gitlab/ui';
-import { CLUSTERS_TABS, MAX_CLUSTERS_LIST, MAX_LIST_COUNT, AGENT } from '../constants';
+import Tracking from '~/tracking';
+import {
+ CLUSTERS_TABS,
+ MAX_CLUSTERS_LIST,
+ MAX_LIST_COUNT,
+ AGENT,
+ EVENT_LABEL_TABS,
+ EVENT_ACTIONS_CHANGE,
+} from '../constants';
import Agents from './agents.vue';
import InstallAgentModal from './install_agent_modal.vue';
import ClustersActions from './clusters_actions.vue';
import Clusters from './clusters.vue';
import ClustersViewAll from './clusters_view_all.vue';
+const trackingMixin = Tracking.mixin({ label: EVENT_LABEL_TABS });
+
export default {
components: {
GlTabs,
@@ -18,6 +28,7 @@ export default {
InstallAgentModal,
},
CLUSTERS_TABS,
+ mixins: [trackingMixin],
props: {
defaultBranchName: {
default: '.noBranch',
@@ -34,9 +45,12 @@ export default {
methods: {
onTabChange(tabName) {
this.selectedTabIndex = CLUSTERS_TABS.findIndex((tab) => tab.queryParamValue === tabName);
-
this.maxAgents = tabName === AGENT ? MAX_LIST_COUNT : MAX_CLUSTERS_LIST;
},
+ trackTabChange(tab) {
+ const tabName = CLUSTERS_TABS[tab].queryParamValue;
+ this.track(EVENT_ACTIONS_CHANGE, { property: tabName });
+ },
},
};
</script>
@@ -47,6 +61,7 @@ export default {
sync-active-tab-with-query-params
nav-class="gl-flex-grow-1 gl-align-items-center"
lazy
+ @input="trackTabChange"
>
<gl-tab
v-for="(tab, idx) in $options.CLUSTERS_TABS"
diff --git a/app/assets/javascripts/clusters_list/components/clusters_view_all.vue b/app/assets/javascripts/clusters_list/components/clusters_view_all.vue
index 285876e57d8..0e312d21e4e 100644
--- a/app/assets/javascripts/clusters_list/components/clusters_view_all.vue
+++ b/app/assets/javascripts/clusters_list/components/clusters_view_all.vue
@@ -34,10 +34,12 @@ export default {
directives: {
GlModalDirective,
},
- AGENT_CARD_INFO,
- CERTIFICATE_BASED_CARD_INFO,
MAX_CLUSTERS_LIST,
INSTALL_AGENT_MODAL_ID,
+ i18n: {
+ agent: AGENT_CARD_INFO,
+ certificate: CERTIFICATE_BASED_CARD_INFO,
+ },
inject: ['addClusterPath'],
props: {
defaultBranchName: {
@@ -122,21 +124,21 @@ export default {
</gl-sprintf>
</h3>
- <gl-badge id="clusters-recommended-badge" size="md" variant="info">{{
- $options.AGENT_CARD_INFO.tooltip.label
+ <gl-badge id="clusters-recommended-badge" variant="info">{{
+ $options.i18n.agent.tooltip.label
}}</gl-badge>
<gl-popover
target="clusters-recommended-badge"
container="viewport"
placement="bottom"
- :title="$options.AGENT_CARD_INFO.tooltip.title"
+ :title="$options.i18n.agent.tooltip.title"
>
<p class="gl-mb-0">
- <gl-sprintf :message="$options.AGENT_CARD_INFO.tooltip.text">
+ <gl-sprintf :message="$options.i18n.agent.tooltip.text">
<template #link="{ content }">
<gl-link
- :href="$options.AGENT_CARD_INFO.tooltip.link"
+ :href="$options.i18n.agent.tooltip.link"
target="_blank"
class="gl-font-sm"
>
@@ -159,9 +161,9 @@ export default {
<gl-link
v-if="totalAgents"
data-testid="agents-tab-footer-link"
- :href="`?tab=${$options.AGENT_CARD_INFO.tabName}`"
- @click="changeTab($event, $options.AGENT_CARD_INFO.tabName)"
- ><gl-sprintf :message="$options.AGENT_CARD_INFO.footerText"
+ :href="`?tab=${$options.i18n.agent.tabName}`"
+ @click="changeTab($event, $options.i18n.agent.tabName)"
+ ><gl-sprintf :message="$options.i18n.agent.footerText"
><template #number>{{ cardFooterNumber(totalAgents) }}</template></gl-sprintf
></gl-link
><gl-button
@@ -169,7 +171,7 @@ export default {
class="gl-ml-4"
category="secondary"
variant="confirm"
- >{{ $options.AGENT_CARD_INFO.actionText }}</gl-button
+ >{{ $options.i18n.agent.actionText }}</gl-button
>
</template>
</gl-card>
@@ -190,6 +192,7 @@ export default {
<template #total>{{ clustersCardTitle.total }}</template>
</gl-sprintf>
</h3>
+ <gl-badge variant="warning">{{ $options.i18n.certificate.badgeText }}</gl-badge>
</template>
<clusters :limit="$options.MAX_CLUSTERS_LIST" :is-child-component="true" />
@@ -198,9 +201,9 @@ export default {
<gl-link
v-if="totalClusters"
data-testid="clusters-tab-footer-link"
- :href="`?tab=${$options.CERTIFICATE_BASED_CARD_INFO.tabName}`"
- @click="changeTab($event, $options.CERTIFICATE_BASED_CARD_INFO.tabName)"
- ><gl-sprintf :message="$options.CERTIFICATE_BASED_CARD_INFO.footerText"
+ :href="`?tab=${$options.i18n.certificate.tabName}`"
+ @click="changeTab($event, $options.i18n.certificate.tabName)"
+ ><gl-sprintf :message="$options.i18n.certificate.footerText"
><template #number>{{ cardFooterNumber(totalClusters) }}</template></gl-sprintf
></gl-link
><gl-button
@@ -209,7 +212,7 @@ export default {
variant="confirm"
class="gl-ml-4"
:href="addClusterPath"
- >{{ $options.CERTIFICATE_BASED_CARD_INFO.actionText }}</gl-button
+ >{{ $options.i18n.certificate.actionText }}</gl-button
>
</template>
</gl-card>
diff --git a/app/assets/javascripts/clusters_list/components/install_agent_modal.vue b/app/assets/javascripts/clusters_list/components/install_agent_modal.vue
index 6eb2e85ecea..5eef76252bd 100644
--- a/app/assets/javascripts/clusters_list/components/install_agent_modal.vue
+++ b/app/assets/javascripts/clusters_list/components/install_agent_modal.vue
@@ -9,22 +9,48 @@ import {
GlSprintf,
} from '@gitlab/ui';
import { helpPagePath } from '~/helpers/help_page_helper';
-import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
+import ModalCopyButton from '~/vue_shared/components/modal_copy_button.vue';
import CodeBlock from '~/vue_shared/components/code_block.vue';
+import Tracking from '~/tracking';
import { generateAgentRegistrationCommand } from '../clusters_util';
-import { INSTALL_AGENT_MODAL_ID, I18N_INSTALL_AGENT_MODAL } from '../constants';
-import { addAgentToStore } from '../graphql/cache_update';
+import {
+ INSTALL_AGENT_MODAL_ID,
+ I18N_AGENT_MODAL,
+ KAS_DISABLED_ERROR,
+ EVENT_LABEL_MODAL,
+ EVENT_ACTIONS_OPEN,
+ EVENT_ACTIONS_SELECT,
+ EVENT_ACTIONS_CLICK,
+ MODAL_TYPE_EMPTY,
+ MODAL_TYPE_REGISTER,
+} from '../constants';
+import { addAgentToStore, addAgentConfigToStore } from '../graphql/cache_update';
import createAgent from '../graphql/mutations/create_agent.mutation.graphql';
import createAgentToken from '../graphql/mutations/create_agent_token.mutation.graphql';
import getAgentsQuery from '../graphql/queries/get_agents.query.graphql';
+import agentConfigurations from '../graphql/queries/agent_configurations.query.graphql';
import AvailableAgentsDropdown from './available_agents_dropdown.vue';
+const trackingMixin = Tracking.mixin({ label: EVENT_LABEL_MODAL });
+
export default {
modalId: INSTALL_AGENT_MODAL_ID,
- i18n: I18N_INSTALL_AGENT_MODAL,
+ EVENT_ACTIONS_OPEN,
+ EVENT_ACTIONS_CLICK,
+ EVENT_LABEL_MODAL,
+ basicInstallPath: helpPagePath('user/clusters/agent/install/index', {
+ anchor: 'install-the-agent-into-the-cluster',
+ }),
+ advancedInstallPath: helpPagePath('user/clusters/agent/install/index', {
+ anchor: 'advanced-installation',
+ }),
+ enableKasPath: helpPagePath('administration/clusters/kas'),
+ installAgentPath: helpPagePath('user/clusters/agent/install/index'),
+ registerAgentPath: helpPagePath('user/clusters/agent/install/index', {
+ anchor: 'register-an-agent-with-gitlab',
+ }),
components: {
AvailableAgentsDropdown,
- ClipboardButton,
CodeBlock,
GlAlert,
GlButton,
@@ -33,8 +59,10 @@ export default {
GlLink,
GlModal,
GlSprintf,
+ ModalCopyButton,
},
- inject: ['projectPath', 'kasAddress'],
+ mixins: [trackingMixin],
+ inject: ['projectPath', 'kasAddress', 'emptyStateImage'],
props: {
defaultBranchName: {
default: '.noBranch',
@@ -46,6 +74,22 @@ export default {
type: Number,
},
},
+ apollo: {
+ agents: {
+ query: agentConfigurations,
+ variables() {
+ return {
+ projectPath: this.projectPath,
+ };
+ },
+ update(data) {
+ this.populateAvailableAgents(data);
+ },
+ error(error) {
+ this.kasDisabled = error?.message?.indexOf(KAS_DISABLED_ERROR) >= 0;
+ },
+ },
+ },
data() {
return {
registering: false,
@@ -53,6 +97,8 @@ export default {
agentToken: null,
error: null,
clusterAgent: null,
+ availableAgents: [],
+ kasDisabled: false,
};
},
computed: {
@@ -63,19 +109,11 @@ export default {
return !this.registering && this.agentName !== null;
},
canCancel() {
- return !this.registered && !this.registering;
+ return !this.registered && !this.registering && this.isAgentRegistrationModal;
},
agentRegistrationCommand() {
return generateAgentRegistrationCommand(this.agentToken, this.kasAddress);
},
- basicInstallPath() {
- return helpPagePath('user/clusters/agent/install/index', {
- anchor: 'install-the-agent-into-the-cluster',
- });
- },
- advancedInstallPath() {
- return helpPagePath('user/clusters/agent/install/index', { anchor: 'advanced-installation' });
- },
getAgentsQueryVariables() {
return {
defaultBranchName: this.defaultBranchName,
@@ -84,10 +122,31 @@ export default {
projectPath: this.projectPath,
};
},
+ i18n() {
+ return I18N_AGENT_MODAL[this.modalType];
+ },
+ repositoryPath() {
+ return `/${this.projectPath}`;
+ },
+ modalType() {
+ return !this.availableAgents?.length && !this.registered
+ ? MODAL_TYPE_EMPTY
+ : MODAL_TYPE_REGISTER;
+ },
+ modalSize() {
+ return this.isEmptyStateModal ? 'sm' : 'md';
+ },
+ isEmptyStateModal() {
+ return this.modalType === MODAL_TYPE_EMPTY;
+ },
+ isAgentRegistrationModal() {
+ return this.modalType === MODAL_TYPE_REGISTER;
+ },
},
methods: {
setAgentName(name) {
this.agentName = name;
+ this.track(EVENT_ACTIONS_SELECT);
},
closeModal() {
this.$refs.modal.hide();
@@ -96,8 +155,16 @@ export default {
this.registering = false;
this.agentName = null;
this.agentToken = null;
+ this.clusterAgent = null;
this.error = null;
},
+ populateAvailableAgents(data) {
+ const installedAgents = data?.project?.clusterAgents?.nodes.map((agent) => agent.name) ?? [];
+ const configuredAgents =
+ data?.project?.agentConfigurations?.nodes.map((config) => config.agentName) ?? [];
+
+ this.availableAgents = configuredAgents.filter((agent) => !installedAgents.includes(agent));
+ },
createAgentMutation() {
return this.$apollo
.mutate({
@@ -117,7 +184,9 @@ export default {
);
},
})
- .then(({ data: { createClusterAgent } }) => createClusterAgent);
+ .then(({ data: { createClusterAgent } }) => {
+ return createClusterAgent;
+ });
},
createAgentTokenMutation(agendId) {
return this.$apollo
@@ -129,6 +198,17 @@ export default {
name: this.agentName,
},
},
+ update: (store, { data: { clusterAgentTokenCreate } }) => {
+ addAgentConfigToStore(
+ store,
+ clusterAgentTokenCreate,
+ this.clusterAgent,
+ agentConfigurations,
+ {
+ projectPath: this.projectPath,
+ },
+ );
+ },
})
.then(({ data: { clusterAgentTokenCreate } }) => clusterAgentTokenCreate);
},
@@ -158,7 +238,7 @@ export default {
if (error) {
this.error = error.message;
} else {
- this.error = this.$options.i18n.unknownError;
+ this.error = this.i18n.unknownError;
}
} finally {
this.registering = false;
@@ -172,115 +252,172 @@ export default {
<gl-modal
ref="modal"
:modal-id="$options.modalId"
- :title="$options.i18n.modalTitle"
+ :title="i18n.modalTitle"
+ :size="modalSize"
static
lazy
@hidden="resetModal"
+ @show="track($options.EVENT_ACTIONS_OPEN, { property: modalType })"
>
- <template v-if="!registered">
- <p>
- <strong>{{ $options.i18n.selectAgentTitle }}</strong>
- </p>
+ <template v-if="isAgentRegistrationModal">
+ <template v-if="!registered">
+ <p>
+ <strong>{{ i18n.selectAgentTitle }}</strong>
+ </p>
- <p>
- <gl-sprintf :message="$options.i18n.selectAgentBody">
- <template #link="{ content }">
- <gl-link :href="basicInstallPath" target="_blank"> {{ content }}</gl-link>
- </template>
- </gl-sprintf>
- </p>
+ <p class="gl-mb-0">{{ i18n.selectAgentBody }}</p>
+ <p>
+ <gl-link :href="$options.registerAgentPath"> {{ i18n.learnMoreLink }}</gl-link>
+ </p>
- <form>
- <gl-form-group label-for="agent-name">
- <available-agents-dropdown
- class="gl-w-70p"
- :is-registering="registering"
- @agentSelected="setAgentName"
- />
- </gl-form-group>
- </form>
+ <form>
+ <gl-form-group label-for="agent-name">
+ <available-agents-dropdown
+ class="gl-w-70p"
+ :is-registering="registering"
+ :available-agents="availableAgents"
+ @agentSelected="setAgentName"
+ />
+ </gl-form-group>
+ </form>
- <p v-if="error">
- <gl-alert
- :title="$options.i18n.registrationErrorTitle"
- variant="danger"
- :dismissible="false"
- >
- {{ error }}
- </gl-alert>
- </p>
- </template>
+ <p v-if="error">
+ <gl-alert :title="i18n.registrationErrorTitle" variant="danger" :dismissible="false">
+ {{ error }}
+ </gl-alert>
+ </p>
+ </template>
- <template v-else>
- <p>
- <strong>{{ $options.i18n.tokenTitle }}</strong>
- </p>
+ <template v-else>
+ <p>
+ <strong>{{ i18n.tokenTitle }}</strong>
+ </p>
- <p>
- <gl-sprintf :message="$options.i18n.tokenBody">
- <template #link="{ content }">
- <gl-link :href="basicInstallPath" target="_blank"> {{ content }}</gl-link>
- </template>
- </gl-sprintf>
- </p>
+ <p>
+ <gl-sprintf :message="i18n.tokenBody">
+ <template #link="{ content }">
+ <gl-link :href="$options.basicInstallPath" target="_blank"> {{ content }}</gl-link>
+ </template>
+ </gl-sprintf>
+ </p>
- <p>
- <gl-alert
- :title="$options.i18n.tokenSingleUseWarningTitle"
- variant="warning"
- :dismissible="false"
- >
- {{ $options.i18n.tokenSingleUseWarningBody }}
- </gl-alert>
- </p>
+ <p>
+ <gl-alert :title="i18n.tokenSingleUseWarningTitle" variant="warning" :dismissible="false">
+ {{ i18n.tokenSingleUseWarningBody }}
+ </gl-alert>
+ </p>
- <p>
- <gl-form-input-group readonly :value="agentToken" :select-on-click="true">
- <template #append>
- <clipboard-button :text="agentToken" :title="$options.i18n.copyToken" />
- </template>
- </gl-form-input-group>
- </p>
+ <p>
+ <gl-form-input-group readonly :value="agentToken" :select-on-click="true">
+ <template #append>
+ <modal-copy-button
+ :text="agentToken"
+ :title="i18n.copyToken"
+ :modal-id="$options.modalId"
+ />
+ </template>
+ </gl-form-input-group>
+ </p>
- <p>
- <strong>{{ $options.i18n.basicInstallTitle }}</strong>
- </p>
+ <p>
+ <strong>{{ i18n.basicInstallTitle }}</strong>
+ </p>
- <p>
- {{ $options.i18n.basicInstallBody }}
- </p>
+ <p>
+ {{ i18n.basicInstallBody }}
+ </p>
- <p>
- <code-block :code="agentRegistrationCommand" />
- </p>
+ <p>
+ <code-block :code="agentRegistrationCommand" />
+ </p>
+
+ <p>
+ <strong>{{ i18n.advancedInstallTitle }}</strong>
+ </p>
+
+ <p>
+ <gl-sprintf :message="i18n.advancedInstallBody">
+ <template #link="{ content }">
+ <gl-link :href="$options.advancedInstallPath" target="_blank"> {{ content }}</gl-link>
+ </template>
+ </gl-sprintf>
+ </p>
+ </template>
+ </template>
+
+ <template v-else>
+ <div class="gl-text-center gl-mb-5">
+ <img :alt="i18n.altText" :src="emptyStateImage" height="100" />
+ </div>
<p>
- <strong>{{ $options.i18n.advancedInstallTitle }}</strong>
+ <gl-sprintf :message="i18n.modalBody">
+ <template #link="{ content }">
+ <gl-link :href="$options.installAgentPath"> {{ content }}</gl-link>
+ </template>
+ </gl-sprintf>
</p>
- <p>
- <gl-sprintf :message="$options.i18n.advancedInstallBody">
+ <p v-if="kasDisabled">
+ <gl-sprintf :message="i18n.enableKasText">
<template #link="{ content }">
- <gl-link :href="advancedInstallPath" target="_blank"> {{ content }}</gl-link>
+ <gl-link :href="$options.enableKasPath"> {{ content }}</gl-link>
</template>
</gl-sprintf>
</p>
</template>
<template #modal-footer>
- <gl-button v-if="canCancel" @click="closeModal">{{ $options.i18n.cancel }} </gl-button>
-
- <gl-button v-if="registered" variant="confirm" category="primary" @click="closeModal"
- >{{ $options.i18n.close }}
+ <gl-button
+ v-if="registered"
+ variant="confirm"
+ category="primary"
+ :data-track-action="$options.EVENT_ACTIONS_CLICK"
+ :data-track-label="$options.EVENT_LABEL_MODAL"
+ data-track-property="close"
+ @click="closeModal"
+ >{{ i18n.close }}
</gl-button>
<gl-button
- v-else
+ v-else-if="isAgentRegistrationModal"
:disabled="!nextButtonDisabled"
variant="confirm"
category="primary"
+ :data-track-action="$options.EVENT_ACTIONS_CLICK"
+ :data-track-label="$options.EVENT_LABEL_MODAL"
+ data-track-property="register"
@click="registerAgent"
- >{{ $options.i18n.registerAgentButton }}
+ >{{ i18n.registerAgentButton }}
+ </gl-button>
+
+ <gl-button
+ v-if="canCancel"
+ :data-track-action="$options.EVENT_ACTIONS_CLICK"
+ :data-track-label="$options.EVENT_LABEL_MODAL"
+ data-track-property="cancel"
+ @click="closeModal"
+ >{{ i18n.cancel }}
+ </gl-button>
+
+ <gl-button
+ v-if="isEmptyStateModal"
+ :href="repositoryPath"
+ variant="confirm"
+ category="secondary"
+ data-testid="agent-secondary-button"
+ >{{ i18n.secondaryButton }}
+ </gl-button>
+
+ <gl-button
+ v-if="isEmptyStateModal"
+ variant="confirm"
+ category="primary"
+ :data-track-action="$options.EVENT_ACTIONS_CLICK"
+ :data-track-label="$options.EVENT_LABEL_MODAL"
+ data-track-property="done"
+ @click="closeModal"
+ >{{ i18n.done }}
</gl-button>
</template>
</gl-modal>
diff --git a/app/assets/javascripts/clusters_list/constants.js b/app/assets/javascripts/clusters_list/constants.js
index 9fefdf450c4..9b52df74fc5 100644
--- a/app/assets/javascripts/clusters_list/constants.js
+++ b/app/assets/javascripts/clusters_list/constants.js
@@ -64,47 +64,63 @@ export const STATUSES = {
creating: { title: __('Creating') },
};
-export const I18N_INSTALL_AGENT_MODAL = {
- registerAgentButton: s__('ClusterAgents|Register Agent'),
- close: __('Close'),
- cancel: __('Cancel'),
-
- modalTitle: s__('ClusterAgents|Install new Agent'),
-
- selectAgentTitle: s__('ClusterAgents|Select which Agent you want to install'),
- selectAgentBody: s__(
- `ClusterAgents|Select the Agent you want to register with GitLab and install on your cluster. To learn more about the Kubernetes Agent registration process %{linkStart}go to the documentation%{linkEnd}.`,
- ),
+export const I18N_AGENT_MODAL = {
+ agent_registration: {
+ registerAgentButton: s__('ClusterAgents|Register'),
+ close: __('Close'),
+ cancel: __('Cancel'),
+
+ modalTitle: s__('ClusterAgents|Connect a cluster through the Agent'),
+ selectAgentTitle: s__('ClusterAgents|Select an agent to register with GitLab'),
+ selectAgentBody: s__(
+ 'ClusterAgents|Register an agent to generate a token that will be used to install the agent on your cluster in the next step.',
+ ),
+ learnMoreLink: s__('ClusterAgents|How to register an agent?'),
- copyToken: s__('ClusterAgents|Copy token'),
- tokenTitle: s__('ClusterAgents|Registration token'),
- tokenBody: s__(
- `ClusterAgents|The registration token will be used to connect the Agent on your cluster to GitLab. To learn more about the registration tokens and how they are used %{linkStart}go to the documentation%{linkEnd}.`,
- ),
+ copyToken: s__('ClusterAgents|Copy token'),
+ tokenTitle: s__('ClusterAgents|Registration token'),
+ tokenBody: s__(
+ `ClusterAgents|The registration token will be used to connect the agent on your cluster to GitLab. %{linkStart}What are registration tokens?%{linkEnd}`,
+ ),
- tokenSingleUseWarningTitle: s__(
- 'ClusterAgents|The token value will not be shown again after you close this window.',
- ),
- tokenSingleUseWarningBody: s__(
- `ClusterAgents|The recommended installation method provided below includes the token. If you want to follow the alternative installation method provided in the docs make sure you save the token value before you close the window.`,
- ),
+ tokenSingleUseWarningTitle: s__(
+ 'ClusterAgents|You cannot see this token again after you close this window.',
+ ),
+ tokenSingleUseWarningBody: s__(
+ `ClusterAgents|The recommended installation method includes the token. If you want to follow the advanced installation method provided in the docs, make sure you save the token value before you close this window.`,
+ ),
- basicInstallTitle: s__('ClusterAgents|Recommended installation method'),
- basicInstallBody: __(
- `Open a CLI and connect to the cluster you want to install the Agent in. Use this installation method to minimize any manual steps. The token is already included in the command.`,
- ),
+ basicInstallTitle: s__('ClusterAgents|Recommended installation method'),
+ basicInstallBody: __(
+ `Open a CLI and connect to the cluster you want to install the agent in. Use this installation method to minimize any manual steps. The token is already included in the command.`,
+ ),
- advancedInstallTitle: s__('ClusterAgents|Alternative installation methods'),
- advancedInstallBody: s__(
- 'ClusterAgents|For alternative installation methods %{linkStart}go to the documentation%{linkEnd}.',
- ),
+ advancedInstallTitle: s__('ClusterAgents|Advanced installation methods'),
+ advancedInstallBody: s__(
+ 'ClusterAgents|For the advanced installation method %{linkStart}see the documentation%{linkEnd}.',
+ ),
- registrationErrorTitle: __('Failed to register Agent'),
- unknownError: s__('ClusterAgents|An unknown error occurred. Please try again.'),
+ registrationErrorTitle: s__('ClusterAgents|Failed to register an agent'),
+ unknownError: s__('ClusterAgents|An unknown error occurred. Please try again.'),
+ },
+ empty_state: {
+ modalTitle: s__('ClusterAgents|Connect your cluster through the Agent'),
+ modalBody: s__(
+ "ClusterAgents|To install a new agent, first add the agent's configuration file to this repository. %{linkStart}What's the agent's configuration file?%{linkEnd}",
+ ),
+ enableKasText: s__(
+ "ClusterAgents|Your instance doesn't have the %{linkStart}GitLab Agent Server (KAS)%{linkEnd} set up. Ask a GitLab Administrator to install it.",
+ ),
+ altText: s__('ClusterAgents|GitLab Agent for Kubernetes'),
+ secondaryButton: s__('ClusterAgents|Go to the repository files'),
+ done: __('Cancel'),
+ },
};
+export const KAS_DISABLED_ERROR = 'Gitlab::Kas::Client::ConfigurationError';
+
export const I18N_AVAILABLE_AGENTS_DROPDOWN = {
- selectAgent: s__('ClusterAgents|Select an Agent'),
+ selectAgent: s__('ClusterAgents|Select an agent'),
registeringAgent: s__('ClusterAgents|Registering Agent'),
};
@@ -125,7 +141,7 @@ export const AGENT_STATUSES = {
title: s__('ClusterAgents|Agent might not be connected to GitLab'),
body: sprintf(
s__(
- 'ClusterAgents|The Agent has not been connected in a long time. There might be a connectivity issue. Last contact was %{timeAgo}.',
+ 'ClusterAgents|The agent has not been connected in a long time. There might be a connectivity issue. Last contact was %{timeAgo}.',
),
),
},
@@ -143,55 +159,48 @@ export const AGENT_STATUSES = {
export const I18N_AGENTS_EMPTY_STATE = {
introText: s__(
- 'ClusterAgents|Use GitLab Agents to more securely integrate with your clusters to deploy your applications, run your pipelines, use review apps and much more.',
- ),
- multipleClustersText: s__(
- 'ClusterAgents|If you are setting up multiple clusters and are using Auto DevOps, %{linkStart}read about using multiple Kubernetes clusters first.%{linkEnd}',
- ),
- learnMoreText: s__('ClusterAgents|Learn more about the GitLab Kubernetes Agent.'),
- warningText: s__(
- 'ClusterAgents|To install an Agent you should create an agent directory in the Repository first. We recommend that you add the Agent configuration to the directory before you start the installation process.',
+ 'ClusterIntegration|Use the %{linkStart}GitLab Agent%{linkEnd} to safely connect your Kubernetes clusters to GitLab. You can deploy your applications, run your pipelines, use Review Apps, and much more.',
),
- readMoreText: s__('ClusterAgents|Read more about getting started'),
- repositoryButtonText: s__('ClusterAgents|Go to the repository'),
- primaryButtonText: s__('ClusterAgents|Connect with a GitLab Agent'),
+ buttonText: s__('ClusterAgents|Connect with the GitLab Agent'),
};
export const I18N_CLUSTERS_EMPTY_STATE = {
- description: s__(
- 'ClusterIntegration|Use certificates to integrate with your clusters to deploy your applications, run your pipelines, use review apps and much more in an easy way.',
- ),
- multipleClustersText: s__(
- 'ClusterIntegration|If you are setting up multiple clusters and are using Auto DevOps, %{linkStart}read about using multiple Kubernetes clusters first.%{linkEnd}',
+ introText: s__(
+ 'ClusterIntegration|Connect your cluster to GitLab through %{linkStart}cluster certificates%{linkEnd}.',
),
- learnMoreLinkText: s__('ClusterIntegration|Learn more about the GitLab managed clusters'),
buttonText: s__('ClusterIntegration|Connect with a certificate'),
+ alertText: s__(
+ 'ClusterIntegration|The certificate-based method to connect clusters to GitLab was %{linkStart}deprecated%{linkEnd} in GitLab 14.5.',
+ ),
};
export const AGENT_CARD_INFO = {
tabName: 'agent',
- title: sprintf(s__('ClusterAgents|%{number} of %{total} Agent based integrations')),
- emptyTitle: s__('ClusterAgents|No Agent based integrations'),
+ title: sprintf(s__('ClusterAgents|%{number} of %{total} agents')),
+ emptyTitle: s__('ClusterAgents|No agents'),
tooltip: {
label: s__('ClusterAgents|Recommended'),
- title: s__('ClusterAgents|GitLab Agents'),
+ title: s__('ClusterAgents|GitLab Agent'),
text: sprintf(
s__(
- 'ClusterAgents|GitLab Agents provide an increased level of security when integrating with clusters. %{linkStart}Learn more about the GitLab Kubernetes Agent.%{linkEnd}',
+ 'ClusterAgents|The GitLab Agent provides an increased level of security when connecting Kubernetes clusters to GitLab. %{linkStart}Learn more about the GitLab Agent.%{linkEnd}',
),
),
link: helpPagePath('user/clusters/agent/index'),
},
- actionText: s__('ClusterAgents|Install new Agent'),
- footerText: sprintf(s__('ClusterAgents|View all %{number} Agent based integrations')),
+ actionText: s__('ClusterAgents|Install a new agent'),
+ footerText: sprintf(s__('ClusterAgents|View all %{number} agents')),
};
export const CERTIFICATE_BASED_CARD_INFO = {
tabName: 'certificate_based',
- title: sprintf(s__('ClusterAgents|%{number} of %{total} Certificate based integrations')),
- emptyTitle: s__('ClusterAgents|No Certificate based integrations'),
+ title: sprintf(
+ s__('ClusterAgents|%{number} of %{total} clusters connected through cluster certificates'),
+ ),
+ emptyTitle: s__('ClusterAgents|No clusters connected through cluster certificates'),
actionText: s__('ClusterAgents|Connect existing cluster'),
- footerText: sprintf(s__('ClusterAgents|View all %{number} Certificate based integrations')),
+ footerText: sprintf(s__('ClusterAgents|View all %{number} clusters')),
+ badgeText: s__('ClusterAgents|Deprecated'),
};
export const MAX_CLUSTERS_LIST = 6;
@@ -208,7 +217,7 @@ export const CLUSTERS_TABS = [
queryParamValue: 'agent',
},
{
- title: s__('ClusterAgents|Certificate based'),
+ title: s__('ClusterAgents|Certificate'),
component: 'clusters',
queryParamValue: 'certificate_based',
},
@@ -216,10 +225,20 @@ export const CLUSTERS_TABS = [
export const CLUSTERS_ACTIONS = {
actionsButton: s__('ClusterAgents|Actions'),
- createNewCluster: s__('ClusterAgents|Create new cluster'),
- connectWithAgent: s__('ClusterAgents|Connect with Agent'),
- connectExistingCluster: s__('ClusterAgents|Connect with certificate'),
+ createNewCluster: s__('ClusterAgents|Create a new cluster'),
+ connectWithAgent: s__('ClusterAgents|Connect with the Agent'),
+ connectExistingCluster: s__('ClusterAgents|Connect with a certificate'),
};
export const AGENT = 'agent';
export const CERTIFICATE_BASED = 'certificate_based';
+
+export const EVENT_LABEL_MODAL = 'agent_registration_modal';
+export const EVENT_LABEL_TABS = 'kubernetes_section_tabs';
+export const EVENT_ACTIONS_OPEN = 'open_modal';
+export const EVENT_ACTIONS_SELECT = 'select_agent';
+export const EVENT_ACTIONS_CLICK = 'click_button';
+export const EVENT_ACTIONS_CHANGE = 'change_tab';
+
+export const MODAL_TYPE_EMPTY = 'empty_state';
+export const MODAL_TYPE_REGISTER = 'agent_registration';
diff --git a/app/assets/javascripts/clusters_list/graphql/cache_update.js b/app/assets/javascripts/clusters_list/graphql/cache_update.js
index dd633820952..4d12bc8151c 100644
--- a/app/assets/javascripts/clusters_list/graphql/cache_update.js
+++ b/app/assets/javascripts/clusters_list/graphql/cache_update.js
@@ -1,29 +1,65 @@
import produce from 'immer';
import { getAgentConfigPath } from '../clusters_util';
+export const hasErrors = ({ errors = [] }) => errors?.length;
+
export function addAgentToStore(store, createClusterAgent, query, variables) {
- const { clusterAgent } = createClusterAgent;
- const sourceData = store.readQuery({
- query,
- variables,
- });
-
- const data = produce(sourceData, (draftData) => {
- const configuration = {
- name: clusterAgent.name,
- path: getAgentConfigPath(clusterAgent.name),
- webPath: clusterAgent.webPath,
- __typename: 'TreeEntry',
- };
-
- draftData.project.clusterAgents.nodes.push(clusterAgent);
- draftData.project.clusterAgents.count += 1;
- draftData.project.repository.tree.trees.nodes.push(configuration);
- });
-
- store.writeQuery({
- query,
- variables,
- data,
- });
+ if (!hasErrors(createClusterAgent)) {
+ const { clusterAgent } = createClusterAgent;
+ const sourceData = store.readQuery({
+ query,
+ variables,
+ });
+
+ const data = produce(sourceData, (draftData) => {
+ const configuration = {
+ id: clusterAgent.id,
+ name: clusterAgent.name,
+ path: getAgentConfigPath(clusterAgent.name),
+ webPath: clusterAgent.webPath,
+ __typename: 'TreeEntry',
+ };
+
+ draftData.project.clusterAgents.nodes.push(clusterAgent);
+ draftData.project.clusterAgents.count += 1;
+ draftData.project.repository.tree.trees.nodes.push(configuration);
+ });
+
+ store.writeQuery({
+ query,
+ variables,
+ data,
+ });
+ }
+}
+
+export function addAgentConfigToStore(
+ store,
+ clusterAgentTokenCreate,
+ clusterAgent,
+ query,
+ variables,
+) {
+ if (!hasErrors(clusterAgentTokenCreate)) {
+ const sourceData = store.readQuery({
+ query,
+ variables,
+ });
+
+ const data = produce(sourceData, (draftData) => {
+ const configuration = {
+ agentName: clusterAgent.name,
+ __typename: 'AgentConfiguration',
+ };
+
+ draftData.project.clusterAgents.nodes.push(clusterAgent);
+ draftData.project.agentConfigurations.nodes.push(configuration);
+ });
+
+ store.writeQuery({
+ query,
+ variables,
+ data,
+ });
+ }
}
diff --git a/app/assets/javascripts/clusters_list/graphql/fragments/cluster_agent.fragment.graphql b/app/assets/javascripts/clusters_list/graphql/fragments/cluster_agent.fragment.graphql
index 9b40260471c..cd46dfee170 100644
--- a/app/assets/javascripts/clusters_list/graphql/fragments/cluster_agent.fragment.graphql
+++ b/app/assets/javascripts/clusters_list/graphql/fragments/cluster_agent.fragment.graphql
@@ -4,6 +4,7 @@ fragment ClusterAgentFragment on ClusterAgent {
webPath
tokens {
nodes {
+ id
lastUsedAt
}
}
diff --git a/app/assets/javascripts/clusters_list/graphql/queries/agent_configurations.query.graphql b/app/assets/javascripts/clusters_list/graphql/queries/agent_configurations.query.graphql
index 40b61337024..9a24cec5a9c 100644
--- a/app/assets/javascripts/clusters_list/graphql/queries/agent_configurations.query.graphql
+++ b/app/assets/javascripts/clusters_list/graphql/queries/agent_configurations.query.graphql
@@ -1,5 +1,6 @@
query agentConfigurations($projectPath: ID!) {
project(fullPath: $projectPath) {
+ id
agentConfigurations {
nodes {
agentName
@@ -8,6 +9,7 @@ query agentConfigurations($projectPath: ID!) {
clusterAgents {
nodes {
+ id
name
}
}
diff --git a/app/assets/javascripts/clusters_list/graphql/queries/get_agents.query.graphql b/app/assets/javascripts/clusters_list/graphql/queries/get_agents.query.graphql
index 47b25988877..f8efb6683f6 100644
--- a/app/assets/javascripts/clusters_list/graphql/queries/get_agents.query.graphql
+++ b/app/assets/javascripts/clusters_list/graphql/queries/get_agents.query.graphql
@@ -12,6 +12,7 @@ query getAgents(
$beforeTree: String
) {
project(fullPath: $projectPath) {
+ id
clusterAgents(first: $first, last: $last, before: $beforeAgent, after: $afterAgent) {
nodes {
...ClusterAgentFragment
@@ -28,6 +29,7 @@ query getAgents(
tree(path: ".gitlab/agents", ref: $defaultBranchName) {
trees(first: $first, last: $last, after: $afterTree, before: $beforeTree) {
nodes {
+ id
name
path
webPath
diff --git a/app/assets/javascripts/code_navigation/components/doc_line.vue b/app/assets/javascripts/code_navigation/components/doc_line.vue
index 69d398893d9..4d44c984833 100644
--- a/app/assets/javascripts/code_navigation/components/doc_line.vue
+++ b/app/assets/javascripts/code_navigation/components/doc_line.vue
@@ -18,5 +18,6 @@ export default {
<span v-for="(token, tokenIndex) in tokens" :key="tokenIndex" :class="token.class">{{
token.value
}}</span>
+ <br />
</span>
</template>
diff --git a/app/assets/javascripts/content_editor/extensions/attachment.js b/app/assets/javascripts/content_editor/extensions/attachment.js
index 29ee282f2d2..72df1d071d1 100644
--- a/app/assets/javascripts/content_editor/extensions/attachment.js
+++ b/app/assets/javascripts/content_editor/extensions/attachment.js
@@ -5,9 +5,11 @@ import { handleFileEvent } from '../services/upload_helpers';
export default Extension.create({
name: 'attachment',
- defaultOptions: {
- uploadsPath: null,
- renderMarkdown: null,
+ addOptions() {
+ return {
+ uploadsPath: null,
+ renderMarkdown: null,
+ };
},
addCommands() {
diff --git a/app/assets/javascripts/content_editor/extensions/audio.js b/app/assets/javascripts/content_editor/extensions/audio.js
index 25d4068c93f..ea48ee0cee0 100644
--- a/app/assets/javascripts/content_editor/extensions/audio.js
+++ b/app/assets/javascripts/content_editor/extensions/audio.js
@@ -2,8 +2,10 @@ import Playable from './playable';
export default Playable.extend({
name: 'audio',
- defaultOptions: {
- ...Playable.options,
- mediaType: 'audio',
+ addOptions() {
+ return {
+ ...this.parent?.(),
+ mediaType: 'audio',
+ };
},
});
diff --git a/app/assets/javascripts/content_editor/extensions/code_block_highlight.js b/app/assets/javascripts/content_editor/extensions/code_block_highlight.js
index 1ed1ab0315f..ea51bee3ba9 100644
--- a/app/assets/javascripts/content_editor/extensions/code_block_highlight.js
+++ b/app/assets/javascripts/content_editor/extensions/code_block_highlight.js
@@ -4,6 +4,8 @@ import * as lowlight from 'lowlight';
const extractLanguage = (element) => element.getAttribute('lang');
export default CodeBlockLowlight.extend({
+ isolating: true,
+
addAttributes() {
return {
language: {
@@ -17,7 +19,7 @@ export default CodeBlockLowlight.extend({
};
},
renderHTML({ HTMLAttributes }) {
- return ['pre', HTMLAttributes, ['code', {}, 0]];
+ return ['div', ['pre', HTMLAttributes, ['code', {}, 0]]];
},
}).configure({
lowlight,
diff --git a/app/assets/javascripts/content_editor/extensions/division.js b/app/assets/javascripts/content_editor/extensions/division.js
index c70d1700941..566ed85acf3 100644
--- a/app/assets/javascripts/content_editor/extensions/division.js
+++ b/app/assets/javascripts/content_editor/extensions/division.js
@@ -1,12 +1,26 @@
import { Node } from '@tiptap/core';
import { PARSE_HTML_PRIORITY_LOWEST } from '../constants';
+const getDiv = (element) => {
+ if (element.nodeName === 'DIV') return element;
+ return element.querySelector('div');
+};
+
export default Node.create({
name: 'division',
content: 'block*',
group: 'block',
defining: true,
+ addAttributes() {
+ return {
+ className: {
+ default: null,
+ parseHTML: (element) => getDiv(element).className || null,
+ },
+ };
+ },
+
parseHTML() {
return [{ tag: 'div', priority: PARSE_HTML_PRIORITY_LOWEST }];
},
diff --git a/app/assets/javascripts/content_editor/extensions/footnote_definition.js b/app/assets/javascripts/content_editor/extensions/footnote_definition.js
new file mode 100644
index 00000000000..dbab0de3421
--- /dev/null
+++ b/app/assets/javascripts/content_editor/extensions/footnote_definition.js
@@ -0,0 +1,21 @@
+import { mergeAttributes, Node } from '@tiptap/core';
+import { PARSE_HTML_PRIORITY_HIGHEST } from '../constants';
+
+export default Node.create({
+ name: 'footnoteDefinition',
+
+ content: 'paragraph',
+
+ group: 'block',
+
+ parseHTML() {
+ return [
+ { tag: 'section.footnotes li' },
+ { tag: '.footnote-backref', priority: PARSE_HTML_PRIORITY_HIGHEST, ignore: true },
+ ];
+ },
+
+ renderHTML({ HTMLAttributes }) {
+ return ['li', mergeAttributes(HTMLAttributes), 0];
+ },
+});
diff --git a/app/assets/javascripts/content_editor/extensions/footnote_reference.js b/app/assets/javascripts/content_editor/extensions/footnote_reference.js
new file mode 100644
index 00000000000..1ac8016f774
--- /dev/null
+++ b/app/assets/javascripts/content_editor/extensions/footnote_reference.js
@@ -0,0 +1,37 @@
+import { Node, mergeAttributes } from '@tiptap/core';
+import { PARSE_HTML_PRIORITY_HIGHEST } from '../constants';
+
+export default Node.create({
+ name: 'footnoteReference',
+
+ inline: true,
+
+ group: 'inline',
+
+ atom: true,
+
+ draggable: true,
+
+ selectable: true,
+
+ addAttributes() {
+ return {
+ footnoteId: {
+ default: null,
+ parseHTML: (element) => element.querySelector('a').getAttribute('id'),
+ },
+ footnoteNumber: {
+ default: null,
+ parseHTML: (element) => element.textContent,
+ },
+ };
+ },
+
+ parseHTML() {
+ return [{ tag: 'sup.footnote-ref', priority: PARSE_HTML_PRIORITY_HIGHEST }];
+ },
+
+ renderHTML({ HTMLAttributes: { footnoteNumber, footnoteId, ...HTMLAttributes } }) {
+ return ['sup', mergeAttributes(HTMLAttributes), footnoteNumber];
+ },
+});
diff --git a/app/assets/javascripts/content_editor/extensions/footnotes_section.js b/app/assets/javascripts/content_editor/extensions/footnotes_section.js
new file mode 100644
index 00000000000..914a8934734
--- /dev/null
+++ b/app/assets/javascripts/content_editor/extensions/footnotes_section.js
@@ -0,0 +1,19 @@
+import { mergeAttributes, Node } from '@tiptap/core';
+
+export default Node.create({
+ name: 'footnotesSection',
+
+ content: 'footnoteDefinition+',
+
+ group: 'block',
+
+ isolating: true,
+
+ parseHTML() {
+ return [{ tag: 'section.footnotes > ol' }];
+ },
+
+ renderHTML({ HTMLAttributes }) {
+ return ['ol', mergeAttributes(HTMLAttributes, { class: 'footnotes gl-font-sm' }), 0];
+ },
+});
diff --git a/app/assets/javascripts/content_editor/extensions/html_marks.js b/app/assets/javascripts/content_editor/extensions/html_marks.js
index 3abf0e3eee2..9579f3b06f6 100644
--- a/app/assets/javascripts/content_editor/extensions/html_marks.js
+++ b/app/assets/javascripts/content_editor/extensions/html_marks.js
@@ -31,13 +31,12 @@ const attrs = {
export default marks.map((name) =>
Mark.create({
name,
-
inclusive: false,
-
- defaultOptions: {
- HTMLAttributes: {},
+ addOptions() {
+ return {
+ HTMLAttributes: {},
+ };
},
-
addAttributes() {
return (attrs[name] || []).reduce(
(acc, attr) => ({
diff --git a/app/assets/javascripts/content_editor/extensions/image.js b/app/assets/javascripts/content_editor/extensions/image.js
index 837fab0585f..d7fb617f7ee 100644
--- a/app/assets/javascripts/content_editor/extensions/image.js
+++ b/app/assets/javascripts/content_editor/extensions/image.js
@@ -7,9 +7,11 @@ const resolveImageEl = (element) =>
element.nodeName === 'IMG' ? element : element.querySelector('img');
export default Image.extend({
- defaultOptions: {
- ...Image.options,
- inline: true,
+ addOptions() {
+ return {
+ ...this.parent?.(),
+ inline: true,
+ };
},
addAttributes() {
return {
diff --git a/app/assets/javascripts/content_editor/extensions/inline_diff.js b/app/assets/javascripts/content_editor/extensions/inline_diff.js
index 22bb1ac072e..f76943a0669 100644
--- a/app/assets/javascripts/content_editor/extensions/inline_diff.js
+++ b/app/assets/javascripts/content_editor/extensions/inline_diff.js
@@ -3,8 +3,10 @@ import { Mark, markInputRule, mergeAttributes } from '@tiptap/core';
export default Mark.create({
name: 'inlineDiff',
- defaultOptions: {
- HTMLAttributes: {},
+ addOptions() {
+ return {
+ HTMLAttributes: {},
+ };
},
addAttributes() {
diff --git a/app/assets/javascripts/content_editor/extensions/link.js b/app/assets/javascripts/content_editor/extensions/link.js
index 27bc05dce6f..f9b12f631fe 100644
--- a/app/assets/javascripts/content_editor/extensions/link.js
+++ b/app/assets/javascripts/content_editor/extensions/link.js
@@ -18,10 +18,13 @@ export const extractHrefFromMarkdownLink = (match) => {
};
export default Link.extend({
- defaultOptions: {
- ...Link.options,
- openOnClick: false,
+ addOptions() {
+ return {
+ ...this.parent?.(),
+ openOnClick: false,
+ };
},
+
addInputRules() {
const markdownLinkSyntaxInputRuleRegExp = /(?:^|\s)\[([\w|\s|-]+)\]\((?<href>.+?)\)$/gm;
const urlSyntaxRegExp = /(?:^|\s)(?<href>(?:https?:\/\/|www\.)[\S]+)(?:\s|\n)$/gim;
diff --git a/app/assets/javascripts/content_editor/extensions/task_item.js b/app/assets/javascripts/content_editor/extensions/task_item.js
index 9b050edcb28..6efef3f8198 100644
--- a/app/assets/javascripts/content_editor/extensions/task_item.js
+++ b/app/assets/javascripts/content_editor/extensions/task_item.js
@@ -2,9 +2,11 @@ import { TaskItem } from '@tiptap/extension-task-item';
import { PARSE_HTML_PRIORITY_HIGHEST } from '../constants';
export default TaskItem.extend({
- defaultOptions: {
- nested: true,
- HTMLAttributes: {},
+ addOptions() {
+ return {
+ nested: true,
+ HTMLAttributes: {},
+ };
},
addAttributes() {
diff --git a/app/assets/javascripts/content_editor/extensions/video.js b/app/assets/javascripts/content_editor/extensions/video.js
index 9923b7c04cd..312e8cd5ff6 100644
--- a/app/assets/javascripts/content_editor/extensions/video.js
+++ b/app/assets/javascripts/content_editor/extensions/video.js
@@ -2,9 +2,11 @@ import Playable from './playable';
export default Playable.extend({
name: 'video',
- defaultOptions: {
- ...Playable.options,
- mediaType: 'video',
- extraElementAttrs: { width: '400' },
+ addOptions() {
+ return {
+ ...this.parent?.(),
+ mediaType: 'video',
+ extraElementAttrs: { width: '400' },
+ };
},
});
diff --git a/app/assets/javascripts/content_editor/extensions/word_break.js b/app/assets/javascripts/content_editor/extensions/word_break.js
index fa7e02f8cc8..457b7c36564 100644
--- a/app/assets/javascripts/content_editor/extensions/word_break.js
+++ b/app/assets/javascripts/content_editor/extensions/word_break.js
@@ -7,10 +7,12 @@ export default Node.create({
selectable: false,
atom: true,
- defaultOptions: {
- HTMLAttributes: {
- class: 'gl-display-inline-flex gl-px-1 gl-bg-blue-100 gl-rounded-base gl-font-sm',
- },
+ addOptions() {
+ return {
+ HTMLAttributes: {
+ class: 'gl-display-inline-flex gl-px-1 gl-bg-blue-100 gl-rounded-base gl-font-sm',
+ },
+ };
},
parseHTML() {
diff --git a/app/assets/javascripts/content_editor/services/create_content_editor.js b/app/assets/javascripts/content_editor/services/create_content_editor.js
index 385f1c63801..f451357e211 100644
--- a/app/assets/javascripts/content_editor/services/create_content_editor.js
+++ b/app/assets/javascripts/content_editor/services/create_content_editor.js
@@ -19,6 +19,9 @@ import Dropcursor from '../extensions/dropcursor';
import Emoji from '../extensions/emoji';
import Figure from '../extensions/figure';
import FigureCaption from '../extensions/figure_caption';
+import FootnoteDefinition from '../extensions/footnote_definition';
+import FootnoteReference from '../extensions/footnote_reference';
+import FootnotesSection from '../extensions/footnotes_section';
import Frontmatter from '../extensions/frontmatter';
import Gapcursor from '../extensions/gapcursor';
import HardBreak from '../extensions/hard_break';
@@ -94,6 +97,9 @@ export const createContentEditor = ({
Emoji,
Figure,
FigureCaption,
+ FootnoteDefinition,
+ FootnoteReference,
+ FootnotesSection,
Frontmatter,
Gapcursor,
HardBreak,
diff --git a/app/assets/javascripts/content_editor/services/markdown_serializer.js b/app/assets/javascripts/content_editor/services/markdown_serializer.js
index 0dd3cb5b73f..278ef326c7a 100644
--- a/app/assets/javascripts/content_editor/services/markdown_serializer.js
+++ b/app/assets/javascripts/content_editor/services/markdown_serializer.js
@@ -17,6 +17,9 @@ import Division from '../extensions/division';
import Emoji from '../extensions/emoji';
import Figure from '../extensions/figure';
import FigureCaption from '../extensions/figure_caption';
+import FootnotesSection from '../extensions/footnotes_section';
+import FootnoteDefinition from '../extensions/footnote_definition';
+import FootnoteReference from '../extensions/footnote_reference';
import Frontmatter from '../extensions/frontmatter';
import HardBreak from '../extensions/hard_break';
import Heading from '../extensions/heading';
@@ -135,7 +138,16 @@ const defaultSerializerConfig = {
state.write('```');
state.closeBlock(node);
},
- [Division.name]: renderHTMLNode('div'),
+ [Division.name]: (state, node) => {
+ if (node.attrs.className?.includes('js-markdown-code')) {
+ state.renderInline(node);
+ } else {
+ const newNode = node;
+ delete newNode.attrs.className;
+
+ renderHTMLNode('div')(state, newNode);
+ }
+ },
[DescriptionList.name]: renderHTMLNode('dl', true),
[DescriptionItem.name]: (state, node, parent, index) => {
if (index === 1) state.ensureNewLine();
@@ -156,6 +168,15 @@ const defaultSerializerConfig = {
state.write(`:${name}:`);
},
+ [FootnoteDefinition.name]: (state, node) => {
+ state.renderInline(node);
+ },
+ [FootnoteReference.name]: (state, node) => {
+ state.write(`[^${node.attrs.footnoteNumber}]`);
+ },
+ [FootnotesSection.name]: (state, node) => {
+ state.renderList(node, '', (index) => `[^${index + 1}]: `);
+ },
[Frontmatter.name]: (state, node) => {
const { language } = node.attrs;
const syntax = {
diff --git a/app/assets/javascripts/crm/components/contact_form.vue b/app/assets/javascripts/crm/components/contact_form.vue
new file mode 100644
index 00000000000..81ae5c246be
--- /dev/null
+++ b/app/assets/javascripts/crm/components/contact_form.vue
@@ -0,0 +1,224 @@
+<script>
+import { GlAlert, GlButton, GlDrawer, GlFormGroup, GlFormInput } from '@gitlab/ui';
+import { produce } from 'immer';
+import { __, s__ } from '~/locale';
+import { convertToGraphQLId } from '~/graphql_shared/utils';
+import { TYPE_GROUP } from '~/graphql_shared/constants';
+import createContactMutation from './queries/create_contact.mutation.graphql';
+import updateContactMutation from './queries/update_contact.mutation.graphql';
+import getGroupContactsQuery from './queries/get_group_contacts.query.graphql';
+
+export default {
+ components: {
+ GlAlert,
+ GlButton,
+ GlDrawer,
+ GlFormGroup,
+ GlFormInput,
+ },
+ inject: ['groupFullPath', 'groupId'],
+ props: {
+ drawerOpen: {
+ type: Boolean,
+ required: true,
+ },
+ contact: {
+ type: Object,
+ required: false,
+ default: () => {},
+ },
+ },
+ data() {
+ return {
+ firstName: '',
+ lastName: '',
+ phone: '',
+ email: '',
+ description: '',
+ submitting: false,
+ errorMessages: [],
+ };
+ },
+ computed: {
+ invalid() {
+ const { firstName, lastName, email } = this;
+
+ return firstName.trim() === '' || lastName.trim() === '' || email.trim() === '';
+ },
+ editMode() {
+ return Boolean(this.contact);
+ },
+ title() {
+ return this.editMode ? this.$options.i18n.editTitle : this.$options.i18n.newTitle;
+ },
+ buttonLabel() {
+ return this.editMode
+ ? this.$options.i18n.editButtonLabel
+ : this.$options.i18n.createButtonLabel;
+ },
+ mutation() {
+ return this.editMode ? updateContactMutation : createContactMutation;
+ },
+ variables() {
+ const { contact, firstName, lastName, phone, email, description, editMode, groupId } = this;
+
+ const variables = {
+ input: {
+ firstName,
+ lastName,
+ phone,
+ email,
+ description,
+ },
+ };
+
+ if (editMode) {
+ variables.input.id = contact.id;
+ } else {
+ variables.input.groupId = convertToGraphQLId(TYPE_GROUP, groupId);
+ }
+
+ return variables;
+ },
+ },
+ mounted() {
+ if (this.editMode) {
+ const { contact } = this;
+
+ this.firstName = contact.firstName || '';
+ this.lastName = contact.lastName || '';
+ this.phone = contact.phone || '';
+ this.email = contact.email || '';
+ this.description = contact.description || '';
+ }
+ },
+ methods: {
+ save() {
+ const { mutation, variables, updateCache, close } = this;
+
+ this.submitting = true;
+
+ return this.$apollo
+ .mutate({
+ mutation,
+ variables,
+ update: updateCache,
+ })
+ .then(({ data }) => {
+ if (
+ data.customerRelationsContactCreate?.errors.length === 0 ||
+ data.customerRelationsContactUpdate?.errors.length === 0
+ ) {
+ close(true);
+ }
+
+ this.submitting = false;
+ })
+ .catch(() => {
+ this.errorMessages = [this.$options.i18n.somethingWentWrong];
+ this.submitting = false;
+ });
+ },
+ close(success) {
+ this.$emit('close', success);
+ },
+ updateCache(store, { data }) {
+ const mutationData =
+ data.customerRelationsContactCreate || data.customerRelationsContactUpdate;
+
+ if (mutationData?.errors.length > 0) {
+ this.errorMessages = mutationData.errors;
+ return;
+ }
+
+ const queryArgs = {
+ query: getGroupContactsQuery,
+ variables: { groupFullPath: this.groupFullPath },
+ };
+
+ const sourceData = store.readQuery(queryArgs);
+
+ queryArgs.data = produce(sourceData, (draftState) => {
+ draftState.group.contacts.nodes = [
+ ...sourceData.group.contacts.nodes.filter(({ id }) => id !== this.contact?.id),
+ mutationData.contact,
+ ];
+ });
+
+ store.writeQuery(queryArgs);
+ },
+ getDrawerHeaderHeight() {
+ const wrapperEl = document.querySelector('.content-wrapper');
+
+ if (wrapperEl) {
+ return `${wrapperEl.offsetTop}px`;
+ }
+
+ return '';
+ },
+ },
+ i18n: {
+ createButtonLabel: s__('Crm|Create new contact'),
+ editButtonLabel: __('Save changes'),
+ cancel: __('Cancel'),
+ firstName: s__('Crm|First name'),
+ lastName: s__('Crm|Last name'),
+ email: s__('Crm|Email'),
+ phone: s__('Crm|Phone number (optional)'),
+ description: s__('Crm|Description (optional)'),
+ newTitle: s__('Crm|New contact'),
+ editTitle: s__('Crm|Edit contact'),
+ somethingWentWrong: __('Something went wrong. Please try again.'),
+ },
+};
+</script>
+
+<template>
+ <gl-drawer
+ class="gl-drawer-responsive"
+ :open="drawerOpen"
+ :header-height="getDrawerHeaderHeight()"
+ @close="close(false)"
+ >
+ <template #title>
+ <h3>{{ title }}</h3>
+ </template>
+ <gl-alert v-if="errorMessages.length" variant="danger" @dismiss="errorMessages = []">
+ <ul class="gl-mb-0! gl-ml-5">
+ <li v-for="error in errorMessages" :key="error">
+ {{ error }}
+ </li>
+ </ul>
+ </gl-alert>
+ <form @submit.prevent="save">
+ <gl-form-group :label="$options.i18n.firstName" label-for="contact-first-name">
+ <gl-form-input id="contact-first-name" v-model="firstName" />
+ </gl-form-group>
+ <gl-form-group :label="$options.i18n.lastName" label-for="contact-last-name">
+ <gl-form-input id="contact-last-name" v-model="lastName" />
+ </gl-form-group>
+ <gl-form-group :label="$options.i18n.email" label-for="contact-email">
+ <gl-form-input id="contact-email" v-model="email" />
+ </gl-form-group>
+ <gl-form-group :label="$options.i18n.phone" label-for="contact-phone">
+ <gl-form-input id="contact-phone" v-model="phone" />
+ </gl-form-group>
+ <gl-form-group :label="$options.i18n.description" label-for="contact-description">
+ <gl-form-input id="contact-description" v-model="description" />
+ </gl-form-group>
+ <span class="gl-float-right">
+ <gl-button data-testid="cancel-button" @click="close(false)">
+ {{ $options.i18n.cancel }}
+ </gl-button>
+ <gl-button
+ variant="confirm"
+ :disabled="invalid"
+ :loading="submitting"
+ data-testid="save-contact-button"
+ type="submit"
+ >{{ buttonLabel }}</gl-button
+ >
+ </span>
+ </form>
+ </gl-drawer>
+</template>
diff --git a/app/assets/javascripts/crm/components/contacts_root.vue b/app/assets/javascripts/crm/components/contacts_root.vue
index 83c02f7d5fe..178ce84c64d 100644
--- a/app/assets/javascripts/crm/components/contacts_root.vue
+++ b/app/assets/javascripts/crm/components/contacts_root.vue
@@ -1,17 +1,30 @@
<script>
-import { GlLoadingIcon, GlTable } from '@gitlab/ui';
-import createFlash from '~/flash';
+import { GlAlert, GlButton, GlLoadingIcon, GlTable, GlTooltipDirective } from '@gitlab/ui';
+import { parseBoolean } from '~/lib/utils/common_utils';
import { s__, __ } from '~/locale';
+import { convertToGraphQLId, getIdFromGraphQLId } from '~/graphql_shared/utils';
+import { TYPE_CRM_CONTACT } from '~/graphql_shared/constants';
+import { INDEX_ROUTE_NAME, NEW_ROUTE_NAME, EDIT_ROUTE_NAME } from '../constants';
import getGroupContactsQuery from './queries/get_group_contacts.query.graphql';
+import ContactForm from './contact_form.vue';
export default {
components: {
+ GlAlert,
+ GlButton,
GlLoadingIcon,
GlTable,
+ ContactForm,
},
- inject: ['groupFullPath'],
+ directives: {
+ GlTooltip: GlTooltipDirective,
+ },
+ inject: ['groupFullPath', 'groupIssuesPath', 'canAdminCrmContact'],
data() {
- return { contacts: [] };
+ return {
+ contacts: [],
+ error: false,
+ };
},
apollo: {
contacts: {
@@ -26,12 +39,8 @@ export default {
update(data) {
return this.extractContacts(data);
},
- error(error) {
- createFlash({
- message: __('Something went wrong. Please try again.'),
- error,
- captureError: true,
- });
+ error() {
+ this.error = true;
},
},
},
@@ -39,12 +48,51 @@ export default {
isLoading() {
return this.$apollo.queries.contacts.loading;
},
+ showNewForm() {
+ return this.$route.name === NEW_ROUTE_NAME;
+ },
+ showEditForm() {
+ return !this.isLoading && this.$route.name === EDIT_ROUTE_NAME;
+ },
+ canAdmin() {
+ return parseBoolean(this.canAdminCrmContact);
+ },
+ editingContact() {
+ return this.contacts.find(
+ (contact) => contact.id === convertToGraphQLId(TYPE_CRM_CONTACT, this.$route.params.id),
+ );
+ },
},
methods: {
extractContacts(data) {
const contacts = data?.group?.contacts?.nodes || [];
return contacts.slice().sort((a, b) => a.firstName.localeCompare(b.firstName));
},
+ displayNewForm() {
+ if (this.showNewForm) return;
+
+ this.$router.push({ name: NEW_ROUTE_NAME });
+ },
+ hideNewForm(success) {
+ if (success) this.$toast.show(s__('Crm|Contact has been added'));
+
+ this.$router.replace({ name: INDEX_ROUTE_NAME });
+ },
+ hideEditForm(success) {
+ if (success) this.$toast.show(s__('Crm|Contact has been updated'));
+
+ this.editingContactId = 0;
+ this.$router.replace({ name: INDEX_ROUTE_NAME });
+ },
+ getIssuesPath(path, value) {
+ return `${path}?scope=all&state=opened&crm_contact_id=${value}`;
+ },
+ edit(value) {
+ if (this.showEditForm) return;
+
+ this.editingContactId = value;
+ this.$router.push({ name: EDIT_ROUTE_NAME, params: { id: value } });
+ },
},
fields: [
{ key: 'firstName', sortable: true },
@@ -59,22 +107,81 @@ export default {
},
sortable: true,
},
+ {
+ key: 'id',
+ label: '',
+ formatter: (id) => {
+ return getIdFromGraphQLId(id);
+ },
+ },
],
i18n: {
emptyText: s__('Crm|No contacts found'),
+ issuesButtonLabel: __('View issues'),
+ editButtonLabel: __('Edit'),
+ title: s__('Crm|Customer Relations Contacts'),
+ newContact: s__('Crm|New contact'),
+ errorText: __('Something went wrong. Please try again.'),
},
};
</script>
<template>
<div>
+ <gl-alert v-if="error" variant="danger" class="gl-mt-6" @dismiss="error = false">
+ {{ $options.i18n.errorText }}
+ </gl-alert>
+ <div
+ class="gl-display-flex gl-align-items-baseline gl-flex-direction-row gl-justify-content-space-between gl-mt-6"
+ >
+ <h2 class="gl-font-size-h2 gl-my-0">
+ {{ $options.i18n.title }}
+ </h2>
+ <div class="gl-display-none gl-md-display-flex gl-align-items-center gl-justify-content-end">
+ <gl-button
+ v-if="canAdmin"
+ variant="confirm"
+ data-testid="new-contact-button"
+ @click="displayNewForm"
+ >
+ {{ $options.i18n.newContact }}
+ </gl-button>
+ </div>
+ </div>
+ <contact-form v-if="showNewForm" :drawer-open="showNewForm" @close="hideNewForm" />
+ <contact-form
+ v-if="showEditForm"
+ :contact="editingContact"
+ :drawer-open="showEditForm"
+ @close="hideEditForm"
+ />
<gl-loading-icon v-if="isLoading" class="gl-mt-5" size="lg" />
<gl-table
v-else
+ class="gl-mt-5"
:items="contacts"
:fields="$options.fields"
:empty-text="$options.i18n.emptyText"
show-empty
- />
+ >
+ <template #cell(id)="data">
+ <gl-button
+ v-gl-tooltip.hover.bottom="$options.i18n.issuesButtonLabel"
+ class="gl-mr-3"
+ data-testid="issues-link"
+ icon="issues"
+ :aria-label="$options.i18n.issuesButtonLabel"
+ :href="getIssuesPath(groupIssuesPath, data.value)"
+ />
+ <gl-button
+ v-if="canAdmin"
+ v-gl-tooltip.hover.bottom="$options.i18n.editButtonLabel"
+ data-testid="edit-contact-button"
+ icon="pencil"
+ :aria-label="$options.i18n.editButtonLabel"
+ @click="edit(data.value)"
+ />
+ </template>
+ </gl-table>
</div>
</template>
diff --git a/app/assets/javascripts/crm/components/new_organization_form.vue b/app/assets/javascripts/crm/components/new_organization_form.vue
new file mode 100644
index 00000000000..3b11edc6935
--- /dev/null
+++ b/app/assets/javascripts/crm/components/new_organization_form.vue
@@ -0,0 +1,164 @@
+<script>
+import { GlAlert, GlButton, GlDrawer, GlFormGroup, GlFormInput } from '@gitlab/ui';
+import { produce } from 'immer';
+import { __, s__ } from '~/locale';
+import { convertToGraphQLId } from '~/graphql_shared/utils';
+import { TYPE_GROUP } from '~/graphql_shared/constants';
+import createOrganization from './queries/create_organization.mutation.graphql';
+import getGroupOrganizationsQuery from './queries/get_group_organizations.query.graphql';
+
+export default {
+ components: {
+ GlAlert,
+ GlButton,
+ GlDrawer,
+ GlFormGroup,
+ GlFormInput,
+ },
+ inject: ['groupFullPath', 'groupId'],
+ props: {
+ drawerOpen: {
+ type: Boolean,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ name: '',
+ defaultRate: null,
+ description: '',
+ submitting: false,
+ errorMessages: [],
+ };
+ },
+ computed: {
+ invalid() {
+ return this.name.trim() === '';
+ },
+ },
+ methods: {
+ save() {
+ this.submitting = true;
+ return this.$apollo
+ .mutate({
+ mutation: createOrganization,
+ variables: {
+ input: {
+ groupId: convertToGraphQLId(TYPE_GROUP, this.groupId),
+ name: this.name,
+ defaultRate: this.defaultRate ? parseFloat(this.defaultRate) : null,
+ description: this.description,
+ },
+ },
+ update: this.updateCache,
+ })
+ .then(({ data }) => {
+ if (data.customerRelationsOrganizationCreate.errors.length === 0) this.close(true);
+
+ this.submitting = false;
+ })
+ .catch(() => {
+ this.errorMessages = [this.$options.i18n.somethingWentWrong];
+ this.submitting = false;
+ });
+ },
+ close(success) {
+ this.$emit('close', success);
+ },
+ updateCache(store, { data: { customerRelationsOrganizationCreate } }) {
+ if (customerRelationsOrganizationCreate.errors.length > 0) {
+ this.errorMessages = customerRelationsOrganizationCreate.errors;
+ return;
+ }
+
+ const variables = {
+ groupFullPath: this.groupFullPath,
+ };
+ const sourceData = store.readQuery({
+ query: getGroupOrganizationsQuery,
+ variables,
+ });
+
+ const data = produce(sourceData, (draftState) => {
+ draftState.group.organizations.nodes = [
+ ...sourceData.group.organizations.nodes,
+ customerRelationsOrganizationCreate.organization,
+ ];
+ });
+
+ store.writeQuery({
+ query: getGroupOrganizationsQuery,
+ variables,
+ data,
+ });
+ },
+ getDrawerHeaderHeight() {
+ const wrapperEl = document.querySelector('.content-wrapper');
+
+ if (wrapperEl) {
+ return `${wrapperEl.offsetTop}px`;
+ }
+
+ return '';
+ },
+ },
+ i18n: {
+ buttonLabel: s__('Crm|Create organization'),
+ cancel: __('Cancel'),
+ name: __('Name'),
+ defaultRate: s__('Crm|Default rate (optional)'),
+ description: __('Description (optional)'),
+ title: s__('Crm|New Organization'),
+ somethingWentWrong: __('Something went wrong. Please try again.'),
+ },
+};
+</script>
+
+<template>
+ <gl-drawer
+ class="gl-drawer-responsive"
+ :open="drawerOpen"
+ :header-height="getDrawerHeaderHeight()"
+ @close="close(false)"
+ >
+ <template #title>
+ <h4>{{ $options.i18n.title }}</h4>
+ </template>
+ <gl-alert v-if="errorMessages.length" variant="danger" @dismiss="errorMessages = []">
+ <ul class="gl-mb-0! gl-ml-5">
+ <li v-for="error in errorMessages" :key="error">
+ {{ error }}
+ </li>
+ </ul>
+ </gl-alert>
+ <form @submit.prevent="save">
+ <gl-form-group :label="$options.i18n.name" label-for="organization-name">
+ <gl-form-input id="organization-name" v-model="name" />
+ </gl-form-group>
+ <gl-form-group :label="$options.i18n.defaultRate" label-for="organization-default-rate">
+ <gl-form-input
+ id="organization-default-rate"
+ v-model="defaultRate"
+ type="number"
+ step="0.01"
+ />
+ </gl-form-group>
+ <gl-form-group :label="$options.i18n.description" label-for="organization-description">
+ <gl-form-input id="organization-description" v-model="description" />
+ </gl-form-group>
+ <span class="gl-float-right">
+ <gl-button data-testid="cancel-button" @click="close(false)">
+ {{ $options.i18n.cancel }}
+ </gl-button>
+ <gl-button
+ variant="confirm"
+ :disabled="invalid"
+ :loading="submitting"
+ data-testid="create-new-organization-button"
+ type="submit"
+ >{{ $options.i18n.buttonLabel }}</gl-button
+ >
+ </span>
+ </form>
+ </gl-drawer>
+</template>
diff --git a/app/assets/javascripts/crm/components/organizations_root.vue b/app/assets/javascripts/crm/components/organizations_root.vue
index 98b45d0a042..9370c6377e9 100644
--- a/app/assets/javascripts/crm/components/organizations_root.vue
+++ b/app/assets/javascripts/crm/components/organizations_root.vue
@@ -1,17 +1,29 @@
<script>
-import { GlLoadingIcon, GlTable } from '@gitlab/ui';
-import createFlash from '~/flash';
+import { GlAlert, GlButton, GlLoadingIcon, GlTable, GlTooltipDirective } from '@gitlab/ui';
+import { parseBoolean } from '~/lib/utils/common_utils';
import { s__, __ } from '~/locale';
+import { getIdFromGraphQLId } from '~/graphql_shared/utils';
+import { INDEX_ROUTE_NAME, NEW_ROUTE_NAME } from '../constants';
import getGroupOrganizationsQuery from './queries/get_group_organizations.query.graphql';
+import NewOrganizationForm from './new_organization_form.vue';
export default {
components: {
+ GlAlert,
+ GlButton,
GlLoadingIcon,
GlTable,
+ NewOrganizationForm,
},
- inject: ['groupFullPath'],
+ directives: {
+ GlTooltip: GlTooltipDirective,
+ },
+ inject: ['canAdminCrmOrganization', 'groupFullPath', 'groupIssuesPath'],
data() {
- return { organizations: [] };
+ return {
+ error: false,
+ organizations: [],
+ };
},
apollo: {
organizations: {
@@ -26,12 +38,8 @@ export default {
update(data) {
return this.extractOrganizations(data);
},
- error(error) {
- createFlash({
- message: __('Something went wrong. Please try again.'),
- error,
- captureError: true,
- });
+ error() {
+ this.error = true;
},
},
},
@@ -39,33 +47,94 @@ export default {
isLoading() {
return this.$apollo.queries.organizations.loading;
},
+ showNewForm() {
+ return this.$route.name === NEW_ROUTE_NAME;
+ },
+ canCreateNew() {
+ return parseBoolean(this.canAdminCrmOrganization);
+ },
},
methods: {
extractOrganizations(data) {
const organizations = data?.group?.organizations?.nodes || [];
return organizations.slice().sort((a, b) => a.name.localeCompare(b.name));
},
+ getIssuesPath(path, value) {
+ return `${path}?scope=all&state=opened&crm_organization_id=${value}`;
+ },
+ displayNewForm() {
+ if (this.showNewForm) return;
+
+ this.$router.push({ name: NEW_ROUTE_NAME });
+ },
+ hideNewForm(success) {
+ if (success) this.$toast.show(this.$options.i18n.organizationAdded);
+
+ this.$router.replace({ name: INDEX_ROUTE_NAME });
+ },
},
fields: [
{ key: 'name', sortable: true },
{ key: 'defaultRate', sortable: true },
{ key: 'description', sortable: true },
+ {
+ key: 'id',
+ label: __('Issues'),
+ formatter: (id) => {
+ return getIdFromGraphQLId(id);
+ },
+ },
],
i18n: {
emptyText: s__('Crm|No organizations found'),
+ issuesButtonLabel: __('View issues'),
+ title: s__('Crm|Customer Relations Organizations'),
+ newOrganization: s__('Crm|New organization'),
+ errorText: __('Something went wrong. Please try again.'),
+ organizationAdded: s__('Crm|Organization has been added'),
},
};
</script>
<template>
<div>
+ <gl-alert v-if="error" variant="danger" class="gl-mt-6" @dismiss="error = false">
+ {{ $options.i18n.errorText }}
+ </gl-alert>
+ <div
+ class="gl-display-flex gl-align-items-baseline gl-flex-direction-row gl-justify-content-space-between gl-mt-6"
+ >
+ <h2 class="gl-font-size-h2 gl-my-0">
+ {{ $options.i18n.title }}
+ </h2>
+ <div
+ v-if="canCreateNew"
+ class="gl-display-none gl-md-display-flex gl-align-items-center gl-justify-content-end"
+ >
+ <gl-button variant="confirm" data-testid="new-organization-button" @click="displayNewForm">
+ {{ $options.i18n.newOrganization }}
+ </gl-button>
+ </div>
+ </div>
+ <new-organization-form v-if="showNewForm" :drawer-open="showNewForm" @close="hideNewForm" />
<gl-loading-icon v-if="isLoading" class="gl-mt-5" size="lg" />
<gl-table
v-else
+ class="gl-mt-5"
:items="organizations"
:fields="$options.fields"
:empty-text="$options.i18n.emptyText"
show-empty
- />
+ >
+ <template #cell(id)="data">
+ <gl-button
+ v-gl-tooltip.hover.bottom="$options.i18n.issuesButtonLabel"
+ data-testid="issues-link"
+ icon="issues"
+ :aria-label="$options.i18n.issuesButtonLabel"
+ :href="getIssuesPath(groupIssuesPath, data.value)"
+ />
+ </template>
+ </gl-table>
</div>
</template>
diff --git a/app/assets/javascripts/crm/components/queries/create_contact.mutation.graphql b/app/assets/javascripts/crm/components/queries/create_contact.mutation.graphql
new file mode 100644
index 00000000000..e0192459609
--- /dev/null
+++ b/app/assets/javascripts/crm/components/queries/create_contact.mutation.graphql
@@ -0,0 +1,10 @@
+#import "./crm_contact_fields.fragment.graphql"
+
+mutation createContact($input: CustomerRelationsContactCreateInput!) {
+ customerRelationsContactCreate(input: $input) {
+ contact {
+ ...ContactFragment
+ }
+ errors
+ }
+}
diff --git a/app/assets/javascripts/crm/components/queries/create_organization.mutation.graphql b/app/assets/javascripts/crm/components/queries/create_organization.mutation.graphql
new file mode 100644
index 00000000000..2cc7e53ee9b
--- /dev/null
+++ b/app/assets/javascripts/crm/components/queries/create_organization.mutation.graphql
@@ -0,0 +1,10 @@
+#import "./crm_organization_fields.fragment.graphql"
+
+mutation createOrganization($input: CustomerRelationsOrganizationCreateInput!) {
+ customerRelationsOrganizationCreate(input: $input) {
+ organization {
+ ...OrganizationFragment
+ }
+ errors
+ }
+}
diff --git a/app/assets/javascripts/crm/components/queries/crm_contact_fields.fragment.graphql b/app/assets/javascripts/crm/components/queries/crm_contact_fields.fragment.graphql
new file mode 100644
index 00000000000..cef4083b446
--- /dev/null
+++ b/app/assets/javascripts/crm/components/queries/crm_contact_fields.fragment.graphql
@@ -0,0 +1,14 @@
+fragment ContactFragment on CustomerRelationsContact {
+ __typename
+ id
+ firstName
+ lastName
+ email
+ phone
+ description
+ organization {
+ __typename
+ id
+ name
+ }
+}
diff --git a/app/assets/javascripts/crm/components/queries/crm_organization_fields.fragment.graphql b/app/assets/javascripts/crm/components/queries/crm_organization_fields.fragment.graphql
new file mode 100644
index 00000000000..4adc5742d3a
--- /dev/null
+++ b/app/assets/javascripts/crm/components/queries/crm_organization_fields.fragment.graphql
@@ -0,0 +1,7 @@
+fragment OrganizationFragment on CustomerRelationsOrganization {
+ __typename
+ id
+ name
+ defaultRate
+ description
+}
diff --git a/app/assets/javascripts/crm/components/queries/get_group_contacts.query.graphql b/app/assets/javascripts/crm/components/queries/get_group_contacts.query.graphql
index f6acd258585..2a8150e42e3 100644
--- a/app/assets/javascripts/crm/components/queries/get_group_contacts.query.graphql
+++ b/app/assets/javascripts/crm/components/queries/get_group_contacts.query.graphql
@@ -1,21 +1,12 @@
+#import "./crm_contact_fields.fragment.graphql"
+
query contacts($groupFullPath: ID!) {
group(fullPath: $groupFullPath) {
__typename
id
contacts {
nodes {
- __typename
- id
- firstName
- lastName
- email
- phone
- description
- organization {
- __typename
- id
- name
- }
+ ...ContactFragment
}
}
}
diff --git a/app/assets/javascripts/crm/components/queries/get_group_organizations.query.graphql b/app/assets/javascripts/crm/components/queries/get_group_organizations.query.graphql
index 7c4ec6ec585..e8d8109431e 100644
--- a/app/assets/javascripts/crm/components/queries/get_group_organizations.query.graphql
+++ b/app/assets/javascripts/crm/components/queries/get_group_organizations.query.graphql
@@ -1,14 +1,12 @@
+#import "./crm_organization_fields.fragment.graphql"
+
query organizations($groupFullPath: ID!) {
group(fullPath: $groupFullPath) {
__typename
id
organizations {
nodes {
- __typename
- id
- name
- defaultRate
- description
+ ...OrganizationFragment
}
}
}
diff --git a/app/assets/javascripts/crm/components/queries/update_contact.mutation.graphql b/app/assets/javascripts/crm/components/queries/update_contact.mutation.graphql
new file mode 100644
index 00000000000..f55f6a10e0a
--- /dev/null
+++ b/app/assets/javascripts/crm/components/queries/update_contact.mutation.graphql
@@ -0,0 +1,10 @@
+#import "./crm_contact_fields.fragment.graphql"
+
+mutation updateContact($input: CustomerRelationsContactUpdateInput!) {
+ customerRelationsContactUpdate(input: $input) {
+ contact {
+ ...ContactFragment
+ }
+ errors
+ }
+}
diff --git a/app/assets/javascripts/crm/constants.js b/app/assets/javascripts/crm/constants.js
new file mode 100644
index 00000000000..3b085837aea
--- /dev/null
+++ b/app/assets/javascripts/crm/constants.js
@@ -0,0 +1,3 @@
+export const INDEX_ROUTE_NAME = 'index';
+export const NEW_ROUTE_NAME = 'new';
+export const EDIT_ROUTE_NAME = 'edit';
diff --git a/app/assets/javascripts/crm/contacts_bundle.js b/app/assets/javascripts/crm/contacts_bundle.js
index 6438953596e..f49ec64210f 100644
--- a/app/assets/javascripts/crm/contacts_bundle.js
+++ b/app/assets/javascripts/crm/contacts_bundle.js
@@ -1,9 +1,14 @@
+import { GlToast } from '@gitlab/ui';
import Vue from 'vue';
import VueApollo from 'vue-apollo';
+import VueRouter from 'vue-router';
import createDefaultClient from '~/lib/graphql';
import CrmContactsRoot from './components/contacts_root.vue';
+import routes from './routes';
Vue.use(VueApollo);
+Vue.use(VueRouter);
+Vue.use(GlToast);
export default () => {
const el = document.getElementById('js-crm-contacts-app');
@@ -16,10 +21,19 @@ export default () => {
return false;
}
+ const { basePath, groupFullPath, groupIssuesPath, canAdminCrmContact, groupId } = el.dataset;
+
+ const router = new VueRouter({
+ base: basePath,
+ mode: 'history',
+ routes,
+ });
+
return new Vue({
el,
+ router,
apolloProvider,
- provide: { groupFullPath: el.dataset.groupFullPath },
+ provide: { groupFullPath, groupIssuesPath, canAdminCrmContact, groupId },
render(createElement) {
return createElement(CrmContactsRoot);
},
diff --git a/app/assets/javascripts/crm/organizations_bundle.js b/app/assets/javascripts/crm/organizations_bundle.js
index ac9990b9fb4..828d7cd426c 100644
--- a/app/assets/javascripts/crm/organizations_bundle.js
+++ b/app/assets/javascripts/crm/organizations_bundle.js
@@ -1,9 +1,14 @@
+import { GlToast } from '@gitlab/ui';
import Vue from 'vue';
import VueApollo from 'vue-apollo';
+import VueRouter from 'vue-router';
import createDefaultClient from '~/lib/graphql';
import CrmOrganizationsRoot from './components/organizations_root.vue';
+import routes from './routes';
Vue.use(VueApollo);
+Vue.use(VueRouter);
+Vue.use(GlToast);
export default () => {
const el = document.getElementById('js-crm-organizations-app');
@@ -16,10 +21,19 @@ export default () => {
return false;
}
+ const { basePath, canAdminCrmOrganization, groupFullPath, groupId, groupIssuesPath } = el.dataset;
+
+ const router = new VueRouter({
+ base: basePath,
+ mode: 'history',
+ routes,
+ });
+
return new Vue({
el,
+ router,
apolloProvider,
- provide: { groupFullPath: el.dataset.groupFullPath },
+ provide: { canAdminCrmOrganization, groupFullPath, groupId, groupIssuesPath },
render(createElement) {
return createElement(CrmOrganizationsRoot);
},
diff --git a/app/assets/javascripts/crm/routes.js b/app/assets/javascripts/crm/routes.js
new file mode 100644
index 00000000000..12aa17d73b6
--- /dev/null
+++ b/app/assets/javascripts/crm/routes.js
@@ -0,0 +1,16 @@
+import { INDEX_ROUTE_NAME, NEW_ROUTE_NAME, EDIT_ROUTE_NAME } from './constants';
+
+export default [
+ {
+ name: INDEX_ROUTE_NAME,
+ path: '/',
+ },
+ {
+ name: NEW_ROUTE_NAME,
+ path: '/new',
+ },
+ {
+ name: EDIT_ROUTE_NAME,
+ path: '/:id/edit',
+ },
+];
diff --git a/app/assets/javascripts/delete_label_modal.js b/app/assets/javascripts/delete_label_modal.js
deleted file mode 100644
index cf7c9e7734f..00000000000
--- a/app/assets/javascripts/delete_label_modal.js
+++ /dev/null
@@ -1,16 +0,0 @@
-import Vue from 'vue';
-import DeleteLabelModal from '~/vue_shared/components/delete_label_modal.vue';
-
-const mountDeleteLabelModal = (optionalProps) =>
- new Vue({
- render(h) {
- return h(DeleteLabelModal, {
- props: {
- selector: '.js-delete-label-modal-button',
- ...optionalProps,
- },
- });
- },
- }).$mount();
-
-export default (optionalProps = {}) => mountDeleteLabelModal(optionalProps);
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 813f87452d8..10976202d06 100644
--- a/app/assets/javascripts/design_management/components/design_notes/design_discussion.vue
+++ b/app/assets/javascripts/design_management/components/design_notes/design_discussion.vue
@@ -1,11 +1,12 @@
<script>
-import { GlTooltipDirective, GlIcon, GlLoadingIcon, GlLink, GlBadge } from '@gitlab/ui';
+import { GlTooltipDirective, GlIcon, GlLoadingIcon, GlLink } from '@gitlab/ui';
import { ApolloMutation } from 'vue-apollo';
import createFlash from '~/flash';
import { s__ } from '~/locale';
import ReplyPlaceholder from '~/notes/components/discussion_reply_placeholder.vue';
import { updateGlobalTodoCount } from '~/vue_shared/components/sidebar/todo_toggle/utils';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
+import DesignNotePin from '~/vue_shared/components/design_management/design_note_pin.vue';
import { ACTIVE_DISCUSSION_SOURCE_TYPES } from '../../constants';
import createNoteMutation from '../../graphql/mutations/create_note.mutation.graphql';
import toggleResolveDiscussionMutation from '../../graphql/mutations/toggle_resolve_discussion.mutation.graphql';
@@ -30,7 +31,7 @@ export default {
GlLink,
ToggleRepliesWidget,
TimeAgoTooltip,
- GlBadge,
+ DesignNotePin,
},
directives: {
GlTooltip: GlTooltipDirective,
@@ -213,12 +214,7 @@ export default {
<template>
<div class="design-discussion-wrapper">
- <gl-badge
- class="gl-display-flex gl-align-items-center gl-justify-content-center gl-cursor-pointer"
- :class="{ resolved: discussion.resolved }"
- >
- {{ discussion.index }}
- </gl-badge>
+ <design-note-pin :is-resolved="discussion.resolved" :label="discussion.index" />
<ul
class="design-discussion bordered-box gl-relative gl-p-0 gl-list-style-none"
data-qa-selector="design_discussion_content"
diff --git a/app/assets/javascripts/design_management/components/design_overlay.vue b/app/assets/javascripts/design_management/components/design_overlay.vue
index 7815a57ce18..b058709b316 100644
--- a/app/assets/javascripts/design_management/components/design_overlay.vue
+++ b/app/assets/javascripts/design_management/components/design_overlay.vue
@@ -1,9 +1,9 @@
<script>
import { __ } from '~/locale';
+import DesignNotePin from '~/vue_shared/components/design_management/design_note_pin.vue';
import { ACTIVE_DISCUSSION_SOURCE_TYPES } from '../constants';
import updateActiveDiscussionMutation from '../graphql/mutations/update_active_discussion.mutation.graphql';
import activeDiscussionQuery from '../graphql/queries/active_discussion.query.graphql';
-import DesignNotePin from './design_note_pin.vue';
export default {
name: 'DesignOverlay',
@@ -251,9 +251,6 @@ export default {
!discussionNotes.some(({ id }) => id === this.activeDiscussion.id)
);
},
- designPinClass(note) {
- return { inactive: this.isNoteInactive(note), resolved: note.resolved };
- },
},
i18n: {
newCommentButtonLabel: __('Add comment to design'),
@@ -287,7 +284,8 @@ export default {
? getNotePositionStyle(movingNoteNewPosition)
: getNotePositionStyle(note.position)
"
- :class="designPinClass(note)"
+ :is-inactive="isNoteInactive(note)"
+ :is-resolved="note.resolved"
@mousedown.stop="onNoteMousedown($event, note)"
@mouseup.stop="onNoteMouseup(note)"
/>
diff --git a/app/assets/javascripts/design_management/graphql/fragments/discussion_resolved_status.fragment.graphql b/app/assets/javascripts/design_management/graphql/fragments/discussion_resolved_status.fragment.graphql
index 7483b508721..9ad85017921 100644
--- a/app/assets/javascripts/design_management/graphql/fragments/discussion_resolved_status.fragment.graphql
+++ b/app/assets/javascripts/design_management/graphql/fragments/discussion_resolved_status.fragment.graphql
@@ -1,8 +1,10 @@
fragment ResolvedStatus on Discussion {
+ id
resolvable
resolved
resolvedAt
resolvedBy {
+ id
name
webUrl
}
diff --git a/app/assets/javascripts/design_management/graphql/mutations/upload_design.mutation.graphql b/app/assets/javascripts/design_management/graphql/mutations/upload_design.mutation.graphql
index 111f5ac18a7..34d683ac1ee 100644
--- a/app/assets/javascripts/design_management/graphql/mutations/upload_design.mutation.graphql
+++ b/app/assets/javascripts/design_management/graphql/mutations/upload_design.mutation.graphql
@@ -3,6 +3,7 @@
mutation uploadDesign($files: [Upload!]!, $projectPath: ID!, $iid: ID!) {
designManagementUpload(input: { projectPath: $projectPath, iid: $iid, files: $files }) {
+ # eslint-disable-next-line @graphql-eslint/require-id-when-available
designs {
...DesignItem
versions {
@@ -14,6 +15,7 @@ mutation uploadDesign($files: [Upload!]!, $projectPath: ID!, $iid: ID!) {
}
}
skippedDesigns {
+ id
filename
}
errors
diff --git a/app/assets/javascripts/design_management/graphql/queries/get_design.query.graphql b/app/assets/javascripts/design_management/graphql/queries/get_design.query.graphql
index 99a61191c6e..a5394457f73 100644
--- a/app/assets/javascripts/design_management/graphql/queries/get_design.query.graphql
+++ b/app/assets/javascripts/design_management/graphql/queries/get_design.query.graphql
@@ -10,8 +10,10 @@ query getDesign(
project(fullPath: $fullPath) {
id
issue(iid: $iid) {
+ id
designCollection {
designs(atVersion: $atVersion, filenames: $filenames) {
+ # eslint-disable-next-line @graphql-eslint/require-id-when-available
nodes {
...DesignItem
issue {
diff --git a/app/assets/javascripts/diffs/components/app.vue b/app/assets/javascripts/diffs/components/app.vue
index f405b82b05b..66d06a3a1b6 100644
--- a/app/assets/javascripts/diffs/components/app.vue
+++ b/app/assets/javascripts/diffs/components/app.vue
@@ -44,7 +44,6 @@ import {
TRACKING_MULTIPLE_FILES_MODE,
} from '../constants';
-import { discussionIntersectionObserverHandlerFactory } from '../utils/discussions';
import diffsEventHub from '../event_hub';
import { reviewStatuses } from '../utils/file_reviews';
import { diffsApp } from '../utils/performance';
@@ -87,9 +86,6 @@ export default {
ALERT_MERGE_CONFLICT,
ALERT_COLLAPSED_FILES,
},
- provide: {
- discussionObserverHandler: discussionIntersectionObserverHandlerFactory(),
- },
props: {
endpoint: {
type: String,
diff --git a/app/assets/javascripts/diffs/components/diff_file.vue b/app/assets/javascripts/diffs/components/diff_file.vue
index d09cc064b2c..4e77bf81c1e 100644
--- a/app/assets/javascripts/diffs/components/diff_file.vue
+++ b/app/assets/javascripts/diffs/components/diff_file.vue
@@ -10,6 +10,7 @@ import {
import { escape } from 'lodash';
import { mapActions, mapGetters, mapState } from 'vuex';
import { IdState } from 'vendor/vue-virtual-scroller';
+import DiffContent from 'jh_else_ce/diffs/components/diff_content.vue';
import createFlash from '~/flash';
import { hasDiff } from '~/helpers/diffs_helper';
import { diffViewerErrors } from '~/ide/constants';
@@ -28,7 +29,6 @@ import {
import eventHub from '../event_hub';
import { DIFF_FILE, GENERIC_ERROR, CONFLICT_TEXT } from '../i18n';
import { collapsedType, getShortShaFromFile } from '../utils/diff_file';
-import DiffContent from './diff_content.vue';
import DiffFileHeader from './diff_file_header.vue';
export default {
diff --git a/app/assets/javascripts/diffs/components/diff_row.vue b/app/assets/javascripts/diffs/components/diff_row.vue
index 4e33a02ca0e..4893803a3b6 100644
--- a/app/assets/javascripts/diffs/components/diff_row.vue
+++ b/app/assets/javascripts/diffs/components/diff_row.vue
@@ -42,6 +42,11 @@ export default {
required: false,
default: false,
},
+ coverageLoaded: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
inline: {
type: Boolean,
required: false,
@@ -83,14 +88,15 @@ export default {
if (!props.inline || !props.line.left) return {};
return props.fileLineCoverage(props.filePath, props.line.left.new_line);
},
- (props) => [props.inline, props.filePath, props.line.left?.new_line].join(':'),
+ (props) =>
+ [props.inline, props.filePath, props.line.left?.new_line, props.coverageLoaded].join(':'),
),
coverageStateRight: memoize(
(props) => {
if (!props.line.right) return {};
return props.fileLineCoverage(props.filePath, props.line.right.new_line);
},
- (props) => [props.line.right?.new_line, props.filePath].join(':'),
+ (props) => [props.line.right?.new_line, props.filePath, props.coverageLoaded].join(':'),
),
showCodequalityLeft: memoize(
(props) => {
diff --git a/app/assets/javascripts/diffs/components/diff_view.vue b/app/assets/javascripts/diffs/components/diff_view.vue
index 55c796182ee..8562a1d44e7 100644
--- a/app/assets/javascripts/diffs/components/diff_view.vue
+++ b/app/assets/javascripts/diffs/components/diff_view.vue
@@ -52,7 +52,7 @@ export default {
},
computed: {
...mapGetters('diffs', ['commitId', 'fileLineCoverage']),
- ...mapState('diffs', ['codequalityDiff', 'highlightedRow']),
+ ...mapState('diffs', ['codequalityDiff', 'highlightedRow', 'coverageLoaded']),
...mapState({
selectedCommentPosition: ({ notes }) => notes.selectedCommentPosition,
selectedCommentPositionHover: ({ notes }) => notes.selectedCommentPositionHover,
@@ -180,6 +180,7 @@ export default {
:index="index"
:is-highlighted="isHighlighted(line)"
:file-line-coverage="fileLineCoverage"
+ :coverage-loaded="coverageLoaded"
@showCommentForm="(code) => singleLineComment(code, line)"
@setHighlightedRow="setHighlightedRow"
@toggleLineDiscussions="
diff --git a/app/assets/javascripts/diffs/store/modules/diff_state.js b/app/assets/javascripts/diffs/store/modules/diff_state.js
index a5b1a577a78..5f66360a040 100644
--- a/app/assets/javascripts/diffs/store/modules/diff_state.js
+++ b/app/assets/javascripts/diffs/store/modules/diff_state.js
@@ -21,6 +21,7 @@ export default () => ({
startVersion: null, // Null unless a target diff is selected for comparison that is not the "base" diff
diffFiles: [],
coverageFiles: {},
+ coverageLoaded: false,
mergeRequestDiffs: [],
mergeRequestDiff: null,
diffViewType: getViewTypeFromQueryString() || viewTypeFromCookie || defaultViewType,
diff --git a/app/assets/javascripts/diffs/store/mutations.js b/app/assets/javascripts/diffs/store/mutations.js
index 4a9df0eafcc..fb35114c0a9 100644
--- a/app/assets/javascripts/diffs/store/mutations.js
+++ b/app/assets/javascripts/diffs/store/mutations.js
@@ -86,7 +86,7 @@ export default {
},
[types.SET_COVERAGE_DATA](state, coverageFiles) {
- Object.assign(state, { coverageFiles });
+ Object.assign(state, { coverageFiles, coverageLoaded: true });
},
[types.RENDER_FILE](state, file) {
diff --git a/app/assets/javascripts/diffs/utils/discussions.js b/app/assets/javascripts/diffs/utils/discussions.js
deleted file mode 100644
index c404705d209..00000000000
--- a/app/assets/javascripts/diffs/utils/discussions.js
+++ /dev/null
@@ -1,76 +0,0 @@
-function normalize(processable) {
- const { entry } = processable;
- const offset = entry.rootBounds.bottom - entry.boundingClientRect.top;
- const direction =
- offset < 0 ? 'Up' : 'Down'; /* eslint-disable-line @gitlab/require-i18n-strings */
-
- return {
- ...processable,
- entry: {
- time: entry.time,
- type: entry.isIntersecting ? 'intersection' : `scroll${direction}`,
- },
- };
-}
-
-function sort({ entry: alpha }, { entry: beta }) {
- const diff = alpha.time - beta.time;
- let order = 0;
-
- if (diff < 0) {
- order = -1;
- } else if (diff > 0) {
- order = 1;
- } else if (alpha.type === 'intersection' && beta.type === 'scrollUp') {
- order = 2;
- } else if (alpha.type === 'scrollUp' && beta.type === 'intersection') {
- order = -2;
- }
-
- return order;
-}
-
-function filter(entry) {
- return entry.type !== 'scrollDown';
-}
-
-export function discussionIntersectionObserverHandlerFactory() {
- let unprocessed = [];
- let timer = null;
-
- return (processable) => {
- unprocessed.push(processable);
-
- if (timer) {
- clearTimeout(timer);
- }
-
- timer = setTimeout(() => {
- unprocessed
- .map(normalize)
- .filter(filter)
- .sort(sort)
- .forEach((discussionObservationContainer) => {
- const {
- entry: { type },
- currentDiscussion,
- isFirstUnresolved,
- isDiffsPage,
- functions: { setCurrentDiscussionId, getPreviousUnresolvedDiscussionId },
- } = discussionObservationContainer;
-
- if (type === 'intersection') {
- setCurrentDiscussionId(currentDiscussion.id);
- } else if (type === 'scrollUp') {
- setCurrentDiscussionId(
- isFirstUnresolved
- ? null
- : getPreviousUnresolvedDiscussionId(currentDiscussion.id, isDiffsPage),
- );
- }
- });
-
- unprocessed = [];
- }, 0);
- };
-}
diff --git a/app/assets/javascripts/dropzone_input.js b/app/assets/javascripts/dropzone_input.js
index f404fa4e0e8..7c7127dfa44 100644
--- a/app/assets/javascripts/dropzone_input.js
+++ b/app/assets/javascripts/dropzone_input.js
@@ -44,6 +44,7 @@ export default function dropzoneInput(form, config = { parallelUploads: 2 }) {
let addFileToForm;
let updateAttachingMessage;
let uploadFile;
+ let hasPlainText;
formTextarea.wrap('<div class="div-dropzone"></div>');
formTextarea.on('paste', (event) => handlePaste(event));
@@ -184,7 +185,7 @@ export default function dropzoneInput(form, config = { parallelUploads: 2 }) {
event.preventDefault();
const text = converter.convertToTableMarkdown();
pasteText(text);
- } else {
+ } else if (!hasPlainText(pasteEvent)) {
const fileList = [...clipboardData.files];
fileList.forEach((file) => {
if (file.type.indexOf('image') !== -1) {
@@ -203,6 +204,11 @@ export default function dropzoneInput(form, config = { parallelUploads: 2 }) {
}
};
+ hasPlainText = (data) => {
+ const clipboardDataList = [...data.clipboardData.items];
+ return clipboardDataList.some((item) => item.type === 'text/plain');
+ };
+
pasteText = (text, shouldPad) => {
let formattedText = text;
if (shouldPad) {
diff --git a/app/assets/javascripts/editor/constants.js b/app/assets/javascripts/editor/constants.js
index e855e304d27..2ae9c377683 100644
--- a/app/assets/javascripts/editor/constants.js
+++ b/app/assets/javascripts/editor/constants.js
@@ -42,6 +42,10 @@ export const EDITOR_EXTENSION_STORE_IS_MISSING_ERROR = s__(
// EXTENSIONS' CONSTANTS
//
+// Source Editor Base Extension
+export const EXTENSION_BASE_LINE_LINK_ANCHOR_CLASS = 'link-anchor';
+export const EXTENSION_BASE_LINE_NUMBERS_CLASS = 'line-numbers';
+
// For CI config schemas the filename must match
// '*.gitlab-ci.yml' regardless of project configuration.
// https://gitlab.com/gitlab-org/gitlab/-/issues/293641
diff --git a/app/assets/javascripts/editor/extensions/example_source_editor_extension.js b/app/assets/javascripts/editor/extensions/example_source_editor_extension.js
index 119a2aea9eb..52e2bb0b5ff 100644
--- a/app/assets/javascripts/editor/extensions/example_source_editor_extension.js
+++ b/app/assets/javascripts/editor/extensions/example_source_editor_extension.js
@@ -7,6 +7,16 @@
export class MyFancyExtension {
/**
+ * A required getter returning the extension's name
+ * We have to provide it for every extension instead of relying on the built-in
+ * `name` prop because the prop does not survive the webpack's minification
+ * and the name mangling.
+ * @returns {string}
+ */
+ static get extensionName() {
+ return 'MyFancyExtension';
+ }
+ /**
* THE LIFE-CYCLE CALLBACKS
*/
@@ -16,11 +26,11 @@ export class MyFancyExtension {
* actions, keystrokes, update options, etc.
* Is called only once before the extension gets registered
*
- * @param { Object } [setupOptions] The setupOptions object
* @param { Object } [instance] The Source Editor instance
+ * @param { Object } [setupOptions] The setupOptions object
*/
// eslint-disable-next-line class-methods-use-this,no-unused-vars
- onSetup(setupOptions, instance) {}
+ onSetup(instance, setupOptions) {}
/**
* The first thing called after the extension is
diff --git a/app/assets/javascripts/editor/extensions/source_editor_ci_schema_ext.js b/app/assets/javascripts/editor/extensions/source_editor_ci_schema_ext.js
index 7069568275d..0290bb84b5f 100644
--- a/app/assets/javascripts/editor/extensions/source_editor_ci_schema_ext.js
+++ b/app/assets/javascripts/editor/extensions/source_editor_ci_schema_ext.js
@@ -1,32 +1,27 @@
import ciSchemaPath from '~/editor/schema/ci.json';
import { registerSchema } from '~/ide/utils';
-import { SourceEditorExtension } from './source_editor_extension_base';
-export class CiSchemaExtension extends SourceEditorExtension {
- /**
- * Registers a syntax schema to the editor based on project
- * identifier and commit.
- *
- * The schema is added to the file that is currently edited
- * in the editor.
- *
- * @param {Object} opts
- * @param {String} opts.projectNamespace
- * @param {String} opts.projectPath
- * @param {String?} opts.ref - Current ref. Defaults to main
- */
- registerCiSchema() {
- // In order for workers loaded from `data://` as the
- // ones loaded by monaco editor, we use absolute URLs
- // to fetch schema files, hence the `gon.gitlab_url`
- // reference. This prevents error:
- // "Failed to execute 'fetch' on 'WorkerGlobalScope'"
- const absoluteSchemaUrl = gon.gitlab_url + ciSchemaPath;
- const modelFileName = this.getModel().uri.path.split('/').pop();
+export class CiSchemaExtension {
+ static get extensionName() {
+ return 'CiSchema';
+ }
+ // eslint-disable-next-line class-methods-use-this
+ provides() {
+ return {
+ registerCiSchema: (instance) => {
+ // In order for workers loaded from `data://` as the
+ // ones loaded by monaco editor, we use absolute URLs
+ // to fetch schema files, hence the `gon.gitlab_url`
+ // reference. This prevents error:
+ // "Failed to execute 'fetch' on 'WorkerGlobalScope'"
+ const absoluteSchemaUrl = gon.gitlab_url + ciSchemaPath;
+ const modelFileName = instance.getModel().uri.path.split('/').pop();
- registerSchema({
- uri: absoluteSchemaUrl,
- fileMatch: [modelFileName],
- });
+ registerSchema({
+ uri: absoluteSchemaUrl,
+ fileMatch: [modelFileName],
+ });
+ },
+ };
}
}
diff --git a/app/assets/javascripts/editor/extensions/source_editor_extension_base.js b/app/assets/javascripts/editor/extensions/source_editor_extension_base.js
index 03c68fed3b1..3aa19df964c 100644
--- a/app/assets/javascripts/editor/extensions/source_editor_extension_base.js
+++ b/app/assets/javascripts/editor/extensions/source_editor_extension_base.js
@@ -1,13 +1,16 @@
import { Range } from 'monaco-editor';
-import { waitForCSSLoaded } from '~/helpers/startup_css_helper';
-import { ERROR_INSTANCE_REQUIRED_FOR_EXTENSION, EDITOR_TYPE_CODE } from '../constants';
+import {
+ EDITOR_TYPE_CODE,
+ EXTENSION_BASE_LINE_LINK_ANCHOR_CLASS,
+ EXTENSION_BASE_LINE_NUMBERS_CLASS,
+} from '../constants';
const hashRegexp = new RegExp('#?L', 'g');
const createAnchor = (href) => {
const fragment = new DocumentFragment();
const el = document.createElement('a');
- el.classList.add('link-anchor');
+ el.classList.add(EXTENSION_BASE_LINE_LINK_ANCHOR_CLASS);
el.href = href;
fragment.appendChild(el);
el.addEventListener('contextmenu', (e) => {
@@ -17,38 +20,46 @@ const createAnchor = (href) => {
};
export class SourceEditorExtension {
- constructor({ instance, ...options } = {}) {
- if (instance) {
- Object.assign(instance, options);
- SourceEditorExtension.highlightLines(instance);
- if (instance.getEditorType && instance.getEditorType() === EDITOR_TYPE_CODE) {
- SourceEditorExtension.setupLineLinking(instance);
- }
- SourceEditorExtension.deferRerender(instance);
- } else if (Object.entries(options).length) {
- throw new Error(ERROR_INSTANCE_REQUIRED_FOR_EXTENSION);
+ static get extensionName() {
+ return 'BaseExtension';
+ }
+
+ // eslint-disable-next-line class-methods-use-this
+ onUse(instance) {
+ SourceEditorExtension.highlightLines(instance);
+ if (instance.getEditorType && instance.getEditorType() === EDITOR_TYPE_CODE) {
+ SourceEditorExtension.setupLineLinking(instance);
}
}
- static deferRerender(instance) {
- waitForCSSLoaded(() => {
- instance.layout();
- });
+ static onMouseMoveHandler(e) {
+ const target = e.target.element;
+ if (target.classList.contains(EXTENSION_BASE_LINE_NUMBERS_CLASS)) {
+ const lineNum = e.target.position.lineNumber;
+ const hrefAttr = `#L${lineNum}`;
+ let lineLink = target.querySelector('a');
+ if (!lineLink) {
+ lineLink = createAnchor(hrefAttr);
+ target.appendChild(lineLink);
+ }
+ }
}
- static removeHighlights(instance) {
- Object.assign(instance, {
- lineDecorations: instance.deltaDecorations(instance.lineDecorations || [], []),
+ static setupLineLinking(instance) {
+ instance.onMouseMove(SourceEditorExtension.onMouseMoveHandler);
+ instance.onMouseDown((e) => {
+ const isCorrectAnchor = e.target.element.classList.contains(
+ EXTENSION_BASE_LINE_LINK_ANCHOR_CLASS,
+ );
+ if (!isCorrectAnchor) {
+ return;
+ }
+ if (instance.lineDecorations) {
+ instance.deltaDecorations(instance.lineDecorations, []);
+ }
});
}
- /**
- * Returns a function that can only be invoked once between
- * each browser screen repaint.
- * @param {Object} instance - The Source Editor instance
- * @param {Array} bounds - The [start, end] array with start
- * and end coordinates for highlighting
- */
static highlightLines(instance, bounds = null) {
const [start, end] =
bounds && Array.isArray(bounds)
@@ -74,29 +85,29 @@ export class SourceEditorExtension {
}
}
- static onMouseMoveHandler(e) {
- const target = e.target.element;
- if (target.classList.contains('line-numbers')) {
- const lineNum = e.target.position.lineNumber;
- const hrefAttr = `#L${lineNum}`;
- let el = target.querySelector('a');
- if (!el) {
- el = createAnchor(hrefAttr);
- target.appendChild(el);
- }
- }
- }
+ // eslint-disable-next-line class-methods-use-this
+ provides() {
+ return {
+ /**
+ * Removes existing line decorations and updates the reference on the instance
+ * @param {module:source_editor_instance~EditorInstance} instance - The Source Editor instance
+ */
+ removeHighlights: (instance) => {
+ Object.assign(instance, {
+ lineDecorations: instance.deltaDecorations(instance.lineDecorations || [], []),
+ });
+ },
- static setupLineLinking(instance) {
- instance.onMouseMove(SourceEditorExtension.onMouseMoveHandler);
- instance.onMouseDown((e) => {
- const isCorrectAnchor = e.target.element.classList.contains('link-anchor');
- if (!isCorrectAnchor) {
- return;
- }
- if (instance.lineDecorations) {
- instance.deltaDecorations(instance.lineDecorations, []);
- }
- });
+ /**
+ * Returns a function that can only be invoked once between
+ * each browser screen repaint.
+ * @param {Array} bounds - The [start, end] array with start
+ * @param {module:source_editor_instance~EditorInstance} instance - The Source Editor instance
+ * and end coordinates for highlighting
+ */
+ highlightLines(instance, bounds = null) {
+ SourceEditorExtension.highlightLines(instance, bounds);
+ },
+ };
}
}
diff --git a/app/assets/javascripts/editor/extensions/source_editor_file_template_ext.js b/app/assets/javascripts/editor/extensions/source_editor_file_template_ext.js
index 397e090ed30..ba4980896e5 100644
--- a/app/assets/javascripts/editor/extensions/source_editor_file_template_ext.js
+++ b/app/assets/javascripts/editor/extensions/source_editor_file_template_ext.js
@@ -1,8 +1,16 @@
import { Position } from 'monaco-editor';
-import { SourceEditorExtension } from './source_editor_extension_base';
-export class FileTemplateExtension extends SourceEditorExtension {
- navigateFileStart() {
- this.setPosition(new Position(1, 1));
+export class FileTemplateExtension {
+ static get extensionName() {
+ return 'FileTemplate';
+ }
+
+ // eslint-disable-next-line class-methods-use-this
+ provides() {
+ return {
+ navigateFileStart: (instance) => {
+ instance.setPosition(new Position(1, 1));
+ },
+ };
}
}
diff --git a/app/assets/javascripts/editor/extensions/source_editor_markdown_ext.js b/app/assets/javascripts/editor/extensions/source_editor_markdown_ext.js
index 57de21c933e..a16fe93026e 100644
--- a/app/assets/javascripts/editor/extensions/source_editor_markdown_ext.js
+++ b/app/assets/javascripts/editor/extensions/source_editor_markdown_ext.js
@@ -1,248 +1,102 @@
-import { debounce } from 'lodash';
-import { BLOB_PREVIEW_ERROR } from '~/blob_edit/constants';
-import createFlash from '~/flash';
-import { sanitize } from '~/lib/dompurify';
-import axios from '~/lib/utils/axios_utils';
-import { __ } from '~/locale';
-import syntaxHighlight from '~/syntax_highlight';
-import {
- EXTENSION_MARKDOWN_PREVIEW_PANEL_CLASS,
- EXTENSION_MARKDOWN_PREVIEW_ACTION_ID,
- EXTENSION_MARKDOWN_PREVIEW_PANEL_WIDTH,
- EXTENSION_MARKDOWN_PREVIEW_PANEL_PARENT_CLASS,
- EXTENSION_MARKDOWN_PREVIEW_UPDATE_DELAY,
-} from '../constants';
-import { SourceEditorExtension } from './source_editor_extension_base';
-
-const getPreview = (text, previewMarkdownPath) => {
- return axios
- .post(previewMarkdownPath, {
- text,
- })
- .then(({ data }) => {
- return data.body;
- });
-};
-
-const setupDomElement = ({ injectToEl = null } = {}) => {
- const previewEl = document.createElement('div');
- previewEl.classList.add(EXTENSION_MARKDOWN_PREVIEW_PANEL_CLASS);
- previewEl.style.display = 'none';
- if (injectToEl) {
- injectToEl.appendChild(previewEl);
+export class EditorMarkdownExtension {
+ static get extensionName() {
+ return 'EditorMarkdown';
}
- return previewEl;
-};
-export class EditorMarkdownExtension extends SourceEditorExtension {
- constructor({ instance, previewMarkdownPath, ...args } = {}) {
- super({ instance, ...args });
- Object.assign(instance, {
- previewMarkdownPath,
- preview: {
- el: undefined,
- action: undefined,
- shown: false,
- modelChangeListener: undefined,
+ // eslint-disable-next-line class-methods-use-this
+ provides() {
+ return {
+ getSelectedText: (instance, selection = instance.getSelection()) => {
+ const { startLineNumber, endLineNumber, startColumn, endColumn } = selection;
+ const valArray = instance.getValue().split('\n');
+ let text = '';
+ if (startLineNumber === endLineNumber) {
+ text = valArray[startLineNumber - 1].slice(startColumn - 1, endColumn - 1);
+ } else {
+ const startLineText = valArray[startLineNumber - 1].slice(startColumn - 1);
+ const endLineText = valArray[endLineNumber - 1].slice(0, endColumn - 1);
+
+ for (let i = startLineNumber, k = endLineNumber - 1; i < k; i += 1) {
+ text += `${valArray[i]}`;
+ if (i !== k - 1) text += `\n`;
+ }
+ text = text
+ ? [startLineText, text, endLineText].join('\n')
+ : [startLineText, endLineText].join('\n');
+ }
+ return text;
},
- });
- this.setupPreviewAction.call(instance);
-
- instance.getModel().onDidChangeLanguage(({ newLanguage, oldLanguage } = {}) => {
- if (newLanguage === 'markdown' && oldLanguage !== newLanguage) {
- instance.setupPreviewAction();
- } else {
- instance.cleanup();
- }
- });
-
- instance.onDidChangeModel(() => {
- const model = instance.getModel();
- if (model) {
- const { language } = model.getLanguageIdentifier();
- instance.cleanup();
- if (language === 'markdown') {
- instance.setupPreviewAction();
+ replaceSelectedText: (instance, text, select) => {
+ const forceMoveMarkers = !select;
+ instance.executeEdits('', [{ range: instance.getSelection(), text, forceMoveMarkers }]);
+ },
+ moveCursor: (instance, dx = 0, dy = 0) => {
+ const pos = instance.getPosition();
+ pos.column += dx;
+ pos.lineNumber += dy;
+ instance.setPosition(pos);
+ },
+ /**
+ * Adjust existing selection to select text within the original selection.
+ * - If `selectedText` is not supplied, we fetch selected text with
+ *
+ * ALGORITHM:
+ *
+ * MULTI-LINE SELECTION
+ * 1. Find line that contains `toSelect` text.
+ * 2. Using the index of this line and the position of `toSelect` text in it,
+ * construct:
+ * * newStartLineNumber
+ * * newStartColumn
+ *
+ * SINGLE-LINE SELECTION
+ * 1. Use `startLineNumber` from the current selection as `newStartLineNumber`
+ * 2. Find the position of `toSelect` text in it to get `newStartColumn`
+ *
+ * 3. `newEndLineNumber` — Since this method is supposed to be used with
+ * markdown decorators that are pretty short, the `newEndLineNumber` is
+ * suggested to be assumed the same as the startLine.
+ * 4. `newEndColumn` — pretty obvious
+ * 5. Adjust the start and end positions of the current selection
+ * 6. Re-set selection on the instance
+ *
+ * @param {module:source_editor_instance~EditorInstance} instance - The Source Editor instance. Is passed automatically.
+ * @param {string} toSelect - New text to select within current selection.
+ * @param {string} selectedText - Currently selected text. It's just a
+ * shortcut: If it's not supplied, we fetch selected text from the instance
+ */
+ selectWithinSelection: (instance, toSelect, selectedText) => {
+ const currentSelection = instance.getSelection();
+ if (currentSelection.isEmpty() || !toSelect) {
+ return;
+ }
+ const text = selectedText || instance.getSelectedText(currentSelection);
+ let lineShift;
+ let newStartLineNumber;
+ let newStartColumn;
+
+ const textLines = text.split('\n');
+
+ if (textLines.length > 1) {
+ // Multi-line selection
+ lineShift = textLines.findIndex((line) => line.indexOf(toSelect) !== -1);
+ newStartLineNumber = currentSelection.startLineNumber + lineShift;
+ newStartColumn = textLines[lineShift].indexOf(toSelect) + 1;
+ } else {
+ // Single-line selection
+ newStartLineNumber = currentSelection.startLineNumber;
+ newStartColumn = currentSelection.startColumn + text.indexOf(toSelect);
}
- }
- });
- }
-
- static togglePreviewLayout() {
- const { width, height } = this.getLayoutInfo();
- const newWidth = this.preview.shown
- ? width / EXTENSION_MARKDOWN_PREVIEW_PANEL_WIDTH
- : width * EXTENSION_MARKDOWN_PREVIEW_PANEL_WIDTH;
- this.layout({ width: newWidth, height });
- }
-
- static togglePreviewPanel() {
- const parentEl = this.getDomNode().parentElement;
- const { el: previewEl } = this.preview;
- parentEl.classList.toggle(EXTENSION_MARKDOWN_PREVIEW_PANEL_PARENT_CLASS);
-
- if (previewEl.style.display === 'none') {
- // Show the preview panel
- this.fetchPreview();
- } else {
- // Hide the preview panel
- previewEl.style.display = 'none';
- }
- }
-
- cleanup() {
- if (this.preview.modelChangeListener) {
- this.preview.modelChangeListener.dispose();
- }
- this.preview.action.dispose();
- if (this.preview.shown) {
- EditorMarkdownExtension.togglePreviewPanel.call(this);
- EditorMarkdownExtension.togglePreviewLayout.call(this);
- }
- this.preview.shown = false;
- }
-
- fetchPreview() {
- const { el: previewEl } = this.preview;
- getPreview(this.getValue(), this.previewMarkdownPath)
- .then((data) => {
- previewEl.innerHTML = sanitize(data);
- syntaxHighlight(previewEl.querySelectorAll('.js-syntax-highlight'));
- previewEl.style.display = 'block';
- })
- .catch(() => createFlash(BLOB_PREVIEW_ERROR));
- }
- setupPreviewAction() {
- if (this.getAction(EXTENSION_MARKDOWN_PREVIEW_ACTION_ID)) return;
+ const newEndLineNumber = newStartLineNumber;
+ const newEndColumn = newStartColumn + toSelect.length;
- this.preview.action = this.addAction({
- id: EXTENSION_MARKDOWN_PREVIEW_ACTION_ID,
- label: __('Preview Markdown'),
- keybindings: [
- // eslint-disable-next-line no-bitwise,no-undef
- monaco.KeyMod.chord(monaco.KeyMod.CtrlCmd | monaco.KeyMod.Shift | monaco.KeyCode.KEY_P),
- ],
- contextMenuGroupId: 'navigation',
- contextMenuOrder: 1.5,
+ const newSelection = currentSelection
+ .setStartPosition(newStartLineNumber, newStartColumn)
+ .setEndPosition(newEndLineNumber, newEndColumn);
- // Method that will be executed when the action is triggered.
- // @param ed The editor instance is passed in as a convenience
- run(instance) {
- instance.togglePreview();
+ instance.setSelection(newSelection);
},
- });
- }
-
- togglePreview() {
- if (!this.preview?.el) {
- this.preview.el = setupDomElement({ injectToEl: this.getDomNode().parentElement });
- }
- EditorMarkdownExtension.togglePreviewLayout.call(this);
- EditorMarkdownExtension.togglePreviewPanel.call(this);
-
- if (!this.preview?.shown) {
- this.preview.modelChangeListener = this.onDidChangeModelContent(
- debounce(this.fetchPreview.bind(this), EXTENSION_MARKDOWN_PREVIEW_UPDATE_DELAY),
- );
- } else {
- this.preview.modelChangeListener.dispose();
- }
-
- this.preview.shown = !this.preview?.shown;
- }
-
- getSelectedText(selection = this.getSelection()) {
- const { startLineNumber, endLineNumber, startColumn, endColumn } = selection;
- const valArray = this.getValue().split('\n');
- let text = '';
- if (startLineNumber === endLineNumber) {
- text = valArray[startLineNumber - 1].slice(startColumn - 1, endColumn - 1);
- } else {
- const startLineText = valArray[startLineNumber - 1].slice(startColumn - 1);
- const endLineText = valArray[endLineNumber - 1].slice(0, endColumn - 1);
-
- for (let i = startLineNumber, k = endLineNumber - 1; i < k; i += 1) {
- text += `${valArray[i]}`;
- if (i !== k - 1) text += `\n`;
- }
- text = text
- ? [startLineText, text, endLineText].join('\n')
- : [startLineText, endLineText].join('\n');
- }
- return text;
- }
-
- replaceSelectedText(text, select = undefined) {
- const forceMoveMarkers = !select;
- this.executeEdits('', [{ range: this.getSelection(), text, forceMoveMarkers }]);
- }
-
- moveCursor(dx = 0, dy = 0) {
- const pos = this.getPosition();
- pos.column += dx;
- pos.lineNumber += dy;
- this.setPosition(pos);
- }
-
- /**
- * Adjust existing selection to select text within the original selection.
- * - If `selectedText` is not supplied, we fetch selected text with
- *
- * ALGORITHM:
- *
- * MULTI-LINE SELECTION
- * 1. Find line that contains `toSelect` text.
- * 2. Using the index of this line and the position of `toSelect` text in it,
- * construct:
- * * newStartLineNumber
- * * newStartColumn
- *
- * SINGLE-LINE SELECTION
- * 1. Use `startLineNumber` from the current selection as `newStartLineNumber`
- * 2. Find the position of `toSelect` text in it to get `newStartColumn`
- *
- * 3. `newEndLineNumber` — Since this method is supposed to be used with
- * markdown decorators that are pretty short, the `newEndLineNumber` is
- * suggested to be assumed the same as the startLine.
- * 4. `newEndColumn` — pretty obvious
- * 5. Adjust the start and end positions of the current selection
- * 6. Re-set selection on the instance
- *
- * @param {string} toSelect - New text to select within current selection.
- * @param {string} selectedText - Currently selected text. It's just a
- * shortcut: If it's not supplied, we fetch selected text from the instance
- */
- selectWithinSelection(toSelect, selectedText) {
- const currentSelection = this.getSelection();
- if (currentSelection.isEmpty() || !toSelect) {
- return;
- }
- const text = selectedText || this.getSelectedText(currentSelection);
- let lineShift;
- let newStartLineNumber;
- let newStartColumn;
-
- const textLines = text.split('\n');
-
- if (textLines.length > 1) {
- // Multi-line selection
- lineShift = textLines.findIndex((line) => line.indexOf(toSelect) !== -1);
- newStartLineNumber = currentSelection.startLineNumber + lineShift;
- newStartColumn = textLines[lineShift].indexOf(toSelect) + 1;
- } else {
- // Single-line selection
- newStartLineNumber = currentSelection.startLineNumber;
- newStartColumn = currentSelection.startColumn + text.indexOf(toSelect);
- }
-
- const newEndLineNumber = newStartLineNumber;
- const newEndColumn = newStartColumn + toSelect.length;
-
- const newSelection = currentSelection
- .setStartPosition(newStartLineNumber, newStartColumn)
- .setEndPosition(newEndLineNumber, newEndColumn);
-
- this.setSelection(newSelection);
+ };
}
}
diff --git a/app/assets/javascripts/editor/extensions/source_editor_markdown_livepreview_ext.js b/app/assets/javascripts/editor/extensions/source_editor_markdown_livepreview_ext.js
new file mode 100644
index 00000000000..9d53268c340
--- /dev/null
+++ b/app/assets/javascripts/editor/extensions/source_editor_markdown_livepreview_ext.js
@@ -0,0 +1,167 @@
+import { debounce } from 'lodash';
+import { BLOB_PREVIEW_ERROR } from '~/blob_edit/constants';
+import createFlash from '~/flash';
+import { sanitize } from '~/lib/dompurify';
+import axios from '~/lib/utils/axios_utils';
+import { __ } from '~/locale';
+import syntaxHighlight from '~/syntax_highlight';
+import {
+ EXTENSION_MARKDOWN_PREVIEW_PANEL_CLASS,
+ EXTENSION_MARKDOWN_PREVIEW_ACTION_ID,
+ EXTENSION_MARKDOWN_PREVIEW_PANEL_WIDTH,
+ EXTENSION_MARKDOWN_PREVIEW_PANEL_PARENT_CLASS,
+ EXTENSION_MARKDOWN_PREVIEW_UPDATE_DELAY,
+} from '../constants';
+
+const fetchPreview = (text, previewMarkdownPath) => {
+ return axios
+ .post(previewMarkdownPath, {
+ text,
+ })
+ .then(({ data }) => {
+ return data.body;
+ });
+};
+
+const setupDomElement = ({ injectToEl = null } = {}) => {
+ const previewEl = document.createElement('div');
+ previewEl.classList.add(EXTENSION_MARKDOWN_PREVIEW_PANEL_CLASS);
+ previewEl.style.display = 'none';
+ if (injectToEl) {
+ injectToEl.appendChild(previewEl);
+ }
+ return previewEl;
+};
+
+export class EditorMarkdownPreviewExtension {
+ static get extensionName() {
+ return 'EditorMarkdownPreview';
+ }
+
+ onSetup(instance, setupOptions) {
+ this.preview = {
+ el: undefined,
+ action: undefined,
+ shown: false,
+ modelChangeListener: undefined,
+ path: setupOptions.previewMarkdownPath,
+ };
+ this.setupPreviewAction(instance);
+
+ instance.getModel().onDidChangeLanguage(({ newLanguage, oldLanguage } = {}) => {
+ if (newLanguage === 'markdown' && oldLanguage !== newLanguage) {
+ instance.setupPreviewAction();
+ } else {
+ instance.cleanup();
+ }
+ });
+
+ instance.onDidChangeModel(() => {
+ const model = instance.getModel();
+ if (model) {
+ const { language } = model.getLanguageIdentifier();
+ instance.cleanup();
+ if (language === 'markdown') {
+ instance.setupPreviewAction();
+ }
+ }
+ });
+ }
+
+ togglePreviewLayout(instance) {
+ const { width, height } = instance.getLayoutInfo();
+ const newWidth = this.preview.shown
+ ? width / EXTENSION_MARKDOWN_PREVIEW_PANEL_WIDTH
+ : width * EXTENSION_MARKDOWN_PREVIEW_PANEL_WIDTH;
+ instance.layout({ width: newWidth, height });
+ }
+
+ togglePreviewPanel(instance) {
+ const parentEl = instance.getDomNode().parentElement;
+ const { el: previewEl } = this.preview;
+ parentEl.classList.toggle(EXTENSION_MARKDOWN_PREVIEW_PANEL_PARENT_CLASS);
+
+ if (previewEl.style.display === 'none') {
+ // Show the preview panel
+ this.fetchPreview(instance);
+ } else {
+ // Hide the preview panel
+ previewEl.style.display = 'none';
+ }
+ }
+
+ fetchPreview(instance) {
+ const { el: previewEl } = this.preview;
+ fetchPreview(instance.getValue(), this.preview.path)
+ .then((data) => {
+ previewEl.innerHTML = sanitize(data);
+ syntaxHighlight(previewEl.querySelectorAll('.js-syntax-highlight'));
+ previewEl.style.display = 'block';
+ })
+ .catch(() => createFlash(BLOB_PREVIEW_ERROR));
+ }
+
+ setupPreviewAction(instance) {
+ if (instance.getAction(EXTENSION_MARKDOWN_PREVIEW_ACTION_ID)) return;
+
+ this.preview.action = instance.addAction({
+ id: EXTENSION_MARKDOWN_PREVIEW_ACTION_ID,
+ label: __('Preview Markdown'),
+ keybindings: [
+ // eslint-disable-next-line no-bitwise,no-undef
+ monaco.KeyMod.chord(monaco.KeyMod.CtrlCmd | monaco.KeyMod.Shift | monaco.KeyCode.KEY_P),
+ ],
+ contextMenuGroupId: 'navigation',
+ contextMenuOrder: 1.5,
+
+ // Method that will be executed when the action is triggered.
+ // @param ed The editor instance is passed in as a convenience
+ run(inst) {
+ inst.togglePreview();
+ },
+ });
+ }
+
+ provides() {
+ return {
+ markdownPreview: this.preview,
+
+ cleanup: (instance) => {
+ if (this.preview.modelChangeListener) {
+ this.preview.modelChangeListener.dispose();
+ }
+ this.preview.action.dispose();
+ if (this.preview.shown) {
+ this.togglePreviewPanel(instance);
+ this.togglePreviewLayout(instance);
+ }
+ this.preview.shown = false;
+ },
+
+ fetchPreview: (instance) => this.fetchPreview(instance),
+
+ setupPreviewAction: (instance) => this.setupPreviewAction(instance),
+
+ togglePreview: (instance) => {
+ if (!this.preview?.el) {
+ this.preview.el = setupDomElement({ injectToEl: instance.getDomNode().parentElement });
+ }
+ this.togglePreviewLayout(instance);
+ this.togglePreviewPanel(instance);
+
+ if (!this.preview?.shown) {
+ this.preview.modelChangeListener = instance.onDidChangeModelContent(
+ debounce(
+ this.fetchPreview.bind(this, instance),
+ EXTENSION_MARKDOWN_PREVIEW_UPDATE_DELAY,
+ ),
+ );
+ } else {
+ this.preview.modelChangeListener.dispose();
+ }
+
+ this.preview.shown = !this.preview?.shown;
+ },
+ };
+ }
+}
diff --git a/app/assets/javascripts/editor/extensions/source_editor_webide_ext.js b/app/assets/javascripts/editor/extensions/source_editor_webide_ext.js
index 98e05489c1c..4e8c11bac54 100644
--- a/app/assets/javascripts/editor/extensions/source_editor_webide_ext.js
+++ b/app/assets/javascripts/editor/extensions/source_editor_webide_ext.js
@@ -1,7 +1,15 @@
+/**
+ * A WebIDE Extension options for Source Editor
+ * @typedef {Object} WebIDEExtensionOptions
+ * @property {Object} modelManager The root manager for WebIDE models
+ * @property {Object} store The state store for communication
+ * @property {Object} file
+ * @property {Object} options The Monaco editor options
+ */
+
import { debounce } from 'lodash';
import { KeyCode, KeyMod, Range } from 'monaco-editor';
import { EDITOR_TYPE_DIFF } from '~/editor/constants';
-import { SourceEditorExtension } from '~/editor/extensions/source_editor_extension_base';
import Disposable from '~/ide/lib/common/disposable';
import { editorOptions } from '~/ide/lib/editor_options';
import keymap from '~/ide/lib/keymap.json';
@@ -11,154 +19,168 @@ const isDiffEditorType = (instance) => {
};
export const UPDATE_DIMENSIONS_DELAY = 200;
+const defaultOptions = {
+ modelManager: undefined,
+ store: undefined,
+ file: undefined,
+ options: {},
+};
-export class EditorWebIdeExtension extends SourceEditorExtension {
- constructor({ instance, modelManager, ...options } = {}) {
- super({
- instance,
- ...options,
- modelManager,
- disposable: new Disposable(),
- debouncedUpdate: debounce(() => {
- instance.updateDimensions();
- }, UPDATE_DIMENSIONS_DELAY),
- });
-
- window.addEventListener('resize', instance.debouncedUpdate, false);
-
- instance.onDidDispose(() => {
- window.removeEventListener('resize', instance.debouncedUpdate);
-
- // catch any potential errors with disposing the error
- // this is mainly for tests caused by elements not existing
- try {
- instance.disposable.dispose();
- } catch (e) {
- if (process.env.NODE_ENV !== 'test') {
- // eslint-disable-next-line no-console
- console.error(e);
- }
- }
- });
+const addActions = (instance, store) => {
+ const getKeyCode = (key) => {
+ const monacoKeyMod = key.indexOf('KEY_') === 0;
- EditorWebIdeExtension.addActions(instance);
- }
+ return monacoKeyMod ? KeyCode[key] : KeyMod[key];
+ };
- static addActions(instance) {
- const { store } = instance;
- const getKeyCode = (key) => {
- const monacoKeyMod = key.indexOf('KEY_') === 0;
+ keymap.forEach((command) => {
+ const { bindings, id, label, action } = command;
- return monacoKeyMod ? KeyCode[key] : KeyMod[key];
- };
+ const keybindings = bindings.map((binding) => {
+ const keys = binding.split('+');
- keymap.forEach((command) => {
- const { bindings, id, label, action } = command;
-
- const keybindings = bindings.map((binding) => {
- const keys = binding.split('+');
-
- // eslint-disable-next-line no-bitwise
- return keys.length > 1 ? getKeyCode(keys[0]) | getKeyCode(keys[1]) : getKeyCode(keys[0]);
- });
-
- instance.addAction({
- id,
- label,
- keybindings,
- run() {
- store.dispatch(action.name, action.params);
- return null;
- },
- });
+ // eslint-disable-next-line no-bitwise
+ return keys.length > 1 ? getKeyCode(keys[0]) | getKeyCode(keys[1]) : getKeyCode(keys[0]);
});
- }
-
- createModel(file, head = null) {
- return this.modelManager.addModel(file, head);
- }
-
- attachModel(model) {
- if (isDiffEditorType(this)) {
- this.setModel({
- original: model.getOriginalModel(),
- modified: model.getModel(),
- });
- return;
- }
-
- this.setModel(model.getModel());
+ instance.addAction({
+ id,
+ label,
+ keybindings,
+ run() {
+ store.dispatch(action.name, action.params);
+ return null;
+ },
+ });
+ });
+};
- this.updateOptions(
- editorOptions.reduce((acc, obj) => {
- Object.keys(obj).forEach((key) => {
- Object.assign(acc, {
- [key]: obj[key](model),
- });
- });
- return acc;
- }, {}),
- );
- }
+const renderSideBySide = (domElement) => {
+ return domElement.offsetWidth >= 700;
+};
- attachMergeRequestModel(model) {
- this.setModel({
- original: model.getBaseModel(),
- modified: model.getModel(),
+const updateInstanceDimensions = (instance) => {
+ instance.layout();
+ if (isDiffEditorType(instance)) {
+ instance.updateOptions({
+ renderSideBySide: renderSideBySide(instance.getDomNode()),
});
}
+};
- updateDimensions() {
- this.layout();
- this.updateDiffView();
+export class EditorWebIdeExtension {
+ static get extensionName() {
+ return 'EditorWebIde';
}
- setPos({ lineNumber, column }) {
- this.revealPositionInCenter({
- lineNumber,
- column,
- });
- this.setPosition({
- lineNumber,
- column,
- });
+ /**
+ * Set up the WebIDE extension for Source Editor
+ * @param {module:source_editor_instance~EditorInstance} instance - The Source Editor instance
+ * @param {WebIDEExtensionOptions} setupOptions
+ */
+ onSetup(instance, setupOptions = defaultOptions) {
+ this.modelManager = setupOptions.modelManager;
+ this.store = setupOptions.store;
+ this.file = setupOptions.file;
+ this.options = setupOptions.options;
+
+ this.disposable = new Disposable();
+ this.debouncedUpdate = debounce(() => {
+ updateInstanceDimensions(instance);
+ }, UPDATE_DIMENSIONS_DELAY);
+
+ addActions(instance, setupOptions.store);
}
- onPositionChange(cb) {
- if (!this.onDidChangeCursorPosition) {
- return;
- }
+ onUse(instance) {
+ window.addEventListener('resize', this.debouncedUpdate, false);
- this.disposable.add(this.onDidChangeCursorPosition((e) => cb(this, e)));
+ instance.onDidDispose(() => {
+ this.onUnuse();
+ });
}
- updateDiffView() {
- if (!isDiffEditorType(this)) {
- return;
+ onUnuse() {
+ window.removeEventListener('resize', this.debouncedUpdate);
+
+ // catch any potential errors with disposing the error
+ // this is mainly for tests caused by elements not existing
+ try {
+ this.disposable.dispose();
+ } catch (e) {
+ if (process.env.NODE_ENV !== 'test') {
+ // eslint-disable-next-line no-console
+ console.error(e);
+ }
}
-
- this.updateOptions({
- renderSideBySide: EditorWebIdeExtension.renderSideBySide(this.getDomNode()),
- });
}
- replaceSelectedText(text) {
- let selection = this.getSelection();
- const range = new Range(
- selection.startLineNumber,
- selection.startColumn,
- selection.endLineNumber,
- selection.endColumn,
- );
+ provides() {
+ return {
+ createModel: (instance, file, head = null) => {
+ return this.modelManager.addModel(file, head);
+ },
+ attachModel: (instance, model) => {
+ if (isDiffEditorType(instance)) {
+ instance.setModel({
+ original: model.getOriginalModel(),
+ modified: model.getModel(),
+ });
- this.executeEdits('', [{ range, text }]);
+ return;
+ }
- selection = this.getSelection();
- this.setPosition({ lineNumber: selection.endLineNumber, column: selection.endColumn });
- }
+ instance.setModel(model.getModel());
+
+ instance.updateOptions(
+ editorOptions.reduce((acc, obj) => {
+ Object.keys(obj).forEach((key) => {
+ Object.assign(acc, {
+ [key]: obj[key](model),
+ });
+ });
+ return acc;
+ }, {}),
+ );
+ },
+ attachMergeRequestModel: (instance, model) => {
+ instance.setModel({
+ original: model.getBaseModel(),
+ modified: model.getModel(),
+ });
+ },
+ updateDimensions: (instance) => updateInstanceDimensions(instance),
+ setPos: (instance, { lineNumber, column }) => {
+ instance.revealPositionInCenter({
+ lineNumber,
+ column,
+ });
+ instance.setPosition({
+ lineNumber,
+ column,
+ });
+ },
+ onPositionChange: (instance, cb) => {
+ if (typeof instance.onDidChangeCursorPosition !== 'function') {
+ return;
+ }
- static renderSideBySide(domElement) {
- return domElement.offsetWidth >= 700;
+ this.disposable.add(instance.onDidChangeCursorPosition((e) => cb(instance, e)));
+ },
+ replaceSelectedText: (instance, text) => {
+ let selection = instance.getSelection();
+ const range = new Range(
+ selection.startLineNumber,
+ selection.startColumn,
+ selection.endLineNumber,
+ selection.endColumn,
+ );
+
+ instance.executeEdits('', [{ range, text }]);
+
+ selection = instance.getSelection();
+ instance.setPosition({ lineNumber: selection.endLineNumber, column: selection.endColumn });
+ },
+ };
}
}
diff --git a/app/assets/javascripts/editor/extensions/source_editor_yaml_ext.js b/app/assets/javascripts/editor/extensions/source_editor_yaml_ext.js
index 212e09c8724..05ce617ca7c 100644
--- a/app/assets/javascripts/editor/extensions/source_editor_yaml_ext.js
+++ b/app/assets/javascripts/editor/extensions/source_editor_yaml_ext.js
@@ -1,50 +1,46 @@
+/**
+ * A Yaml Editor Extension options for Source Editor
+ * @typedef {Object} YamlEditorExtensionOptions
+ * @property { boolean } enableComments Convert model nodes with the comment
+ * pattern to comments?
+ * @property { string } highlightPath Add a line highlight to the
+ * node specified by this e.g. `"foo.bar[0]"`
+ * @property { * } model Any JS Object that will be stringified and used as the
+ * editor's value. Equivalent to using `setDataModel()`
+ * @property options SourceEditorExtension Options
+ */
+
import { toPath } from 'lodash';
import { parseDocument, Document, visit, isScalar, isCollection, isMap } from 'yaml';
import { findPair } from 'yaml/util';
-import { SourceEditorExtension } from '~/editor/extensions/source_editor_extension_base';
-export class YamlEditorExtension extends SourceEditorExtension {
+export class YamlEditorExtension {
+ static get extensionName() {
+ return 'YamlEditor';
+ }
+
/**
* Extends the source editor with capabilities for yaml files.
*
- * @param { Instance } instance Source Editor Instance
- * @param { boolean } enableComments Convert model nodes with the comment
- * pattern to comments?
- * @param { string } highlightPath Add a line highlight to the
- * node specified by this e.g. `"foo.bar[0]"`
- * @param { * } model Any JS Object that will be stringified and used as the
- * editor's value. Equivalent to using `setDataModel()`
- * @param options SourceEditorExtension Options
+ * @param {module:source_editor_instance~EditorInstance} instance - The Source Editor instance
+ * @param {YamlEditorExtensionOptions} setupOptions
*/
- constructor({
- instance,
- enableComments = false,
- highlightPath = null,
- model = null,
- ...options
- } = {}) {
- super({
- instance,
- options: {
- ...options,
- enableComments,
- highlightPath,
- },
- });
+ onSetup(instance, setupOptions = {}) {
+ const { enableComments = false, highlightPath = null, model = null } = setupOptions;
+ this.enableComments = enableComments;
+ this.highlightPath = highlightPath;
+ this.model = model;
if (model) {
- YamlEditorExtension.initFromModel(instance, model);
+ this.initFromModel(instance, model);
}
instance.onDidChangeModelContent(() => instance.onUpdate());
}
- /**
- * @private
- */
- static initFromModel(instance, model) {
+ initFromModel(instance, model) {
const doc = new Document(model);
- if (instance.options.enableComments) {
+ if (this.enableComments) {
YamlEditorExtension.transformComments(doc);
}
instance.setValue(doc.toString());
@@ -160,110 +156,13 @@ export class YamlEditorExtension extends SourceEditorExtension {
return doc;
}
- /**
- * Get the editor's value parsed as a `Document` as defined by the `yaml`
- * package
- * @returns {Document}
- */
- getDoc() {
- return parseDocument(this.getValue());
- }
-
- /**
- * Accepts a `Document` as defined by the `yaml` package and
- * sets the Editor's value to a stringified version of it.
- * @param { Document } doc
- */
- setDoc(doc) {
- if (this.options.enableComments) {
- YamlEditorExtension.transformComments(doc);
- }
-
- if (!this.getValue()) {
- this.setValue(doc.toString());
- } else {
- this.updateValue(doc.toString());
- }
- }
-
- /**
- * Returns the parsed value of the Editor's content as JS.
- * @returns {*}
- */
- getDataModel() {
- return this.getDoc().toJS();
- }
-
- /**
- * Accepts any JS Object and sets the Editor's value to a stringified version
- * of that value.
- *
- * @param value
- */
- setDataModel(value) {
- this.setDoc(new Document(value));
- }
-
- /**
- * Method to be executed when the Editor's <TextModel> was updated
- */
- onUpdate() {
- if (this.options.highlightPath) {
- this.highlight(this.options.highlightPath);
- }
- }
-
- /**
- * Set the editors content to the input without recreating the content model.
- *
- * @param blob
- */
- updateValue(blob) {
- // Using applyEdits() instead of setValue() ensures that tokens such as
- // highlighted lines aren't deleted/recreated which causes a flicker.
- const model = this.getModel();
- model.applyEdits([
- {
- // A nice improvement would be to replace getFullModelRange() with
- // a range of the actual diff, avoiding re-formatting the document,
- // but that's something for a later iteration.
- range: model.getFullModelRange(),
- text: blob,
- },
- ]);
- }
-
- /**
- * Add a line highlight style to the node specified by the path.
- *
- * @param {string|null|false} path A path to a node of the Editor's value,
- * e.g. `"foo.bar[0]"`. If the value is falsy, this will remove all
- * highlights.
- */
- highlight(path) {
- if (this.options.highlightPath === path) return;
- if (!path) {
- SourceEditorExtension.removeHighlights(this);
- } else {
- const res = this.locate(path);
- SourceEditorExtension.highlightLines(this, res);
- }
- this.options.highlightPath = path || null;
+ static getDoc(instance) {
+ return parseDocument(instance.getValue());
}
- /**
- * Return the line numbers of a certain node identified by `path` within
- * the yaml.
- *
- * @param {string} path A path to a node, eg. `foo.bar[0]`
- * @returns {number[]} Array following the schema `[firstLine, lastLine]`
- * (both inclusive)
- *
- * @throws {Error} Will throw if the path is not found inside the document
- */
- locate(path) {
+ static locate(instance, path) {
if (!path) throw Error(`No path provided.`);
- const blob = this.getValue();
+ const blob = instance.getValue();
const doc = parseDocument(blob);
const pathArray = toPath(path);
@@ -290,4 +189,120 @@ export class YamlEditorExtension extends SourceEditorExtension {
const endLine = (endSlice.match(/\n/g) || []).length;
return [startLine, endLine];
}
+
+ setDoc(instance, doc) {
+ if (this.enableComments) {
+ YamlEditorExtension.transformComments(doc);
+ }
+
+ if (!instance.getValue()) {
+ instance.setValue(doc.toString());
+ } else {
+ instance.updateValue(doc.toString());
+ }
+ }
+
+ highlight(instance, path) {
+ // IMPORTANT
+ // removeHighlight and highlightLines both come from
+ // SourceEditorExtension. So it has to be installed prior to this extension
+ if (this.highlightPath === path) return;
+ if (!path) {
+ instance.removeHighlights();
+ } else {
+ const res = YamlEditorExtension.locate(instance, path);
+ instance.highlightLines(res);
+ }
+ this.highlightPath = path || null;
+ }
+
+ provides() {
+ return {
+ /**
+ * Get the editor's value parsed as a `Document` as defined by the `yaml`
+ * package
+ * @param {module:source_editor_instance~EditorInstance} instance - The Source Editor instance
+ * @returns {Document}
+ */
+ getDoc: (instance) => YamlEditorExtension.getDoc(instance),
+
+ /**
+ * Accepts a `Document` as defined by the `yaml` package and
+ * sets the Editor's value to a stringified version of it.
+ * @param {module:source_editor_instance~EditorInstance} instance - The Source Editor instance
+ * @param { Document } doc
+ */
+ setDoc: (instance, doc) => this.setDoc(instance, doc),
+
+ /**
+ * Returns the parsed value of the Editor's content as JS.
+ * @returns {*}
+ */
+ getDataModel: (instance) => YamlEditorExtension.getDoc(instance).toJS(),
+
+ /**
+ * Accepts any JS Object and sets the Editor's value to a stringified version
+ * of that value.
+ *
+ * @param {module:source_editor_instance~EditorInstance} instance - The Source Editor instance
+ * @param value
+ */
+ setDataModel: (instance, value) => this.setDoc(instance, new Document(value)),
+
+ /**
+ * Method to be executed when the Editor's <TextModel> was updated
+ */
+ onUpdate: (instance) => {
+ if (this.highlightPath) {
+ this.highlight(instance, this.highlightPath);
+ }
+ },
+
+ /**
+ * Set the editors content to the input without recreating the content model.
+ *
+ * @param {module:source_editor_instance~EditorInstance} instance - The Source Editor instance
+ * @param blob
+ */
+ updateValue: (instance, blob) => {
+ // Using applyEdits() instead of setValue() ensures that tokens such as
+ // highlighted lines aren't deleted/recreated which causes a flicker.
+ const model = instance.getModel();
+ model.applyEdits([
+ {
+ // A nice improvement would be to replace getFullModelRange() with
+ // a range of the actual diff, avoiding re-formatting the document,
+ // but that's something for a later iteration.
+ range: model.getFullModelRange(),
+ text: blob,
+ },
+ ]);
+ },
+
+ /**
+ * Add a line highlight style to the node specified by the path.
+ *
+ * @param {module:source_editor_instance~EditorInstance} instance - The Source Editor instance
+ * @param {string|null|false} path A path to a node of the Editor's value,
+ * e.g. `"foo.bar[0]"`. If the value is falsy, this will remove all
+ * highlights.
+ */
+ highlight: (instance, path) => this.highlight(instance, path),
+
+ /**
+ * Return the line numbers of a certain node identified by `path` within
+ * the yaml.
+ *
+ * @param {module:source_editor_instance~EditorInstance} instance - The Source Editor instance
+ * @param {string} path A path to a node, eg. `foo.bar[0]`
+ * @returns {number[]} Array following the schema `[firstLine, lastLine]`
+ * (both inclusive)
+ *
+ * @throws {Error} Will throw if the path is not found inside the document
+ */
+ locate: (instance, path) => YamlEditorExtension.locate(instance, path),
+
+ initFromModel: (instance, model) => this.initFromModel(instance, model),
+ };
+ }
}
diff --git a/app/assets/javascripts/editor/source_editor.js b/app/assets/javascripts/editor/source_editor.js
index 81ddf8d77fa..57e2b0da565 100644
--- a/app/assets/javascripts/editor/source_editor.js
+++ b/app/assets/javascripts/editor/source_editor.js
@@ -1,4 +1,5 @@
import { editor as monacoEditor, Uri } from 'monaco-editor';
+import { waitForCSSLoaded } from '~/helpers/startup_css_helper';
import { defaultEditorOptions } from '~/ide/lib/editor_options';
import languages from '~/ide/lib/languages';
import { registerLanguages } from '~/ide/utils';
@@ -11,10 +12,39 @@ import {
EDITOR_TYPE_DIFF,
} from './constants';
import { clearDomElement, setupEditorTheme, getBlobLanguage } from './utils';
+import EditorInstance from './source_editor_instance';
+
+const instanceRemoveFromRegistry = (editor, instance) => {
+ const index = editor.instances.findIndex((inst) => inst === instance);
+ editor.instances.splice(index, 1);
+};
+
+const 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();
+ }
+};
export default class SourceEditor {
+ /**
+ * Constructs a global editor.
+ * @param {Object} options - Monaco config options used to create the editor
+ */
constructor(options = {}) {
this.instances = [];
+ this.extensionsStore = new Map();
this.options = {
extraEditorClassName: 'gl-source-editor',
...defaultEditorOptions,
@@ -26,39 +56,6 @@ export default class SourceEditor {
registerLanguages(...languages);
}
- static pushToImportsArray(arr, toImport) {
- arr.push(import(toImport));
- }
-
- static loadExtensions(extensions) {
- if (!extensions) {
- return Promise.resolve();
- }
- const promises = [];
- const extensionsArray = typeof extensions === 'string' ? extensions.split(',') : extensions;
-
- extensionsArray.forEach((ext) => {
- const prefix = ext.includes('/') ? '' : 'editor/';
- const trimmedExt = ext.replace(/^\//, '').trim();
- SourceEditor.pushToImportsArray(promises, `~/${prefix}${trimmedExt}`);
- });
-
- return Promise.all(promises);
- }
-
- static mixIntoInstance(source, inst) {
- if (!inst) {
- return;
- }
- const isClassInstance = source.constructor.prototype !== Object.prototype;
- const sanitizedSource = isClassInstance ? source.constructor.prototype : source;
- Object.getOwnPropertyNames(sanitizedSource).forEach((prop) => {
- if (prop !== 'constructor') {
- Object.assign(inst, { [prop]: source[prop] });
- }
- });
- }
-
static prepareInstance(el) {
if (!el) {
throw new Error(SOURCE_EDITOR_INSTANCE_ERROR_NO_EL);
@@ -71,23 +68,6 @@ export default class SourceEditor {
});
}
- static manageDefaultExtensions(instance, el, extensions) {
- SourceEditor.loadExtensions(extensions, instance)
- .then((modules) => {
- if (modules) {
- modules.forEach((module) => {
- instance.use(module.default);
- });
- }
- })
- .then(() => {
- el.dispatchEvent(new Event(EDITOR_READY_EVENT));
- })
- .catch((e) => {
- throw e;
- });
- }
-
static createEditorModel({
blobPath,
blobContent,
@@ -115,71 +95,17 @@ export default class SourceEditor {
return diffModel;
}
- static convertMonacoToELInstance = (inst) => {
- const sourceEditorInstanceAPI = {
- updateModelLanguage: (path) => {
- return SourceEditor.instanceUpdateLanguage(inst, path);
- },
- use: (exts = []) => {
- return SourceEditor.instanceApplyExtension(inst, exts);
- },
- };
- const handler = {
- get(target, prop, receiver) {
- if (Reflect.has(sourceEditorInstanceAPI, prop)) {
- return sourceEditorInstanceAPI[prop];
- }
- return Reflect.get(target, prop, receiver);
- },
- };
- return new Proxy(inst, handler);
- };
-
- static instanceUpdateLanguage(inst, path) {
- const lang = getBlobLanguage(path);
- const model = inst.getModel();
- return monacoEditor.setModelLanguage(model, lang);
- }
-
- static instanceApplyExtension(inst, exts = []) {
- const extensions = [].concat(exts);
- extensions.forEach((extension) => {
- SourceEditor.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.
+ * Creates a Source Editor Instance with the given options.
+ * @param {Object} options Options used to initialize the instance.
+ * @param {Element} options.el The element to attach the instance for.
* @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.blobOriginalContent The original blob's content. Is used when creating a Diff Instance.
* @param {string} options.blobGlobalId This is used to help globally identify monaco instances that are created with the same blobPath.
+ * @param {Boolean} options.isDiff Flag to enable creation of a Diff Instance?
+ * @param {...*} options.instanceOptions Configuration options used to instantiate an instance.
+ * @returns {EditorInstance}
*/
createInstance({
el = undefined,
@@ -187,20 +113,24 @@ export default class SourceEditor {
blobContent = '',
blobOriginalContent = '',
blobGlobalId = uuids()[0],
- extensions = [],
isDiff = false,
...instanceOptions
} = {}) {
SourceEditor.prepareInstance(el);
const createEditorFn = isDiff ? 'createDiffEditor' : 'create';
- const instance = SourceEditor.convertMonacoToELInstance(
+ const instance = new EditorInstance(
monacoEditor[createEditorFn].call(this, el, {
...this.options,
...instanceOptions,
}),
+ this.extensionsStore,
);
+ waitForCSSLoaded(() => {
+ instance.layout();
+ });
+
let model;
if (instanceOptions.model !== null) {
model = SourceEditor.createEditorModel({
@@ -214,16 +144,20 @@ export default class SourceEditor {
}
instance.onDidDispose(() => {
- SourceEditor.instanceRemoveFromRegistry(this, instance);
- SourceEditor.instanceDisposeModels(this, instance, model);
+ instanceRemoveFromRegistry(this, instance);
+ instanceDisposeModels(this, instance, model);
});
- SourceEditor.manageDefaultExtensions(instance, el, extensions);
-
this.instances.push(instance);
+ el.dispatchEvent(new CustomEvent(EDITOR_READY_EVENT, { instance }));
return instance;
}
+ /**
+ * Create a Diff Instance
+ * @param {Object} args Options to be passed further down to createInstance() with the same signature
+ * @returns {EditorInstance}
+ */
createDiffInstance(args) {
return this.createInstance({
...args,
@@ -231,14 +165,11 @@ export default class SourceEditor {
});
}
+ /**
+ * Dispose global editor
+ * Automatically disposes all the instances registered for this editor
+ */
dispose() {
this.instances.forEach((instance) => instance.dispose());
}
-
- use(exts) {
- this.instances.forEach((inst) => {
- inst.use(exts);
- });
- return this;
- }
}
diff --git a/app/assets/javascripts/editor/source_editor_extension.js b/app/assets/javascripts/editor/source_editor_extension.js
index f6bc62a1c09..6d47e1e2248 100644
--- a/app/assets/javascripts/editor/source_editor_extension.js
+++ b/app/assets/javascripts/editor/source_editor_extension.js
@@ -5,10 +5,10 @@ export default class EditorExtension {
if (typeof definition !== 'function') {
throw new Error(EDITOR_EXTENSION_DEFINITION_ERROR);
}
- this.name = definition.name; // both class- and fn-based extensions have a name
this.setupOptions = setupOptions;
// eslint-disable-next-line new-cap
this.obj = new definition();
+ this.extensionName = definition.extensionName || this.obj.extensionName; // both class- and fn-based extensions have a name
}
get api() {
diff --git a/app/assets/javascripts/editor/source_editor_instance.js b/app/assets/javascripts/editor/source_editor_instance.js
index e0ca4ea518b..8372a59964b 100644
--- a/app/assets/javascripts/editor/source_editor_instance.js
+++ b/app/assets/javascripts/editor/source_editor_instance.js
@@ -13,7 +13,7 @@
* A Source Editor Extension
* @typedef {Object} SourceEditorExtension
* @property {Object} obj
- * @property {string} name
+ * @property {string} extensionName
* @property {Object} api
*/
@@ -43,12 +43,12 @@ const utils = {
}
},
- getStoredExtension: (extensionsStore, name) => {
+ getStoredExtension: (extensionsStore, extensionName) => {
if (!extensionsStore) {
logError(EDITOR_EXTENSION_STORE_IS_MISSING_ERROR);
return undefined;
}
- return extensionsStore.get(name);
+ return extensionsStore.get(extensionName);
},
};
@@ -73,32 +73,18 @@ export default class EditorInstance {
if (methodExtension) {
const extension = extensionsStore.get(methodExtension);
- return (...args) => {
- return extension.api[prop].call(seInstance, ...args, receiver);
- };
+ if (typeof extension.api[prop] === 'function') {
+ return extension.api[prop].bind(extension.obj, receiver);
+ }
+
+ return extension.api[prop];
}
return Reflect.get(seInstance[prop] ? seInstance : target, prop, receiver);
},
- set(target, prop, value) {
- Object.assign(seInstance, {
- [prop]: value,
- });
- return true;
- },
};
const instProxy = new Proxy(rootInstance, getHandler);
- /**
- * Main entry point to apply an extension to the instance
- * @param {SourceEditorExtensionDefinition}
- */
- this.use = EditorInstance.useUnuse.bind(instProxy, extensionsStore, this.useExtension);
-
- /**
- * Main entry point to un-use an extension and remove it from the instance
- * @param {SourceEditorExtension}
- */
- this.unuse = EditorInstance.useUnuse.bind(instProxy, extensionsStore, this.unuseExtension);
+ this.dispatchExtAction = EditorInstance.useUnuse.bind(instProxy, extensionsStore);
return instProxy;
}
@@ -143,7 +129,7 @@ export default class EditorInstance {
}
// Existing Extension Path
- const existingExt = utils.getStoredExtension(extensionsStore, definition.name);
+ const existingExt = utils.getStoredExtension(extensionsStore, definition.extensionName);
if (existingExt) {
if (isEqual(extension.setupOptions, existingExt.setupOptions)) {
return existingExt;
@@ -155,7 +141,7 @@ export default class EditorInstance {
const extensionInstance = new EditorExtension(extension);
const { setupOptions, obj: extensionObj } = extensionInstance;
if (extensionObj.onSetup) {
- extensionObj.onSetup(setupOptions, this);
+ extensionObj.onSetup(this, setupOptions);
}
if (extensionsStore) {
this.registerExtension(extensionInstance, extensionsStore);
@@ -170,14 +156,14 @@ export default class EditorInstance {
* @param {Map} extensionsStore - The global registry for the extension instances
*/
registerExtension(extension, extensionsStore) {
- const { name } = extension;
+ const { extensionName } = extension;
const hasExtensionRegistered =
- extensionsStore.has(name) &&
- isEqual(extension.setupOptions, extensionsStore.get(name).setupOptions);
+ extensionsStore.has(extensionName) &&
+ isEqual(extension.setupOptions, extensionsStore.get(extensionName).setupOptions);
if (hasExtensionRegistered) {
return;
}
- extensionsStore.set(name, extension);
+ extensionsStore.set(extensionName, extension);
const { obj: extensionObj } = extension;
if (extensionObj.onUse) {
extensionObj.onUse(this);
@@ -189,7 +175,7 @@ export default class EditorInstance {
* @param {SourceEditorExtension} extension - Instance of Source Editor extension
*/
registerExtensionMethods(extension) {
- const { api, name } = extension;
+ const { api, extensionName } = extension;
if (!api) {
return;
@@ -199,7 +185,7 @@ export default class EditorInstance {
if (this[prop]) {
logError(sprintf(EDITOR_EXTENSION_NAMING_CONFLICT_ERROR, { prop }));
} else {
- this.methods[prop] = name;
+ this.methods[prop] = extensionName;
}
}, this);
}
@@ -217,10 +203,10 @@ export default class EditorInstance {
if (!extension) {
throw new Error(EDITOR_EXTENSION_NOT_SPECIFIED_FOR_UNUSE_ERROR);
}
- const { name } = extension;
- const existingExt = utils.getStoredExtension(extensionsStore, name);
+ const { extensionName } = extension;
+ const existingExt = utils.getStoredExtension(extensionsStore, extensionName);
if (!existingExt) {
- throw new Error(sprintf(EDITOR_EXTENSION_NOT_REGISTERED_ERROR, { name }));
+ throw new Error(sprintf(EDITOR_EXTENSION_NOT_REGISTERED_ERROR, { extensionName }));
}
const { obj: extensionObj } = existingExt;
if (extensionObj.onBeforeUnuse) {
@@ -237,12 +223,12 @@ export default class EditorInstance {
* @param {SourceEditorExtension} extension - Instance of Source Editor extension to un-use
*/
unregisterExtensionMethods(extension) {
- const { api, name } = extension;
+ const { api, extensionName } = extension;
if (!api) {
return;
}
Object.keys(api).forEach((method) => {
- utils.removeExtFromMethod(method, name, this.methods);
+ utils.removeExtFromMethod(method, extensionName, this.methods);
});
}
@@ -262,6 +248,24 @@ export default class EditorInstance {
}
/**
+ * Main entry point to apply an extension to the instance
+ * @param {SourceEditorExtensionDefinition[]|SourceEditorExtensionDefinition} extDefs - The extension(s) to use
+ * @returns {EditorExtension|*}
+ */
+ use(extDefs) {
+ return this.dispatchExtAction(this.useExtension, extDefs);
+ }
+
+ /**
+ * Main entry point to remove an extension to the instance
+ * @param {SourceEditorExtension[]|SourceEditorExtension} exts -
+ * @returns {*}
+ */
+ unuse(exts) {
+ return this.dispatchExtAction(this.unuseExtension, exts);
+ }
+
+ /**
* Get the methods returned by extensions.
* @returns {Array}
*/
diff --git a/app/assets/javascripts/emoji/constants.js b/app/assets/javascripts/emoji/constants.js
index e9f2272e759..a6eb4256561 100644
--- a/app/assets/javascripts/emoji/constants.js
+++ b/app/assets/javascripts/emoji/constants.js
@@ -16,3 +16,6 @@ export const CATEGORY_ICON_MAP = {
export const EMOJIS_PER_ROW = 9;
export const EMOJI_ROW_HEIGHT = 34;
export const CATEGORY_ROW_HEIGHT = 37;
+
+export const CACHE_VERSION_KEY = 'gl-emoji-map-version';
+export const CACHE_KEY = 'gl-emoji-map';
diff --git a/app/assets/javascripts/emoji/index.js b/app/assets/javascripts/emoji/index.js
index 478e3f6aed9..b507792cc91 100644
--- a/app/assets/javascripts/emoji/index.js
+++ b/app/assets/javascripts/emoji/index.js
@@ -1,26 +1,31 @@
import { escape, minBy } from 'lodash';
+import emojiRegexFactory from 'emoji-regex';
import emojiAliases from 'emojis/aliases.json';
-import { sanitize } from '~/lib/dompurify';
import AccessorUtilities from '../lib/utils/accessor';
import axios from '../lib/utils/axios_utils';
-import { CATEGORY_ICON_MAP, FREQUENTLY_USED_KEY } from './constants';
+import { CACHE_KEY, CACHE_VERSION_KEY, CATEGORY_ICON_MAP, FREQUENTLY_USED_KEY } from './constants';
let emojiMap = null;
let validEmojiNames = null;
export const FALLBACK_EMOJI_KEY = 'grey_question';
// Keep the version in sync with `lib/gitlab/emoji.rb`
-export const EMOJI_VERSION = '1';
+export const EMOJI_VERSION = '2';
const isLocalStorageAvailable = AccessorUtilities.canUseLocalStorage();
async function loadEmoji() {
if (
isLocalStorageAvailable &&
- window.localStorage.getItem('gl-emoji-map-version') === EMOJI_VERSION &&
- window.localStorage.getItem('gl-emoji-map')
+ window.localStorage.getItem(CACHE_VERSION_KEY) === EMOJI_VERSION &&
+ window.localStorage.getItem(CACHE_KEY)
) {
- return JSON.parse(window.localStorage.getItem('gl-emoji-map'));
+ const emojis = JSON.parse(window.localStorage.getItem(CACHE_KEY));
+ // Workaround because the pride flag is broken in EMOJI_VERSION = '1'
+ if (emojis.gay_pride_flag) {
+ emojis.gay_pride_flag.e = '🏳️‍🌈';
+ }
+ return emojis;
}
// We load the JSON file direct from the server
@@ -29,15 +34,19 @@ async function loadEmoji() {
const { data } = await axios.get(
`${gon.relative_url_root || ''}/-/emojis/${EMOJI_VERSION}/emojis.json`,
);
- window.localStorage.setItem('gl-emoji-map-version', EMOJI_VERSION);
- window.localStorage.setItem('gl-emoji-map', JSON.stringify(data));
+ window.localStorage.setItem(CACHE_VERSION_KEY, EMOJI_VERSION);
+ window.localStorage.setItem(CACHE_KEY, JSON.stringify(data));
return data;
}
async function loadEmojiWithNames() {
- return Object.entries(await loadEmoji()).reduce((acc, [key, value]) => {
- acc[key] = { ...value, name: key, e: sanitize(value.e) };
+ const emojiRegex = emojiRegexFactory();
+ return Object.entries(await loadEmoji()).reduce((acc, [key, value]) => {
+ // Filter out entries which aren't emojis
+ if (value.e.match(emojiRegex)?.[0] === value.e) {
+ acc[key] = { ...value, name: key };
+ }
return acc;
}, {});
}
diff --git a/app/assets/javascripts/environments/components/confirm_rollback_modal.vue b/app/assets/javascripts/environments/components/confirm_rollback_modal.vue
index 4783b92942c..0e556f093e2 100644
--- a/app/assets/javascripts/environments/components/confirm_rollback_modal.vue
+++ b/app/assets/javascripts/environments/components/confirm_rollback_modal.vue
@@ -7,6 +7,7 @@ import { escape } from 'lodash';
import csrf from '~/lib/utils/csrf';
import { __, s__, sprintf } from '~/locale';
+import rollbackEnvironment from '../graphql/mutations/rollback_environment.mutation.graphql';
import eventHub from '../event_hub';
export default {
@@ -40,10 +41,15 @@ export default {
required: false,
default: null,
},
+ graphql: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
},
computed: {
modalTitle() {
- const title = this.environment.isLastDeployment
+ const title = this.isLastDeployment
? s__('Environments|Re-deploy environment %{name}?')
: s__('Environments|Rollback environment %{name}?');
@@ -53,6 +59,11 @@ export default {
},
commitShortSha() {
if (this.hasMultipleCommits) {
+ if (this.graphql) {
+ const { lastDeployment } = this.environment;
+ return this.commitData(lastDeployment, 'shortId');
+ }
+
const { last_deployment } = this.environment;
return this.commitData(last_deployment, 'short_id');
}
@@ -61,6 +72,11 @@ export default {
},
commitUrl() {
if (this.hasMultipleCommits) {
+ if (this.graphql) {
+ const { lastDeployment } = this.environment;
+ return this.commitData(lastDeployment, 'commitPath');
+ }
+
const { last_deployment } = this.environment;
return this.commitData(last_deployment, 'commit_path');
}
@@ -68,9 +84,7 @@ export default {
return this.environment.commitUrl;
},
modalActionText() {
- return this.environment.isLastDeployment
- ? s__('Environments|Re-deploy')
- : s__('Environments|Rollback');
+ return this.isLastDeployment ? s__('Environments|Re-deploy') : s__('Environments|Rollback');
},
primaryProps() {
let attributes = [{ variant: 'danger' }];
@@ -84,20 +98,27 @@ export default {
attributes,
};
},
+ isLastDeployment() {
+ // eslint-disable-next-line @gitlab/require-i18n-strings
+ return this.environment?.isLastDeployment || this.environment?.lastDeployment?.['last?'];
+ },
},
methods: {
handleChange(event) {
this.$emit('change', event);
},
onOk() {
- eventHub.$emit('rollbackEnvironment', this.environment);
+ if (this.graphql) {
+ this.$apollo.mutate({
+ mutation: rollbackEnvironment,
+ variables: { environment: this.environment },
+ });
+ } else {
+ eventHub.$emit('rollbackEnvironment', this.environment);
+ }
},
commitData(lastDeployment, key) {
- if (lastDeployment && lastDeployment.commit) {
- return lastDeployment.commit[key];
- }
-
- return '';
+ return lastDeployment?.commit?.[key] ?? '';
},
},
csrf,
diff --git a/app/assets/javascripts/environments/components/delete_environment_modal.vue b/app/assets/javascripts/environments/components/delete_environment_modal.vue
index 26ec882472b..d3d4c7d23d8 100644
--- a/app/assets/javascripts/environments/components/delete_environment_modal.vue
+++ b/app/assets/javascripts/environments/components/delete_environment_modal.vue
@@ -1,7 +1,9 @@
<script>
import { GlTooltipDirective, GlModal } from '@gitlab/ui';
+import createFlash from '~/flash';
import { __, s__, sprintf } from '~/locale';
import eventHub from '../event_hub';
+import deleteEnvironmentMutation from '../graphql/mutations/delete_environment.mutation.graphql';
export default {
id: 'delete-environment-modal',
@@ -17,6 +19,11 @@ export default {
type: Object,
required: true,
},
+ graphql: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
},
computed: {
primaryProps() {
@@ -49,7 +56,29 @@ export default {
},
methods: {
onSubmit() {
- eventHub.$emit('deleteEnvironment', this.environment);
+ if (this.graphql) {
+ this.$apollo
+ .mutate({
+ mutation: deleteEnvironmentMutation,
+ variables: { environment: this.environment },
+ })
+ .then(([message]) => {
+ if (message) {
+ createFlash({ message });
+ }
+ })
+ .catch((error) =>
+ createFlash({
+ message: s__(
+ 'Environments|An error occurred while deleting the environment. Check if the environment stopped; if not, stop it and try again.',
+ ),
+ error,
+ captureError: true,
+ }),
+ );
+ } else {
+ eventHub.$emit('deleteEnvironment', this.environment);
+ }
},
},
};
diff --git a/app/assets/javascripts/environments/components/enable_review_app_modal.vue b/app/assets/javascripts/environments/components/enable_review_app_modal.vue
index d770a2302e8..b757c55bfdb 100644
--- a/app/assets/javascripts/environments/components/enable_review_app_modal.vue
+++ b/app/assets/javascripts/environments/components/enable_review_app_modal.vue
@@ -12,11 +12,20 @@ export default {
ModalCopyButton,
},
inject: ['defaultBranchName'],
+ model: {
+ prop: 'visible',
+ event: 'change',
+ },
props: {
modalId: {
type: String,
required: true,
},
+ visible: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
},
instructionText: {
step1: s__(
@@ -57,12 +66,15 @@ export default {
</script>
<template>
<gl-modal
+ :visible="visible"
:modal-id="modalId"
:title="$options.modalInfo.title"
+ static
size="lg"
ok-only
ok-variant="light"
:ok-title="$options.modalInfo.closeText"
+ @change="$emit('change', $event)"
>
<p>
<gl-sprintf :message="$options.instructionText.step1">
diff --git a/app/assets/javascripts/environments/components/environment_delete.vue b/app/assets/javascripts/environments/components/environment_delete.vue
index 8609503e486..63169b790c7 100644
--- a/app/assets/javascripts/environments/components/environment_delete.vue
+++ b/app/assets/javascripts/environments/components/environment_delete.vue
@@ -7,6 +7,7 @@
import { GlDropdownItem, GlModalDirective } from '@gitlab/ui';
import { s__ } from '~/locale';
import eventHub from '../event_hub';
+import setEnvironmentToDelete from '../graphql/mutations/set_environment_to_delete.mutation.graphql';
export default {
components: {
@@ -20,6 +21,11 @@ export default {
type: Object,
required: true,
},
+ graphql: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
},
data() {
return {
@@ -30,14 +36,25 @@ export default {
title: s__('Environments|Delete environment'),
},
mounted() {
- eventHub.$on('deleteEnvironment', this.onDeleteEnvironment);
+ if (!this.graphql) {
+ eventHub.$on('deleteEnvironment', this.onDeleteEnvironment);
+ }
},
beforeDestroy() {
- eventHub.$off('deleteEnvironment', this.onDeleteEnvironment);
+ if (!this.graphql) {
+ eventHub.$off('deleteEnvironment', this.onDeleteEnvironment);
+ }
},
methods: {
onClick() {
- eventHub.$emit('requestDeleteEnvironment', this.environment);
+ if (this.graphql) {
+ this.$apollo.mutate({
+ mutation: setEnvironmentToDelete,
+ variables: { environment: this.environment },
+ });
+ } else {
+ eventHub.$emit('requestDeleteEnvironment', this.environment);
+ }
},
onDeleteEnvironment(environment) {
if (this.environment.id === environment.id) {
diff --git a/app/assets/javascripts/environments/components/environment_item.vue b/app/assets/javascripts/environments/components/environment_item.vue
index db01d455b2b..be9bfb50de5 100644
--- a/app/assets/javascripts/environments/components/environment_item.vue
+++ b/app/assets/javascripts/environments/components/environment_item.vue
@@ -5,7 +5,7 @@ import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import { __, s__, sprintf } from '~/locale';
import CiIcon from '~/vue_shared/components/ci_icon.vue';
import CommitComponent from '~/vue_shared/components/commit.vue';
-import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate.vue';
+import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate/tooltip_on_truncate.vue';
import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue';
import timeagoMixin from '~/vue_shared/mixins/timeago';
import eventHub from '../event_hub';
diff --git a/app/assets/javascripts/environments/components/environment_rollback.vue b/app/assets/javascripts/environments/components/environment_rollback.vue
index 00497b3c683..f7f0cf4cb8d 100644
--- a/app/assets/javascripts/environments/components/environment_rollback.vue
+++ b/app/assets/javascripts/environments/components/environment_rollback.vue
@@ -8,6 +8,7 @@
import { GlModalDirective, GlDropdownItem } from '@gitlab/ui';
import { s__ } from '~/locale';
import eventHub from '../event_hub';
+import setEnvironmentToRollback from '../graphql/mutations/set_environment_to_rollback.mutation.graphql';
export default {
components: {
@@ -32,11 +33,12 @@ export default {
type: String,
required: true,
},
- },
- data() {
- return {
- isLoading: false,
- };
+
+ graphql: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
},
computed: {
@@ -49,16 +51,18 @@ export default {
methods: {
onClick() {
- eventHub.$emit('requestRollbackEnvironment', {
- ...this.environment,
- retryUrl: this.retryUrl,
- isLastDeployment: this.isLastDeployment,
- });
- eventHub.$on('rollbackEnvironment', (environment) => {
- if (environment.id === this.environment.id) {
- this.isLoading = true;
- }
- });
+ if (this.graphql) {
+ this.$apollo.mutate({
+ mutation: setEnvironmentToRollback,
+ variables: { environment: this.environment },
+ });
+ } else {
+ eventHub.$emit('requestRollbackEnvironment', {
+ ...this.environment,
+ retryUrl: this.retryUrl,
+ isLastDeployment: this.isLastDeployment,
+ });
+ }
},
},
};
diff --git a/app/assets/javascripts/environments/components/new_environment_folder.vue b/app/assets/javascripts/environments/components/new_environment_folder.vue
index 0615bdef537..fe3d6f1e8ca 100644
--- a/app/assets/javascripts/environments/components/new_environment_folder.vue
+++ b/app/assets/javascripts/environments/components/new_environment_folder.vue
@@ -1,9 +1,11 @@
<script>
-import { GlCollapse, GlIcon, GlBadge, GlLink } from '@gitlab/ui';
+import { GlButton, GlCollapse, GlIcon, GlBadge, GlLink } from '@gitlab/ui';
+import { __, s__ } from '~/locale';
import folderQuery from '../graphql/queries/folder.query.graphql';
export default {
components: {
+ GlButton,
GlCollapse,
GlIcon,
GlBadge,
@@ -26,12 +28,20 @@ export default {
},
},
},
+ i18n: {
+ collapse: __('Collapse'),
+ expand: __('Expand'),
+ link: s__('Environments|Show all'),
+ },
computed: {
icons() {
return this.visible
? { caret: 'angle-down', folder: 'folder-open' }
: { caret: 'angle-right', folder: 'folder-o' };
},
+ label() {
+ return this.visible ? this.$options.i18n.collapse : this.$options.i18n.expand;
+ },
count() {
return this.folder?.availableCount ?? 0;
},
@@ -51,18 +61,21 @@ export default {
</script>
<template>
<div class="gl-border-b-solid gl-border-gray-100 gl-border-1 gl-px-3 gl-pt-3 gl-pb-5">
- <div class="gl-w-full gl-display-flex gl-align-items-center" @click="toggleCollapse">
- <gl-icon
- class="gl-mr-2 gl-fill-current-color gl-text-gray-500"
- :name="icons.caret"
- :size="12"
+ <div class="gl-w-full gl-display-flex gl-align-items-center">
+ <gl-button
+ class="gl-mr-4 gl-fill-current-color gl-text-gray-500"
+ :aria-label="label"
+ :icon="icons.caret"
+ size="small"
+ category="tertiary"
+ @click="toggleCollapse"
/>
<gl-icon class="gl-mr-2 gl-fill-current-color gl-text-gray-500" :name="icons.folder" />
<div class="gl-mr-2 gl-text-gray-500" :class="folderClass">
{{ nestedEnvironment.name }}
</div>
<gl-badge size="sm" class="gl-mr-auto">{{ count }}</gl-badge>
- <gl-link v-if="visible" :href="folderPath">{{ s__('Environments|Show all') }}</gl-link>
+ <gl-link v-if="visible" :href="folderPath">{{ $options.i18n.link }}</gl-link>
</div>
<gl-collapse :visible="visible" />
</div>
diff --git a/app/assets/javascripts/environments/components/new_environments_app.vue b/app/assets/javascripts/environments/components/new_environments_app.vue
index a5526f9cd71..8d94e7021ca 100644
--- a/app/assets/javascripts/environments/components/new_environments_app.vue
+++ b/app/assets/javascripts/environments/components/new_environments_app.vue
@@ -1,47 +1,205 @@
<script>
-import { GlBadge, GlTab, GlTabs } from '@gitlab/ui';
-import environmentAppQuery from '../graphql/queries/environmentApp.query.graphql';
+import { GlBadge, GlPagination, GlTab, GlTabs } from '@gitlab/ui';
+import { s__, __, sprintf } from '~/locale';
+import { updateHistory, setUrlParams, queryToObject } from '~/lib/utils/url_utility';
+import environmentAppQuery from '../graphql/queries/environment_app.query.graphql';
+import pollIntervalQuery from '../graphql/queries/poll_interval.query.graphql';
+import pageInfoQuery from '../graphql/queries/page_info.query.graphql';
import EnvironmentFolder from './new_environment_folder.vue';
+import EnableReviewAppModal from './enable_review_app_modal.vue';
export default {
components: {
EnvironmentFolder,
+ EnableReviewAppModal,
GlBadge,
+ GlPagination,
GlTab,
GlTabs,
},
apollo: {
environmentApp: {
query: environmentAppQuery,
+ variables() {
+ return {
+ scope: this.scope,
+ page: this.page ?? 1,
+ };
+ },
+ pollInterval() {
+ return this.interval;
+ },
},
+ interval: {
+ query: pollIntervalQuery,
+ },
+ pageInfo: {
+ query: pageInfoQuery,
+ },
+ },
+ inject: ['newEnvironmentPath', 'canCreateEnvironment'],
+ i18n: {
+ newEnvironmentButtonLabel: s__('Environments|New environment'),
+ reviewAppButtonLabel: s__('Environments|Enable review app'),
+ available: __('Available'),
+ stopped: __('Stopped'),
+ prevPage: __('Go to previous page'),
+ nextPage: __('Go to next page'),
+ next: __('Next'),
+ prev: __('Prev'),
+ goto: (page) => sprintf(__('Go to page %{page}'), { page }),
+ },
+ modalId: 'enable-review-app-info',
+ data() {
+ const { page = '1', scope = 'available' } = queryToObject(window.location.search);
+ return {
+ interval: undefined,
+ isReviewAppModalVisible: false,
+ page: parseInt(page, 10),
+ scope,
+ };
},
computed: {
+ canSetupReviewApp() {
+ return this.environmentApp?.reviewApp?.canSetupReviewApp;
+ },
folders() {
return this.environmentApp?.environments.filter((e) => e.size > 1) ?? [];
},
availableCount() {
return this.environmentApp?.availableCount;
},
+ addEnvironment() {
+ if (!this.canCreateEnvironment) {
+ return null;
+ }
+
+ return {
+ text: this.$options.i18n.newEnvironmentButtonLabel,
+ attributes: {
+ href: this.newEnvironmentPath,
+ category: 'primary',
+ variant: 'confirm',
+ },
+ };
+ },
+ openReviewAppModal() {
+ if (!this.canSetupReviewApp) {
+ return null;
+ }
+
+ return {
+ text: this.$options.i18n.reviewAppButtonLabel,
+ attributes: {
+ category: 'secondary',
+ variant: 'confirm',
+ },
+ };
+ },
+ stoppedCount() {
+ return this.environmentApp?.stoppedCount;
+ },
+ totalItems() {
+ return this.pageInfo?.total;
+ },
+ itemsPerPage() {
+ return this.pageInfo?.perPage;
+ },
+ },
+ mounted() {
+ window.addEventListener('popstate', this.syncPageFromQueryParams);
+ },
+ destroyed() {
+ window.removeEventListener('popstate', this.syncPageFromQueryParams);
+ this.$apollo.queries.environmentApp.stopPolling();
+ },
+ methods: {
+ showReviewAppModal() {
+ this.isReviewAppModalVisible = true;
+ },
+ setScope(scope) {
+ this.scope = scope;
+ this.resetPolling();
+ },
+ movePage(direction) {
+ this.moveToPage(this.pageInfo[`${direction}Page`]);
+ },
+ moveToPage(page) {
+ this.page = page;
+ updateHistory({
+ url: setUrlParams({ page: this.page }),
+ title: document.title,
+ });
+ this.resetPolling();
+ },
+ syncPageFromQueryParams() {
+ const { page = '1' } = queryToObject(window.location.search);
+ this.page = parseInt(page, 10);
+ },
+ resetPolling() {
+ this.$apollo.queries.environmentApp.stopPolling();
+ this.$nextTick(() => {
+ if (this.interval) {
+ this.$apollo.queries.environmentApp.startPolling(this.interval);
+ } else {
+ this.$apollo.queries.environmentApp.refetch({ scope: this.scope, page: this.page });
+ }
+ });
+ },
},
};
</script>
<template>
<div>
- <gl-tabs>
- <gl-tab>
+ <enable-review-app-modal
+ v-if="canSetupReviewApp"
+ v-model="isReviewAppModalVisible"
+ :modal-id="$options.modalId"
+ data-testid="enable-review-app-modal"
+ />
+ <gl-tabs
+ :action-secondary="addEnvironment"
+ :action-primary="openReviewAppModal"
+ sync-active-tab-with-query-params
+ query-param-name="scope"
+ @primary="showReviewAppModal"
+ >
+ <gl-tab query-param-value="available" @click="setScope('available')">
<template #title>
- <span>{{ __('Available') }}</span>
+ <span>{{ $options.i18n.available }}</span>
<gl-badge size="sm" class="gl-tab-counter-badge">
{{ availableCount }}
</gl-badge>
</template>
- <environment-folder
- v-for="folder in folders"
- :key="folder.name"
- class="gl-mb-3"
- :nested-environment="folder"
- />
+ </gl-tab>
+ <gl-tab query-param-value="stopped" @click="setScope('stopped')">
+ <template #title>
+ <span>{{ $options.i18n.stopped }}</span>
+ <gl-badge size="sm" class="gl-tab-counter-badge">
+ {{ stoppedCount }}
+ </gl-badge>
+ </template>
</gl-tab>
</gl-tabs>
+ <environment-folder
+ v-for="folder in folders"
+ :key="folder.name"
+ class="gl-mb-3"
+ :nested-environment="folder"
+ />
+ <gl-pagination
+ align="center"
+ :total-items="totalItems"
+ :per-page="itemsPerPage"
+ :value="page"
+ :next="$options.i18n.next"
+ :prev="$options.i18n.prev"
+ :label-previous-page="$options.prevPage"
+ :label-next-page="$options.nextPage"
+ :label-page="$options.goto"
+ @next="movePage('next')"
+ @previous="movePage('previous')"
+ @input="moveToPage"
+ />
</div>
</template>
diff --git a/app/assets/javascripts/environments/graphql/client.js b/app/assets/javascripts/environments/graphql/client.js
index c734c2fba0c..64b18c2003b 100644
--- a/app/assets/javascripts/environments/graphql/client.js
+++ b/app/assets/javascripts/environments/graphql/client.js
@@ -1,6 +1,7 @@
import VueApollo from 'vue-apollo';
import createDefaultClient from '~/lib/graphql';
-import environmentApp from './queries/environmentApp.query.graphql';
+import environmentApp from './queries/environment_app.query.graphql';
+import pageInfoQuery from './queries/page_info.query.graphql';
import { resolvers } from './resolvers';
import typeDefs from './typedefs.graphql';
@@ -19,6 +20,19 @@ export const apolloProvider = (endpoint) => {
stoppedCount: 0,
},
});
+
+ cache.writeQuery({
+ query: pageInfoQuery,
+ data: {
+ pageInfo: {
+ total: 0,
+ perPage: 20,
+ nextPage: 0,
+ previousPage: 0,
+ __typename: 'LocalPageInfo',
+ },
+ },
+ });
return new VueApollo({
defaultClient,
});
diff --git a/app/assets/javascripts/environments/graphql/mutations/set_environment_to_delete.mutation.graphql b/app/assets/javascripts/environments/graphql/mutations/set_environment_to_delete.mutation.graphql
new file mode 100644
index 00000000000..ea72067bd37
--- /dev/null
+++ b/app/assets/javascripts/environments/graphql/mutations/set_environment_to_delete.mutation.graphql
@@ -0,0 +1,3 @@
+mutation SetEnvironmentToDelete($environment: Environment) {
+ setEnvironmentToDelete(environment: $environment) @client
+}
diff --git a/app/assets/javascripts/environments/graphql/mutations/set_environment_to_rollback.mutation.graphql b/app/assets/javascripts/environments/graphql/mutations/set_environment_to_rollback.mutation.graphql
new file mode 100644
index 00000000000..aba978ed79e
--- /dev/null
+++ b/app/assets/javascripts/environments/graphql/mutations/set_environment_to_rollback.mutation.graphql
@@ -0,0 +1,3 @@
+mutation SetEnvironmentToRollback($environment: Environment) {
+ setEnvironmentToRollback(environment: $environment) @client
+}
diff --git a/app/assets/javascripts/environments/graphql/queries/environmentApp.query.graphql b/app/assets/javascripts/environments/graphql/queries/environmentApp.query.graphql
deleted file mode 100644
index faa76c0a42c..00000000000
--- a/app/assets/javascripts/environments/graphql/queries/environmentApp.query.graphql
+++ /dev/null
@@ -1,8 +0,0 @@
-query getEnvironmentApp {
- environmentApp @client {
- availableCount
- environments
- reviewApp
- stoppedCount
- }
-}
diff --git a/app/assets/javascripts/environments/graphql/queries/environment_app.query.graphql b/app/assets/javascripts/environments/graphql/queries/environment_app.query.graphql
new file mode 100644
index 00000000000..2c17c42dd6d
--- /dev/null
+++ b/app/assets/javascripts/environments/graphql/queries/environment_app.query.graphql
@@ -0,0 +1,9 @@
+query getEnvironmentApp($page: Int, $scope: String) {
+ environmentApp(page: $page, scope: $scope) @client {
+ availableCount
+ stoppedCount
+ environments
+ reviewApp
+ stoppedCount
+ }
+}
diff --git a/app/assets/javascripts/environments/graphql/queries/environment_to_delete.query.graphql b/app/assets/javascripts/environments/graphql/queries/environment_to_delete.query.graphql
new file mode 100644
index 00000000000..5d39de8a0f1
--- /dev/null
+++ b/app/assets/javascripts/environments/graphql/queries/environment_to_delete.query.graphql
@@ -0,0 +1,7 @@
+query environmentToDelete {
+ environmentToDelete @client {
+ id
+ name
+ deletePath
+ }
+}
diff --git a/app/assets/javascripts/environments/graphql/queries/environment_to_rollback.query.graphql b/app/assets/javascripts/environments/graphql/queries/environment_to_rollback.query.graphql
new file mode 100644
index 00000000000..f7586e27665
--- /dev/null
+++ b/app/assets/javascripts/environments/graphql/queries/environment_to_rollback.query.graphql
@@ -0,0 +1,7 @@
+query environmentToRollback {
+ environmentToRollback @client {
+ id
+ name
+ lastDeployment
+ }
+}
diff --git a/app/assets/javascripts/environments/graphql/queries/page_info.query.graphql b/app/assets/javascripts/environments/graphql/queries/page_info.query.graphql
new file mode 100644
index 00000000000..d77ca05d46f
--- /dev/null
+++ b/app/assets/javascripts/environments/graphql/queries/page_info.query.graphql
@@ -0,0 +1,8 @@
+query getPageInfo {
+ pageInfo @client {
+ total
+ perPage
+ nextPage
+ previousPage
+ }
+}
diff --git a/app/assets/javascripts/environments/graphql/queries/poll_interval.query.graphql b/app/assets/javascripts/environments/graphql/queries/poll_interval.query.graphql
new file mode 100644
index 00000000000..28afc30a0dd
--- /dev/null
+++ b/app/assets/javascripts/environments/graphql/queries/poll_interval.query.graphql
@@ -0,0 +1,3 @@
+query pollInterval {
+ interval @client
+}
diff --git a/app/assets/javascripts/environments/graphql/resolvers.js b/app/assets/javascripts/environments/graphql/resolvers.js
index 8322b806370..9ebbc0ad1f8 100644
--- a/app/assets/javascripts/environments/graphql/resolvers.js
+++ b/app/assets/javascripts/environments/graphql/resolvers.js
@@ -1,5 +1,20 @@
import axios from '~/lib/utils/axios_utils';
-import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
+import { s__ } from '~/locale';
+import {
+ convertObjectPropsToCamelCase,
+ parseIntPagination,
+ normalizeHeaders,
+} from '~/lib/utils/common_utils';
+
+import pollIntervalQuery from './queries/poll_interval.query.graphql';
+import environmentToRollbackQuery from './queries/environment_to_rollback.query.graphql';
+import environmentToDeleteQuery from './queries/environment_to_delete.query.graphql';
+import pageInfoQuery from './queries/page_info.query.graphql';
+
+const buildErrors = (errors = []) => ({
+ errors,
+ __typename: 'LocalEnvironmentErrors',
+});
const mapNestedEnvironment = (env) => ({
...convertObjectPropsToCamelCase(env, { deep: true }),
@@ -12,17 +27,34 @@ const mapEnvironment = (env) => ({
export const resolvers = (endpoint) => ({
Query: {
- environmentApp() {
- return axios.get(endpoint, { params: { nested: true } }).then((res) => ({
- availableCount: res.data.available_count,
- environments: res.data.environments.map(mapNestedEnvironment),
- reviewApp: {
- ...convertObjectPropsToCamelCase(res.data.review_app),
- __typename: 'ReviewApp',
- },
- stoppedCount: res.data.stopped_count,
- __typename: 'LocalEnvironmentApp',
- }));
+ environmentApp(_context, { page, scope }, { cache }) {
+ return axios.get(endpoint, { params: { nested: true, page, scope } }).then((res) => {
+ const headers = normalizeHeaders(res.headers);
+ const interval = headers['POLL-INTERVAL'];
+ const pageInfo = { ...parseIntPagination(headers), __typename: 'LocalPageInfo' };
+
+ if (interval) {
+ cache.writeQuery({ query: pollIntervalQuery, data: { interval: parseFloat(interval) } });
+ } else {
+ cache.writeQuery({ query: pollIntervalQuery, data: { interval: undefined } });
+ }
+
+ cache.writeQuery({
+ query: pageInfoQuery,
+ data: { pageInfo },
+ });
+
+ return {
+ availableCount: res.data.available_count,
+ environments: res.data.environments.map(mapNestedEnvironment),
+ reviewApp: {
+ ...convertObjectPropsToCamelCase(res.data.review_app),
+ __typename: 'ReviewApp',
+ },
+ stoppedCount: res.data.stopped_count,
+ __typename: 'LocalEnvironmentApp',
+ };
+ });
},
folder(_, { environment: { folderPath } }) {
return axios.get(folderPath, { params: { per_page: 3 } }).then((res) => ({
@@ -32,19 +64,72 @@ export const resolvers = (endpoint) => ({
__typename: 'LocalEnvironmentFolder',
}));
},
+ isLastDeployment(_, { environment }) {
+ // eslint-disable-next-line @gitlab/require-i18n-strings
+ return environment?.lastDeployment?.['last?'];
+ },
},
- Mutations: {
- stopEnvironment(_, { environment: { stopPath } }) {
- return axios.post(stopPath);
+ Mutation: {
+ stopEnvironment(_, { environment }) {
+ return axios
+ .post(environment.stopPath)
+ .then(() => buildErrors())
+ .catch(() => {
+ return buildErrors([
+ s__('Environments|An error occurred while stopping the environment, please try again'),
+ ]);
+ });
},
deleteEnvironment(_, { environment: { deletePath } }) {
- return axios.delete(deletePath);
+ return axios
+ .delete(deletePath)
+ .then(() => buildErrors())
+ .catch(() =>
+ buildErrors([
+ s__(
+ 'Environments|An error occurred while deleting the environment. Check if the environment stopped; if not, stop it and try again.',
+ ),
+ ]),
+ );
+ },
+ rollbackEnvironment(_, { environment, isLastDeployment }) {
+ return axios
+ .post(environment?.retryUrl)
+ .then(() => buildErrors())
+ .catch(() => {
+ buildErrors([
+ isLastDeployment
+ ? s__(
+ 'Environments|An error occurred while re-deploying the environment, please try again',
+ )
+ : s__(
+ 'Environments|An error occurred while rolling back the environment, please try again',
+ ),
+ ]);
+ });
+ },
+ setEnvironmentToDelete(_, { environment }, { client }) {
+ client.writeQuery({
+ query: environmentToDeleteQuery,
+ data: { environmentToDelete: environment },
+ });
},
- rollbackEnvironment(_, { environment: { retryUrl } }) {
- return axios.post(retryUrl);
+ setEnvironmentToRollback(_, { environment }, { client }) {
+ client.writeQuery({
+ query: environmentToRollbackQuery,
+ data: { environmentToRollback: environment },
+ });
},
cancelAutoStop(_, { environment: { autoStopPath } }) {
- return axios.post(autoStopPath);
+ return axios
+ .post(autoStopPath)
+ .then(() => buildErrors())
+ .catch((err) =>
+ buildErrors([
+ err?.response?.data?.message ||
+ s__('Environments|An error occurred while canceling the auto stop, please try again'),
+ ]),
+ );
},
},
});
diff --git a/app/assets/javascripts/environments/graphql/typedefs.graphql b/app/assets/javascripts/environments/graphql/typedefs.graphql
index 49ea719449e..4a3abb0e89f 100644
--- a/app/assets/javascripts/environments/graphql/typedefs.graphql
+++ b/app/assets/javascripts/environments/graphql/typedefs.graphql
@@ -9,12 +9,29 @@ type LocalEnvironment {
autoStopPath: String
}
+input LocalEnvironmentInput {
+ id: Int!
+ globalId: ID!
+ name: String!
+ folderPath: String
+ stopPath: String
+ deletePath: String
+ retryUrl: String
+ autoStopPath: String
+}
+
type NestedLocalEnvironment {
name: String!
size: Int!
latest: LocalEnvironment!
}
+input NestedLocalEnvironmentInput {
+ name: String!
+ size: Int!
+ latest: LocalEnvironmentInput!
+}
+
type LocalEnvironmentFolder {
environments: [LocalEnvironment!]!
availableCount: Int!
@@ -33,3 +50,32 @@ type LocalEnvironmentApp {
environments: [NestedLocalEnvironment!]!
reviewApp: ReviewApp!
}
+
+type LocalErrors {
+ errors: [String!]!
+}
+
+type LocalPageInfo {
+ total: Int!
+ perPage: Int!
+ nextPage: Int!
+ previousPage: Int!
+}
+
+extend type Query {
+ environmentApp(page: Int, scope: String): LocalEnvironmentApp
+ folder(environment: NestedLocalEnvironmentInput): LocalEnvironmentFolder
+ environmentToDelete: LocalEnvironment
+ pageInfo: LocalPageInfo
+ environmentToRollback: LocalEnvironment
+ isLastDeployment: Boolean
+}
+
+extend type Mutation {
+ stopEnvironment(environment: LocalEnvironmentInput): LocalErrors
+ deleteEnvironment(environment: LocalEnvironmentInput): LocalErrors
+ rollbackEnvironment(environment: LocalEnvironmentInput): LocalErrors
+ cancelAutoStop(environment: LocalEnvironmentInput): LocalErrors
+ setEnvironmentToDelete(environment: LocalEnvironmentInput): LocalErrors
+ setEnvironmentToRollback(environment: LocalEnvironmentInput): LocalErrors
+}
diff --git a/app/assets/javascripts/error_tracking/components/error_details.vue b/app/assets/javascripts/error_tracking/components/error_details.vue
index 4adbf5362b7..e00fec6fddf 100644
--- a/app/assets/javascripts/error_tracking/components/error_details.vue
+++ b/app/assets/javascripts/error_tracking/components/error_details.vue
@@ -17,7 +17,7 @@ import createFlash from '~/flash';
import { __, sprintf, n__ } from '~/locale';
import Tracking from '~/tracking';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
-import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate.vue';
+import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate/tooltip_on_truncate.vue';
import TrackEventDirective from '~/vue_shared/directives/track_event';
import query from '../queries/details.query.graphql';
import {
diff --git a/app/assets/javascripts/error_tracking/queries/details.query.graphql b/app/assets/javascripts/error_tracking/queries/details.query.graphql
index af386528f00..f70e09d76f7 100644
--- a/app/assets/javascripts/error_tracking/queries/details.query.graphql
+++ b/app/assets/javascripts/error_tracking/queries/details.query.graphql
@@ -1,5 +1,6 @@
query errorDetails($fullPath: ID!, $errorId: ID!) {
project(fullPath: $fullPath) {
+ id
sentryErrors {
detailedError(id: $errorId) {
id
diff --git a/app/assets/javascripts/experimentation/utils.js b/app/assets/javascripts/experimentation/utils.js
index dcb6a8e20a3..69fa7adc653 100644
--- a/app/assets/javascripts/experimentation/utils.js
+++ b/app/assets/javascripts/experimentation/utils.js
@@ -1,5 +1,5 @@
-// This file only applies to use of experiments through https://gitlab.com/gitlab-org/gitlab-experiment
-import { get } from 'lodash';
+// This file only applies to use of experiments through https://gitlab.com/gitlab-org/ruby/gems/gitlab-experiment
+import { get, mapValues, pick } from 'lodash';
import { DEFAULT_VARIANT, CANDIDATE_VARIANT, TRACKING_CONTEXT_SCHEMA } from './constants';
function getExperimentsData() {
@@ -8,19 +8,18 @@ function getExperimentsData() {
// Pull from preferred window.gl.experiments
const experimentsFromGl = get(window, ['gl', 'experiments'], {});
- return { ...experimentsFromGon, ...experimentsFromGl };
-}
-
-function convertExperimentDataToExperimentContext(experimentData) {
- // Bandaid to allow-list only the properties which the current gitlab_experiment context schema suppports.
+ // Bandaid to allow-list only the properties which the current gitlab_experiment
+ // context schema suppports, since we most often use this data to create that
+ // Snowplow context.
// See TRACKING_CONTEXT_SCHEMA for current version (1-0-0)
// https://gitlab.com/gitlab-org/iglu/-/blob/master/public/schemas/com.gitlab/gitlab_experiment/jsonschema/1-0-0
- const { experiment: experimentName, key, variant, migration_keys } = experimentData;
+ return mapValues({ ...experimentsFromGon, ...experimentsFromGl }, (xp) => {
+ return pick(xp, ['experiment', 'key', 'variant', 'migration_keys']);
+ });
+}
- return {
- schema: TRACKING_CONTEXT_SCHEMA,
- data: { experiment: experimentName, key, variant, migration_keys },
- };
+function createGitlabExperimentContext(experimentData) {
+ return { schema: TRACKING_CONTEXT_SCHEMA, data: experimentData };
}
export function getExperimentData(experimentName) {
@@ -28,10 +27,10 @@ export function getExperimentData(experimentName) {
}
export function getAllExperimentContexts() {
- return Object.values(getExperimentsData()).map(convertExperimentDataToExperimentContext);
+ return Object.values(getExperimentsData()).map(createGitlabExperimentContext);
}
-export function isExperimentVariant(experimentName, variantName) {
+export function isExperimentVariant(experimentName, variantName = CANDIDATE_VARIANT) {
return getExperimentData(experimentName)?.variant === variantName;
}
diff --git a/app/assets/javascripts/feature_flags/components/form.vue b/app/assets/javascripts/feature_flags/components/form.vue
index 29e82289107..26da0d56f9a 100644
--- a/app/assets/javascripts/feature_flags/components/form.vue
+++ b/app/assets/javascripts/feature_flags/components/form.vue
@@ -142,7 +142,14 @@ export default {
return !this.$options.rolloutPercentageRegex.test(percentage);
}),
onFormStrategyChange(strategy, index) {
+ const currentUserListId = this.filteredStrategies[index]?.userList?.id;
+ const newUserListId = strategy?.userList?.id;
+
Object.assign(this.filteredStrategies[index], strategy);
+
+ if (currentUserListId !== newUserListId) {
+ this.formStrategies = [...this.formStrategies];
+ }
},
},
};
diff --git a/app/assets/javascripts/filtered_search/available_dropdown_mappings.js b/app/assets/javascripts/filtered_search/available_dropdown_mappings.js
index e0281b8f443..3cd4d48a4a3 100644
--- a/app/assets/javascripts/filtered_search/available_dropdown_mappings.js
+++ b/app/assets/javascripts/filtered_search/available_dropdown_mappings.js
@@ -1,4 +1,4 @@
-import { sortMilestonesByDueDate } from '~/milestones/milestone_utils';
+import { sortMilestonesByDueDate } from '~/milestones/utils';
import { mergeUrlParams } from '../lib/utils/url_utility';
import DropdownAjaxFilter from './dropdown_ajax_filter';
import DropdownEmoji from './dropdown_emoji';
diff --git a/app/assets/javascripts/filtered_search/constants.js b/app/assets/javascripts/filtered_search/constants.js
index 08736b09407..e2d6936acbd 100644
--- a/app/assets/javascripts/filtered_search/constants.js
+++ b/app/assets/javascripts/filtered_search/constants.js
@@ -11,3 +11,10 @@ export const FILTER_TYPE = {
};
export const MAX_HISTORY_SIZE = 5;
+
+export const FILTERED_SEARCH = {
+ MERGE_REQUESTS: 'merge_requests',
+ ISSUES: 'issues',
+ ADMIN_RUNNERS: 'admin/runners',
+ GROUP_RUNNERS_ANCHOR: 'runners-settings',
+};
diff --git a/app/assets/javascripts/flash.js b/app/assets/javascripts/flash.js
index 1287a7ed746..f0ef55f73eb 100644
--- a/app/assets/javascripts/flash.js
+++ b/app/assets/javascripts/flash.js
@@ -62,7 +62,7 @@ const createFlashEl = (message, type) => `
</div>
`;
-const removeFlashClickListener = (flashEl, fadeTransition) => {
+const addDismissFlashClickListener = (flashEl, fadeTransition) => {
// There are some flash elements which do not have a closeEl.
// https://gitlab.com/gitlab-org/gitlab/blob/763426ef344488972eb63ea5be8744e0f8459e6b/ee/app/views/layouts/header/_read_only_banner.html.haml
getCloseEl(flashEl)?.addEventListener('click', () => hideFlash(flashEl, fadeTransition));
@@ -113,7 +113,7 @@ const createFlash = function createFlash({
}
}
- removeFlashClickListener(flashEl, fadeTransition);
+ addDismissFlashClickListener(flashEl, fadeTransition);
flashContainer.classList.add('gl-display-block');
@@ -130,10 +130,8 @@ const createFlash = function createFlash({
export {
createFlash as default,
- createFlashEl,
- createAction,
hideFlash,
- removeFlashClickListener,
+ addDismissFlashClickListener,
FLASH_TYPES,
FLASH_CLOSED_EVENT,
};
diff --git a/app/assets/javascripts/google_cloud/components/app.vue b/app/assets/javascripts/google_cloud/components/app.vue
index 1e5be9df019..64784755b66 100644
--- a/app/assets/javascripts/google_cloud/components/app.vue
+++ b/app/assets/javascripts/google_cloud/components/app.vue
@@ -1,22 +1,42 @@
<script>
-import { GlTab, GlTabs } from '@gitlab/ui';
+import { __ } from '~/locale';
+
+import Home from './home.vue';
import IncubationBanner from './incubation_banner.vue';
-import ServiceAccounts from './service_accounts.vue';
+import ServiceAccountsForm from './service_accounts_form.vue';
+import NoGcpProjects from './errors/no_gcp_projects.vue';
+import GcpError from './errors/gcp_error.vue';
+
+const SCREEN_GCP_ERROR = 'gcp_error';
+const SCREEN_HOME = 'home';
+const SCREEN_NO_GCP_PROJECTS = 'no_gcp_projects';
+const SCREEN_SERVICE_ACCOUNTS_FORM = 'service_accounts_form';
export default {
- components: { GlTab, GlTabs, IncubationBanner, ServiceAccounts },
+ components: {
+ IncubationBanner,
+ },
+ inheritAttrs: false,
props: {
- serviceAccounts: {
- type: Array,
+ screen: {
required: true,
- },
- createServiceAccountUrl: {
type: String,
- required: true,
},
- emptyIllustrationUrl: {
- type: String,
- required: true,
+ },
+ computed: {
+ mainComponent() {
+ switch (this.screen) {
+ case SCREEN_HOME:
+ return Home;
+ case SCREEN_GCP_ERROR:
+ return GcpError;
+ case SCREEN_NO_GCP_PROJECTS:
+ return NoGcpProjects;
+ case SCREEN_SERVICE_ACCOUNTS_FORM:
+ return ServiceAccountsForm;
+ default:
+ throw new Error(__('Unknown screen'));
+ }
},
},
methods: {
@@ -34,17 +54,6 @@ export default {
:report-bug-url="feedbackUrl('report_bug')"
:feature-request-url="feedbackUrl('feature_request')"
/>
- <gl-tabs>
- <gl-tab :title="__('Configuration')">
- <service-accounts
- class="gl-mx-3"
- :list="serviceAccounts"
- :create-url="createServiceAccountUrl"
- :empty-illustration-url="emptyIllustrationUrl"
- />
- </gl-tab>
- <gl-tab :title="__('Deployments')" disabled />
- <gl-tab :title="__('Services')" disabled />
- </gl-tabs>
+ <component :is="mainComponent" v-bind="$attrs" />
</div>
</template>
diff --git a/app/assets/javascripts/google_cloud/components/errors/gcp_error.vue b/app/assets/javascripts/google_cloud/components/errors/gcp_error.vue
new file mode 100644
index 00000000000..90aa0e1ae68
--- /dev/null
+++ b/app/assets/javascripts/google_cloud/components/errors/gcp_error.vue
@@ -0,0 +1,29 @@
+<script>
+import { GlAlert } from '@gitlab/ui';
+import { __ } from '~/locale';
+
+export default {
+ components: { GlAlert },
+ props: {
+ error: {
+ type: String,
+ required: true,
+ },
+ },
+ i18n: {
+ title: __('Google Cloud project misconfigured'),
+ description: __(
+ 'GitLab and Google Cloud configuration seems to be incomplete. This probably can be fixed by your GitLab administration team. You may share these logs with them:',
+ ),
+ },
+};
+</script>
+
+<template>
+ <gl-alert :dismissible="false" variant="warning" :title="$options.i18n.title">
+ {{ $options.i18n.description }}
+ <blockquote>
+ <code>{{ error }}</code>
+ </blockquote>
+ </gl-alert>
+</template>
diff --git a/app/assets/javascripts/google_cloud/components/errors/no_gcp_projects.vue b/app/assets/javascripts/google_cloud/components/errors/no_gcp_projects.vue
new file mode 100644
index 00000000000..da229ac3f0e
--- /dev/null
+++ b/app/assets/javascripts/google_cloud/components/errors/no_gcp_projects.vue
@@ -0,0 +1,26 @@
+<script>
+import { GlAlert, GlButton } from '@gitlab/ui';
+import { __ } from '~/locale';
+
+export default {
+ components: { GlAlert, GlButton },
+ i18n: {
+ title: __('Google Cloud project required'),
+ description: __(
+ 'You do not have any Google Cloud projects. Please create a Google Cloud project and then reload this page.',
+ ),
+ createLabel: __('Create Google Cloud project'),
+ },
+};
+</script>
+
+<template>
+ <gl-alert :dismissible="false" variant="warning" :title="$options.i18n.title">
+ {{ $options.i18n.description }}
+ <template #actions>
+ <gl-button href="https://console.cloud.google.com/projectcreate" target="_blank">
+ {{ $options.i18n.createLabel }}
+ </gl-button>
+ </template>
+ </gl-alert>
+</template>
diff --git a/app/assets/javascripts/google_cloud/components/home.vue b/app/assets/javascripts/google_cloud/components/home.vue
new file mode 100644
index 00000000000..05f39de66ee
--- /dev/null
+++ b/app/assets/javascripts/google_cloud/components/home.vue
@@ -0,0 +1,41 @@
+<script>
+import { GlTabs, GlTab } from '@gitlab/ui';
+import ServiceAccountsList from './service_accounts_list.vue';
+
+export default {
+ components: {
+ GlTabs,
+ GlTab,
+ ServiceAccountsList,
+ },
+ props: {
+ serviceAccounts: {
+ type: Array,
+ required: true,
+ },
+ createServiceAccountUrl: {
+ type: String,
+ required: true,
+ },
+ emptyIllustrationUrl: {
+ type: String,
+ required: true,
+ },
+ },
+};
+</script>
+
+<template>
+ <gl-tabs>
+ <gl-tab :title="__('Configuration')">
+ <service-accounts-list
+ class="gl-mx-4"
+ :list="serviceAccounts"
+ :create-url="createServiceAccountUrl"
+ :empty-illustration-url="emptyIllustrationUrl"
+ />
+ </gl-tab>
+ <gl-tab :title="__('Deployments')" disabled />
+ <gl-tab :title="__('Services')" disabled />
+ </gl-tabs>
+</template>
diff --git a/app/assets/javascripts/google_cloud/components/service_accounts_form.vue b/app/assets/javascripts/google_cloud/components/service_accounts_form.vue
new file mode 100644
index 00000000000..e7a09668473
--- /dev/null
+++ b/app/assets/javascripts/google_cloud/components/service_accounts_form.vue
@@ -0,0 +1,70 @@
+<script>
+import { GlButton, GlFormGroup, GlFormSelect } from '@gitlab/ui';
+import { __ } from '~/locale';
+
+export default {
+ components: { GlButton, GlFormGroup, GlFormSelect },
+ props: {
+ gcpProjects: { required: true, type: Array },
+ environments: { required: true, type: Array },
+ cancelPath: { required: true, type: String },
+ },
+ i18n: {
+ title: __('Create service account'),
+ gcpProjectLabel: __('Google Cloud project'),
+ gcpProjectDescription: __(
+ 'New service account is generated for the selected Google Cloud project',
+ ),
+ environmentLabel: __('Environment'),
+ environmentDescription: __('Generated service account is linked to the selected environment'),
+ submitLabel: __('Create service account'),
+ cancelLabel: __('Cancel'),
+ },
+};
+</script>
+
+<template>
+ <div>
+ <header class="gl-my-5 gl-border-b-1 gl-border-b-gray-100 gl-border-b-solid">
+ <h2 class="gl-font-size-h1">{{ $options.i18n.title }}</h2>
+ </header>
+ <gl-form-group
+ label-for="gcp_project"
+ :label="$options.i18n.gcpProjectLabel"
+ :description="$options.i18n.gcpProjectDescription"
+ >
+ <gl-form-select id="gcp_project" name="gcp_project" required>
+ <option
+ v-for="gcpProject in gcpProjects"
+ :key="gcpProject.project_id"
+ :value="gcpProject.project_id"
+ >
+ {{ gcpProject.name }}
+ </option>
+ </gl-form-select>
+ </gl-form-group>
+ <gl-form-group
+ label-for="environment"
+ :label="$options.i18n.environmentLabel"
+ :description="$options.i18n.environmentDescription"
+ >
+ <gl-form-select id="environment" name="environment" required>
+ <option value="*">{{ __('All') }}</option>
+ <option
+ v-for="environment in environments"
+ :key="environment.name"
+ :value="environment.name"
+ >
+ {{ environment.name }}
+ </option>
+ </gl-form-select>
+ </gl-form-group>
+
+ <div class="form-actions row">
+ <gl-button type="submit" category="primary" variant="confirm">
+ {{ $options.i18n.submitLabel }}
+ </gl-button>
+ <gl-button class="gl-ml-1" :href="cancelPath">{{ $options.i18n.cancelLabel }}</gl-button>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/google_cloud/components/service_accounts.vue b/app/assets/javascripts/google_cloud/components/service_accounts_list.vue
index b70b25a5dc3..b70b25a5dc3 100644
--- a/app/assets/javascripts/google_cloud/components/service_accounts.vue
+++ b/app/assets/javascripts/google_cloud/components/service_accounts_list.vue
diff --git a/app/assets/javascripts/google_cloud/index.js b/app/assets/javascripts/google_cloud/index.js
index a156a632e9a..ab9e8227812 100644
--- a/app/assets/javascripts/google_cloud/index.js
+++ b/app/assets/javascripts/google_cloud/index.js
@@ -1,11 +1,12 @@
import Vue from 'vue';
import App from './components/app.vue';
-const elementRenderer = (element, props = {}) => (createElement) =>
- createElement(element, { props });
-
export default () => {
- const root = document.querySelector('#js-google-cloud');
- const props = JSON.parse(root.getAttribute('data'));
- return new Vue({ el: root, render: elementRenderer(App, props) });
+ const root = '#js-google-cloud';
+ const element = document.querySelector(root);
+ const { screen, ...attrs } = JSON.parse(element.getAttribute('data'));
+ return new Vue({
+ el: element,
+ render: (createElement) => createElement(App, { props: { screen }, attrs }),
+ });
};
diff --git a/app/assets/javascripts/graphql_shared/constants.js b/app/assets/javascripts/graphql_shared/constants.js
index 692de9dcb88..3b36c3e6ac5 100644
--- a/app/assets/javascripts/graphql_shared/constants.js
+++ b/app/assets/javascripts/graphql_shared/constants.js
@@ -1,6 +1,8 @@
export const MINIMUM_SEARCH_LENGTH = 3;
export const TYPE_CI_RUNNER = 'Ci::Runner';
+export const TYPE_CRM_CONTACT = 'CustomerRelations::Contact';
+export const TYPE_DISCUSSION = 'Discussion';
export const TYPE_EPIC = 'Epic';
export const TYPE_GROUP = 'Group';
export const TYPE_ISSUE = 'Issue';
@@ -8,10 +10,10 @@ export const TYPE_ITERATION = 'Iteration';
export const TYPE_ITERATIONS_CADENCE = 'Iterations::Cadence';
export const TYPE_MERGE_REQUEST = 'MergeRequest';
export const TYPE_MILESTONE = 'Milestone';
+export const TYPE_NOTE = 'Note';
+export const TYPE_PACKAGES_PACKAGE = 'Packages::Package';
export const TYPE_PROJECT = 'Project';
export const TYPE_SCANNER_PROFILE = 'DastScannerProfile';
export const TYPE_SITE_PROFILE = 'DastSiteProfile';
export const TYPE_USER = 'User';
export const TYPE_VULNERABILITY = 'Vulnerability';
-export const TYPE_NOTE = 'Note';
-export const TYPE_DISCUSSION = 'Discussion';
diff --git a/app/assets/javascripts/graphql_shared/fragments/alert.fragment.graphql b/app/assets/javascripts/graphql_shared/fragments/alert.fragment.graphql
index 2c771c32e16..64f547f933a 100644
--- a/app/assets/javascripts/graphql_shared/fragments/alert.fragment.graphql
+++ b/app/assets/javascripts/graphql_shared/fragments/alert.fragment.graphql
@@ -6,6 +6,7 @@ fragment AlertListItem on AlertManagementAlert {
startedAt
eventCount
issue {
+ id
iid
state
title
diff --git a/app/assets/javascripts/graphql_shared/fragments/alert_detail_item.fragment.graphql b/app/assets/javascripts/graphql_shared/fragments/alert_detail_item.fragment.graphql
index 9a9ae369519..794fe0a6151 100644
--- a/app/assets/javascripts/graphql_shared/fragments/alert_detail_item.fragment.graphql
+++ b/app/assets/javascripts/graphql_shared/fragments/alert_detail_item.fragment.graphql
@@ -12,6 +12,7 @@ fragment AlertDetailItem on AlertManagementAlert {
endedAt
hosts
environment {
+ id
name
path
}
diff --git a/app/assets/javascripts/graphql_shared/fragments/issuable_timelogs.fragment.graphql b/app/assets/javascripts/graphql_shared/fragments/issuable_timelogs.fragment.graphql
index 3551394ff97..78b2cd34a5c 100644
--- a/app/assets/javascripts/graphql_shared/fragments/issuable_timelogs.fragment.graphql
+++ b/app/assets/javascripts/graphql_shared/fragments/issuable_timelogs.fragment.graphql
@@ -1,10 +1,12 @@
fragment TimelogFragment on Timelog {
timeSpent
user {
+ id
name
}
spentAt
note {
+ id
body
}
summary
diff --git a/app/assets/javascripts/graphql_shared/fragments/user_availability.fragment.graphql b/app/assets/javascripts/graphql_shared/fragments/user_availability.fragment.graphql
index 0b451262b5a..429993b37bf 100644
--- a/app/assets/javascripts/graphql_shared/fragments/user_availability.fragment.graphql
+++ b/app/assets/javascripts/graphql_shared/fragments/user_availability.fragment.graphql
@@ -1,3 +1,4 @@
+# eslint-disable-next-line @graphql-eslint/require-id-when-available
fragment UserAvailability on User {
status {
availability
diff --git a/app/assets/javascripts/graphql_shared/mutations/create_merge_request.mutation.graphql b/app/assets/javascripts/graphql_shared/mutations/create_merge_request.mutation.graphql
index 79c56448b3f..2adaf24ed34 100644
--- a/app/assets/javascripts/graphql_shared/mutations/create_merge_request.mutation.graphql
+++ b/app/assets/javascripts/graphql_shared/mutations/create_merge_request.mutation.graphql
@@ -1,6 +1,7 @@
mutation createMergeRequest($input: MergeRequestCreateInput!) {
mergeRequestCreate(input: $input) {
mergeRequest {
+ id
iid
}
errors
diff --git a/app/assets/javascripts/graphql_shared/queries/alert_details.query.graphql b/app/assets/javascripts/graphql_shared/queries/alert_details.query.graphql
index 5ee2cf7ca44..8debc6113d1 100644
--- a/app/assets/javascripts/graphql_shared/queries/alert_details.query.graphql
+++ b/app/assets/javascripts/graphql_shared/queries/alert_details.query.graphql
@@ -2,6 +2,7 @@
query alertDetails($fullPath: ID!, $alertId: String) {
project(fullPath: $fullPath) {
+ id
alertManagementAlerts(iid: $alertId) {
nodes {
...AlertDetailItem
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 095e4fe29df..9ffa0bad9ad 100644
--- a/app/assets/javascripts/graphql_shared/queries/get_alerts.query.graphql
+++ b/app/assets/javascripts/graphql_shared/queries/get_alerts.query.graphql
@@ -14,6 +14,7 @@ query getAlerts(
$domain: AlertManagementDomainFilter = operations
) {
project(fullPath: $projectPath) {
+ id
alertManagementAlerts(
search: $searchTerm
assigneeUsername: $assigneeUsername
diff --git a/app/assets/javascripts/graphql_shared/queries/group_users_search.query.graphql b/app/assets/javascripts/graphql_shared/queries/group_users_search.query.graphql
index c5f99a1657e..7c88e494a2e 100644
--- a/app/assets/javascripts/graphql_shared/queries/group_users_search.query.graphql
+++ b/app/assets/javascripts/graphql_shared/queries/group_users_search.query.graphql
@@ -6,6 +6,7 @@ query groupUsersSearch($search: String!, $fullPath: ID!) {
id
users: groupMembers(search: $search, relations: [DIRECT, DESCENDANTS, INHERITED]) {
nodes {
+ id
user {
...User
...UserAvailability
diff --git a/app/assets/javascripts/graphql_shared/queries/project_user_members_search.query.graphql b/app/assets/javascripts/graphql_shared/queries/project_user_members_search.query.graphql
index 62ce27815c7..ef3070d3437 100644
--- a/app/assets/javascripts/graphql_shared/queries/project_user_members_search.query.graphql
+++ b/app/assets/javascripts/graphql_shared/queries/project_user_members_search.query.graphql
@@ -3,6 +3,7 @@ query searchProjectMembers($fullPath: ID!, $search: String) {
id
projectMembers(search: $search) {
nodes {
+ id
user {
id
name
diff --git a/app/assets/javascripts/graphql_shared/queries/users_search.query.graphql b/app/assets/javascripts/graphql_shared/queries/users_search.query.graphql
index d04a49f8b3a..bb34e4032f4 100644
--- a/app/assets/javascripts/graphql_shared/queries/users_search.query.graphql
+++ b/app/assets/javascripts/graphql_shared/queries/users_search.query.graphql
@@ -3,8 +3,10 @@
query projectUsersSearch($search: String!, $fullPath: ID!) {
workspace: project(fullPath: $fullPath) {
+ id
users: projectMembers(search: $search, relations: [DIRECT, INHERITED, INVITED_GROUPS]) {
nodes {
+ id
user {
...User
...UserAvailability
diff --git a/app/assets/javascripts/header_search/components/app.vue b/app/assets/javascripts/header_search/components/app.vue
index c6590fd8eb3..edc6573a489 100644
--- a/app/assets/javascripts/header_search/components/app.vue
+++ b/app/assets/javascripts/header_search/components/app.vue
@@ -1,8 +1,17 @@
<script>
import { GlSearchBoxByType, GlOutsideDirective as Outside } from '@gitlab/ui';
import { mapState, mapActions, mapGetters } from 'vuex';
+import { debounce } from 'lodash';
import { visitUrl } from '~/lib/utils/url_utility';
-import { __ } from '~/locale';
+import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
+import { s__, sprintf } from '~/locale';
+import DropdownKeyboardNavigation from '~/vue_shared/components/dropdown_keyboard_navigation.vue';
+import {
+ FIRST_DROPDOWN_INDEX,
+ SEARCH_BOX_INDEX,
+ SEARCH_INPUT_DESCRIPTION,
+ SEARCH_RESULTS_DESCRIPTION,
+} from '../constants';
import HeaderSearchAutocompleteItems from './header_search_autocomplete_items.vue';
import HeaderSearchDefaultItems from './header_search_default_items.vue';
import HeaderSearchScopedItems from './header_search_scoped_items.vue';
@@ -10,7 +19,21 @@ import HeaderSearchScopedItems from './header_search_scoped_items.vue';
export default {
name: 'HeaderSearchApp',
i18n: {
- searchPlaceholder: __('Search or jump to...'),
+ searchPlaceholder: s__('GlobalSearch|Search or jump to...'),
+ searchAria: s__('GlobalSearch|Search GitLab'),
+ searchInputDescribeByNoDropdown: s__(
+ 'GlobalSearch|Type and press the enter key to submit search.',
+ ),
+ searchInputDescribeByWithDropdown: s__(
+ 'GlobalSearch|Type for new suggestions to appear below.',
+ ),
+ searchDescribedByDefault: s__(
+ 'GlobalSearch|%{count} default results provided. Use the up and down arrow keys to navigate search results list.',
+ ),
+ searchDescribedByUpdated: s__(
+ 'GlobalSearch|Results updated. %{count} results available. Use the up and down arrow keys to navigate search results list, or ENTER to submit.',
+ ),
+ searchResultsLoading: s__('GlobalSearch|Search results are loading'),
},
directives: { Outside },
components: {
@@ -18,15 +41,17 @@ export default {
HeaderSearchDefaultItems,
HeaderSearchScopedItems,
HeaderSearchAutocompleteItems,
+ DropdownKeyboardNavigation,
},
data() {
return {
showDropdown: false,
+ currentFocusIndex: SEARCH_BOX_INDEX,
};
},
computed: {
- ...mapState(['search']),
- ...mapGetters(['searchQuery']),
+ ...mapState(['search', 'loading']),
+ ...mapGetters(['searchQuery', 'searchOptions']),
searchText: {
get() {
return this.search;
@@ -35,15 +60,55 @@ export default {
this.setSearch(value);
},
},
+ currentFocusedOption() {
+ return this.searchOptions[this.currentFocusIndex];
+ },
+ currentFocusedId() {
+ return this.currentFocusedOption?.html_id;
+ },
+ isLoggedIn() {
+ return gon?.current_username;
+ },
showSearchDropdown() {
- return this.showDropdown && gon?.current_username;
+ return this.showDropdown && this.isLoggedIn;
},
showDefaultItems() {
return !this.searchText;
},
+ defaultIndex() {
+ if (this.showDefaultItems) {
+ return SEARCH_BOX_INDEX;
+ }
+
+ return FIRST_DROPDOWN_INDEX;
+ },
+ searchInputDescribeBy() {
+ if (this.isLoggedIn) {
+ return this.$options.i18n.searchInputDescribeByWithDropdown;
+ }
+
+ return this.$options.i18n.searchInputDescribeByNoDropdown;
+ },
+ dropdownResultsDescription() {
+ if (!this.showSearchDropdown) {
+ return ''; // This allows aria-live to see register an update when the dropdown is shown
+ }
+
+ if (this.showDefaultItems) {
+ return sprintf(this.$options.i18n.searchDescribedByDefault, {
+ count: this.searchOptions.length,
+ });
+ }
+
+ return this.loading
+ ? this.$options.i18n.searchResultsLoading
+ : sprintf(this.$options.i18n.searchDescribedByUpdated, {
+ count: this.searchOptions.length,
+ });
+ },
},
methods: {
- ...mapActions(['setSearch', 'fetchAutocompleteOptions']),
+ ...mapActions(['setSearch', 'fetchAutocompleteOptions', 'clearAutocomplete']),
openDropdown() {
this.showDropdown = true;
},
@@ -51,44 +116,77 @@ export default {
this.showDropdown = false;
},
submitSearch() {
- return visitUrl(this.searchQuery);
+ return visitUrl(this.currentFocusedOption?.url || this.searchQuery);
},
- getAutocompleteOptions(searchTerm) {
+ getAutocompleteOptions: debounce(function debouncedSearch(searchTerm) {
if (!searchTerm) {
- return;
+ this.clearAutocomplete();
+ } else {
+ this.fetchAutocompleteOptions();
}
-
- this.fetchAutocompleteOptions();
- },
+ }, DEFAULT_DEBOUNCE_AND_THROTTLE_MS),
},
+ SEARCH_BOX_INDEX,
+ SEARCH_INPUT_DESCRIPTION,
+ SEARCH_RESULTS_DESCRIPTION,
};
</script>
<template>
- <section v-outside="closeDropdown" class="header-search gl-relative">
+ <form
+ v-outside="closeDropdown"
+ role="search"
+ :aria-label="$options.i18n.searchAria"
+ class="header-search gl-relative"
+ >
<gl-search-box-by-type
+ id="search"
v-model="searchText"
- :debounce="500"
+ role="searchbox"
+ class="gl-z-index-1"
autocomplete="off"
:placeholder="$options.i18n.searchPlaceholder"
+ :aria-activedescendant="currentFocusedId"
+ :aria-describedby="$options.SEARCH_INPUT_DESCRIPTION"
@focus="openDropdown"
@click="openDropdown"
@input="getAutocompleteOptions"
- @keydown.enter="submitSearch"
- @keydown.esc="closeDropdown"
+ @keydown.enter.stop.prevent="submitSearch"
/>
+ <span :id="$options.SEARCH_INPUT_DESCRIPTION" role="region" class="gl-sr-only">{{
+ searchInputDescribeBy
+ }}</span>
+ <span
+ role="region"
+ :data-testid="$options.SEARCH_RESULTS_DESCRIPTION"
+ class="gl-sr-only"
+ aria-live="polite"
+ aria-atomic="true"
+ >
+ {{ dropdownResultsDescription }}
+ </span>
<div
v-if="showSearchDropdown"
data-testid="header-search-dropdown-menu"
class="header-search-dropdown-menu gl-absolute gl-w-full gl-bg-white gl-border-1 gl-rounded-base gl-border-solid gl-border-gray-200 gl-shadow-x0-y2-b4-s0"
>
<div class="header-search-dropdown-content gl-overflow-y-auto gl-py-2">
- <header-search-default-items v-if="showDefaultItems" />
+ <dropdown-keyboard-navigation
+ v-model="currentFocusIndex"
+ :max="searchOptions.length - 1"
+ :min="$options.SEARCH_BOX_INDEX"
+ :default-index="defaultIndex"
+ @tab="closeDropdown"
+ />
+ <header-search-default-items
+ v-if="showDefaultItems"
+ :current-focused-option="currentFocusedOption"
+ />
<template v-else>
- <header-search-scoped-items />
- <header-search-autocomplete-items />
+ <header-search-scoped-items :current-focused-option="currentFocusedOption" />
+ <header-search-autocomplete-items :current-focused-option="currentFocusedOption" />
</template>
</div>
</div>
- </section>
+ </form>
</template>
diff --git a/app/assets/javascripts/header_search/components/header_search_autocomplete_items.vue b/app/assets/javascripts/header_search/components/header_search_autocomplete_items.vue
index 9bea2b280f7..9f4f4768247 100644
--- a/app/assets/javascripts/header_search/components/header_search_autocomplete_items.vue
+++ b/app/assets/javascripts/header_search/components/header_search_autocomplete_items.vue
@@ -23,10 +23,26 @@ export default {
directives: {
SafeHtml,
},
+ props: {
+ currentFocusedOption: {
+ type: Object,
+ required: false,
+ default: () => null,
+ },
+ },
computed: {
...mapState(['search', 'loading']),
...mapGetters(['autocompleteGroupedSearchOptions']),
},
+ watch: {
+ currentFocusedOption() {
+ const focusedElement = this.$refs[this.currentFocusedOption?.html_id]?.[0]?.$el;
+
+ if (focusedElement) {
+ focusedElement.scrollIntoView(false);
+ }
+ },
+ },
methods: {
highlightedName(val) {
return highlight(val, this.search);
@@ -38,6 +54,9 @@ export default {
return SMALL_AVATAR_PX;
},
+ isOptionFocused(data) {
+ return this.currentFocusedOption?.html_id === data.html_id;
+ },
},
};
</script>
@@ -49,13 +68,17 @@ export default {
<gl-dropdown-divider />
<gl-dropdown-section-header>{{ option.category }}</gl-dropdown-section-header>
<gl-dropdown-item
- v-for="(data, index) in option.data"
- :id="`autocomplete-${option.category}-${index}`"
- :key="index"
+ v-for="data in option.data"
+ :id="data.html_id"
+ :ref="data.html_id"
+ :key="data.html_id"
+ :class="{ 'gl-bg-gray-50': isOptionFocused(data) }"
+ :aria-selected="isOptionFocused(data)"
+ :aria-label="data.label"
tabindex="-1"
:href="data.url"
>
- <div class="gl-display-flex gl-align-items-center">
+ <div class="gl-display-flex gl-align-items-center" aria-hidden="true">
<gl-avatar
v-if="data.avatar_url !== undefined"
:src="data.avatar_url"
diff --git a/app/assets/javascripts/header_search/components/header_search_default_items.vue b/app/assets/javascripts/header_search/components/header_search_default_items.vue
index 2871937ed3a..53e63bc6cca 100644
--- a/app/assets/javascripts/header_search/components/header_search_default_items.vue
+++ b/app/assets/javascripts/header_search/components/header_search_default_items.vue
@@ -12,6 +12,13 @@ export default {
GlDropdownSectionHeader,
GlDropdownItem,
},
+ props: {
+ currentFocusedOption: {
+ type: Object,
+ required: false,
+ default: () => null,
+ },
+ },
computed: {
...mapState(['searchContext']),
...mapGetters(['defaultSearchOptions']),
@@ -23,6 +30,11 @@ export default {
);
},
},
+ methods: {
+ isOptionFocused(option) {
+ return this.currentFocusedOption?.html_id === option.html_id;
+ },
+ },
};
</script>
@@ -30,13 +42,17 @@ export default {
<div>
<gl-dropdown-section-header>{{ sectionHeader }}</gl-dropdown-section-header>
<gl-dropdown-item
- v-for="(option, index) in defaultSearchOptions"
- :id="`default-${index}`"
- :key="index"
+ v-for="option in defaultSearchOptions"
+ :id="option.html_id"
+ :ref="option.html_id"
+ :key="option.html_id"
+ :class="{ 'gl-bg-gray-50': isOptionFocused(option) }"
+ :aria-selected="isOptionFocused(option)"
+ :aria-label="option.title"
tabindex="-1"
:href="option.url"
>
- {{ option.title }}
+ <span aria-hidden="true">{{ option.title }}</span>
</gl-dropdown-item>
</div>
</template>
diff --git a/app/assets/javascripts/header_search/components/header_search_scoped_items.vue b/app/assets/javascripts/header_search/components/header_search_scoped_items.vue
index 645eba05148..3aebee71509 100644
--- a/app/assets/javascripts/header_search/components/header_search_scoped_items.vue
+++ b/app/assets/javascripts/header_search/components/header_search_scoped_items.vue
@@ -1,31 +1,57 @@
<script>
import { GlDropdownItem } from '@gitlab/ui';
import { mapState, mapGetters } from 'vuex';
+import { __, sprintf } from '~/locale';
export default {
name: 'HeaderSearchScopedItems',
components: {
GlDropdownItem,
},
+ props: {
+ currentFocusedOption: {
+ type: Object,
+ required: false,
+ default: () => null,
+ },
+ },
computed: {
...mapState(['search']),
...mapGetters(['scopedSearchOptions']),
},
+ methods: {
+ isOptionFocused(option) {
+ return this.currentFocusedOption?.html_id === option.html_id;
+ },
+ ariaLabel(option) {
+ return sprintf(__('%{search} %{description} %{scope}'), {
+ search: this.search,
+ description: option.description,
+ scope: option.scope || '',
+ });
+ },
+ },
};
</script>
<template>
<div>
<gl-dropdown-item
- v-for="(option, index) in scopedSearchOptions"
- :id="`scoped-${index}`"
- :key="index"
+ v-for="option in scopedSearchOptions"
+ :id="option.html_id"
+ :ref="option.html_id"
+ :key="option.html_id"
+ :class="{ 'gl-bg-gray-50': isOptionFocused(option) }"
+ :aria-selected="isOptionFocused(option)"
+ :aria-label="ariaLabel(option)"
tabindex="-1"
:href="option.url"
>
- "<span class="gl-font-weight-bold">{{ search }}</span
- >" {{ option.description }}
- <span v-if="option.scope" class="gl-font-style-italic">{{ option.scope }}</span>
+ <span aria-hidden="true">
+ "<span class="gl-font-weight-bold">{{ search }}</span
+ >" {{ option.description }}
+ <span v-if="option.scope" class="gl-font-style-italic">{{ option.scope }}</span>
+ </span>
</gl-dropdown-item>
</div>
</template>
diff --git a/app/assets/javascripts/header_search/constants.js b/app/assets/javascripts/header_search/constants.js
index 2fadb1bd1ee..b2e45fcd648 100644
--- a/app/assets/javascripts/header_search/constants.js
+++ b/app/assets/javascripts/header_search/constants.js
@@ -1,20 +1,20 @@
-import { __ } from '~/locale';
+import { s__ } from '~/locale';
-export const MSG_ISSUES_ASSIGNED_TO_ME = __('Issues assigned to me');
+export const MSG_ISSUES_ASSIGNED_TO_ME = s__('GlobalSearch|Issues assigned to me');
-export const MSG_ISSUES_IVE_CREATED = __("Issues I've created");
+export const MSG_ISSUES_IVE_CREATED = s__("GlobalSearch|Issues I've created");
-export const MSG_MR_ASSIGNED_TO_ME = __('Merge requests assigned to me');
+export const MSG_MR_ASSIGNED_TO_ME = s__('GlobalSearch|Merge requests assigned to me');
-export const MSG_MR_IM_REVIEWER = __("Merge requests that I'm a reviewer");
+export const MSG_MR_IM_REVIEWER = s__("GlobalSearch|Merge requests that I'm a reviewer");
-export const MSG_MR_IVE_CREATED = __("Merge requests I've created");
+export const MSG_MR_IVE_CREATED = s__("GlobalSearch|Merge requests I've created");
-export const MSG_IN_ALL_GITLAB = __('in all GitLab');
+export const MSG_IN_ALL_GITLAB = s__('GlobalSearch|in all GitLab');
-export const MSG_IN_GROUP = __('in group');
+export const MSG_IN_GROUP = s__('GlobalSearch|in group');
-export const MSG_IN_PROJECT = __('in project');
+export const MSG_IN_PROJECT = s__('GlobalSearch|in project');
export const GROUPS_CATEGORY = 'Groups';
@@ -23,3 +23,11 @@ export const PROJECTS_CATEGORY = 'Projects';
export const LARGE_AVATAR_PX = 32;
export const SMALL_AVATAR_PX = 16;
+
+export const FIRST_DROPDOWN_INDEX = 0;
+
+export const SEARCH_BOX_INDEX = -1;
+
+export const SEARCH_INPUT_DESCRIPTION = 'search-input-description';
+
+export const SEARCH_RESULTS_DESCRIPTION = 'search-results-description';
diff --git a/app/assets/javascripts/header_search/store/actions.js b/app/assets/javascripts/header_search/store/actions.js
index 2c3b1bd4c0f..0ba956f3ed1 100644
--- a/app/assets/javascripts/header_search/store/actions.js
+++ b/app/assets/javascripts/header_search/store/actions.js
@@ -14,6 +14,10 @@ export const fetchAutocompleteOptions = ({ commit, getters }) => {
});
};
+export const clearAutocomplete = ({ commit }) => {
+ commit(types.CLEAR_AUTOCOMPLETE);
+};
+
export const setSearch = ({ commit }, value) => {
commit(types.SET_SEARCH, value);
};
diff --git a/app/assets/javascripts/header_search/store/getters.js b/app/assets/javascripts/header_search/store/getters.js
index 3f4e231ca55..a1348a8aa3f 100644
--- a/app/assets/javascripts/header_search/store/getters.js
+++ b/app/assets/javascripts/header_search/store/getters.js
@@ -1,3 +1,4 @@
+import { omitBy, isNil } from 'lodash';
import { objectToQuery } from '~/lib/utils/url_utility';
import {
@@ -12,23 +13,29 @@ import {
} from '../constants';
export const searchQuery = (state) => {
- const query = {
- search: state.search,
- nav_source: 'navbar',
- project_id: state.searchContext.project?.id,
- group_id: state.searchContext.group?.id,
- scope: state.searchContext.scope,
- };
+ const query = omitBy(
+ {
+ search: state.search,
+ nav_source: 'navbar',
+ project_id: state.searchContext.project?.id,
+ group_id: state.searchContext.group?.id,
+ scope: state.searchContext?.scope,
+ },
+ isNil,
+ );
return `${state.searchPath}?${objectToQuery(query)}`;
};
export const autocompleteQuery = (state) => {
- const query = {
- term: state.search,
- project_id: state.searchContext.project?.id,
- project_ref: state.searchContext.ref,
- };
+ const query = omitBy(
+ {
+ term: state.search,
+ project_id: state.searchContext.project?.id,
+ project_ref: state.searchContext?.ref,
+ },
+ isNil,
+ );
return `${state.autocompletePath}?${objectToQuery(query)}`;
};
@@ -54,22 +61,27 @@ export const defaultSearchOptions = (state, getters) => {
return [
{
+ html_id: 'default-issues-assigned',
title: MSG_ISSUES_ASSIGNED_TO_ME,
url: `${getters.scopedIssuesPath}/?assignee_username=${userName}`,
},
{
+ html_id: 'default-issues-created',
title: MSG_ISSUES_IVE_CREATED,
url: `${getters.scopedIssuesPath}/?author_username=${userName}`,
},
{
+ html_id: 'default-mrs-assigned',
title: MSG_MR_ASSIGNED_TO_ME,
url: `${getters.scopedMRPath}/?assignee_username=${userName}`,
},
{
+ html_id: 'default-mrs-reviewer',
title: MSG_MR_IM_REVIEWER,
url: `${getters.scopedMRPath}/?reviewer_username=${userName}`,
},
{
+ html_id: 'default-mrs-created',
title: MSG_MR_IVE_CREATED,
url: `${getters.scopedMRPath}/?author_username=${userName}`,
},
@@ -77,42 +89,43 @@ export const defaultSearchOptions = (state, getters) => {
};
export const projectUrl = (state) => {
- if (!state.searchContext.project || !state.searchContext.group) {
- return null;
- }
-
- const query = {
- search: state.search,
- nav_source: 'navbar',
- project_id: state.searchContext.project.id,
- group_id: state.searchContext.group.id,
- scope: state.searchContext.scope,
- };
+ const query = omitBy(
+ {
+ search: state.search,
+ nav_source: 'navbar',
+ project_id: state.searchContext?.project?.id,
+ group_id: state.searchContext?.group?.id,
+ scope: state.searchContext?.scope,
+ },
+ isNil,
+ );
return `${state.searchPath}?${objectToQuery(query)}`;
};
export const groupUrl = (state) => {
- if (!state.searchContext.group) {
- return null;
- }
-
- const query = {
- search: state.search,
- nav_source: 'navbar',
- group_id: state.searchContext.group.id,
- scope: state.searchContext.scope,
- };
+ const query = omitBy(
+ {
+ search: state.search,
+ nav_source: 'navbar',
+ group_id: state.searchContext?.group?.id,
+ scope: state.searchContext?.scope,
+ },
+ isNil,
+ );
return `${state.searchPath}?${objectToQuery(query)}`;
};
export const allUrl = (state) => {
- const query = {
- search: state.search,
- nav_source: 'navbar',
- scope: state.searchContext.scope,
- };
+ const query = omitBy(
+ {
+ search: state.search,
+ nav_source: 'navbar',
+ scope: state.searchContext?.scope,
+ },
+ isNil,
+ );
return `${state.searchPath}?${objectToQuery(query)}`;
};
@@ -122,6 +135,7 @@ export const scopedSearchOptions = (state, getters) => {
if (state.searchContext.project) {
options.push({
+ html_id: 'scoped-in-project',
scope: state.searchContext.project.name,
description: MSG_IN_PROJECT,
url: getters.projectUrl,
@@ -130,6 +144,7 @@ export const scopedSearchOptions = (state, getters) => {
if (state.searchContext.group) {
options.push({
+ html_id: 'scoped-in-group',
scope: state.searchContext.group.name,
description: MSG_IN_GROUP,
url: getters.groupUrl,
@@ -137,6 +152,7 @@ export const scopedSearchOptions = (state, getters) => {
}
options.push({
+ html_id: 'scoped-in-all',
description: MSG_IN_ALL_GITLAB,
url: getters.allUrl,
});
@@ -165,3 +181,18 @@ export const autocompleteGroupedSearchOptions = (state) => {
return results;
};
+
+export const searchOptions = (state, getters) => {
+ if (!state.search) {
+ return getters.defaultSearchOptions;
+ }
+
+ const sortedAutocompleteOptions = Object.values(getters.autocompleteGroupedSearchOptions).reduce(
+ (options, group) => {
+ return [...options, ...group.data];
+ },
+ [],
+ );
+
+ return getters.scopedSearchOptions.concat(sortedAutocompleteOptions);
+};
diff --git a/app/assets/javascripts/header_search/store/mutation_types.js b/app/assets/javascripts/header_search/store/mutation_types.js
index a2358621ce6..6e65345757f 100644
--- a/app/assets/javascripts/header_search/store/mutation_types.js
+++ b/app/assets/javascripts/header_search/store/mutation_types.js
@@ -1,5 +1,6 @@
export const REQUEST_AUTOCOMPLETE = 'REQUEST_AUTOCOMPLETE';
export const RECEIVE_AUTOCOMPLETE_SUCCESS = 'RECEIVE_AUTOCOMPLETE_SUCCESS';
export const RECEIVE_AUTOCOMPLETE_ERROR = 'RECEIVE_AUTOCOMPLETE_ERROR';
+export const CLEAR_AUTOCOMPLETE = 'CLEAR_AUTOCOMPLETE';
export const SET_SEARCH = 'SET_SEARCH';
diff --git a/app/assets/javascripts/header_search/store/mutations.js b/app/assets/javascripts/header_search/store/mutations.js
index 175b5406540..26b4a8854fe 100644
--- a/app/assets/javascripts/header_search/store/mutations.js
+++ b/app/assets/javascripts/header_search/store/mutations.js
@@ -7,12 +7,17 @@ export default {
},
[types.RECEIVE_AUTOCOMPLETE_SUCCESS](state, data) {
state.loading = false;
- state.autocompleteOptions = data;
+ state.autocompleteOptions = data.map((d, i) => {
+ return { html_id: `autocomplete-${d.category}-${i}`, ...d };
+ });
},
[types.RECEIVE_AUTOCOMPLETE_ERROR](state) {
state.loading = false;
state.autocompleteOptions = [];
},
+ [types.CLEAR_AUTOCOMPLETE](state) {
+ state.autocompleteOptions = [];
+ },
[types.SET_SEARCH](state, value) {
state.search = value;
},
diff --git a/app/assets/javascripts/ide/components/activity_bar.vue b/app/assets/javascripts/ide/components/activity_bar.vue
index c71d911adfb..846b4d92724 100644
--- a/app/assets/javascripts/ide/components/activity_bar.vue
+++ b/app/assets/javascripts/ide/components/activity_bar.vue
@@ -63,7 +63,7 @@ export default {
class="ide-sidebar-link js-ide-review-mode"
@click.prevent="changedActivityView($event, $options.leftSidebarViews.review.name)"
>
- <gl-icon name="file-modified" />
+ <gl-icon name="review-list" />
</button>
</li>
<li>
diff --git a/app/assets/javascripts/ide/components/ide_tree_list.vue b/app/assets/javascripts/ide/components/ide_tree_list.vue
index b987adc8bae..0fc7337ad26 100644
--- a/app/assets/javascripts/ide/components/ide_tree_list.vue
+++ b/app/assets/javascripts/ide/components/ide_tree_list.vue
@@ -29,14 +29,20 @@ export default {
},
},
watch: {
- showLoading(newVal) {
- if (!newVal) {
- this.$emit('tree-ready');
- }
+ showLoading() {
+ this.notifyTreeReady();
},
},
+ mounted() {
+ this.notifyTreeReady();
+ },
methods: {
...mapActions(['toggleTreeOpen']),
+ notifyTreeReady() {
+ if (!this.showLoading) {
+ this.$emit('tree-ready');
+ }
+ },
clickedFile() {
performanceMarkAndMeasure({ mark: WEBIDE_MARK_FILE_CLICKED });
},
diff --git a/app/assets/javascripts/ide/components/new_dropdown/index.vue b/app/assets/javascripts/ide/components/new_dropdown/index.vue
index bdd201aac1b..87b60eca73c 100644
--- a/app/assets/javascripts/ide/components/new_dropdown/index.vue
+++ b/app/assets/javascripts/ide/components/new_dropdown/index.vue
@@ -67,7 +67,7 @@ export default {
data-qa-selector="dropdown_button"
@click.stop="openDropdown()"
>
- <gl-icon name="ellipsis_v" /> <gl-icon name="chevron-down" />
+ <gl-icon name="ellipsis_v" />
</button>
<ul ref="dropdownMenu" class="dropdown-menu dropdown-menu-right">
<template v-if="type === 'tree'">
diff --git a/app/assets/javascripts/ide/components/pipelines/empty_state.vue b/app/assets/javascripts/ide/components/pipelines/empty_state.vue
new file mode 100644
index 00000000000..194deb2ece0
--- /dev/null
+++ b/app/assets/javascripts/ide/components/pipelines/empty_state.vue
@@ -0,0 +1,35 @@
+<script>
+import { GlEmptyState } from '@gitlab/ui';
+import { mapState } from 'vuex';
+import { s__ } from '~/locale';
+import { helpPagePath } from '~/helpers/help_page_helper';
+
+export default {
+ components: {
+ GlEmptyState,
+ },
+ computed: {
+ ...mapState(['pipelinesEmptyStateSvgPath']),
+ ciHelpPagePath() {
+ return helpPagePath('ci/quick_start/index.md');
+ },
+ },
+ i18n: {
+ title: s__('Pipelines|Build with confidence'),
+ description: s__(`Pipelines|GitLab CI/CD can automatically build,
+ test, and deploy your code. Let GitLab take care of time
+ consuming tasks, so you can spend more time creating.`),
+ primaryButtonText: s__('Pipelines|Get started with GitLab CI/CD'),
+ },
+};
+</script>
+
+<template>
+ <gl-empty-state
+ :title="$options.i18n.title"
+ :svg-path="pipelinesEmptyStateSvgPath"
+ :description="$options.i18n.description"
+ :primary-button-text="$options.i18n.primaryButtonText"
+ :primary-button-link="ciHelpPagePath"
+ />
+</template>
diff --git a/app/assets/javascripts/ide/components/pipelines/list.vue b/app/assets/javascripts/ide/components/pipelines/list.vue
index e1caf1ba44a..7f513afe82e 100644
--- a/app/assets/javascripts/ide/components/pipelines/list.vue
+++ b/app/assets/javascripts/ide/components/pipelines/list.vue
@@ -11,10 +11,17 @@ import {
import { escape } from 'lodash';
import { mapActions, mapGetters, mapState } from 'vuex';
import IDEServices from '~/ide/services';
-import { sprintf, __ } from '../../../locale';
-import EmptyState from '../../../pipelines/components/pipelines_list/empty_state.vue';
-import CiIcon from '../../../vue_shared/components/ci_icon.vue';
+import { sprintf, __ } from '~/locale';
+import CiIcon from '~/vue_shared/components/ci_icon.vue';
import JobsList from '../jobs/list.vue';
+import EmptyState from './empty_state.vue';
+
+const CLASSES_FLEX_VERTICAL_CENTER = [
+ 'gl-h-full',
+ 'gl-display-flex',
+ 'gl-flex-direction-column',
+ 'gl-justify-content-center',
+];
export default {
components: {
@@ -32,7 +39,6 @@ export default {
SafeHtml,
},
computed: {
- ...mapState(['pipelinesEmptyStateSvgPath']),
...mapGetters(['currentProject']),
...mapGetters('pipelines', ['jobsCount', 'failedJobsCount', 'failedStages', 'pipelineFailed']),
...mapState('pipelines', [
@@ -63,12 +69,15 @@ export default {
methods: {
...mapActions('pipelines', ['fetchLatestPipeline']),
},
+ CLASSES_FLEX_VERTICAL_CENTER,
};
</script>
<template>
<div class="ide-pipeline">
- <gl-loading-icon v-if="showLoadingIcon" size="lg" class="gl-mt-3" />
+ <div v-if="showLoadingIcon" :class="$options.CLASSES_FLEX_VERTICAL_CENTER">
+ <gl-loading-icon size="lg" />
+ </div>
<template v-else-if="hasLoadedPipeline">
<header v-if="latestPipeline" class="ide-tree-header ide-pipeline-header">
<ci-icon :status="latestPipeline.details.status" :size="24" class="d-flex" />
@@ -83,12 +92,9 @@ export default {
</a>
</span>
</header>
- <empty-state
- v-if="!latestPipeline"
- :empty-state-svg-path="pipelinesEmptyStateSvgPath"
- :can-set-ci="true"
- class="gl-p-5"
- />
+ <div v-if="!latestPipeline" :class="$options.CLASSES_FLEX_VERTICAL_CENTER">
+ <empty-state />
+ </div>
<gl-alert
v-else-if="latestPipeline.yamlError"
variant="danger"
diff --git a/app/assets/javascripts/ide/components/repo_editor.vue b/app/assets/javascripts/ide/components/repo_editor.vue
index 2bf99550bf2..05493db1dff 100644
--- a/app/assets/javascripts/ide/components/repo_editor.vue
+++ b/app/assets/javascripts/ide/components/repo_editor.vue
@@ -7,6 +7,7 @@ import {
EDITOR_CODE_INSTANCE_FN,
EDITOR_DIFF_INSTANCE_FN,
} from '~/editor/constants';
+import { SourceEditorExtension } from '~/editor/extensions/source_editor_extension_base';
import { EditorWebIdeExtension } from '~/editor/extensions/source_editor_webide_ext';
import SourceEditor from '~/editor/source_editor';
import createFlash from '~/flash';
@@ -302,30 +303,32 @@ export default {
...instanceOptions,
...this.editorOptions,
});
-
- this.editor.use(
- new EditorWebIdeExtension({
- instance: this.editor,
- modelManager: this.modelManager,
- store: this.$store,
- file: this.file,
- options: this.editorOptions,
- }),
- );
+ this.editor.use([
+ {
+ definition: SourceEditorExtension,
+ },
+ {
+ definition: EditorWebIdeExtension,
+ setupOptions: {
+ modelManager: this.modelManager,
+ store: this.$store,
+ file: this.file,
+ options: this.editorOptions,
+ },
+ },
+ ]);
if (
this.fileType === MARKDOWN_FILE_TYPE &&
this.editor?.getEditorType() === EDITOR_TYPE_CODE &&
this.previewMarkdownPath
) {
- import('~/editor/extensions/source_editor_markdown_ext')
- .then(({ EditorMarkdownExtension: MarkdownExtension } = {}) => {
- this.editor.use(
- new MarkdownExtension({
- instance: this.editor,
- previewMarkdownPath: this.previewMarkdownPath,
- }),
- );
+ import('~/editor/extensions/source_editor_markdown_livepreview_ext')
+ .then(({ EditorMarkdownPreviewExtension: MarkdownLivePreview }) => {
+ this.editor.use({
+ definition: MarkdownLivePreview,
+ setupOptions: { previewMarkdownPath: this.previewMarkdownPath },
+ });
})
.catch((e) =>
createFlash({
diff --git a/app/assets/javascripts/ide/constants.js b/app/assets/javascripts/ide/constants.js
index 706d98fdb90..775b6906498 100644
--- a/app/assets/javascripts/ide/constants.js
+++ b/app/assets/javascripts/ide/constants.js
@@ -76,15 +76,15 @@ export const stageKeys = {
export const commitItemIconMap = {
addition: {
icon: 'file-addition',
- class: 'ide-file-addition',
+ class: 'file-addition ide-file-addition',
},
modified: {
icon: 'file-modified',
- class: 'ide-file-modified',
+ class: 'file-modified ide-file-modified',
},
deleted: {
icon: 'file-deletion',
- class: 'ide-file-deletion',
+ class: 'file-deletion ide-file-deletion',
},
};
diff --git a/app/assets/javascripts/ide/ide_router.js b/app/assets/javascripts/ide/ide_router.js
index 27cedd80347..1fc447886bb 100644
--- a/app/assets/javascripts/ide/ide_router.js
+++ b/app/assets/javascripts/ide/ide_router.js
@@ -1,8 +1,6 @@
import Vue from 'vue';
-import createFlash from '~/flash';
import IdeRouter from '~/ide/ide_router_extension';
import { joinPaths } from '~/lib/utils/url_utility';
-import { __ } from '~/locale';
import {
WEBIDE_MARK_FETCH_PROJECT_DATA_START,
WEBIDE_MARK_FETCH_PROJECT_DATA_FINISH,
@@ -75,49 +73,34 @@ export const createRouter = (store, defaultBranch) => {
router.beforeEach((to, from, next) => {
if (to.params.namespace && to.params.project) {
- performanceMarkAndMeasure({ mark: WEBIDE_MARK_FETCH_PROJECT_DATA_START });
- store
- .dispatch('getProjectData', {
- namespace: to.params.namespace,
- projectId: to.params.project,
- })
- .then(() => {
- const basePath = to.params.pathMatch || '';
- const projectId = `${to.params.namespace}/${to.params.project}`;
- const branchId = to.params.branchid;
- const mergeRequestId = to.params.mrid;
+ const basePath = to.params.pathMatch || '';
+ const projectId = `${to.params.namespace}/${to.params.project}`;
+ const branchId = to.params.branchid;
+ const mergeRequestId = to.params.mrid;
- if (branchId) {
- performanceMarkAndMeasure({
- mark: WEBIDE_MARK_FETCH_PROJECT_DATA_FINISH,
- measures: [
- {
- name: WEBIDE_MEASURE_FETCH_PROJECT_DATA,
- start: WEBIDE_MARK_FETCH_PROJECT_DATA_START,
- },
- ],
- });
- store.dispatch('openBranch', {
- projectId,
- branchId,
- basePath,
- });
- } else if (mergeRequestId) {
- store.dispatch('openMergeRequest', {
- projectId,
- mergeRequestId,
- targetProjectId: to.query.target_project,
- });
- }
- })
- .catch((e) => {
- createFlash({
- message: __('Error while loading the project data. Please try again.'),
- fadeTransition: false,
- addBodyClass: true,
- });
- throw e;
+ performanceMarkAndMeasure({ mark: WEBIDE_MARK_FETCH_PROJECT_DATA_START });
+ if (branchId) {
+ performanceMarkAndMeasure({
+ mark: WEBIDE_MARK_FETCH_PROJECT_DATA_FINISH,
+ measures: [
+ {
+ name: WEBIDE_MEASURE_FETCH_PROJECT_DATA,
+ start: WEBIDE_MARK_FETCH_PROJECT_DATA_START,
+ },
+ ],
+ });
+ store.dispatch('openBranch', {
+ projectId,
+ branchId,
+ basePath,
+ });
+ } else if (mergeRequestId) {
+ store.dispatch('openMergeRequest', {
+ projectId,
+ mergeRequestId,
+ targetProjectId: to.query.target_project,
});
+ }
}
next();
diff --git a/app/assets/javascripts/ide/index.js b/app/assets/javascripts/ide/index.js
index bdffed70882..df643675357 100644
--- a/app/assets/javascripts/ide/index.js
+++ b/app/assets/javascripts/ide/index.js
@@ -34,11 +34,18 @@ Vue.use(PerformancePlugin, {
* @param {extendStoreCallback} options.extendStore -
* Function that receives the default store and returns an extended one.
*/
-export function initIde(el, options = {}) {
+export const initIde = (el, options = {}) => {
if (!el) return null;
const { rootComponent = ide, extendStore = identity } = options;
+
const store = createStore();
+ const project = JSON.parse(el.dataset.project);
+ store.dispatch('setProject', { project });
+
+ // fire and forget fetching non-critical project info
+ store.dispatch('fetchProjectPermissions');
+
const router = createRouter(store, el.dataset.defaultBranch || DEFAULT_BRANCH);
return new Vue({
@@ -77,7 +84,7 @@ export function initIde(el, options = {}) {
return createElement(rootComponent);
},
});
-}
+};
/**
* Start the IDE.
diff --git a/app/assets/javascripts/ide/lib/themes/monokai.js b/app/assets/javascripts/ide/lib/themes/monokai.js
index d7636574754..36fa5039be7 100644
--- a/app/assets/javascripts/ide/lib/themes/monokai.js
+++ b/app/assets/javascripts/ide/lib/themes/monokai.js
@@ -162,8 +162,8 @@ export default {
'editor.selectionBackground': '#49483E',
'editor.lineHighlightBackground': '#3E3D32',
'editorCursor.foreground': '#F8F8F0',
- 'editorWhitespace.foreground': '#3B3A32',
'editorIndentGuide.activeBackground': '#9D550FB0',
'editor.selectionHighlightBorder': '#222218',
+ 'editorWhitespace.foreground': '#75715e',
},
};
diff --git a/app/assets/javascripts/ide/lib/themes/none.js b/app/assets/javascripts/ide/lib/themes/none.js
index 8e722c4ff88..0842bc04cff 100644
--- a/app/assets/javascripts/ide/lib/themes/none.js
+++ b/app/assets/javascripts/ide/lib/themes/none.js
@@ -13,5 +13,6 @@ export default {
'diffEditor.insertedTextBackground': '#a0f5b420',
'diffEditor.removedTextBackground': '#f9d7dc20',
'editorIndentGuide.activeBackground': '#cccccc',
+ 'editorSuggestWidget.focusHighlightForeground': '#96D8FD',
},
};
diff --git a/app/assets/javascripts/ide/lib/themes/solarized_dark.js b/app/assets/javascripts/ide/lib/themes/solarized_dark.js
index 3c9414b9dc9..8ae609285ac 100644
--- a/app/assets/javascripts/ide/lib/themes/solarized_dark.js
+++ b/app/assets/javascripts/ide/lib/themes/solarized_dark.js
@@ -1105,6 +1105,6 @@ export default {
'editor.selectionBackground': '#073642',
'editor.lineHighlightBackground': '#073642',
'editorCursor.foreground': '#819090',
- 'editorWhitespace.foreground': '#073642',
+ 'editorWhitespace.foreground': '#586e75',
},
};
diff --git a/app/assets/javascripts/ide/lib/themes/solarized_light.js b/app/assets/javascripts/ide/lib/themes/solarized_light.js
index b7bfcf33b0f..2c9f3d904f1 100644
--- a/app/assets/javascripts/ide/lib/themes/solarized_light.js
+++ b/app/assets/javascripts/ide/lib/themes/solarized_light.js
@@ -1096,6 +1096,6 @@ export default {
'editor.selectionBackground': '#EEE8D5',
'editor.lineHighlightBackground': '#EEE8D5',
'editorCursor.foreground': '#000000',
- 'editorWhitespace.foreground': '#EAE3C9',
+ 'editorWhitespace.foreground': '#93a1a1',
},
};
diff --git a/app/assets/javascripts/ide/lib/themes/white.js b/app/assets/javascripts/ide/lib/themes/white.js
index f06458d8a16..69c63c82021 100644
--- a/app/assets/javascripts/ide/lib/themes/white.js
+++ b/app/assets/javascripts/ide/lib/themes/white.js
@@ -142,5 +142,6 @@ export default {
'diffEditor.insertedTextBackground': '#a0f5b420',
'diffEditor.removedTextBackground': '#f9d7dc20',
'editorIndentGuide.activeBackground': '#cccccc',
+ 'editorSuggestWidget.focusHighlightForeground': '#96D8FD',
},
};
diff --git a/app/assets/javascripts/ide/queries/ide_project.fragment.graphql b/app/assets/javascripts/ide/queries/ide_project.fragment.graphql
index c107f2376f9..a0b520858e6 100644
--- a/app/assets/javascripts/ide/queries/ide_project.fragment.graphql
+++ b/app/assets/javascripts/ide/queries/ide_project.fragment.graphql
@@ -1,4 +1,5 @@
fragment IdeProject on Project {
+ id
userPermissions {
createMergeRequestIn
readMergeRequest
diff --git a/app/assets/javascripts/ide/services/index.js b/app/assets/javascripts/ide/services/index.js
index ef4f47f226a..805476c71bc 100644
--- a/app/assets/javascripts/ide/services/index.js
+++ b/app/assets/javascripts/ide/services/index.js
@@ -1,19 +1,12 @@
-import getIdeProject from 'ee_else_ce/ide/queries/get_ide_project.query.graphql';
import Api from '~/api';
+import getIdeProject from 'ee_else_ce/ide/queries/get_ide_project.query.graphql';
import dismissUserCallout from '~/graphql_shared/mutations/dismiss_user_callout.mutation.graphql';
+import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import axios from '~/lib/utils/axios_utils';
import { joinPaths, escapeFileUrl } from '~/lib/utils/url_utility';
-import ciConfig from '~/pipeline_editor/graphql/queries/ci_config.graphql';
+import ciConfig from '~/pipeline_editor/graphql/queries/ci_config.query.graphql';
import { query, mutate } from './gql';
-const fetchApiProjectData = (projectPath) => Api.project(projectPath).then(({ data }) => data);
-
-const fetchGqlProjectData = (projectPath) =>
- query({
- query: getIdeProject,
- variables: { projectPath },
- }).then(({ data }) => data.project);
-
export default {
getFileData(endpoint) {
return axios.get(endpoint, {
@@ -61,18 +54,6 @@ export default {
)
.then(({ data }) => data);
},
- getProjectData(namespace, project) {
- const projectPath = `${namespace}/${project}`;
-
- return Promise.all([fetchApiProjectData(projectPath), fetchGqlProjectData(projectPath)]).then(
- ([apiProjectData, gqlProjectData]) => ({
- data: {
- ...apiProjectData,
- ...gqlProjectData,
- },
- }),
- );
- },
getProjectMergeRequests(projectId, params = {}) {
return Api.projectMergeRequests(projectId, params);
},
@@ -115,4 +96,13 @@ export default {
variables: { input: { featureName: name } },
}).then(({ data }) => data);
},
+ getProjectPermissionsData(projectPath) {
+ return query({
+ query: getIdeProject,
+ variables: { projectPath },
+ }).then(({ data }) => ({
+ ...data.project,
+ id: getIdFromGraphQLId(data.project.id),
+ }));
+ },
};
diff --git a/app/assets/javascripts/ide/stores/actions/project.js b/app/assets/javascripts/ide/stores/actions/project.js
index 93ad19ba81e..0ec808339fb 100644
--- a/app/assets/javascripts/ide/stores/actions/project.js
+++ b/app/assets/javascripts/ide/stores/actions/project.js
@@ -1,35 +1,44 @@
import { escape } from 'lodash';
import createFlash from '~/flash';
import { __, sprintf } from '~/locale';
+import { logError } from '~/lib/logger';
import api from '../../../api';
import service from '../../services';
import * as types from '../mutation_types';
-export const getProjectData = ({ commit, state }, { namespace, projectId, force = false } = {}) =>
- new Promise((resolve, reject) => {
- if (!state.projects[`${namespace}/${projectId}`] || force) {
- commit(types.TOGGLE_LOADING, { entry: state });
- service
- .getProjectData(namespace, projectId)
- .then((res) => res.data)
- .then((data) => {
- commit(types.TOGGLE_LOADING, { entry: state });
- commit(types.SET_PROJECT, { projectPath: `${namespace}/${projectId}`, project: data });
- commit(types.SET_CURRENT_PROJECT, `${namespace}/${projectId}`);
- resolve(data);
- })
- .catch(() => {
- createFlash({
- message: __('Error loading project data. Please try again.'),
- fadeTransition: false,
- addBodyClass: true,
- });
- reject(new Error(`Project not loaded ${namespace}/${projectId}`));
- });
- } else {
- resolve(state.projects[`${namespace}/${projectId}`]);
- }
+const ERROR_LOADING_PROJECT = __('Error loading project data. Please try again.');
+
+const errorFetchingData = (e) => {
+ logError(ERROR_LOADING_PROJECT, e);
+
+ createFlash({
+ message: ERROR_LOADING_PROJECT,
+ fadeTransition: false,
+ addBodyClass: true,
});
+};
+
+export const setProject = ({ commit }, { project } = {}) => {
+ if (!project) {
+ return;
+ }
+ const projectPath = project.path_with_namespace;
+ commit(types.SET_PROJECT, { projectPath, project });
+ commit(types.SET_CURRENT_PROJECT, projectPath);
+};
+
+export const fetchProjectPermissions = ({ commit, state }) => {
+ const projectPath = state.currentProjectId;
+ if (!projectPath) {
+ return undefined;
+ }
+ return service
+ .getProjectPermissionsData(projectPath)
+ .then((permissions) => {
+ commit(types.UPDATE_PROJECT, { projectPath, props: permissions });
+ })
+ .catch(errorFetchingData);
+};
export const refreshLastCommitData = ({ commit }, { projectId, branchId } = {}) =>
service
diff --git a/app/assets/javascripts/ide/stores/mutation_types.js b/app/assets/javascripts/ide/stores/mutation_types.js
index 77755b179ef..13f338c4a48 100644
--- a/app/assets/javascripts/ide/stores/mutation_types.js
+++ b/app/assets/javascripts/ide/stores/mutation_types.js
@@ -8,6 +8,7 @@ export const SET_LINKS = 'SET_LINKS';
// Project Mutation Types
export const SET_PROJECT = 'SET_PROJECT';
export const SET_CURRENT_PROJECT = 'SET_CURRENT_PROJECT';
+export const UPDATE_PROJECT = 'UPDATE_PROJECT';
export const TOGGLE_EMPTY_STATE = 'TOGGLE_EMPTY_STATE';
// Merge request mutation types
diff --git a/app/assets/javascripts/ide/stores/mutations/project.js b/app/assets/javascripts/ide/stores/mutations/project.js
index 034fdad4305..9f65d3a543e 100644
--- a/app/assets/javascripts/ide/stores/mutations/project.js
+++ b/app/assets/javascripts/ide/stores/mutations/project.js
@@ -1,3 +1,4 @@
+import Vue from 'vue';
import * as types from '../mutation_types';
export default {
@@ -24,4 +25,15 @@ export default {
empty_repo: value,
});
},
+ [types.UPDATE_PROJECT](state, { projectPath, props }) {
+ const project = state.projects[projectPath];
+
+ if (!project || !props) {
+ return;
+ }
+
+ Object.keys(props).forEach((key) => {
+ Vue.set(project, key, props[key]);
+ });
+ },
};
diff --git a/app/assets/javascripts/import_entities/import_groups/components/import_actions_cell.vue b/app/assets/javascripts/import_entities/import_groups/components/import_actions_cell.vue
index e004bc35087..deaf2654424 100644
--- a/app/assets/javascripts/import_entities/import_groups/components/import_actions_cell.vue
+++ b/app/assets/javascripts/import_entities/import_groups/components/import_actions_cell.vue
@@ -44,7 +44,7 @@ export default {
:size="16"
name="information-o"
:title="
- s__('BulkImports|Re-import creates a new group. It does not sync with the existing group.')
+ s__('BulkImport|Re-import creates a new group. It does not sync with the existing group.')
"
class="gl-ml-3"
/>
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 ec6025c84bb..028197ec9b1 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,9 +1,8 @@
<script>
import {
+ GlAlert,
GlButton,
GlEmptyState,
- GlDropdown,
- GlDropdownItem,
GlIcon,
GlLink,
GlLoadingIcon,
@@ -14,8 +13,8 @@ import {
} from '@gitlab/ui';
import { debounce } from 'lodash';
import createFlash from '~/flash';
-import { s__, __, n__ } from '~/locale';
-import PaginationLinks from '~/vue_shared/components/pagination_links.vue';
+import { s__, __, n__, sprintf } from '~/locale';
+import PaginationBar from '~/vue_shared/components/pagination_bar/pagination_bar.vue';
import { getGroupPathAvailability } from '~/rest_api';
import axios from '~/lib/utils/axios_utils';
import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
@@ -42,10 +41,9 @@ const DEFAULT_TD_CLASSES = 'gl-vertical-align-top!';
export default {
components: {
+ GlAlert,
GlButton,
GlEmptyState,
- GlDropdown,
- GlDropdownItem,
GlIcon,
GlLink,
GlLoadingIcon,
@@ -57,7 +55,7 @@ export default {
ImportTargetCell,
ImportStatusCell,
ImportActionsCell,
- PaginationLinks,
+ PaginationBar,
},
props: {
@@ -83,6 +81,7 @@ export default {
selectedGroupsIds: [],
pendingGroupsIds: [],
importTargets: {},
+ unavailableFeaturesAlertVisible: true,
};
},
@@ -170,7 +169,7 @@ export default {
},
availableGroupsForImport() {
- return this.groupsTableData.filter((g) => g.flags.isAvailableForImport && g.flags.isInvalid);
+ return this.groupsTableData.filter((g) => g.flags.isAvailableForImport && !g.flags.isInvalid);
},
humanizedTotal() {
@@ -204,6 +203,23 @@ export default {
return { start, end, total };
},
+
+ unavailableFeatures() {
+ if (!this.hasGroups) {
+ return [];
+ }
+
+ return Object.entries(this.bulkImportSourceGroups.versionValidation.features)
+ .filter(([, { available }]) => available === false)
+ .map(([k, v]) => ({ title: i18n.features[k] || k, version: v.minVersion }));
+ },
+
+ unavailableFeaturesAlertTitle() {
+ return sprintf(s__('BulkImport| %{host} is running outdated GitLab version (v%{version})'), {
+ host: this.sourceUrl,
+ version: this.bulkImportSourceGroups.versionValidation.features.sourceInstanceVersion,
+ });
+ },
},
watch: {
@@ -314,9 +330,8 @@ export default {
variables: { importRequests },
});
} catch (error) {
- const message = error?.networkError?.response?.data?.error ?? i18n.ERROR_IMPORT;
createFlash({
- message,
+ message: i18n.ERROR_IMPORT,
captureError: true,
error,
});
@@ -476,6 +491,38 @@ export default {
<img :src="$options.gitlabLogo" class="gl-w-6 gl-h-6 gl-mb-2 gl-display-inline gl-mr-2" />
{{ s__('BulkImport|Import groups from GitLab') }}
</h1>
+ <gl-alert
+ v-if="unavailableFeatures.length > 0 && unavailableFeaturesAlertVisible"
+ variant="warning"
+ :title="unavailableFeaturesAlertTitle"
+ @dismiss="unavailableFeaturesAlertVisible = false"
+ >
+ <gl-sprintf
+ :message="
+ s__(
+ 'BulkImport|Following data will not be migrated: %{bullets} Contact system administrator of %{host} to upgrade GitLab if you need this data in your migration',
+ )
+ "
+ >
+ <template #host>
+ <gl-link :href="sourceUrl" target="_blank">
+ {{ sourceUrl }}<gl-icon name="external-link" class="vertical-align-middle" />
+ </gl-link>
+ </template>
+ <template #bullets>
+ <ul>
+ <li v-for="feature in unavailableFeatures" :key="feature.title">
+ <gl-sprintf :message="s__('BulkImport|%{feature} (require v%{version})')">
+ <template #feature>{{ feature.title }}</template>
+ <template #version>
+ <strong>{{ feature.version }}</strong>
+ </template>
+ </gl-sprintf>
+ </li>
+ </ul>
+ </template>
+ </gl-sprintf>
+ </gl-alert>
<div
class="gl-py-5 gl-border-solid gl-border-gray-200 gl-border-0 gl-border-b-1 gl-display-flex"
>
@@ -495,7 +542,7 @@ export default {
</template>
<template #link>
<gl-link :href="sourceUrl" target="_blank">
- {{ sourceUrl }} <gl-icon name="external-link" class="vertical-align-middle" />
+ {{ sourceUrl }}<gl-icon name="external-link" class="vertical-align-middle" />
</gl-link>
</template>
</gl-sprintf>
@@ -521,13 +568,15 @@ export default {
/>
<template v-else>
<div
- class="gl-bg-gray-10 gl-border-solid gl-border-gray-200 gl-border-0 gl-border-b-1 gl-p-4 gl-display-flex gl-align-items-center"
+ class="gl-bg-gray-10 gl-border-solid gl-border-gray-200 gl-border-0 gl-border-b-1 gl-px-4 gl-display-flex gl-align-items-center import-table-bar"
>
- <gl-sprintf :message="__('%{count} selected')">
- <template #count>
- {{ selectedGroupsIds.length }}
- </template>
- </gl-sprintf>
+ <span data-test-id="selection-count">
+ <gl-sprintf :message="__('%{count} selected')">
+ <template #count>
+ {{ selectedGroupsIds.length }}
+ </template>
+ </gl-sprintf>
+ </span>
<gl-button
category="primary"
variant="confirm"
@@ -539,7 +588,7 @@ export default {
</div>
<gl-table
ref="table"
- class="gl-w-full"
+ class="gl-w-full import-table"
data-qa-selector="import_table"
:tbody-tr-class="rowClasses"
:tbody-tr-attr="qaRowAttributes"
@@ -599,49 +648,13 @@ export default {
/>
</template>
</gl-table>
- <div v-if="hasGroups" class="gl-display-flex gl-mt-3 gl-align-items-center">
- <pagination-links
- :change="setPage"
- :page-info="bulkImportSourceGroups.pageInfo"
- class="gl-m-0"
- />
- <gl-dropdown category="tertiary" :aria-label="__('Page size')" class="gl-ml-auto">
- <template #button-content>
- <span class="font-weight-bold">
- <gl-sprintf :message="__('%{count} items per page')">
- <template #count>
- {{ perPage }}
- </template>
- </gl-sprintf>
- </span>
- <gl-icon class="gl-button-icon dropdown-chevron" name="chevron-down" />
- </template>
- <gl-dropdown-item
- v-for="size in $options.PAGE_SIZES"
- :key="size"
- @click="setPageSize(size)"
- >
- <gl-sprintf :message="__('%{count} items per page')">
- <template #count>
- {{ size }}
- </template>
- </gl-sprintf>
- </gl-dropdown-item>
- </gl-dropdown>
- <div class="gl-ml-2">
- <gl-sprintf :message="s__('BulkImport|Showing %{start}-%{end} of %{total}')">
- <template #start>
- {{ paginationInfo.start }}
- </template>
- <template #end>
- {{ paginationInfo.end }}
- </template>
- <template #total>
- {{ humanizedTotal }}
- </template>
- </gl-sprintf>
- </div>
- </div>
+ <pagination-bar
+ v-if="hasGroups"
+ :page-info="bulkImportSourceGroups.pageInfo"
+ class="gl-mt-3"
+ @set-page="setPage"
+ @set-page-size="setPageSize"
+ />
</template>
</template>
</div>
diff --git a/app/assets/javascripts/import_entities/import_groups/components/import_target_cell.vue b/app/assets/javascripts/import_entities/import_groups/components/import_target_cell.vue
index ca9ae9447d0..344a6e45370 100644
--- a/app/assets/javascripts/import_entities/import_groups/components/import_target_cell.vue
+++ b/app/assets/javascripts/import_entities/import_groups/components/import_target_cell.vue
@@ -32,72 +32,84 @@ export default {
fullPath() {
return this.group.importTarget.targetNamespace.fullPath || s__('BulkImport|No parent');
},
- invalidNameValidationMessage() {
- return getInvalidNameValidationMessage(this.group.importTarget);
+ validationMessage() {
+ return (
+ this.group.progress?.message || getInvalidNameValidationMessage(this.group.importTarget)
+ );
+ },
+ validNameState() {
+ // bootstrap-vue requires null for "indifferent" state, if we return true
+ // this will highlight field in green like "passed validation"
+ return this.group.flags.isInvalid && this.group.flags.isAvailableForImport ? false : null;
},
},
};
</script>
<template>
- <div class="gl-display-flex gl-align-items-stretch">
- <import-group-dropdown
- #default="{ namespaces }"
- :text="fullPath"
- :disabled="!group.flags.isAvailableForImport"
- :namespaces="availableNamespaces"
- toggle-class="gl-rounded-top-right-none! gl-rounded-bottom-right-none!"
- class="gl-h-7 gl-flex-grow-1"
- data-qa-selector="target_namespace_selector_dropdown"
- >
- <gl-dropdown-item @click="$emit('update-target-namespace', { fullPath: '', id: null })">{{
- s__('BulkImport|No parent')
- }}</gl-dropdown-item>
- <template v-if="namespaces.length">
- <gl-dropdown-divider />
- <gl-dropdown-section-header>
- {{ s__('BulkImport|Existing groups') }}
- </gl-dropdown-section-header>
- <gl-dropdown-item
- v-for="ns in namespaces"
- :key="ns.fullPath"
- data-qa-selector="target_group_dropdown_item"
- :data-qa-group-name="ns.fullPath"
- @click="$emit('update-target-namespace', ns)"
- >
- {{ ns.fullPath }}
- </gl-dropdown-item>
- </template>
- </import-group-dropdown>
- <div
- class="gl-h-7 gl-px-3 gl-display-flex gl-align-items-center gl-border-solid gl-border-0 gl-border-t-1 gl-border-b-1 gl-bg-gray-10"
- :class="{
- 'gl-text-gray-400 gl-border-gray-100': !group.flags.isAvailableForImport,
- 'gl-border-gray-200': group.flags.isAvailableForImport,
- }"
- >
- /
- </div>
- <div class="gl-flex-grow-1">
- <gl-form-input
- class="gl-rounded-top-left-none gl-rounded-bottom-left-none"
+ <div>
+ <div class="gl-display-flex gl-align-items-stretch">
+ <import-group-dropdown
+ #default="{ namespaces }"
+ :text="fullPath"
+ :disabled="!group.flags.isAvailableForImport"
+ :namespaces="availableNamespaces"
+ toggle-class="gl-rounded-top-right-none! gl-rounded-bottom-right-none!"
+ class="gl-h-7 gl-flex-grow-1"
+ data-qa-selector="target_namespace_selector_dropdown"
+ >
+ <gl-dropdown-item @click="$emit('update-target-namespace', { fullPath: '', id: null })">{{
+ s__('BulkImport|No parent')
+ }}</gl-dropdown-item>
+ <template v-if="namespaces.length">
+ <gl-dropdown-divider />
+ <gl-dropdown-section-header>
+ {{ s__('BulkImport|Existing groups') }}
+ </gl-dropdown-section-header>
+ <gl-dropdown-item
+ v-for="ns in namespaces"
+ :key="ns.fullPath"
+ data-qa-selector="target_group_dropdown_item"
+ :data-qa-group-name="ns.fullPath"
+ @click="$emit('update-target-namespace', ns)"
+ >
+ {{ ns.fullPath }}
+ </gl-dropdown-item>
+ </template>
+ </import-group-dropdown>
+ <div
+ class="gl-h-7 gl-px-3 gl-display-flex gl-align-items-center gl-border-solid gl-border-0 gl-border-t-1 gl-border-b-1 gl-bg-gray-10"
:class="{
- 'gl-inset-border-1-gray-200!': group.flags.isAvailableForImport,
- 'gl-inset-border-1-gray-100!': !group.flags.isAvailableForImport,
- 'is-invalid': group.flags.isInvalid && group.flags.isAvailableForImport,
+ 'gl-text-gray-400 gl-border-gray-100': !group.flags.isAvailableForImport,
+ 'gl-border-gray-200': group.flags.isAvailableForImport,
}"
- debounce="500"
- :disabled="!group.flags.isAvailableForImport"
- :value="group.importTarget.newName"
- :aria-label="__('New name')"
- @input="$emit('update-new-name', $event)"
- />
- <p
- v-if="group.flags.isAvailableForImport && group.flags.isInvalid"
- class="gl-text-red-500 gl-m-0 gl-mt-2"
>
- {{ invalidNameValidationMessage }}
- </p>
+ /
+ </div>
+ <div class="gl-flex-grow-1">
+ <gl-form-input
+ class="gl-rounded-top-left-none gl-rounded-bottom-left-none"
+ :class="{
+ 'gl-inset-border-1-gray-200!':
+ group.flags.isAvailableForImport && !group.flags.isInvalid,
+ 'gl-inset-border-1-gray-100!':
+ !group.flags.isAvailableForImport && !group.flags.isInvalid,
+ }"
+ debounce="500"
+ :disabled="!group.flags.isAvailableForImport"
+ :value="group.importTarget.newName"
+ :aria-label="__('New name')"
+ :state="validNameState"
+ @input="$emit('update-new-name', $event)"
+ />
+ </div>
+ </div>
+ <div
+ v-if="group.flags.isAvailableForImport && (group.flags.isInvalid || validationMessage)"
+ class="gl-text-red-500 gl-m-0 gl-mt-2"
+ role="alert"
+ >
+ {{ validationMessage }}
</div>
</div>
</template>
diff --git a/app/assets/javascripts/import_entities/import_groups/constants.js b/app/assets/javascripts/import_entities/import_groups/constants.js
index aa9cf3897e6..ac1466238d0 100644
--- a/app/assets/javascripts/import_entities/import_groups/constants.js
+++ b/app/assets/javascripts/import_entities/import_groups/constants.js
@@ -11,6 +11,10 @@ export const i18n = {
),
ERROR_IMPORT: s__('BulkImport|Importing the group failed.'),
ERROR_IMPORT_COMPLETED: s__('BulkImport|Import is finished. Pick another name for re-import'),
+
+ features: {
+ projectMigration: __('projects'),
+ },
};
export const NEW_NAME_FIELD = 'newName';
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 bce6e7bcb1f..36da996ea17 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
@@ -14,6 +14,9 @@ export const clientTypenames = {
BulkImportPageInfo: 'ClientBulkImportPageInfo',
BulkImportTarget: 'ClientBulkImportTarget',
BulkImportProgress: 'ClientBulkImportProgress',
+ BulkImportVersionValidation: 'ClientBulkImportVersionValidation',
+ BulkImportVersionValidationFeature: 'ClientBulkImportVersionValidationFeature',
+ BulkImportVersionValidationFeatures: 'ClientBulkImportVersionValidationFeatures',
};
function makeLastImportTarget(data) {
@@ -92,6 +95,18 @@ export function createResolvers({ endpoints }) {
__typename: clientTypenames.BulkImportPageInfo,
...pagination,
},
+ versionValidation: {
+ __typename: clientTypenames.BulkImportVersionValidation,
+ features: {
+ __typename: clientTypenames.BulkImportVersionValidationFeatures,
+ sourceInstanceVersion: data.version_validation.features.source_instance_version,
+ projectMigration: {
+ __typename: clientTypenames.BulkImportVersionValidationFeature,
+ available: data.version_validation.features.project_migration.available,
+ minVersion: data.version_validation.features.project_migration.min_version,
+ },
+ },
+ },
};
return response;
},
@@ -142,9 +157,7 @@ export function createResolvers({ endpoints }) {
};
});
- const {
- data: { id: jobId },
- } = await axios.post(endpoints.createBulkImport, {
+ const { data: originalResponse } = await axios.post(endpoints.createBulkImport, {
bulk_import: importOperations.map((op) => ({
source_type: 'group_entity',
source_full_path: op.group.fullPath,
@@ -153,15 +166,21 @@ export function createResolvers({ endpoints }) {
})),
});
- return importOperations.map((op) => {
+ const responses = Array.isArray(originalResponse)
+ ? originalResponse
+ : [{ success: true, id: originalResponse.id }];
+
+ return importOperations.map((op, idx) => {
+ const response = responses[idx];
const lastImportTarget = {
targetNamespace: op.targetNamespace,
newName: op.newName,
};
const progress = {
- id: jobId,
- status: STATUSES.CREATED,
+ id: response.id || `local-${Date.now()}-${idx}`,
+ status: response.success ? STATUSES.CREATED : STATUSES.FAILED,
+ message: response.message || null,
};
localStorageCache.set(op.group.webUrl, { progress, lastImportTarget });
diff --git a/app/assets/javascripts/import_entities/import_groups/graphql/fragments/bulk_import_source_group_progress.fragment.graphql b/app/assets/javascripts/import_entities/import_groups/graphql/fragments/bulk_import_source_group_progress.fragment.graphql
index 2d60bf82d65..33c564f36a8 100644
--- a/app/assets/javascripts/import_entities/import_groups/graphql/fragments/bulk_import_source_group_progress.fragment.graphql
+++ b/app/assets/javascripts/import_entities/import_groups/graphql/fragments/bulk_import_source_group_progress.fragment.graphql
@@ -1,4 +1,5 @@
fragment BulkImportSourceGroupProgress on ClientBulkImportProgress {
id
status
+ message
}
diff --git a/app/assets/javascripts/import_entities/import_groups/graphql/mutations/import_groups.mutation.graphql b/app/assets/javascripts/import_entities/import_groups/graphql/mutations/import_groups.mutation.graphql
index 75215471d0f..39289887b75 100644
--- a/app/assets/javascripts/import_entities/import_groups/graphql/mutations/import_groups.mutation.graphql
+++ b/app/assets/javascripts/import_entities/import_groups/graphql/mutations/import_groups.mutation.graphql
@@ -9,6 +9,7 @@ mutation importGroups($importRequests: [ImportGroupInput!]!) {
progress {
id
status
+ message
}
}
}
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 28dfefdf8a7..ace8bffc012 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
@@ -11,5 +11,14 @@ query bulkImportSourceGroups($page: Int = 1, $perPage: Int = 20, $filter: String
total
totalPages
}
+ versionValidation {
+ features {
+ sourceInstanceVersion
+ projectMigration {
+ available
+ minVersion
+ }
+ }
+ }
}
}
diff --git a/app/assets/javascripts/import_entities/import_groups/graphql/services/local_storage_cache.js b/app/assets/javascripts/import_entities/import_groups/graphql/services/local_storage_cache.js
index 09bc7b33692..1aad22f0f3f 100644
--- a/app/assets/javascripts/import_entities/import_groups/graphql/services/local_storage_cache.js
+++ b/app/assets/javascripts/import_entities/import_groups/graphql/services/local_storage_cache.js
@@ -22,7 +22,14 @@ export class LocalStorageCache {
loadCacheFromStorage() {
try {
- return JSON.parse(this.storage.getItem(KEY)) ?? {};
+ const storage = JSON.parse(this.storage.getItem(KEY)) ?? {};
+ Object.values(storage).forEach((entry) => {
+ if (entry.progress && !('message' in entry.progress)) {
+ // eslint-disable-next-line no-param-reassign
+ entry.progress.message = '';
+ }
+ });
+ return storage;
} catch {
return {};
}
diff --git a/app/assets/javascripts/import_entities/import_groups/graphql/typedefs.graphql b/app/assets/javascripts/import_entities/import_groups/graphql/typedefs.graphql
index b8dd79a5000..c48e22a7717 100644
--- a/app/assets/javascripts/import_entities/import_groups/graphql/typedefs.graphql
+++ b/app/assets/javascripts/import_entities/import_groups/graphql/typedefs.graphql
@@ -11,11 +11,13 @@ type ClientBulkImportTarget {
type ClientBulkImportSourceGroupConnection {
nodes: [ClientBulkImportSourceGroup!]!
pageInfo: ClientBulkImportPageInfo!
+ versionValidation: ClientBulkImportVersionValidation!
}
type ClientBulkImportProgress {
id: ID!
status: String!
+ message: String
}
type ClientBulkImportValidationError {
@@ -45,6 +47,20 @@ type ClientBulkImportNamespaceSuggestion {
suggestions: [String!]!
}
+type ClientBulkImportVersionValidation {
+ features: ClientBulkImportVersionValidationFeatures!
+}
+
+type ClientBulkImportVersionValidationFeatures {
+ project_migration: ClientBulkImportVersionValidationFeature!
+ sourceInstanceVersion: String!
+}
+
+type ClientBulkImportVersionValidationFeature {
+ available: Boolean!
+ min_version: String!
+}
+
extend type Query {
bulkImportSourceGroups(
page: Int!
diff --git a/app/assets/javascripts/incidents/graphql/fragments/incident_fields.fragment.graphql b/app/assets/javascripts/incidents/graphql/fragments/incident_fields.fragment.graphql
index eb2dde14464..faa68d37088 100644
--- a/app/assets/javascripts/incidents/graphql/fragments/incident_fields.fragment.graphql
+++ b/app/assets/javascripts/incidents/graphql/fragments/incident_fields.fragment.graphql
@@ -1,3 +1,4 @@
+# eslint-disable-next-line @graphql-eslint/require-id-when-available
fragment IncidentFields on Issue {
severity
}
diff --git a/app/assets/javascripts/incidents/graphql/queries/get_count_by_status.query.graphql b/app/assets/javascripts/incidents/graphql/queries/get_count_by_status.query.graphql
index 4e44a506c4f..fda8a65d4a4 100644
--- a/app/assets/javascripts/incidents/graphql/queries/get_count_by_status.query.graphql
+++ b/app/assets/javascripts/incidents/graphql/queries/get_count_by_status.query.graphql
@@ -6,6 +6,7 @@ query getIncidentsCountByStatus(
$assigneeUsername: String = ""
) {
project(fullPath: $projectPath) {
+ id
issueStatusCounts(
search: $searchTerm
types: $issueTypes
diff --git a/app/assets/javascripts/incidents/graphql/queries/get_incidents.query.graphql b/app/assets/javascripts/incidents/graphql/queries/get_incidents.query.graphql
index f97664a3b77..1e18d89b656 100644
--- a/app/assets/javascripts/incidents/graphql/queries/get_incidents.query.graphql
+++ b/app/assets/javascripts/incidents/graphql/queries/get_incidents.query.graphql
@@ -14,6 +14,7 @@ query getIncidents(
$assigneeUsername: String = ""
) {
project(fullPath: $projectPath) {
+ id
issues(
search: $searchTerm
types: $issueTypes
@@ -27,18 +28,21 @@ query getIncidents(
before: $prevPageCursor
) {
nodes {
+ id
iid
title
createdAt
state
labels {
nodes {
+ id
title
color
}
}
assignees {
nodes {
+ id
name
username
avatarUrl
diff --git a/app/assets/javascripts/init_confirm_danger.js b/app/assets/javascripts/init_confirm_danger.js
index d3d32c8be54..a8833a17467 100644
--- a/app/assets/javascripts/init_confirm_danger.js
+++ b/app/assets/javascripts/init_confirm_danger.js
@@ -10,6 +10,7 @@ export default () => {
removeFormId = null,
phrase,
buttonText,
+ buttonClass = '',
buttonTestid = null,
confirmDangerMessage,
disabled = false,
@@ -25,6 +26,7 @@ export default () => {
props: {
phrase,
buttonText,
+ buttonClass,
buttonTestid,
disabled: parseBoolean(disabled),
},
diff --git a/app/assets/javascripts/init_issuable_sidebar.js b/app/assets/javascripts/init_issuable_sidebar.js
deleted file mode 100644
index 7a70d893008..00000000000
--- a/app/assets/javascripts/init_issuable_sidebar.js
+++ /dev/null
@@ -1,16 +0,0 @@
-/* eslint-disable no-new */
-
-import { getSidebarOptions } from '~/sidebar/mount_sidebar';
-import IssuableContext from './issuable_context';
-import Sidebar from './right_sidebar';
-
-export default () => {
- const sidebarOptEl = document.querySelector('.js-sidebar-options');
-
- if (!sidebarOptEl) return;
-
- const sidebarOptions = getSidebarOptions(sidebarOptEl);
-
- new IssuableContext(sidebarOptions.currentUser);
- Sidebar.initialize();
-};
diff --git a/app/assets/javascripts/init_labels.js b/app/assets/javascripts/init_labels.js
deleted file mode 100644
index 10bfbf7960c..00000000000
--- a/app/assets/javascripts/init_labels.js
+++ /dev/null
@@ -1,19 +0,0 @@
-import $ from 'jquery';
-import GroupLabelSubscription from './group_label_subscription';
-import LabelManager from './label_manager';
-import ProjectLabelSubscription from './project_label_subscription';
-
-export default () => {
- if ($('.prioritized-labels').length) {
- new LabelManager(); // eslint-disable-line no-new
- }
- $('.label-subscription').each((i, el) => {
- const $el = $(el);
-
- if ($el.find('.dropdown-group-label').length) {
- new GroupLabelSubscription($el); // eslint-disable-line no-new
- } else {
- new ProjectLabelSubscription($el); // eslint-disable-line no-new
- }
- });
-};
diff --git a/app/assets/javascripts/integrations/constants.js b/app/assets/javascripts/integrations/constants.js
index d214ee4ded6..84656bd41bb 100644
--- a/app/assets/javascripts/integrations/constants.js
+++ b/app/assets/javascripts/integrations/constants.js
@@ -1,9 +1,5 @@
import { s__, __ } from '~/locale';
-export const TEST_INTEGRATION_EVENT = 'testIntegration';
-export const SAVE_INTEGRATION_EVENT = 'saveIntegration';
-export const GET_JIRA_ISSUE_TYPES_EVENT = 'getJiraIssueTypes';
-export const TOGGLE_INTEGRATION_EVENT = 'toggleIntegration';
export const VALIDATE_INTEGRATION_FORM_EVENT = 'validateIntegrationForm';
export const integrationLevels = {
diff --git a/app/assets/javascripts/integrations/edit/api.js b/app/assets/javascripts/integrations/edit/api.js
new file mode 100644
index 00000000000..7bce5604f9d
--- /dev/null
+++ b/app/assets/javascripts/integrations/edit/api.js
@@ -0,0 +1,9 @@
+import axios from '~/lib/utils/axios_utils';
+
+/**
+ * Test the validity of [integrationFormData].
+ * @return Promise<{ issuetypes: []String }> - issuetypes contains valid Jira issue types.
+ */
+export const testIntegrationSettings = (testPath, integrationFormData) => {
+ return axios.put(testPath, integrationFormData);
+};
diff --git a/app/assets/javascripts/integrations/edit/components/active_checkbox.vue b/app/assets/javascripts/integrations/edit/components/active_checkbox.vue
index 9804a9e15f6..5ddf3aeb639 100644
--- a/app/assets/javascripts/integrations/edit/components/active_checkbox.vue
+++ b/app/assets/javascripts/integrations/edit/components/active_checkbox.vue
@@ -1,8 +1,6 @@
<script>
import { GlFormGroup, GlFormCheckbox } from '@gitlab/ui';
import { mapGetters } from 'vuex';
-import { TOGGLE_INTEGRATION_EVENT } from '~/integrations/constants';
-import eventHub from '../event_hub';
export default {
name: 'ActiveCheckbox',
@@ -20,14 +18,11 @@ export default {
},
mounted() {
this.activated = this.propsSource.initialActivated;
- // Initialize view
- this.$nextTick(() => {
- this.onChange(this.activated);
- });
+ this.onChange(this.activated);
},
methods: {
- onChange(e) {
- eventHub.$emit(TOGGLE_INTEGRATION_EVENT, e);
+ onChange(isChecked) {
+ this.$emit('toggle-integration-active', isChecked);
},
},
};
diff --git a/app/assets/javascripts/integrations/edit/components/confirmation_modal.vue b/app/assets/javascripts/integrations/edit/components/confirmation_modal.vue
index 89f7e3b7a89..bc6aa231a93 100644
--- a/app/assets/javascripts/integrations/edit/components/confirmation_modal.vue
+++ b/app/assets/javascripts/integrations/edit/components/confirmation_modal.vue
@@ -1,22 +1,17 @@
<script>
import { GlModal } from '@gitlab/ui';
-import { mapGetters } from 'vuex';
import { __ } from '~/locale';
export default {
components: {
GlModal,
},
+
computed: {
- ...mapGetters(['isDisabled']),
primaryProps() {
return {
text: __('Save'),
- attributes: [
- { variant: 'confirm' },
- { category: 'primary' },
- { disabled: this.isDisabled },
- ],
+ attributes: [{ variant: 'confirm' }, { category: 'primary' }],
};
},
cancelProps() {
diff --git a/app/assets/javascripts/integrations/edit/components/integration_form.vue b/app/assets/javascripts/integrations/edit/components/integration_form.vue
index ba1aeb28616..e570a468944 100644
--- a/app/assets/javascripts/integrations/edit/components/integration_form.vue
+++ b/app/assets/javascripts/integrations/edit/components/integration_form.vue
@@ -1,14 +1,17 @@
<script>
import { GlButton, GlModalDirective, GlSafeHtmlDirective as SafeHtml } from '@gitlab/ui';
+import * as Sentry from '@sentry/browser';
import { mapState, mapActions, mapGetters } from 'vuex';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import {
- TEST_INTEGRATION_EVENT,
- SAVE_INTEGRATION_EVENT,
+ VALIDATE_INTEGRATION_FORM_EVENT,
+ I18N_FETCH_TEST_SETTINGS_DEFAULT_ERROR_MESSAGE,
+ I18N_DEFAULT_ERROR_MESSAGE,
+ I18N_SUCCESSFUL_CONNECTION_MESSAGE,
integrationLevels,
} from '~/integrations/constants';
import eventHub from '../event_hub';
-
+import { testIntegrationSettings } from '../api';
import ActiveCheckbox from './active_checkbox.vue';
import ConfirmationModal from './confirmation_modal.vue';
import DynamicField from './dynamic_field.vue';
@@ -37,22 +40,26 @@ export default {
},
mixins: [glFeatureFlagsMixin()],
props: {
+ formSelector: {
+ type: String,
+ required: true,
+ },
helpHtml: {
type: String,
required: false,
default: '',
},
},
+ data() {
+ return {
+ integrationActive: false,
+ isTesting: false,
+ isSaving: false,
+ };
+ },
computed: {
- ...mapGetters(['currentKey', 'propsSource', 'isDisabled']),
- ...mapState([
- 'defaultState',
- 'customState',
- 'override',
- 'isSaving',
- 'isTesting',
- 'isResetting',
- ]),
+ ...mapGetters(['currentKey', 'propsSource']),
+ ...mapState(['defaultState', 'customState', 'override', 'isResetting']),
isEditable() {
return this.propsSource.editable;
},
@@ -65,29 +72,81 @@ export default {
this.customState.integrationLevel === integrationLevels.GROUP
);
},
- showReset() {
+ showResetButton() {
return this.isInstanceOrGroupLevel && this.propsSource.resetPath;
},
+ showTestButton() {
+ return this.propsSource.canTest;
+ },
+ disableButtons() {
+ return Boolean(this.isSaving || this.isResetting || this.isTesting);
+ },
+ },
+ mounted() {
+ // this form element is defined in Haml
+ this.form = document.querySelector(this.formSelector);
},
methods: {
- ...mapActions([
- 'setOverride',
- 'setIsSaving',
- 'setIsTesting',
- 'setIsResetting',
- 'fetchResetIntegration',
- ]),
+ ...mapActions(['setOverride', 'fetchResetIntegration', 'requestJiraIssueTypes']),
onSaveClick() {
- this.setIsSaving(true);
- eventHub.$emit(SAVE_INTEGRATION_EVENT);
+ this.isSaving = true;
+
+ if (this.integrationActive && !this.form.checkValidity()) {
+ this.isSaving = false;
+ eventHub.$emit(VALIDATE_INTEGRATION_FORM_EVENT);
+ return;
+ }
+
+ this.form.submit();
},
onTestClick() {
- this.setIsTesting(true);
- eventHub.$emit(TEST_INTEGRATION_EVENT);
+ this.isTesting = true;
+
+ if (!this.form.checkValidity()) {
+ eventHub.$emit(VALIDATE_INTEGRATION_FORM_EVENT);
+ return;
+ }
+
+ testIntegrationSettings(this.propsSource.testPath, this.getFormData())
+ .then(({ data: { error, message = I18N_FETCH_TEST_SETTINGS_DEFAULT_ERROR_MESSAGE } }) => {
+ if (error) {
+ eventHub.$emit(VALIDATE_INTEGRATION_FORM_EVENT);
+ this.$toast.show(message);
+ return;
+ }
+
+ this.$toast.show(I18N_SUCCESSFUL_CONNECTION_MESSAGE);
+ })
+ .catch((error) => {
+ this.$toast.show(I18N_DEFAULT_ERROR_MESSAGE);
+ Sentry.captureException(error);
+ })
+ .finally(() => {
+ this.isTesting = false;
+ });
},
onResetClick() {
this.fetchResetIntegration();
},
+ onRequestJiraIssueTypes() {
+ this.requestJiraIssueTypes(this.getFormData());
+ },
+ getFormData() {
+ return new FormData(this.form);
+ },
+ onToggleIntegrationState(integrationActive) {
+ this.integrationActive = integrationActive;
+ if (!this.form) {
+ return;
+ }
+
+ // If integration will be active, enable form validation.
+ if (integrationActive) {
+ this.form.removeAttribute('novalidate');
+ } else {
+ this.form.setAttribute('novalidate', true);
+ }
+ },
},
helpHtmlConfig: {
ADD_ATTR: ['target'], // allow external links, can be removed after https://gitlab.com/gitlab-org/gitlab-ui/-/issues/1427 is implemented
@@ -114,7 +173,11 @@ export default {
<!-- 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`" />
+ <active-checkbox
+ v-if="propsSource.showActive"
+ :key="`${currentKey}-active-checkbox`"
+ @toggle-integration-active="onToggleIntegrationState"
+ />
<jira-trigger-fields
v-if="isJira"
:key="`${currentKey}-jira-trigger-fields`"
@@ -135,6 +198,7 @@ export default {
v-if="isJira && !isInstanceOrGroupLevel"
:key="`${currentKey}-jira-issues-fields`"
v-bind="propsSource.jiraIssuesProps"
+ @request-jira-issue-types="onRequestJiraIssueTypes"
/>
<div v-if="isEditable" class="footer-block row-content-block">
<template v-if="isInstanceOrGroupLevel">
@@ -143,7 +207,7 @@ export default {
category="primary"
variant="confirm"
:loading="isSaving"
- :disabled="isDisabled"
+ :disabled="disableButtons"
data-qa-selector="save_changes_button"
>
{{ __('Save changes') }}
@@ -156,7 +220,8 @@ export default {
variant="confirm"
type="submit"
:loading="isSaving"
- :disabled="isDisabled"
+ :disabled="disableButtons"
+ data-testid="save-button"
data-qa-selector="save_changes_button"
@click.prevent="onSaveClick"
>
@@ -164,24 +229,24 @@ export default {
</gl-button>
<gl-button
- v-if="propsSource.canTest"
+ v-if="showTestButton"
category="secondary"
variant="confirm"
:loading="isTesting"
- :disabled="isDisabled"
- :href="propsSource.testPath"
+ :disabled="disableButtons"
+ data-testid="test-button"
@click.prevent="onTestClick"
>
{{ __('Test settings') }}
</gl-button>
- <template v-if="showReset">
+ <template v-if="showResetButton">
<gl-button
v-gl-modal.confirmResetIntegration
category="secondary"
variant="confirm"
:loading="isResetting"
- :disabled="isDisabled"
+ :disabled="disableButtons"
data-testid="reset-button"
>
{{ __('Reset') }}
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 7cbfb35aeaa..99498501f6c 100644
--- a/app/assets/javascripts/integrations/edit/components/jira_issues_fields.vue
+++ b/app/assets/javascripts/integrations/edit/components/jira_issues_fields.vue
@@ -1,10 +1,7 @@
<script>
import { GlFormGroup, GlFormCheckbox, GlFormInput, GlSprintf, GlLink } from '@gitlab/ui';
import { mapGetters } from 'vuex';
-import {
- VALIDATE_INTEGRATION_FORM_EVENT,
- GET_JIRA_ISSUE_TYPES_EVENT,
-} from '~/integrations/constants';
+import { VALIDATE_INTEGRATION_FORM_EVENT } from '~/integrations/constants';
import { s__, __ } from '~/locale';
import eventHub from '../event_hub';
import JiraUpgradeCta from './jira_upgrade_cta.vue';
@@ -91,9 +88,6 @@ export default {
validateForm() {
this.validated = true;
},
- getJiraIssueTypes() {
- eventHub.$emit(GET_JIRA_ISSUE_TYPES_EVENT);
- },
},
i18n: {
sectionTitle: s__('JiraService|View Jira issues in GitLab'),
@@ -123,7 +117,11 @@ export default {
</p>
<template v-if="showJiraIssuesIntegration">
<input name="service[issues_enabled]" type="hidden" :value="enableJiraIssues || false" />
- <gl-form-checkbox v-model="enableJiraIssues" :disabled="isInheriting">
+ <gl-form-checkbox
+ v-model="enableJiraIssues"
+ :disabled="isInheriting"
+ data-qa-selector="service_jira_issues_enabled_checkbox"
+ >
{{ $options.i18n.enableCheckboxLabel }}
<template #help>
{{ $options.i18n.enableCheckboxHelp }}
@@ -136,7 +134,7 @@ export default {
:initial-issue-type-id="initialVulnerabilitiesIssuetype"
:show-full-feature="showJiraVulnerabilitiesIntegration"
data-testid="jira-for-vulnerabilities"
- @request-get-issue-types="getJiraIssueTypes"
+ @request-jira-issue-types="$emit('request-jira-issue-types')"
/>
<jira-upgrade-cta
v-if="!showJiraVulnerabilitiesIntegration"
@@ -168,6 +166,7 @@ export default {
id="service_project_key"
v-model="projectKey"
name="service[project_key]"
+ data-qa-selector="service_jira_project_key_field"
:placeholder="$options.i18n.projectKeyPlaceholder"
:required="enableJiraIssues"
:state="validProjectKey"
diff --git a/app/assets/javascripts/integrations/edit/components/reset_confirmation_modal.vue b/app/assets/javascripts/integrations/edit/components/reset_confirmation_modal.vue
index 9472a3eeafe..5a445235219 100644
--- a/app/assets/javascripts/integrations/edit/components/reset_confirmation_modal.vue
+++ b/app/assets/javascripts/integrations/edit/components/reset_confirmation_modal.vue
@@ -1,6 +1,5 @@
<script>
import { GlModal } from '@gitlab/ui';
-import { mapGetters } from 'vuex';
import { __ } from '~/locale';
@@ -9,15 +8,10 @@ export default {
GlModal,
},
computed: {
- ...mapGetters(['isDisabled']),
primaryProps() {
return {
text: __('Reset'),
- attributes: [
- { variant: 'warning' },
- { category: 'primary' },
- { disabled: this.isDisabled },
- ],
+ attributes: [{ variant: 'warning' }, { category: 'primary' }],
};
},
cancelProps() {
diff --git a/app/assets/javascripts/integrations/edit/index.js b/app/assets/javascripts/integrations/edit/index.js
index 792e7d8e85e..9c9e3edbeb8 100644
--- a/app/assets/javascripts/integrations/edit/index.js
+++ b/app/assets/javascripts/integrations/edit/index.js
@@ -85,35 +85,39 @@ function parseDatasetToProps(data) {
};
}
-export default (el, defaultEl) => {
- if (!el) {
+export default function initIntegrationSettingsForm(formSelector) {
+ const customSettingsEl = document.querySelector('.js-vue-integration-settings');
+ const defaultSettingsEl = document.querySelector('.js-vue-default-integration-settings');
+
+ if (!customSettingsEl) {
return null;
}
- const props = parseDatasetToProps(el.dataset);
+ const customSettingsProps = parseDatasetToProps(customSettingsEl.dataset);
const initialState = {
defaultState: null,
- customState: props,
+ customState: customSettingsProps,
};
- if (defaultEl) {
- initialState.defaultState = Object.freeze(parseDatasetToProps(defaultEl.dataset));
+ if (defaultSettingsEl) {
+ initialState.defaultState = Object.freeze(parseDatasetToProps(defaultSettingsEl.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;
+ const helpHtml = customSettingsEl.querySelector('.js-integration-help-html')?.innerHTML;
return new Vue({
- el,
+ el: customSettingsEl,
store: createStore(initialState),
render(createElement) {
return createElement(IntegrationForm, {
props: {
helpHtml,
+ formSelector,
},
});
},
});
-};
+}
diff --git a/app/assets/javascripts/integrations/edit/store/actions.js b/app/assets/javascripts/integrations/edit/store/actions.js
index 400397c050c..97565a3a69c 100644
--- a/app/assets/javascripts/integrations/edit/store/actions.js
+++ b/app/assets/javascripts/integrations/edit/store/actions.js
@@ -1,10 +1,15 @@
import axios from 'axios';
import { refreshCurrentPage } from '~/lib/utils/url_utility';
+import {
+ VALIDATE_INTEGRATION_FORM_EVENT,
+ I18N_FETCH_TEST_SETTINGS_DEFAULT_ERROR_MESSAGE,
+ I18N_DEFAULT_ERROR_MESSAGE,
+} from '~/integrations/constants';
+import { testIntegrationSettings } from '../api';
+import eventHub from '../event_hub';
import * as types from './mutation_types';
export const setOverride = ({ commit }, override) => commit(types.SET_OVERRIDE, override);
-export const setIsSaving = ({ commit }, isSaving) => commit(types.SET_IS_SAVING, isSaving);
-export const setIsTesting = ({ commit }, isTesting) => commit(types.SET_IS_TESTING, isTesting);
export const setIsResetting = ({ commit }, isResetting) =>
commit(types.SET_IS_RESETTING, isResetting);
@@ -27,10 +32,28 @@ export const fetchResetIntegration = ({ dispatch, getters }) => {
.catch(() => dispatch('receiveResetIntegrationError'));
};
-export const requestJiraIssueTypes = ({ commit }) => {
+export const requestJiraIssueTypes = ({ commit, dispatch, getters }, formData) => {
commit(types.SET_JIRA_ISSUE_TYPES_ERROR_MESSAGE, '');
commit(types.SET_IS_LOADING_JIRA_ISSUE_TYPES, true);
+
+ return testIntegrationSettings(getters.propsSource.testPath, formData)
+ .then(
+ ({
+ data: { issuetypes, error, message = I18N_FETCH_TEST_SETTINGS_DEFAULT_ERROR_MESSAGE },
+ }) => {
+ if (error || !issuetypes?.length) {
+ eventHub.$emit(VALIDATE_INTEGRATION_FORM_EVENT);
+ throw new Error(message);
+ }
+
+ dispatch('receiveJiraIssueTypesSuccess', issuetypes);
+ },
+ )
+ .catch(({ message = I18N_DEFAULT_ERROR_MESSAGE }) => {
+ dispatch('receiveJiraIssueTypesError', message);
+ });
};
+
export const receiveJiraIssueTypesSuccess = ({ commit }, issueTypes = []) => {
commit(types.SET_IS_LOADING_JIRA_ISSUE_TYPES, false);
commit(types.SET_JIRA_ISSUE_TYPES, issueTypes);
diff --git a/app/assets/javascripts/integrations/edit/store/getters.js b/app/assets/javascripts/integrations/edit/store/getters.js
index 39e14de2d0d..b79132128cc 100644
--- a/app/assets/javascripts/integrations/edit/store/getters.js
+++ b/app/assets/javascripts/integrations/edit/store/getters.js
@@ -1,7 +1,5 @@
export const isInheriting = (state) => (state.defaultState === null ? false : !state.override);
-export const isDisabled = (state) => state.isSaving || state.isTesting || state.isResetting;
-
export const propsSource = (state, getters) =>
getters.isInheriting ? state.defaultState : state.customState;
diff --git a/app/assets/javascripts/integrations/edit/store/mutation_types.js b/app/assets/javascripts/integrations/edit/store/mutation_types.js
index c681056a515..ddf6bef7554 100644
--- a/app/assets/javascripts/integrations/edit/store/mutation_types.js
+++ b/app/assets/javascripts/integrations/edit/store/mutation_types.js
@@ -1,6 +1,4 @@
export const SET_OVERRIDE = 'SET_OVERRIDE';
-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';
diff --git a/app/assets/javascripts/integrations/edit/store/mutations.js b/app/assets/javascripts/integrations/edit/store/mutations.js
index 279df1b9266..e7e312ce650 100644
--- a/app/assets/javascripts/integrations/edit/store/mutations.js
+++ b/app/assets/javascripts/integrations/edit/store/mutations.js
@@ -4,12 +4,6 @@ export default {
[types.SET_OVERRIDE](state, override) {
state.override = override;
},
- [types.SET_IS_SAVING](state, isSaving) {
- state.isSaving = isSaving;
- },
- [types.SET_IS_TESTING](state, isTesting) {
- state.isTesting = isTesting;
- },
[types.SET_IS_RESETTING](state, isResetting) {
state.isResetting = isResetting;
},
diff --git a/app/assets/javascripts/integrations/edit/store/state.js b/app/assets/javascripts/integrations/edit/store/state.js
index 1c0b274e4ef..3d40d1b90d5 100644
--- a/app/assets/javascripts/integrations/edit/store/state.js
+++ b/app/assets/javascripts/integrations/edit/store/state.js
@@ -6,7 +6,6 @@ export default ({ defaultState = null, customState = {} } = {}) => {
defaultState,
customState,
isSaving: false,
- isTesting: false,
isResetting: false,
isLoadingJiraIssueTypes: false,
loadingJiraIssueTypesErrorMessage: '',
diff --git a/app/assets/javascripts/integrations/integration_settings_form.js b/app/assets/javascripts/integrations/integration_settings_form.js
deleted file mode 100644
index f519fc87c46..00000000000
--- a/app/assets/javascripts/integrations/integration_settings_form.js
+++ /dev/null
@@ -1,151 +0,0 @@
-import { delay } from 'lodash';
-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';
-import {
- TEST_INTEGRATION_EVENT,
- SAVE_INTEGRATION_EVENT,
- GET_JIRA_ISSUE_TYPES_EVENT,
- TOGGLE_INTEGRATION_EVENT,
- VALIDATE_INTEGRATION_FORM_EVENT,
- I18N_FETCH_TEST_SETTINGS_DEFAULT_ERROR_MESSAGE,
- I18N_DEFAULT_ERROR_MESSAGE,
- I18N_SUCCESSFUL_CONNECTION_MESSAGE,
-} from './constants';
-
-export default class IntegrationSettingsForm {
- constructor(formSelector) {
- this.$form = document.querySelector(formSelector);
- this.formActive = false;
-
- this.vue = null;
-
- // Form Metadata
- this.testEndPoint = this.$form.dataset.testUrl;
- }
-
- init() {
- // Init Vue component
- this.vue = initForm(
- document.querySelector('.js-vue-integration-settings'),
- document.querySelector('.js-vue-default-integration-settings'),
- );
- eventHub.$on(TOGGLE_INTEGRATION_EVENT, (active) => {
- this.formActive = active;
- this.toggleServiceState();
- });
- eventHub.$on(TEST_INTEGRATION_EVENT, () => {
- this.testIntegration();
- });
- eventHub.$on(SAVE_INTEGRATION_EVENT, () => {
- this.saveIntegration();
- });
- eventHub.$on(GET_JIRA_ISSUE_TYPES_EVENT, () => {
- this.getJiraIssueTypes(new FormData(this.$form));
- });
- }
-
- saveIntegration() {
- // Save Service if not active and check the following if active;
- // 1) If form contents are valid
- // 2) If this service can be saved
- // If both conditions are true, we override form submission
- // and save the service using provided configuration.
- const formValid = this.$form.checkValidity() || this.formActive === false;
-
- if (formValid) {
- delay(() => {
- this.$form.submit();
- }, 100);
- } else {
- eventHub.$emit(VALIDATE_INTEGRATION_FORM_EVENT);
- this.vue.$store.dispatch('setIsSaving', false);
- }
- }
-
- testIntegration() {
- // Service was marked active so now we check;
- // 1) If form contents are valid
- // 2) If this service can be tested
- // If both conditions are true, we override form submission
- // and test the service using provided configuration.
- if (this.$form.checkValidity()) {
- this.testSettings(new FormData(this.$form));
- } else {
- eventHub.$emit(VALIDATE_INTEGRATION_FORM_EVENT);
- this.vue.$store.dispatch('setIsTesting', false);
- }
- }
-
- /**
- * Change Form's validation enforcement based on service status (active/inactive)
- */
- toggleServiceState() {
- if (this.formActive) {
- this.$form.removeAttribute('novalidate');
- } else if (!this.$form.getAttribute('novalidate')) {
- this.$form.setAttribute('novalidate', 'novalidate');
- }
- }
-
- /**
- * 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 = I18N_FETCH_TEST_SETTINGS_DEFAULT_ERROR_MESSAGE },
- }) => {
- if (error || !issuetypes?.length) {
- eventHub.$emit(VALIDATE_INTEGRATION_FORM_EVENT);
- throw new Error(message);
- }
-
- dispatch('receiveJiraIssueTypesSuccess', issuetypes);
- },
- )
- .catch(({ message = I18N_DEFAULT_ERROR_MESSAGE }) => {
- 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 this.fetchTestSettings(formData)
- .then(({ data }) => {
- if (data.error) {
- toast(`${data.message} ${data.service_response}`);
- } else {
- this.vue.$store.dispatch('receiveJiraIssueTypesSuccess', data.issuetypes);
- toast(I18N_SUCCESSFUL_CONNECTION_MESSAGE);
- }
- })
- .catch(() => {
- toast(I18N_DEFAULT_ERROR_MESSAGE);
- })
- .finally(() => {
- this.vue.$store.dispatch('setIsTesting', false);
- });
- }
-}
diff --git a/app/assets/javascripts/integrations/overrides/components/integration_overrides.vue b/app/assets/javascripts/integrations/overrides/components/integration_overrides.vue
index 85018f133cb..3fc554c5371 100644
--- a/app/assets/javascripts/integrations/overrides/components/integration_overrides.vue
+++ b/app/assets/javascripts/integrations/overrides/components/integration_overrides.vue
@@ -6,8 +6,12 @@ import { DEFAULT_PER_PAGE } from '~/api';
import { fetchOverrides } from '~/integrations/overrides/api';
import { parseIntPagination, normalizeHeaders } from '~/lib/utils/common_utils';
import { truncateNamespace } from '~/lib/utils/text_utility';
+import { getParameterByName } from '~/lib/utils/url_utility';
import { __, s__ } from '~/locale';
import ProjectAvatar from '~/vue_shared/components/project_avatar.vue';
+import UrlSync from '~/vue_shared/components/url_sync.vue';
+
+const DEFAULT_PAGE = 1;
export default {
name: 'IntegrationOverrides',
@@ -18,6 +22,7 @@ export default {
GlTable,
GlAlert,
ProjectAvatar,
+ UrlSync,
},
props: {
overridesPath: {
@@ -35,7 +40,7 @@ export default {
return {
isLoading: true,
overrides: [],
- page: 1,
+ page: DEFAULT_PAGE,
totalItems: 0,
errorMessage: null,
};
@@ -44,12 +49,21 @@ export default {
showPagination() {
return this.totalItems > this.$options.DEFAULT_PER_PAGE && this.overrides.length > 0;
},
+ query() {
+ return {
+ page: this.page,
+ };
+ },
},
- mounted() {
- this.loadOverrides();
+ created() {
+ const initialPage = this.getInitialPage();
+ this.loadOverrides(initialPage);
},
methods: {
- loadOverrides(page = this.page) {
+ getInitialPage() {
+ return getParameterByName('page') ?? DEFAULT_PAGE;
+ },
+ loadOverrides(page) {
this.isLoading = true;
this.errorMessage = null;
@@ -119,14 +133,16 @@ export default {
</template>
</gl-table>
<div class="gl-display-flex gl-justify-content-center gl-mt-5">
- <gl-pagination
- v-if="showPagination"
- :per-page="$options.DEFAULT_PER_PAGE"
- :total-items="totalItems"
- :value="page"
- :disabled="isLoading"
- @input="loadOverrides"
- />
+ <template v-if="showPagination">
+ <gl-pagination
+ :per-page="$options.DEFAULT_PER_PAGE"
+ :total-items="totalItems"
+ :value="page"
+ :disabled="isLoading"
+ @input="loadOverrides"
+ />
+ <url-sync :query="query" />
+ </template>
</div>
</div>
</template>
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 cf4f434a7a8..91a139a5105 100644
--- a/app/assets/javascripts/invite_members/components/invite_members_modal.vue
+++ b/app/assets/javascripts/invite_members/components/invite_members_modal.vue
@@ -20,12 +20,11 @@ import { BV_SHOW_MODAL } from '~/lib/utils/constants';
import { getParameterValues } from '~/lib/utils/url_utility';
import { sprintf } from '~/locale';
import {
- INVITE_MEMBERS_IN_COMMENT,
GROUP_FILTERS,
USERS_FILTER_ALL,
- MEMBER_AREAS_OF_FOCUS,
INVITE_MEMBERS_FOR_TASK,
MODAL_LABELS,
+ LEARN_GITLAB,
} from '../constants';
import eventHub from '../event_hub';
import {
@@ -100,14 +99,6 @@ export default {
type: String,
required: true,
},
- areasOfFocusOptions: {
- type: Array,
- required: true,
- },
- noSelectionAreasOfFocus: {
- type: Array,
- required: true,
- },
tasksToBeDoneOptions: {
type: Array,
required: true,
@@ -125,7 +116,6 @@ export default {
inviteeType: 'members',
newUsersToInvite: [],
selectedDate: undefined,
- selectedAreasOfFocus: [],
selectedTasksToBeDone: [],
selectedTaskProject: this.projects[0],
groupToBeSharedWith: {},
@@ -181,16 +171,6 @@ export default {
this.newUsersToInvite.length === 0 && Object.keys(this.groupToBeSharedWith).length === 0
);
},
- areasOfFocusEnabled() {
- return !this.tasksToBeDoneEnabled && this.areasOfFocusOptions.length !== 0;
- },
- areasOfFocusForPost() {
- if (this.selectedAreasOfFocus.length === 0 && this.areasOfFocusEnabled) {
- return this.noSelectionAreasOfFocus;
- }
-
- return this.selectedAreasOfFocus;
- },
errorFieldDescription() {
if (this.inviteeType === 'group') {
return '';
@@ -200,7 +180,8 @@ export default {
},
tasksToBeDoneEnabled() {
return (
- getParameterValues('open_modal')[0] === 'invite_members_for_task' &&
+ (getParameterValues('open_modal')[0] === 'invite_members_for_task' ||
+ this.isOnLearnGitlab) &&
this.tasksToBeDoneOptions.length
);
},
@@ -221,11 +202,16 @@ export default {
? this.selectedTaskProject.id
: '';
},
+ isOnLearnGitlab() {
+ return this.source === LEARN_GITLAB;
+ },
},
mounted() {
eventHub.$on('openModal', (options) => {
this.openModal(options);
- this.trackEvent(MEMBER_AREAS_OF_FOCUS.name, MEMBER_AREAS_OF_FOCUS.view);
+ if (this.isOnLearnGitlab) {
+ this.trackEvent(INVITE_MEMBERS_FOR_TASK.name, this.source);
+ }
});
if (this.tasksToBeDoneEnabled) {
@@ -267,13 +253,6 @@ export default {
this.submitInviteMembers();
}
},
- trackInvite() {
- if (this.source === INVITE_MEMBERS_IN_COMMENT) {
- this.trackEvent(INVITE_MEMBERS_IN_COMMENT, 'comment_invite_success');
- }
-
- this.trackEvent(MEMBER_AREAS_OF_FOCUS.name, MEMBER_AREAS_OF_FOCUS.submit);
- },
trackinviteMembersForTask() {
const label = 'selected_tasks_to_be_done';
const property = this.selectedTasksToBeDone.join(',');
@@ -287,7 +266,6 @@ export default {
this.newUsersToInvite = [];
this.groupToBeSharedWith = {};
this.invalidFeedbackMessage = '';
- this.selectedAreasOfFocus = [];
this.selectedTasksToBeDone = [];
[this.selectedTaskProject] = this.projects;
},
@@ -303,7 +281,7 @@ export default {
: Api.groupShareWithGroup.bind(Api);
apiShareWithGroup(this.id, this.shareWithGroupPostData(this.groupToBeSharedWith.id))
- .then(this.showToastMessageSuccess)
+ .then(this.showSuccessMessage)
.catch(this.showInvalidFeedbackMessage);
},
submitInviteMembers() {
@@ -328,11 +306,10 @@ export default {
promises.push(apiAddByUserId(this.id, this.addByUserIdPostData(usersToAddById)));
}
- this.trackInvite();
this.trackinviteMembersForTask();
Promise.all(promises)
- .then(this.conditionallyShowToastSuccess)
+ .then(this.conditionallyShowSuccessMessage)
.catch(this.showInvalidFeedbackMessage);
},
inviteByEmailPostData(usersToInviteByEmail) {
@@ -341,7 +318,6 @@ export default {
email: usersToInviteByEmail,
access_level: this.selectedAccessLevel,
invite_source: this.source,
- areas_of_focus: this.areasOfFocusForPost,
tasks_to_be_done: this.tasksToBeDoneForPost,
tasks_project_id: this.tasksProjectForPost,
};
@@ -352,7 +328,6 @@ export default {
user_id: usersToAddById,
access_level: this.selectedAccessLevel,
invite_source: this.source,
- areas_of_focus: this.areasOfFocusForPost,
tasks_to_be_done: this.tasksToBeDoneForPost,
tasks_project_id: this.tasksProjectForPost,
};
@@ -364,11 +339,11 @@ export default {
group_access: this.selectedAccessLevel,
};
},
- conditionallyShowToastSuccess(response) {
+ conditionallyShowSuccessMessage(response) {
const message = this.unescapeMsg(responseMessageFromSuccess(response));
if (message === '') {
- this.showToastMessageSuccess();
+ this.showSuccessMessage();
return;
}
@@ -376,8 +351,12 @@ export default {
this.invalidFeedbackMessage = message;
this.isLoading = false;
},
- showToastMessageSuccess() {
- this.$toast.show(this.$options.labels.toastMessageSuccessful, this.toastOptions);
+ showSuccessMessage() {
+ if (this.isOnLearnGitlab) {
+ eventHub.$emit('showSuccessfulInvitationsAlert');
+ } else {
+ this.$toast.show(this.$options.labels.toastMessageSuccessful, this.toastOptions);
+ }
this.closeModal();
},
showInvalidFeedbackMessage(response) {
@@ -504,16 +483,6 @@ export default {
</template>
</gl-datepicker>
</div>
- <div v-if="areasOfFocusEnabled">
- <label class="gl-mt-5">
- {{ $options.labels.areasOfFocusLabel }}
- </label>
- <gl-form-checkbox-group
- v-model="selectedAreasOfFocus"
- :options="areasOfFocusOptions"
- data-testid="area-of-focus-checks"
- />
- </div>
<div v-if="showTasksToBeDone" data-testid="invite-members-modal-tasks-to-be-done">
<label class="gl-mt-5">
{{ $options.labels.members.tasksToBeDone.title }}
diff --git a/app/assets/javascripts/invite_members/components/invite_members_trigger.vue b/app/assets/javascripts/invite_members/components/invite_members_trigger.vue
index bf3250f63a5..7dd74f8803a 100644
--- a/app/assets/javascripts/invite_members/components/invite_members_trigger.vue
+++ b/app/assets/javascripts/invite_members/components/invite_members_trigger.vue
@@ -1,6 +1,5 @@
<script>
import { GlButton, GlLink, GlIcon } from '@gitlab/ui';
-import ExperimentTracking from '~/experimentation/experiment_tracking';
import { s__ } from '~/locale';
import eventHub from '../event_hub';
import { TRIGGER_ELEMENT_BUTTON, TRIGGER_ELEMENT_SIDE_NAV } from '../constants';
@@ -32,11 +31,6 @@ export default {
type: String,
required: true,
},
- trackExperiment: {
- type: String,
- required: false,
- default: undefined,
- },
triggerElement: {
type: String,
required: false,
@@ -72,9 +66,6 @@ export default {
return baseAttributes;
},
},
- mounted() {
- this.trackExperimentOnShow();
- },
methods: {
checkTrigger(targetTriggerElement) {
return this.triggerElement === targetTriggerElement;
@@ -82,12 +73,6 @@ export default {
openModal() {
eventHub.$emit('openModal', { inviteeType: 'members', source: this.triggerSource });
},
- trackExperimentOnShow() {
- if (this.trackExperiment) {
- const tracking = new ExperimentTracking(this.trackExperiment);
- tracking.event('comment_invite_shown');
- }
- },
},
TRIGGER_ELEMENT_BUTTON,
TRIGGER_ELEMENT_SIDE_NAV,
diff --git a/app/assets/javascripts/invite_members/constants.js b/app/assets/javascripts/invite_members/constants.js
index 59d4c2f3077..ec59b3909fe 100644
--- a/app/assets/javascripts/invite_members/constants.js
+++ b/app/assets/javascripts/invite_members/constants.js
@@ -2,12 +2,6 @@ import { __, s__ } from '~/locale';
export const SEARCH_DELAY = 200;
-export const INVITE_MEMBERS_IN_COMMENT = 'invite_members_in_comment';
-export const MEMBER_AREAS_OF_FOCUS = {
- name: 'member_areas_of_focus',
- view: 'view',
- submit: 'submit',
-};
export const INVITE_MEMBERS_FOR_TASK = {
minimum_access_level: 30,
name: 'invite_members_for_task',
@@ -77,9 +71,6 @@ export const READ_MORE_TEXT = s__(
export const INVITE_BUTTON_TEXT = s__('InviteMembersModal|Invite');
export const CANCEL_BUTTON_TEXT = s__('InviteMembersModal|Cancel');
export const HEADER_CLOSE_LABEL = s__('InviteMembersModal|Close invite team members');
-export const AREAS_OF_FOCUS_LABEL = s__(
- 'InviteMembersModal|What would you like new member(s) to focus on? (optional)',
-);
export const MODAL_LABELS = {
members: {
@@ -142,5 +133,6 @@ export const MODAL_LABELS = {
inviteButtonText: INVITE_BUTTON_TEXT,
cancelButtonText: CANCEL_BUTTON_TEXT,
headerCloseLabel: HEADER_CLOSE_LABEL,
- areasOfFocusLabel: AREAS_OF_FOCUS_LABEL,
};
+
+export const LEARN_GITLAB = 'learn_gitlab';
diff --git a/app/assets/javascripts/invite_members/init_invite_members_modal.js b/app/assets/javascripts/invite_members/init_invite_members_modal.js
index fc657a064dd..2cc056f2ddb 100644
--- a/app/assets/javascripts/invite_members/init_invite_members_modal.js
+++ b/app/assets/javascripts/invite_members/init_invite_members_modal.js
@@ -40,10 +40,8 @@ export default function initInviteMembersModal() {
defaultAccessLevel: parseInt(el.dataset.defaultAccessLevel, 10),
groupSelectFilter: el.dataset.groupsFilter,
groupSelectParentId: parseInt(el.dataset.parentId, 10),
- areasOfFocusOptions: JSON.parse(el.dataset.areasOfFocusOptions),
tasksToBeDoneOptions: JSON.parse(el.dataset.tasksToBeDoneOptions || '[]'),
projects: JSON.parse(el.dataset.projects || '[]'),
- noSelectionAreasOfFocus: JSON.parse(el.dataset.noSelectionAreasOfFocus),
usersFilter: el.dataset.usersFilter,
filterId: parseInt(el.dataset.filterId, 10),
},
diff --git a/app/assets/javascripts/issuable_bulk_update_sidebar/components/status_select.vue b/app/assets/javascripts/issuable/bulk_update_sidebar/components/status_select.vue
index 9509399e91d..9509399e91d 100644
--- a/app/assets/javascripts/issuable_bulk_update_sidebar/components/status_select.vue
+++ b/app/assets/javascripts/issuable/bulk_update_sidebar/components/status_select.vue
diff --git a/app/assets/javascripts/issuable_bulk_update_sidebar/constants.js b/app/assets/javascripts/issuable/bulk_update_sidebar/constants.js
index ad15b25f9cf..ad15b25f9cf 100644
--- a/app/assets/javascripts/issuable_bulk_update_sidebar/constants.js
+++ b/app/assets/javascripts/issuable/bulk_update_sidebar/constants.js
diff --git a/app/assets/javascripts/issuable_bulk_update_sidebar/init_issue_status_select.js b/app/assets/javascripts/issuable/bulk_update_sidebar/init_issue_status_select.js
index 43179a86d70..43179a86d70 100644
--- a/app/assets/javascripts/issuable_bulk_update_sidebar/init_issue_status_select.js
+++ b/app/assets/javascripts/issuable/bulk_update_sidebar/init_issue_status_select.js
diff --git a/app/assets/javascripts/issuable_bulk_update_sidebar/issuable_bulk_update_actions.js b/app/assets/javascripts/issuable/bulk_update_sidebar/issuable_bulk_update_actions.js
index 463e0e5837e..14824820c0d 100644
--- a/app/assets/javascripts/issuable_bulk_update_sidebar/issuable_bulk_update_actions.js
+++ b/app/assets/javascripts/issuable/bulk_update_sidebar/issuable_bulk_update_actions.js
@@ -115,7 +115,7 @@ export default {
});
// Add uniqueIds to add it as argument for _.intersection
labelIds.unshift(uniqueIds);
- // Return IDs that are present but not in all selected issueables
+ // Return IDs that are present but not in all selected issuables
return uniqueIds.filter((x) => !intersection.apply(this, labelIds).includes(x));
},
diff --git a/app/assets/javascripts/issuable_bulk_update_sidebar/issuable_bulk_update_sidebar.js b/app/assets/javascripts/issuable/bulk_update_sidebar/issuable_bulk_update_sidebar.js
index a9d4548f8cf..1eb3ffc9808 100644
--- a/app/assets/javascripts/issuable_bulk_update_sidebar/issuable_bulk_update_sidebar.js
+++ b/app/assets/javascripts/issuable/bulk_update_sidebar/issuable_bulk_update_sidebar.js
@@ -3,9 +3,9 @@
import $ from 'jquery';
import { property } from 'lodash';
-import issueableEventHub from '~/issues_list/eventhub';
-import LabelsSelect from '~/labels_select';
-import MilestoneSelect from '~/milestone_select';
+import issuableEventHub from '~/issues_list/eventhub';
+import LabelsSelect from '~/labels/labels_select';
+import MilestoneSelect from '~/milestones/milestone_select';
import initIssueStatusSelect from './init_issue_status_select';
import IssuableBulkUpdateActions from './issuable_bulk_update_actions';
import subscriptionSelect from './subscription_select';
@@ -50,8 +50,8 @@ export default class IssuableBulkUpdateSidebar {
// The event hub connects this bulk update logic with `issues_list_app.vue`.
// We can remove it once we've refactored the issues list page bulk edit sidebar to Vue.
// https://gitlab.com/gitlab-org/gitlab/-/issues/325874
- issueableEventHub.$on('issuables:enableBulkEdit', () => this.toggleBulkEdit(null, true));
- issueableEventHub.$on('issuables:updateBulkEdit', () => this.updateFormState());
+ issuableEventHub.$on('issuables:enableBulkEdit', () => this.toggleBulkEdit(null, true));
+ issuableEventHub.$on('issuables:updateBulkEdit', () => this.updateFormState());
}
initDropdowns() {
@@ -110,7 +110,7 @@ export default class IssuableBulkUpdateSidebar {
toggleBulkEdit(e, enable) {
e?.preventDefault();
- issueableEventHub.$emit('issuables:toggleBulkEdit', enable);
+ issuableEventHub.$emit('issuables:toggleBulkEdit', enable);
this.toggleSidebarDisplay(enable);
this.toggleBulkEditButtonDisabled(enable);
diff --git a/app/assets/javascripts/issuable_bulk_update_sidebar/issuable_init_bulk_update_sidebar.js b/app/assets/javascripts/issuable/bulk_update_sidebar/issuable_init_bulk_update_sidebar.js
index 179c2b83c6c..179c2b83c6c 100644
--- a/app/assets/javascripts/issuable_bulk_update_sidebar/issuable_init_bulk_update_sidebar.js
+++ b/app/assets/javascripts/issuable/bulk_update_sidebar/issuable_init_bulk_update_sidebar.js
diff --git a/app/assets/javascripts/issuable_bulk_update_sidebar/subscription_select.js b/app/assets/javascripts/issuable/bulk_update_sidebar/subscription_select.js
index b12ac776b4f..b12ac776b4f 100644
--- a/app/assets/javascripts/issuable_bulk_update_sidebar/subscription_select.js
+++ b/app/assets/javascripts/issuable/bulk_update_sidebar/subscription_select.js
diff --git a/app/assets/javascripts/issuable/components/issuable_by_email.vue b/app/assets/javascripts/issuable/components/issuable_by_email.vue
index 799d2bdc9e2..512fa6f8c68 100644
--- a/app/assets/javascripts/issuable/components/issuable_by_email.vue
+++ b/app/assets/javascripts/issuable/components/issuable_by_email.vue
@@ -54,8 +54,7 @@ export default {
data() {
return {
email: this.initialEmail,
- // eslint-disable-next-line @gitlab/require-i18n-strings
- issuableName: this.issuableType === 'issue' ? 'issue' : 'merge request',
+ issuableName: this.issuableType === 'issue' ? __('issue') : __('merge request'),
};
},
computed: {
diff --git a/app/assets/javascripts/vue_shared/components/issuable/issuable_header_warnings.vue b/app/assets/javascripts/issuable/components/issuable_header_warnings.vue
index 82223ab9ef4..82223ab9ef4 100644
--- a/app/assets/javascripts/vue_shared/components/issuable/issuable_header_warnings.vue
+++ b/app/assets/javascripts/issuable/components/issuable_header_warnings.vue
diff --git a/app/assets/javascripts/vue_shared/components/issue/issue_assignees.vue b/app/assets/javascripts/issuable/components/issue_assignees.vue
index 5955f31fc70..5955f31fc70 100644
--- a/app/assets/javascripts/vue_shared/components/issue/issue_assignees.vue
+++ b/app/assets/javascripts/issuable/components/issue_assignees.vue
diff --git a/app/assets/javascripts/vue_shared/components/issue/issue_milestone.vue b/app/assets/javascripts/issuable/components/issue_milestone.vue
index 6a0c21602bd..6a0c21602bd 100644
--- a/app/assets/javascripts/vue_shared/components/issue/issue_milestone.vue
+++ b/app/assets/javascripts/issuable/components/issue_milestone.vue
diff --git a/app/assets/javascripts/vue_shared/components/issue/related_issuable_item.vue b/app/assets/javascripts/issuable/components/related_issuable_item.vue
index 8aeff9257a5..2bb0e3c80f9 100644
--- a/app/assets/javascripts/vue_shared/components/issue/related_issuable_item.vue
+++ b/app/assets/javascripts/issuable/components/related_issuable_item.vue
@@ -9,8 +9,8 @@ import {
} from '@gitlab/ui';
import IssueDueDate from '~/boards/components/issue_due_date.vue';
import { sprintf } from '~/locale';
-import relatedIssuableMixin from '../../mixins/related_issuable_mixin';
-import CiIcon from '../ci_icon.vue';
+import CiIcon from '~/vue_shared/components/ci_icon.vue';
+import relatedIssuableMixin from '../mixins/related_issuable_mixin';
import IssueAssignees from './issue_assignees.vue';
import IssueMilestone from './issue_milestone.vue';
diff --git a/app/assets/javascripts/issuable/constants.js b/app/assets/javascripts/issuable/constants.js
index 9344f4a7c9a..5327f251fda 100644
--- a/app/assets/javascripts/issuable/constants.js
+++ b/app/assets/javascripts/issuable/constants.js
@@ -4,3 +4,8 @@ export const ISSUABLE_TYPE = {
issues: 'issues',
mergeRequests: 'merge-requests',
};
+
+export const ISSUABLE_INDEX = {
+ ISSUE: 'issue_',
+ MERGE_REQUEST: 'merge_request_',
+};
diff --git a/app/assets/javascripts/issuable/index.js b/app/assets/javascripts/issuable/index.js
new file mode 100644
index 00000000000..072422944f5
--- /dev/null
+++ b/app/assets/javascripts/issuable/index.js
@@ -0,0 +1,116 @@
+import { GlToast } from '@gitlab/ui';
+import Vue from 'vue';
+import IssuableContext from '~/issuable/issuable_context';
+import { parseBoolean } from '~/lib/utils/common_utils';
+import Sidebar from '~/right_sidebar';
+import { getSidebarOptions } from '~/sidebar/mount_sidebar';
+import CsvImportExportButtons from './components/csv_import_export_buttons.vue';
+import IssuableByEmail from './components/issuable_by_email.vue';
+import IssuableHeaderWarnings from './components/issuable_header_warnings.vue';
+
+export function initCsvImportExportButtons() {
+ const el = document.querySelector('.js-csv-import-export-buttons');
+
+ if (!el) return null;
+
+ const {
+ showExportButton,
+ showImportButton,
+ issuableType,
+ issuableCount,
+ email,
+ exportCsvPath,
+ importCsvIssuesPath,
+ containerClass,
+ canEdit,
+ projectImportJiraPath,
+ maxAttachmentSize,
+ showLabel,
+ } = el.dataset;
+
+ return new Vue({
+ el,
+ provide: {
+ showExportButton: parseBoolean(showExportButton),
+ showImportButton: parseBoolean(showImportButton),
+ issuableType,
+ email,
+ importCsvIssuesPath,
+ containerClass,
+ canEdit: parseBoolean(canEdit),
+ projectImportJiraPath,
+ maxAttachmentSize,
+ showLabel,
+ },
+ render(h) {
+ return h(CsvImportExportButtons, {
+ props: {
+ exportCsvPath,
+ issuableCount: parseInt(issuableCount, 10),
+ },
+ });
+ },
+ });
+}
+
+export function initIssuableByEmail() {
+ Vue.use(GlToast);
+
+ const el = document.querySelector('.js-issuable-by-email');
+
+ if (!el) return null;
+
+ const {
+ initialEmail,
+ issuableType,
+ emailsHelpPagePath,
+ quickActionsHelpPath,
+ markdownHelpPath,
+ resetPath,
+ } = el.dataset;
+
+ return new Vue({
+ el,
+ provide: {
+ initialEmail,
+ issuableType,
+ emailsHelpPagePath,
+ quickActionsHelpPath,
+ markdownHelpPath,
+ resetPath,
+ },
+ render(h) {
+ return h(IssuableByEmail);
+ },
+ });
+}
+
+export function initIssuableHeaderWarnings(store) {
+ const el = document.getElementById('js-issuable-header-warnings');
+
+ if (!el) {
+ return false;
+ }
+
+ const { hidden } = el.dataset;
+
+ return new Vue({
+ el,
+ store,
+ provide: { hidden: parseBoolean(hidden) },
+ render(createElement) {
+ return createElement(IssuableHeaderWarnings);
+ },
+ });
+}
+
+export function initIssuableSidebar() {
+ const sidebarOptEl = document.querySelector('.js-sidebar-options');
+
+ if (!sidebarOptEl) return;
+
+ const sidebarOptions = getSidebarOptions(sidebarOptEl);
+
+ new IssuableContext(sidebarOptions.currentUser); // eslint-disable-line no-new
+ Sidebar.initialize();
+}
diff --git a/app/assets/javascripts/issuable/init_csv_import_export_buttons.js b/app/assets/javascripts/issuable/init_csv_import_export_buttons.js
deleted file mode 100644
index 83163e3c478..00000000000
--- a/app/assets/javascripts/issuable/init_csv_import_export_buttons.js
+++ /dev/null
@@ -1,48 +0,0 @@
-import Vue from 'vue';
-import { parseBoolean } from '~/lib/utils/common_utils';
-import CsvImportExportButtons from './components/csv_import_export_buttons.vue';
-
-export default () => {
- const el = document.querySelector('.js-csv-import-export-buttons');
-
- if (!el) return null;
-
- const {
- showExportButton,
- showImportButton,
- issuableType,
- issuableCount,
- email,
- exportCsvPath,
- importCsvIssuesPath,
- containerClass,
- canEdit,
- projectImportJiraPath,
- maxAttachmentSize,
- showLabel,
- } = el.dataset;
-
- return new Vue({
- el,
- provide: {
- showExportButton: parseBoolean(showExportButton),
- showImportButton: parseBoolean(showImportButton),
- issuableType,
- email,
- importCsvIssuesPath,
- containerClass,
- canEdit: parseBoolean(canEdit),
- projectImportJiraPath,
- maxAttachmentSize,
- showLabel,
- },
- render(h) {
- return h(CsvImportExportButtons, {
- props: {
- exportCsvPath,
- issuableCount: parseInt(issuableCount, 10),
- },
- });
- },
- });
-};
diff --git a/app/assets/javascripts/issuable/init_issuable_by_email.js b/app/assets/javascripts/issuable/init_issuable_by_email.js
deleted file mode 100644
index 984b826234c..00000000000
--- a/app/assets/javascripts/issuable/init_issuable_by_email.js
+++ /dev/null
@@ -1,35 +0,0 @@
-import { GlToast } from '@gitlab/ui';
-import Vue from 'vue';
-import IssuableByEmail from './components/issuable_by_email.vue';
-
-Vue.use(GlToast);
-
-export default () => {
- const el = document.querySelector('.js-issueable-by-email');
-
- if (!el) return null;
-
- const {
- initialEmail,
- issuableType,
- emailsHelpPagePath,
- quickActionsHelpPath,
- markdownHelpPath,
- resetPath,
- } = el.dataset;
-
- return new Vue({
- el,
- provide: {
- initialEmail,
- issuableType,
- emailsHelpPagePath,
- quickActionsHelpPath,
- markdownHelpPath,
- resetPath,
- },
- render(h) {
- return h(IssuableByEmail);
- },
- });
-};
diff --git a/app/assets/javascripts/issuable_context.js b/app/assets/javascripts/issuable/issuable_context.js
index 51b5237a339..453305dd6e0 100644
--- a/app/assets/javascripts/issuable_context.js
+++ b/app/assets/javascripts/issuable/issuable_context.js
@@ -1,8 +1,8 @@
import { GlBreakpointInstance as bp } from '@gitlab/ui/dist/utils';
import $ from 'jquery';
import Cookies from 'js-cookie';
-import { loadCSSFile } from './lib/utils/css_utils';
-import UsersSelect from './users_select';
+import { loadCSSFile } from '~/lib/utils/css_utils';
+import UsersSelect from '~/users_select';
export default class IssuableContext {
constructor(currentUser) {
diff --git a/app/assets/javascripts/issuable_form.js b/app/assets/javascripts/issuable/issuable_form.js
index bafc26befda..91f47a86cb7 100644
--- a/app/assets/javascripts/issuable_form.js
+++ b/app/assets/javascripts/issuable/issuable_form.js
@@ -1,14 +1,14 @@
import $ from 'jquery';
import Pikaday from 'pikaday';
import GfmAutoComplete from 'ee_else_ce/gfm_auto_complete';
-import Autosave from './autosave';
-import AutoWidthDropdownSelect from './issuable/auto_width_dropdown_select';
-import { loadCSSFile } from './lib/utils/css_utils';
-import { parsePikadayDate, pikadayToString } from './lib/utils/datetime_utility';
-import { select2AxiosTransport } from './lib/utils/select2_utils';
-import { queryToObject, objectToQuery } from './lib/utils/url_utility';
-import UsersSelect from './users_select';
-import ZenMode from './zen_mode';
+import Autosave from '~/autosave';
+import AutoWidthDropdownSelect from '~/issuable/auto_width_dropdown_select';
+import { loadCSSFile } from '~/lib/utils/css_utils';
+import { parsePikadayDate, pikadayToString } from '~/lib/utils/datetime_utility';
+import { select2AxiosTransport } from '~/lib/utils/select2_utils';
+import { queryToObject, objectToQuery } from '~/lib/utils/url_utility';
+import UsersSelect from '~/users_select';
+import ZenMode from '~/zen_mode';
const MR_SOURCE_BRANCH = 'merge_request[source_branch]';
const MR_TARGET_BRANCH = 'merge_request[target_branch]';
diff --git a/app/assets/javascripts/templates/issuable_template_selector.js b/app/assets/javascripts/issuable/issuable_template_selector.js
index 1bb5e214c2e..cce903d388d 100644
--- a/app/assets/javascripts/templates/issuable_template_selector.js
+++ b/app/assets/javascripts/issuable/issuable_template_selector.js
@@ -1,9 +1,7 @@
-/* eslint-disable no-useless-return */
-
import $ from 'jquery';
+import TemplateSelector from '~/blob/template_selector';
import { __ } from '~/locale';
import Api from '../api';
-import TemplateSelector from '../blob/template_selector';
export default class IssuableTemplateSelector extends TemplateSelector {
constructor(...args) {
@@ -109,7 +107,5 @@ export default class IssuableTemplateSelector extends TemplateSelector {
} else {
this.setEditorContent(this.currentTemplate, { skipFocus: false });
}
-
- return;
}
}
diff --git a/app/assets/javascripts/templates/issuable_template_selectors.js b/app/assets/javascripts/issuable/issuable_template_selectors.js
index 443b3084113..92f825e55d3 100644
--- a/app/assets/javascripts/templates/issuable_template_selectors.js
+++ b/app/assets/javascripts/issuable/issuable_template_selectors.js
@@ -1,5 +1,3 @@
-/* eslint-disable no-new, class-methods-use-this */
-
import $ from 'jquery';
import IssuableTemplateSelector from './issuable_template_selector';
@@ -10,6 +8,8 @@ export default class IssuableTemplateSelectors {
this.$dropdowns.each((i, dropdown) => {
const $dropdown = $(dropdown);
+
+ // eslint-disable-next-line no-new
new IssuableTemplateSelector({
pattern: /(\.md)/,
data: $dropdown.data('data'),
@@ -21,6 +21,7 @@ export default class IssuableTemplateSelectors {
});
}
+ // eslint-disable-next-line class-methods-use-this
initEditor() {
const editor = $('.markdown-area');
// Proxy ace-editor's .setValue to jQuery's .val
diff --git a/app/assets/javascripts/vue_shared/mixins/related_issuable_mixin.js b/app/assets/javascripts/issuable/mixins/related_issuable_mixin.js
index 4a6edae0c06..4a6edae0c06 100644
--- a/app/assets/javascripts/vue_shared/mixins/related_issuable_mixin.js
+++ b/app/assets/javascripts/issuable/mixins/related_issuable_mixin.js
diff --git a/app/assets/javascripts/issuable_index.js b/app/assets/javascripts/issuable_index.js
deleted file mode 100644
index 5a57da292a0..00000000000
--- a/app/assets/javascripts/issuable_index.js
+++ /dev/null
@@ -1,7 +0,0 @@
-import issuableInitBulkUpdateSidebar from '~/issuable_bulk_update_sidebar/issuable_init_bulk_update_sidebar';
-
-export default class IssuableIndex {
- constructor(pagePrefix = 'issuable_') {
- issuableInitBulkUpdateSidebar.init(pagePrefix);
- }
-}
diff --git a/app/assets/javascripts/issuable_type_selector/index.js b/app/assets/javascripts/issuable_type_selector/index.js
deleted file mode 100644
index 433a62d1ae8..00000000000
--- a/app/assets/javascripts/issuable_type_selector/index.js
+++ /dev/null
@@ -1,16 +0,0 @@
-import Vue from 'vue';
-import InfoPopover from './components/info_popover.vue';
-
-export default function initIssuableTypeSelector() {
- const el = document.getElementById('js-type-popover');
-
- return new Vue({
- el,
- components: {
- InfoPopover,
- },
- render(h) {
- return h(InfoPopover);
- },
- });
-}
diff --git a/app/assets/javascripts/issues/constants.js b/app/assets/javascripts/issues/constants.js
new file mode 100644
index 00000000000..b7b123dfd5f
--- /dev/null
+++ b/app/assets/javascripts/issues/constants.js
@@ -0,0 +1,25 @@
+import { __ } from '~/locale';
+
+export const IssuableStatus = {
+ Closed: 'closed',
+ Open: 'opened',
+ Reopened: 'reopened',
+};
+
+export const IssuableStatusText = {
+ [IssuableStatus.Closed]: __('Closed'),
+ [IssuableStatus.Open]: __('Open'),
+ [IssuableStatus.Reopened]: __('Open'),
+};
+
+export const IssuableType = {
+ Issue: 'issue',
+ Epic: 'epic',
+ MergeRequest: 'merge_request',
+ Alert: 'alert',
+};
+
+export const WorkspaceType = {
+ project: 'project',
+ group: 'group',
+};
diff --git a/app/assets/javascripts/pages/projects/issues/service_desk/filtered_search.js b/app/assets/javascripts/issues/filtered_search_service_desk.js
index bec207aa439..bec207aa439 100644
--- a/app/assets/javascripts/pages/projects/issues/service_desk/filtered_search.js
+++ b/app/assets/javascripts/issues/filtered_search_service_desk.js
diff --git a/app/assets/javascripts/pages/projects/issues/form.js b/app/assets/javascripts/issues/form.js
index c0da0069a99..33371d065f9 100644
--- a/app/assets/javascripts/pages/projects/issues/form.js
+++ b/app/assets/javascripts/issues/form.js
@@ -1,14 +1,13 @@
/* eslint-disable no-new */
import $ from 'jquery';
-import IssuableForm from 'ee_else_ce/issuable_form';
+import IssuableForm from 'ee_else_ce/issuable/issuable_form';
import ShortcutsNavigation from '~/behaviors/shortcuts/shortcuts_navigation';
import GLForm from '~/gl_form';
-import initSuggestions from '~/issuable_suggestions';
-import initIssuableTypeSelector from '~/issuable_type_selector';
-import LabelsSelect from '~/labels_select';
-import MilestoneSelect from '~/milestone_select';
-import IssuableTemplateSelectors from '~/templates/issuable_template_selectors';
+import { initTitleSuggestions, initTypePopover } from '~/issues/new';
+import LabelsSelect from '~/labels/labels_select';
+import MilestoneSelect from '~/milestones/milestone_select';
+import IssuableTemplateSelectors from '~/issuable/issuable_template_selectors';
export default () => {
new ShortcutsNavigation();
@@ -20,6 +19,6 @@ export default () => {
warnTemplateOverride: true,
});
- initSuggestions();
- initIssuableTypeSelector();
+ initTitleSuggestions();
+ initTypePopover();
};
diff --git a/app/assets/javascripts/issues/init_filtered_search_service_desk.js b/app/assets/javascripts/issues/init_filtered_search_service_desk.js
new file mode 100644
index 00000000000..1901802c11c
--- /dev/null
+++ b/app/assets/javascripts/issues/init_filtered_search_service_desk.js
@@ -0,0 +1,11 @@
+import FilteredSearchServiceDesk from './filtered_search_service_desk';
+
+export function initFilteredSearchServiceDesk() {
+ if (document.querySelector('.filtered-search')) {
+ const supportBotData = JSON.parse(
+ document.querySelector('.js-service-desk-issues').dataset.supportBot,
+ );
+ const filteredSearchManager = new FilteredSearchServiceDesk(supportBotData);
+ filteredSearchManager.setup();
+ }
+}
diff --git a/app/assets/javascripts/issue.js b/app/assets/javascripts/issues/issue.js
index 1e053d7daaa..c471875654b 100644
--- a/app/assets/javascripts/issue.js
+++ b/app/assets/javascripts/issues/issue.js
@@ -1,11 +1,11 @@
import $ from 'jquery';
import { joinPaths } from '~/lib/utils/url_utility';
-import CreateMergeRequestDropdown from './create_merge_request_dropdown';
-import createFlash from './flash';
-import { EVENT_ISSUABLE_VUE_APP_CHANGE } from './issuable/constants';
-import axios from './lib/utils/axios_utils';
-import { addDelimiter } from './lib/utils/text_utility';
-import { __ } from './locale';
+import CreateMergeRequestDropdown from '~/create_merge_request_dropdown';
+import createFlash from '~/flash';
+import { EVENT_ISSUABLE_VUE_APP_CHANGE } from '~/issuable/constants';
+import axios from '~/lib/utils/axios_utils';
+import { addDelimiter } from '~/lib/utils/text_utility';
+import { __ } from '~/locale';
export default class Issue {
constructor() {
diff --git a/app/assets/javascripts/manual_ordering.js b/app/assets/javascripts/issues/manual_ordering.js
index 9613246d6a6..9613246d6a6 100644
--- a/app/assets/javascripts/manual_ordering.js
+++ b/app/assets/javascripts/issues/manual_ordering.js
diff --git a/app/assets/javascripts/issuable_suggestions/components/app.vue b/app/assets/javascripts/issues/new/components/title_suggestions.vue
index 48a5e220abf..0a9cdb12519 100644
--- a/app/assets/javascripts/issuable_suggestions/components/app.vue
+++ b/app/assets/javascripts/issues/new/components/title_suggestions.vue
@@ -2,12 +2,12 @@
import { GlTooltipDirective, GlIcon } from '@gitlab/ui';
import { __ } from '~/locale';
import query from '../queries/issues.query.graphql';
-import Suggestion from './item.vue';
+import TitleSuggestionsItem from './title_suggestions_item.vue';
export default {
components: {
- Suggestion,
GlIcon,
+ TitleSuggestionsItem,
},
directives: {
GlTooltip: GlTooltipDirective,
@@ -66,7 +66,7 @@ export default {
</script>
<template>
- <div v-show="showSuggestions" class="form-group row issuable-suggestions">
+ <div v-show="showSuggestions" class="form-group row">
<div v-once class="col-form-label col-sm-2 pt-0">
{{ __('Similar issues') }}
<gl-icon
@@ -86,7 +86,7 @@ export default {
'gl-mb-3': index !== issues.length - 1,
}"
>
- <suggestion :suggestion="suggestion" />
+ <title-suggestions-item :suggestion="suggestion" />
</li>
</ul>
</div>
diff --git a/app/assets/javascripts/issuable_suggestions/components/item.vue b/app/assets/javascripts/issues/new/components/title_suggestions_item.vue
index a01f4f747b9..a01f4f747b9 100644
--- a/app/assets/javascripts/issuable_suggestions/components/item.vue
+++ b/app/assets/javascripts/issues/new/components/title_suggestions_item.vue
diff --git a/app/assets/javascripts/issuable_type_selector/components/info_popover.vue b/app/assets/javascripts/issues/new/components/type_popover.vue
index 3a20ccba814..a70e79b70f9 100644
--- a/app/assets/javascripts/issuable_type_selector/components/info_popover.vue
+++ b/app/assets/javascripts/issues/new/components/type_popover.vue
@@ -19,9 +19,9 @@ export default {
<template>
<span id="popovercontainer">
- <gl-icon id="issuable-type-info" name="question-o" class="gl-ml-5 gl-text-gray-500" />
+ <gl-icon id="issue-type-info" name="question-o" class="gl-ml-5 gl-text-gray-500" />
<gl-popover
- target="issuable-type-info"
+ target="issue-type-info"
container="popovercontainer"
:title="$options.i18n.issueTypes"
triggers="focus hover"
diff --git a/app/assets/javascripts/issuable_suggestions/index.js b/app/assets/javascripts/issues/new/index.js
index 8f7f317d6b4..59a7cbec627 100644
--- a/app/assets/javascripts/issuable_suggestions/index.js
+++ b/app/assets/javascripts/issues/new/index.js
@@ -1,14 +1,19 @@
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import createDefaultClient from '~/lib/graphql';
-import App from './components/app.vue';
+import TitleSuggestions from './components/title_suggestions.vue';
+import TypePopover from './components/type_popover.vue';
-Vue.use(VueApollo);
+export function initTitleSuggestions() {
+ Vue.use(VueApollo);
-export default function initIssuableSuggestions() {
const el = document.getElementById('js-suggestions');
const issueTitle = document.getElementById('issue_title');
- const { projectPath } = el.dataset;
+
+ if (!el) {
+ return undefined;
+ }
+
const apolloProvider = new VueApollo({
defaultClient: createDefaultClient(),
});
@@ -26,13 +31,26 @@ export default function initIssuableSuggestions() {
this.search = issueTitle.value;
});
},
- render(h) {
- return h(App, {
+ render(createElement) {
+ return createElement(TitleSuggestions, {
props: {
- projectPath,
+ projectPath: el.dataset.projectPath,
search: this.search,
},
});
},
});
}
+
+export function initTypePopover() {
+ const el = document.getElementById('js-type-popover');
+
+ if (!el) {
+ return undefined;
+ }
+
+ return new Vue({
+ el,
+ render: (createElement) => createElement(TypePopover),
+ });
+}
diff --git a/app/assets/javascripts/issuable_suggestions/queries/issues.query.graphql b/app/assets/javascripts/issues/new/queries/issues.query.graphql
index 2384b381344..dc0757b141f 100644
--- a/app/assets/javascripts/issuable_suggestions/queries/issues.query.graphql
+++ b/app/assets/javascripts/issues/new/queries/issues.query.graphql
@@ -1,8 +1,10 @@
query issueSuggestion($fullPath: ID!, $search: String) {
project(fullPath: $fullPath) {
+ id
issues(search: $search, sort: updated_desc, first: 5) {
edges {
node {
+ id
iid
title
confidential
@@ -14,6 +16,7 @@ query issueSuggestion($fullPath: ID!, $search: String) {
createdAt
updatedAt
author {
+ id
name
username
avatarUrl
diff --git a/app/assets/javascripts/related_merge_requests/components/related_merge_requests.vue b/app/assets/javascripts/issues/related_merge_requests/components/related_merge_requests.vue
index 50835142d28..1d48446b083 100644
--- a/app/assets/javascripts/related_merge_requests/components/related_merge_requests.vue
+++ b/app/assets/javascripts/issues/related_merge_requests/components/related_merge_requests.vue
@@ -2,8 +2,8 @@
import { GlLink, GlLoadingIcon, GlIcon } from '@gitlab/ui';
import { mapState, mapActions } from 'vuex';
import { sprintf, __, n__ } from '~/locale';
-import RelatedIssuableItem from '~/vue_shared/components/issue/related_issuable_item.vue';
-import { parseIssuableData } from '../../issue_show/utils/parse_data';
+import RelatedIssuableItem from '~/issuable/components/related_issuable_item.vue';
+import { parseIssuableData } from '~/issues/show/utils/parse_data';
export default {
name: 'RelatedMergeRequests',
diff --git a/app/assets/javascripts/related_merge_requests/index.js b/app/assets/javascripts/issues/related_merge_requests/index.js
index ce33cf7df1d..ce33cf7df1d 100644
--- a/app/assets/javascripts/related_merge_requests/index.js
+++ b/app/assets/javascripts/issues/related_merge_requests/index.js
diff --git a/app/assets/javascripts/related_merge_requests/store/actions.js b/app/assets/javascripts/issues/related_merge_requests/store/actions.js
index 94abb50de89..94abb50de89 100644
--- a/app/assets/javascripts/related_merge_requests/store/actions.js
+++ b/app/assets/javascripts/issues/related_merge_requests/store/actions.js
diff --git a/app/assets/javascripts/related_merge_requests/store/index.js b/app/assets/javascripts/issues/related_merge_requests/store/index.js
index 925cc36cd76..925cc36cd76 100644
--- a/app/assets/javascripts/related_merge_requests/store/index.js
+++ b/app/assets/javascripts/issues/related_merge_requests/store/index.js
diff --git a/app/assets/javascripts/related_merge_requests/store/mutation_types.js b/app/assets/javascripts/issues/related_merge_requests/store/mutation_types.js
index 31d4fe032e1..31d4fe032e1 100644
--- a/app/assets/javascripts/related_merge_requests/store/mutation_types.js
+++ b/app/assets/javascripts/issues/related_merge_requests/store/mutation_types.js
diff --git a/app/assets/javascripts/related_merge_requests/store/mutations.js b/app/assets/javascripts/issues/related_merge_requests/store/mutations.js
index 11ca28a5fb9..11ca28a5fb9 100644
--- a/app/assets/javascripts/related_merge_requests/store/mutations.js
+++ b/app/assets/javascripts/issues/related_merge_requests/store/mutations.js
diff --git a/app/assets/javascripts/related_merge_requests/store/state.js b/app/assets/javascripts/issues/related_merge_requests/store/state.js
index bc3468a025b..bc3468a025b 100644
--- a/app/assets/javascripts/related_merge_requests/store/state.js
+++ b/app/assets/javascripts/issues/related_merge_requests/store/state.js
diff --git a/app/assets/javascripts/sentry_error_stack_trace/components/sentry_error_stack_trace.vue b/app/assets/javascripts/issues/sentry_error_stack_trace/components/sentry_error_stack_trace.vue
index 1530e9a15b5..1530e9a15b5 100644
--- a/app/assets/javascripts/sentry_error_stack_trace/components/sentry_error_stack_trace.vue
+++ b/app/assets/javascripts/issues/sentry_error_stack_trace/components/sentry_error_stack_trace.vue
diff --git a/app/assets/javascripts/sentry_error_stack_trace/index.js b/app/assets/javascripts/issues/sentry_error_stack_trace/index.js
index 8e9ee25e7a8..8e9ee25e7a8 100644
--- a/app/assets/javascripts/sentry_error_stack_trace/index.js
+++ b/app/assets/javascripts/issues/sentry_error_stack_trace/index.js
diff --git a/app/assets/javascripts/pages/projects/issues/show.js b/app/assets/javascripts/issues/show.js
index 24aa2f0da13..e43e56d7b4e 100644
--- a/app/assets/javascripts/pages/projects/issues/show.js
+++ b/app/assets/javascripts/issues/show.js
@@ -1,16 +1,15 @@
import loadAwardsHandler from '~/awards_handler';
import ShortcutsIssuable from '~/behaviors/shortcuts/shortcuts_issuable';
-import initIssuableSidebar from '~/init_issuable_sidebar';
-import { IssuableType } from '~/issuable_show/constants';
-import Issue from '~/issue';
-import { initIncidentApp, initIncidentHeaderActions } from '~/issue_show/incident';
-import { initIssuableApp, initIssueHeaderActions } from '~/issue_show/issue';
-import { parseIssuableData } from '~/issue_show/utils/parse_data';
+import { initIssuableHeaderWarnings, initIssuableSidebar } from '~/issuable';
+import { IssuableType } from '~/vue_shared/issuable/show/constants';
+import Issue from '~/issues/issue';
+import { initIncidentApp, initIncidentHeaderActions } from '~/issues/show/incident';
+import { initIssuableApp, initIssueHeaderActions } from '~/issues/show/issue';
+import { parseIssuableData } from '~/issues/show/utils/parse_data';
import initNotesApp from '~/notes';
import { store } from '~/notes/stores';
-import initRelatedMergeRequestsApp from '~/related_merge_requests';
-import initSentryErrorStackTraceApp from '~/sentry_error_stack_trace';
-import initIssuableHeaderWarning from '~/vue_shared/components/issuable/init_issuable_header_warning';
+import initRelatedMergeRequestsApp from '~/issues/related_merge_requests';
+import initSentryErrorStackTraceApp from '~/issues/sentry_error_stack_trace';
import ZenMode from '~/zen_mode';
export default function initShowIssue() {
@@ -33,7 +32,7 @@ export default function initShowIssue() {
break;
}
- initIssuableHeaderWarning(store);
+ initIssuableHeaderWarnings(store);
initSentryErrorStackTraceApp();
initRelatedMergeRequestsApp();
diff --git a/app/assets/javascripts/issue_show/components/app.vue b/app/assets/javascripts/issues/show/components/app.vue
index d3b58ed3012..eeaf865a35f 100644
--- a/app/assets/javascripts/issue_show/components/app.vue
+++ b/app/assets/javascripts/issues/show/components/app.vue
@@ -2,18 +2,11 @@
import { GlIcon, GlIntersectionObserver, GlTooltipDirective } from '@gitlab/ui';
import Visibility from 'visibilityjs';
import createFlash from '~/flash';
+import { IssuableStatus, IssuableStatusText, IssuableType } from '~/issues/constants';
import Poll from '~/lib/utils/poll';
import { visitUrl } from '~/lib/utils/url_utility';
import { __, sprintf } from '~/locale';
-import {
- IssuableStatus,
- IssuableStatusText,
- IssuableType,
- IssueTypePath,
- IncidentTypePath,
- IncidentType,
- POLLING_DELAY,
-} from '../constants';
+import { IssueTypePath, IncidentTypePath, IncidentType, POLLING_DELAY } from '../constants';
import eventHub from '../event_hub';
import getIssueStateQuery from '../queries/get_issue_state.query.graphql';
import Service from '../services/index';
@@ -296,13 +289,11 @@ export default {
window.addEventListener('beforeunload', this.handleBeforeUnloadEvent);
- eventHub.$on('delete.issuable', this.deleteIssuable);
eventHub.$on('update.issuable', this.updateIssuable);
eventHub.$on('close.form', this.closeForm);
eventHub.$on('open.form', this.openForm);
},
beforeDestroy() {
- eventHub.$off('delete.issuable', this.deleteIssuable);
eventHub.$off('update.issuable', this.updateIssuable);
eventHub.$off('close.form', this.closeForm);
eventHub.$off('open.form', this.openForm);
@@ -425,25 +416,6 @@ export default {
});
},
- deleteIssuable(payload) {
- return this.service
- .deleteIssuable(payload)
- .then((res) => res.data)
- .then((data) => {
- // Stop the poll so we don't get 404's with the issuable not existing
- this.poll.stop();
-
- visitUrl(data.web_url);
- })
- .catch(() => {
- createFlash({
- message: sprintf(__('Error deleting %{issuableType}'), {
- issuableType: this.issuableType,
- }),
- });
- });
- },
-
hideStickyHeader() {
this.isStickyHeaderShowing = false;
},
@@ -482,6 +454,7 @@ export default {
<div>
<div v-if="canUpdate && showForm">
<form-component
+ :endpoint="endpoint"
:form-state="formState"
:initial-description-text="initialDescriptionText"
:can-destroy="canDestroy"
diff --git a/app/assets/javascripts/issues/show/components/delete_issue_modal.vue b/app/assets/javascripts/issues/show/components/delete_issue_modal.vue
new file mode 100644
index 00000000000..26862346b86
--- /dev/null
+++ b/app/assets/javascripts/issues/show/components/delete_issue_modal.vue
@@ -0,0 +1,71 @@
+<script>
+import { GlModal } from '@gitlab/ui';
+import csrf from '~/lib/utils/csrf';
+import { capitalizeFirstCharacter } from '~/lib/utils/text_utility';
+import { __, sprintf } from '~/locale';
+
+export default {
+ actionCancel: { text: __('Cancel') },
+ csrf,
+ components: {
+ GlModal,
+ },
+ props: {
+ issuePath: {
+ type: String,
+ required: true,
+ },
+ issueType: {
+ type: String,
+ required: true,
+ },
+ modalId: {
+ type: String,
+ required: true,
+ },
+ title: {
+ type: String,
+ required: true,
+ },
+ },
+ computed: {
+ actionPrimary() {
+ return {
+ attributes: { variant: 'danger' },
+ text: this.title,
+ };
+ },
+ bodyText() {
+ return this.issueType.toLowerCase() === 'epic'
+ ? __('Delete this epic and all descendants?')
+ : sprintf(__('%{issuableType} will be removed! Are you sure?'), {
+ issuableType: capitalizeFirstCharacter(this.issueType),
+ });
+ },
+ },
+ methods: {
+ submitForm() {
+ this.$emit('delete');
+ this.$refs.form.submit();
+ },
+ },
+};
+</script>
+
+<template>
+ <gl-modal
+ :action-cancel="$options.actionCancel"
+ :action-primary="actionPrimary"
+ :modal-id="modalId"
+ size="sm"
+ :title="title"
+ @primary="submitForm"
+ >
+ <form ref="form" :action="issuePath" method="post">
+ <input type="hidden" name="_method" value="delete" />
+ <input type="hidden" name="authenticity_token" :value="$options.csrf.token" />
+ <input type="hidden" name="destroy_confirm" value="true" />
+ {{ bodyText }}
+ </form>
+ </gl-modal>
+</template>
diff --git a/app/assets/javascripts/issue_show/components/description.vue b/app/assets/javascripts/issues/show/components/description.vue
index 9dc122d426c..7be4c13f544 100644
--- a/app/assets/javascripts/issue_show/components/description.vue
+++ b/app/assets/javascripts/issues/show/components/description.vue
@@ -3,7 +3,7 @@ import { GlSafeHtmlDirective as SafeHtml } from '@gitlab/ui';
import $ from 'jquery';
import createFlash from '~/flash';
import { __, sprintf } from '~/locale';
-import TaskList from '../../task_list';
+import TaskList from '~/task_list';
import animateMixin from '../mixins/animate';
export default {
@@ -133,7 +133,7 @@ export default {
}
},
},
- safeHtmlConfig: { ADD_TAGS: ['gl-emoji'] },
+ safeHtmlConfig: { ADD_TAGS: ['gl-emoji', 'copy-code'] },
};
</script>
diff --git a/app/assets/javascripts/issue_show/components/edit_actions.vue b/app/assets/javascripts/issues/show/components/edit_actions.vue
index 5b7d232fde7..4daf6f2b61b 100644
--- a/app/assets/javascripts/issue_show/components/edit_actions.vue
+++ b/app/assets/javascripts/issues/show/components/edit_actions.vue
@@ -1,10 +1,12 @@
<script>
-import { GlButton, GlModal, GlModalDirective } from '@gitlab/ui';
+import { GlButton, GlModalDirective } from '@gitlab/ui';
import { uniqueId } from 'lodash';
import { __, sprintf } from '~/locale';
+import Tracking from '~/tracking';
import eventHub from '../event_hub';
import updateMixin from '../mixins/update';
import getIssueStateQuery from '../queries/get_issue_state.query.graphql';
+import DeleteIssueModal from './delete_issue_modal.vue';
const issuableTypes = {
issue: __('Issue'),
@@ -12,20 +14,26 @@ const issuableTypes = {
incident: __('Incident'),
};
+const trackingMixin = Tracking.mixin({ label: 'delete_issue' });
+
export default {
components: {
+ DeleteIssueModal,
GlButton,
- GlModal,
},
directives: {
GlModal: GlModalDirective,
},
- mixins: [updateMixin],
+ mixins: [trackingMixin, updateMixin],
props: {
canDestroy: {
type: Boolean,
required: true,
},
+ endpoint: {
+ required: true,
+ type: String,
+ },
formState: {
type: Object,
required: true,
@@ -65,27 +73,9 @@ export default {
issuableType: this.typeToShow.toLowerCase(),
});
},
- deleteIssuableModalText() {
- return this.issuableType === 'epic'
- ? __('Delete this epic and all descendants?')
- : sprintf(__('%{issuableType} will be removed! Are you sure?'), {
- issuableType: this.typeToShow,
- });
- },
isSubmitEnabled() {
return this.formState.title.trim() !== '';
},
- modalActionProps() {
- return {
- primary: {
- text: this.deleteIssuableButtonText,
- attributes: [{ variant: 'danger' }, { loading: this.deleteLoading }],
- },
- cancel: {
- text: __('Cancel'),
- },
- };
- },
shouldShowDeleteButton() {
return this.canDestroy && this.showDeleteButton;
},
@@ -101,7 +91,7 @@ export default {
},
deleteIssuable() {
this.deleteLoading = true;
- eventHub.$emit('delete.issuable', { destroy_confirm: true });
+ eventHub.$emit('delete.issuable');
},
},
};
@@ -135,22 +125,17 @@ export default {
variant="danger"
class="qa-delete-button"
data-testid="issuable-delete-button"
+ @click="track('click_button')"
>
{{ deleteIssuableButtonText }}
</gl-button>
- <gl-modal
- ref="removeModal"
+ <delete-issue-modal
+ :issue-path="endpoint"
+ :issue-type="typeToShow"
:modal-id="modalId"
- size="sm"
- :action-primary="modalActionProps.primary"
- :action-cancel="modalActionProps.cancel"
- @primary="deleteIssuable"
- >
- <template #modal-title>{{ deleteIssuableButtonText }}</template>
- <div>
- <p class="gl-mb-1">{{ deleteIssuableModalText }}</p>
- </div>
- </gl-modal>
+ :title="deleteIssuableButtonText"
+ @delete="deleteIssuable"
+ />
</div>
</div>
</template>
diff --git a/app/assets/javascripts/issue_show/components/edited.vue b/app/assets/javascripts/issues/show/components/edited.vue
index 64f61a1b88e..0da1900a6d0 100644
--- a/app/assets/javascripts/issue_show/components/edited.vue
+++ b/app/assets/javascripts/issues/show/components/edited.vue
@@ -1,6 +1,6 @@
<script>
/* eslint-disable @gitlab/vue-require-i18n-strings */
-import timeAgoTooltip from '../../vue_shared/components/time_ago_tooltip.vue';
+import timeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
export default {
components: {
diff --git a/app/assets/javascripts/issue_show/components/fields/description.vue b/app/assets/javascripts/issues/show/components/fields/description.vue
index 5476a1ef897..5476a1ef897 100644
--- a/app/assets/javascripts/issue_show/components/fields/description.vue
+++ b/app/assets/javascripts/issues/show/components/fields/description.vue
diff --git a/app/assets/javascripts/issue_show/components/fields/description_template.vue b/app/assets/javascripts/issues/show/components/fields/description_template.vue
index 35e7860cd9b..9ce49b65a1a 100644
--- a/app/assets/javascripts/issue_show/components/fields/description_template.vue
+++ b/app/assets/javascripts/issues/show/components/fields/description_template.vue
@@ -1,7 +1,7 @@
<script>
import { GlIcon } from '@gitlab/ui';
import $ from 'jquery';
-import IssuableTemplateSelectors from '../../../templates/issuable_template_selectors';
+import IssuableTemplateSelectors from '~/issuable/issuable_template_selectors';
export default {
components: {
diff --git a/app/assets/javascripts/issue_show/components/fields/title.vue b/app/assets/javascripts/issues/show/components/fields/title.vue
index a73926575d0..a73926575d0 100644
--- a/app/assets/javascripts/issue_show/components/fields/title.vue
+++ b/app/assets/javascripts/issues/show/components/fields/title.vue
diff --git a/app/assets/javascripts/issue_show/components/fields/type.vue b/app/assets/javascripts/issues/show/components/fields/type.vue
index 9110a6924b4..9110a6924b4 100644
--- a/app/assets/javascripts/issue_show/components/fields/type.vue
+++ b/app/assets/javascripts/issues/show/components/fields/type.vue
diff --git a/app/assets/javascripts/issue_show/components/form.vue b/app/assets/javascripts/issues/show/components/form.vue
index 001e8abb941..6447ec85b4e 100644
--- a/app/assets/javascripts/issue_show/components/form.vue
+++ b/app/assets/javascripts/issues/show/components/form.vue
@@ -2,7 +2,7 @@
import { GlAlert } from '@gitlab/ui';
import $ from 'jquery';
import Autosave from '~/autosave';
-import { IssuableType } from '~/issue_show/constants';
+import { IssuableType } from '~/issues/constants';
import eventHub from '../event_hub';
import EditActions from './edit_actions.vue';
import DescriptionField from './fields/description.vue';
@@ -26,6 +26,10 @@ export default {
type: Boolean,
required: true,
},
+ endpoint: {
+ type: String,
+ required: true,
+ },
formState: {
type: Object,
required: true,
@@ -213,6 +217,7 @@ export default {
:enable-autocomplete="enableAutocomplete"
/>
<edit-actions
+ :endpoint="endpoint"
:form-state="formState"
:can-destroy="canDestroy"
:show-delete-button="showDeleteButton"
diff --git a/app/assets/javascripts/issue_show/components/header_actions.vue b/app/assets/javascripts/issues/show/components/header_actions.vue
index 2c314ce1c3f..700ef92a0f3 100644
--- a/app/assets/javascripts/issue_show/components/header_actions.vue
+++ b/app/assets/javascripts/issues/show/components/header_actions.vue
@@ -1,31 +1,38 @@
<script>
-import { GlButton, GlDropdown, GlDropdownItem, GlLink, GlModal } from '@gitlab/ui';
+import {
+ GlButton,
+ GlDropdown,
+ GlDropdownDivider,
+ GlDropdownItem,
+ GlLink,
+ GlModal,
+ GlModalDirective,
+} from '@gitlab/ui';
import { mapActions, mapGetters, mapState } from 'vuex';
import createFlash, { FLASH_TYPES } from '~/flash';
import { EVENT_ISSUABLE_VUE_APP_CHANGE } from '~/issuable/constants';
-import { IssuableType } from '~/issuable_show/constants';
-import { IssuableStatus, IssueStateEvent } from '~/issue_show/constants';
+import { IssuableType } from '~/vue_shared/issuable/show/constants';
+import { IssuableStatus } from '~/issues/constants';
+import { IssueStateEvent } from '~/issues/show/constants';
import { capitalizeFirstCharacter } from '~/lib/utils/text_utility';
import { visitUrl } from '~/lib/utils/url_utility';
-import { __, sprintf } from '~/locale';
+import { s__, __, sprintf } from '~/locale';
import eventHub from '~/notes/event_hub';
+import Tracking from '~/tracking';
import promoteToEpicMutation from '../queries/promote_to_epic.mutation.graphql';
import updateIssueMutation from '../queries/update_issue.mutation.graphql';
+import DeleteIssueModal from './delete_issue_modal.vue';
+
+const trackingMixin = Tracking.mixin({ label: 'delete_issue' });
export default {
- components: {
- GlButton,
- GlDropdown,
- GlDropdownItem,
- GlLink,
- GlModal,
- },
actionCancel: {
text: __('Cancel'),
},
actionPrimary: {
text: __('Yes, close issue'),
},
+ deleteModalId: 'delete-modal-id',
i18n: {
promoteErrorMessage: __(
'Something went wrong while promoting the issue to an epic. Please try again.',
@@ -34,10 +41,26 @@ export default {
'The issue was successfully promoted to an epic. Redirecting to epic...',
),
},
+ components: {
+ DeleteIssueModal,
+ GlButton,
+ GlDropdown,
+ GlDropdownDivider,
+ GlDropdownItem,
+ GlLink,
+ GlModal,
+ },
+ directives: {
+ GlModal: GlModalDirective,
+ },
+ mixins: [trackingMixin],
inject: {
canCreateIssue: {
default: false,
},
+ canDestroyIssue: {
+ default: false,
+ },
canPromoteToEpic: {
default: false,
},
@@ -56,6 +79,9 @@ export default {
isIssueAuthor: {
default: false,
},
+ issuePath: {
+ default: '',
+ },
issueType: {
default: IssuableType.Issue,
},
@@ -78,10 +104,21 @@ export default {
isClosed() {
return this.openState === IssuableStatus.Closed;
},
+ issueTypeText() {
+ const issueTypeTexts = {
+ [IssuableType.Issue]: s__('HeaderAction|issue'),
+ [IssuableType.Incident]: s__('HeaderAction|incident'),
+ };
+
+ return issueTypeTexts[this.issueType] ?? this.issueType;
+ },
buttonText() {
return this.isClosed
- ? sprintf(__('Reopen %{issueType}'), { issueType: this.issueType })
- : sprintf(__('Close %{issueType}'), { issueType: this.issueType });
+ ? sprintf(__('Reopen %{issueType}'), { issueType: this.issueTypeText })
+ : sprintf(__('Close %{issueType}'), { issueType: this.issueTypeText });
+ },
+ deleteButtonText() {
+ return sprintf(__('Delete %{issuableType}'), { issuableType: this.issueTypeText });
},
qaSelector() {
return this.isClosed ? 'reopen_issue_button' : 'close_issue_button';
@@ -132,8 +169,7 @@ export default {
})
.then(({ data }) => {
if (data.updateIssue.errors.length) {
- createFlash({ message: data.updateIssue.errors.join('. ') });
- return;
+ throw new Error();
}
const payload = {
@@ -166,8 +202,7 @@ export default {
})
.then(({ data }) => {
if (data.promoteToEpic.errors.length) {
- createFlash({ message: data.promoteToEpic.errors.join('; ') });
- return;
+ throw new Error();
}
createFlash({
@@ -219,6 +254,16 @@ export default {
>
{{ __('Submit as spam') }}
</gl-dropdown-item>
+ <template v-if="canDestroyIssue">
+ <gl-dropdown-divider />
+ <gl-dropdown-item
+ v-gl-modal="$options.deleteModalId"
+ variant="danger"
+ @click="track('click_dropdown')"
+ >
+ {{ deleteButtonText }}
+ </gl-dropdown-item>
+ </template>
</gl-dropdown>
<gl-button
@@ -262,6 +307,16 @@ export default {
>
{{ __('Submit as spam') }}
</gl-dropdown-item>
+ <template v-if="canDestroyIssue">
+ <gl-dropdown-divider />
+ <gl-dropdown-item
+ v-gl-modal="$options.deleteModalId"
+ variant="danger"
+ @click="track('click_dropdown')"
+ >
+ {{ deleteButtonText }}
+ </gl-dropdown-item>
+ </template>
</gl-dropdown>
<gl-modal
@@ -279,5 +334,12 @@ export default {
</li>
</ul>
</gl-modal>
+
+ <delete-issue-modal
+ :issue-path="issuePath"
+ :issue-type="issueType"
+ :modal-id="$options.deleteModalId"
+ :title="deleteButtonText"
+ />
</div>
</template>
diff --git a/app/assets/javascripts/issue_show/components/incidents/graphql/queries/get_alert.graphql b/app/assets/javascripts/issues/show/components/incidents/graphql/queries/get_alert.graphql
index 938b90b3f7c..d88633f2ae9 100644
--- a/app/assets/javascripts/issue_show/components/incidents/graphql/queries/get_alert.graphql
+++ b/app/assets/javascripts/issues/show/components/incidents/graphql/queries/get_alert.graphql
@@ -1,5 +1,6 @@
query getAlert($iid: String!, $fullPath: ID!) {
project(fullPath: $fullPath) {
+ id
issue(iid: $iid) {
id
alertManagementAlert {
diff --git a/app/assets/javascripts/issue_show/components/incidents/highlight_bar.vue b/app/assets/javascripts/issues/show/components/incidents/highlight_bar.vue
index 96f187f26dd..d509f0dbc09 100644
--- a/app/assets/javascripts/issue_show/components/incidents/highlight_bar.vue
+++ b/app/assets/javascripts/issues/show/components/incidents/highlight_bar.vue
@@ -5,7 +5,7 @@ import { formatDate } from '~/lib/utils/datetime_utility';
export default {
components: {
GlLink,
- IncidentSla: () => import('ee_component/issue_show/components/incidents/incident_sla.vue'),
+ IncidentSla: () => import('ee_component/issues/show/components/incidents/incident_sla.vue'),
},
directives: {
GlTooltip: GlTooltipDirective,
diff --git a/app/assets/javascripts/issue_show/components/incidents/incident_tabs.vue b/app/assets/javascripts/issues/show/components/incidents/incident_tabs.vue
index 84107d3eaca..4790062ab7d 100644
--- a/app/assets/javascripts/issue_show/components/incidents/incident_tabs.vue
+++ b/app/assets/javascripts/issues/show/components/incidents/incident_tabs.vue
@@ -16,7 +16,7 @@ export default {
GlTab,
GlTabs,
HighlightBar,
- MetricsTab: () => import('ee_component/issue_show/components/incidents/metrics_tab.vue'),
+ MetricsTab: () => import('ee_component/issues/show/components/incidents/metrics_tab.vue'),
},
inject: ['fullPath', 'iid', 'uploadMetricsFeatureAvailable'],
apollo: {
diff --git a/app/assets/javascripts/issue_show/components/locked_warning.vue b/app/assets/javascripts/issues/show/components/locked_warning.vue
index 4b99888ae73..4b99888ae73 100644
--- a/app/assets/javascripts/issue_show/components/locked_warning.vue
+++ b/app/assets/javascripts/issues/show/components/locked_warning.vue
diff --git a/app/assets/javascripts/issue_show/components/pinned_links.vue b/app/assets/javascripts/issues/show/components/pinned_links.vue
index d38189307bd..d38189307bd 100644
--- a/app/assets/javascripts/issue_show/components/pinned_links.vue
+++ b/app/assets/javascripts/issues/show/components/pinned_links.vue
diff --git a/app/assets/javascripts/issue_show/components/title.vue b/app/assets/javascripts/issues/show/components/title.vue
index 5e92211685a..5e92211685a 100644
--- a/app/assets/javascripts/issue_show/components/title.vue
+++ b/app/assets/javascripts/issues/show/components/title.vue
diff --git a/app/assets/javascripts/issue_show/constants.js b/app/assets/javascripts/issues/show/constants.js
index ef9699deb42..35f3bcdad70 100644
--- a/app/assets/javascripts/issue_show/constants.js
+++ b/app/assets/javascripts/issues/show/constants.js
@@ -1,24 +1,5 @@
import { __ } from '~/locale';
-export const IssuableStatus = {
- Closed: 'closed',
- Open: 'opened',
- Reopened: 'reopened',
-};
-
-export const IssuableStatusText = {
- [IssuableStatus.Closed]: __('Closed'),
- [IssuableStatus.Open]: __('Open'),
- [IssuableStatus.Reopened]: __('Open'),
-};
-
-export const IssuableType = {
- Issue: 'issue',
- Epic: 'epic',
- MergeRequest: 'merge_request',
- Alert: 'alert',
-};
-
export const IssueStateEvent = {
Close: 'CLOSE',
Reopen: 'REOPEN',
@@ -39,8 +20,3 @@ export const IncidentType = 'incident';
export const issueState = { issueType: undefined, isDirty: false };
export const POLLING_DELAY = 2000;
-
-export const WorkspaceType = {
- project: 'project',
- group: 'group',
-};
diff --git a/app/assets/javascripts/issuable_show/event_hub.js b/app/assets/javascripts/issues/show/event_hub.js
index e31806ad199..e31806ad199 100644
--- a/app/assets/javascripts/issuable_show/event_hub.js
+++ b/app/assets/javascripts/issues/show/event_hub.js
diff --git a/app/assets/javascripts/issue_show/graphql.js b/app/assets/javascripts/issues/show/graphql.js
index 5b8630f7d63..5b8630f7d63 100644
--- a/app/assets/javascripts/issue_show/graphql.js
+++ b/app/assets/javascripts/issues/show/graphql.js
diff --git a/app/assets/javascripts/issue_show/incident.js b/app/assets/javascripts/issues/show/incident.js
index 3aff2d9c54a..a260c31e1da 100644
--- a/app/assets/javascripts/issue_show/incident.js
+++ b/app/assets/javascripts/issues/show/incident.js
@@ -81,12 +81,14 @@ export function initIncidentHeaderActions(store) {
store,
provide: {
canCreateIssue: parseBoolean(el.dataset.canCreateIncident),
+ canDestroyIssue: parseBoolean(el.dataset.canDestroyIssue),
canPromoteToEpic: parseBoolean(el.dataset.canPromoteToEpic),
canReopenIssue: parseBoolean(el.dataset.canReopenIssue),
canReportSpam: parseBoolean(el.dataset.canReportSpam),
canUpdateIssue: parseBoolean(el.dataset.canUpdateIssue),
iid: el.dataset.iid,
isIssueAuthor: parseBoolean(el.dataset.isIssueAuthor),
+ issuePath: el.dataset.issuePath,
issueType: el.dataset.issueType,
newIssuePath: el.dataset.newIssuePath,
projectPath: el.dataset.projectPath,
diff --git a/app/assets/javascripts/issue_show/issue.js b/app/assets/javascripts/issues/show/issue.js
index 25cc51478ff..60e90934af8 100644
--- a/app/assets/javascripts/issue_show/issue.js
+++ b/app/assets/javascripts/issues/show/issue.js
@@ -44,6 +44,7 @@ export function initIssuableApp(issuableData, store) {
isConfidential: this.getNoteableData?.confidential,
isLocked: this.getNoteableData?.discussion_locked,
issuableStatus: this.getNoteableData?.state,
+ id: this.getNoteableData?.id,
},
});
},
@@ -65,12 +66,14 @@ export function initIssueHeaderActions(store) {
store,
provide: {
canCreateIssue: parseBoolean(el.dataset.canCreateIssue),
+ canDestroyIssue: parseBoolean(el.dataset.canDestroyIssue),
canPromoteToEpic: parseBoolean(el.dataset.canPromoteToEpic),
canReopenIssue: parseBoolean(el.dataset.canReopenIssue),
canReportSpam: parseBoolean(el.dataset.canReportSpam),
canUpdateIssue: parseBoolean(el.dataset.canUpdateIssue),
iid: el.dataset.iid,
isIssueAuthor: parseBoolean(el.dataset.isIssueAuthor),
+ issuePath: el.dataset.issuePath,
issueType: el.dataset.issueType,
newIssuePath: el.dataset.newIssuePath,
projectPath: el.dataset.projectPath,
diff --git a/app/assets/javascripts/issue_show/mixins/animate.js b/app/assets/javascripts/issues/show/mixins/animate.js
index 4816393da1f..4816393da1f 100644
--- a/app/assets/javascripts/issue_show/mixins/animate.js
+++ b/app/assets/javascripts/issues/show/mixins/animate.js
diff --git a/app/assets/javascripts/issue_show/mixins/update.js b/app/assets/javascripts/issues/show/mixins/update.js
index 72be65b426f..72be65b426f 100644
--- a/app/assets/javascripts/issue_show/mixins/update.js
+++ b/app/assets/javascripts/issues/show/mixins/update.js
diff --git a/app/assets/javascripts/issue_show/queries/get_issue_state.query.graphql b/app/assets/javascripts/issues/show/queries/get_issue_state.query.graphql
index 33b737d2315..33b737d2315 100644
--- a/app/assets/javascripts/issue_show/queries/get_issue_state.query.graphql
+++ b/app/assets/javascripts/issues/show/queries/get_issue_state.query.graphql
diff --git a/app/assets/javascripts/issue_show/queries/promote_to_epic.mutation.graphql b/app/assets/javascripts/issues/show/queries/promote_to_epic.mutation.graphql
index 12d05af0f5e..e3e3a2bc667 100644
--- a/app/assets/javascripts/issue_show/queries/promote_to_epic.mutation.graphql
+++ b/app/assets/javascripts/issues/show/queries/promote_to_epic.mutation.graphql
@@ -1,6 +1,7 @@
mutation promoteToEpic($input: PromoteToEpicInput!) {
promoteToEpic(input: $input) {
epic {
+ id
webPath
}
errors
diff --git a/app/assets/javascripts/issue_show/queries/update_issue.mutation.graphql b/app/assets/javascripts/issues/show/queries/update_issue.mutation.graphql
index ec8d8f32d8b..ec8d8f32d8b 100644
--- a/app/assets/javascripts/issue_show/queries/update_issue.mutation.graphql
+++ b/app/assets/javascripts/issues/show/queries/update_issue.mutation.graphql
diff --git a/app/assets/javascripts/issue_show/queries/update_issue_state.mutation.graphql b/app/assets/javascripts/issues/show/queries/update_issue_state.mutation.graphql
index d91ca746066..d91ca746066 100644
--- a/app/assets/javascripts/issue_show/queries/update_issue_state.mutation.graphql
+++ b/app/assets/javascripts/issues/show/queries/update_issue_state.mutation.graphql
diff --git a/app/assets/javascripts/issue_show/services/index.js b/app/assets/javascripts/issues/show/services/index.js
index b1deeaae0fc..dba07f623f9 100644
--- a/app/assets/javascripts/issue_show/services/index.js
+++ b/app/assets/javascripts/issues/show/services/index.js
@@ -1,4 +1,4 @@
-import axios from '../../lib/utils/axios_utils';
+import axios from '~/lib/utils/axios_utils';
export default class Service {
constructor(endpoint) {
diff --git a/app/assets/javascripts/issue_show/stores/index.js b/app/assets/javascripts/issues/show/stores/index.js
index a50913d3455..a50913d3455 100644
--- a/app/assets/javascripts/issue_show/stores/index.js
+++ b/app/assets/javascripts/issues/show/stores/index.js
diff --git a/app/assets/javascripts/issue_show/utils/parse_data.js b/app/assets/javascripts/issues/show/utils/parse_data.js
index f1e6bd2419a..f1e6bd2419a 100644
--- a/app/assets/javascripts/issue_show/utils/parse_data.js
+++ b/app/assets/javascripts/issues/show/utils/parse_data.js
diff --git a/app/assets/javascripts/issue_show/utils/update_description.js b/app/assets/javascripts/issues/show/utils/update_description.js
index c5811290e61..c5811290e61 100644
--- a/app/assets/javascripts/issue_show/utils/update_description.js
+++ b/app/assets/javascripts/issues/show/utils/update_description.js
diff --git a/app/assets/javascripts/issues_list/components/issuable.vue b/app/assets/javascripts/issues_list/components/issuable.vue
index 6dc7460b037..6476d5be38c 100644
--- a/app/assets/javascripts/issues_list/components/issuable.vue
+++ b/app/assets/javascripts/issues_list/components/issuable.vue
@@ -28,7 +28,7 @@ import { convertToCamelCase } from '~/lib/utils/text_utility';
import { mergeUrlParams, setUrlFragment, isExternal } from '~/lib/utils/url_utility';
import { sprintf, __ } from '~/locale';
import initUserPopovers from '~/user_popovers';
-import IssueAssignees from '~/vue_shared/components/issue/issue_assignees.vue';
+import IssueAssignees from '~/issuable/components/issue_assignees.vue';
export default {
i18n: {
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 62b52afdaca..71136bf0159 100644
--- a/app/assets/javascripts/issues_list/components/issuables_list_app.vue
+++ b/app/assets/javascripts/issues_list/components/issuables_list_app.vue
@@ -11,7 +11,7 @@ import axios from '~/lib/utils/axios_utils';
import { scrollToElement, historyPushState } from '~/lib/utils/common_utils';
import { setUrlParams, queryToObject, getParameterByName } from '~/lib/utils/url_utility';
import { __ } from '~/locale';
-import initManualOrdering from '~/manual_ordering';
+import initManualOrdering from '~/issues/manual_ordering';
import FilteredSearchBar from '~/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue';
import {
sortOrderMap,
@@ -21,12 +21,12 @@ import {
PAGE_SIZE_MANUAL,
LOADING_LIST_ITEMS_LENGTH,
} from '../constants';
-import issueableEventHub from '../eventhub';
+import issuableEventHub from '../eventhub';
import { emptyStateHelper } from '../service_desk_helper';
import Issuable from './issuable.vue';
/**
- * @deprecated Use app/assets/javascripts/issuable_list/components/issuable_list_root.vue instead
+ * @deprecated Use app/assets/javascripts/vue_shared/issuable/list/components/issuable_list_root.vue instead
*/
export default {
LOADING_LIST_ITEMS_LENGTH,
@@ -192,7 +192,7 @@ export default {
// We need to call nextTick here to wait for all of the boxes to be checked and rendered
// before we query the dom in issuable_bulk_update_actions.js.
this.$nextTick(() => {
- issueableEventHub.$emit('issuables:updateBulkEdit');
+ issuableEventHub.$emit('issuables:updateBulkEdit');
});
},
issuables() {
@@ -203,7 +203,7 @@ export default {
},
mounted() {
if (this.canBulkEdit) {
- this.unsubscribeToggleBulkEdit = issueableEventHub.$on('issuables:toggleBulkEdit', (val) => {
+ this.unsubscribeToggleBulkEdit = issuableEventHub.$on('issuables:toggleBulkEdit', (val) => {
this.isBulkEditing = val;
});
}
@@ -211,7 +211,7 @@ export default {
},
beforeDestroy() {
// eslint-disable-next-line @gitlab/no-global-event-off
- issueableEventHub.$off('issuables:toggleBulkEdit');
+ issuableEventHub.$off('issuables:toggleBulkEdit');
},
methods: {
isSelected(issuableId) {
diff --git a/app/assets/javascripts/issues_list/components/issue_card_time_info.vue b/app/assets/javascripts/issues_list/components/issue_card_time_info.vue
index 4a2f7861492..aece7372182 100644
--- a/app/assets/javascripts/issues_list/components/issue_card_time_info.vue
+++ b/app/assets/javascripts/issues_list/components/issue_card_time_info.vue
@@ -7,25 +7,16 @@ import {
isInPast,
isToday,
} from '~/lib/utils/datetime_utility';
-import { convertToCamelCase } from '~/lib/utils/text_utility';
import { __ } from '~/locale';
export default {
components: {
GlLink,
GlIcon,
- IssueHealthStatus: () =>
- import('ee_component/related_items_tree/components/issue_health_status.vue'),
- WeightCount: () => import('ee_component/issues/components/weight_count.vue'),
},
directives: {
GlTooltip: GlTooltipDirective,
},
- inject: {
- hasIssuableHealthStatusFeature: {
- default: false,
- },
- },
props: {
issue: {
type: Object,
@@ -54,12 +45,6 @@ export default {
timeEstimate() {
return this.issue.humanTimeEstimate || this.issue.timeStats?.humanTimeEstimate;
},
- showHealthStatus() {
- return this.hasIssuableHealthStatusFeature && this.issue.healthStatus;
- },
- healthStatus() {
- return convertToCamelCase(this.issue.healthStatus);
- },
},
methods: {
milestoneRemainingTime(dueDate, startDate) {
@@ -114,7 +99,6 @@ export default {
<gl-icon name="timer" />
{{ timeEstimate }}
</span>
- <weight-count class="issuable-weight gl-mr-3" :weight="issue.weight" />
- <issue-health-status v-if="showHealthStatus" :health-status="healthStatus" />
+ <slot></slot>
</span>
</template>
diff --git a/app/assets/javascripts/issues_list/components/issues_list_app.vue b/app/assets/javascripts/issues_list/components/issues_list_app.vue
index 7f2082e5b90..6ced1080b71 100644
--- a/app/assets/javascripts/issues_list/components/issues_list_app.vue
+++ b/app/assets/javascripts/issues_list/components/issues_list_app.vue
@@ -8,17 +8,20 @@ import {
GlSprintf,
GlTooltipDirective,
} from '@gitlab/ui';
+import * as Sentry from '@sentry/browser';
import fuzzaldrinPlus from 'fuzzaldrin-plus';
+import { orderBy } from 'lodash';
import getIssuesQuery from 'ee_else_ce/issues_list/queries/get_issues.query.graphql';
import getIssuesCountsQuery from 'ee_else_ce/issues_list/queries/get_issues_counts.query.graphql';
-import createFlash from '~/flash';
+import IssueCardTimeInfo from 'ee_else_ce/issues_list/components/issue_card_time_info.vue';
+import createFlash, { FLASH_TYPES } from '~/flash';
import { TYPE_USER } from '~/graphql_shared/constants';
import { convertToGraphQLId, getIdFromGraphQLId } from '~/graphql_shared/utils';
import { ITEM_TYPE } from '~/groups/constants';
import CsvImportExportButtons from '~/issuable/components/csv_import_export_buttons.vue';
import IssuableByEmail from '~/issuable/components/issuable_by_email.vue';
-import IssuableList from '~/issuable_list/components/issuable_list_root.vue';
-import { IssuableListTabs, IssuableStates } from '~/issuable_list/constants';
+import IssuableList from '~/vue_shared/issuable/list/components/issuable_list_root.vue';
+import { IssuableListTabs, IssuableStates } from '~/vue_shared/issuable/list/constants';
import {
CREATED_DESC,
i18n,
@@ -31,14 +34,11 @@ import {
TOKEN_TYPE_ASSIGNEE,
TOKEN_TYPE_AUTHOR,
TOKEN_TYPE_CONFIDENTIAL,
- TOKEN_TYPE_EPIC,
- TOKEN_TYPE_ITERATION,
TOKEN_TYPE_LABEL,
TOKEN_TYPE_MILESTONE,
TOKEN_TYPE_MY_REACTION,
TOKEN_TYPE_RELEASE,
TOKEN_TYPE_TYPE,
- TOKEN_TYPE_WEIGHT,
UPDATED_DESC,
urlSortParams,
} from '~/issues_list/constants';
@@ -61,39 +61,29 @@ import {
TOKEN_TITLE_ASSIGNEE,
TOKEN_TITLE_AUTHOR,
TOKEN_TITLE_CONFIDENTIAL,
- TOKEN_TITLE_EPIC,
- TOKEN_TITLE_ITERATION,
TOKEN_TITLE_LABEL,
TOKEN_TITLE_MILESTONE,
TOKEN_TITLE_MY_REACTION,
TOKEN_TITLE_RELEASE,
TOKEN_TITLE_TYPE,
- TOKEN_TITLE_WEIGHT,
} from '~/vue_shared/components/filtered_search_bar/constants';
import eventHub from '../eventhub';
import reorderIssuesMutation from '../queries/reorder_issues.mutation.graphql';
-import searchIterationsQuery from '../queries/search_iterations.query.graphql';
import searchLabelsQuery from '../queries/search_labels.query.graphql';
import searchMilestonesQuery from '../queries/search_milestones.query.graphql';
import searchUsersQuery from '../queries/search_users.query.graphql';
-import IssueCardTimeInfo from './issue_card_time_info.vue';
import NewIssueDropdown from './new_issue_dropdown.vue';
const AuthorToken = () =>
import('~/vue_shared/components/filtered_search_bar/tokens/author_token.vue');
const EmojiToken = () =>
import('~/vue_shared/components/filtered_search_bar/tokens/emoji_token.vue');
-const EpicToken = () => import('~/vue_shared/components/filtered_search_bar/tokens/epic_token.vue');
-const IterationToken = () =>
- import('~/vue_shared/components/filtered_search_bar/tokens/iteration_token.vue');
const LabelToken = () =>
import('~/vue_shared/components/filtered_search_bar/tokens/label_token.vue');
const MilestoneToken = () =>
import('~/vue_shared/components/filtered_search_bar/tokens/milestone_token.vue');
const ReleaseToken = () =>
import('~/vue_shared/components/filtered_search_bar/tokens/release_token.vue');
-const WeightToken = () =>
- import('~/vue_shared/components/filtered_search_bar/tokens/weight_token.vue');
export default {
i18n,
@@ -109,7 +99,6 @@ export default {
IssuableList,
IssueCardTimeInfo,
NewIssueDropdown,
- BlockingIssuesCount: () => import('ee_component/issues/components/blocking_issues_count.vue'),
},
directives: {
GlTooltip: GlTooltipDirective,
@@ -133,9 +122,6 @@ export default {
fullPath: {
default: '',
},
- groupPath: {
- default: '',
- },
hasAnyIssues: {
default: false,
},
@@ -148,15 +134,18 @@ export default {
hasIssueWeightsFeature: {
default: false,
},
- hasIterationsFeature: {
- default: false,
- },
hasMultipleIssueAssigneesFeature: {
default: false,
},
initialEmail: {
default: '',
},
+ isAnonymousSearchDisabled: {
+ default: false,
+ },
+ isIssueRepositioningDisabled: {
+ default: false,
+ },
isProject: {
default: false,
},
@@ -182,21 +171,43 @@ export default {
default: '',
},
},
+ props: {
+ eeSearchTokens: {
+ type: Array,
+ required: false,
+ default: () => [],
+ },
+ },
data() {
const state = getParameterByName(PARAM_STATE);
- const sortKey = getSortKey(getParameterByName(PARAM_SORT));
const defaultSortKey = state === IssuableStates.Closed ? UPDATED_DESC : CREATED_DESC;
+ let sortKey = getSortKey(getParameterByName(PARAM_SORT)) || defaultSortKey;
+
+ if (this.isIssueRepositioningDisabled && sortKey === RELATIVE_POSITION_ASC) {
+ this.showIssueRepositioningMessage();
+ sortKey = defaultSortKey;
+ }
+
+ const isSearchDisabled =
+ this.isAnonymousSearchDisabled &&
+ !this.isSignedIn &&
+ window.location.search.includes('search=');
+
+ if (isSearchDisabled) {
+ this.showAnonymousSearchingMessage();
+ }
return {
dueDateFilter: getDueDateValue(getParameterByName(PARAM_DUE_DATE)),
exportCsvPathWithQuery: this.getExportCsvPathWithQuery(),
- filterTokens: getFilterTokens(window.location.search),
+ filterTokens: isSearchDisabled ? [] : getFilterTokens(window.location.search),
issues: [],
issuesCounts: {},
+ issuesError: null,
pageInfo: {},
pageParams: getInitialPageParams(sortKey),
showBulkEditSidebar: false,
- sortKey: sortKey || defaultSortKey,
+ sortKey,
state: state || IssuableStates.Opened,
};
},
@@ -214,7 +225,8 @@ export default {
this.exportCsvPathWithQuery = this.getExportCsvPathWithQuery();
},
error(error) {
- createFlash({ message: this.$options.i18n.errorFetchingIssues, captureError: true, error });
+ this.issuesError = this.$options.i18n.errorFetchingIssues;
+ Sentry.captureException(error);
},
skip() {
return !this.hasAnyIssues;
@@ -230,7 +242,8 @@ export default {
return data[this.namespace] ?? {};
},
error(error) {
- createFlash({ message: this.$options.i18n.errorFetchingCounts, captureError: true, error });
+ this.issuesError = this.$options.i18n.errorFetchingCounts;
+ Sentry.captureException(error);
},
skip() {
return !this.hasAnyIssues;
@@ -306,6 +319,7 @@ export default {
unique: true,
defaultAuthors: [],
fetchAuthors: this.fetchUsers,
+ recentSuggestionsStorageKey: `${this.fullPath}-issues-recent-tokens-author`,
preloadedAuthors,
},
{
@@ -317,6 +331,7 @@ export default {
unique: !this.hasMultipleIssueAssigneesFeature,
defaultAuthors: DEFAULT_NONE_ANY,
fetchAuthors: this.fetchUsers,
+ recentSuggestionsStorageKey: `${this.fullPath}-issues-recent-tokens-assignee`,
preloadedAuthors,
},
{
@@ -325,6 +340,7 @@ export default {
icon: 'clock',
token: MilestoneToken,
fetchMilestones: this.fetchMilestones,
+ recentSuggestionsStorageKey: `${this.fullPath}-issues-recent-tokens-milestone`,
},
{
type: TOKEN_TYPE_LABEL,
@@ -333,6 +349,7 @@ export default {
token: LabelToken,
defaultLabels: DEFAULT_NONE_ANY,
fetchLabels: this.fetchLabels,
+ recentSuggestionsStorageKey: `${this.fullPath}-issues-recent-tokens-label`,
},
{
type: TOKEN_TYPE_TYPE,
@@ -354,6 +371,7 @@ export default {
icon: 'rocket',
token: ReleaseToken,
fetchReleases: this.fetchReleases,
+ recentSuggestionsStorageKey: `${this.fullPath}-issues-recent-tokens-release`,
});
}
@@ -365,6 +383,7 @@ export default {
token: EmojiToken,
unique: true,
fetchEmojis: this.fetchEmojis,
+ recentSuggestionsStorageKey: `${this.fullPath}-issues-recent-tokens-my_reaction`,
});
tokens.push({
@@ -381,42 +400,13 @@ export default {
});
}
- if (this.hasIterationsFeature) {
- tokens.push({
- type: TOKEN_TYPE_ITERATION,
- title: TOKEN_TITLE_ITERATION,
- icon: 'iteration',
- token: IterationToken,
- fetchIterations: this.fetchIterations,
- });
+ if (this.eeSearchTokens.length) {
+ tokens.push(...this.eeSearchTokens);
}
- if (this.groupPath) {
- tokens.push({
- type: TOKEN_TYPE_EPIC,
- title: TOKEN_TITLE_EPIC,
- icon: 'epic',
- token: EpicToken,
- unique: true,
- symbol: '&',
- idProperty: 'id',
- useIdValue: true,
- recentSuggestionsStorageKey: `${this.fullPath}-issues-recent-tokens-epic_id`,
- fullPath: this.groupPath,
- });
- }
+ tokens.sort((a, b) => a.title.localeCompare(b.title));
- if (this.hasIssueWeightsFeature) {
- tokens.push({
- type: TOKEN_TYPE_WEIGHT,
- title: TOKEN_TITLE_WEIGHT,
- icon: 'weight',
- token: WeightToken,
- unique: true,
- });
- }
-
- return tokens;
+ return orderBy(tokens, ['title']);
},
showPaginationControls() {
return this.issues.length > 0 && (this.pageInfo.hasNextPage || this.pageInfo.hasPreviousPage);
@@ -481,7 +471,12 @@ export default {
query: searchLabelsQuery,
variables: { fullPath: this.fullPath, search, isProject: this.isProject },
})
- .then(({ data }) => data[this.namespace]?.labels.nodes);
+ .then(({ data }) => data[this.namespace]?.labels.nodes)
+ .then((labels) =>
+ // TODO remove once we can search by title-only on the backend
+ // https://gitlab.com/gitlab-org/gitlab/-/issues/346353
+ labels.filter((label) => label.title.toLowerCase().includes(search.toLowerCase())),
+ );
},
fetchMilestones(search) {
return this.$apollo
@@ -491,20 +486,6 @@ export default {
})
.then(({ data }) => data[this.namespace]?.milestones.nodes);
},
- fetchIterations(search) {
- const id = Number(search);
- const variables =
- !search || Number.isNaN(id)
- ? { fullPath: this.fullPath, search, isProject: this.isProject }
- : { fullPath: this.fullPath, id, isProject: this.isProject };
-
- return this.$apollo
- .query({
- query: searchIterationsQuery,
- variables,
- })
- .then(({ data }) => data[this.namespace]?.iterations.nodes);
- },
fetchUsers(search) {
return this.$apollo
.query({
@@ -537,7 +518,7 @@ export default {
async handleBulkUpdateClick() {
if (!this.hasInitBulkEdit) {
const initBulkUpdateSidebar = await import(
- '~/issuable_bulk_update_sidebar/issuable_init_bulk_update_sidebar'
+ '~/issuable/bulk_update_sidebar/issuable_init_bulk_update_sidebar'
);
initBulkUpdateSidebar.default.init('issuable_');
@@ -556,7 +537,14 @@ export default {
}
this.state = state;
},
+ handleDismissAlert() {
+ this.issuesError = null;
+ },
handleFilter(filter) {
+ if (this.isAnonymousSearchDisabled && !this.isSignedIn) {
+ this.showAnonymousSearchingMessage();
+ return;
+ }
this.pageParams = getInitialPageParams(this.sortKey);
this.filterTokens = filter;
},
@@ -607,15 +595,33 @@ export default {
});
})
.catch((error) => {
- createFlash({ message: this.$options.i18n.reorderError, captureError: true, error });
+ this.issuesError = this.$options.i18n.reorderError;
+ Sentry.captureException(error);
});
},
handleSort(sortKey) {
+ if (this.isIssueRepositioningDisabled && sortKey === RELATIVE_POSITION_ASC) {
+ this.showIssueRepositioningMessage();
+ return;
+ }
+
if (this.sortKey !== sortKey) {
this.pageParams = getInitialPageParams(sortKey);
}
this.sortKey = sortKey;
},
+ showAnonymousSearchingMessage() {
+ createFlash({
+ message: this.$options.i18n.anonymousSearchingMessage,
+ type: FLASH_TYPES.NOTICE,
+ });
+ },
+ showIssueRepositioningMessage() {
+ createFlash({
+ message: this.$options.i18n.issueRepositioningMessage,
+ type: FLASH_TYPES.NOTICE,
+ });
+ },
toggleBulkEditSidebar(showBulkEditSidebar) {
this.showBulkEditSidebar = showBulkEditSidebar;
},
@@ -634,6 +640,7 @@ export default {
:sort-options="sortOptions"
:initial-sort-by="sortKey"
:issuables="issues"
+ :error="issuesError"
label-filter-param="label_name"
:tabs="$options.IssuableListTabs"
:current-tab="state"
@@ -647,6 +654,7 @@ export default {
:has-previous-page="pageInfo.hasPreviousPage"
:url-params="urlParams"
@click-tab="handleClickTab"
+ @dismiss-alert="handleDismissAlert"
@filter="handleFilter"
@next-page="handleNextPage"
@previous-page="handlePreviousPage"
@@ -727,12 +735,7 @@ export default {
<gl-icon name="thumb-down" />
{{ issuable.downvotes }}
</li>
- <blocking-issues-count
- class="blocking-issues gl-display-none gl-sm-display-block"
- :blocking-issues-count="issuable.blockingCount"
- :is-list-item="true"
- data-testid="blocking-issues"
- />
+ <slot :issuable="issuable"></slot>
</template>
<template #empty-state>
diff --git a/app/assets/javascripts/issues_list/constants.js b/app/assets/javascripts/issues_list/constants.js
index da9b96d0e22..c9eaf0b9908 100644
--- a/app/assets/javascripts/issues_list/constants.js
+++ b/app/assets/javascripts/issues_list/constants.js
@@ -66,6 +66,7 @@ export const availableSortOptionsJira = [
];
export const i18n = {
+ anonymousSearchingMessage: __('You must sign in to search for specific terms.'),
calendarLabel: __('Subscribe to calendar'),
closed: __('CLOSED'),
closedMoved: __('CLOSED (MOVED)'),
@@ -75,6 +76,9 @@ export const i18n = {
editIssues: __('Edit issues'),
errorFetchingCounts: __('An error occurred while getting issue counts'),
errorFetchingIssues: __('An error occurred while loading issues'),
+ issueRepositioningMessage: __(
+ 'Issues are being rebalanced at the moment, so manual reordering is disabled.',
+ ),
jiraIntegrationMessage: s__(
'JiraService|%{jiraDocsLinkStart}Enable the Jira integration%{jiraDocsLinkEnd} to view your Jira issues in GitLab.',
),
@@ -133,6 +137,7 @@ export const DUE_DATE_VALUES = [
DUE_DATE_NEXT_MONTH_AND_PREVIOUS_TWO_WEEKS,
];
+export const BLOCKING_ISSUES_ASC = 'BLOCKING_ISSUES_ASC';
export const BLOCKING_ISSUES_DESC = 'BLOCKING_ISSUES_DESC';
export const CREATED_ASC = 'CREATED_ASC';
export const CREATED_DESC = 'CREATED_DESC';
@@ -154,42 +159,28 @@ export const UPDATED_DESC = 'UPDATED_DESC';
export const WEIGHT_ASC = 'WEIGHT_ASC';
export const WEIGHT_DESC = 'WEIGHT_DESC';
-const PRIORITY_ASC_SORT = 'priority_asc';
-const CREATED_DATE_SORT = 'created_date';
-const CREATED_ASC_SORT = 'created_asc';
-const UPDATED_DESC_SORT = 'updated_desc';
-const UPDATED_ASC_SORT = 'updated_asc';
-const MILESTONE_SORT = 'milestone';
-const MILESTONE_DUE_DESC_SORT = 'milestone_due_desc';
-const DUE_DATE_DESC_SORT = 'due_date_desc';
-const LABEL_PRIORITY_ASC_SORT = 'label_priority_asc';
-const POPULARITY_ASC_SORT = 'popularity_asc';
-const WEIGHT_DESC_SORT = 'weight_desc';
-const BLOCKING_ISSUES_DESC_SORT = 'blocking_issues_desc';
-const TITLE_ASC_SORT = 'title_asc';
-const TITLE_DESC_SORT = 'title_desc';
-
export const urlSortParams = {
- [PRIORITY_ASC]: PRIORITY_ASC_SORT,
- [PRIORITY_DESC]: PRIORITY,
- [CREATED_ASC]: CREATED_ASC_SORT,
- [CREATED_DESC]: CREATED_DATE_SORT,
- [UPDATED_ASC]: UPDATED_ASC_SORT,
- [UPDATED_DESC]: UPDATED_DESC_SORT,
- [MILESTONE_DUE_ASC]: MILESTONE_SORT,
- [MILESTONE_DUE_DESC]: MILESTONE_DUE_DESC_SORT,
- [DUE_DATE_ASC]: DUE_DATE,
- [DUE_DATE_DESC]: DUE_DATE_DESC_SORT,
- [POPULARITY_ASC]: POPULARITY_ASC_SORT,
- [POPULARITY_DESC]: POPULARITY,
- [LABEL_PRIORITY_ASC]: LABEL_PRIORITY_ASC_SORT,
- [LABEL_PRIORITY_DESC]: LABEL_PRIORITY,
+ [PRIORITY_ASC]: 'priority',
+ [PRIORITY_DESC]: 'priority_desc',
+ [CREATED_ASC]: 'created_asc',
+ [CREATED_DESC]: 'created_date',
+ [UPDATED_ASC]: 'updated_asc',
+ [UPDATED_DESC]: 'updated_desc',
+ [MILESTONE_DUE_ASC]: 'milestone',
+ [MILESTONE_DUE_DESC]: 'milestone_due_desc',
+ [DUE_DATE_ASC]: 'due_date',
+ [DUE_DATE_DESC]: 'due_date_desc',
+ [POPULARITY_ASC]: 'popularity_asc',
+ [POPULARITY_DESC]: 'popularity',
+ [LABEL_PRIORITY_ASC]: 'label_priority',
+ [LABEL_PRIORITY_DESC]: 'label_priority_desc',
[RELATIVE_POSITION_ASC]: RELATIVE_POSITION,
- [WEIGHT_ASC]: WEIGHT,
- [WEIGHT_DESC]: WEIGHT_DESC_SORT,
- [BLOCKING_ISSUES_DESC]: BLOCKING_ISSUES_DESC_SORT,
- [TITLE_ASC]: TITLE_ASC_SORT,
- [TITLE_DESC]: TITLE_DESC_SORT,
+ [WEIGHT_ASC]: 'weight',
+ [WEIGHT_DESC]: 'weight_desc',
+ [BLOCKING_ISSUES_ASC]: 'blocking_issues_asc',
+ [BLOCKING_ISSUES_DESC]: 'blocking_issues_desc',
+ [TITLE_ASC]: 'title_asc',
+ [TITLE_DESC]: 'title_desc',
};
export const MAX_LIST_SIZE = 10;
diff --git a/app/assets/javascripts/issues_list/index.js b/app/assets/javascripts/issues_list/index.js
index 59034964afb..9d2ec8b32d2 100644
--- a/app/assets/javascripts/issues_list/index.js
+++ b/app/assets/javascripts/issues_list/index.js
@@ -2,7 +2,7 @@ import produce from 'immer';
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import getIssuesQuery from 'ee_else_ce/issues_list/queries/get_issues.query.graphql';
-import IssuesListApp from '~/issues_list/components/issues_list_app.vue';
+import IssuesListApp from 'ee_else_ce/issues_list/components/issues_list_app.vue';
import createDefaultClient from '~/lib/graphql';
import { convertObjectPropsToCamelCase, parseBoolean } from '~/lib/utils/common_utils';
import IssuablesListApp from './components/issuables_list_app.vue';
@@ -129,6 +129,8 @@ export function mountIssuesListApp() {
hasMultipleIssueAssigneesFeature,
importCsvIssuesPath,
initialEmail,
+ isAnonymousSearchDisabled,
+ isIssueRepositioningDisabled,
isProject,
isSignedIn,
jiraIntegrationPath,
@@ -161,6 +163,8 @@ export function mountIssuesListApp() {
hasIssueWeightsFeature: parseBoolean(hasIssueWeightsFeature),
hasIterationsFeature: parseBoolean(hasIterationsFeature),
hasMultipleIssueAssigneesFeature: parseBoolean(hasMultipleIssueAssigneesFeature),
+ isAnonymousSearchDisabled: parseBoolean(isAnonymousSearchDisabled),
+ isIssueRepositioningDisabled: parseBoolean(isIssueRepositioningDisabled),
isProject: parseBoolean(isProject),
isSignedIn: parseBoolean(isSignedIn),
jiraIntegrationPath,
diff --git a/app/assets/javascripts/issues_list/queries/get_issues.query.graphql b/app/assets/javascripts/issues_list/queries/get_issues.query.graphql
index 9866efbcecc..be8deb3fe97 100644
--- a/app/assets/javascripts/issues_list/queries/get_issues.query.graphql
+++ b/app/assets/javascripts/issues_list/queries/get_issues.query.graphql
@@ -26,6 +26,7 @@ query getIssues(
$lastPageSize: Int
) {
group(fullPath: $fullPath) @skip(if: $isProject) {
+ id
issues(
includeSubgroups: true
search: $search
@@ -56,6 +57,7 @@ query getIssues(
}
}
project(fullPath: $fullPath) @include(if: $isProject) {
+ id
issues(
search: $search
sort: $sort
diff --git a/app/assets/javascripts/issues_list/queries/get_issues_counts.query.graphql b/app/assets/javascripts/issues_list/queries/get_issues_counts.query.graphql
index 5e755ec5870..1a345fd2877 100644
--- a/app/assets/javascripts/issues_list/queries/get_issues_counts.query.graphql
+++ b/app/assets/javascripts/issues_list/queries/get_issues_counts.query.graphql
@@ -16,6 +16,7 @@ query getIssuesCount(
$not: NegatedIssueFilterInput
) {
group(fullPath: $fullPath) @skip(if: $isProject) {
+ id
openedIssues: issues(
includeSubgroups: true
state: opened
@@ -69,6 +70,7 @@ query getIssuesCount(
}
}
project(fullPath: $fullPath) @include(if: $isProject) {
+ id
openedIssues: issues(
state: opened
search: $search
diff --git a/app/assets/javascripts/issues_list/queries/get_issues_list_details.query.graphql b/app/assets/javascripts/issues_list/queries/get_issues_list_details.query.graphql
index 8c95e6114d3..a53dba8c7c8 100644
--- a/app/assets/javascripts/issues_list/queries/get_issues_list_details.query.graphql
+++ b/app/assets/javascripts/issues_list/queries/get_issues_list_details.query.graphql
@@ -1,9 +1,12 @@
query getIssuesListDetails($fullPath: ID!) {
project(fullPath: $fullPath) {
+ id
issues {
nodes {
+ id
labels {
nodes {
+ id
title
color
}
diff --git a/app/assets/javascripts/issues_list/queries/issue.fragment.graphql b/app/assets/javascripts/issues_list/queries/issue.fragment.graphql
index 9c46cb3ef64..07dae3fd756 100644
--- a/app/assets/javascripts/issues_list/queries/issue.fragment.graphql
+++ b/app/assets/javascripts/issues_list/queries/issue.fragment.graphql
@@ -6,6 +6,7 @@ fragment IssueFragment on Issue {
createdAt
downvotes
dueDate
+ hidden
humanTimeEstimate
mergeRequestsCount
moved
diff --git a/app/assets/javascripts/issues_list/queries/iteration.fragment.graphql b/app/assets/javascripts/issues_list/queries/iteration.fragment.graphql
deleted file mode 100644
index 4f7217be7f7..00000000000
--- a/app/assets/javascripts/issues_list/queries/iteration.fragment.graphql
+++ /dev/null
@@ -1,10 +0,0 @@
-fragment Iteration on Iteration {
- id
- title
- startDate
- dueDate
- iterationCadence {
- id
- title
- }
-}
diff --git a/app/assets/javascripts/issues_list/queries/search_iterations.query.graphql b/app/assets/javascripts/issues_list/queries/search_iterations.query.graphql
deleted file mode 100644
index 93600c62905..00000000000
--- a/app/assets/javascripts/issues_list/queries/search_iterations.query.graphql
+++ /dev/null
@@ -1,18 +0,0 @@
-#import "./iteration.fragment.graphql"
-
-query searchIterations($fullPath: ID!, $search: String, $id: ID, $isProject: Boolean = false) {
- group(fullPath: $fullPath) @skip(if: $isProject) {
- iterations(title: $search, id: $id, includeAncestors: true) {
- nodes {
- ...Iteration
- }
- }
- }
- project(fullPath: $fullPath) @include(if: $isProject) {
- iterations(title: $search, id: $id, includeAncestors: true) {
- nodes {
- ...Iteration
- }
- }
- }
-}
diff --git a/app/assets/javascripts/issues_list/queries/search_labels.query.graphql b/app/assets/javascripts/issues_list/queries/search_labels.query.graphql
index 1515bd91da3..44b57317161 100644
--- a/app/assets/javascripts/issues_list/queries/search_labels.query.graphql
+++ b/app/assets/javascripts/issues_list/queries/search_labels.query.graphql
@@ -2,6 +2,7 @@
query searchLabels($fullPath: ID!, $search: String, $isProject: Boolean = false) {
group(fullPath: $fullPath) @skip(if: $isProject) {
+ id
labels(searchTerm: $search, includeAncestorGroups: true, includeDescendantGroups: true) {
nodes {
...Label
@@ -9,6 +10,7 @@ query searchLabels($fullPath: ID!, $search: String, $isProject: Boolean = false)
}
}
project(fullPath: $fullPath) @include(if: $isProject) {
+ id
labels(searchTerm: $search, includeAncestorGroups: true) {
nodes {
...Label
diff --git a/app/assets/javascripts/issues_list/queries/search_milestones.query.graphql b/app/assets/javascripts/issues_list/queries/search_milestones.query.graphql
index 8c6c50e9dc2..e7eb08104a6 100644
--- a/app/assets/javascripts/issues_list/queries/search_milestones.query.graphql
+++ b/app/assets/javascripts/issues_list/queries/search_milestones.query.graphql
@@ -2,6 +2,7 @@
query searchMilestones($fullPath: ID!, $search: String, $isProject: Boolean = false) {
group(fullPath: $fullPath) @skip(if: $isProject) {
+ id
milestones(searchTitle: $search, includeAncestors: true, includeDescendants: true) {
nodes {
...Milestone
@@ -9,6 +10,7 @@ query searchMilestones($fullPath: ID!, $search: String, $isProject: Boolean = fa
}
}
project(fullPath: $fullPath) @include(if: $isProject) {
+ id
milestones(searchTitle: $search, includeAncestors: true) {
nodes {
...Milestone
diff --git a/app/assets/javascripts/issues_list/queries/search_projects.query.graphql b/app/assets/javascripts/issues_list/queries/search_projects.query.graphql
index 75463f643a2..bd2f9bc2340 100644
--- a/app/assets/javascripts/issues_list/queries/search_projects.query.graphql
+++ b/app/assets/javascripts/issues_list/queries/search_projects.query.graphql
@@ -1,5 +1,6 @@
query searchProjects($fullPath: ID!, $search: String) {
group(fullPath: $fullPath) {
+ id
projects(search: $search, includeSubgroups: true) {
nodes {
id
diff --git a/app/assets/javascripts/issues_list/queries/search_users.query.graphql b/app/assets/javascripts/issues_list/queries/search_users.query.graphql
index 0211fc66235..92517ad35d0 100644
--- a/app/assets/javascripts/issues_list/queries/search_users.query.graphql
+++ b/app/assets/javascripts/issues_list/queries/search_users.query.graphql
@@ -2,8 +2,10 @@
query searchUsers($fullPath: ID!, $search: String, $isProject: Boolean = false) {
group(fullPath: $fullPath) @skip(if: $isProject) {
+ id
groupMembers(search: $search) {
nodes {
+ id
user {
...User
}
@@ -11,8 +13,10 @@ query searchUsers($fullPath: ID!, $search: String, $isProject: Boolean = false)
}
}
project(fullPath: $fullPath) @include(if: $isProject) {
+ id
projectMembers(search: $search) {
nodes {
+ id
user {
...User
}
diff --git a/app/assets/javascripts/issues_list/utils.js b/app/assets/javascripts/issues_list/utils.js
index 0e57e2bff83..99946e4e851 100644
--- a/app/assets/javascripts/issues_list/utils.js
+++ b/app/assets/javascripts/issues_list/utils.js
@@ -1,5 +1,6 @@
import {
API_PARAM,
+ BLOCKING_ISSUES_ASC,
BLOCKING_ISSUES_DESC,
CREATED_ASC,
CREATED_DESC,
@@ -143,7 +144,7 @@ export const getSortOptions = (hasIssueWeightsFeature, hasBlockedIssuesFeature)
id: sortOptions.length + 1,
title: __('Blocking'),
sortDirection: {
- ascending: BLOCKING_ISSUES_DESC,
+ ascending: BLOCKING_ISSUES_ASC,
descending: BLOCKING_ISSUES_DESC,
},
});
diff --git a/app/assets/javascripts/jira_connect/branches/graphql/queries/get_project.query.graphql b/app/assets/javascripts/jira_connect/branches/graphql/queries/get_project.query.graphql
index f3428e816d7..df72a1ca6e6 100644
--- a/app/assets/javascripts/jira_connect/branches/graphql/queries/get_project.query.graphql
+++ b/app/assets/javascripts/jira_connect/branches/graphql/queries/get_project.query.graphql
@@ -5,6 +5,7 @@ query getProject(
$branchNamesSearchPattern: String!
) {
project(fullPath: $projectPath) {
+ id
repository {
branchNames(
limit: $branchNamesLimit
diff --git a/app/assets/javascripts/jira_connect/branches/index.js b/app/assets/javascripts/jira_connect/branches/index.js
index 04510fcff4b..a9a56a6362e 100644
--- a/app/assets/javascripts/jira_connect/branches/index.js
+++ b/app/assets/javascripts/jira_connect/branches/index.js
@@ -5,7 +5,7 @@ import createDefaultClient from '~/lib/graphql';
Vue.use(VueApollo);
-export default async function initJiraConnectBranches() {
+export default function initJiraConnectBranches() {
const el = document.querySelector('.js-jira-connect-create-branch');
if (!el) {
return null;
diff --git a/app/assets/javascripts/jira_connect/subscriptions/components/app.vue b/app/assets/javascripts/jira_connect/subscriptions/components/app.vue
index c0504cbb645..7fd4cc38f11 100644
--- a/app/assets/javascripts/jira_connect/subscriptions/components/app.vue
+++ b/app/assets/javascripts/jira_connect/subscriptions/components/app.vue
@@ -7,6 +7,7 @@ import { SET_ALERT } from '../store/mutation_types';
import SubscriptionsList from './subscriptions_list.vue';
import AddNamespaceButton from './add_namespace_button.vue';
import SignInButton from './sign_in_button.vue';
+import UserLink from './user_link.vue';
export default {
name: 'JiraConnectApp',
@@ -18,6 +19,7 @@ export default {
SubscriptionsList,
AddNamespaceButton,
SignInButton,
+ UserLink,
},
inject: {
usersPath: {
@@ -74,6 +76,8 @@ export default {
</template>
</gl-alert>
+ <user-link :user-signed-in="userSignedIn" :has-subscriptions="hasSubscriptions" />
+
<h2 class="gl-text-center gl-mb-7">{{ s__('JiraService|GitLab for Jira Configuration') }}</h2>
<div class="jira-connect-app-body gl-mx-auto gl-px-5 gl-mb-7">
<template v-if="hasSubscriptions">
diff --git a/app/assets/javascripts/jira_connect/subscriptions/components/user_link.vue b/app/assets/javascripts/jira_connect/subscriptions/components/user_link.vue
new file mode 100644
index 00000000000..fad3d2616d8
--- /dev/null
+++ b/app/assets/javascripts/jira_connect/subscriptions/components/user_link.vue
@@ -0,0 +1,67 @@
+<script>
+import { GlLink, GlSprintf } from '@gitlab/ui';
+import { __ } from '~/locale';
+import { getGitlabSignInURL } from '~/jira_connect/subscriptions/utils';
+
+export default {
+ components: {
+ GlLink,
+ GlSprintf,
+ },
+ inject: {
+ usersPath: {
+ default: '',
+ },
+ gitlabUserPath: {
+ default: '',
+ },
+ },
+ props: {
+ userSignedIn: {
+ type: Boolean,
+ required: true,
+ },
+ hasSubscriptions: {
+ type: Boolean,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ signInURL: '',
+ };
+ },
+ computed: {
+ gitlabUserHandle() {
+ return `@${gon.current_username}`;
+ },
+ },
+ async created() {
+ this.signInURL = await getGitlabSignInURL(this.usersPath);
+ },
+ i18n: {
+ signInText: __('Sign in to GitLab'),
+ signedInAsUserText: __('Signed in to GitLab as %{user_link}'),
+ },
+};
+</script>
+<template>
+ <div class="jira-connect-user gl-font-base">
+ <gl-sprintf v-if="userSignedIn" :message="$options.i18n.signedInAsUserText">
+ <template #user_link>
+ <gl-link data-testid="gitlab-user-link" :href="gitlabUserPath" target="_blank">
+ {{ gitlabUserHandle }}
+ </gl-link>
+ </template>
+ </gl-sprintf>
+
+ <gl-link
+ v-else-if="hasSubscriptions"
+ data-testid="sign-in-link"
+ :href="signInURL"
+ target="_blank"
+ >
+ {{ $options.i18n.signInText }}
+ </gl-link>
+ </div>
+</template>
diff --git a/app/assets/javascripts/jira_connect/subscriptions/index.js b/app/assets/javascripts/jira_connect/subscriptions/index.js
index 8a7a80d885d..cd1fc1d4455 100644
--- a/app/assets/javascripts/jira_connect/subscriptions/index.js
+++ b/app/assets/javascripts/jira_connect/subscriptions/index.js
@@ -7,25 +7,11 @@ import Translate from '~/vue_shared/translate';
import JiraConnectApp from './components/app.vue';
import createStore from './store';
-import { getGitlabSignInURL, sizeToParent } from './utils';
+import { sizeToParent } from './utils';
const store = createStore();
-/**
- * Add `return_to` query param to all HAML-defined GitLab sign in links.
- */
-const updateSignInLinks = async () => {
- await Promise.all(
- Array.from(document.querySelectorAll('.js-jira-connect-sign-in')).map(async (el) => {
- const updatedLink = await getGitlabSignInURL(el.getAttribute('href'));
- el.setAttribute('href', updatedLink);
- }),
- );
-};
-
-export async function initJiraConnect() {
- await updateSignInLinks();
-
+export function initJiraConnect() {
const el = document.querySelector('.js-jira-connect-app');
if (!el) {
return null;
@@ -35,7 +21,7 @@ export async function initJiraConnect() {
Vue.use(Translate);
Vue.use(GlFeatureFlagsPlugin);
- const { groupsPath, subscriptions, subscriptionsPath, usersPath } = el.dataset;
+ const { groupsPath, subscriptions, subscriptionsPath, usersPath, gitlabUserPath } = el.dataset;
sizeToParent();
return new Vue({
@@ -46,6 +32,7 @@ export async function initJiraConnect() {
subscriptions: JSON.parse(subscriptions),
subscriptionsPath,
usersPath,
+ gitlabUserPath,
},
render(createElement) {
return createElement(JiraConnectApp);
diff --git a/app/assets/javascripts/jira_import/queries/get_jira_import_details.query.graphql b/app/assets/javascripts/jira_import/queries/get_jira_import_details.query.graphql
index 6fec07cc6f8..4c26399e16b 100644
--- a/app/assets/javascripts/jira_import/queries/get_jira_import_details.query.graphql
+++ b/app/assets/javascripts/jira_import/queries/get_jira_import_details.query.graphql
@@ -2,6 +2,7 @@
query getJiraImportDetails($fullPath: ID!) {
project(fullPath: $fullPath) {
+ id
jiraImportStatus
jiraImports {
nodes {
diff --git a/app/assets/javascripts/jira_import/queries/jira_import.fragment.graphql b/app/assets/javascripts/jira_import/queries/jira_import.fragment.graphql
index fde2ebeff91..fe797879d07 100644
--- a/app/assets/javascripts/jira_import/queries/jira_import.fragment.graphql
+++ b/app/assets/javascripts/jira_import/queries/jira_import.fragment.graphql
@@ -2,6 +2,7 @@ fragment JiraImport on JiraImport {
jiraProjectKey
scheduledAt
scheduledBy {
+ id
name
}
}
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
index 6ea8963e6a6..7666fa3bd97 100644
--- a/app/assets/javascripts/jira_import/queries/search_project_members.query.graphql
+++ b/app/assets/javascripts/jira_import/queries/search_project_members.query.graphql
@@ -1,7 +1,9 @@
query jiraSearchProjectMembers($fullPath: ID!, $search: String) {
project(fullPath: $fullPath) {
+ id
projectMembers(search: $search) {
nodes {
+ id
user {
id
name
diff --git a/app/assets/javascripts/jobs/bridge/app.vue b/app/assets/javascripts/jobs/bridge/app.vue
new file mode 100644
index 00000000000..67c22712776
--- /dev/null
+++ b/app/assets/javascripts/jobs/bridge/app.vue
@@ -0,0 +1,20 @@
+<script>
+import BridgeEmptyState from './components/empty_state.vue';
+import BridgeSidebar from './components/sidebar.vue';
+
+export default {
+ name: 'BridgePageApp',
+ components: {
+ BridgeEmptyState,
+ BridgeSidebar,
+ },
+};
+</script>
+<template>
+ <div>
+ <!-- TODO: get job details and show CI header -->
+ <!-- TODO: add downstream pipeline path -->
+ <bridge-empty-state downstream-pipeline-path="#" />
+ <bridge-sidebar />
+ </div>
+</template>
diff --git a/app/assets/javascripts/jobs/bridge/components/constants.js b/app/assets/javascripts/jobs/bridge/components/constants.js
new file mode 100644
index 00000000000..33310b3157a
--- /dev/null
+++ b/app/assets/javascripts/jobs/bridge/components/constants.js
@@ -0,0 +1 @@
+export const SIDEBAR_COLLAPSE_BREAKPOINTS = ['xs', 'sm'];
diff --git a/app/assets/javascripts/jobs/bridge/components/empty_state.vue b/app/assets/javascripts/jobs/bridge/components/empty_state.vue
new file mode 100644
index 00000000000..bd07d863719
--- /dev/null
+++ b/app/assets/javascripts/jobs/bridge/components/empty_state.vue
@@ -0,0 +1,45 @@
+<script>
+import { GlButton } from '@gitlab/ui';
+import { __ } from '~/locale';
+
+export default {
+ name: 'BridgeEmptyState',
+ i18n: {
+ title: __('This job triggers a downstream pipeline'),
+ linkBtnText: __('View downstream pipeline'),
+ },
+ components: {
+ GlButton,
+ },
+ inject: {
+ emptyStateIllustrationPath: {
+ type: String,
+ require: true,
+ },
+ },
+ props: {
+ downstreamPipelinePath: {
+ type: String,
+ required: false,
+ default: undefined,
+ },
+ },
+};
+</script>
+
+<template>
+ <div class="gl-display-flex gl-flex-direction-column gl-align-items-center gl-mt-11">
+ <img :src="emptyStateIllustrationPath" />
+ <h1 class="gl-font-size-h1">{{ $options.i18n.title }}</h1>
+ <gl-button
+ v-if="downstreamPipelinePath"
+ class="gl-mt-3"
+ category="secondary"
+ variant="confirm"
+ size="medium"
+ :href="downstreamPipelinePath"
+ >
+ {{ $options.i18n.linkBtnText }}
+ </gl-button>
+ </div>
+</template>
diff --git a/app/assets/javascripts/jobs/bridge/components/sidebar.vue b/app/assets/javascripts/jobs/bridge/components/sidebar.vue
new file mode 100644
index 00000000000..68b767408f0
--- /dev/null
+++ b/app/assets/javascripts/jobs/bridge/components/sidebar.vue
@@ -0,0 +1,98 @@
+<script>
+import { GlButton, GlDropdown, GlDropdownItem } from '@gitlab/ui';
+import { GlBreakpointInstance as bp } from '@gitlab/ui/dist/utils';
+import { __ } from '~/locale';
+import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate/tooltip_on_truncate.vue';
+import { JOB_SIDEBAR } from '../../constants';
+import { SIDEBAR_COLLAPSE_BREAKPOINTS } from './constants';
+
+export default {
+ styles: {
+ top: '75px',
+ width: '290px',
+ },
+ name: 'BridgeSidebar',
+ i18n: {
+ ...JOB_SIDEBAR,
+ retryButton: __('Retry'),
+ retryTriggerJob: __('Retry the trigger job'),
+ retryDownstreamPipeline: __('Retry the downstream pipeline'),
+ },
+ borderTopClass: ['gl-border-t-solid', 'gl-border-t-1', 'gl-border-t-gray-100'],
+ components: {
+ GlButton,
+ GlDropdown,
+ GlDropdownItem,
+ TooltipOnTruncate,
+ },
+ inject: {
+ buildName: {
+ type: String,
+ default: '',
+ },
+ },
+ data() {
+ return {
+ isSidebarExpanded: true,
+ };
+ },
+ created() {
+ window.addEventListener('resize', this.onResize);
+ },
+ mounted() {
+ this.onResize();
+ },
+ methods: {
+ toggleSidebar() {
+ this.isSidebarExpanded = !this.isSidebarExpanded;
+ },
+ onResize() {
+ const breakpoint = bp.getBreakpointSize();
+ if (SIDEBAR_COLLAPSE_BREAKPOINTS.includes(breakpoint)) {
+ this.isSidebarExpanded = false;
+ } else if (!this.isSidebarExpanded) {
+ this.isSidebarExpanded = true;
+ }
+ },
+ },
+};
+</script>
+<template>
+ <aside
+ class="gl-fixed gl-right-0 gl-px-5 gl-bg-gray-10 gl-h-full gl-border-l-solid gl-border-1 gl-border-gray-100 gl-z-index-200 gl-overflow-hidden"
+ :style="this.$options.styles"
+ :class="{
+ 'gl-display-none': !isSidebarExpanded,
+ }"
+ >
+ <div class="gl-py-5 gl-display-flex gl-align-items-center">
+ <tooltip-on-truncate :title="buildName" truncate-target="child"
+ ><h4 class="gl-mb-0 gl-mr-2 gl-text-truncate">
+ {{ buildName }}
+ </h4>
+ </tooltip-on-truncate>
+ <!-- TODO: implement retry actions -->
+ <div class="gl-flex-grow-1 gl-flex-shrink-0 gl-text-right">
+ <gl-dropdown
+ :text="$options.i18n.retryButton"
+ category="primary"
+ variant="confirm"
+ right
+ size="medium"
+ >
+ <gl-dropdown-item>{{ $options.i18n.retryTriggerJob }}</gl-dropdown-item>
+ <gl-dropdown-item>{{ $options.i18n.retryDownstreamPipeline }}</gl-dropdown-item>
+ </gl-dropdown>
+ </div>
+ <gl-button
+ :aria-label="$options.i18n.toggleSidebar"
+ data-testid="sidebar-expansion-toggle"
+ category="tertiary"
+ class="gl-md-display-none gl-ml-2"
+ icon="chevron-double-lg-right"
+ @click="toggleSidebar"
+ />
+ </div>
+ <!-- TODO: get job details and show commit block, stage dropdown, jobs list -->
+ </aside>
+</template>
diff --git a/app/assets/javascripts/jobs/components/job_log_controllers.vue b/app/assets/javascripts/jobs/components/job_log_controllers.vue
index 6105299e15c..97141a27a5e 100644
--- a/app/assets/javascripts/jobs/components/job_log_controllers.vue
+++ b/app/assets/javascripts/jobs/components/job_log_controllers.vue
@@ -5,7 +5,7 @@ import { __, s__, sprintf } from '~/locale';
export default {
i18n: {
- eraseLogButtonLabel: s__('Job|Erase job log'),
+ eraseLogButtonLabel: s__('Job|Erase job log and artifacts'),
scrollToBottomButtonLabel: s__('Job|Scroll to bottom'),
scrollToTopButtonLabel: s__('Job|Scroll to top'),
showRawButtonLabel: s__('Job|Show complete raw'),
diff --git a/app/assets/javascripts/jobs/components/sidebar.vue b/app/assets/javascripts/jobs/components/sidebar.vue
index 1b50006239c..9aa1503c7c3 100644
--- a/app/assets/javascripts/jobs/components/sidebar.vue
+++ b/app/assets/javascripts/jobs/components/sidebar.vue
@@ -2,7 +2,7 @@
import { GlButton, GlIcon } from '@gitlab/ui';
import { isEmpty } from 'lodash';
import { mapActions, mapGetters, mapState } from 'vuex';
-import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate.vue';
+import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate/tooltip_on_truncate.vue';
import { JOB_SIDEBAR } from '../constants';
import ArtifactsBlock from './artifacts_block.vue';
import CommitBlock from './commit_block.vue';
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 d90377029c5..5451cd21c14 100644
--- a/app/assets/javascripts/jobs/components/sidebar_job_details_container.vue
+++ b/app/assets/javascripts/jobs/components/sidebar_job_details_container.vue
@@ -20,6 +20,9 @@ export default {
duration() {
return timeIntervalInWords(this.job.duration);
},
+ durationTitle() {
+ return this.job.finished_at ? __('Duration') : __('Elapsed time');
+ },
erasedAt() {
return this.timeFormatted(this.job.erased_at);
},
@@ -76,7 +79,7 @@ export default {
<template>
<div v-if="shouldRenderBlock">
- <detail-row v-if="job.duration" :value="duration" title="Duration" />
+ <detail-row v-if="job.duration" :value="duration" :title="durationTitle" />
<detail-row
v-if="job.finished_at"
:value="finishedAt"
diff --git a/app/assets/javascripts/jobs/components/table/cells/actions_cell.vue b/app/assets/javascripts/jobs/components/table/cells/actions_cell.vue
index 51251c0cacc..7dfa963a857 100644
--- a/app/assets/javascripts/jobs/components/table/cells/actions_cell.vue
+++ b/app/assets/javascripts/jobs/components/table/cells/actions_cell.vue
@@ -12,6 +12,7 @@ import {
JOB_SCHEDULED,
PLAY_JOB_CONFIRMATION_MESSAGE,
RUN_JOB_NOW_HEADER_TITLE,
+ FILE_TYPE_ARCHIVE,
} from '../constants';
import eventHub from '../event_hub';
import cancelJobMutation from '../graphql/mutations/job_cancel.mutation.graphql';
@@ -58,12 +59,21 @@ export default {
},
},
computed: {
+ hasArtifacts() {
+ return this.job.artifacts.nodes.find((artifact) => artifact.fileType === FILE_TYPE_ARCHIVE);
+ },
artifactDownloadPath() {
- return this.job.artifacts?.nodes[0]?.downloadPath;
+ return this.hasArtifacts.downloadPath;
},
canReadJob() {
return this.job.userPermissions?.readBuild;
},
+ canUpdateJob() {
+ return this.job.userPermissions?.updateBuild;
+ },
+ canReadArtifacts() {
+ return this.job.userPermissions?.readJobArtifacts;
+ },
isActive() {
return this.job.active;
},
@@ -86,7 +96,7 @@ export default {
return this.job.detailedStatus?.action?.method;
},
shouldDisplayArtifacts() {
- return this.job.userPermissions?.readJobArtifacts && this.job.artifacts?.nodes.length > 0;
+ return this.canReadArtifacts && this.hasArtifacts;
},
},
methods: {
@@ -139,7 +149,7 @@ export default {
<template>
<gl-button-group>
- <template v-if="canReadJob">
+ <template v-if="canReadJob && canUpdateJob">
<gl-button
v-if="isActive"
data-testid="cancel-button"
diff --git a/app/assets/javascripts/jobs/components/table/cells/pipeline_cell.vue b/app/assets/javascripts/jobs/components/table/cells/pipeline_cell.vue
index 71f9397f5f5..1a6d1a341b0 100644
--- a/app/assets/javascripts/jobs/components/table/cells/pipeline_cell.vue
+++ b/app/assets/javascripts/jobs/components/table/cells/pipeline_cell.vue
@@ -35,10 +35,12 @@ export default {
</script>
<template>
- <div class="gl-text-truncate">
- <gl-link class="gl-text-gray-500!" :href="pipelinePath" data-testid="pipeline-id">
- {{ pipelineId }}
- </gl-link>
+ <div>
+ <div class="gl-text-truncate">
+ <gl-link class="gl-text-gray-500!" :href="pipelinePath" data-testid="pipeline-id">
+ {{ pipelineId }}
+ </gl-link>
+ </div>
<div>
<span>{{ __('created by') }}</span>
<gl-link v-if="showAvatar" :href="userPath" data-testid="pipeline-user-link">
diff --git a/app/assets/javascripts/jobs/components/table/constants.js b/app/assets/javascripts/jobs/components/table/constants.js
index e5d1bc01cbf..962979ba573 100644
--- a/app/assets/javascripts/jobs/components/table/constants.js
+++ b/app/assets/javascripts/jobs/components/table/constants.js
@@ -1,4 +1,5 @@
import { s__, __ } from '~/locale';
+import { DEFAULT_TH_CLASSES } from '~/lib/utils/constants';
export const GRAPHQL_PAGE_SIZE = 30;
@@ -17,6 +18,9 @@ export const DEFAULT = 'default';
/* Job Status Constants */
export const JOB_SCHEDULED = 'SCHEDULED';
+/* Artifact file types */
+export const FILE_TYPE_ARCHIVE = 'ARCHIVE';
+
/* i18n */
export const ACTIONS_DOWNLOAD_ARTIFACTS = __('Download artifacts');
export const ACTIONS_START_NOW = s__('DelayedJobs|Start now');
@@ -30,3 +34,66 @@ export const PLAY_JOB_CONFIRMATION_MESSAGE = s__(
`DelayedJobs|Are you sure you want to run %{job_name} immediately? This job will run automatically after its timer finishes.`,
);
export const RUN_JOB_NOW_HEADER_TITLE = s__('DelayedJobs|Run the delayed job now?');
+
+/* Table constants */
+
+const defaultTableClasses = {
+ tdClass: 'gl-p-5!',
+ thClass: DEFAULT_TH_CLASSES,
+};
+// eslint-disable-next-line @gitlab/require-i18n-strings
+const coverageTdClasses = `${defaultTableClasses.tdClass} gl-display-none! gl-lg-display-table-cell!`;
+
+export const DEFAULT_FIELDS = [
+ {
+ key: 'status',
+ label: __('Status'),
+ ...defaultTableClasses,
+ columnClass: 'gl-w-10p',
+ },
+ {
+ key: 'job',
+ label: __('Job'),
+ ...defaultTableClasses,
+ columnClass: 'gl-w-20p',
+ },
+ {
+ key: 'pipeline',
+ label: __('Pipeline'),
+ ...defaultTableClasses,
+ columnClass: 'gl-w-10p',
+ },
+ {
+ key: 'stage',
+ label: __('Stage'),
+ ...defaultTableClasses,
+ columnClass: 'gl-w-10p',
+ },
+ {
+ key: 'name',
+ label: __('Name'),
+ ...defaultTableClasses,
+ columnClass: 'gl-w-15p',
+ },
+ {
+ key: 'duration',
+ label: __('Duration'),
+ ...defaultTableClasses,
+ columnClass: 'gl-w-15p',
+ },
+ {
+ key: 'coverage',
+ label: __('Coverage'),
+ tdClass: coverageTdClasses,
+ thClass: defaultTableClasses.thClass,
+ columnClass: 'gl-w-10p',
+ },
+ {
+ key: 'actions',
+ label: '',
+ ...defaultTableClasses,
+ columnClass: 'gl-w-10p',
+ },
+];
+
+export const JOBS_TAB_FIELDS = DEFAULT_FIELDS.filter((field) => field.key !== 'pipeline');
diff --git a/app/assets/javascripts/jobs/components/table/graphql/queries/get_jobs.query.graphql b/app/assets/javascripts/jobs/components/table/graphql/queries/get_jobs.query.graphql
index c8763d4767e..88937185a8c 100644
--- a/app/assets/javascripts/jobs/components/table/graphql/queries/get_jobs.query.graphql
+++ b/app/assets/javascripts/jobs/components/table/graphql/queries/get_jobs.query.graphql
@@ -7,6 +7,7 @@ query getJobs(
$statuses: [CiJobStatus!]
) {
project(fullPath: $fullPath) {
+ id
jobs(after: $after, before: $before, first: $first, last: $last, statuses: $statuses) {
pageInfo {
endCursor
@@ -18,6 +19,7 @@ query getJobs(
artifacts {
nodes {
downloadPath
+ fileType
}
}
allowFailure
@@ -27,6 +29,7 @@ query getJobs(
triggered
createdByTag
detailedStatus {
+ id
detailsPath
group
icon
@@ -34,6 +37,7 @@ query getJobs(
text
tooltip
action {
+ id
buttonTitle
icon
method
@@ -51,11 +55,13 @@ query getJobs(
id
path
user {
+ id
webPath
avatarUrl
}
}
stage {
+ id
name
}
name
@@ -70,6 +76,7 @@ query getJobs(
userPermissions {
readBuild
readJobArtifacts
+ updateBuild
}
}
}
diff --git a/app/assets/javascripts/jobs/components/table/jobs_table.vue b/app/assets/javascripts/jobs/components/table/jobs_table.vue
index 298c99c4162..f513d2090fa 100644
--- a/app/assets/javascripts/jobs/components/table/jobs_table.vue
+++ b/app/assets/javascripts/jobs/components/table/jobs_table.vue
@@ -1,75 +1,17 @@
<script>
import { GlTable } from '@gitlab/ui';
-import { DEFAULT_TH_CLASSES } from '~/lib/utils/constants';
-import { s__, __ } from '~/locale';
+import { s__ } from '~/locale';
import CiBadge from '~/vue_shared/components/ci_badge_link.vue';
import ActionsCell from './cells/actions_cell.vue';
import DurationCell from './cells/duration_cell.vue';
import JobCell from './cells/job_cell.vue';
import PipelineCell from './cells/pipeline_cell.vue';
-
-const defaultTableClasses = {
- tdClass: 'gl-p-5!',
- thClass: DEFAULT_TH_CLASSES,
-};
-// eslint-disable-next-line @gitlab/require-i18n-strings
-const coverageTdClasses = `${defaultTableClasses.tdClass} gl-display-none! gl-lg-display-table-cell!`;
+import { DEFAULT_FIELDS } from './constants';
export default {
i18n: {
emptyText: s__('Jobs|No jobs to show'),
},
- fields: [
- {
- key: 'status',
- label: __('Status'),
- ...defaultTableClasses,
- columnClass: 'gl-w-10p',
- },
- {
- key: 'job',
- label: __('Job'),
- ...defaultTableClasses,
- columnClass: 'gl-w-20p',
- },
- {
- key: 'pipeline',
- label: __('Pipeline'),
- ...defaultTableClasses,
- columnClass: 'gl-w-10p',
- },
- {
- key: 'stage',
- label: __('Stage'),
- ...defaultTableClasses,
- columnClass: 'gl-w-10p',
- },
- {
- key: 'name',
- label: __('Name'),
- ...defaultTableClasses,
- columnClass: 'gl-w-15p',
- },
- {
- key: 'duration',
- label: __('Duration'),
- ...defaultTableClasses,
- columnClass: 'gl-w-15p',
- },
- {
- key: 'coverage',
- label: __('Coverage'),
- tdClass: coverageTdClasses,
- thClass: defaultTableClasses.thClass,
- columnClass: 'gl-w-10p',
- },
- {
- key: 'actions',
- label: '',
- ...defaultTableClasses,
- columnClass: 'gl-w-10p',
- },
- ],
components: {
ActionsCell,
CiBadge,
@@ -83,6 +25,11 @@ export default {
type: Array,
required: true,
},
+ tableFields: {
+ type: Array,
+ required: false,
+ default: () => DEFAULT_FIELDS,
+ },
},
methods: {
formatCoverage(coverage) {
@@ -95,7 +42,7 @@ export default {
<template>
<gl-table
:items="jobs"
- :fields="$options.fields"
+ :fields="tableFields"
:tbody-tr-attr="{ 'data-testid': 'jobs-table-row' }"
:empty-text="$options.i18n.emptyText"
show-empty
diff --git a/app/assets/javascripts/jobs/index.js b/app/assets/javascripts/jobs/index.js
index 1fb6a6f9850..e078a6c2319 100644
--- a/app/assets/javascripts/jobs/index.js
+++ b/app/assets/javascripts/jobs/index.js
@@ -1,10 +1,11 @@
import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+import createDefaultClient from '~/lib/graphql';
+import BridgeApp from './bridge/app.vue';
import JobApp from './components/job_app.vue';
import createStore from './store';
-export default () => {
- const element = document.getElementById('js-job-vue-app');
-
+const initializeJobPage = (element) => {
const store = createStore();
// Let's start initializing the store (i.e. fetching data) right away
@@ -51,3 +52,35 @@ export default () => {
},
});
};
+
+const initializeBridgePage = (el) => {
+ const { buildName, emptyStateIllustrationPath } = el.dataset;
+
+ Vue.use(VueApollo);
+ const apolloProvider = new VueApollo({
+ defaultClient: createDefaultClient(),
+ });
+
+ return new Vue({
+ el,
+ apolloProvider,
+ provide: {
+ buildName,
+ emptyStateIllustrationPath,
+ },
+ render(h) {
+ return h(BridgeApp);
+ },
+ });
+};
+
+export default () => {
+ const jobElement = document.getElementById('js-job-page');
+ const bridgeElement = document.getElementById('js-bridge-page');
+
+ if (jobElement) {
+ initializeJobPage(jobElement);
+ } else {
+ initializeBridgePage(bridgeElement);
+ }
+};
diff --git a/app/assets/javascripts/vue_shared/components/delete_label_modal.vue b/app/assets/javascripts/labels/components/delete_label_modal.vue
index 1ff0938d086..1ff0938d086 100644
--- a/app/assets/javascripts/vue_shared/components/delete_label_modal.vue
+++ b/app/assets/javascripts/labels/components/delete_label_modal.vue
diff --git a/app/assets/javascripts/pages/projects/labels/components/promote_label_modal.vue b/app/assets/javascripts/labels/components/promote_label_modal.vue
index e708cd32fff..e708cd32fff 100644
--- a/app/assets/javascripts/pages/projects/labels/components/promote_label_modal.vue
+++ b/app/assets/javascripts/labels/components/promote_label_modal.vue
diff --git a/app/assets/javascripts/create_label.js b/app/assets/javascripts/labels/create_label_dropdown.js
index 07fe2c7e01f..8c166158a44 100644
--- a/app/assets/javascripts/create_label.js
+++ b/app/assets/javascripts/labels/create_label_dropdown.js
@@ -1,8 +1,8 @@
/* eslint-disable func-names */
import $ from 'jquery';
-import Api from './api';
-import { humanize } from './lib/utils/text_utility';
+import Api from '~/api';
+import { humanize } from '~/lib/utils/text_utility';
export default class CreateLabelDropdown {
constructor($el, namespacePath, projectPath) {
diff --git a/app/assets/javascripts/issue_show/event_hub.js b/app/assets/javascripts/labels/event_hub.js
index e31806ad199..e31806ad199 100644
--- a/app/assets/javascripts/issue_show/event_hub.js
+++ b/app/assets/javascripts/labels/event_hub.js
diff --git a/app/assets/javascripts/group_label_subscription.js b/app/assets/javascripts/labels/group_label_subscription.js
index 378259eb9c8..ea69e6585e6 100644
--- a/app/assets/javascripts/group_label_subscription.js
+++ b/app/assets/javascripts/labels/group_label_subscription.js
@@ -1,8 +1,8 @@
import $ from 'jquery';
import { __ } from '~/locale';
import { fixTitle, hide } from '~/tooltips';
-import createFlash from './flash';
-import axios from './lib/utils/axios_utils';
+import createFlash from '~/flash';
+import axios from '~/lib/utils/axios_utils';
const tooltipTitles = {
group: __('Unsubscribe at group level'),
diff --git a/app/assets/javascripts/labels/index.js b/app/assets/javascripts/labels/index.js
new file mode 100644
index 00000000000..22a9c0a89c0
--- /dev/null
+++ b/app/assets/javascripts/labels/index.js
@@ -0,0 +1,137 @@
+import $ from 'jquery';
+import Vue from 'vue';
+import { BV_SHOW_MODAL } from '~/lib/utils/constants';
+import Translate from '~/vue_shared/translate';
+import DeleteLabelModal from './components/delete_label_modal.vue';
+import PromoteLabelModal from './components/promote_label_modal.vue';
+import eventHub from './event_hub';
+import GroupLabelSubscription from './group_label_subscription';
+import LabelManager from './label_manager';
+import ProjectLabelSubscription from './project_label_subscription';
+
+export function initDeleteLabelModal(optionalProps = {}) {
+ new Vue({
+ render(h) {
+ return h(DeleteLabelModal, {
+ props: {
+ selector: '.js-delete-label-modal-button',
+ ...optionalProps,
+ },
+ });
+ },
+ }).$mount();
+}
+
+export function initLabels() {
+ if ($('.prioritized-labels').length) {
+ new LabelManager(); // eslint-disable-line no-new
+ }
+ $('.label-subscription').each((i, el) => {
+ const $el = $(el);
+
+ if ($el.find('.dropdown-group-label').length) {
+ new GroupLabelSubscription($el); // eslint-disable-line no-new
+ } else {
+ new ProjectLabelSubscription($el); // eslint-disable-line no-new
+ }
+ });
+}
+
+export function initLabelIndex() {
+ Vue.use(Translate);
+
+ initLabels();
+ initDeleteLabelModal();
+
+ const onRequestFinished = ({ labelUrl, successful }) => {
+ const button = document.querySelector(
+ `.js-promote-project-label-button[data-url="${labelUrl}"]`,
+ );
+
+ if (!successful) {
+ button.removeAttribute('disabled');
+ }
+ };
+
+ const onRequestStarted = (labelUrl) => {
+ const button = document.querySelector(
+ `.js-promote-project-label-button[data-url="${labelUrl}"]`,
+ );
+ button.setAttribute('disabled', '');
+ eventHub.$once('promoteLabelModal.requestFinished', onRequestFinished);
+ };
+
+ const promoteLabelButtons = document.querySelectorAll('.js-promote-project-label-button');
+
+ return new Vue({
+ el: '#js-promote-label-modal',
+ data() {
+ return {
+ modalProps: {
+ labelTitle: '',
+ labelColor: '',
+ labelTextColor: '',
+ url: '',
+ groupName: '',
+ },
+ };
+ },
+ mounted() {
+ eventHub.$on('promoteLabelModal.props', this.setModalProps);
+ eventHub.$emit('promoteLabelModal.mounted');
+
+ promoteLabelButtons.forEach((button) => {
+ button.removeAttribute('disabled');
+ button.addEventListener('click', () => {
+ this.$root.$emit(BV_SHOW_MODAL, 'promote-label-modal');
+ eventHub.$once('promoteLabelModal.requestStarted', onRequestStarted);
+
+ this.setModalProps({
+ labelTitle: button.dataset.labelTitle,
+ labelColor: button.dataset.labelColor,
+ labelTextColor: button.dataset.labelTextColor,
+ url: button.dataset.url,
+ groupName: button.dataset.groupName,
+ });
+ });
+ });
+ },
+ beforeDestroy() {
+ eventHub.$off('promoteLabelModal.props', this.setModalProps);
+ },
+ methods: {
+ setModalProps(modalProps) {
+ this.modalProps = modalProps;
+ },
+ },
+ render(createElement) {
+ return createElement(PromoteLabelModal, {
+ props: this.modalProps,
+ });
+ },
+ });
+}
+
+export function initAdminLabels() {
+ const labelsContainer = document.querySelector('.js-admin-labels-container');
+ const pagination = labelsContainer?.querySelector('.gl-pagination');
+ const emptyState = document.querySelector('.js-admin-labels-empty-state');
+
+ function removeLabelSuccessCallback() {
+ this.closest('li').classList.add('gl-display-none!');
+
+ const labelsCount = document.querySelectorAll(
+ 'ul.manage-labels-list li:not(.gl-display-none\\!)',
+ ).length;
+
+ // display the empty state if there are no more labels
+ if (labelsCount < 1 && !pagination && emptyState) {
+ emptyState.classList.remove('gl-display-none');
+ labelsContainer.classList.add('gl-display-none');
+ }
+ }
+
+ document.querySelectorAll('.js-remove-label').forEach((row) => {
+ row.addEventListener('ajax:success', removeLabelSuccessCallback);
+ });
+}
diff --git a/app/assets/javascripts/label_manager.js b/app/assets/javascripts/labels/label_manager.js
index e0068edbb9b..1927ac6e1ec 100644
--- a/app/assets/javascripts/label_manager.js
+++ b/app/assets/javascripts/labels/label_manager.js
@@ -3,9 +3,9 @@
import $ from 'jquery';
import Sortable from 'sortablejs';
import { dispose } from '~/tooltips';
-import createFlash from './flash';
-import axios from './lib/utils/axios_utils';
-import { __ } from './locale';
+import createFlash from '~/flash';
+import axios from '~/lib/utils/axios_utils';
+import { __ } from '~/locale';
export default class LabelManager {
constructor({ togglePriorityButton, prioritizedLabels, otherLabels } = {}) {
diff --git a/app/assets/javascripts/labels.js b/app/assets/javascripts/labels/labels.js
index cd8cf0d354c..cd8cf0d354c 100644
--- a/app/assets/javascripts/labels.js
+++ b/app/assets/javascripts/labels/labels.js
diff --git a/app/assets/javascripts/labels_select.js b/app/assets/javascripts/labels/labels_select.js
index 68019a35dbb..9d8ee165df2 100644
--- a/app/assets/javascripts/labels_select.js
+++ b/app/assets/javascripts/labels/labels_select.js
@@ -4,12 +4,12 @@
import $ from 'jquery';
import { difference, isEqual, escape, sortBy, template, union } from 'lodash';
import initDeprecatedJQueryDropdown from '~/deprecated_jquery_dropdown';
-import IssuableBulkUpdateActions from '~/issuable_bulk_update_sidebar/issuable_bulk_update_actions';
+import IssuableBulkUpdateActions from '~/issuable/bulk_update_sidebar/issuable_bulk_update_actions';
import { isScopedLabel } from '~/lib/utils/common_utils';
-import CreateLabelDropdown from './create_label';
-import createFlash from './flash';
-import axios from './lib/utils/axios_utils';
-import { sprintf, __ } from './locale';
+import createFlash from '~/flash';
+import axios from '~/lib/utils/axios_utils';
+import { sprintf, __ } from '~/locale';
+import CreateLabelDropdown from './create_label_dropdown';
export default class LabelsSelect {
constructor(els, options = {}) {
@@ -101,7 +101,7 @@ export default class LabelsSelect {
if (IS_EE) {
/**
* For Scoped labels, the last label selected with the
- * same key will be applied to the current issueable.
+ * same key will be applied to the current issuable.
*
* If these are the labels - priority::1, priority::2; and if
* we apply them in the same order, only priority::2 will stick
diff --git a/app/assets/javascripts/project_label_subscription.js b/app/assets/javascripts/labels/project_label_subscription.js
index f7804c2faa4..b2612e9ede0 100644
--- a/app/assets/javascripts/project_label_subscription.js
+++ b/app/assets/javascripts/labels/project_label_subscription.js
@@ -1,8 +1,8 @@
import $ from 'jquery';
import { fixTitle } from '~/tooltips';
-import createFlash from './flash';
-import axios from './lib/utils/axios_utils';
-import { __ } from './locale';
+import createFlash from '~/flash';
+import axios from '~/lib/utils/axios_utils';
+import { __ } from '~/locale';
const tooltipTitles = {
group: {
diff --git a/app/assets/javascripts/lib/dompurify.js b/app/assets/javascripts/lib/dompurify.js
index 47ede8cb1bb..47568f0ecff 100644
--- a/app/assets/javascripts/lib/dompurify.js
+++ b/app/assets/javascripts/lib/dompurify.js
@@ -3,7 +3,7 @@ import { getNormalizedURL, getBaseURL, relativePathToAbsolute } from '~/lib/util
const defaultConfig = {
// Safely allow SVG <use> tags
- ADD_TAGS: ['use', 'gl-emoji'],
+ ADD_TAGS: ['use', 'gl-emoji', 'copy-code'],
// Prevent possible XSS attacks with data-* attributes used by @rails/ujs
// See https://gitlab.com/gitlab-org/gitlab-ui/-/issues/1421
FORBID_ATTR: ['data-remote', 'data-url', 'data-type', 'data-method'],
diff --git a/app/assets/javascripts/lib/utils/common_utils.js b/app/assets/javascripts/lib/utils/common_utils.js
index a82dad7e2c9..7235b38848c 100644
--- a/app/assets/javascripts/lib/utils/common_utils.js
+++ b/app/assets/javascripts/lib/utils/common_utils.js
@@ -735,3 +735,14 @@ export const isFeatureFlagEnabled = (flag) => window.gon.features?.[flag];
export const convertArrayToCamelCase = (array) => array.map((i) => convertToCamelCase(i));
export const isLoggedIn = () => Boolean(window.gon?.current_user_id);
+
+/**
+ * This method takes in array of objects with snake_case
+ * property names and returns a new array of objects with
+ * camelCase property names
+ *
+ * @param {Array[Object]} array - Array to be converted
+ * @returns {Array[Object]} Converted array
+ */
+export const convertArrayOfObjectsToCamelCase = (array) =>
+ array.map((o) => convertObjectPropsToCamelCase(o));
diff --git a/app/assets/javascripts/lib/utils/constants.js b/app/assets/javascripts/lib/utils/constants.js
index 36c6545164e..a108b02bcbf 100644
--- a/app/assets/javascripts/lib/utils/constants.js
+++ b/app/assets/javascripts/lib/utils/constants.js
@@ -17,6 +17,7 @@ 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';
+export const BV_COLLAPSE_STATE = 'bv::collapse::state';
export const DEFAULT_TH_CLASSES =
'gl-bg-transparent! gl-border-b-solid! gl-border-b-gray-100! gl-p-5! gl-border-b-1!';
diff --git a/app/assets/javascripts/lib/utils/dom_utils.js b/app/assets/javascripts/lib/utils/dom_utils.js
index f7687a929de..b52a736f153 100644
--- a/app/assets/javascripts/lib/utils/dom_utils.js
+++ b/app/assets/javascripts/lib/utils/dom_utils.js
@@ -89,3 +89,17 @@ export const getParents = (element) => {
return parents;
};
+
+/**
+ * This method takes a HTML element and an object of attributes
+ * to save repeated calls to `setAttribute` when multiple
+ * attributes need to be set.
+ *
+ * @param {HTMLElement} el
+ * @param {Object} attributes
+ */
+export const setAttributes = (el, attributes) => {
+ Object.keys(attributes).forEach((key) => {
+ el.setAttribute(key, attributes[key]);
+ });
+};
diff --git a/app/assets/javascripts/lib/utils/intersection_observer.js b/app/assets/javascripts/lib/utils/intersection_observer.js
new file mode 100644
index 00000000000..0959df9a186
--- /dev/null
+++ b/app/assets/javascripts/lib/utils/intersection_observer.js
@@ -0,0 +1,28 @@
+import { memoize } from 'lodash';
+
+import { uuids } from './uuids';
+
+export const create = memoize((options = {}) => {
+ const id = uuids()[0];
+
+ return {
+ id,
+ observer: new IntersectionObserver((entries) => {
+ entries.forEach((entry) => {
+ entry.target.dispatchEvent(
+ new CustomEvent(`IntersectionUpdate`, { detail: { entry, observer: id } }),
+ );
+
+ if (entry.isIntersecting) {
+ entry.target.dispatchEvent(
+ new CustomEvent(`IntersectionAppear`, { detail: { observer: id } }),
+ );
+ } else {
+ entry.target.dispatchEvent(
+ new CustomEvent(`IntersectionDisappear`, { detail: { observer: id } }),
+ );
+ }
+ });
+ }, options),
+ };
+});
diff --git a/app/assets/javascripts/lib/utils/navigation_utility.js b/app/assets/javascripts/lib/utils/navigation_utility.js
index 1579b225e44..029e9f5fd9f 100644
--- a/app/assets/javascripts/lib/utils/navigation_utility.js
+++ b/app/assets/javascripts/lib/utils/navigation_utility.js
@@ -13,3 +13,42 @@ export default function findAndFollowLink(selector) {
visitUrl(link);
}
}
+
+export function prefetchDocument(url) {
+ const newPrefetchLink = document.createElement('link');
+ newPrefetchLink.rel = 'prefetch';
+ newPrefetchLink.href = url;
+ newPrefetchLink.setAttribute('as', 'document');
+ document.head.appendChild(newPrefetchLink);
+}
+
+export function initPrefetchLinks(selector) {
+ document.querySelectorAll(selector).forEach((el) => {
+ let mouseOverTimer;
+
+ const mouseOutHandler = () => {
+ if (mouseOverTimer) {
+ clearTimeout(mouseOverTimer);
+ mouseOverTimer = undefined;
+ }
+ };
+
+ const mouseOverHandler = () => {
+ el.addEventListener('mouseout', mouseOutHandler, { once: true, passive: true });
+
+ mouseOverTimer = setTimeout(() => {
+ if (el.href) prefetchDocument(el.href);
+
+ // Only execute once
+ el.removeEventListener('mouseover', mouseOverHandler, true);
+
+ mouseOverTimer = undefined;
+ }, 100);
+ };
+
+ el.addEventListener('mouseover', mouseOverHandler, {
+ capture: true,
+ passive: true,
+ });
+ });
+}
diff --git a/app/assets/javascripts/lib/utils/url_utility.js b/app/assets/javascripts/lib/utils/url_utility.js
index e53a39cde06..12462a2575e 100644
--- a/app/assets/javascripts/lib/utils/url_utility.js
+++ b/app/assets/javascripts/lib/utils/url_utility.js
@@ -1,6 +1,6 @@
export const DASH_SCOPE = '-';
-const PATH_SEPARATOR = '/';
+export const PATH_SEPARATOR = '/';
const PATH_SEPARATOR_LEADING_REGEX = new RegExp(`^${PATH_SEPARATOR}+`);
const PATH_SEPARATOR_ENDING_REGEX = new RegExp(`${PATH_SEPARATOR}+$`);
const SHA_REGEX = /[\da-f]{40}/gi;
diff --git a/app/assets/javascripts/main.js b/app/assets/javascripts/main.js
index e422d9b1a32..e221a54d9c6 100644
--- a/app/assets/javascripts/main.js
+++ b/app/assets/javascripts/main.js
@@ -14,9 +14,9 @@ import { GlBreakpointInstance as bp } from '@gitlab/ui/dist/utils';
import { initRails } from '~/lib/utils/rails_ujs';
import * as popovers from '~/popovers';
import * as tooltips from '~/tooltips';
-import { initHeaderSearchApp } from '~/header_search';
+import { initPrefetchLinks } from '~/lib/utils/navigation_utility';
import initAlertHandler from './alert_handler';
-import { removeFlashClickListener } from './flash';
+import { addDismissFlashClickListener } from './flash';
import initTodoToggle from './header';
import initLayoutNav from './layout_nav';
import { logHelloDeferred } from './lib/logger/hello_deferred';
@@ -36,6 +36,7 @@ import GlFieldErrors from './gl_field_errors';
import initUserPopovers from './user_popovers';
import initBroadcastNotifications from './broadcast_notification';
import { initTopNav } from './nav';
+import { initCopyCodeButton } from './behaviors/copy_code';
import 'ee_else_ce/main_ee';
import 'jh_else_ce/main_jh';
@@ -90,6 +91,7 @@ function deferredInitialisation() {
initTopNav();
initBreadcrumbs();
initTodoToggle();
+ initPrefetchLinks('.js-prefetch-document');
initLogoAnimation();
initServicePingConsent();
initUserPopovers();
@@ -97,25 +99,31 @@ function deferredInitialisation() {
initPersistentUserCallouts();
initDefaultTrackers();
initFeatureHighlight();
-
- if (gon.features?.newHeaderSearch) {
- initHeaderSearchApp();
- } else {
- const search = document.querySelector('#search');
- if (search) {
- search.addEventListener(
- 'focus',
- () => {
+ initCopyCodeButton();
+
+ const search = document.querySelector('#search');
+ if (search) {
+ search.addEventListener(
+ 'focus',
+ () => {
+ if (gon.features?.newHeaderSearch) {
+ import(/* webpackChunkName: 'globalSearch' */ '~/header_search')
+ .then(async ({ initHeaderSearchApp }) => {
+ await initHeaderSearchApp();
+ document.querySelector('#search').focus();
+ })
+ .catch(() => {});
+ } else {
import(/* webpackChunkName: 'globalSearch' */ './search_autocomplete')
.then(({ default: initSearchAutocomplete }) => {
const searchDropdown = initSearchAutocomplete();
searchDropdown.onSearchInputFocus();
})
.catch(() => {});
- },
- { once: true },
- );
- }
+ }
+ },
+ { once: true },
+ );
}
addSelectOnFocusBehaviour('.js-select-on-focus');
@@ -259,7 +267,7 @@ if (flashContainer && flashContainer.children.length) {
flashContainer
.querySelectorAll('.flash-alert, .flash-notice, .flash-success')
.forEach((flashEl) => {
- removeFlashClickListener(flashEl);
+ addDismissFlashClickListener(flashEl);
});
}
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 35966be7363..d092283338c 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
@@ -53,6 +53,7 @@ export default {
:title="s__('Member|Deny access')"
:is-access-request="true"
icon="close"
+ button-category="primary"
/>
</div>
</action-button-group>
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 91062c222f4..ab9abfd38c6 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
@@ -41,6 +41,8 @@ export default {
<remove-member-button
:member-id="member.id"
:message="message"
+ icon="remove"
+ button-category="primary"
:title="s__('Member|Revoke invite')"
is-invite
/>
diff --git a/app/assets/javascripts/members/components/action_buttons/remove_member_button.vue b/app/assets/javascripts/members/components/action_buttons/remove_member_button.vue
index 69137ce615b..01606d07554 100644
--- a/app/assets/javascripts/members/components/action_buttons/remove_member_button.vue
+++ b/app/assets/javascripts/members/components/action_buttons/remove_member_button.vue
@@ -30,7 +30,17 @@ export default {
icon: {
type: String,
required: false,
- default: 'remove',
+ default: undefined,
+ },
+ buttonText: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ buttonCategory: {
+ type: String,
+ required: false,
+ default: 'secondary',
},
isAccessRequest: {
type: Boolean,
@@ -79,10 +89,12 @@ export default {
<gl-button
v-gl-tooltip
variant="danger"
+ :category="buttonCategory"
:title="title"
:aria-label="title"
:icon="icon"
data-qa-selector="delete_member_button"
@click="showRemoveMemberModal(modalData)"
- />
+ ><template v-if="buttonText">{{ buttonText }}</template></gl-button
+ >
</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 44d658c90a0..594da7f68cc 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,5 +1,5 @@
<script>
-import { s__, sprintf } from '~/locale';
+import { __, s__, sprintf } from '~/locale';
import { parseUserDeletionObstacles } from '~/vue_shared/components/user_deletion_obstacles/utils';
import ActionButtonGroup from './action_button_group.vue';
import LeaveButton from './leave_button.vue';
@@ -23,6 +23,10 @@ export default {
type: Boolean,
required: true,
},
+ isInvitedUser: {
+ type: Boolean,
+ required: true,
+ },
permissions: {
type: Object,
required: true,
@@ -56,6 +60,15 @@ export default {
obstacles: parseUserDeletionObstacles(this.member.user),
};
},
+ removeMemberButtonText() {
+ return this.isInvitedUser ? null : __('Remove user');
+ },
+ removeMemberButtonIcon() {
+ return this.isInvitedUser ? 'remove' : '';
+ },
+ removeMemberButtonCategory() {
+ return this.isInvitedUser ? 'primary' : 'secondary';
+ },
},
};
</script>
@@ -70,6 +83,9 @@ export default {
:member-type="member.type"
:user-deletion-obstacles="userDeletionObstaclesUserData"
:message="message"
+ :icon="removeMemberButtonIcon"
+ :button-text="removeMemberButtonText"
+ :button-category="removeMemberButtonCategory"
:title="s__('Member|Remove member')"
/>
</div>
diff --git a/app/assets/javascripts/members/components/table/member_action_buttons.vue b/app/assets/javascripts/members/components/table/member_action_buttons.vue
index 6f15f079d2d..971b1a8435e 100644
--- a/app/assets/javascripts/members/components/table/member_action_buttons.vue
+++ b/app/assets/javascripts/members/components/table/member_action_buttons.vue
@@ -30,6 +30,10 @@ export default {
type: Boolean,
required: true,
},
+ isInvitedUser: {
+ type: Boolean,
+ required: true,
+ },
},
computed: {
actionButtonComponent() {
@@ -53,5 +57,6 @@ export default {
:member="member"
:permissions="permissions"
:is-current-user="isCurrentUser"
+ :is-invited-user="isInvitedUser"
/>
</template>
diff --git a/app/assets/javascripts/members/components/table/members_table.vue b/app/assets/javascripts/members/components/table/members_table.vue
index 202f3aa89e1..de733ae75df 100644
--- a/app/assets/javascripts/members/components/table/members_table.vue
+++ b/app/assets/javascripts/members/components/table/members_table.vue
@@ -8,6 +8,7 @@ import initUserPopovers from '~/user_popovers';
import {
FIELDS,
ACTIVE_TAB_QUERY_PARAM_NAME,
+ TAB_QUERY_PARAM_VALUES,
MEMBER_STATE_AWAITING,
USER_STATE_BLOCKED_PENDING_APPROVAL,
BADGE_LABELS_PENDING_OWNER_APPROVAL,
@@ -82,6 +83,9 @@ export default {
return paramName && currentPage && perPage && totalItems;
},
+ isInvitedUser() {
+ return this.tabQueryParamValue === TAB_QUERY_PARAM_VALUES.invite;
+ },
},
mounted() {
initUserPopovers(this.$el.querySelectorAll('.js-user-link'));
@@ -275,6 +279,7 @@ export default {
<member-action-buttons
:member-type="memberType"
:is-current-user="isCurrentUser"
+ :is-invited-user="isInvitedUser"
:permissions="permissions"
:member="member"
/>
diff --git a/app/assets/javascripts/merge_conflicts/merge_conflicts_bundle.js b/app/assets/javascripts/merge_conflicts/merge_conflicts_bundle.js
index cf02c6fbd6b..8c96f8a017e 100644
--- a/app/assets/javascripts/merge_conflicts/merge_conflicts_bundle.js
+++ b/app/assets/javascripts/merge_conflicts/merge_conflicts_bundle.js
@@ -1,5 +1,5 @@
import Vue from 'vue';
-import initIssuableSidebar from '../init_issuable_sidebar';
+import { initIssuableSidebar } from '~/issuable';
import MergeConflictsResolverApp from './merge_conflict_resolver_app.vue';
import { createStore } from './store';
diff --git a/app/assets/javascripts/milestone.js b/app/assets/javascripts/milestone.js
deleted file mode 100644
index b4e53c1fab6..00000000000
--- a/app/assets/javascripts/milestone.js
+++ /dev/null
@@ -1,49 +0,0 @@
-import $ from 'jquery';
-import createFlash from './flash';
-import axios from './lib/utils/axios_utils';
-import { __ } from './locale';
-
-export default class Milestone {
- constructor() {
- this.bindTabsSwitching();
- this.loadInitialTab();
- }
-
- bindTabsSwitching() {
- return $('a[data-toggle="tab"]').on('show.bs.tab', (e) => {
- const $target = $(e.target);
-
- window.location.hash = $target.attr('href');
- this.loadTab($target);
- });
- }
-
- loadInitialTab() {
- const $target = $(`.js-milestone-tabs a:not(.active)[href="${window.location.hash}"]`);
-
- if ($target.length) {
- $target.tab('show');
- } else {
- this.loadTab($('.js-milestone-tabs a.active'));
- }
- }
- // eslint-disable-next-line class-methods-use-this
- loadTab($target) {
- const endpoint = $target.data('endpoint');
- const tabElId = $target.attr('href');
-
- if (endpoint && !$target.hasClass('is-loaded')) {
- axios
- .get(endpoint)
- .then(({ data }) => {
- $(tabElId).html(data.html);
- $target.addClass('is-loaded');
- })
- .catch(() =>
- createFlash({
- message: __('Error loading milestone tab'),
- }),
- );
- }
- }
-}
diff --git a/app/assets/javascripts/pages/milestones/shared/components/delete_milestone_modal.vue b/app/assets/javascripts/milestones/components/delete_milestone_modal.vue
index 34f9fe778ea..34f9fe778ea 100644
--- a/app/assets/javascripts/pages/milestones/shared/components/delete_milestone_modal.vue
+++ b/app/assets/javascripts/milestones/components/delete_milestone_modal.vue
diff --git a/app/assets/javascripts/pages/milestones/shared/components/promote_milestone_modal.vue b/app/assets/javascripts/milestones/components/promote_milestone_modal.vue
index b41611001ab..b41611001ab 100644
--- a/app/assets/javascripts/pages/milestones/shared/components/promote_milestone_modal.vue
+++ b/app/assets/javascripts/milestones/components/promote_milestone_modal.vue
diff --git a/app/assets/javascripts/pages/milestones/shared/event_hub.js b/app/assets/javascripts/milestones/event_hub.js
index e31806ad199..e31806ad199 100644
--- a/app/assets/javascripts/pages/milestones/shared/event_hub.js
+++ b/app/assets/javascripts/milestones/event_hub.js
diff --git a/app/assets/javascripts/pages/milestones/shared/delete_milestone_modal_init.js b/app/assets/javascripts/milestones/index.js
index 3aeff2db2e0..2ca5f104b4f 100644
--- a/app/assets/javascripts/pages/milestones/shared/delete_milestone_modal_init.js
+++ b/app/assets/javascripts/milestones/index.js
@@ -1,10 +1,58 @@
+import $ from 'jquery';
import Vue from 'vue';
+import initDatePicker from '~/behaviors/date_picker';
+import GLForm from '~/gl_form';
import { BV_SHOW_MODAL } from '~/lib/utils/constants';
+import Milestone from '~/milestones/milestone';
+import Sidebar from '~/right_sidebar';
+import MountMilestoneSidebar from '~/sidebar/mount_milestone_sidebar';
import Translate from '~/vue_shared/translate';
+import ZenMode from '~/zen_mode';
import DeleteMilestoneModal from './components/delete_milestone_modal.vue';
+import PromoteMilestoneModal from './components/promote_milestone_modal.vue';
import eventHub from './event_hub';
-export default () => {
+export function initForm(initGFM = true) {
+ new ZenMode(); // eslint-disable-line no-new
+ initDatePicker();
+
+ // eslint-disable-next-line no-new
+ new GLForm($('.milestone-form'), {
+ emojis: true,
+ members: initGFM,
+ issues: initGFM,
+ mergeRequests: initGFM,
+ epics: initGFM,
+ milestones: initGFM,
+ labels: initGFM,
+ snippets: initGFM,
+ vulnerabilities: initGFM,
+ });
+}
+
+export function initShow() {
+ new Milestone(); // eslint-disable-line no-new
+ new Sidebar(); // eslint-disable-line no-new
+ new MountMilestoneSidebar(); // eslint-disable-line no-new
+}
+
+export function initPromoteMilestoneModal() {
+ Vue.use(Translate);
+
+ const promoteMilestoneModal = document.getElementById('promote-milestone-modal');
+ if (!promoteMilestoneModal) {
+ return null;
+ }
+
+ return new Vue({
+ el: promoteMilestoneModal,
+ render(createElement) {
+ return createElement(PromoteMilestoneModal);
+ },
+ });
+}
+
+export function initDeleteMilestoneModal() {
Vue.use(Translate);
const onRequestFinished = ({ milestoneUrl, successful }) => {
@@ -72,4 +120,4 @@ export default () => {
});
},
});
-};
+}
diff --git a/app/assets/javascripts/milestones/milestone.js b/app/assets/javascripts/milestones/milestone.js
new file mode 100644
index 00000000000..05102f73f92
--- /dev/null
+++ b/app/assets/javascripts/milestones/milestone.js
@@ -0,0 +1,49 @@
+import createFlash from '~/flash';
+import { sanitize } from '~/lib/dompurify';
+import axios from '~/lib/utils/axios_utils';
+import { historyPushState } from '~/lib/utils/common_utils';
+import { __ } from '~/locale';
+import { GlTabsBehavior, TAB_SHOWN_EVENT } from '~/tabs';
+
+export default class Milestone {
+ constructor() {
+ this.tabsEl = document.querySelector('.js-milestone-tabs');
+ this.glTabs = new GlTabsBehavior(this.tabsEl);
+ this.loadedTabs = new WeakSet();
+
+ this.bindTabsSwitching();
+ this.loadInitialTab();
+ }
+
+ bindTabsSwitching() {
+ this.tabsEl.addEventListener(TAB_SHOWN_EVENT, (event) => {
+ const tab = event.target;
+ const { activeTabPanel } = event.detail;
+ historyPushState(tab.getAttribute('href'));
+ this.loadTab(tab, activeTabPanel);
+ });
+ }
+
+ loadInitialTab() {
+ const tab = this.tabsEl.querySelector(`a[href="${window.location.hash}"]`);
+ this.glTabs.activateTab(tab || this.glTabs.activeTab);
+ }
+ loadTab(tab, tabPanel) {
+ const { endpoint } = tab.dataset;
+
+ if (endpoint && !this.loadedTabs.has(tab)) {
+ axios
+ .get(endpoint)
+ .then(({ data }) => {
+ // eslint-disable-next-line no-param-reassign
+ tabPanel.innerHTML = sanitize(data.html);
+ this.loadedTabs.add(tab);
+ })
+ .catch(() =>
+ createFlash({
+ message: __('Error loading milestone tab'),
+ }),
+ );
+ }
+ }
+}
diff --git a/app/assets/javascripts/milestone_select.js b/app/assets/javascripts/milestones/milestone_select.js
index aa8a40b6a87..c95ec3dd10b 100644
--- a/app/assets/javascripts/milestone_select.js
+++ b/app/assets/javascripts/milestones/milestone_select.js
@@ -6,9 +6,9 @@ import { template, escape } from 'lodash';
import Api from '~/api';
import initDeprecatedJQueryDropdown from '~/deprecated_jquery_dropdown';
import { __, sprintf } from '~/locale';
-import { sortMilestonesByDueDate } from '~/milestones/milestone_utils';
-import axios from './lib/utils/axios_utils';
-import { timeFor, parsePikadayDate, dateInWords } from './lib/utils/datetime_utility';
+import { sortMilestonesByDueDate } from '~/milestones/utils';
+import axios from '~/lib/utils/axios_utils';
+import { timeFor, parsePikadayDate, dateInWords } from '~/lib/utils/datetime_utility';
export default class MilestoneSelect {
constructor(currentProject, els, options = {}) {
diff --git a/app/assets/javascripts/milestones/milestone_utils.js b/app/assets/javascripts/milestones/utils.js
index 3ae5e676138..3ae5e676138 100644
--- a/app/assets/javascripts/milestones/milestone_utils.js
+++ b/app/assets/javascripts/milestones/utils.js
diff --git a/app/assets/javascripts/monitoring/queries/getDashboardValidationWarnings.query.graphql b/app/assets/javascripts/monitoring/queries/getDashboardValidationWarnings.query.graphql
index 302383512d3..a61d601cd34 100644
--- a/app/assets/javascripts/monitoring/queries/getDashboardValidationWarnings.query.graphql
+++ b/app/assets/javascripts/monitoring/queries/getDashboardValidationWarnings.query.graphql
@@ -7,6 +7,7 @@ query getDashboardValidationWarnings(
id
environments(name: $environmentName) {
nodes {
+ id
name
metricsDashboard(path: $dashboardPath) {
path
diff --git a/app/assets/javascripts/mr_popover/components/mr_popover.vue b/app/assets/javascripts/mr_popover/components/mr_popover.vue
index 791fdf7660f..d99a3adb358 100644
--- a/app/assets/javascripts/mr_popover/components/mr_popover.vue
+++ b/app/assets/javascripts/mr_popover/components/mr_popover.vue
@@ -65,6 +65,9 @@ export default {
return humanMRStates.open;
}
},
+ title() {
+ return this.mergeRequest?.title || this.mergeRequestTitle;
+ },
showDetails() {
return Object.keys(this.mergeRequest).length > 0;
},
@@ -89,7 +92,7 @@ export default {
<template>
<gl-popover :target="target" boundary="viewport" placement="top" show>
<div class="mr-popover">
- <div v-if="$apollo.loading">
+ <div v-if="$apollo.queries.mergeRequest.loading">
<gl-skeleton-loading :lines="1" class="animation-container-small mt-1" />
</div>
<div v-else-if="showDetails" class="d-flex align-items-center justify-content-between">
@@ -97,13 +100,13 @@ export default {
<div :class="`issuable-status-box status-box ${statusBoxClass}`">
{{ stateHumanName }}
</div>
- <span class="text-secondary">Opened <time v-text="formattedTime"></time></span>
+ <span class="gl-text-secondary">Opened <time v-text="formattedTime"></time></span>
</div>
<ci-icon v-if="detailedStatus" :status="detailedStatus" />
</div>
- <h5 class="my-2">{{ mergeRequestTitle }}</h5>
+ <h5 v-if="!$apollo.queries.mergeRequest.loading" class="my-2">{{ title }}</h5>
<!-- eslint-disable @gitlab/vue-require-i18n-strings -->
- <div class="text-secondary">
+ <div class="gl-text-secondary">
{{ `${projectPath}!${mergeRequestIID}` }}
</div>
<!-- eslint-enable @gitlab/vue-require-i18n-strings -->
diff --git a/app/assets/javascripts/mr_popover/queries/merge_request.query.graphql b/app/assets/javascripts/mr_popover/queries/merge_request.query.graphql
index 37d4bc88a69..b3e5d89d495 100644
--- a/app/assets/javascripts/mr_popover/queries/merge_request.query.graphql
+++ b/app/assets/javascripts/mr_popover/queries/merge_request.query.graphql
@@ -1,10 +1,15 @@
query mergeRequest($projectPath: ID!, $mergeRequestIID: String!) {
project(fullPath: $projectPath) {
+ id
mergeRequest(iid: $mergeRequestIID) {
+ id
+ title
createdAt
state
headPipeline {
+ id
detailedStatus {
+ id
icon
group
}
diff --git a/app/assets/javascripts/network/branch_graph.js b/app/assets/javascripts/network/branch_graph.js
index 54fe9d19002..71894b4ff3e 100644
--- a/app/assets/javascripts/network/branch_graph.js
+++ b/app/assets/javascripts/network/branch_graph.js
@@ -98,6 +98,7 @@ export default class BranchGraph {
let len = 0;
let cuday = 0;
let cumonth = '';
+ let cuyear = '';
const { r } = this;
r.rect(0, 0, 40, this.barHeight).attr({
fill: '#222',
@@ -108,24 +109,21 @@ export default class BranchGraph {
const ref = this.days;
for (mm = 0, len = ref.length; mm < len; mm += 1) {
const day = ref[mm];
- if (cuday !== day[0] || cumonth !== day[1]) {
+ if (cuday !== day[0] || cumonth !== day[1] || cuyear !== day[2]) {
// Dates
r.text(55, this.offsetY + this.unitTime * mm, day[0]).attr({
font: '12px Monaco, monospace',
fill: '#BBB',
});
- [cuday] = day;
}
- if (cumonth !== day[1]) {
+ if (cumonth !== day[1] || cuyear !== day[2]) {
// Months
r.text(20, this.offsetY + this.unitTime * mm, day[1]).attr({
font: '12px Monaco, monospace',
fill: '#EEE',
});
-
- // eslint-disable-next-line prefer-destructuring
- cumonth = day[1];
}
+ [cuday, cumonth, cuyear] = day;
}
this.renderPartialGraph();
return this.bindEvents();
diff --git a/app/assets/javascripts/notes/components/comment_form.vue b/app/assets/javascripts/notes/components/comment_form.vue
index 4e31fdcd4f0..996c008b881 100644
--- a/app/assets/javascripts/notes/components/comment_form.vue
+++ b/app/assets/javascripts/notes/components/comment_form.vue
@@ -11,7 +11,6 @@ import httpStatusCodes from '~/lib/utils/http_status';
import {
capitalizeFirstCharacter,
convertToCamelCase,
- splitCamelCase,
slugifyWithUnderscore,
} from '~/lib/utils/text_utility';
import { sprintf } from '~/locale';
@@ -77,7 +76,15 @@ export default {
]),
...mapState(['isToggleStateButtonLoading']),
noteableDisplayName() {
- return splitCamelCase(this.noteableType).toLowerCase();
+ const displayNameMap = {
+ [constants.ISSUE_NOTEABLE_TYPE]: this.$options.i18n.issue,
+ [constants.EPIC_NOTEABLE_TYPE]: this.$options.i18n.epic,
+ [constants.MERGE_REQUEST_NOTEABLE_TYPE]: this.$options.i18n.mergeRequest,
+ };
+
+ const noteableTypeKey =
+ constants.NOTEABLE_TYPE_MAPPING[this.noteableType] || constants.ISSUE_NOTEABLE_TYPE;
+ return displayNameMap[noteableTypeKey];
},
isLoggedIn() {
return this.getUserData.id;
@@ -103,15 +110,13 @@ export default {
const openOrClose = this.isOpen ? 'close' : 'reopen';
if (this.note.length) {
- return sprintf(this.$options.i18n.actionButtonWithNote, {
+ return sprintf(this.$options.i18n.actionButton.withNote[openOrClose], {
actionText: this.commentButtonTitle,
- openOrClose,
noteable: this.noteableDisplayName,
});
}
- return sprintf(this.$options.i18n.actionButton, {
- openOrClose: capitalizeFirstCharacter(openOrClose),
+ return sprintf(this.$options.i18n.actionButton.withoutNote[openOrClose], {
noteable: this.noteableDisplayName,
});
},
@@ -151,13 +156,8 @@ export default {
draftEndpoint() {
return this.getNotesData.draftsPath;
},
- issuableTypeTitle() {
- return this.noteableType === constants.MERGE_REQUEST_NOTEABLE_TYPE
- ? this.$options.i18n.mergeRequest
- : this.$options.i18n.issue;
- },
isIssue() {
- return this.noteableDisplayName === constants.ISSUE_NOTEABLE_TYPE;
+ return constants.NOTEABLE_TYPE_MAPPING[this.noteableType] === constants.ISSUE_NOTEABLE_TYPE;
},
trackingLabel() {
return slugifyWithUnderscore(`${this.commentButtonTitle} button`);
@@ -329,7 +329,7 @@ export default {
<template>
<div>
<note-signed-out-widget v-if="!isLoggedIn" />
- <discussion-locked-widget v-else-if="!canCreateNote" :issuable-type="issuableTypeTitle" />
+ <discussion-locked-widget v-else-if="!canCreateNote" :issuable-type="noteableDisplayName" />
<ul v-else-if="canCreateNote" class="notes notes-form timeline">
<timeline-entry-item class="note-form">
<gl-alert
diff --git a/app/assets/javascripts/notes/components/diff_with_note.vue b/app/assets/javascripts/notes/components/diff_with_note.vue
index b04aa74d46e..b2d5910fd3f 100644
--- a/app/assets/javascripts/notes/components/diff_with_note.vue
+++ b/app/assets/javascripts/notes/components/diff_with_note.vue
@@ -1,5 +1,8 @@
<script>
-import { GlDeprecatedSkeletonLoading as GlSkeletonLoading } from '@gitlab/ui';
+import {
+ GlDeprecatedSkeletonLoading as GlSkeletonLoading,
+ GlSafeHtmlDirective as SafeHtml,
+} from '@gitlab/ui';
import { mapState, mapActions } from 'vuex';
import DiffFileHeader from '~/diffs/components/diff_file_header.vue';
import ImageDiffOverlay from '~/diffs/components/image_diff_overlay.vue';
@@ -17,6 +20,9 @@ export default {
DiffViewer,
ImageDiffOverlay,
},
+ directives: {
+ SafeHtml,
+ },
props: {
discussion: {
type: Object,
@@ -92,11 +98,7 @@ export default {
>
<td :class="line.type" class="diff-line-num old_line">{{ line.old_line }}</td>
<td :class="line.type" class="diff-line-num new_line">{{ line.new_line }}</td>
- <td
- :class="line.type"
- class="line_content"
- v-html="trimChar(line.rich_text) /* eslint-disable-line vue/no-v-html */"
- ></td>
+ <td v-safe-html="trimChar(line.rich_text)" :class="line.type" class="line_content"></td>
</tr>
</template>
<tr v-if="!hasTruncatedDiffLines" class="line_holder line-holder-placeholder">
diff --git a/app/assets/javascripts/notes/components/discussion_filter.vue b/app/assets/javascripts/notes/components/discussion_filter.vue
index 88f053aed67..102afaf308f 100644
--- a/app/assets/javascripts/notes/components/discussion_filter.vue
+++ b/app/assets/javascripts/notes/components/discussion_filter.vue
@@ -39,7 +39,7 @@ export default {
};
},
computed: {
- ...mapGetters(['getNotesDataByProp', 'timelineEnabled']),
+ ...mapGetters(['getNotesDataByProp', 'timelineEnabled', 'isLoading']),
currentFilter() {
if (!this.currentValue) return this.filters[0];
return this.filters.find((filter) => filter.value === this.currentValue);
@@ -119,6 +119,7 @@ export default {
class="gl-mr-3 full-width-mobile discussion-filter-container js-discussion-filter-container"
data-qa-selector="discussion_filter_dropdown"
:text="currentFilter.title"
+ :disabled="isLoading"
>
<div v-for="filter in filters" :key="filter.value" class="dropdown-item-wrapper">
<gl-dropdown-item
diff --git a/app/assets/javascripts/notes/components/discussion_locked_widget.vue b/app/assets/javascripts/notes/components/discussion_locked_widget.vue
index 2f215e36d5b..8ac3f6bea68 100644
--- a/app/assets/javascripts/notes/components/discussion_locked_widget.vue
+++ b/app/assets/javascripts/notes/components/discussion_locked_widget.vue
@@ -1,7 +1,6 @@
<script>
import { GlLink, GlIcon } from '@gitlab/ui';
import { __, sprintf } from '~/locale';
-import Issuable from '~/vue_shared/mixins/issuable';
import issuableStateMixin from '../mixins/issuable_state';
export default {
@@ -9,8 +8,17 @@ export default {
GlIcon,
GlLink,
},
- mixins: [Issuable, issuableStateMixin],
+ mixins: [issuableStateMixin],
+ props: {
+ issuableType: {
+ required: true,
+ type: String,
+ },
+ },
computed: {
+ issuableDisplayName() {
+ return this.issuableType.replace(/_/g, ' ');
+ },
projectArchivedWarning() {
return __('This project is archived and cannot be commented on.');
},
diff --git a/app/assets/javascripts/notes/components/discussion_notes.vue b/app/assets/javascripts/notes/components/discussion_notes.vue
index d1df4eb848b..6fcfa66ea49 100644
--- a/app/assets/javascripts/notes/components/discussion_notes.vue
+++ b/app/assets/javascripts/notes/components/discussion_notes.vue
@@ -1,6 +1,5 @@
<script>
import { mapGetters, mapActions } from 'vuex';
-import { GlIntersectionObserver } from '@gitlab/ui';
import { __ } from '~/locale';
import PlaceholderNote from '~/vue_shared/components/notes/placeholder_note.vue';
import PlaceholderSystemNote from '~/vue_shared/components/notes/placeholder_system_note.vue';
@@ -17,9 +16,7 @@ export default {
ToggleRepliesWidget,
NoteEditedText,
DiscussionNotesRepliesWrapper,
- GlIntersectionObserver,
},
- inject: ['discussionObserverHandler'],
props: {
discussion: {
type: Object,
@@ -57,11 +54,7 @@ export default {
},
},
computed: {
- ...mapGetters([
- 'userCanReply',
- 'previousUnresolvedDiscussionId',
- 'firstUnresolvedDiscussionId',
- ]),
+ ...mapGetters(['userCanReply']),
hasReplies() {
return Boolean(this.replies.length);
},
@@ -84,20 +77,9 @@ export default {
url: this.discussion.discussion_path,
};
},
- isFirstUnresolved() {
- return this.firstUnresolvedDiscussionId === this.discussion.id;
- },
- },
- observerOptions: {
- threshold: 0,
- rootMargin: '0px 0px -50% 0px',
},
methods: {
- ...mapActions([
- 'toggleDiscussion',
- 'setSelectedCommentPositionHover',
- 'setCurrentDiscussionId',
- ]),
+ ...mapActions(['toggleDiscussion', 'setSelectedCommentPositionHover']),
componentName(note) {
if (note.isPlaceholderNote) {
if (note.placeholderType === SYSTEM_NOTE) {
@@ -128,18 +110,6 @@ export default {
this.setSelectedCommentPositionHover();
}
},
- observerTriggered(entry) {
- this.discussionObserverHandler({
- entry,
- isFirstUnresolved: this.isFirstUnresolved,
- currentDiscussion: { ...this.discussion },
- isDiffsPage: !this.isOverviewTab,
- functions: {
- setCurrentDiscussionId: this.setCurrentDiscussionId,
- getPreviousUnresolvedDiscussionId: this.previousUnresolvedDiscussionId,
- },
- });
- },
},
};
</script>
@@ -152,35 +122,33 @@ export default {
@mouseleave="handleMouseLeave(discussion)"
>
<template v-if="shouldGroupReplies">
- <gl-intersection-observer :options="$options.observerOptions" @update="observerTriggered">
- <component
- :is="componentName(firstNote)"
- :note="componentData(firstNote)"
- :line="line || diffLine"
- :discussion-file="discussion.diff_file"
- :commit="commit"
- :help-page-path="helpPagePath"
- :show-reply-button="userCanReply"
- :discussion-root="true"
- :discussion-resolve-path="discussion.resolve_path"
- :is-overview-tab="isOverviewTab"
- @handleDeleteNote="$emit('deleteNote')"
- @startReplying="$emit('startReplying')"
- >
- <template #discussion-resolved-text>
- <note-edited-text
- v-if="discussion.resolved"
- :edited-at="discussion.resolved_at"
- :edited-by="discussion.resolved_by"
- :action-text="resolvedText"
- class-name="discussion-headline-light js-discussion-headline discussion-resolved-text"
- />
- </template>
- <template #avatar-badge>
- <slot name="avatar-badge"></slot>
- </template>
- </component>
- </gl-intersection-observer>
+ <component
+ :is="componentName(firstNote)"
+ :note="componentData(firstNote)"
+ :line="line || diffLine"
+ :discussion-file="discussion.diff_file"
+ :commit="commit"
+ :help-page-path="helpPagePath"
+ :show-reply-button="userCanReply"
+ :discussion-root="true"
+ :discussion-resolve-path="discussion.resolve_path"
+ :is-overview-tab="isOverviewTab"
+ @handleDeleteNote="$emit('deleteNote')"
+ @startReplying="$emit('startReplying')"
+ >
+ <template #discussion-resolved-text>
+ <note-edited-text
+ v-if="discussion.resolved"
+ :edited-at="discussion.resolved_at"
+ :edited-by="discussion.resolved_by"
+ :action-text="resolvedText"
+ class-name="discussion-headline-light js-discussion-headline discussion-resolved-text"
+ />
+ </template>
+ <template #avatar-badge>
+ <slot name="avatar-badge"></slot>
+ </template>
+ </component>
<discussion-notes-replies-wrapper :is-diff-discussion="discussion.diff_discussion">
<toggle-replies-widget
v-if="hasReplies"
diff --git a/app/assets/javascripts/notes/components/note_body.vue b/app/assets/javascripts/notes/components/note_body.vue
index c09582d6287..f465ad23a06 100644
--- a/app/assets/javascripts/notes/components/note_body.vue
+++ b/app/assets/javascripts/notes/components/note_body.vue
@@ -149,7 +149,7 @@ export default {
},
},
safeHtmlConfig: {
- ADD_TAGS: ['use', 'gl-emoji'],
+ ADD_TAGS: ['use', 'gl-emoji', 'copy-code'],
},
};
</script>
diff --git a/app/assets/javascripts/notes/components/noteable_discussion.vue b/app/assets/javascripts/notes/components/noteable_discussion.vue
index 77f796fe8b0..8e32c3b3073 100644
--- a/app/assets/javascripts/notes/components/noteable_discussion.vue
+++ b/app/assets/javascripts/notes/components/noteable_discussion.vue
@@ -223,17 +223,20 @@ export default {
})
.catch((err) => {
this.removePlaceholderNotes();
- const msg = __(
- 'Your comment could not be submitted! Please check your network connection and try again.',
- );
- createFlash({
- message: msg,
- parent: this.$el,
- });
+ this.handleSaveError(err); // The 'err' parameter is being used in JH, don't remove it
this.$refs.noteForm.note = noteText;
callback(err);
});
},
+ handleSaveError() {
+ const msg = __(
+ 'Your comment could not be submitted! Please check your network connection and try again.',
+ );
+ createFlash({
+ message: msg,
+ parent: this.$el,
+ });
+ },
deleteNoteHandler(note) {
this.$emit('noteDeleted', this.discussion, note);
},
@@ -280,6 +283,7 @@ export default {
v-if="showDraft(discussion.reply_id)"
:key="`draft_${discussion.id}`"
:draft="draftForDiscussion(discussion.reply_id)"
+ :line="line"
/>
<div
v-else-if="canShowReplyActions && showReplies"
diff --git a/app/assets/javascripts/notes/components/noteable_note.vue b/app/assets/javascripts/notes/components/noteable_note.vue
index e35d8d94289..3250a4818c7 100644
--- a/app/assets/javascripts/notes/components/noteable_note.vue
+++ b/app/assets/javascripts/notes/components/noteable_note.vue
@@ -331,17 +331,20 @@ export default {
this.isEditing = true;
this.setSelectedCommentPositionHover();
this.$nextTick(() => {
- const msg = __('Something went wrong while editing your comment. Please try again.');
- createFlash({
- message: msg,
- parent: this.$el,
- });
+ this.handleUpdateError(response); // The 'response' parameter is being used in JH, don't remove it
this.recoverNoteContent(noteText);
callback();
});
}
});
},
+ handleUpdateError() {
+ const msg = __('Something went wrong while editing your comment. Please try again.');
+ createFlash({
+ message: msg,
+ parent: this.$el,
+ });
+ },
formCancelHandler({ shouldConfirm, isDirty }) {
if (shouldConfirm && isDirty) {
// eslint-disable-next-line no-alert
@@ -388,7 +391,7 @@ export default {
<div
v-if="showMultiLineComment"
data-testid="multiline-comment"
- class="gl-mb-3 gl-text-gray-500 gl-border-gray-200 gl-border-b-solid gl-border-b-1 gl-pb-3"
+ class="gl-mb-5 gl-text-gray-500 gl-border-gray-100 gl-border-b-solid gl-border-b-1 gl-pb-4"
>
<gl-sprintf :message="__('Comment on lines %{startLine} to %{endLine}')">
<template #startLine>
diff --git a/app/assets/javascripts/notes/components/notes_app.vue b/app/assets/javascripts/notes/components/notes_app.vue
index 3ab3e7a20d4..c4924cd41f5 100644
--- a/app/assets/javascripts/notes/components/notes_app.vue
+++ b/app/assets/javascripts/notes/components/notes_app.vue
@@ -8,7 +8,6 @@ import TimelineEntryItem from '~/vue_shared/components/notes/timeline_entry_item
import OrderedLayout from '~/vue_shared/components/ordered_layout.vue';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import draftNote from '../../batch_comments/components/draft_note.vue';
-import { discussionIntersectionObserverHandlerFactory } from '../../diffs/utils/discussions';
import { getLocationHash, doesHashExistInUrl } from '../../lib/utils/url_utility';
import placeholderNote from '../../vue_shared/components/notes/placeholder_note.vue';
import placeholderSystemNote from '../../vue_shared/components/notes/placeholder_system_note.vue';
@@ -39,9 +38,6 @@ export default {
TimelineEntryItem,
},
mixins: [glFeatureFlagsMixin()],
- provide: {
- discussionObserverHandler: discussionIntersectionObserverHandlerFactory(),
- },
props: {
noteableData: {
type: Object,
@@ -108,6 +104,10 @@ export default {
});
}
+ if (this.sortDirDesc) {
+ return skeletonNotes.concat(this.discussions);
+ }
+
return this.discussions.concat(skeletonNotes);
},
canReply() {
diff --git a/app/assets/javascripts/notes/components/sidebar_subscription.vue b/app/assets/javascripts/notes/components/sidebar_subscription.vue
index 047c04c8482..52dadc7b4c3 100644
--- a/app/assets/javascripts/notes/components/sidebar_subscription.vue
+++ b/app/assets/javascripts/notes/components/sidebar_subscription.vue
@@ -1,6 +1,6 @@
<script>
import { mapActions } from 'vuex';
-import { IssuableType } from '~/issue_show/constants';
+import { IssuableType } from '~/issues/constants';
import { fetchPolicies } from '~/lib/graphql';
import { confidentialityQueries } from '~/sidebar/constants';
import { defaultClient as gqlClient } from '~/sidebar/graphql';
diff --git a/app/assets/javascripts/notes/i18n.js b/app/assets/javascripts/notes/i18n.js
index 1ffb94d11ad..951fa9733d4 100644
--- a/app/assets/javascripts/notes/i18n.js
+++ b/app/assets/javascripts/notes/i18n.js
@@ -9,15 +9,27 @@ export const COMMENT_FORM = {
issue: __('issue'),
startThread: __('Start thread'),
mergeRequest: __('merge request'),
+ epic: __('epic'),
bodyPlaceholder: __('Write a comment or drag your files here…'),
confidential: s__('Notes|Make this comment confidential'),
- confidentialVisibility: s__('Notes|Confidential comments are only visible to project members'),
+ confidentialVisibility: s__(
+ 'Notes|Confidential comments are only visible to members with the role of Reporter or higher',
+ ),
discussionThatNeedsResolution: __(
'Discuss a specific suggestion or question that needs to be resolved.',
),
discussion: __('Discuss a specific suggestion or question.'),
actionButtonWithNote: __('%{actionText} & %{openOrClose} %{noteable}'),
- actionButton: __('%{openOrClose} %{noteable}'),
+ actionButton: {
+ withNote: {
+ reopen: __('%{actionText} & reopen %{noteable}'),
+ close: __('%{actionText} & close %{noteable}'),
+ },
+ withoutNote: {
+ reopen: __('Reopen %{noteable}'),
+ close: __('Close %{noteable}'),
+ },
+ },
submitButton: {
startThread: __('Start thread'),
comment: __('Comment'),
diff --git a/app/assets/javascripts/notes/stores/actions.js b/app/assets/javascripts/notes/stores/actions.js
index c862a29ad9c..50b05ea9d69 100644
--- a/app/assets/javascripts/notes/stores/actions.js
+++ b/app/assets/javascripts/notes/stores/actions.js
@@ -601,7 +601,8 @@ export const setLoadingState = ({ commit }, data) => {
commit(types.SET_NOTES_LOADING_STATE, data);
};
-export const filterDiscussion = ({ dispatch }, { path, filter, persistFilter }) => {
+export const filterDiscussion = ({ commit, dispatch }, { path, filter, persistFilter }) => {
+ commit(types.CLEAR_DISCUSSIONS);
dispatch('setLoadingState', true);
dispatch('fetchDiscussions', { path, filter, persistFilter })
.then(() => {
diff --git a/app/assets/javascripts/notes/stores/mutation_types.js b/app/assets/javascripts/notes/stores/mutation_types.js
index fcd2846ff0d..ebda08a3d62 100644
--- a/app/assets/javascripts/notes/stores/mutation_types.js
+++ b/app/assets/javascripts/notes/stores/mutation_types.js
@@ -1,6 +1,7 @@
export const ADD_NEW_NOTE = 'ADD_NEW_NOTE';
export const ADD_NEW_REPLY_TO_DISCUSSION = 'ADD_NEW_REPLY_TO_DISCUSSION';
export const ADD_OR_UPDATE_DISCUSSIONS = 'ADD_OR_UPDATE_DISCUSSIONS';
+export const CLEAR_DISCUSSIONS = 'CLEAR_DISCUSSIONS';
export const DELETE_NOTE = 'DELETE_NOTE';
export const REMOVE_PLACEHOLDER_NOTES = 'REMOVE_PLACEHOLDER_NOTES';
export const SET_NOTES_DATA = 'SET_NOTES_DATA';
diff --git a/app/assets/javascripts/notes/stores/mutations.js b/app/assets/javascripts/notes/stores/mutations.js
index 1a99750ddb3..ba19ecd0c04 100644
--- a/app/assets/javascripts/notes/stores/mutations.js
+++ b/app/assets/javascripts/notes/stores/mutations.js
@@ -129,6 +129,10 @@ export default {
Object.assign(state, { userData: data });
},
+ [types.CLEAR_DISCUSSIONS](state) {
+ state.discussions = [];
+ },
+
[types.ADD_OR_UPDATE_DISCUSSIONS](state, discussionsData) {
discussionsData.forEach((d) => {
const discussion = { ...d };
diff --git a/app/assets/javascripts/packages/list/packages_list_app_bundle.js b/app/assets/javascripts/packages/list/packages_list_app_bundle.js
deleted file mode 100644
index 2911cf70a33..00000000000
--- a/app/assets/javascripts/packages/list/packages_list_app_bundle.js
+++ /dev/null
@@ -1,23 +0,0 @@
-import Vue from 'vue';
-import Translate from '~/vue_shared/translate';
-import PackagesListApp from './components/packages_list_app.vue';
-import { createStore } from './stores';
-
-Vue.use(Translate);
-
-export default () => {
- const el = document.getElementById('js-vue-packages-list');
- const store = createStore();
- store.dispatch('setInitialState', el.dataset);
-
- return new Vue({
- el,
- store,
- components: {
- PackagesListApp,
- },
- render(createElement) {
- return createElement('packages-list-app');
- },
- });
-};
diff --git a/app/assets/javascripts/packages/shared/constants.js b/app/assets/javascripts/packages/shared/constants.js
deleted file mode 100644
index c284b8358b4..00000000000
--- a/app/assets/javascripts/packages/shared/constants.js
+++ /dev/null
@@ -1,49 +0,0 @@
-import { s__ } from '~/locale';
-
-export const PackageType = {
- CONAN: 'conan',
- MAVEN: 'maven',
- NPM: 'npm',
- NUGET: 'nuget',
- PYPI: 'pypi',
- COMPOSER: 'composer',
- RUBYGEMS: 'rubygems',
- GENERIC: 'generic',
- DEBIAN: 'debian',
- HELM: 'helm',
-};
-
-// we want this separated from the main dictionary to avoid it being pulled in the search of package
-export const TERRAFORM_PACKAGE_TYPE = 'terraform_module';
-
-export const TrackingActions = {
- DELETE_PACKAGE: 'delete_package',
- REQUEST_DELETE_PACKAGE: 'request_delete_package',
- CANCEL_DELETE_PACKAGE: 'cancel_delete_package',
- PULL_PACKAGE: 'pull_package',
- DELETE_PACKAGE_FILE: 'delete_package_file',
- REQUEST_DELETE_PACKAGE_FILE: 'request_delete_package_file',
- CANCEL_DELETE_PACKAGE_FILE: 'cancel_delete_package_file',
-};
-
-export const TrackingCategories = {
- [PackageType.MAVEN]: 'MavenPackages',
- [PackageType.NPM]: 'NpmPackages',
- [PackageType.CONAN]: 'ConanPackages',
-};
-
-export const SHOW_DELETE_SUCCESS_ALERT = 'showSuccessDeleteAlert';
-export const DELETE_PACKAGE_ERROR_MESSAGE = s__(
- 'PackageRegistry|Something went wrong while deleting the package.',
-);
-export const DELETE_PACKAGE_FILE_ERROR_MESSAGE = s__(
- 'PackageRegistry|Something went wrong while deleting the package file.',
-);
-export const DELETE_PACKAGE_FILE_SUCCESS_MESSAGE = s__(
- 'PackageRegistry|Package file deleted successfully',
-);
-
-export const PACKAGE_ERROR_STATUS = 'error';
-export const PACKAGE_DEFAULT_STATUS = 'default';
-export const PACKAGE_HIDDEN_STATUS = 'hidden';
-export const PACKAGE_PROCESSING_STATUS = 'processing';
diff --git a/app/assets/javascripts/packages/shared/utils.js b/app/assets/javascripts/packages/shared/utils.js
deleted file mode 100644
index 7e86e5b2991..00000000000
--- a/app/assets/javascripts/packages/shared/utils.js
+++ /dev/null
@@ -1,43 +0,0 @@
-import { s__ } from '~/locale';
-import { PackageType, TrackingCategories } from './constants';
-
-export const packageTypeToTrackCategory = (type) =>
- // eslint-disable-next-line @gitlab/require-i18n-strings
- `UI::${TrackingCategories[type]}`;
-
-export const beautifyPath = (path) => (path ? path.split('/').join(' / ') : '');
-
-export const getPackageTypeLabel = (packageType) => {
- switch (packageType) {
- case PackageType.CONAN:
- return s__('PackageRegistry|Conan');
- case PackageType.MAVEN:
- return s__('PackageRegistry|Maven');
- case PackageType.NPM:
- return s__('PackageRegistry|npm');
- case PackageType.NUGET:
- return s__('PackageRegistry|NuGet');
- case PackageType.PYPI:
- return s__('PackageRegistry|PyPI');
- case PackageType.RUBYGEMS:
- return s__('PackageRegistry|RubyGems');
- case PackageType.COMPOSER:
- return s__('PackageRegistry|Composer');
- case PackageType.GENERIC:
- return s__('PackageRegistry|Generic');
- case PackageType.DEBIAN:
- return s__('PackageRegistry|Debian');
- case PackageType.HELM:
- return s__('PackageRegistry|Helm');
- default:
- return null;
- }
-};
-
-export const getCommitLink = ({ project_path: projectPath, pipeline = {} }, isGroup = false) => {
- if (isGroup) {
- return `/${projectPath}/commit/${pipeline.sha}`;
- }
-
- return `../commit/${pipeline.sha}`;
-};
diff --git a/app/assets/javascripts/packages_and_registries/container_registry/explorer/components/details_page/delete_modal.vue b/app/assets/javascripts/packages_and_registries/container_registry/explorer/components/details_page/delete_modal.vue
index f857c96c9d1..7a8a1bbcf09 100644
--- a/app/assets/javascripts/packages_and_registries/container_registry/explorer/components/details_page/delete_modal.vue
+++ b/app/assets/javascripts/packages_and_registries/container_registry/explorer/components/details_page/delete_modal.vue
@@ -82,6 +82,7 @@ export default {
ref="deleteModal"
modal-id="delete-tag-modal"
ok-variant="danger"
+ size="sm"
:action-primary="{
text: __('Delete'),
attributes: [{ variant: 'danger' }, { disabled: disablePrimaryButton }],
diff --git a/app/assets/javascripts/packages_and_registries/container_registry/explorer/components/details_page/details_header.vue b/app/assets/javascripts/packages_and_registries/container_registry/explorer/components/details_page/details_header.vue
index e9e36151fe6..d988ad8d8ca 100644
--- a/app/assets/javascripts/packages_and_registries/container_registry/explorer/components/details_page/details_header.vue
+++ b/app/assets/javascripts/packages_and_registries/container_registry/explorer/components/details_page/details_header.vue
@@ -46,7 +46,6 @@ export default {
data() {
return {
containerRepository: {},
- fetchTagsCount: false,
};
},
apollo: {
diff --git a/app/assets/javascripts/packages_and_registries/container_registry/explorer/components/details_page/tags_list.vue b/app/assets/javascripts/packages_and_registries/container_registry/explorer/components/details_page/tags_list.vue
index 3e19a646f53..2d32295b537 100644
--- a/app/assets/javascripts/packages_and_registries/container_registry/explorer/components/details_page/tags_list.vue
+++ b/app/assets/javascripts/packages_and_registries/container_registry/explorer/components/details_page/tags_list.vue
@@ -1,7 +1,8 @@
<script>
-import { GlButton, GlKeysetPagination } from '@gitlab/ui';
import createFlash from '~/flash';
+import { n__ } from '~/locale';
import { joinPaths } from '~/lib/utils/url_utility';
+import RegistryList from '~/packages_and_registries/shared/components/registry_list.vue';
import {
REMOVE_TAGS_BUTTON_TITLE,
TAGS_LIST_TITLE,
@@ -16,11 +17,10 @@ import TagsLoader from './tags_loader.vue';
export default {
name: 'TagsList',
components: {
- GlButton,
- GlKeysetPagination,
TagsListRow,
EmptyState,
TagsLoader,
+ RegistryList,
},
inject: ['config'],
props: {
@@ -61,11 +61,13 @@ export default {
},
data() {
return {
- selectedItems: {},
containerRepository: {},
};
},
computed: {
+ listTitle() {
+ return n__('%d tag', '%d tags', this.tags.length);
+ },
tags() {
return this.containerRepository?.tags?.nodes || [];
},
@@ -78,18 +80,9 @@ export default {
first: GRAPHQL_PAGE_SIZE,
};
},
- hasSelectedItems() {
- return this.tags.some((tag) => this.selectedItems[tag.name]);
- },
showMultiDeleteButton() {
return this.tags.some((tag) => tag.canDelete) && !this.isMobile;
},
- multiDeleteButtonIsDisabled() {
- return !this.hasSelectedItems || this.disabled;
- },
- showPagination() {
- return this.tagsPageInfo.hasPreviousPage || this.tagsPageInfo.hasNextPage;
- },
hasNoTags() {
return this.tags.length === 0;
},
@@ -98,19 +91,13 @@ export default {
},
},
methods: {
- updateSelectedItems(name) {
- this.$set(this.selectedItems, name, !this.selectedItems[name]);
- },
- mapTagsToBeDleeted(items) {
- return this.tags.filter((tag) => items[tag.name]);
- },
fetchNextPage() {
this.$apollo.queries.containerRepository.fetchMore({
variables: {
after: this.tagsPageInfo?.endCursor,
first: GRAPHQL_PAGE_SIZE,
},
- updateQuery(previousResult, { fetchMoreResult }) {
+ updateQuery(_, { fetchMoreResult }) {
return fetchMoreResult;
},
});
@@ -122,7 +109,7 @@ export default {
before: this.tagsPageInfo?.startCursor,
last: GRAPHQL_PAGE_SIZE,
},
- updateQuery(previousResult, { fetchMoreResult }) {
+ updateQuery(_, { fetchMoreResult }) {
return fetchMoreResult;
},
});
@@ -137,42 +124,27 @@ export default {
<template v-else>
<empty-state v-if="hasNoTags" :no-containers-image="config.noContainersImage" />
<template v-else>
- <div class="gl-display-flex gl-justify-content-space-between gl-mb-3">
- <h5 data-testid="list-title">
- {{ $options.i18n.TAGS_LIST_TITLE }}
- </h5>
-
- <gl-button
- v-if="showMultiDeleteButton"
- :disabled="multiDeleteButtonIsDisabled"
- category="secondary"
- variant="danger"
- @click="$emit('delete', mapTagsToBeDleeted(selectedItems))"
- >
- {{ $options.i18n.REMOVE_TAGS_BUTTON_TITLE }}
- </gl-button>
- </div>
- <tags-list-row
- v-for="(tag, index) in tags"
- :key="tag.path"
- :tag="tag"
- :first="index === 0"
- :selected="selectedItems[tag.name]"
- :is-mobile="isMobile"
- :disabled="disabled"
- @select="updateSelectedItems(tag.name)"
- @delete="$emit('delete', mapTagsToBeDleeted({ [tag.name]: true }))"
- />
- <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>
+ <registry-list
+ :title="listTitle"
+ :pagination="tagsPageInfo"
+ :items="tags"
+ id-property="name"
+ @prev-page="fetchPreviousPage"
+ @next-page="fetchNextPage"
+ @delete="$emit('delete', $event)"
+ >
+ <template #default="{ selectItem, isSelected, item, first }">
+ <tags-list-row
+ :tag="item"
+ :first="first"
+ :selected="isSelected(item)"
+ :is-mobile="isMobile"
+ :disabled="disabled"
+ @select="selectItem(item)"
+ @delete="$emit('delete', [item])"
+ />
+ </template>
+ </registry-list>
</template>
</template>
</div>
diff --git a/app/assets/javascripts/packages_and_registries/container_registry/explorer/graphql/queries/get_container_repositories_details.query.graphql b/app/assets/javascripts/packages_and_registries/container_registry/explorer/graphql/queries/get_container_repositories_details.query.graphql
index 01cb7fa1cab..bc34e9b5ef2 100644
--- a/app/assets/javascripts/packages_and_registries/container_registry/explorer/graphql/queries/get_container_repositories_details.query.graphql
+++ b/app/assets/javascripts/packages_and_registries/container_registry/explorer/graphql/queries/get_container_repositories_details.query.graphql
@@ -9,6 +9,7 @@ query getContainerRepositoriesDetails(
$sort: ContainerRepositorySort
) {
project(fullPath: $fullPath) @skip(if: $isGroupPage) {
+ id
containerRepositories(
name: $name
after: $after
@@ -24,6 +25,7 @@ query getContainerRepositoriesDetails(
}
}
group(fullPath: $fullPath) @include(if: $isGroupPage) {
+ id
containerRepositories(
name: $name
after: $after
diff --git a/app/assets/javascripts/packages_and_registries/container_registry/explorer/graphql/queries/get_container_repository_details.query.graphql b/app/assets/javascripts/packages_and_registries/container_registry/explorer/graphql/queries/get_container_repository_details.query.graphql
index b5a99fd9ac1..916740f41b8 100644
--- a/app/assets/javascripts/packages_and_registries/container_registry/explorer/graphql/queries/get_container_repository_details.query.graphql
+++ b/app/assets/javascripts/packages_and_registries/container_registry/explorer/graphql/queries/get_container_repository_details.query.graphql
@@ -11,6 +11,7 @@ query getContainerRepositoryDetails($id: ID!) {
expirationPolicyStartedAt
expirationPolicyCleanupStatus
project {
+ id
visibility
path
containerExpirationPolicy {
diff --git a/app/assets/javascripts/packages_and_registries/container_registry/explorer/graphql/queries/get_container_repository_tags.query.graphql b/app/assets/javascripts/packages_and_registries/container_registry/explorer/graphql/queries/get_container_repository_tags.query.graphql
index a703c2dd0ac..502382010f9 100644
--- a/app/assets/javascripts/packages_and_registries/container_registry/explorer/graphql/queries/get_container_repository_tags.query.graphql
+++ b/app/assets/javascripts/packages_and_registries/container_registry/explorer/graphql/queries/get_container_repository_tags.query.graphql
@@ -9,6 +9,7 @@ query getContainerRepositoryTags(
) {
containerRepository(id: $id) {
id
+ tagsCount
tags(after: $after, before: $before, first: $first, last: $last) {
nodes {
digest
diff --git a/app/assets/javascripts/packages_and_registries/container_registry/explorer/pages/details.vue b/app/assets/javascripts/packages_and_registries/container_registry/explorer/pages/details.vue
index feabc4f770b..bc6e3091f0e 100644
--- a/app/assets/javascripts/packages_and_registries/container_registry/explorer/pages/details.vue
+++ b/app/assets/javascripts/packages_and_registries/container_registry/explorer/pages/details.vue
@@ -25,9 +25,11 @@ import {
UNFINISHED_STATUS,
MISSING_OR_DELETED_IMAGE_BREADCRUMB,
ROOT_IMAGE_TEXT,
+ GRAPHQL_PAGE_SIZE,
} from '../constants/index';
import deleteContainerRepositoryTagsMutation from '../graphql/mutations/delete_container_repository_tags.mutation.graphql';
import getContainerRepositoryDetailsQuery from '../graphql/queries/get_container_repository_details.query.graphql';
+import getContainerRepositoryTagsQuery from '../graphql/queries/get_container_repository_tags.query.graphql';
export default {
name: 'RegistryDetailsPage',
@@ -133,8 +135,8 @@ export default {
awaitRefetchQueries: true,
refetchQueries: [
{
- query: getContainerRepositoryDetailsQuery,
- variables: this.queryVariables,
+ query: getContainerRepositoryTagsQuery,
+ variables: { ...this.queryVariables, first: GRAPHQL_PAGE_SIZE },
},
],
});
diff --git a/app/assets/javascripts/packages_and_registries/container_registry/explorer/pages/list.vue b/app/assets/javascripts/packages_and_registries/container_registry/explorer/pages/list.vue
index 73b957f42f2..3274de05803 100644
--- a/app/assets/javascripts/packages_and_registries/container_registry/explorer/pages/list.vue
+++ b/app/assets/javascripts/packages_and_registries/container_registry/explorer/pages/list.vue
@@ -388,6 +388,7 @@ export default {
<template #default="{ doDelete }">
<gl-modal
ref="deleteModal"
+ size="sm"
modal-id="delete-image-modal"
:action-primary="{ text: __('Remove'), attributes: { variant: 'danger' } }"
@primary="doDelete"
diff --git a/app/assets/javascripts/packages_and_registries/dependency_proxy/app.vue b/app/assets/javascripts/packages_and_registries/dependency_proxy/app.vue
index 71e8cf4f634..eb112238c11 100644
--- a/app/assets/javascripts/packages_and_registries/dependency_proxy/app.vue
+++ b/app/assets/javascripts/packages_and_registries/dependency_proxy/app.vue
@@ -1,11 +1,11 @@
<script>
import {
GlAlert,
+ GlEmptyState,
GlFormGroup,
GlFormInputGroup,
GlSkeletonLoader,
GlSprintf,
- GlEmptyState,
} from '@gitlab/ui';
import { s__ } from '~/locale';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
@@ -36,15 +36,15 @@ export default {
proxyNotAvailableText: s__(
'DependencyProxy|Dependency Proxy feature is limited to public groups for now.',
),
- proxyDisabledText: s__(
- 'DependencyProxy|Dependency Proxy disabled. To enable it, contact the group owner.',
- ),
proxyImagePrefix: s__('DependencyProxy|Dependency Proxy image prefix'),
copyImagePrefixText: s__('DependencyProxy|Copy prefix'),
blobCountAndSize: s__('DependencyProxy|Contains %{count} blobs of images (%{size})'),
pageTitle: s__('DependencyProxy|Dependency Proxy'),
noManifestTitle: s__('DependencyProxy|There are no images in the cache'),
},
+ links: {
+ DEPENDENCY_PROXY_DOCS_PATH,
+ },
data() {
return {
group: {},
@@ -70,9 +70,7 @@ export default {
},
];
},
- dependencyProxyEnabled() {
- return this.group?.dependencyProxySetting?.enabled;
- },
+
queryVariables() {
return { fullPath: this.groupPath, first: GRAPHQL_PAGE_SIZE };
},
@@ -122,7 +120,7 @@ export default {
<gl-skeleton-loader v-else-if="$apollo.queries.group.loading" />
- <div v-else-if="dependencyProxyEnabled" data-testid="main-area">
+ <div v-else data-testid="main-area">
<gl-form-group :label="$options.i18n.proxyImagePrefix">
<gl-form-input-group
readonly
@@ -161,8 +159,5 @@ export default {
:title="$options.i18n.noManifestTitle"
/>
</div>
- <gl-alert v-else :dismissible="false" data-testid="proxy-disabled">
- {{ $options.i18n.proxyDisabledText }}
- </gl-alert>
</div>
</template>
diff --git a/app/assets/javascripts/packages_and_registries/dependency_proxy/graphql/queries/get_dependency_proxy_details.query.graphql b/app/assets/javascripts/packages_and_registries/dependency_proxy/graphql/queries/get_dependency_proxy_details.query.graphql
index 63d5469c955..9241dccb2d5 100644
--- a/app/assets/javascripts/packages_and_registries/dependency_proxy/graphql/queries/get_dependency_proxy_details.query.graphql
+++ b/app/assets/javascripts/packages_and_registries/dependency_proxy/graphql/queries/get_dependency_proxy_details.query.graphql
@@ -8,6 +8,7 @@ query getDependencyProxyDetails(
$before: String
) {
group(fullPath: $fullPath) {
+ id
dependencyProxyBlobCount
dependencyProxyTotalSize
dependencyProxyImagePrefix
@@ -16,6 +17,7 @@ query getDependencyProxyDetails(
}
dependencyProxyManifests(after: $after, before: $before, first: $first, last: $last) {
nodes {
+ id
createdAt
imageName
}
diff --git a/app/assets/javascripts/packages_and_registries/infrastructure_registry/details/components/app.vue b/app/assets/javascripts/packages_and_registries/infrastructure_registry/details/components/app.vue
index 6016757c1b9..f198d2e1bfa 100644
--- a/app/assets/javascripts/packages_and_registries/infrastructure_registry/details/components/app.vue
+++ b/app/assets/javascripts/packages_and_registries/infrastructure_registry/details/components/app.vue
@@ -16,10 +16,13 @@ import { s__, __ } from '~/locale';
import TerraformTitle from '~/packages_and_registries/infrastructure_registry/details/components/details_title.vue';
import TerraformInstallation from '~/packages_and_registries/infrastructure_registry/details/components/terraform_installation.vue';
import Tracking from '~/tracking';
-import PackageListRow from '~/packages/shared/components/package_list_row.vue';
-import PackagesListLoader from '~/packages/shared/components/packages_list_loader.vue';
-import { TrackingActions, SHOW_DELETE_SUCCESS_ALERT } from '~/packages/shared/constants';
-import { packageTypeToTrackCategory } from '~/packages/shared/utils';
+import PackageListRow from '~/packages_and_registries/infrastructure_registry/shared/package_list_row.vue';
+import PackagesListLoader from '~/packages_and_registries/shared/components/packages_list_loader.vue';
+import {
+ TRACKING_ACTIONS,
+ SHOW_DELETE_SUCCESS_ALERT,
+} from '~/packages_and_registries/shared/constants';
+import { TRACK_CATEGORY } from '~/packages_and_registries/infrastructure_registry/shared/constants';
import PackageFiles from './package_files.vue';
import PackageHistory from './package_history.vue';
@@ -44,7 +47,7 @@ export default {
GlModal: GlModalDirective,
},
mixins: [Tracking.mixin()],
- trackingActions: { ...TrackingActions },
+ trackingActions: { ...TRACKING_ACTIONS },
data() {
return {
fileToDelete: null,
@@ -68,7 +71,7 @@ export default {
},
tracking() {
return {
- category: packageTypeToTrackCategory(this.packageEntity.package_type),
+ category: TRACK_CATEGORY,
};
},
hasVersions() {
@@ -86,7 +89,7 @@ export default {
}
},
async confirmPackageDeletion() {
- this.track(TrackingActions.DELETE_PACKAGE);
+ this.track(TRACKING_ACTIONS.DELETE_PACKAGE);
await this.deletePackage();
const returnTo =
!this.groupListUrl || document.referrer.includes(this.projectName)
@@ -96,12 +99,12 @@ export default {
window.location.replace(`${returnTo}?${modalQuery}`);
},
handleFileDelete(file) {
- this.track(TrackingActions.REQUEST_DELETE_PACKAGE_FILE);
+ this.track(TRACKING_ACTIONS.REQUEST_DELETE_PACKAGE_FILE);
this.fileToDelete = { ...file };
this.$refs.deleteFileModal.show();
},
confirmFileDelete() {
- this.track(TrackingActions.DELETE_PACKAGE_FILE);
+ this.track(TRACKING_ACTIONS.DELETE_PACKAGE_FILE);
this.deletePackageFile(this.fileToDelete.id);
this.fileToDelete = null;
},
@@ -203,6 +206,7 @@ export default {
<gl-modal
ref="deleteModal"
+ size="sm"
modal-id="delete-modal"
:action-primary="$options.modal.packageDeletePrimaryAction"
:action-cancel="$options.modal.cancelAction"
@@ -223,6 +227,7 @@ export default {
<gl-modal
ref="deleteFileModal"
+ size="sm"
modal-id="delete-file-modal"
:action-primary="$options.modal.fileDeletePrimaryAction"
:action-cancel="$options.modal.cancelAction"
diff --git a/app/assets/javascripts/packages_and_registries/infrastructure_registry/details/store/actions.js b/app/assets/javascripts/packages_and_registries/infrastructure_registry/details/store/actions.js
index a03fa8d9d63..26d4aa13715 100644
--- a/app/assets/javascripts/packages_and_registries/infrastructure_registry/details/store/actions.js
+++ b/app/assets/javascripts/packages_and_registries/infrastructure_registry/details/store/actions.js
@@ -4,7 +4,7 @@ import {
DELETE_PACKAGE_ERROR_MESSAGE,
DELETE_PACKAGE_FILE_ERROR_MESSAGE,
DELETE_PACKAGE_FILE_SUCCESS_MESSAGE,
-} from '~/packages/shared/constants';
+} from '~/packages_and_registries/shared/constants';
import { FETCH_PACKAGE_VERSIONS_ERROR } from '../constants';
import * as types from './mutation_types';
diff --git a/app/assets/javascripts/packages_and_registries/infrastructure_registry/components/infrastructure_search.vue b/app/assets/javascripts/packages_and_registries/infrastructure_registry/list/components/infrastructure_search.vue
index 4928da862ea..c611f92036d 100644
--- a/app/assets/javascripts/packages_and_registries/infrastructure_registry/components/infrastructure_search.vue
+++ b/app/assets/javascripts/packages_and_registries/infrastructure_registry/list/components/infrastructure_search.vue
@@ -1,7 +1,7 @@
<script>
import { mapState, mapActions } from 'vuex';
-import { LIST_KEY_PACKAGE_TYPE } from '~/packages/list/constants';
-import { sortableFields } from '~/packages/list/utils';
+import { LIST_KEY_PACKAGE_TYPE } from '~/packages_and_registries/infrastructure_registry/list/constants';
+import { sortableFields } from '~/packages_and_registries/infrastructure_registry/list/utils';
import RegistrySearch from '~/vue_shared/components/registry/registry_search.vue';
import UrlSync from '~/vue_shared/components/url_sync.vue';
diff --git a/app/assets/javascripts/packages_and_registries/infrastructure_registry/components/infrastructure_title.vue b/app/assets/javascripts/packages_and_registries/infrastructure_registry/list/components/infrastructure_title.vue
index 2a479c65d0c..2a479c65d0c 100644
--- a/app/assets/javascripts/packages_and_registries/infrastructure_registry/components/infrastructure_title.vue
+++ b/app/assets/javascripts/packages_and_registries/infrastructure_registry/list/components/infrastructure_title.vue
diff --git a/app/assets/javascripts/packages/list/components/packages_list.vue b/app/assets/javascripts/packages_and_registries/infrastructure_registry/list/components/packages_list.vue
index 23ba070aa26..a5f367bc1f6 100644
--- a/app/assets/javascripts/packages/list/components/packages_list.vue
+++ b/app/assets/javascripts/packages_and_registries/infrastructure_registry/list/components/packages_list.vue
@@ -3,10 +3,10 @@ import { GlPagination, GlModal, GlSprintf } from '@gitlab/ui';
import { mapState, mapGetters } from 'vuex';
import { s__ } from '~/locale';
import Tracking from '~/tracking';
-import PackagesListRow from '../../shared/components/package_list_row.vue';
-import PackagesListLoader from '../../shared/components/packages_list_loader.vue';
-import { TrackingActions } from '../../shared/constants';
-import { packageTypeToTrackCategory } from '../../shared/utils';
+import PackagesListRow from '~/packages_and_registries/infrastructure_registry/shared/package_list_row.vue';
+import PackagesListLoader from '~/packages_and_registries/shared/components/packages_list_loader.vue';
+import { TRACKING_ACTIONS } from '~/packages_and_registries/shared/constants';
+import { TRACK_CATEGORY } from '~/packages_and_registries/infrastructure_registry/shared/constants';
export default {
components: {
@@ -49,27 +49,24 @@ export default {
return this.itemToBeDeleted?.name ?? '';
},
tracking() {
- const category = this.itemToBeDeleted
- ? packageTypeToTrackCategory(this.itemToBeDeleted.package_type)
- : undefined;
return {
- category,
+ category: TRACK_CATEGORY,
};
},
},
methods: {
setItemToBeDeleted(item) {
this.itemToBeDeleted = { ...item };
- this.track(TrackingActions.REQUEST_DELETE_PACKAGE);
+ this.track(TRACKING_ACTIONS.REQUEST_DELETE_PACKAGE);
this.$refs.packageListDeleteModal.show();
},
deleteItemConfirmation() {
this.$emit('package:delete', this.itemToBeDeleted);
- this.track(TrackingActions.DELETE_PACKAGE);
+ this.track(TRACKING_ACTIONS.DELETE_PACKAGE);
this.itemToBeDeleted = null;
},
deleteItemCanceled() {
- this.track(TrackingActions.CANCEL_DELETE_PACKAGE);
+ this.track(TRACKING_ACTIONS.CANCEL_DELETE_PACKAGE);
this.itemToBeDeleted = null;
},
},
@@ -111,6 +108,7 @@ export default {
<gl-modal
ref="packageListDeleteModal"
+ size="sm"
modal-id="confirm-delete-pacakge"
ok-variant="danger"
@ok="deleteItemConfirmation"
diff --git a/app/assets/javascripts/packages/list/components/packages_list_app.vue b/app/assets/javascripts/packages_and_registries/infrastructure_registry/list/components/packages_list_app.vue
index 31d90fa4dee..462618a7f12 100644
--- a/app/assets/javascripts/packages/list/components/packages_list_app.vue
+++ b/app/assets/javascripts/packages_and_registries/infrastructure_registry/list/components/packages_list_app.vue
@@ -4,13 +4,16 @@ import { mapActions, mapState } from 'vuex';
import createFlash from '~/flash';
import { historyReplaceState } from '~/lib/utils/common_utils';
import { s__ } from '~/locale';
-import { SHOW_DELETE_SUCCESS_ALERT } from '~/packages/shared/constants';
-import { FILTERED_SEARCH_TERM } from '~/packages_and_registries/shared/constants';
+import {
+ SHOW_DELETE_SUCCESS_ALERT,
+ FILTERED_SEARCH_TERM,
+} from '~/packages_and_registries/shared/constants';
+
import { getQueryParams, extractFilterAndSorting } from '~/packages_and_registries/shared/utils';
-import InfrastructureTitle from '~/packages_and_registries/infrastructure_registry/components/infrastructure_title.vue';
-import InfrastructureSearch from '~/packages_and_registries/infrastructure_registry/components/infrastructure_search.vue';
-import { DELETE_PACKAGE_SUCCESS_MESSAGE } from '../constants';
-import PackageList from './packages_list.vue';
+import InfrastructureTitle from '~/packages_and_registries/infrastructure_registry/list/components/infrastructure_title.vue';
+import InfrastructureSearch from '~/packages_and_registries/infrastructure_registry/list/components/infrastructure_search.vue';
+import PackageList from '~/packages_and_registries/infrastructure_registry/list/components/packages_list.vue';
+import { DELETE_PACKAGE_SUCCESS_MESSAGE } from '~/packages_and_registries/infrastructure_registry/list/constants';
export default {
components: {
diff --git a/app/assets/javascripts/packages/list/constants.js b/app/assets/javascripts/packages_and_registries/infrastructure_registry/list/constants.js
index 4f5071e784b..7af3fc1c2db 100644
--- a/app/assets/javascripts/packages/list/constants.js
+++ b/app/assets/javascripts/packages_and_registries/infrastructure_registry/list/constants.js
@@ -1,10 +1,8 @@
-import { __, s__ } from '~/locale';
-import { PackageType } from '../shared/constants';
+import { __ } from '~/locale';
export const FETCH_PACKAGES_LIST_ERROR_MESSAGE = __(
'Something went wrong while fetching the packages list.',
);
-export const FETCH_PACKAGE_ERROR_MESSAGE = __('Something went wrong while fetching the package.');
export const DELETE_PACKAGE_SUCCESS_MESSAGE = __('Package deleted successfully');
export const DEFAULT_PAGE = 1;
@@ -17,14 +15,12 @@ export const LIST_KEY_PROJECT = 'project_path';
export const LIST_KEY_VERSION = 'version';
export const LIST_KEY_PACKAGE_TYPE = 'type';
export const LIST_KEY_CREATED_AT = 'created_at';
-export const LIST_KEY_ACTIONS = 'actions';
export const LIST_LABEL_NAME = __('Name');
export const LIST_LABEL_PROJECT = __('Project');
export const LIST_LABEL_VERSION = __('Version');
export const LIST_LABEL_PACKAGE_TYPE = __('Type');
export const LIST_LABEL_CREATED_AT = __('Published');
-export const LIST_LABEL_ACTIONS = '';
// The following is not translated because it is used to build a JavaScript exception error message
export const MISSING_DELETE_PATH_ERROR = 'Missing delete_api_path link';
@@ -52,48 +48,4 @@ export const SORT_FIELDS = [
},
];
-export const PACKAGE_TYPES = [
- {
- title: s__('PackageRegistry|Composer'),
- type: PackageType.COMPOSER,
- },
- {
- title: s__('PackageRegistry|Conan'),
- type: PackageType.CONAN,
- },
- {
- title: s__('PackageRegistry|Generic'),
- type: PackageType.GENERIC,
- },
-
- {
- title: s__('PackageRegistry|Maven'),
- type: PackageType.MAVEN,
- },
- {
- title: s__('PackageRegistry|npm'),
- type: PackageType.NPM,
- },
- {
- title: s__('PackageRegistry|NuGet'),
- type: PackageType.NUGET,
- },
- {
- title: s__('PackageRegistry|PyPI'),
- type: PackageType.PYPI,
- },
- {
- title: s__('PackageRegistry|RubyGems'),
- type: PackageType.RUBYGEMS,
- },
- {
- title: s__('PackageRegistry|Debian'),
- type: PackageType.DEBIAN,
- },
- {
- title: s__('PackageRegistry|Helm'),
- type: PackageType.HELM,
- },
-];
-
export const TERRAFORM_SEARCH_TYPE = Object.freeze({ value: { data: 'terraform_module' } });
diff --git a/app/assets/javascripts/packages/list/stores/actions.js b/app/assets/javascripts/packages_and_registries/infrastructure_registry/list/stores/actions.js
index 81f587971c2..488860e5bc2 100644
--- a/app/assets/javascripts/packages/list/stores/actions.js
+++ b/app/assets/javascripts/packages_and_registries/infrastructure_registry/list/stores/actions.js
@@ -1,7 +1,7 @@
import Api from '~/api';
import createFlash from '~/flash';
import axios from '~/lib/utils/axios_utils';
-import { DELETE_PACKAGE_ERROR_MESSAGE } from '~/packages/shared/constants';
+import { DELETE_PACKAGE_ERROR_MESSAGE } from '~/packages_and_registries/shared/constants';
import {
FETCH_PACKAGES_LIST_ERROR_MESSAGE,
DELETE_PACKAGE_SUCCESS_MESSAGE,
diff --git a/app/assets/javascripts/packages/list/stores/getters.js b/app/assets/javascripts/packages_and_registries/infrastructure_registry/list/stores/getters.js
index 482c111b58b..5989303280e 100644
--- a/app/assets/javascripts/packages/list/stores/getters.js
+++ b/app/assets/javascripts/packages_and_registries/infrastructure_registry/list/stores/getters.js
@@ -1,4 +1,4 @@
-import { beautifyPath } from '../../shared/utils';
+import { beautifyPath } from '~/packages_and_registries/shared/utils';
import { LIST_KEY_PROJECT } from '../constants';
export default (state) =>
diff --git a/app/assets/javascripts/packages/list/stores/index.js b/app/assets/javascripts/packages_and_registries/infrastructure_registry/list/stores/index.js
index 1d6a4bf831d..1d6a4bf831d 100644
--- a/app/assets/javascripts/packages/list/stores/index.js
+++ b/app/assets/javascripts/packages_and_registries/infrastructure_registry/list/stores/index.js
diff --git a/app/assets/javascripts/packages/list/stores/mutation_types.js b/app/assets/javascripts/packages_and_registries/infrastructure_registry/list/stores/mutation_types.js
index 561ad97f7e3..561ad97f7e3 100644
--- a/app/assets/javascripts/packages/list/stores/mutation_types.js
+++ b/app/assets/javascripts/packages_and_registries/infrastructure_registry/list/stores/mutation_types.js
diff --git a/app/assets/javascripts/packages/list/stores/mutations.js b/app/assets/javascripts/packages_and_registries/infrastructure_registry/list/stores/mutations.js
index 98165e581b0..98165e581b0 100644
--- a/app/assets/javascripts/packages/list/stores/mutations.js
+++ b/app/assets/javascripts/packages_and_registries/infrastructure_registry/list/stores/mutations.js
diff --git a/app/assets/javascripts/packages/list/stores/state.js b/app/assets/javascripts/packages_and_registries/infrastructure_registry/list/stores/state.js
index 60f02eddc9f..60f02eddc9f 100644
--- a/app/assets/javascripts/packages/list/stores/state.js
+++ b/app/assets/javascripts/packages_and_registries/infrastructure_registry/list/stores/state.js
diff --git a/app/assets/javascripts/packages/list/utils.js b/app/assets/javascripts/packages_and_registries/infrastructure_registry/list/utils.js
index 537b30d2ca4..537b30d2ca4 100644
--- a/app/assets/javascripts/packages/list/utils.js
+++ b/app/assets/javascripts/packages_and_registries/infrastructure_registry/list/utils.js
diff --git a/app/assets/javascripts/packages_and_registries/infrastructure_registry/list_app_bundle.js b/app/assets/javascripts/packages_and_registries/infrastructure_registry/list_app_bundle.js
index 7e6e98f4fb5..1467218dd41 100644
--- a/app/assets/javascripts/packages_and_registries/infrastructure_registry/list_app_bundle.js
+++ b/app/assets/javascripts/packages_and_registries/infrastructure_registry/list_app_bundle.js
@@ -1,7 +1,7 @@
import Vue from 'vue';
import { s__ } from '~/locale';
-import PackagesListApp from '~/packages/list/components/packages_list_app.vue';
-import { createStore } from '~/packages/list/stores';
+import PackagesListApp from '~/packages_and_registries/infrastructure_registry/list/components/packages_list_app.vue';
+import { createStore } from '~/packages_and_registries/infrastructure_registry/list/stores';
import Translate from '~/vue_shared/translate';
Vue.use(Translate);
@@ -18,9 +18,6 @@ export default () => {
PackagesListApp,
},
provide: {
- titleComponent: 'InfrastructureTitle',
- searchComponent: 'InfrastructureSearch',
- iconComponent: 'InfrastructureIconAndName',
emptyPageTitle: s__('InfrastructureRegistry|You have no Terraform modules in your project'),
noResultsText: s__(
'InfrastructureRegistry|Terraform modules are the main way to package and reuse resource configurations with Terraform. Learn more about how to %{noPackagesLinkStart}create Terraform modules%{noPackagesLinkEnd} in GitLab.',
diff --git a/app/assets/javascripts/packages_and_registries/infrastructure_registry/shared/constants.js b/app/assets/javascripts/packages_and_registries/infrastructure_registry/shared/constants.js
new file mode 100644
index 00000000000..ab52ec01d40
--- /dev/null
+++ b/app/assets/javascripts/packages_and_registries/infrastructure_registry/shared/constants.js
@@ -0,0 +1 @@
+export const TRACK_CATEGORY = 'UI::TerraformPackages';
diff --git a/app/assets/javascripts/packages_and_registries/infrastructure_registry/components/infrastructure_icon_and_name.vue b/app/assets/javascripts/packages_and_registries/infrastructure_registry/shared/infrastructure_icon_and_name.vue
index 3100a1a7296..3100a1a7296 100644
--- a/app/assets/javascripts/packages_and_registries/infrastructure_registry/components/infrastructure_icon_and_name.vue
+++ b/app/assets/javascripts/packages_and_registries/infrastructure_registry/shared/infrastructure_icon_and_name.vue
diff --git a/app/assets/javascripts/packages/shared/components/package_list_row.vue b/app/assets/javascripts/packages_and_registries/infrastructure_registry/shared/package_list_row.vue
index eee0e470c7b..3c6b8344c34 100644
--- a/app/assets/javascripts/packages/shared/components/package_list_row.vue
+++ b/app/assets/javascripts/packages_and_registries/infrastructure_registry/shared/package_list_row.vue
@@ -3,11 +3,14 @@ import { GlButton, GlLink, GlSprintf, GlTooltipDirective, GlTruncate } from '@gi
import { s__ } from '~/locale';
import ListItem from '~/vue_shared/components/registry/list_item.vue';
import timeagoMixin from '~/vue_shared/mixins/timeago';
-import { PACKAGE_ERROR_STATUS, PACKAGE_DEFAULT_STATUS } from '../constants';
-import { getPackageTypeLabel } from '../utils';
-import PackagePath from './package_path.vue';
-import PackageTags from './package_tags.vue';
-import PublishMethod from './publish_method.vue';
+import {
+ PACKAGE_ERROR_STATUS,
+ PACKAGE_DEFAULT_STATUS,
+} from '~/packages_and_registries/shared/constants';
+import PackagePath from '~/packages_and_registries/shared/components/package_path.vue';
+import PackageTags from '~/packages_and_registries/shared/components/package_tags.vue';
+import PublishMethod from '~/packages_and_registries/shared/components/publish_method.vue';
+import InfrastructureIconAndName from '~/packages_and_registries/infrastructure_registry/shared/infrastructure_icon_and_name.vue';
export default {
name: 'PackageListRow',
@@ -20,23 +23,12 @@ export default {
PackagePath,
PublishMethod,
ListItem,
- PackageIconAndName: () =>
- import(/* webpackChunkName: 'package_registry_components' */ './package_icon_and_name.vue'),
- InfrastructureIconAndName: () =>
- import(
- /* webpackChunkName: 'infrastructure_registry_components' */ '~/packages_and_registries/infrastructure_registry/components/infrastructure_icon_and_name.vue'
- ),
+ InfrastructureIconAndName,
},
directives: {
GlTooltip: GlTooltipDirective,
},
mixins: [timeagoMixin],
- inject: {
- iconComponent: {
- from: 'iconComponent',
- default: 'PackageIconAndName',
- },
- },
props: {
packageEntity: {
type: Object,
@@ -63,9 +55,6 @@ export default {
},
},
computed: {
- packageType() {
- return getPackageTypeLabel(this.packageEntity.package_type);
- },
hasPipeline() {
return Boolean(this.packageEntity.pipeline);
},
@@ -130,9 +119,7 @@ export default {
</gl-sprintf>
</div>
- <component :is="iconComponent" v-if="showPackageType">
- {{ packageType }}
- </component>
+ <infrastructure-icon-and-name v-if="showPackageType" />
<package-path
v-if="hasProjectLink"
diff --git a/app/assets/javascripts/packages_and_registries/package_registry/components/details/app.vue b/app/assets/javascripts/packages_and_registries/package_registry/components/details/app.vue
index bcbeec72961..d49c1be5202 100644
--- a/app/assets/javascripts/packages_and_registries/package_registry/components/details/app.vue
+++ b/app/assets/javascripts/packages_and_registries/package_registry/components/details/app.vue
@@ -15,7 +15,7 @@ import { convertToGraphQLId } from '~/graphql_shared/utils';
import { numberToHumanSize } from '~/lib/utils/number_utils';
import { objectToQuery } from '~/lib/utils/url_utility';
import { s__, __ } from '~/locale';
-import { packageTypeToTrackCategory } from '~/packages/shared/utils';
+import { packageTypeToTrackCategory } from '~/packages_and_registries/package_registry/utils';
import AdditionalMetadata from '~/packages_and_registries/package_registry/components/details/additional_metadata.vue';
import DependencyRow from '~/packages_and_registries/package_registry/components/details/dependency_row.vue';
import InstallationCommands from '~/packages_and_registries/package_registry/components/details/installation_commands.vue';
@@ -304,6 +304,7 @@ export default {
<template #default="{ deletePackage }">
<gl-modal
ref="deleteModal"
+ size="sm"
modal-id="delete-modal"
data-testid="delete-modal"
:action-primary="$options.modal.packageDeletePrimaryAction"
@@ -327,6 +328,7 @@ export default {
<gl-modal
ref="deleteFileModal"
+ size="sm"
modal-id="delete-file-modal"
:action-primary="$options.modal.fileDeletePrimaryAction"
:action-cancel="$options.modal.cancelAction"
diff --git a/app/assets/javascripts/packages_and_registries/package_registry/components/details/package_title.vue b/app/assets/javascripts/packages_and_registries/package_registry/components/details/package_title.vue
index 44d7807639d..118c509828c 100644
--- a/app/assets/javascripts/packages_and_registries/package_registry/components/details/package_title.vue
+++ b/app/assets/javascripts/packages_and_registries/package_registry/components/details/package_title.vue
@@ -3,7 +3,7 @@ import { GlIcon, GlSprintf, GlBadge, GlResizeObserverDirective } from '@gitlab/u
import { GlBreakpointInstance } from '@gitlab/ui/dist/utils';
import { numberToHumanSize } from '~/lib/utils/number_utils';
import { __ } from '~/locale';
-import PackageTags from '~/packages/shared/components/package_tags.vue';
+import PackageTags from '~/packages_and_registries/shared/components/package_tags.vue';
import { PACKAGE_TYPE_NUGET } from '~/packages_and_registries/package_registry/constants';
import { getPackageTypeLabel } from '~/packages_and_registries/package_registry/utils';
import MetadataItem from '~/vue_shared/components/registry/metadata_item.vue';
diff --git a/app/assets/javascripts/packages_and_registries/package_registry/components/details/version_row.vue b/app/assets/javascripts/packages_and_registries/package_registry/components/details/version_row.vue
index d218a405af6..1afd1b69db0 100644
--- a/app/assets/javascripts/packages_and_registries/package_registry/components/details/version_row.vue
+++ b/app/assets/javascripts/packages_and_registries/package_registry/components/details/version_row.vue
@@ -1,8 +1,8 @@
<script>
import { GlLink, GlSprintf, GlTruncate } from '@gitlab/ui';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
-import PackageTags from '~/packages/shared/components/package_tags.vue';
-import PublishMethod from '~/packages/shared/components/publish_method.vue';
+import PackageTags from '~/packages_and_registries/shared/components/package_tags.vue';
+import PublishMethod from '~/packages_and_registries/shared/components/publish_method.vue';
import ListItem from '~/vue_shared/components/registry/list_item.vue';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
import { PACKAGE_DEFAULT_STATUS } from '../../constants';
diff --git a/app/assets/javascripts/packages_and_registries/package_registry/components/list/package_list_row.vue b/app/assets/javascripts/packages_and_registries/package_registry/components/list/package_list_row.vue
index 195ff7af583..6fd96c0654f 100644
--- a/app/assets/javascripts/packages_and_registries/package_registry/components/list/package_list_row.vue
+++ b/app/assets/javascripts/packages_and_registries/package_registry/components/list/package_list_row.vue
@@ -1,16 +1,16 @@
<script>
import { GlButton, GlLink, GlSprintf, GlTooltipDirective, GlTruncate } from '@gitlab/ui';
-import { s__ } from '~/locale';
+import { s__, __ } from '~/locale';
import ListItem from '~/vue_shared/components/registry/list_item.vue';
import {
PACKAGE_ERROR_STATUS,
PACKAGE_DEFAULT_STATUS,
} from '~/packages_and_registries/package_registry/constants';
-import { getPackageTypeLabel } from '~/packages/shared/utils';
-import PackagePath from '~/packages/shared/components/package_path.vue';
-import PackageTags from '~/packages/shared/components/package_tags.vue';
+import { getPackageTypeLabel } from '~/packages_and_registries/package_registry/utils';
+import PackagePath from '~/packages_and_registries/shared/components/package_path.vue';
+import PackageTags from '~/packages_and_registries/shared/components/package_tags.vue';
import PublishMethod from '~/packages_and_registries/package_registry/components/list/publish_method.vue';
-import PackageIconAndName from '~/packages/shared/components/package_icon_and_name.vue';
+import PackageIconAndName from '~/packages_and_registries/shared/components/package_icon_and_name.vue';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import TimeagoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
@@ -40,7 +40,7 @@ export default {
},
computed: {
packageType() {
- return getPackageTypeLabel(this.packageEntity.packageType.toLowerCase());
+ return getPackageTypeLabel(this.packageEntity.packageType);
},
packageLink() {
const { project, id } = this.packageEntity;
@@ -64,6 +64,7 @@ export default {
},
i18n: {
erroredPackageText: s__('PackageRegistry|Invalid Package: failed metadata extraction'),
+ createdAt: __('Created %{timestamp}'),
},
};
</script>
@@ -127,8 +128,8 @@ export default {
</template>
<template #right-secondary>
- <span>
- <gl-sprintf :message="__('Created %{timestamp}')">
+ <span data-testid="created-date">
+ <gl-sprintf :message="$options.i18n.createdAt">
<template #timestamp>
<timeago-tooltip :time="packageEntity.createdAt" />
</template>
diff --git a/app/assets/javascripts/packages_and_registries/package_registry/components/list/packages_list.vue b/app/assets/javascripts/packages_and_registries/package_registry/components/list/packages_list.vue
index 2a946544c2f..298ed9bccdb 100644
--- a/app/assets/javascripts/packages_and_registries/package_registry/components/list/packages_list.vue
+++ b/app/assets/javascripts/packages_and_registries/package_registry/components/list/packages_list.vue
@@ -2,7 +2,7 @@
import { GlModal, GlSprintf, GlKeysetPagination } from '@gitlab/ui';
import { s__ } from '~/locale';
import PackagesListRow from '~/packages_and_registries/package_registry/components/list/package_list_row.vue';
-import PackagesListLoader from '~/packages/shared/components/packages_list_loader.vue';
+import PackagesListLoader from '~/packages_and_registries/shared/components/packages_list_loader.vue';
import {
DELETE_PACKAGE_TRACKING_ACTION,
REQUEST_DELETE_PACKAGE_TRACKING_ACTION,
@@ -124,6 +124,7 @@ export default {
<gl-modal
v-model="showDeleteModal"
modal-id="confirm-delete-pacakge"
+ size="sm"
ok-variant="danger"
@ok="deleteItemConfirmation"
@cancel="deleteItemCanceled"
diff --git a/app/assets/javascripts/packages_and_registries/package_registry/constants.js b/app/assets/javascripts/packages_and_registries/package_registry/constants.js
index 9fd8880861c..ab6541e4264 100644
--- a/app/assets/javascripts/packages_and_registries/package_registry/constants.js
+++ b/app/assets/javascripts/packages_and_registries/package_registry/constants.js
@@ -1,4 +1,15 @@
import { s__, __ } from '~/locale';
+import { helpPagePath } from '~/helpers/help_page_helper';
+
+export {
+ DELETE_PACKAGE_TRACKING_ACTION,
+ REQUEST_DELETE_PACKAGE_TRACKING_ACTION,
+ CANCEL_DELETE_PACKAGE_TRACKING_ACTION,
+ PULL_PACKAGE_TRACKING_ACTION,
+ DELETE_PACKAGE_FILE_TRACKING_ACTION,
+ REQUEST_DELETE_PACKAGE_FILE_TRACKING_ACTION,
+ CANCEL_DELETE_PACKAGE_FILE_TRACKING_ACTION,
+} from '~/packages_and_registries/shared/constants';
export const PACKAGE_TYPE_CONAN = 'CONAN';
export const PACKAGE_TYPE_MAVEN = 'MAVEN';
@@ -11,14 +22,6 @@ export const PACKAGE_TYPE_GENERIC = 'GENERIC';
export const PACKAGE_TYPE_DEBIAN = 'DEBIAN';
export const PACKAGE_TYPE_HELM = 'HELM';
-export const DELETE_PACKAGE_TRACKING_ACTION = 'delete_package';
-export const REQUEST_DELETE_PACKAGE_TRACKING_ACTION = 'request_delete_package';
-export const CANCEL_DELETE_PACKAGE_TRACKING_ACTION = 'cancel_delete_package';
-export const PULL_PACKAGE_TRACKING_ACTION = 'pull_package';
-export const DELETE_PACKAGE_FILE_TRACKING_ACTION = 'delete_package_file';
-export const REQUEST_DELETE_PACKAGE_FILE_TRACKING_ACTION = 'request_delete_package_file';
-export const CANCEL_DELETE_PACKAGE_FILE_TRACKING_ACTION = 'cancel_delete_package_file';
-
export const TRACKING_LABEL_CODE_INSTRUCTION = 'code_instruction';
export const TRACKING_LABEL_CONAN_INSTALLATION = 'conan_installation';
export const TRACKING_LABEL_MAVEN_INSTALLATION = 'maven_installation';
@@ -134,3 +137,8 @@ export const PACKAGE_TYPES = [
s__('PackageRegistry|Debian'),
s__('PackageRegistry|Helm'),
];
+
+// links
+
+export const EMPTY_LIST_HELP_URL = helpPagePath('user/packages/package_registry/index');
+export const PACKAGE_HELP_URL = helpPagePath('user/packages/index');
diff --git a/app/assets/javascripts/packages_and_registries/package_registry/graphql/fragments/package_data.fragment.graphql b/app/assets/javascripts/packages_and_registries/package_registry/graphql/fragments/package_data.fragment.graphql
index aaf0eb54aff..66315fda9e9 100644
--- a/app/assets/javascripts/packages_and_registries/package_registry/graphql/fragments/package_data.fragment.graphql
+++ b/app/assets/javascripts/packages_and_registries/package_registry/graphql/fragments/package_data.fragment.graphql
@@ -7,20 +7,24 @@ fragment PackageData on Package {
status
tags {
nodes {
+ id
name
}
}
- pipelines {
+ pipelines(last: 1) {
nodes {
+ id
sha
ref
commitPath
user {
+ id
name
}
}
}
project {
+ id
fullPath
webUrl
}
diff --git a/app/assets/javascripts/packages_and_registries/package_registry/graphql/queries/get_package_details.query.graphql b/app/assets/javascripts/packages_and_registries/package_registry/graphql/queries/get_package_details.query.graphql
index 14aa14e9822..08ea0938a59 100644
--- a/app/assets/javascripts/packages_and_registries/package_registry/graphql/queries/get_package_details.query.graphql
+++ b/app/assets/javascripts/packages_and_registries/package_registry/graphql/queries/get_package_details.query.graphql
@@ -8,6 +8,7 @@ query getPackageDetails($id: ID!) {
updatedAt
status
project {
+ id
path
}
tags(first: 10) {
@@ -25,9 +26,11 @@ query getPackageDetails($id: ID!) {
commitPath
path
user {
+ id
name
}
project {
+ id
name
webUrl
}
@@ -86,15 +89,18 @@ query getPackageDetails($id: ID!) {
}
}
... on PypiMetadata {
+ id
requiredPython
}
... on ConanMetadata {
+ id
packageChannel
packageUsername
recipe
recipePath
}
... on MavenMetadata {
+ id
appName
appGroup
appVersion
@@ -102,6 +108,7 @@ query getPackageDetails($id: ID!) {
}
... on NugetMetadata {
+ id
iconUrl
licenseUrl
projectUrl
diff --git a/app/assets/javascripts/packages_and_registries/package_registry/graphql/queries/get_packages.query.graphql b/app/assets/javascripts/packages_and_registries/package_registry/graphql/queries/get_packages.query.graphql
index e3115365f8b..4b913590949 100644
--- a/app/assets/javascripts/packages_and_registries/package_registry/graphql/queries/get_packages.query.graphql
+++ b/app/assets/javascripts/packages_and_registries/package_registry/graphql/queries/get_packages.query.graphql
@@ -14,6 +14,7 @@ query getPackages(
$before: String
) {
project(fullPath: $fullPath) @skip(if: $isGroupPage) {
+ id
packages(
sort: $sort
packageName: $packageName
@@ -33,6 +34,7 @@ query getPackages(
}
}
group(fullPath: $fullPath) @include(if: $isGroupPage) {
+ id
packages(
sort: $groupSort
packageName: $packageName
diff --git a/app/assets/javascripts/packages_and_registries/package_registry/index.js b/app/assets/javascripts/packages_and_registries/package_registry/index.js
new file mode 100644
index 00000000000..7ec931ff9a0
--- /dev/null
+++ b/app/assets/javascripts/packages_and_registries/package_registry/index.js
@@ -0,0 +1,30 @@
+import Vue from 'vue';
+import Translate from '~/vue_shared/translate';
+import { apolloProvider } from '~/packages_and_registries/package_registry/graphql/index';
+import PackageRegistry from '~/packages_and_registries/package_registry/pages/index.vue';
+import createRouter from './router';
+
+Vue.use(Translate);
+
+export default () => {
+ const el = document.getElementById('js-vue-packages-list');
+ const { endpoint, resourceId, fullPath, pageType, emptyListIllustration } = el.dataset;
+ const router = createRouter(endpoint);
+
+ const isGroupPage = pageType === 'groups';
+
+ return new Vue({
+ el,
+ router,
+ apolloProvider,
+ provide: {
+ resourceId,
+ fullPath,
+ emptyListIllustration,
+ isGroupPage,
+ },
+ render(createElement) {
+ return createElement(PackageRegistry);
+ },
+ });
+};
diff --git a/app/assets/javascripts/packages_and_registries/package_registry/pages/index.vue b/app/assets/javascripts/packages_and_registries/package_registry/pages/index.vue
new file mode 100644
index 00000000000..a14d0c32cbe
--- /dev/null
+++ b/app/assets/javascripts/packages_and_registries/package_registry/pages/index.vue
@@ -0,0 +1,5 @@
+<template>
+ <div>
+ <router-view />
+ </div>
+</template>
diff --git a/app/assets/javascripts/packages_and_registries/package_registry/pages/list.js b/app/assets/javascripts/packages_and_registries/package_registry/pages/list.js
deleted file mode 100644
index d797a0a5327..00000000000
--- a/app/assets/javascripts/packages_and_registries/package_registry/pages/list.js
+++ /dev/null
@@ -1,24 +0,0 @@
-import Vue from 'vue';
-import Translate from '~/vue_shared/translate';
-import { apolloProvider } from '~/packages_and_registries/package_registry/graphql/index';
-import PackagesListApp from '../components/list/app.vue';
-
-Vue.use(Translate);
-
-export default () => {
- const el = document.getElementById('js-vue-packages-list');
-
- const isGroupPage = el.dataset.pageType === 'groups';
-
- return new Vue({
- el,
- apolloProvider,
- provide: {
- ...el.dataset,
- isGroupPage,
- },
- render(createElement) {
- return createElement(PackagesListApp);
- },
- });
-};
diff --git a/app/assets/javascripts/packages_and_registries/package_registry/components/list/app.vue b/app/assets/javascripts/packages_and_registries/package_registry/pages/list.vue
index 11eeaf933ff..38df701157a 100644
--- a/app/assets/javascripts/packages_and_registries/package_registry/components/list/app.vue
+++ b/app/assets/javascripts/packages_and_registries/package_registry/pages/list.vue
@@ -3,19 +3,21 @@ import { GlEmptyState, GlLink, GlSprintf } from '@gitlab/ui';
import createFlash from '~/flash';
import { historyReplaceState } from '~/lib/utils/common_utils';
import { s__ } from '~/locale';
-import { SHOW_DELETE_SUCCESS_ALERT } from '~/packages/shared/constants';
+import { SHOW_DELETE_SUCCESS_ALERT } from '~/packages_and_registries/shared/constants';
import {
PROJECT_RESOURCE_TYPE,
GROUP_RESOURCE_TYPE,
GRAPHQL_PAGE_SIZE,
DELETE_PACKAGE_SUCCESS_MESSAGE,
+ EMPTY_LIST_HELP_URL,
+ PACKAGE_HELP_URL,
} from '~/packages_and_registries/package_registry/constants';
import getPackagesQuery from '~/packages_and_registries/package_registry/graphql/queries/get_packages.query.graphql';
import DeletePackage from '~/packages_and_registries/package_registry/components/functional/delete_package.vue';
-import PackageTitle from './package_title.vue';
-import PackageSearch from './package_search.vue';
-import PackageList from './packages_list.vue';
+import PackageTitle from '~/packages_and_registries/package_registry/components/list/package_title.vue';
+import PackageSearch from '~/packages_and_registries/package_registry/components/list/package_search.vue';
+import PackageList from '~/packages_and_registries/package_registry/components/list/packages_list.vue';
export default {
components: {
@@ -27,13 +29,7 @@ export default {
PackageSearch,
DeletePackage,
},
- inject: [
- 'packageHelpUrl',
- 'emptyListIllustration',
- 'emptyListHelpUrl',
- 'isGroupPage',
- 'fullPath',
- ],
+ inject: ['emptyListIllustration', 'isGroupPage', 'fullPath'],
data() {
return {
packages: {},
@@ -156,12 +152,16 @@ export default {
'PackageRegistry|Learn how to %{noPackagesLinkStart}publish and share your packages%{noPackagesLinkEnd} with GitLab.',
),
},
+ links: {
+ EMPTY_LIST_HELP_URL,
+ PACKAGE_HELP_URL,
+ },
};
</script>
<template>
<div>
- <package-title :help-url="packageHelpUrl" :count="packagesCount" />
+ <package-title :help-url="$options.links.PACKAGE_HELP_URL" :count="packagesCount" />
<package-search @update="handleSearchUpdate" />
<delete-package
@@ -185,7 +185,9 @@ export default {
<gl-sprintf v-if="hasFilters" :message="$options.i18n.widenFilters" />
<gl-sprintf v-else :message="$options.i18n.noResultsText">
<template #noPackagesLink="{ content }">
- <gl-link :href="emptyListHelpUrl" target="_blank">{{ content }}</gl-link>
+ <gl-link :href="$options.links.EMPTY_LIST_HELP_URL" target="_blank">{{
+ content
+ }}</gl-link>
</template>
</gl-sprintf>
</template>
diff --git a/app/assets/javascripts/packages_and_registries/package_registry/router.js b/app/assets/javascripts/packages_and_registries/package_registry/router.js
new file mode 100644
index 00000000000..ea5b740e879
--- /dev/null
+++ b/app/assets/javascripts/packages_and_registries/package_registry/router.js
@@ -0,0 +1,21 @@
+import Vue from 'vue';
+import VueRouter from 'vue-router';
+import List from '~/packages_and_registries/package_registry/pages/list.vue';
+
+Vue.use(VueRouter);
+
+export default function createRouter(base) {
+ const router = new VueRouter({
+ base,
+ mode: 'history',
+ routes: [
+ {
+ name: 'list',
+ path: '/',
+ component: List,
+ },
+ ],
+ });
+
+ return router;
+}
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 9b5a0d221b8..85a7aeb5561 100644
--- a/app/assets/javascripts/packages_and_registries/settings/group/bundle.js
+++ b/app/assets/javascripts/packages_and_registries/settings/group/bundle.js
@@ -18,9 +18,10 @@ export default () => {
el,
apolloProvider,
provide: {
+ groupPath: el.dataset.groupPath,
+ groupDependencyProxyPath: el.dataset.groupDependencyProxyPath,
defaultExpanded: parseBoolean(el.dataset.defaultExpanded),
dependencyProxyAvailable: parseBoolean(el.dataset.dependencyProxyAvailable),
- groupPath: el.dataset.groupPath,
},
render(createElement) {
return createElement(SettingsApp);
diff --git a/app/assets/javascripts/packages_and_registries/settings/group/components/dependency_proxy_settings.vue b/app/assets/javascripts/packages_and_registries/settings/group/components/dependency_proxy_settings.vue
index 5815c6393a7..fd62fe144b2 100644
--- a/app/assets/javascripts/packages_and_registries/settings/group/components/dependency_proxy_settings.vue
+++ b/app/assets/javascripts/packages_and_registries/settings/group/components/dependency_proxy_settings.vue
@@ -2,9 +2,14 @@
import { GlToggle, GlSprintf, GlLink } from '@gitlab/ui';
import { s__ } from '~/locale';
import SettingsBlock from '~/vue_shared/components/settings/settings_block.vue';
+import SettingsTitles from '~/packages_and_registries/settings/group/components/settings_titles.vue';
import updateDependencyProxySettings from '~/packages_and_registries/settings/group/graphql/mutations/update_dependency_proxy_settings.mutation.graphql';
+import updateDependencyProxyImageTtlGroupPolicy from '~/packages_and_registries/settings/group/graphql/mutations/update_dependency_proxy_image_ttl_group_policy.mutation.graphql';
import { updateGroupPackageSettings } from '~/packages_and_registries/settings/group/graphql/utils/cache_update';
-import { updateGroupDependencyProxySettingsOptimisticResponse } from '~/packages_and_registries/settings/group/graphql/utils/optimistic_responses';
+import {
+ updateGroupDependencyProxySettingsOptimisticResponse,
+ updateDependencyProxyImageTtlGroupPolicyOptimisticResponse,
+} from '~/packages_and_registries/settings/group/graphql/utils/optimistic_responses';
import {
DEPENDENCY_PROXY_HEADER,
@@ -19,21 +24,34 @@ export default {
GlSprintf,
GlLink,
SettingsBlock,
+ SettingsTitles,
},
i18n: {
DEPENDENCY_PROXY_HEADER,
DEPENDENCY_PROXY_SETTINGS_DESCRIPTION,
- label: s__('DependencyProxy|Enable Proxy'),
+ enabledProxyLabel: s__('DependencyProxy|Enable Dependency Proxy'),
+ enabledProxyHelpText: s__(
+ 'DependencyProxy|To see the image prefix and what is in the cache, visit the %{linkStart}Dependency Proxy%{linkEnd}',
+ ),
+ storageSettingsTitle: s__('DependencyProxy|Storage settings'),
+ ttlPolicyEnabledLabel: s__('DependencyProxy|Clear the Dependency Proxy cache automatically'),
+ ttlPolicyEnabledHelpText: s__(
+ 'DependencyProxy|When enabled, images older than 90 days will be removed from the cache.',
+ ),
},
links: {
DEPENDENCY_PROXY_DOCS_PATH,
},
- inject: ['defaultExpanded', 'groupPath'],
+ inject: ['defaultExpanded', 'groupPath', 'groupDependencyProxyPath'],
props: {
dependencyProxySettings: {
type: Object,
required: true,
},
+ dependencyProxyImageTtlPolicy: {
+ type: Object,
+ required: true,
+ },
isLoading: {
type: Boolean,
required: false,
@@ -49,26 +67,35 @@ export default {
this.updateSettings({ enabled });
},
},
+ ttlEnabled: {
+ get() {
+ return this.dependencyProxyImageTtlPolicy.enabled;
+ },
+ set(enabled) {
+ const payload = {
+ enabled,
+ ttl: 90, // hardocded TTL for the MVC version
+ };
+ this.updateDependencyProxyImageTtlGroupPolicy(payload);
+ },
+ },
+ helpText() {
+ return this.enabled ? this.$options.i18n.enabledProxyHelpText : '';
+ },
},
methods: {
- async updateSettings(payload) {
+ mutationVariables(payload) {
+ return {
+ input: {
+ groupPath: this.groupPath,
+ ...payload,
+ },
+ };
+ },
+ async executeMutation(config, resource) {
try {
- const { data } = await this.$apollo.mutate({
- mutation: updateDependencyProxySettings,
- variables: {
- input: {
- groupPath: this.groupPath,
- ...payload,
- },
- },
- update: updateGroupPackageSettings(this.groupPath),
- optimisticResponse: updateGroupDependencyProxySettingsOptimisticResponse({
- ...this.dependencyProxySettings,
- ...payload,
- }),
- });
-
- if (data.updateDependencyProxySettings?.errors?.length > 0) {
+ const { data } = await this.$apollo.mutate(config);
+ if (data[resource]?.errors.length > 0) {
throw new Error();
} else {
this.$emit('success');
@@ -77,6 +104,32 @@ export default {
this.$emit('error');
}
},
+ async updateSettings(payload) {
+ const apolloConfig = {
+ mutation: updateDependencyProxySettings,
+ variables: this.mutationVariables(payload),
+ update: updateGroupPackageSettings(this.groupPath),
+ optimisticResponse: updateGroupDependencyProxySettingsOptimisticResponse({
+ ...this.dependencyProxySettings,
+ ...payload,
+ }),
+ };
+
+ this.executeMutation(apolloConfig, 'updateDependencyProxySettings');
+ },
+ async updateDependencyProxyImageTtlGroupPolicy(payload) {
+ const apolloConfig = {
+ mutation: updateDependencyProxyImageTtlGroupPolicy,
+ variables: this.mutationVariables(payload),
+ update: updateGroupPackageSettings(this.groupPath),
+ optimisticResponse: updateDependencyProxyImageTtlGroupPolicyOptimisticResponse({
+ ...this.dependencyProxyImageTtlPolicy,
+ ...payload,
+ }),
+ };
+
+ this.executeMutation(apolloConfig, 'updateDependencyProxyImageTtlGroupPolicy');
+ },
},
};
</script>
@@ -91,7 +144,11 @@ export default {
<span data-testid="description">
<gl-sprintf :message="$options.i18n.DEPENDENCY_PROXY_SETTINGS_DESCRIPTION">
<template #docLink="{ content }">
- <gl-link :href="$options.links.DEPENDENCY_PROXY_DOCS_PATH">{{ content }}</gl-link>
+ <gl-link
+ data-testid="description-link"
+ :href="$options.links.DEPENDENCY_PROXY_DOCS_PATH"
+ >{{ content }}</gl-link
+ >
</template>
</gl-sprintf>
</span>
@@ -101,9 +158,31 @@ export default {
<gl-toggle
v-model="enabled"
:disabled="isLoading"
- :label="$options.i18n.label"
+ :label="$options.i18n.enabledProxyLabel"
+ :help="helpText"
data-qa-selector="dependency_proxy_setting_toggle"
data-testid="dependency-proxy-setting-toggle"
+ >
+ <template #help>
+ <span class="gl-overflow-break-word gl-max-w-100vw gl-display-inline-block">
+ <gl-sprintf :message="$options.i18n.enabledProxyHelpText">
+ <template #link="{ content }">
+ <gl-link data-testid="toggle-help-link" :href="groupDependencyProxyPath">{{
+ content
+ }}</gl-link>
+ </template>
+ </gl-sprintf>
+ </span>
+ </template>
+ </gl-toggle>
+
+ <settings-titles :title="$options.i18n.storageSettingsTitle" class="gl-my-6" />
+ <gl-toggle
+ v-model="ttlEnabled"
+ :disabled="isLoading"
+ :label="$options.i18n.ttlPolicyEnabledLabel"
+ :help="$options.i18n.ttlPolicyEnabledHelpText"
+ data-testid="dependency-proxy-ttl-policies-toggle"
/>
</div>
</template>
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 b45cedcdd66..64c12b4be6a 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
@@ -37,6 +37,9 @@ export default {
dependencyProxySettings() {
return this.group?.dependencyProxySetting || {};
},
+ dependencyProxyImageTtlPolicy() {
+ return this.group?.dependencyProxyImageTtlPolicy || {};
+ },
isLoading() {
return this.$apollo.queries.group.loading;
},
@@ -82,6 +85,7 @@ export default {
<dependency-proxy-settings
v-if="dependencyProxyAvailable"
:dependency-proxy-settings="dependencyProxySettings"
+ :dependency-proxy-image-ttl-policy="dependencyProxyImageTtlPolicy"
:is-loading="isLoading"
@success="handleSuccess"
@error="handleError"
diff --git a/app/assets/javascripts/packages_and_registries/settings/group/components/settings_titles.vue b/app/assets/javascripts/packages_and_registries/settings/group/components/settings_titles.vue
index 3f0ab7686e5..1e93875c1e3 100644
--- a/app/assets/javascripts/packages_and_registries/settings/group/components/settings_titles.vue
+++ b/app/assets/javascripts/packages_and_registries/settings/group/components/settings_titles.vue
@@ -8,7 +8,8 @@ export default {
},
subTitle: {
type: String,
- required: true,
+ required: false,
+ default: '',
},
},
};
@@ -16,10 +17,10 @@ export default {
<template>
<div>
- <h5 class="gl-border-b-solid gl-border-b-1 gl-border-gray-200">
+ <h5 class="gl-border-b-solid gl-border-b-1 gl-border-gray-200 gl-pb-3">
{{ title }}
</h5>
- <p>{{ subTitle }}</p>
+ <p v-if="subTitle">{{ subTitle }}</p>
<slot></slot>
</div>
</template>
diff --git a/app/assets/javascripts/packages_and_registries/settings/group/graphql/mutations/update_dependency_proxy_image_ttl_group_policy.mutation.graphql b/app/assets/javascripts/packages_and_registries/settings/group/graphql/mutations/update_dependency_proxy_image_ttl_group_policy.mutation.graphql
new file mode 100644
index 00000000000..81250f52dfb
--- /dev/null
+++ b/app/assets/javascripts/packages_and_registries/settings/group/graphql/mutations/update_dependency_proxy_image_ttl_group_policy.mutation.graphql
@@ -0,0 +1,11 @@
+mutation updateDependencyProxyImageTtlGroupPolicy(
+ $input: UpdateDependencyProxyImageTtlGroupPolicyInput!
+) {
+ updateDependencyProxyImageTtlGroupPolicy(input: $input) {
+ dependencyProxyImageTtlPolicy {
+ enabled
+ ttl
+ }
+ 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
index d3edebfbe20..404d9d26d49 100644
--- 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
@@ -1,8 +1,13 @@
query getGroupPackagesSettings($fullPath: ID!) {
group(fullPath: $fullPath) {
+ id
dependencyProxySetting {
enabled
}
+ dependencyProxyImageTtlPolicy {
+ ttl
+ enabled
+ }
packageSettings {
mavenDuplicatesAllowed
mavenDuplicateExceptionRegex
diff --git a/app/assets/javascripts/packages_and_registries/settings/group/graphql/utils/cache_update.js b/app/assets/javascripts/packages_and_registries/settings/group/graphql/utils/cache_update.js
index fe94203f51b..c7b0899fa4c 100644
--- a/app/assets/javascripts/packages_and_registries/settings/group/graphql/utils/cache_update.js
+++ b/app/assets/javascripts/packages_and_registries/settings/group/graphql/utils/cache_update.js
@@ -19,6 +19,11 @@ export const updateGroupPackageSettings = (fullPath) => (client, { data: updated
...updatedData.updateDependencyProxySettings.dependencyProxySetting,
};
}
+ if (updatedData.updateDependencyProxyImageTtlGroupPolicy) {
+ draftState.group.dependencyProxyImageTtlPolicy = {
+ ...updatedData.updateDependencyProxyImageTtlGroupPolicy.dependencyProxyImageTtlPolicy,
+ };
+ }
});
client.writeQuery({
diff --git a/app/assets/javascripts/packages_and_registries/settings/group/graphql/utils/optimistic_responses.js b/app/assets/javascripts/packages_and_registries/settings/group/graphql/utils/optimistic_responses.js
index a30d8ca0b81..92f6e117911 100644
--- a/app/assets/javascripts/packages_and_registries/settings/group/graphql/utils/optimistic_responses.js
+++ b/app/assets/javascripts/packages_and_registries/settings/group/graphql/utils/optimistic_responses.js
@@ -21,3 +21,15 @@ export const updateGroupDependencyProxySettingsOptimisticResponse = (changes) =>
},
},
});
+
+export const updateDependencyProxyImageTtlGroupPolicyOptimisticResponse = (changes) => ({
+ // eslint-disable-next-line @gitlab/require-i18n-strings
+ __typename: 'Mutation',
+ updateDependencyProxyImageTtlGroupPolicy: {
+ __typename: 'UpdateDependencyProxyImageTtlGroupPolicyPayload',
+ errors: [],
+ dependencyProxyImageTtlPolicy: {
+ ...changes,
+ },
+ },
+});
diff --git a/app/assets/javascripts/packages_and_registries/settings/project/graphql/queries/get_expiration_policy.query.graphql b/app/assets/javascripts/packages_and_registries/settings/project/graphql/queries/get_expiration_policy.query.graphql
index c171be0ad07..6a862da92df 100644
--- a/app/assets/javascripts/packages_and_registries/settings/project/graphql/queries/get_expiration_policy.query.graphql
+++ b/app/assets/javascripts/packages_and_registries/settings/project/graphql/queries/get_expiration_policy.query.graphql
@@ -2,6 +2,7 @@
query getProjectExpirationPolicy($projectPath: ID!) {
project(fullPath: $projectPath) {
+ id
containerExpirationPolicy {
...ContainerExpirationPolicyFields
}
diff --git a/app/assets/javascripts/packages/shared/components/package_icon_and_name.vue b/app/assets/javascripts/packages_and_registries/shared/components/package_icon_and_name.vue
index 105f7bbe132..105f7bbe132 100644
--- a/app/assets/javascripts/packages/shared/components/package_icon_and_name.vue
+++ b/app/assets/javascripts/packages_and_registries/shared/components/package_icon_and_name.vue
diff --git a/app/assets/javascripts/packages/shared/components/package_path.vue b/app/assets/javascripts/packages_and_registries/shared/components/package_path.vue
index 6fb001e5e92..6fb001e5e92 100644
--- a/app/assets/javascripts/packages/shared/components/package_path.vue
+++ b/app/assets/javascripts/packages_and_registries/shared/components/package_path.vue
diff --git a/app/assets/javascripts/packages/shared/components/package_tags.vue b/app/assets/javascripts/packages_and_registries/shared/components/package_tags.vue
index 5ec950e4d45..5ec950e4d45 100644
--- a/app/assets/javascripts/packages/shared/components/package_tags.vue
+++ b/app/assets/javascripts/packages_and_registries/shared/components/package_tags.vue
diff --git a/app/assets/javascripts/packages/shared/components/packages_list_loader.vue b/app/assets/javascripts/packages_and_registries/shared/components/packages_list_loader.vue
index cf555f46f8c..cf555f46f8c 100644
--- a/app/assets/javascripts/packages/shared/components/packages_list_loader.vue
+++ b/app/assets/javascripts/packages_and_registries/shared/components/packages_list_loader.vue
diff --git a/app/assets/javascripts/packages/shared/components/publish_method.vue b/app/assets/javascripts/packages_and_registries/shared/components/publish_method.vue
index 8a66a33f2ab..8a66a33f2ab 100644
--- a/app/assets/javascripts/packages/shared/components/publish_method.vue
+++ b/app/assets/javascripts/packages_and_registries/shared/components/publish_method.vue
diff --git a/app/assets/javascripts/packages_and_registries/shared/components/registry_list.vue b/app/assets/javascripts/packages_and_registries/shared/components/registry_list.vue
new file mode 100644
index 00000000000..79381f82009
--- /dev/null
+++ b/app/assets/javascripts/packages_and_registries/shared/components/registry_list.vue
@@ -0,0 +1,124 @@
+<script>
+import { GlButton, GlFormCheckbox, GlKeysetPagination } from '@gitlab/ui';
+import { filter } from 'lodash';
+import { __ } from '~/locale';
+
+export default {
+ name: 'RegistryList',
+ components: {
+ GlButton,
+ GlFormCheckbox,
+ GlKeysetPagination,
+ },
+ props: {
+ title: {
+ type: String,
+ required: true,
+ },
+ isLoading: {
+ type: Boolean,
+ default: false,
+ required: false,
+ },
+ hiddenDelete: {
+ type: Boolean,
+ default: false,
+ required: false,
+ },
+ pagination: {
+ type: Object,
+ required: false,
+ default: () => ({}),
+ },
+ items: {
+ type: Array,
+ required: false,
+ default: () => [],
+ },
+ idProperty: {
+ type: String,
+ required: false,
+ default: 'id',
+ },
+ },
+ data() {
+ return {
+ selectedReferences: {},
+ };
+ },
+ computed: {
+ showPagination() {
+ return this.pagination.hasPreviousPage || this.pagination.hasNextPage;
+ },
+ disableDeleteButton() {
+ return this.isLoading || filter(this.selectedReferences).length === 0;
+ },
+ selectedItems() {
+ return this.items.filter(this.isSelected);
+ },
+ selectAll: {
+ get() {
+ return this.items.every(this.isSelected);
+ },
+ set(value) {
+ this.items.forEach((item) => {
+ const id = item[this.idProperty];
+ this.$set(this.selectedReferences, id, value);
+ });
+ },
+ },
+ },
+ methods: {
+ selectItem(item) {
+ const id = item[this.idProperty];
+ this.$set(this.selectedReferences, id, !this.selectedReferences[id]);
+ },
+ isSelected(item) {
+ const id = item[this.idProperty];
+ return this.selectedReferences[id];
+ },
+ },
+ i18n: {
+ deleteSelected: __('Delete Selected'),
+ },
+};
+</script>
+
+<template>
+ <div>
+ <div class="gl-display-flex gl-justify-content-space-between gl-mb-3 gl-align-items-center">
+ <gl-form-checkbox v-if="!hiddenDelete" v-model="selectAll" class="gl-ml-2 gl-pt-2">
+ <span class="gl-font-weight-bold">{{ title }}</span>
+ </gl-form-checkbox>
+
+ <gl-button
+ v-if="!hiddenDelete"
+ :disabled="disableDeleteButton"
+ category="secondary"
+ variant="danger"
+ @click="$emit('delete', selectedItems)"
+ >
+ {{ $options.i18n.deleteSelected }}
+ </gl-button>
+ </div>
+
+ <div v-for="(item, index) in items" :key="index">
+ <slot
+ :select-item="selectItem"
+ :is-selected="isSelected"
+ :item="item"
+ :first="index === 0"
+ ></slot>
+ </div>
+
+ <div class="gl-display-flex gl-justify-content-center">
+ <gl-keyset-pagination
+ v-if="showPagination"
+ v-bind="pagination"
+ class="gl-mt-3"
+ @prev="$emit('prev-page')"
+ @next="$emit('next-page')"
+ />
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/packages_and_registries/shared/constants.js b/app/assets/javascripts/packages_and_registries/shared/constants.js
index 7d2971bd8c7..afc72a2c627 100644
--- a/app/assets/javascripts/packages_and_registries/shared/constants.js
+++ b/app/assets/javascripts/packages_and_registries/shared/constants.js
@@ -1,3 +1,39 @@
+import { s__ } from '~/locale';
+
export const FILTERED_SEARCH_TERM = 'filtered-search-term';
export const FILTERED_SEARCH_TYPE = 'type';
export const HISTORY_PIPELINES_LIMIT = 5;
+
+export const DELETE_PACKAGE_TRACKING_ACTION = 'delete_package';
+export const REQUEST_DELETE_PACKAGE_TRACKING_ACTION = 'request_delete_package';
+export const CANCEL_DELETE_PACKAGE_TRACKING_ACTION = 'cancel_delete_package';
+export const PULL_PACKAGE_TRACKING_ACTION = 'pull_package';
+export const DELETE_PACKAGE_FILE_TRACKING_ACTION = 'delete_package_file';
+export const REQUEST_DELETE_PACKAGE_FILE_TRACKING_ACTION = 'request_delete_package_file';
+export const CANCEL_DELETE_PACKAGE_FILE_TRACKING_ACTION = 'cancel_delete_package_file';
+
+export const TRACKING_ACTIONS = {
+ DELETE_PACKAGE: DELETE_PACKAGE_TRACKING_ACTION,
+ REQUEST_DELETE_PACKAGE: REQUEST_DELETE_PACKAGE_TRACKING_ACTION,
+ CANCEL_DELETE_PACKAGE: CANCEL_DELETE_PACKAGE_TRACKING_ACTION,
+ PULL_PACKAGE: PULL_PACKAGE_TRACKING_ACTION,
+ DELETE_PACKAGE_FILE: DELETE_PACKAGE_FILE_TRACKING_ACTION,
+ REQUEST_DELETE_PACKAGE_FILE: REQUEST_DELETE_PACKAGE_FILE_TRACKING_ACTION,
+ CANCEL_DELETE_PACKAGE_FILE: CANCEL_DELETE_PACKAGE_FILE_TRACKING_ACTION,
+};
+
+export const SHOW_DELETE_SUCCESS_ALERT = 'showSuccessDeleteAlert';
+export const DELETE_PACKAGE_ERROR_MESSAGE = s__(
+ 'PackageRegistry|Something went wrong while deleting the package.',
+);
+export const DELETE_PACKAGE_FILE_ERROR_MESSAGE = s__(
+ 'PackageRegistry|Something went wrong while deleting the package file.',
+);
+export const DELETE_PACKAGE_FILE_SUCCESS_MESSAGE = s__(
+ 'PackageRegistry|Package file deleted successfully',
+);
+
+export const PACKAGE_ERROR_STATUS = 'error';
+export const PACKAGE_DEFAULT_STATUS = 'default';
+export const PACKAGE_HIDDEN_STATUS = 'hidden';
+export const PACKAGE_PROCESSING_STATUS = 'processing';
diff --git a/app/assets/javascripts/packages_and_registries/shared/utils.js b/app/assets/javascripts/packages_and_registries/shared/utils.js
index 93eb90535d1..cf18f655e79 100644
--- a/app/assets/javascripts/packages_and_registries/shared/utils.js
+++ b/app/assets/javascripts/packages_and_registries/shared/utils.js
@@ -28,3 +28,13 @@ export const extractFilterAndSorting = (queryObject) => {
}
return { filters, sorting };
};
+
+export const beautifyPath = (path) => (path ? path.split('/').join(' / ') : '');
+
+export const getCommitLink = ({ project_path: projectPath, pipeline = {} }, isGroup = false) => {
+ if (isGroup) {
+ return `/${projectPath}/commit/${pipeline.sha}`;
+ }
+
+ return `../commit/${pipeline.sha}`;
+};
diff --git a/app/assets/javascripts/pages/admin/integrations/edit/index.js b/app/assets/javascripts/pages/admin/integrations/edit/index.js
index 8002fa8bf78..8485b460261 100644
--- a/app/assets/javascripts/pages/admin/integrations/edit/index.js
+++ b/app/assets/javascripts/pages/admin/integrations/edit/index.js
@@ -1,15 +1,11 @@
-import IntegrationSettingsForm from '~/integrations/integration_settings_form';
+import initIntegrationSettingsForm from '~/integrations/edit';
import PrometheusMetrics from '~/prometheus_metrics/prometheus_metrics';
-function initIntegrations() {
- const prometheusSettingsWrapper = document.querySelector('.js-prometheus-metrics-monitoring');
- const integrationSettingsForm = new IntegrationSettingsForm('.js-integration-settings-form');
- integrationSettingsForm.init();
+initIntegrationSettingsForm('.js-integration-settings-form');
- if (prometheusSettingsWrapper) {
- const prometheusMetrics = new PrometheusMetrics('.js-prometheus-metrics-monitoring');
- prometheusMetrics.loadActiveMetrics();
- }
+const prometheusSettingsSelector = '.js-prometheus-metrics-monitoring';
+const prometheusSettingsWrapper = document.querySelector(prometheusSettingsSelector);
+if (prometheusSettingsWrapper) {
+ const prometheusMetrics = new PrometheusMetrics(prometheusSettingsSelector);
+ prometheusMetrics.loadActiveMetrics();
}
-
-initIntegrations();
diff --git a/app/assets/javascripts/pages/admin/labels/edit/index.js b/app/assets/javascripts/pages/admin/labels/edit/index.js
index f7c25347e75..a3b9c43388a 100644
--- a/app/assets/javascripts/pages/admin/labels/edit/index.js
+++ b/app/assets/javascripts/pages/admin/labels/edit/index.js
@@ -1,3 +1,3 @@
-import Labels from '../../../../labels';
+import Labels from '~/labels/labels';
new Labels(); // eslint-disable-line no-new
diff --git a/app/assets/javascripts/pages/admin/labels/index/index.js b/app/assets/javascripts/pages/admin/labels/index/index.js
index 0ceab3b922f..132fe5ce8fc 100644
--- a/app/assets/javascripts/pages/admin/labels/index/index.js
+++ b/app/assets/javascripts/pages/admin/labels/index/index.js
@@ -1,23 +1,3 @@
-function initLabels() {
- const pagination = document.querySelector('.labels .gl-pagination');
- const emptyState = document.querySelector('.labels .nothing-here-block.hidden');
+import { initAdminLabels } from '~/labels';
- function removeLabelSuccessCallback() {
- this.closest('li').classList.add('gl-display-none!');
-
- const labelsCount = document.querySelectorAll(
- 'ul.manage-labels-list li:not(.gl-display-none\\!)',
- ).length;
-
- // display the empty state if there are no more labels
- if (labelsCount < 1 && !pagination && emptyState) {
- emptyState.classList.remove('hidden');
- }
- }
-
- document.querySelectorAll('.js-remove-label').forEach((row) => {
- row.addEventListener('ajax:success', removeLabelSuccessCallback);
- });
-}
-
-initLabels();
+initAdminLabels();
diff --git a/app/assets/javascripts/pages/admin/labels/new/index.js b/app/assets/javascripts/pages/admin/labels/new/index.js
index f7c25347e75..a3b9c43388a 100644
--- a/app/assets/javascripts/pages/admin/labels/new/index.js
+++ b/app/assets/javascripts/pages/admin/labels/new/index.js
@@ -1,3 +1,3 @@
-import Labels from '../../../../labels';
+import Labels from '~/labels/labels';
new Labels(); // eslint-disable-line no-new
diff --git a/app/assets/javascripts/pages/admin/services/edit/index.js b/app/assets/javascripts/pages/admin/services/edit/index.js
deleted file mode 100644
index b8080ddff77..00000000000
--- a/app/assets/javascripts/pages/admin/services/edit/index.js
+++ /dev/null
@@ -1,4 +0,0 @@
-import IntegrationSettingsForm from '~/integrations/integration_settings_form';
-
-const integrationSettingsForm = new IntegrationSettingsForm('.js-integration-settings-form');
-integrationSettingsForm.init();
diff --git a/app/assets/javascripts/pages/admin/services/index/index.js b/app/assets/javascripts/pages/admin/services/index/index.js
deleted file mode 100644
index b695cf70c5d..00000000000
--- a/app/assets/javascripts/pages/admin/services/index/index.js
+++ /dev/null
@@ -1,4 +0,0 @@
-import PersistentUserCallout from '~/persistent_user_callout';
-
-const callout = document.querySelector('.js-service-templates-deprecated');
-PersistentUserCallout.factory(callout);
diff --git a/app/assets/javascripts/pages/constants.js b/app/assets/javascripts/pages/constants.js
deleted file mode 100644
index a9773807212..00000000000
--- a/app/assets/javascripts/pages/constants.js
+++ /dev/null
@@ -1,6 +0,0 @@
-export const FILTERED_SEARCH = {
- MERGE_REQUESTS: 'merge_requests',
- ISSUES: 'issues',
- ADMIN_RUNNERS: 'admin/runners',
- GROUP_RUNNERS_ANCHOR: 'runners-settings',
-};
diff --git a/app/assets/javascripts/pages/dashboard/issues/index.js b/app/assets/javascripts/pages/dashboard/issues/index.js
index 3e09b1796b1..d0903ad53bc 100644
--- a/app/assets/javascripts/pages/dashboard/issues/index.js
+++ b/app/assets/javascripts/pages/dashboard/issues/index.js
@@ -1,6 +1,6 @@
import IssuableFilteredSearchTokenKeys from '~/filtered_search/issuable_filtered_search_token_keys';
-import initManualOrdering from '~/manual_ordering';
-import { FILTERED_SEARCH } from '~/pages/constants';
+import initManualOrdering from '~/issues/manual_ordering';
+import { FILTERED_SEARCH } from '~/filtered_search/constants';
import initFilteredSearch from '~/pages/search/init_filtered_search';
import projectSelect from '~/project_select';
diff --git a/app/assets/javascripts/pages/dashboard/merge_requests/index.js b/app/assets/javascripts/pages/dashboard/merge_requests/index.js
index 6c134e4fad6..1350837476b 100644
--- a/app/assets/javascripts/pages/dashboard/merge_requests/index.js
+++ b/app/assets/javascripts/pages/dashboard/merge_requests/index.js
@@ -1,6 +1,6 @@
import addExtraTokensForMergeRequests from 'ee_else_ce/filtered_search/add_extra_tokens_for_merge_requests';
import IssuableFilteredSearchTokenKeys from '~/filtered_search/issuable_filtered_search_token_keys';
-import { FILTERED_SEARCH } from '~/pages/constants';
+import { FILTERED_SEARCH } from '~/filtered_search/constants';
import initFilteredSearch from '~/pages/search/init_filtered_search';
import projectSelect from '~/project_select';
diff --git a/app/assets/javascripts/pages/dashboard/milestones/show/index.js b/app/assets/javascripts/pages/dashboard/milestones/show/index.js
index 1f3e458fe17..d1ff7ec336c 100644
--- a/app/assets/javascripts/pages/dashboard/milestones/show/index.js
+++ b/app/assets/javascripts/pages/dashboard/milestones/show/index.js
@@ -1,4 +1,4 @@
-import Milestone from '~/milestone';
+import Milestone from '~/milestones/milestone';
import Sidebar from '~/right_sidebar';
import MountMilestoneSidebar from '~/sidebar/mount_milestone_sidebar';
diff --git a/app/assets/javascripts/pages/dashboard/projects/index/components/customize_homepage_banner.vue b/app/assets/javascripts/pages/dashboard/projects/index/components/customize_homepage_banner.vue
deleted file mode 100644
index 99461475af0..00000000000
--- a/app/assets/javascripts/pages/dashboard/projects/index/components/customize_homepage_banner.vue
+++ /dev/null
@@ -1,102 +0,0 @@
-<script>
-import { GlBanner } from '@gitlab/ui';
-import axios from '~/lib/utils/axios_utils';
-import { s__ } from '~/locale';
-import Tracking from '~/tracking';
-
-const trackingMixin = Tracking.mixin();
-
-export default {
- components: {
- GlBanner,
- },
- mixins: [trackingMixin],
- inject: {
- svgPath: {
- default: '',
- },
- preferencesBehaviorPath: {
- default: '',
- },
- calloutsPath: {
- default: '',
- },
- calloutsFeatureId: {
- default: '',
- },
- trackLabel: {
- default: '',
- },
- },
- i18n: {
- title: s__('CustomizeHomepageBanner|Do you want to customize this page?'),
- body: s__(
- 'CustomizeHomepageBanner|This page shows a list of your projects by default but it can be changed to show projects\' activity, groups, your to-do list, assigned issues, assigned merge requests, and more. You can change this under "Homepage content" in your preferences',
- ),
- button_text: s__('CustomizeHomepageBanner|Go to preferences'),
- },
- data() {
- return {
- visible: true,
- tracking: {
- label: this.trackLabel,
- },
- };
- },
- created() {
- this.$nextTick(() => {
- this.addTrackingAttributesToButton();
- });
- },
- mounted() {
- this.trackOnShow();
- },
- methods: {
- handleClose() {
- axios
- .post(this.calloutsPath, {
- feature_name: this.calloutsFeatureId,
- })
- .catch((e) => {
- // eslint-disable-next-line @gitlab/require-i18n-strings, no-console
- console.error('Failed to dismiss banner.', e);
- });
-
- this.visible = false;
- this.track('click_dismiss');
- },
- trackOnShow() {
- if (this.visible) this.track('show_home_page_banner');
- },
- addTrackingAttributesToButton() {
- // we can't directly add these on the button like we need to due to
- // button not being modifiable currently
- // https://gitlab.com/gitlab-org/gitlab-ui/-/blob/9209ec424e5cca14bc8a1b5c9fa12636d8c83dad/src/components/base/banner/banner.vue#L60
- const button = this.$refs.banner.$el.querySelector(
- `[href='${this.preferencesBehaviorPath}']`,
- );
-
- if (button) {
- button.setAttribute('data-track-action', 'click_go_to_preferences');
- button.setAttribute('data-track-label', this.trackLabel);
- }
- },
- },
-};
-</script>
-
-<template>
- <gl-banner
- v-if="visible"
- ref="banner"
- :title="$options.i18n.title"
- :button-text="$options.i18n.button_text"
- :button-link="preferencesBehaviorPath"
- :svg-path="svgPath"
- @close="handleClose"
- >
- <p>
- {{ $options.i18n.body }}
- </p>
- </gl-banner>
-</template>
diff --git a/app/assets/javascripts/pages/dashboard/projects/index/index.js b/app/assets/javascripts/pages/dashboard/projects/index/index.js
index c34d15b869a..6c9378b7231 100644
--- a/app/assets/javascripts/pages/dashboard/projects/index/index.js
+++ b/app/assets/javascripts/pages/dashboard/projects/index/index.js
@@ -1,5 +1,3 @@
import ProjectsList from '~/projects_list';
-import initCustomizeHomepageBanner from './init_customize_homepage_banner';
new ProjectsList(); // eslint-disable-line no-new
-initCustomizeHomepageBanner();
diff --git a/app/assets/javascripts/pages/dashboard/projects/index/init_customize_homepage_banner.js b/app/assets/javascripts/pages/dashboard/projects/index/init_customize_homepage_banner.js
deleted file mode 100644
index 8cdcd3134ee..00000000000
--- a/app/assets/javascripts/pages/dashboard/projects/index/init_customize_homepage_banner.js
+++ /dev/null
@@ -1,16 +0,0 @@
-import Vue from 'vue';
-import CustomizeHomepageBanner from './components/customize_homepage_banner.vue';
-
-export default () => {
- const el = document.querySelector('.js-customize-homepage-banner');
-
- if (!el) {
- return false;
- }
-
- return new Vue({
- el,
- provide: { ...el.dataset },
- render: (createElement) => createElement(CustomizeHomepageBanner),
- });
-};
diff --git a/app/assets/javascripts/pages/groups/issues/index.js b/app/assets/javascripts/pages/groups/issues/index.js
index 8c9f23732aa..966d55e5587 100644
--- a/app/assets/javascripts/pages/groups/issues/index.js
+++ b/app/assets/javascripts/pages/groups/issues/index.js
@@ -1,8 +1,8 @@
import IssuableFilteredSearchTokenKeys from 'ee_else_ce/filtered_search/issuable_filtered_search_token_keys';
-import issuableInitBulkUpdateSidebar from '~/issuable_bulk_update_sidebar/issuable_init_bulk_update_sidebar';
+import issuableInitBulkUpdateSidebar from '~/issuable/bulk_update_sidebar/issuable_init_bulk_update_sidebar';
import { mountIssuablesListApp, mountIssuesListApp } from '~/issues_list';
-import initManualOrdering from '~/manual_ordering';
-import { FILTERED_SEARCH } from '~/pages/constants';
+import initManualOrdering from '~/issues/manual_ordering';
+import { FILTERED_SEARCH } from '~/filtered_search/constants';
import initFilteredSearch from '~/pages/search/init_filtered_search';
import projectSelect from '~/project_select';
diff --git a/app/assets/javascripts/pages/groups/labels/edit/index.js b/app/assets/javascripts/pages/groups/labels/edit/index.js
index 2e8308fe084..e4e377f62fc 100644
--- a/app/assets/javascripts/pages/groups/labels/edit/index.js
+++ b/app/assets/javascripts/pages/groups/labels/edit/index.js
@@ -1,4 +1,4 @@
-import Labels from 'ee_else_ce/labels';
+import Labels from 'ee_else_ce/labels/labels';
// eslint-disable-next-line no-new
new Labels();
diff --git a/app/assets/javascripts/pages/groups/labels/index/index.js b/app/assets/javascripts/pages/groups/labels/index/index.js
index 95c2c7cd7d0..bf670e8576f 100644
--- a/app/assets/javascripts/pages/groups/labels/index/index.js
+++ b/app/assets/javascripts/pages/groups/labels/index/index.js
@@ -1,5 +1,4 @@
-import initDeleteLabelModal from '~/delete_label_modal';
-import initLabels from '~/init_labels';
+import { initDeleteLabelModal, initLabels } from '~/labels';
initLabels();
initDeleteLabelModal();
diff --git a/app/assets/javascripts/pages/groups/labels/new/index.js b/app/assets/javascripts/pages/groups/labels/new/index.js
index 2e8308fe084..e4e377f62fc 100644
--- a/app/assets/javascripts/pages/groups/labels/new/index.js
+++ b/app/assets/javascripts/pages/groups/labels/new/index.js
@@ -1,4 +1,4 @@
-import Labels from 'ee_else_ce/labels';
+import Labels from 'ee_else_ce/labels/labels';
// eslint-disable-next-line no-new
new Labels();
diff --git a/app/assets/javascripts/pages/groups/merge_requests/index.js b/app/assets/javascripts/pages/groups/merge_requests/index.js
index 02a0a50f984..cb38ee1c6e0 100644
--- a/app/assets/javascripts/pages/groups/merge_requests/index.js
+++ b/app/assets/javascripts/pages/groups/merge_requests/index.js
@@ -1,7 +1,7 @@
import addExtraTokensForMergeRequests from 'ee_else_ce/filtered_search/add_extra_tokens_for_merge_requests';
import IssuableFilteredSearchTokenKeys from '~/filtered_search/issuable_filtered_search_token_keys';
-import issuableInitBulkUpdateSidebar from '~/issuable_bulk_update_sidebar/issuable_init_bulk_update_sidebar';
-import { FILTERED_SEARCH } from '~/pages/constants';
+import issuableInitBulkUpdateSidebar from '~/issuable/bulk_update_sidebar/issuable_init_bulk_update_sidebar';
+import { FILTERED_SEARCH } from '~/filtered_search/constants';
import initFilteredSearch from '~/pages/search/init_filtered_search';
import projectSelect from '~/project_select';
diff --git a/app/assets/javascripts/pages/groups/milestones/edit/index.js b/app/assets/javascripts/pages/groups/milestones/edit/index.js
index 4f8514a9a1d..7fda129a85d 100644
--- a/app/assets/javascripts/pages/groups/milestones/edit/index.js
+++ b/app/assets/javascripts/pages/groups/milestones/edit/index.js
@@ -1,3 +1,3 @@
-import initForm from '~/shared/milestones/form';
+import { initForm } from '~/milestones';
initForm();
diff --git a/app/assets/javascripts/pages/groups/milestones/new/index.js b/app/assets/javascripts/pages/groups/milestones/new/index.js
index 4f8514a9a1d..7fda129a85d 100644
--- a/app/assets/javascripts/pages/groups/milestones/new/index.js
+++ b/app/assets/javascripts/pages/groups/milestones/new/index.js
@@ -1,3 +1,3 @@
-import initForm from '~/shared/milestones/form';
+import { initForm } from '~/milestones';
initForm();
diff --git a/app/assets/javascripts/pages/groups/milestones/show/index.js b/app/assets/javascripts/pages/groups/milestones/show/index.js
index 914e2831185..f2ab5d78374 100644
--- a/app/assets/javascripts/pages/groups/milestones/show/index.js
+++ b/app/assets/javascripts/pages/groups/milestones/show/index.js
@@ -1,5 +1,4 @@
-import initDeleteMilestoneModal from '~/pages/milestones/shared/delete_milestone_modal_init';
-import initMilestonesShow from '~/pages/milestones/shared/init_milestones_show';
+import { initDeleteMilestoneModal, initShow } from '~/milestones';
-initMilestonesShow();
+initShow();
initDeleteMilestoneModal();
diff --git a/app/assets/javascripts/pages/groups/packages/index/index.js b/app/assets/javascripts/pages/groups/packages/index/index.js
index f9eecff4ac4..174973a9fad 100644
--- a/app/assets/javascripts/pages/groups/packages/index/index.js
+++ b/app/assets/javascripts/pages/groups/packages/index/index.js
@@ -1,3 +1,3 @@
-import packageList from '~/packages_and_registries/package_registry/pages/list';
+import packageApp from '~/packages_and_registries/package_registry/index';
-packageList();
+packageApp();
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 a8d7a83cdd6..5d8ee146e62 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
@@ -1,7 +1,7 @@
import initVariableList from '~/ci_variable_list';
import GroupRunnersFilteredSearchTokenKeys from '~/filtered_search/group_runners_filtered_search_token_keys';
import initSharedRunnersForm from '~/group_settings/mount_shared_runners';
-import { FILTERED_SEARCH } from '~/pages/constants';
+import { FILTERED_SEARCH } from '~/filtered_search/constants';
import initFilteredSearch from '~/pages/search/init_filtered_search';
import { initRunnerAwsDeployments } from '~/pages/shared/mount_runner_aws_deployments';
import { initInstallRunner } from '~/pages/shared/mount_runner_instructions';
diff --git a/app/assets/javascripts/pages/groups/settings/integrations/edit/index.js b/app/assets/javascripts/pages/groups/settings/integrations/edit/index.js
index a8698e10c57..8485b460261 100644
--- a/app/assets/javascripts/pages/groups/settings/integrations/edit/index.js
+++ b/app/assets/javascripts/pages/groups/settings/integrations/edit/index.js
@@ -1,11 +1,11 @@
-import IntegrationSettingsForm from '~/integrations/integration_settings_form';
+import initIntegrationSettingsForm from '~/integrations/edit';
import PrometheusMetrics from '~/prometheus_metrics/prometheus_metrics';
-const prometheusSettingsWrapper = document.querySelector('.js-prometheus-metrics-monitoring');
-const integrationSettingsForm = new IntegrationSettingsForm('.js-integration-settings-form');
-integrationSettingsForm.init();
+initIntegrationSettingsForm('.js-integration-settings-form');
+const prometheusSettingsSelector = '.js-prometheus-metrics-monitoring';
+const prometheusSettingsWrapper = document.querySelector(prometheusSettingsSelector);
if (prometheusSettingsWrapper) {
- const prometheusMetrics = new PrometheusMetrics('.js-prometheus-metrics-monitoring');
+ const prometheusMetrics = new PrometheusMetrics(prometheusSettingsSelector);
prometheusMetrics.loadActiveMetrics();
}
diff --git a/app/assets/javascripts/pages/help/ui/index.js b/app/assets/javascripts/pages/help/ui/index.js
deleted file mode 100644
index 9ccc9123506..00000000000
--- a/app/assets/javascripts/pages/help/ui/index.js
+++ /dev/null
@@ -1,3 +0,0 @@
-import initUIKit from '~/ui_development_kit';
-
-initUIKit();
diff --git a/app/assets/javascripts/pages/import/bulk_imports/history/components/bulk_imports_history_app.vue b/app/assets/javascripts/pages/import/bulk_imports/history/components/bulk_imports_history_app.vue
index ec3cf4a8a92..0ec382983a5 100644
--- a/app/assets/javascripts/pages/import/bulk_imports/history/components/bulk_imports_history_app.vue
+++ b/app/assets/javascripts/pages/import/bulk_imports/history/components/bulk_imports_history_app.vue
@@ -7,7 +7,7 @@ import { parseIntPagination, normalizeHeaders } from '~/lib/utils/common_utils';
import { joinPaths } from '~/lib/utils/url_utility';
import { getBulkImportsHistory } from '~/rest_api';
import ImportStatus from '~/import_entities/components/import_status.vue';
-import PaginationBar from '~/import_entities/components/pagination_bar.vue';
+import PaginationBar from '~/vue_shared/components/pagination_bar/pagination_bar.vue';
import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue';
import { DEFAULT_ERROR } from '../utils/error_messages';
@@ -166,7 +166,6 @@ export default {
</gl-table>
<pagination-bar
:page-info="pageInfo"
- :items-count="historyItems.length"
class="gl-m-0 gl-mt-3"
@set-page="paginationConfig.page = $event"
@set-page-size="paginationConfig.perPage = $event"
diff --git a/app/assets/javascripts/pages/milestones/shared/index.js b/app/assets/javascripts/pages/milestones/shared/index.js
deleted file mode 100644
index dabfe32848b..00000000000
--- a/app/assets/javascripts/pages/milestones/shared/index.js
+++ /dev/null
@@ -1,7 +0,0 @@
-import initDeleteMilestoneModal from './delete_milestone_modal_init';
-import initPromoteMilestoneModal from './promote_milestone_modal_init';
-
-export default () => {
- initDeleteMilestoneModal();
- initPromoteMilestoneModal();
-};
diff --git a/app/assets/javascripts/pages/milestones/shared/init_milestones_show.js b/app/assets/javascripts/pages/milestones/shared/init_milestones_show.js
deleted file mode 100644
index b2a896a3265..00000000000
--- a/app/assets/javascripts/pages/milestones/shared/init_milestones_show.js
+++ /dev/null
@@ -1,11 +0,0 @@
-/* eslint-disable no-new */
-
-import Milestone from '~/milestone';
-import Sidebar from '~/right_sidebar';
-import MountMilestoneSidebar from '~/sidebar/mount_milestone_sidebar';
-
-export default () => {
- new Milestone();
- new Sidebar();
- new MountMilestoneSidebar();
-};
diff --git a/app/assets/javascripts/pages/milestones/shared/promote_milestone_modal_init.js b/app/assets/javascripts/pages/milestones/shared/promote_milestone_modal_init.js
deleted file mode 100644
index 5472b8c684f..00000000000
--- a/app/assets/javascripts/pages/milestones/shared/promote_milestone_modal_init.js
+++ /dev/null
@@ -1,19 +0,0 @@
-import Vue from 'vue';
-import Translate from '~/vue_shared/translate';
-import PromoteMilestoneModal from './components/promote_milestone_modal.vue';
-
-Vue.use(Translate);
-
-export default () => {
- const promoteMilestoneModal = document.getElementById('promote-milestone-modal');
- if (!promoteMilestoneModal) {
- return null;
- }
-
- return new Vue({
- el: promoteMilestoneModal,
- render(createElement) {
- return createElement(PromoteMilestoneModal);
- },
- });
-};
diff --git a/app/assets/javascripts/pages/profiles/personal_access_tokens/index.js b/app/assets/javascripts/pages/profiles/personal_access_tokens/index.js
index fdbfc35456f..37e9b7e99d4 100644
--- a/app/assets/javascripts/pages/profiles/personal_access_tokens/index.js
+++ b/app/assets/javascripts/pages/profiles/personal_access_tokens/index.js
@@ -1,4 +1,5 @@
-import { initExpiresAtField, initProjectsField } from '~/access_tokens';
+import { initExpiresAtField, initProjectsField, initTokensApp } from '~/access_tokens';
initExpiresAtField();
initProjectsField();
+initTokensApp();
diff --git a/app/assets/javascripts/pages/projects/constants.js b/app/assets/javascripts/pages/projects/constants.js
deleted file mode 100644
index 8dc765e5d10..00000000000
--- a/app/assets/javascripts/pages/projects/constants.js
+++ /dev/null
@@ -1,4 +0,0 @@
-export const ISSUABLE_INDEX = {
- MERGE_REQUEST: 'merge_request_',
- ISSUE: 'issue_',
-};
diff --git a/app/assets/javascripts/pages/projects/edit/index.js b/app/assets/javascripts/pages/projects/edit/index.js
index f4beefea90c..100ca5b36d9 100644
--- a/app/assets/javascripts/pages/projects/edit/index.js
+++ b/app/assets/javascripts/pages/projects/edit/index.js
@@ -1,13 +1,14 @@
import { PROJECT_BADGE } from '~/badges/constants';
import initLegacyConfirmDangerModal from '~/confirm_danger_modal';
+import initConfirmDanger from '~/init_confirm_danger';
import dirtySubmitFactory from '~/dirty_submit/dirty_submit_factory';
import initFilePickers from '~/file_pickers';
import mountBadgeSettings from '~/pages/shared/mount_badge_settings';
import initProjectDeleteButton from '~/projects/project_delete_button';
import initServiceDesk from '~/projects/settings_service_desk';
+import initTransferProjectForm from '~/projects/settings/init_transfer_project_form';
import initSearchSettings from '~/search_settings';
import initSettingsPanels from '~/settings_panels';
-import setupTransferEdit from '~/transfer_edit';
import UserCallout from '~/user_callout';
import initTopicsTokenSelector from '~/projects/settings/topics';
import initProjectPermissionsSettings from '../shared/permissions';
@@ -15,6 +16,7 @@ import initProjectLoadingSpinner from '../shared/save_project_loader';
initFilePickers();
initLegacyConfirmDangerModal();
+initConfirmDanger();
initSettingsPanels();
initProjectDeleteButton();
mountBadgeSettings(PROJECT_BADGE);
@@ -24,7 +26,7 @@ initServiceDesk();
initProjectLoadingSpinner();
initProjectPermissionsSettings();
-setupTransferEdit('.js-project-transfer-form', 'select.select2');
+initTransferProjectForm();
dirtySubmitFactory(document.querySelectorAll('.js-general-settings-form, .js-mr-settings-form'));
diff --git a/app/assets/javascripts/pages/projects/incidents/show/index.js b/app/assets/javascripts/pages/projects/incidents/show/index.js
index a75b68873ef..4633eaef8f9 100644
--- a/app/assets/javascripts/pages/projects/incidents/show/index.js
+++ b/app/assets/javascripts/pages/projects/incidents/show/index.js
@@ -1,6 +1,6 @@
import initRelatedIssues from '~/related_issues';
import initSidebarBundle from '~/sidebar/sidebar_bundle';
-import initShow from '../../issues/show';
+import initShow from '~/issues/show';
initShow();
initSidebarBundle();
diff --git a/app/assets/javascripts/pages/projects/issues/edit/index.js b/app/assets/javascripts/pages/projects/issues/edit/index.js
index 48afd2142ee..aa00d1f58bd 100644
--- a/app/assets/javascripts/pages/projects/issues/edit/index.js
+++ b/app/assets/javascripts/pages/projects/issues/edit/index.js
@@ -1,3 +1,3 @@
-import initForm from 'ee_else_ce/pages/projects/issues/form';
+import initForm from 'ee_else_ce/issues/form';
initForm();
diff --git a/app/assets/javascripts/pages/projects/issues/index/index.js b/app/assets/javascripts/pages/projects/issues/index/index.js
index 8cd703133f5..e937713044c 100644
--- a/app/assets/javascripts/pages/projects/issues/index/index.js
+++ b/app/assets/javascripts/pages/projects/issues/index/index.js
@@ -1,12 +1,11 @@
import IssuableFilteredSearchTokenKeys from 'ee_else_ce/filtered_search/issuable_filtered_search_token_keys';
import ShortcutsNavigation from '~/behaviors/shortcuts/shortcuts_navigation';
-import initCsvImportExportButtons from '~/issuable/init_csv_import_export_buttons';
-import initIssuableByEmail from '~/issuable/init_issuable_by_email';
-import IssuableIndex from '~/issuable_index';
+import { initCsvImportExportButtons, initIssuableByEmail } from '~/issuable';
+import issuableInitBulkUpdateSidebar from '~/issuable/bulk_update_sidebar/issuable_init_bulk_update_sidebar';
import { mountIssuablesListApp, mountIssuesListApp, mountJiraIssuesListApp } from '~/issues_list';
-import initManualOrdering from '~/manual_ordering';
-import { FILTERED_SEARCH } from '~/pages/constants';
-import { ISSUABLE_INDEX } from '~/pages/projects/constants';
+import initManualOrdering from '~/issues/manual_ordering';
+import { FILTERED_SEARCH } from '~/filtered_search/constants';
+import { ISSUABLE_INDEX } from '~/issuable/constants';
import initFilteredSearch from '~/pages/search/init_filtered_search';
import UsersSelect from '~/users_select';
@@ -21,7 +20,7 @@ if (gon.features?.vueIssuesList) {
useDefaultState: true,
});
- new IssuableIndex(ISSUABLE_INDEX.ISSUE); // eslint-disable-line no-new
+ issuableInitBulkUpdateSidebar.init(ISSUABLE_INDEX.ISSUE);
new UsersSelect(); // eslint-disable-line no-new
initCsvImportExportButtons();
diff --git a/app/assets/javascripts/pages/projects/issues/new/index.js b/app/assets/javascripts/pages/projects/issues/new/index.js
index 48afd2142ee..aa00d1f58bd 100644
--- a/app/assets/javascripts/pages/projects/issues/new/index.js
+++ b/app/assets/javascripts/pages/projects/issues/new/index.js
@@ -1,3 +1,3 @@
-import initForm from 'ee_else_ce/pages/projects/issues/form';
+import initForm from 'ee_else_ce/issues/form';
initForm();
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 d906c579697..69639d17f8a 100644
--- a/app/assets/javascripts/pages/projects/issues/service_desk/index.js
+++ b/app/assets/javascripts/pages/projects/issues/service_desk/index.js
@@ -1,14 +1,7 @@
import { mountIssuablesListApp } from '~/issues_list';
-import FilteredSearchServiceDesk from './filtered_search';
+import { initFilteredSearchServiceDesk } from '~/issues/init_filtered_search_service_desk';
-const supportBotData = JSON.parse(
- document.querySelector('.js-service-desk-issues').dataset.supportBot,
-);
-
-if (document.querySelector('.filtered-search')) {
- const filteredSearchManager = new FilteredSearchServiceDesk(supportBotData);
- filteredSearchManager.setup();
-}
+initFilteredSearchServiceDesk();
if (gon.features?.vueIssuablesList) {
mountIssuablesListApp();
diff --git a/app/assets/javascripts/pages/projects/issues/show/index.js b/app/assets/javascripts/pages/projects/issues/show/index.js
index 1282d2aa303..d0b1942f2a4 100644
--- a/app/assets/javascripts/pages/projects/issues/show/index.js
+++ b/app/assets/javascripts/pages/projects/issues/show/index.js
@@ -1,7 +1,7 @@
import { store } from '~/notes/stores';
import initRelatedIssues from '~/related_issues';
import initSidebarBundle from '~/sidebar/sidebar_bundle';
-import initShow from '../show';
+import initShow from '~/issues/show';
initShow();
initSidebarBundle(store);
diff --git a/app/assets/javascripts/pages/projects/labels/edit/index.js b/app/assets/javascripts/pages/projects/labels/edit/index.js
index 3b7562deed9..c4d7af39767 100644
--- a/app/assets/javascripts/pages/projects/labels/edit/index.js
+++ b/app/assets/javascripts/pages/projects/labels/edit/index.js
@@ -1,3 +1,3 @@
-import Labels from 'ee_else_ce/labels';
+import Labels from 'ee_else_ce/labels/labels';
new Labels(); // eslint-disable-line no-new
diff --git a/app/assets/javascripts/pages/projects/labels/index/index.js b/app/assets/javascripts/pages/projects/labels/index/index.js
index 94ab0d64de4..1f8ff7e0bb1 100644
--- a/app/assets/javascripts/pages/projects/labels/index/index.js
+++ b/app/assets/javascripts/pages/projects/labels/index/index.js
@@ -1,83 +1,3 @@
-import Vue from 'vue';
-import initDeleteLabelModal from '~/delete_label_modal';
-import initLabels from '~/init_labels';
-import { BV_SHOW_MODAL } from '~/lib/utils/constants';
-import Translate from '~/vue_shared/translate';
-import PromoteLabelModal from '../components/promote_label_modal.vue';
-import eventHub from '../event_hub';
+import { initLabelIndex } from '~/labels';
-Vue.use(Translate);
-
-const initLabelIndex = () => {
- initLabels();
- initDeleteLabelModal();
-
- const onRequestFinished = ({ labelUrl, successful }) => {
- const button = document.querySelector(
- `.js-promote-project-label-button[data-url="${labelUrl}"]`,
- );
-
- if (!successful) {
- button.removeAttribute('disabled');
- }
- };
-
- const onRequestStarted = (labelUrl) => {
- const button = document.querySelector(
- `.js-promote-project-label-button[data-url="${labelUrl}"]`,
- );
- button.setAttribute('disabled', '');
- eventHub.$once('promoteLabelModal.requestFinished', onRequestFinished);
- };
-
- const promoteLabelButtons = document.querySelectorAll('.js-promote-project-label-button');
-
- return new Vue({
- el: '#js-promote-label-modal',
- data() {
- return {
- modalProps: {
- labelTitle: '',
- labelColor: '',
- labelTextColor: '',
- url: '',
- groupName: '',
- },
- };
- },
- mounted() {
- eventHub.$on('promoteLabelModal.props', this.setModalProps);
- eventHub.$emit('promoteLabelModal.mounted');
-
- promoteLabelButtons.forEach((button) => {
- button.removeAttribute('disabled');
- button.addEventListener('click', () => {
- this.$root.$emit(BV_SHOW_MODAL, 'promote-label-modal');
- eventHub.$once('promoteLabelModal.requestStarted', onRequestStarted);
-
- this.setModalProps({
- labelTitle: button.dataset.labelTitle,
- labelColor: button.dataset.labelColor,
- labelTextColor: button.dataset.labelTextColor,
- url: button.dataset.url,
- groupName: button.dataset.groupName,
- });
- });
- });
- },
- beforeDestroy() {
- eventHub.$off('promoteLabelModal.props', this.setModalProps);
- },
- methods: {
- setModalProps(modalProps) {
- this.modalProps = modalProps;
- },
- },
- render(createElement) {
- return createElement(PromoteLabelModal, {
- props: this.modalProps,
- });
- },
- });
-};
initLabelIndex();
diff --git a/app/assets/javascripts/pages/projects/labels/new/index.js b/app/assets/javascripts/pages/projects/labels/new/index.js
index 2e8308fe084..e4e377f62fc 100644
--- a/app/assets/javascripts/pages/projects/labels/new/index.js
+++ b/app/assets/javascripts/pages/projects/labels/new/index.js
@@ -1,4 +1,4 @@
-import Labels from 'ee_else_ce/labels';
+import Labels from 'ee_else_ce/labels/labels';
// eslint-disable-next-line no-new
new Labels();
diff --git a/app/assets/javascripts/pages/projects/learn_gitlab/components/learn_gitlab.vue b/app/assets/javascripts/pages/projects/learn_gitlab/components/learn_gitlab.vue
index 95afcb6bda8..42c40cda601 100644
--- a/app/assets/javascripts/pages/projects/learn_gitlab/components/learn_gitlab.vue
+++ b/app/assets/javascripts/pages/projects/learn_gitlab/components/learn_gitlab.vue
@@ -1,18 +1,21 @@
<script>
-import { GlProgressBar, GlSprintf } from '@gitlab/ui';
+import { GlProgressBar, GlSprintf, GlAlert } from '@gitlab/ui';
import eventHub from '~/invite_members/event_hub';
import { s__ } from '~/locale';
import { ACTION_LABELS, ACTION_SECTIONS } from '../constants';
import LearnGitlabSectionCard from './learn_gitlab_section_card.vue';
export default {
- components: { GlProgressBar, GlSprintf, LearnGitlabSectionCard },
+ components: { GlProgressBar, GlSprintf, GlAlert, LearnGitlabSectionCard },
i18n: {
title: s__('LearnGitLab|Learn GitLab'),
description: s__(
'LearnGitLab|Ready to get started with GitLab? Follow these steps to set up your workspace, plan and commit changes, and deploy your project.',
),
percentageCompleted: s__(`LearnGitLab|%{percentage}%{percentSymbol} completed`),
+ successfulInvitations: s__(
+ "LearnGitLab|Your team is growing! You've successfully invited new team members to the %{projectName} project.",
+ ),
},
props: {
actions: {
@@ -28,12 +31,22 @@ export default {
required: false,
default: false,
},
+ project: {
+ required: true,
+ type: Object,
+ },
+ },
+ data() {
+ return {
+ showSuccessfulInvitationsAlert: false,
+ actionsData: this.actions,
+ };
},
maxValue: Object.keys(ACTION_LABELS).length,
actionSections: Object.keys(ACTION_SECTIONS),
computed: {
progressValue() {
- return Object.values(this.actions).filter((a) => a.completed).length;
+ return Object.values(this.actionsData).filter((a) => a.completed).length;
},
progressPercentage() {
return Math.round((this.progressValue / this.$options.maxValue) * 100);
@@ -43,14 +56,23 @@ export default {
if (this.inviteMembersOpen) {
this.openInviteMembersModal('celebrate');
}
+
+ eventHub.$on('showSuccessfulInvitationsAlert', this.handleShowSuccessfulInvitationsAlert);
+ },
+ beforeDestroy() {
+ eventHub.$off('showSuccessfulInvitationsAlert', this.handleShowSuccessfulInvitationsAlert);
},
methods: {
openInviteMembersModal(mode) {
eventHub.$emit('openModal', { mode, inviteeType: 'members', source: 'learn-gitlab' });
},
+ handleShowSuccessfulInvitationsAlert() {
+ this.showSuccessfulInvitationsAlert = true;
+ this.markActionAsCompleted('userAdded');
+ },
actionsFor(section) {
const actions = Object.fromEntries(
- Object.entries(this.actions).filter(
+ Object.entries(this.actionsData).filter(
([action]) => ACTION_LABELS[action].section === section,
),
);
@@ -59,11 +81,34 @@ export default {
svgFor(section) {
return this.sections[section].svg;
},
+ markActionAsCompleted(completedAction) {
+ Object.keys(this.actionsData).forEach((action) => {
+ if (action === completedAction) {
+ this.actionsData[action].completed = true;
+ this.modifySidebarPercentage();
+ }
+ });
+ },
+ modifySidebarPercentage() {
+ const el = document.querySelector('.sidebar-top-level-items .active .count');
+ el.textContent = `${this.progressPercentage}%`;
+ },
},
};
</script>
<template>
<div>
+ <gl-alert
+ v-if="showSuccessfulInvitationsAlert"
+ class="gl-mt-5"
+ @dismiss="showSuccessfulInvitationsAlert = false"
+ >
+ <gl-sprintf :message="$options.i18n.successfulInvitations">
+ <template #projectName>
+ <strong>{{ project.name }}</strong>
+ </template>
+ </gl-sprintf>
+ </gl-alert>
<div class="row">
<div class="gl-mb-7 gl-ml-5">
<h1 class="gl-font-size-h1">{{ $options.i18n.title }}</h1>
diff --git a/app/assets/javascripts/pages/projects/learn_gitlab/components/learn_gitlab_section_link.vue b/app/assets/javascripts/pages/projects/learn_gitlab/components/learn_gitlab_section_link.vue
index 0995947f3e7..3a401f5cb31 100644
--- a/app/assets/javascripts/pages/projects/learn_gitlab/components/learn_gitlab_section_link.vue
+++ b/app/assets/javascripts/pages/projects/learn_gitlab/components/learn_gitlab_section_link.vue
@@ -1,5 +1,7 @@
<script>
import { GlLink, GlIcon } from '@gitlab/ui';
+import { isExperimentVariant } from '~/experimentation/utils';
+import eventHub from '~/invite_members/event_hub';
import { s__ } from '~/locale';
import { ACTION_LABELS } from '../constants';
@@ -24,6 +26,20 @@ export default {
trialOnly() {
return ACTION_LABELS[this.action].trialRequired;
},
+ showInviteModalLink() {
+ return (
+ this.action === 'userAdded' && isExperimentVariant('invite_for_help_continuous_onboarding')
+ );
+ },
+ },
+ methods: {
+ openModal() {
+ eventHub.$emit('openModal', {
+ inviteeType: 'members',
+ source: 'learn_gitlab',
+ tasksToBeDoneEnabled: true,
+ });
+ },
},
};
</script>
@@ -33,18 +49,27 @@ export default {
<gl-icon name="check-circle-filled" :size="16" data-testid="completed-icon" />
{{ $options.i18n.ACTION_LABELS[action].title }}
</span>
- <span v-else>
- <gl-link
- target="_blank"
- :href="value.url"
- data-track-action="click_link"
- :data-track-label="$options.i18n.ACTION_LABELS[action].title"
- data-track-property="Growth::Conversion::Experiment::LearnGitLab"
- data-track-experiment="change_continuous_onboarding_link_urls"
- >
- {{ $options.i18n.ACTION_LABELS[action].title }}
- </gl-link>
- </span>
+ <gl-link
+ v-else-if="showInviteModalLink"
+ data-track-action="click_link"
+ :data-track-label="$options.i18n.ACTION_LABELS[action].title"
+ data-track-property="Growth::Activation::Experiment::InviteForHelpContinuousOnboarding"
+ data-testid="invite-for-help-continuous-onboarding-experiment-link"
+ @click="openModal"
+ >
+ {{ $options.i18n.ACTION_LABELS[action].title }}
+ </gl-link>
+ <gl-link
+ v-else
+ target="_blank"
+ :href="value.url"
+ data-track-action="click_link"
+ :data-track-label="$options.i18n.ACTION_LABELS[action].title"
+ data-track-property="Growth::Conversion::Experiment::LearnGitLab"
+ data-track-experiment="change_continuous_onboarding_link_urls"
+ >
+ {{ $options.i18n.ACTION_LABELS[action].title }}
+ </gl-link>
<span v-if="trialOnly" class="gl-font-style-italic gl-text-gray-500" data-testid="trial-only">
- {{ $options.i18n.trialOnly }}
</span>
diff --git a/app/assets/javascripts/pages/projects/learn_gitlab/index/index.js b/app/assets/javascripts/pages/projects/learn_gitlab/index/index.js
index ea9eec2595f..1f91cc46946 100644
--- a/app/assets/javascripts/pages/projects/learn_gitlab/index/index.js
+++ b/app/assets/javascripts/pages/projects/learn_gitlab/index/index.js
@@ -12,17 +12,18 @@ function initLearnGitlab() {
const actions = convertObjectPropsToCamelCase(JSON.parse(el.dataset.actions));
const sections = convertObjectPropsToCamelCase(JSON.parse(el.dataset.sections));
+ const project = convertObjectPropsToCamelCase(JSON.parse(el.dataset.project));
const { inviteMembersOpen } = el.dataset;
return new Vue({
el,
render(createElement) {
return createElement(LearnGitlab, {
- props: { actions, sections, inviteMembersOpen },
+ props: { actions, sections, project, inviteMembersOpen },
});
},
});
}
-initInviteMembersModal();
initLearnGitlab();
+initInviteMembersModal();
diff --git a/app/assets/javascripts/pages/projects/merge_requests/index/index.js b/app/assets/javascripts/pages/projects/merge_requests/index/index.js
index d279086df7b..acd1731a700 100644
--- a/app/assets/javascripts/pages/projects/merge_requests/index/index.js
+++ b/app/assets/javascripts/pages/projects/merge_requests/index/index.js
@@ -1,17 +1,17 @@
import addExtraTokensForMergeRequests from 'ee_else_ce/filtered_search/add_extra_tokens_for_merge_requests';
import ShortcutsNavigation from '~/behaviors/shortcuts/shortcuts_navigation';
import IssuableFilteredSearchTokenKeys from '~/filtered_search/issuable_filtered_search_token_keys';
-import initCsvImportExportButtons from '~/issuable/init_csv_import_export_buttons';
-import initIssuableByEmail from '~/issuable/init_issuable_by_email';
-import IssuableIndex from '~/issuable_index';
-import { FILTERED_SEARCH } from '~/pages/constants';
-import { ISSUABLE_INDEX } from '~/pages/projects/constants';
+import { initCsvImportExportButtons, initIssuableByEmail } from '~/issuable';
+import issuableInitBulkUpdateSidebar from '~/issuable/bulk_update_sidebar/issuable_init_bulk_update_sidebar';
+import { FILTERED_SEARCH } from '~/filtered_search/constants';
+import { ISSUABLE_INDEX } from '~/issuable/constants';
import initFilteredSearch from '~/pages/search/init_filtered_search';
import UsersSelect from '~/users_select';
-new IssuableIndex(ISSUABLE_INDEX.MERGE_REQUEST); // eslint-disable-line no-new
+issuableInitBulkUpdateSidebar.init(ISSUABLE_INDEX.MERGE_REQUEST);
addExtraTokensForMergeRequests(IssuableFilteredSearchTokenKeys);
+IssuableFilteredSearchTokenKeys.removeTokensForKeys('iteration');
initFilteredSearch({
page: FILTERED_SEARCH.MERGE_REQUESTS,
diff --git a/app/assets/javascripts/pages/projects/merge_requests/init_merge_request.js b/app/assets/javascripts/pages/projects/merge_requests/init_merge_request.js
index 7d5719cf8a8..ebf7c266482 100644
--- a/app/assets/javascripts/pages/projects/merge_requests/init_merge_request.js
+++ b/app/assets/javascripts/pages/projects/merge_requests/init_merge_request.js
@@ -1,13 +1,13 @@
/* eslint-disable no-new */
import $ from 'jquery';
-import IssuableForm from 'ee_else_ce/issuable_form';
+import IssuableForm from 'ee_else_ce/issuable/issuable_form';
import ShortcutsNavigation from '~/behaviors/shortcuts/shortcuts_navigation';
import Diff from '~/diff';
import GLForm from '~/gl_form';
-import LabelsSelect from '~/labels_select';
-import MilestoneSelect from '~/milestone_select';
-import IssuableTemplateSelectors from '~/templates/issuable_template_selectors';
+import LabelsSelect from '~/labels/labels_select';
+import MilestoneSelect from '~/milestones/milestone_select';
+import IssuableTemplateSelectors from '~/issuable/issuable_template_selectors';
export default () => {
new Diff();
diff --git a/app/assets/javascripts/pages/projects/merge_requests/init_merge_request_show.js b/app/assets/javascripts/pages/projects/merge_requests/init_merge_request_show.js
index 99094617b0a..c548ea9bb80 100644
--- a/app/assets/javascripts/pages/projects/merge_requests/init_merge_request_show.js
+++ b/app/assets/javascripts/pages/projects/merge_requests/init_merge_request_show.js
@@ -3,7 +3,7 @@ import VueApollo from 'vue-apollo';
import loadAwardsHandler from '~/awards_handler';
import ShortcutsIssuable from '~/behaviors/shortcuts/shortcuts_issuable';
import { initPipelineCountListener } from '~/commit/pipelines/utils';
-import initIssuableSidebar from '~/init_issuable_sidebar';
+import { initIssuableSidebar } from '~/issuable';
import StatusBox from '~/issuable/components/status_box.vue';
import createDefaultClient from '~/lib/graphql';
import initSourcegraph from '~/sourcegraph';
diff --git a/app/assets/javascripts/pages/projects/merge_requests/queries/get_state.query.graphql b/app/assets/javascripts/pages/projects/merge_requests/queries/get_state.query.graphql
index b5a82b9428e..1edb37a228d 100644
--- a/app/assets/javascripts/pages/projects/merge_requests/queries/get_state.query.graphql
+++ b/app/assets/javascripts/pages/projects/merge_requests/queries/get_state.query.graphql
@@ -1,6 +1,8 @@
query getMergeRequestState($projectPath: ID!, $iid: String!) {
workspace: project(fullPath: $projectPath) {
+ id
issuable: mergeRequest(iid: $iid) {
+ id
state
}
}
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 25dede33880..7f49eb60c5c 100644
--- a/app/assets/javascripts/pages/projects/merge_requests/show/index.js
+++ b/app/assets/javascripts/pages/projects/merge_requests/show/index.js
@@ -1,8 +1,8 @@
import { initReviewBar } from '~/batch_comments';
+import { initIssuableHeaderWarnings } from '~/issuable';
import initMrNotes from '~/mr_notes';
import store from '~/mr_notes/stores';
import initSidebarBundle from '~/sidebar/sidebar_bundle';
-import initIssuableHeaderWarning from '~/vue_shared/components/issuable/init_issuable_header_warning';
import initShow from '../init_merge_request_show';
initMrNotes();
@@ -11,5 +11,5 @@ initShow();
requestIdleCallback(() => {
initSidebarBundle(store);
initReviewBar();
- initIssuableHeaderWarning(store);
+ initIssuableHeaderWarnings(store);
});
diff --git a/app/assets/javascripts/pages/projects/milestones/edit/index.js b/app/assets/javascripts/pages/projects/milestones/edit/index.js
index 4f8514a9a1d..7fda129a85d 100644
--- a/app/assets/javascripts/pages/projects/milestones/edit/index.js
+++ b/app/assets/javascripts/pages/projects/milestones/edit/index.js
@@ -1,3 +1,3 @@
-import initForm from '~/shared/milestones/form';
+import { initForm } from '~/milestones';
initForm();
diff --git a/app/assets/javascripts/pages/projects/milestones/index/index.js b/app/assets/javascripts/pages/projects/milestones/index/index.js
index 150b506b121..ef1c9ab83db 100644
--- a/app/assets/javascripts/pages/projects/milestones/index/index.js
+++ b/app/assets/javascripts/pages/projects/milestones/index/index.js
@@ -1,3 +1,4 @@
-import milestones from '~/pages/milestones/shared';
+import { initDeleteMilestoneModal, initPromoteMilestoneModal } from '~/milestones';
-milestones();
+initDeleteMilestoneModal();
+initPromoteMilestoneModal();
diff --git a/app/assets/javascripts/pages/projects/milestones/new/index.js b/app/assets/javascripts/pages/projects/milestones/new/index.js
index 4f8514a9a1d..7fda129a85d 100644
--- a/app/assets/javascripts/pages/projects/milestones/new/index.js
+++ b/app/assets/javascripts/pages/projects/milestones/new/index.js
@@ -1,3 +1,3 @@
-import initForm from '~/shared/milestones/form';
+import { initForm } from '~/milestones';
initForm();
diff --git a/app/assets/javascripts/pages/projects/milestones/show/index.js b/app/assets/javascripts/pages/projects/milestones/show/index.js
index 3c755e9b98c..16aac7748da 100644
--- a/app/assets/javascripts/pages/projects/milestones/show/index.js
+++ b/app/assets/javascripts/pages/projects/milestones/show/index.js
@@ -1,5 +1,5 @@
-import milestones from '~/pages/milestones/shared';
-import initMilestonesShow from '~/pages/milestones/shared/init_milestones_show';
+import { initDeleteMilestoneModal, initPromoteMilestoneModal, initShow } from '~/milestones';
-initMilestonesShow();
-milestones();
+initShow();
+initDeleteMilestoneModal();
+initPromoteMilestoneModal();
diff --git a/app/assets/javascripts/pages/projects/packages/packages/index/index.js b/app/assets/javascripts/pages/projects/packages/packages/index/index.js
index f9eecff4ac4..174973a9fad 100644
--- a/app/assets/javascripts/pages/projects/packages/packages/index/index.js
+++ b/app/assets/javascripts/pages/projects/packages/packages/index/index.js
@@ -1,3 +1,3 @@
-import packageList from '~/packages_and_registries/package_registry/pages/list';
+import packageApp from '~/packages_and_registries/package_registry/index';
-packageList();
+packageApp();
diff --git a/app/assets/javascripts/pages/projects/path_locks/index.js b/app/assets/javascripts/pages/projects/path_locks/index.js
deleted file mode 100644
index e5ab5d43bbf..00000000000
--- a/app/assets/javascripts/pages/projects/path_locks/index.js
+++ /dev/null
@@ -1,3 +0,0 @@
-import initDeprecatedRemoveRowBehavior from '~/behaviors/deprecated_remove_row_behavior';
-
-document.addEventListener('DOMContentLoaded', initDeprecatedRemoveRowBehavior);
diff --git a/app/assets/javascripts/pages/projects/services/edit/index.js b/app/assets/javascripts/pages/projects/services/edit/index.js
index 03ffc323fc0..a2b18d86240 100644
--- a/app/assets/javascripts/pages/projects/services/edit/index.js
+++ b/app/assets/javascripts/pages/projects/services/edit/index.js
@@ -1,9 +1,8 @@
-import IntegrationSettingsForm from '~/integrations/integration_settings_form';
+import initIntegrationSettingsForm from '~/integrations/edit';
import PrometheusAlerts from '~/prometheus_alerts';
import CustomMetrics from '~/prometheus_metrics/custom_metrics';
-const integrationSettingsForm = new IntegrationSettingsForm('.js-integration-settings-form');
-integrationSettingsForm.init();
+initIntegrationSettingsForm('.js-integration-settings-form');
const prometheusSettingsSelector = '.js-prometheus-metrics-monitoring';
const prometheusSettingsWrapper = document.querySelector(prometheusSettingsSelector);
diff --git a/app/assets/javascripts/pages/projects/usage_quotas/index.js b/app/assets/javascripts/pages/projects/usage_quotas/index.js
deleted file mode 100644
index 9cd80b85c8a..00000000000
--- a/app/assets/javascripts/pages/projects/usage_quotas/index.js
+++ /dev/null
@@ -1,23 +0,0 @@
-import LinkedTabs from '~/lib/utils/bootstrap_linked_tabs';
-import storageCounter from '~/projects/storage_counter';
-import initSearchSettings from '~/search_settings';
-
-const initLinkedTabs = () => {
- if (!document.querySelector('.js-usage-quota-tabs')) {
- return false;
- }
-
- return new LinkedTabs({
- defaultAction: '#storage-quota-tab',
- parentEl: '.js-usage-quota-tabs',
- hashedTabs: true,
- });
-};
-
-const initVueApp = () => {
- storageCounter('js-project-storage-count-app');
-};
-
-initVueApp();
-initLinkedTabs();
-initSearchSettings();
diff --git a/app/assets/javascripts/pages/shared/wikis/components/wiki_form.vue b/app/assets/javascripts/pages/shared/wikis/components/wiki_form.vue
index 6f19a9f4379..b29e9455755 100644
--- a/app/assets/javascripts/pages/shared/wikis/components/wiki_form.vue
+++ b/app/assets/javascripts/pages/shared/wikis/components/wiki_form.vue
@@ -15,6 +15,7 @@ import { setUrlFragment } from '~/lib/utils/url_utility';
import { __, s__, sprintf } from '~/locale';
import Tracking from '~/tracking';
import MarkdownField from '~/vue_shared/components/markdown/field.vue';
+import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import {
CONTENT_EDITOR_LOADED_ACTION,
SAVED_USING_CONTENT_EDITOR_ACTION,
@@ -46,7 +47,7 @@ export default {
newPage: s__(
'WikiPage|Tip: You can specify the full path for the new file. We will automatically create any missing directories.',
),
- moreInformation: s__('WikiPage|More Information.'),
+ learnMore: s__('WikiPage|Learn more.'),
},
},
format: {
@@ -104,6 +105,8 @@ export default {
newPage: s__('WikiPage|Create page'),
},
cancel: s__('WikiPage|Cancel'),
+ editSourceButtonText: s__('WikiPage|Edit source'),
+ editRichTextButtonText: s__('WikiPage|Edit rich text'),
},
contentEditorFeedbackIssue: 'https://gitlab.com/gitlab-org/gitlab/-/issues/332629',
components: {
@@ -123,7 +126,7 @@ export default {
directives: {
GlModalDirective,
},
- mixins: [trackingMixin],
+ mixins: [trackingMixin, glFeatureFlagMixin()],
inject: ['formatOptions', 'pageInfo'],
data() {
return {
@@ -131,7 +134,6 @@ export default {
format: this.pageInfo.format || 'markdown',
content: this.pageInfo.content || '',
isContentEditorAlertDismissed: false,
- isContentEditorLoading: true,
useContentEditor: false,
commitMessage: '',
isDirty: false,
@@ -164,6 +166,11 @@ export default {
linkExample() {
return MARKDOWN_LINK_TEXT[this.format];
},
+ toggleEditingModeButtonText() {
+ return this.isContentEditorActive
+ ? this.$options.i18n.editSourceButtonText
+ : this.$options.i18n.editRichTextButtonText;
+ },
submitButtonText() {
return this.pageInfo.persisted
? this.$options.i18n.submitButton.existingPage
@@ -188,7 +195,23 @@ export default {
return this.format === 'markdown';
},
showContentEditorAlert() {
- return this.isMarkdownFormat && !this.useContentEditor && !this.isContentEditorAlertDismissed;
+ return (
+ !this.glFeatures.wikiSwitchBetweenContentEditorRawMarkdown &&
+ this.isMarkdownFormat &&
+ !this.useContentEditor &&
+ !this.isContentEditorAlertDismissed
+ );
+ },
+ showSwitchEditingModeButton() {
+ return this.glFeatures.wikiSwitchBetweenContentEditorRawMarkdown && this.isMarkdownFormat;
+ },
+ displayWikiSpecificMarkdownHelp() {
+ return !this.isContentEditorActive;
+ },
+ displaySwitchBackToClassicEditorMessage() {
+ return (
+ !this.glFeatures.wikiSwitchBetweenContentEditorRawMarkdown && this.isContentEditorActive
+ );
},
disableSubmitButton() {
return this.noContent || !this.title || this.contentEditorRenderFailed;
@@ -212,6 +235,14 @@ export default {
.then(({ data }) => data.body);
},
+ toggleEditingMode() {
+ if (this.useContentEditor) {
+ this.content = this.contentEditor.getSerializedContent();
+ }
+
+ this.useContentEditor = !this.useContentEditor;
+ },
+
async handleFormSubmit(e) {
e.preventDefault();
@@ -311,8 +342,11 @@ export default {
trackWikiFormat() {
this.track(WIKI_FORMAT_UPDATED_ACTION, {
label: WIKI_FORMAT_LABEL,
- value: this.format,
- extra: { project_path: this.pageInfo.path, old_format: this.pageInfo.format },
+ extra: {
+ project_path: this.pageInfo.path,
+ old_format: this.pageInfo.format,
+ value: this.format,
+ },
});
},
@@ -371,10 +405,9 @@ export default {
<span class="gl-display-inline-block gl-max-w-full gl-mt-2 gl-text-gray-600">
<gl-icon class="gl-mr-n1" name="bulb" />
{{ titleHelpText }}
- <gl-link :href="helpPath" target="_blank"
- ><gl-icon name="question-o" />
- {{ $options.i18n.title.helpText.moreInformation }}</gl-link
- >
+ <gl-link :href="helpPath" target="_blank">
+ {{ $options.i18n.title.helpText.learnMore }}
+ </gl-link>
</span>
</div>
</div>
@@ -405,6 +438,19 @@ export default {
}}</label>
</div>
<div class="col-sm-10">
+ <div
+ v-if="showSwitchEditingModeButton"
+ class="gl-display-flex gl-justify-content-end gl-mb-3"
+ >
+ <gl-button
+ data-testid="toggle-editing-mode-button"
+ data-qa-selector="editing_mode_button"
+ :data-qa-mode="toggleEditingModeButtonText"
+ variant="link"
+ @click="toggleEditingMode"
+ >{{ toggleEditingModeButtonText }}</gl-button
+ >
+ </div>
<gl-alert
v-if="showContentEditorAlert"
class="gl-mb-6"
@@ -498,7 +544,7 @@ export default {
<div class="error-alert"></div>
<div class="form-text gl-text-gray-600">
- <gl-sprintf v-if="!isContentEditorActive" :message="$options.i18n.linksHelpText">
+ <gl-sprintf v-if="displayWikiSpecificMarkdownHelp" :message="$options.i18n.linksHelpText">
<template #linkExample
><code>{{ linkExample }}</code></template
>
@@ -513,7 +559,7 @@ export default {
></template
>
</gl-sprintf>
- <span v-else>
+ <span v-if="displaySwitchBackToClassicEditorMessage">
{{ $options.i18n.contentEditor.switchToOldEditor.helpText }}
<gl-button variant="link" @click="confirmSwitchToOldEditor">{{
$options.i18n.contentEditor.switchToOldEditor.label
diff --git a/app/assets/javascripts/pdf/index.vue b/app/assets/javascripts/pdf/index.vue
index 6a64538abfe..644eccc0232 100644
--- a/app/assets/javascripts/pdf/index.vue
+++ b/app/assets/javascripts/pdf/index.vue
@@ -45,7 +45,7 @@ export default {
.promise.then(this.renderPages)
.then((pages) => {
this.pages = pages;
- this.$emit('pdflabload');
+ this.$emit('pdflabload', pages.length);
})
.catch((error) => {
this.$emit('pdflaberror', error);
diff --git a/app/assets/javascripts/pipeline_editor/components/commit/commit_form.vue b/app/assets/javascripts/pipeline_editor/components/commit/commit_form.vue
index 905a5f2d271..9f82d4a5395 100644
--- a/app/assets/javascripts/pipeline_editor/components/commit/commit_form.vue
+++ b/app/assets/javascripts/pipeline_editor/components/commit/commit_form.vue
@@ -73,7 +73,7 @@ export default {
});
},
onReset() {
- this.$emit('cancel');
+ this.$emit('resetContent');
},
scrollIntoView() {
this.$el.scrollIntoView({ behavior: 'smooth' });
@@ -86,7 +86,7 @@ export default {
startMergeRequest: __('Start a %{new_merge_request} with these changes'),
newMergeRequest: __('new merge request'),
commitChanges: __('Commit changes'),
- cancel: __('Cancel'),
+ resetContent: __('Reset'),
},
};
</script>
@@ -148,7 +148,7 @@ export default {
{{ $options.i18n.commitChanges }}
</gl-button>
<gl-button type="reset" category="secondary" class="gl-mr-3">
- {{ $options.i18n.cancel }}
+ {{ $options.i18n.resetContent }}
</gl-button>
</div>
</gl-form>
diff --git a/app/assets/javascripts/pipeline_editor/components/commit/commit_section.vue b/app/assets/javascripts/pipeline_editor/components/commit/commit_section.vue
index 14c11099756..54c9688d88f 100644
--- a/app/assets/javascripts/pipeline_editor/components/commit/commit_section.vue
+++ b/app/assets/javascripts/pipeline_editor/components/commit/commit_section.vue
@@ -8,10 +8,10 @@ import {
COMMIT_SUCCESS,
} from '../../constants';
import commitCIFile from '../../graphql/mutations/commit_ci_file.mutation.graphql';
-import updateCurrentBranchMutation from '../../graphql/mutations/update_current_branch.mutation.graphql';
-import updateLastCommitBranchMutation from '../../graphql/mutations/update_last_commit_branch.mutation.graphql';
-import updatePipelineEtag from '../../graphql/mutations/update_pipeline_etag.mutation.graphql';
-import getCurrentBranch from '../../graphql/queries/client/current_branch.graphql';
+import updateCurrentBranchMutation from '../../graphql/mutations/client/update_current_branch.mutation.graphql';
+import updateLastCommitBranchMutation from '../../graphql/mutations/client/update_last_commit_branch.mutation.graphql';
+import updatePipelineEtag from '../../graphql/mutations/client/update_pipeline_etag.mutation.graphql';
+import getCurrentBranch from '../../graphql/queries/client/current_branch.query.graphql';
import CommitForm from './commit_form.vue';
@@ -60,6 +60,9 @@ export default {
apollo: {
currentBranch: {
query: getCurrentBranch,
+ update(data) {
+ return data.workBranches.current.name;
+ },
},
},
computed: {
@@ -87,7 +90,7 @@ export default {
try {
const {
data: {
- commitCreate: { errors },
+ commitCreate: { errors, commitPipelinePath: pipelineEtag },
},
} = await this.$apollo.mutate({
mutation: commitCIFile,
@@ -101,14 +104,12 @@ export default {
content: this.ciFileContent,
lastCommitId: this.commitSha,
},
- update(_, { data }) {
- const pipelineEtag = data?.commitCreate?.commit?.commitPipelinePath;
- if (pipelineEtag) {
- this.$apollo.mutate({ mutation: updatePipelineEtag, variables: pipelineEtag });
- }
- },
});
+ if (pipelineEtag) {
+ this.updatePipelineEtag(pipelineEtag);
+ }
+
if (errors?.length) {
this.$emit('showError', { type: COMMIT_FAILURE, reasons: errors });
} else if (openMergeRequest) {
@@ -127,9 +128,6 @@ export default {
this.isSaving = false;
}
},
- onCommitCancel() {
- this.$emit('resetContent');
- },
updateCurrentBranch(currentBranch) {
this.$apollo.mutate({
mutation: updateCurrentBranchMutation,
@@ -142,6 +140,9 @@ export default {
variables: { lastCommitBranch },
});
},
+ updatePipelineEtag(pipelineEtag) {
+ this.$apollo.mutate({ mutation: updatePipelineEtag, variables: { pipelineEtag } });
+ },
},
};
</script>
@@ -153,7 +154,6 @@ export default {
:is-saving="isSaving"
:scroll-to-commit-form="scrollToCommitForm"
v-on="$listeners"
- @cancel="onCommitCancel"
@submit="onCommitSubmit"
/>
</template>
diff --git a/app/assets/javascripts/pipeline_editor/components/drawer/pipeline_editor_drawer.vue b/app/assets/javascripts/pipeline_editor/components/drawer/pipeline_editor_drawer.vue
index d7594fb318a..7bc096ce2c8 100644
--- a/app/assets/javascripts/pipeline_editor/components/drawer/pipeline_editor_drawer.vue
+++ b/app/assets/javascripts/pipeline_editor/components/drawer/pipeline_editor_drawer.vue
@@ -90,7 +90,7 @@ export default {
<local-storage-sync v-model="isExpanded" :storage-key="$options.localDrawerKey" as-json>
<aside
aria-live="polite"
- class="gl-fixed gl-right-0 gl-bg-gray-10 gl-shadow-drawer gl-transition-property-width gl-transition-duration-medium gl-border-l-solid gl-border-1 gl-border-gray-100 gl-h-full gl-z-index-3 gl-overflow-y-auto"
+ class="gl-fixed gl-right-0 gl-bg-gray-10 gl-shadow-drawer gl-transition-property-width gl-transition-duration-medium gl-border-l-solid gl-border-1 gl-border-gray-100 gl-h-full gl-z-index-200 gl-overflow-y-auto"
:style="rootStyle"
>
<gl-button
@@ -98,6 +98,7 @@ export default {
class="gl-w-full gl-h-9 gl-rounded-0! gl-border-none! gl-border-b-solid! gl-border-1! gl-border-gray-100 gl-text-decoration-none! gl-outline-0! gl-display-flex"
:class="buttonClass"
:title="__('Toggle sidebar')"
+ data-qa-selector="toggle_sidebar_collapse_button"
@click="toggleDrawer"
>
<span v-if="isExpanded" class="gl-text-gray-500 gl-mr-3" data-testid="collapse-text">
@@ -105,7 +106,12 @@ export default {
</span>
<gl-icon data-testid="toggle-icon" :name="buttonIconName" />
</gl-button>
- <div v-if="isExpanded" class="gl-h-full gl-p-5" data-testid="drawer-content">
+ <div
+ v-if="isExpanded"
+ class="gl-h-full gl-p-5"
+ data-testid="drawer-content"
+ data-qa-selector="drawer_content"
+ >
<getting-started-card class="gl-mb-4" />
<first-pipeline-card class="gl-mb-4" />
<visualize-and-lint-card class="gl-mb-4" />
diff --git a/app/assets/javascripts/pipeline_editor/components/editor/text_editor.vue b/app/assets/javascripts/pipeline_editor/components/editor/text_editor.vue
index 7b8e97b573e..92fa411d5af 100644
--- a/app/assets/javascripts/pipeline_editor/components/editor/text_editor.vue
+++ b/app/assets/javascripts/pipeline_editor/components/editor/text_editor.vue
@@ -19,7 +19,7 @@ export default {
if (this.glFeatures.schemaLinting) {
const editorInstance = this.$refs.editor.getEditor();
- editorInstance.use(new CiSchemaExtension({ instance: editorInstance }));
+ editorInstance.use({ definition: CiSchemaExtension });
editorInstance.registerCiSchema();
}
},
diff --git a/app/assets/javascripts/pipeline_editor/components/file_nav/branch_switcher.vue b/app/assets/javascripts/pipeline_editor/components/file_nav/branch_switcher.vue
index baf1d17b233..4f79a81d539 100644
--- a/app/assets/javascripts/pipeline_editor/components/file_nav/branch_switcher.vue
+++ b/app/assets/javascripts/pipeline_editor/components/file_nav/branch_switcher.vue
@@ -18,10 +18,10 @@ import {
BRANCH_SEARCH_DEBOUNCE,
DEFAULT_FAILURE,
} from '~/pipeline_editor/constants';
-import updateCurrentBranchMutation from '~/pipeline_editor/graphql/mutations/update_current_branch.mutation.graphql';
-import getAvailableBranchesQuery from '~/pipeline_editor/graphql/queries/available_branches.graphql';
-import getCurrentBranchQuery from '~/pipeline_editor/graphql/queries/client/current_branch.graphql';
-import getLastCommitBranchQuery from '~/pipeline_editor/graphql/queries/client/last_commit_branch.query.graphql';
+import updateCurrentBranchMutation from '~/pipeline_editor/graphql/mutations/client/update_current_branch.mutation.graphql';
+import getAvailableBranchesQuery from '~/pipeline_editor/graphql/queries/available_branches.query.graphql';
+import getCurrentBranch from '~/pipeline_editor/graphql/queries/client/current_branch.query.graphql';
+import getLastCommitBranch from '~/pipeline_editor/graphql/queries/client/last_commit_branch.query.graphql';
export default {
i18n: {
@@ -61,8 +61,8 @@ export default {
},
data() {
return {
- branchSelected: null,
availableBranches: [],
+ branchSelected: null,
filteredBranches: [],
isSearchingBranches: false,
pageLimit: this.paginationLimit,
@@ -93,15 +93,25 @@ export default {
},
},
currentBranch: {
- query: getCurrentBranchQuery,
+ query: getCurrentBranch,
+ update(data) {
+ return data.workBranches.current.name;
+ },
},
lastCommitBranch: {
- query: getLastCommitBranchQuery,
- result({ data: { lastCommitBranch } }) {
- if (lastCommitBranch === '' || this.availableBranches.includes(lastCommitBranch)) {
- return;
+ query: getLastCommitBranch,
+ update(data) {
+ return data.workBranches.lastCommit.name;
+ },
+ result({ data }) {
+ if (data) {
+ const { name: lastCommitBranch } = data.workBranches.lastCommit;
+ if (lastCommitBranch === '' || this.availableBranches.includes(lastCommitBranch)) {
+ return;
+ }
+
+ this.availableBranches.unshift(lastCommitBranch);
}
- this.availableBranches.unshift(lastCommitBranch);
},
},
},
@@ -109,12 +119,12 @@ export default {
branches() {
return this.searchTerm.length > 0 ? this.filteredBranches : this.availableBranches;
},
- isBranchesLoading() {
- return this.$apollo.queries.availableBranches.loading || this.isSearchingBranches;
- },
enableBranchSwitcher() {
return this.branches.length > 0 || this.searchTerm.length > 0;
},
+ isBranchesLoading() {
+ return this.$apollo.queries.availableBranches.loading || this.isSearchingBranches;
+ },
},
watch: {
shouldLoadNewBranch(flag) {
@@ -247,6 +257,7 @@ export default {
<gl-infinite-scroll
:fetched-items="branches.length"
:max-list-height="250"
+ data-qa-selector="branch_menu_container"
@bottomReached="fetchNextBranches"
>
<template #items>
@@ -255,7 +266,7 @@ export default {
:key="branch"
:is-checked="currentBranch === branch"
:is-check-item="true"
- data-qa-selector="menu_branch_button"
+ data-qa-selector="branch_menu_item_button"
@click="selectBranch(branch)"
>
{{ branch }}
diff --git a/app/assets/javascripts/pipeline_editor/components/header/pipeline_status.vue b/app/assets/javascripts/pipeline_editor/components/header/pipeline_status.vue
index 6fe1459c80c..16ad648afca 100644
--- a/app/assets/javascripts/pipeline_editor/components/header/pipeline_status.vue
+++ b/app/assets/javascripts/pipeline_editor/components/header/pipeline_status.vue
@@ -3,8 +3,8 @@ import { GlButton, GlIcon, GlLink, GlLoadingIcon, GlSprintf, GlTooltipDirective
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { truncateSha } from '~/lib/utils/text_utility';
import { s__ } from '~/locale';
-import getPipelineQuery from '~/pipeline_editor/graphql/queries/client/pipeline.graphql';
-import getPipelineEtag from '~/pipeline_editor/graphql/queries/client/pipeline_etag.graphql';
+import getPipelineQuery from '~/pipeline_editor/graphql/queries/pipeline.query.graphql';
+import getPipelineEtag from '~/pipeline_editor/graphql/queries/client/pipeline_etag.query.graphql';
import {
getQueryHeaders,
toggleQueryPollingByVisibility,
@@ -21,9 +21,6 @@ export const i18n = {
),
viewBtn: s__('Pipeline|View pipeline'),
viewCommit: s__('Pipeline|View commit'),
- pipelineNotTriggeredMsg: s__(
- 'Pipeline|No pipeline was triggered for the latest changes due to the current CI/CD configuration.',
- ),
};
export default {
@@ -51,6 +48,9 @@ export default {
apollo: {
pipelineEtag: {
query: getPipelineEtag,
+ update(data) {
+ return data.etags.pipeline;
+ },
},
pipeline: {
context() {
@@ -79,22 +79,16 @@ export default {
result(res) {
if (res.data?.project?.pipeline) {
this.hasError = false;
- } else {
- this.hasError = true;
- this.pipelineNotTriggered = true;
}
},
error() {
this.hasError = true;
- this.networkError = true;
},
pollInterval: POLL_INTERVAL,
},
},
data() {
return {
- networkError: false,
- pipelineNotTriggered: false,
hasError: false,
};
},
@@ -148,16 +142,8 @@ export default {
</div>
</template>
<template v-else-if="hasError">
- <div v-if="networkError">
- <gl-icon class="gl-mr-auto" name="warning-solid" />
- <span data-testid="pipeline-error-msg">{{ $options.i18n.fetchError }}</span>
- </div>
- <div v-else>
- <gl-icon class="gl-mr-auto" name="information-o" />
- <span data-testid="pipeline-not-triggered-error-msg">
- {{ $options.i18n.pipelineNotTriggeredMsg }}
- </span>
- </div>
+ <gl-icon class="gl-mr-auto" name="warning-solid" />
+ <span data-testid="pipeline-error-msg">{{ $options.i18n.fetchError }}</span>
</template>
<template v-else>
<div class="gl-text-truncate gl-md-max-w-50p gl-mr-1">
diff --git a/app/assets/javascripts/pipeline_editor/components/header/validation_segment.vue b/app/assets/javascripts/pipeline_editor/components/header/validation_segment.vue
index 611b78b3c5e..833d784f940 100644
--- a/app/assets/javascripts/pipeline_editor/components/header/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 getAppStatus from '~/pipeline_editor/graphql/queries/client/app_status.graphql';
-import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate.vue';
+import getAppStatus from '~/pipeline_editor/graphql/queries/client/app_status.query.graphql';
+import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate/tooltip_on_truncate.vue';
import {
EDITOR_APP_STATUS_EMPTY,
EDITOR_APP_STATUS_LOADING,
@@ -43,6 +43,9 @@ export default {
apollo: {
appStatus: {
query: getAppStatus,
+ update(data) {
+ return data.app.status;
+ },
},
},
computed: {
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 7f6dce05b6e..13e254f138a 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,5 +1,5 @@
<script>
-import { GlAlert, GlLink, GlSprintf, GlTable } from '@gitlab/ui';
+import { GlAlert, GlLink, GlSprintf, GlTableLite } from '@gitlab/ui';
import { __ } from '~/locale';
import CiLintResultsParam from './ci_lint_results_param.vue';
import CiLintResultsValue from './ci_lint_results_value.vue';
@@ -36,7 +36,7 @@ export default {
GlAlert,
GlLink,
GlSprintf,
- GlTable,
+ GlTableLite,
CiLintWarnings,
CiLintResultsValue,
CiLintResultsParam,
@@ -129,7 +129,7 @@ export default {
@dismiss="isWarningDismissed = true"
/>
- <gl-table
+ <gl-table-lite
v-if="shouldShowTable"
:items="jobs"
:fields="$options.fields"
@@ -142,6 +142,6 @@ export default {
<template #cell(value)="{ item }">
<ci-lint-results-value :item="item" :dry-run="dryRun" />
</template>
- </gl-table>
+ </gl-table-lite>
</div>
</template>
diff --git a/app/assets/javascripts/pipeline_editor/components/pipeline_editor_tabs.vue b/app/assets/javascripts/pipeline_editor/components/pipeline_editor_tabs.vue
index 0cd0d17d944..3f50a1225d8 100644
--- a/app/assets/javascripts/pipeline_editor/components/pipeline_editor_tabs.vue
+++ b/app/assets/javascripts/pipeline_editor/components/pipeline_editor_tabs.vue
@@ -17,7 +17,7 @@ import {
TABS_INDEX,
VISUALIZE_TAB,
} from '../constants';
-import getAppStatus from '../graphql/queries/client/app_status.graphql';
+import getAppStatus from '../graphql/queries/client/app_status.query.graphql';
import CiConfigMergedPreview from './editor/ci_config_merged_preview.vue';
import CiEditorHeader from './editor/ci_editor_header.vue';
import TextEditor from './editor/text_editor.vue';
@@ -91,6 +91,9 @@ export default {
apollo: {
appStatus: {
query: getAppStatus,
+ update(data) {
+ return data.app.status;
+ },
},
},
computed: {
diff --git a/app/assets/javascripts/pipeline_editor/graphql/mutations/lint_ci.mutation.graphql b/app/assets/javascripts/pipeline_editor/graphql/mutations/client/lint_ci.mutation.graphql
index 5091d63111f..5091d63111f 100644
--- a/app/assets/javascripts/pipeline_editor/graphql/mutations/lint_ci.mutation.graphql
+++ b/app/assets/javascripts/pipeline_editor/graphql/mutations/client/lint_ci.mutation.graphql
diff --git a/app/assets/javascripts/pipeline_editor/graphql/mutations/update_app_status.mutation.graphql b/app/assets/javascripts/pipeline_editor/graphql/mutations/client/update_app_status.mutation.graphql
index 7487e328668..7487e328668 100644
--- a/app/assets/javascripts/pipeline_editor/graphql/mutations/update_app_status.mutation.graphql
+++ b/app/assets/javascripts/pipeline_editor/graphql/mutations/client/update_app_status.mutation.graphql
diff --git a/app/assets/javascripts/pipeline_editor/graphql/mutations/update_current_branch.mutation.graphql b/app/assets/javascripts/pipeline_editor/graphql/mutations/client/update_current_branch.mutation.graphql
index b722c147f5f..b722c147f5f 100644
--- a/app/assets/javascripts/pipeline_editor/graphql/mutations/update_current_branch.mutation.graphql
+++ b/app/assets/javascripts/pipeline_editor/graphql/mutations/client/update_current_branch.mutation.graphql
diff --git a/app/assets/javascripts/pipeline_editor/graphql/mutations/update_last_commit_branch.mutation.graphql b/app/assets/javascripts/pipeline_editor/graphql/mutations/client/update_last_commit_branch.mutation.graphql
index 9561312f2b6..9561312f2b6 100644
--- a/app/assets/javascripts/pipeline_editor/graphql/mutations/update_last_commit_branch.mutation.graphql
+++ b/app/assets/javascripts/pipeline_editor/graphql/mutations/client/update_last_commit_branch.mutation.graphql
diff --git a/app/assets/javascripts/pipeline_editor/graphql/mutations/update_pipeline_etag.mutation.graphql b/app/assets/javascripts/pipeline_editor/graphql/mutations/client/update_pipeline_etag.mutation.graphql
index 9025f00b343..9025f00b343 100644
--- a/app/assets/javascripts/pipeline_editor/graphql/mutations/update_pipeline_etag.mutation.graphql
+++ b/app/assets/javascripts/pipeline_editor/graphql/mutations/client/update_pipeline_etag.mutation.graphql
diff --git a/app/assets/javascripts/pipeline_editor/graphql/mutations/commit_ci_file.mutation.graphql b/app/assets/javascripts/pipeline_editor/graphql/mutations/commit_ci_file.mutation.graphql
index 94e6facabfd..77a3cdf586c 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
@@ -19,7 +19,10 @@ mutation commitCIFile(
]
}
) {
+ __typename
commit {
+ __typename
+ id
sha
}
commitPipelinePath
diff --git a/app/assets/javascripts/pipeline_editor/graphql/queries/available_branches.graphql b/app/assets/javascripts/pipeline_editor/graphql/queries/available_branches.query.graphql
index 46e9b108b41..359b4a846c7 100644
--- a/app/assets/javascripts/pipeline_editor/graphql/queries/available_branches.graphql
+++ b/app/assets/javascripts/pipeline_editor/graphql/queries/available_branches.query.graphql
@@ -5,6 +5,7 @@ query getAvailableBranches(
$searchPattern: String!
) {
project(fullPath: $projectFullPath) {
+ id
repository {
branchNames(limit: $limit, offset: $offset, searchPattern: $searchPattern)
}
diff --git a/app/assets/javascripts/pipeline_editor/graphql/queries/blob_content.graphql b/app/assets/javascripts/pipeline_editor/graphql/queries/blob_content.query.graphql
index 5500244b430..5928d90f7c4 100644
--- a/app/assets/javascripts/pipeline_editor/graphql/queries/blob_content.graphql
+++ b/app/assets/javascripts/pipeline_editor/graphql/queries/blob_content.query.graphql
@@ -1,8 +1,10 @@
query getBlobContent($projectPath: ID!, $path: String!, $ref: String) {
project(fullPath: $projectPath) {
+ id
repository {
blobs(paths: [$path], ref: $ref) {
nodes {
+ id
rawBlob
}
}
diff --git a/app/assets/javascripts/pipeline_editor/graphql/queries/ci_config.graphql b/app/assets/javascripts/pipeline_editor/graphql/queries/ci_config.query.graphql
index df7de6a1f54..df7de6a1f54 100644
--- a/app/assets/javascripts/pipeline_editor/graphql/queries/ci_config.graphql
+++ b/app/assets/javascripts/pipeline_editor/graphql/queries/ci_config.query.graphql
diff --git a/app/assets/javascripts/pipeline_editor/graphql/queries/client/app_status.graphql b/app/assets/javascripts/pipeline_editor/graphql/queries/client/app_status.graphql
deleted file mode 100644
index 938f36c7d5c..00000000000
--- a/app/assets/javascripts/pipeline_editor/graphql/queries/client/app_status.graphql
+++ /dev/null
@@ -1,3 +0,0 @@
-query getAppStatus {
- appStatus @client
-}
diff --git a/app/assets/javascripts/pipeline_editor/graphql/queries/client/app_status.query.graphql b/app/assets/javascripts/pipeline_editor/graphql/queries/client/app_status.query.graphql
new file mode 100644
index 00000000000..0df8cafa3cb
--- /dev/null
+++ b/app/assets/javascripts/pipeline_editor/graphql/queries/client/app_status.query.graphql
@@ -0,0 +1,5 @@
+query getAppStatus {
+ app @client {
+ status
+ }
+}
diff --git a/app/assets/javascripts/pipeline_editor/graphql/queries/client/current_branch.graphql b/app/assets/javascripts/pipeline_editor/graphql/queries/client/current_branch.graphql
deleted file mode 100644
index acd46013f5b..00000000000
--- a/app/assets/javascripts/pipeline_editor/graphql/queries/client/current_branch.graphql
+++ /dev/null
@@ -1,3 +0,0 @@
-query getCurrentBranch {
- currentBranch @client
-}
diff --git a/app/assets/javascripts/pipeline_editor/graphql/queries/client/current_branch.query.graphql b/app/assets/javascripts/pipeline_editor/graphql/queries/client/current_branch.query.graphql
new file mode 100644
index 00000000000..1f4f9d26f24
--- /dev/null
+++ b/app/assets/javascripts/pipeline_editor/graphql/queries/client/current_branch.query.graphql
@@ -0,0 +1,7 @@
+query getCurrentBranch {
+ workBranches @client {
+ current {
+ name
+ }
+ }
+}
diff --git a/app/assets/javascripts/pipeline_editor/graphql/queries/client/last_commit_branch.query.graphql b/app/assets/javascripts/pipeline_editor/graphql/queries/client/last_commit_branch.query.graphql
index e8a32d728d5..a83129759de 100644
--- a/app/assets/javascripts/pipeline_editor/graphql/queries/client/last_commit_branch.query.graphql
+++ b/app/assets/javascripts/pipeline_editor/graphql/queries/client/last_commit_branch.query.graphql
@@ -1,3 +1,7 @@
query getLastCommitBranchQuery {
- lastCommitBranch @client
+ workBranches @client {
+ lastCommit {
+ name
+ }
+ }
}
diff --git a/app/assets/javascripts/pipeline_editor/graphql/queries/client/pipeline_etag.graphql b/app/assets/javascripts/pipeline_editor/graphql/queries/client/pipeline_etag.graphql
deleted file mode 100644
index b9946a9e233..00000000000
--- a/app/assets/javascripts/pipeline_editor/graphql/queries/client/pipeline_etag.graphql
+++ /dev/null
@@ -1,3 +0,0 @@
-query getPipelineEtag {
- pipelineEtag @client
-}
diff --git a/app/assets/javascripts/pipeline_editor/graphql/queries/client/pipeline_etag.query.graphql b/app/assets/javascripts/pipeline_editor/graphql/queries/client/pipeline_etag.query.graphql
new file mode 100644
index 00000000000..8df6e74a5d9
--- /dev/null
+++ b/app/assets/javascripts/pipeline_editor/graphql/queries/client/pipeline_etag.query.graphql
@@ -0,0 +1,5 @@
+query getPipelineEtag {
+ etags @client {
+ pipeline
+ }
+}
diff --git a/app/assets/javascripts/pipeline_editor/graphql/queries/get_starter_template.query.graphql b/app/assets/javascripts/pipeline_editor/graphql/queries/get_starter_template.query.graphql
index 88825718f7b..a34c8f365f4 100644
--- a/app/assets/javascripts/pipeline_editor/graphql/queries/get_starter_template.query.graphql
+++ b/app/assets/javascripts/pipeline_editor/graphql/queries/get_starter_template.query.graphql
@@ -1,5 +1,6 @@
query getTemplate($projectPath: ID!, $templateName: String!) {
project(fullPath: $projectPath) {
+ id
ciTemplate(name: $templateName) {
content
}
diff --git a/app/assets/javascripts/pipeline_editor/graphql/queries/latest_commit_sha.query.graphql b/app/assets/javascripts/pipeline_editor/graphql/queries/latest_commit_sha.query.graphql
index 02d49507947..d62fda40237 100644
--- a/app/assets/javascripts/pipeline_editor/graphql/queries/latest_commit_sha.query.graphql
+++ b/app/assets/javascripts/pipeline_editor/graphql/queries/latest_commit_sha.query.graphql
@@ -1,8 +1,10 @@
query getLatestCommitSha($projectPath: ID!, $ref: String) {
project(fullPath: $projectPath) {
+ id
repository {
tree(ref: $ref) {
lastCommit {
+ id
sha
}
}
diff --git a/app/assets/javascripts/pipeline_editor/graphql/queries/client/pipeline.graphql b/app/assets/javascripts/pipeline_editor/graphql/queries/pipeline.query.graphql
index 34e98ae3eb3..021b858d72e 100644
--- a/app/assets/javascripts/pipeline_editor/graphql/queries/client/pipeline.graphql
+++ b/app/assets/javascripts/pipeline_editor/graphql/queries/pipeline.query.graphql
@@ -1,14 +1,17 @@
query getPipeline($fullPath: ID!, $sha: String!) {
project(fullPath: $fullPath) {
+ id
pipeline(sha: $sha) {
id
iid
status
commit {
+ id
title
webPath
}
detailedStatus {
+ id
detailsPath
icon
group
diff --git a/app/assets/javascripts/pipeline_editor/graphql/resolvers.js b/app/assets/javascripts/pipeline_editor/graphql/resolvers.js
index e4965e00af3..fa1c70c1994 100644
--- a/app/assets/javascripts/pipeline_editor/graphql/resolvers.js
+++ b/app/assets/javascripts/pipeline_editor/graphql/resolvers.js
@@ -1,8 +1,8 @@
import axios from '~/lib/utils/axios_utils';
-import getAppStatus from './queries/client/app_status.graphql';
-import getCurrentBranchQuery from './queries/client/current_branch.graphql';
-import getLastCommitBranchQuery from './queries/client/last_commit_branch.query.graphql';
-import getPipelineEtag from './queries/client/pipeline_etag.graphql';
+import getAppStatus from './queries/client/app_status.query.graphql';
+import getCurrentBranch from './queries/client/current_branch.query.graphql';
+import getLastCommitBranch from './queries/client/last_commit_branch.query.graphql';
+import getPipelineEtag from './queries/client/pipeline_etag.query.graphql';
export const resolvers = {
Mutation: {
@@ -35,25 +35,51 @@ export const resolvers = {
updateAppStatus: (_, { appStatus }, { cache }) => {
cache.writeQuery({
query: getAppStatus,
- data: { appStatus },
+ data: {
+ app: {
+ __typename: 'PipelineEditorApp',
+ status: appStatus,
+ },
+ },
});
},
updateCurrentBranch: (_, { currentBranch }, { cache }) => {
cache.writeQuery({
- query: getCurrentBranchQuery,
- data: { currentBranch },
+ query: getCurrentBranch,
+ data: {
+ workBranches: {
+ __typename: 'BranchList',
+ current: {
+ __typename: 'WorkBranch',
+ name: currentBranch,
+ },
+ },
+ },
});
},
updateLastCommitBranch: (_, { lastCommitBranch }, { cache }) => {
cache.writeQuery({
- query: getLastCommitBranchQuery,
- data: { lastCommitBranch },
+ query: getLastCommitBranch,
+ data: {
+ workBranches: {
+ __typename: 'BranchList',
+ lastCommit: {
+ __typename: 'WorkBranch',
+ name: lastCommitBranch,
+ },
+ },
+ },
});
},
updatePipelineEtag: (_, { pipelineEtag }, { cache }) => {
cache.writeQuery({
query: getPipelineEtag,
- data: { pipelineEtag },
+ data: {
+ etags: {
+ __typename: 'EtagValues',
+ pipeline: pipelineEtag,
+ },
+ },
});
},
},
diff --git a/app/assets/javascripts/pipeline_editor/graphql/typedefs.graphql b/app/assets/javascripts/pipeline_editor/graphql/typedefs.graphql
index f4f65262158..508ff22c46e 100644
--- a/app/assets/javascripts/pipeline_editor/graphql/typedefs.graphql
+++ b/app/assets/javascripts/pipeline_editor/graphql/typedefs.graphql
@@ -1,7 +1,23 @@
-type BlobContent {
- rawData: String!
+type PipelineEditorApp {
+ status: String!
+}
+
+type BranchList {
+ current: WorkBranch!
+ lastCommit: WorkBranch!
+}
+
+type EtagValues {
+ pipeline: String!
+}
+
+type WorkBranch {
+ name: String!
+ commit: String
}
extend type Query {
- blobContent: BlobContent
+ app: PipelineEditorApp
+ etags: EtagValues
+ workBranches: BranchList
}
diff --git a/app/assets/javascripts/pipeline_editor/index.js b/app/assets/javascripts/pipeline_editor/index.js
index 4f7f2743aca..ee93e327b76 100644
--- a/app/assets/javascripts/pipeline_editor/index.js
+++ b/app/assets/javascripts/pipeline_editor/index.js
@@ -5,10 +5,10 @@ import createDefaultClient from '~/lib/graphql';
import { resetServiceWorkersPublicPath } from '../lib/utils/webpack';
import { EDITOR_APP_STATUS_LOADING } from './constants';
import { CODE_SNIPPET_SOURCE_SETTINGS } from './components/code_snippet_alert/constants';
-import getCurrentBranch from './graphql/queries/client/current_branch.graphql';
-import getAppStatus from './graphql/queries/client/app_status.graphql';
-import getLastCommitBranchQuery from './graphql/queries/client/last_commit_branch.query.graphql';
-import getPipelineEtag from './graphql/queries/client/pipeline_etag.graphql';
+import getCurrentBranch from './graphql/queries/client/current_branch.query.graphql';
+import getAppStatus from './graphql/queries/client/app_status.query.graphql';
+import getLastCommitBranch from './graphql/queries/client/last_commit_branch.query.graphql';
+import getPipelineEtag from './graphql/queries/client/pipeline_etag.query.graphql';
import { resolvers } from './graphql/resolvers';
import typeDefs from './graphql/typedefs.graphql';
import PipelineEditorApp from './pipeline_editor_app.vue';
@@ -68,28 +68,46 @@ export const initPipelineEditor = (selector = '#js-pipeline-editor') => {
cache.writeQuery({
query: getAppStatus,
data: {
- appStatus: EDITOR_APP_STATUS_LOADING,
+ app: {
+ __typename: 'PipelineEditorApp',
+ status: EDITOR_APP_STATUS_LOADING,
+ },
},
});
cache.writeQuery({
query: getCurrentBranch,
data: {
- currentBranch: initialBranchName || defaultBranch,
+ workBranches: {
+ __typename: 'BranchList',
+ current: {
+ __typename: 'WorkBranch',
+ name: initialBranchName || defaultBranch,
+ },
+ },
},
});
cache.writeQuery({
- query: getPipelineEtag,
+ query: getLastCommitBranch,
data: {
- pipelineEtag,
+ workBranches: {
+ __typename: 'BranchList',
+ lastCommit: {
+ __typename: 'WorkBranch',
+ name: '',
+ },
+ },
},
});
cache.writeQuery({
- query: getLastCommitBranchQuery,
+ query: getPipelineEtag,
data: {
- lastCommitBranch: '',
+ etags: {
+ __typename: 'EtagValues',
+ pipeline: pipelineEtag,
+ },
},
});
diff --git a/app/assets/javascripts/pipeline_editor/pipeline_editor_app.vue b/app/assets/javascripts/pipeline_editor/pipeline_editor_app.vue
index 68db5d8078f..e397054f06a 100644
--- a/app/assets/javascripts/pipeline_editor/pipeline_editor_app.vue
+++ b/app/assets/javascripts/pipeline_editor/pipeline_editor_app.vue
@@ -1,8 +1,8 @@
<script>
-import { GlLoadingIcon } from '@gitlab/ui';
+import { GlLoadingIcon, GlModal } from '@gitlab/ui';
import { fetchPolicies } from '~/lib/graphql';
import { queryToObject } from '~/lib/utils/url_utility';
-import { s__ } from '~/locale';
+import { __, s__ } from '~/locale';
import { unwrapStagesWithNeeds } from '~/pipelines/components/unwrapping_utils';
@@ -17,11 +17,11 @@ import {
LOAD_FAILURE_UNKNOWN,
STARTER_TEMPLATE_NAME,
} from './constants';
-import updateAppStatus from './graphql/mutations/update_app_status.mutation.graphql';
-import getBlobContent from './graphql/queries/blob_content.graphql';
-import getCiConfigData from './graphql/queries/ci_config.graphql';
-import getAppStatus from './graphql/queries/client/app_status.graphql';
-import getCurrentBranch from './graphql/queries/client/current_branch.graphql';
+import updateAppStatus from './graphql/mutations/client/update_app_status.mutation.graphql';
+import getBlobContent from './graphql/queries/blob_content.query.graphql';
+import getCiConfigData from './graphql/queries/ci_config.query.graphql';
+import getAppStatus from './graphql/queries/client/app_status.query.graphql';
+import getCurrentBranch from './graphql/queries/client/current_branch.query.graphql';
import getTemplate from './graphql/queries/get_starter_template.query.graphql';
import getLatestCommitShaQuery from './graphql/queries/latest_commit_sha.query.graphql';
import PipelineEditorHome from './pipeline_editor_home.vue';
@@ -30,6 +30,7 @@ export default {
components: {
ConfirmUnsavedChangesDialog,
GlLoadingIcon,
+ GlModal,
PipelineEditorEmptyState,
PipelineEditorHome,
PipelineEditorMessages,
@@ -54,6 +55,7 @@ export default {
lastCommittedContent: '',
shouldSkipStartScreen: false,
showFailure: false,
+ showResetComfirmationModal: false,
showStartScreen: false,
showSuccess: false,
starterTemplate: '',
@@ -158,6 +160,9 @@ export default {
},
appStatus: {
query: getAppStatus,
+ update(data) {
+ return data.app.status;
+ },
},
commitSha: {
query: getLatestCommitShaQuery,
@@ -182,6 +187,9 @@ export default {
},
currentBranch: {
query: getCurrentBranch,
+ update(data) {
+ return data.workBranches.current.name;
+ },
},
starterTemplate: {
query: getTemplate,
@@ -220,9 +228,18 @@ export default {
},
},
i18n: {
- tabEdit: s__('Pipelines|Edit'),
- tabGraph: s__('Pipelines|Visualize'),
- tabLint: s__('Pipelines|Lint'),
+ resetModal: {
+ actionPrimary: {
+ text: __('Reset file'),
+ },
+ actionCancel: {
+ text: __('Cancel'),
+ },
+ body: s__(
+ 'Pipeline Editor|Are you sure you want to reset the file to its last committed version?',
+ ),
+ title: __('Discard changes'),
+ },
},
watch: {
isEmpty(flag) {
@@ -242,15 +259,24 @@ export default {
hideSuccess() {
this.showSuccess = false;
},
+ confirmReset() {
+ if (this.hasUnsavedChanges) {
+ this.showResetComfirmationModal = true;
+ }
+ },
async refetchContent() {
this.$apollo.queries.initialCiFileContent.skip = false;
await this.$apollo.queries.initialCiFileContent.refetch();
},
reportFailure(type, reasons = []) {
- window.scrollTo({ top: 0, behavior: 'smooth' });
- this.showFailure = true;
- this.failureType = type;
- this.failureReasons = reasons;
+ const isCurrentFailure = this.failureType === type && this.failureReasons[0] === reasons[0];
+
+ if (!isCurrentFailure) {
+ this.showFailure = true;
+ this.failureType = type;
+ this.failureReasons = reasons;
+ window.scrollTo({ top: 0, behavior: 'smooth' });
+ }
},
reportSuccess(type) {
window.scrollTo({ top: 0, behavior: 'smooth' });
@@ -258,6 +284,7 @@ export default {
this.successType = type;
},
resetContent() {
+ this.showResetComfirmationModal = false;
this.currentCiFileContent = this.lastCommittedContent;
},
setAppStatus(appStatus) {
@@ -331,12 +358,22 @@ export default {
:has-unsaved-changes="hasUnsavedChanges"
:is-new-ci-config-file="isNewCiConfigFile"
@commit="updateOnCommit"
- @resetContent="resetContent"
+ @resetContent="confirmReset"
@showError="showErrorAlert"
@refetchContent="refetchContent"
@updateCiConfig="updateCiConfig"
@updateCommitSha="updateCommitSha"
/>
+ <gl-modal
+ v-model="showResetComfirmationModal"
+ modal-id="reset-content"
+ :title="$options.i18n.resetModal.title"
+ :action-cancel="$options.i18n.resetModal.actionCancel"
+ :action-primary="$options.i18n.resetModal.actionPrimary"
+ @primary="resetContent"
+ >
+ {{ $options.i18n.resetModal.body }}
+ </gl-modal>
<confirm-unsaved-changes-dialog :has-unsaved-changes="hasUnsavedChanges" />
</div>
</div>
diff --git a/app/assets/javascripts/pipelines/components/graph/graph_view_selector.vue b/app/assets/javascripts/pipelines/components/graph/graph_view_selector.vue
index 3c78b655dc7..1920fed84ec 100644
--- a/app/assets/javascripts/pipelines/components/graph/graph_view_selector.vue
+++ b/app/assets/javascripts/pipelines/components/graph/graph_view_selector.vue
@@ -1,5 +1,5 @@
<script>
-import { GlAlert, GlLoadingIcon, GlSegmentedControl, GlToggle } from '@gitlab/ui';
+import { GlAlert, GlButton, GlButtonGroup, GlLoadingIcon, GlToggle } from '@gitlab/ui';
import { __, s__ } from '~/locale';
import { STAGE_VIEW, LAYER_VIEW } from './constants';
@@ -7,8 +7,9 @@ export default {
name: 'GraphViewSelector',
components: {
GlAlert,
+ GlButton,
+ GlButtonGroup,
GlLoadingIcon,
- GlSegmentedControl,
GlToggle,
},
props: {
@@ -96,6 +97,9 @@ export default {
this.hoverTipDismissed = true;
this.$emit('dismissHoverTip');
},
+ isCurrentType(type) {
+ return this.segmentSelectedType === type;
+ },
/*
In both toggle methods, we use setTimeout so that the loading indicator displays,
then the work is done to update the DOM. The process is:
@@ -110,11 +114,14 @@ export default {
See https://www.hesselinkwebdesign.nl/2019/nexttick-vs-settimeout-in-vue/ for more details.
*/
- toggleView(type) {
- this.isSwitcherLoading = true;
- setTimeout(() => {
- this.$emit('updateViewType', type);
- });
+ setViewType(type) {
+ if (!this.isCurrentType(type)) {
+ this.isSwitcherLoading = true;
+ this.segmentSelectedType = type;
+ setTimeout(() => {
+ this.$emit('updateViewType', type);
+ });
+ }
},
toggleShowLinksActive(val) {
this.isToggleLoading = true;
@@ -136,14 +143,16 @@ export default {
size="lg"
/>
<span class="gl-font-weight-bold">{{ $options.i18n.viewLabelText }}</span>
- <gl-segmented-control
- v-model="segmentSelectedType"
- :options="viewTypesList"
- :disabled="isSwitcherLoading"
- data-testid="pipeline-view-selector"
- class="gl-mx-4"
- @input="toggleView"
- />
+ <gl-button-group class="gl-mx-4">
+ <gl-button
+ v-for="viewType in viewTypesList"
+ :key="viewType.value"
+ :selected="isCurrentType(viewType.value)"
+ @click="setViewType(viewType.value)"
+ >
+ {{ viewType.text }}
+ </gl-button>
+ </gl-button-group>
<div v-if="showLinksToggle" class="gl-display-flex gl-align-items-center">
<gl-toggle
diff --git a/app/assets/javascripts/pipelines/components/graph/job_group_dropdown.vue b/app/assets/javascripts/pipelines/components/graph/job_group_dropdown.vue
index 6f4360649ff..12c3f9a7f40 100644
--- a/app/assets/javascripts/pipelines/components/graph/job_group_dropdown.vue
+++ b/app/assets/javascripts/pipelines/components/graph/job_group_dropdown.vue
@@ -67,7 +67,7 @@ export default {
:class="cssClassJobName"
class="dropdown-menu-toggle gl-pipeline-job-width! gl-pr-4!"
>
- <div class="gl-display-flex gl-align-items-center gl-justify-content-space-between">
+ <div class="gl-display-flex gl-align-items-stretch gl-justify-content-space-between">
<job-item
:type="$options.jobItemTypes.jobDropdown"
:group-tooltip="tooltipText"
diff --git a/app/assets/javascripts/pipelines/components/graph/job_item.vue b/app/assets/javascripts/pipelines/components/graph/job_item.vue
index 0216b2717ed..ee58dcc4882 100644
--- a/app/assets/javascripts/pipelines/components/graph/job_item.vue
+++ b/app/assets/javascripts/pipelines/components/graph/job_item.vue
@@ -203,7 +203,7 @@ export default {
<template>
<div
:id="computedJobId"
- class="ci-job-component gl-display-flex gl-align-items-center gl-justify-content-space-between gl-w-full"
+ class="ci-job-component gl-display-flex gl-justify-content-space-between gl-pipeline-job-width"
data-qa-selector="job_item_container"
>
<component
@@ -223,12 +223,12 @@ export default {
>
<div class="ci-job-name-component gl-display-flex gl-align-items-center">
<ci-icon :size="24" :status="job.status" class="gl-line-height-0" />
- <div class="gl-pl-3 gl-display-flex gl-flex-direction-column gl-w-full">
- <div class="gl-text-truncate gl-w-70p gl-line-height-normal">{{ job.name }}</div>
+ <div class="gl-pl-3 gl-pr-3 gl-display-flex gl-flex-direction-column gl-pipeline-job-width">
+ <div class="gl-text-truncate gl-pr-9 gl-line-height-normal">{{ job.name }}</div>
<div
v-if="showStageName"
data-testid="stage-name-in-job"
- class="gl-text-truncate gl-w-70p gl-font-sm gl-text-gray-500 gl-line-height-normal"
+ class="gl-text-truncate gl-pr-9 gl-font-sm gl-text-gray-500 gl-line-height-normal"
>
{{ stageName }}
</div>
diff --git a/app/assets/javascripts/pipelines/components/graph/linked_pipeline.vue b/app/assets/javascripts/pipelines/components/graph/linked_pipeline.vue
index be47799868b..e0c1dcc5be5 100644
--- a/app/assets/javascripts/pipelines/components/graph/linked_pipeline.vue
+++ b/app/assets/javascripts/pipelines/components/graph/linked_pipeline.vue
@@ -124,7 +124,7 @@ export default {
<div
ref="linkedPipeline"
v-gl-tooltip
- class="gl-pipeline-job-width"
+ class="gl-downstream-pipeline-job-width"
:title="tooltipText"
data-qa-selector="child_pipeline"
@mouseover="onDownstreamHovered"
@@ -134,7 +134,7 @@ export default {
class="gl-relative gl-bg-white gl-p-3 gl-border-solid gl-border-gray-100 gl-border-1"
:class="{ 'gl-pl-9': isUpstream }"
>
- <div class="gl-display-flex">
+ <div class="gl-display-flex gl-pr-7 gl-pipeline-job-width">
<ci-status
v-if="!pipelineIsLoading"
:status="pipelineStatus"
@@ -142,7 +142,9 @@ export default {
css-classes="gl-top-0 gl-pr-2"
/>
<div v-else class="gl-pr-2"><gl-loading-icon size="sm" inline /></div>
- <div class="gl-display-flex gl-flex-direction-column gl-w-13">
+ <div
+ class="gl-display-flex gl-flex-direction-column gl-pipeline-job-width gl-text-truncate"
+ >
<span class="gl-text-truncate" data-testid="downstream-title">
{{ downstreamTitle }}
</span>
diff --git a/app/assets/javascripts/pipelines/components/jobs/jobs_app.vue b/app/assets/javascripts/pipelines/components/jobs/jobs_app.vue
new file mode 100644
index 00000000000..ffac8206b58
--- /dev/null
+++ b/app/assets/javascripts/pipelines/components/jobs/jobs_app.vue
@@ -0,0 +1,121 @@
+<script>
+import { GlIntersectionObserver, GlLoadingIcon, GlSkeletonLoader } from '@gitlab/ui';
+import produce from 'immer';
+import createFlash from '~/flash';
+import { __ } from '~/locale';
+import eventHub from '~/jobs/components/table/event_hub';
+import JobsTable from '~/jobs/components/table/jobs_table.vue';
+import { JOBS_TAB_FIELDS } from '~/jobs/components/table/constants';
+import getPipelineJobs from '../../graphql/queries/get_pipeline_jobs.query.graphql';
+
+export default {
+ fields: JOBS_TAB_FIELDS,
+ components: {
+ GlIntersectionObserver,
+ GlLoadingIcon,
+ GlSkeletonLoader,
+ JobsTable,
+ },
+ inject: {
+ fullPath: {
+ default: '',
+ },
+ pipelineIid: {
+ default: '',
+ },
+ },
+ apollo: {
+ jobs: {
+ query: getPipelineJobs,
+ variables() {
+ return {
+ ...this.queryVariables,
+ };
+ },
+ update(data) {
+ return data.project?.pipeline?.jobs?.nodes || [];
+ },
+ result({ data }) {
+ this.jobsPageInfo = data.project?.pipeline?.jobs?.pageInfo || {};
+ },
+ error() {
+ createFlash({ message: __('An error occured while fetching the pipelines jobs.') });
+ },
+ },
+ },
+ data() {
+ return {
+ jobs: [],
+ jobsPageInfo: {},
+ firstLoad: true,
+ };
+ },
+ computed: {
+ queryVariables() {
+ return {
+ fullPath: this.fullPath,
+ iid: this.pipelineIid,
+ };
+ },
+ },
+ mounted() {
+ eventHub.$on('jobActionPerformed', this.handleJobAction);
+ },
+ beforeDestroy() {
+ eventHub.$off('jobActionPerformed', this.handleJobAction);
+ },
+ methods: {
+ handleJobAction() {
+ this.firstLoad = true;
+
+ this.$apollo.queries.jobs.refetch();
+ },
+ fetchMoreJobs() {
+ this.firstLoad = false;
+
+ this.$apollo.queries.jobs.fetchMore({
+ variables: {
+ ...this.queryVariables,
+ after: this.jobsPageInfo.endCursor,
+ },
+ updateQuery: (previousResult, { fetchMoreResult }) => {
+ const results = produce(fetchMoreResult, (draftData) => {
+ draftData.project.pipeline.jobs.nodes = [
+ ...previousResult.project.pipeline.jobs.nodes,
+ ...draftData.project.pipeline.jobs.nodes,
+ ];
+ });
+ return results;
+ },
+ });
+ },
+ },
+};
+</script>
+
+<template>
+ <div>
+ <div v-if="$apollo.loading && firstLoad" class="gl-mt-5">
+ <gl-skeleton-loader :width="1248" :height="73">
+ <circle cx="748.031" cy="37.7193" r="15.0307" />
+ <circle cx="787.241" cy="37.7193" r="15.0307" />
+ <circle cx="827.759" cy="37.7193" r="15.0307" />
+ <circle cx="866.969" cy="37.7193" r="15.0307" />
+ <circle cx="380" cy="37" r="18" />
+ <rect x="432" y="19" width="126.587" height="15" />
+ <rect x="432" y="41" width="247" height="15" />
+ <rect x="158" y="19" width="86.1" height="15" />
+ <rect x="158" y="41" width="168" height="15" />
+ <rect x="22" y="19" width="96" height="36" />
+ <rect x="924" y="30" width="96" height="15" />
+ <rect x="1057" y="20" width="166" height="35" />
+ </gl-skeleton-loader>
+ </div>
+
+ <jobs-table v-else :jobs="jobs" :table-fields="$options.fields" />
+
+ <gl-intersection-observer v-if="jobsPageInfo.hasNextPage" @appear="fetchMoreJobs">
+ <gl-loading-icon v-if="$apollo.loading" size="md" />
+ </gl-intersection-observer>
+ </div>
+</template>
diff --git a/app/assets/javascripts/pipelines/components/pipeline_graph/job_pill.vue b/app/assets/javascripts/pipelines/components/pipeline_graph/job_pill.vue
index 836333c8bde..793e343a02a 100644
--- a/app/assets/javascripts/pipelines/components/pipeline_graph/job_pill.vue
+++ b/app/assets/javascripts/pipelines/components/pipeline_graph/job_pill.vue
@@ -1,5 +1,5 @@
<script>
-import tooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate.vue';
+import tooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate/tooltip_on_truncate.vue';
export default {
components: {
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 78771b6a072..64210576b29 100644
--- a/app/assets/javascripts/pipelines/components/pipeline_graph/pipeline_graph.vue
+++ b/app/assets/javascripts/pipelines/components/pipeline_graph/pipeline_graph.vue
@@ -25,7 +25,7 @@ export default {
// The max width and the width make sure the ellipsis to work and the min width
// is for when there is less text than the stage column width (which the width 100% does not fix)
jobWrapperClasses:
- 'gl-display-flex gl-flex-direction-column gl-align-items-center gl-w-full gl-px-8 gl-min-w-full gl-max-w-15',
+ 'gl-display-flex gl-flex-direction-column gl-align-items-stretch gl-w-full gl-px-8 gl-min-w-full gl-max-w-15',
props: {
pipelineData: {
required: true,
diff --git a/app/assets/javascripts/pipelines/components/pipeline_graph/stage_name.vue b/app/assets/javascripts/pipelines/components/pipeline_graph/stage_name.vue
index 367a18af248..e485b38ce11 100644
--- a/app/assets/javascripts/pipelines/components/pipeline_graph/stage_name.vue
+++ b/app/assets/javascripts/pipelines/components/pipeline_graph/stage_name.vue
@@ -1,6 +1,6 @@
<script>
import { capitalize, escape } from 'lodash';
-import tooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate.vue';
+import tooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate/tooltip_on_truncate.vue';
export default {
components: {
diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_stage.vue b/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_stage.vue
index 7552ddb61dc..afcb04cd7eb 100644
--- a/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_stage.vue
+++ b/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_stage.vue
@@ -15,7 +15,7 @@
import { GlDropdown, GlLoadingIcon, GlTooltipDirective, GlIcon } from '@gitlab/ui';
import createFlash from '~/flash';
import axios from '~/lib/utils/axios_utils';
-import { __ } from '~/locale';
+import { __, sprintf } from '~/locale';
import eventHub from '../../event_hub';
import JobItem from './job_item.vue';
@@ -98,6 +98,9 @@ export default {
// warn the pipelines table to update
this.$emit('pipelineActionRequestComplete');
},
+ stageAriaLabel(title) {
+ return sprintf(__('View Stage: %{title}'), { title });
+ },
},
};
</script>
@@ -106,9 +109,10 @@ export default {
<gl-dropdown
ref="dropdown"
v-gl-tooltip.hover.ds0
+ v-gl-tooltip="stage.title"
data-testid="mini-pipeline-graph-dropdown"
- :title="stage.title"
variant="link"
+ :aria-label="stageAriaLabel(stage.title)"
:lazy="true"
:popper-opts="{ placement: 'bottom' }"
:toggle-class="['mini-pipeline-graph-dropdown-toggle', triggerButtonClass]"
diff --git a/app/assets/javascripts/pipelines/graphql/queries/get_dag_vis_data.query.graphql b/app/assets/javascripts/pipelines/graphql/queries/get_dag_vis_data.query.graphql
index 887c217da41..2a0b13dd0cc 100644
--- a/app/assets/javascripts/pipelines/graphql/queries/get_dag_vis_data.query.graphql
+++ b/app/assets/javascripts/pipelines/graphql/queries/get_dag_vis_data.query.graphql
@@ -1,19 +1,24 @@
query getDagVisData($projectPath: ID!, $iid: ID!) {
project(fullPath: $projectPath) {
+ id
pipeline(iid: $iid) {
id
stages {
nodes {
+ id
name
groups {
nodes {
+ id
name
size
jobs {
nodes {
+ id
name
needs {
nodes {
+ id
name
}
}
diff --git a/app/assets/javascripts/pipelines/graphql/queries/get_pipeline_header_data.query.graphql b/app/assets/javascripts/pipelines/graphql/queries/get_pipeline_header_data.query.graphql
index 8fcae9dbad8..47bc167ca52 100644
--- a/app/assets/javascripts/pipelines/graphql/queries/get_pipeline_header_data.query.graphql
+++ b/app/assets/javascripts/pipelines/graphql/queries/get_pipeline_header_data.query.graphql
@@ -1,5 +1,6 @@
query getPipelineHeaderData($fullPath: ID!, $iid: ID!) {
project(fullPath: $fullPath) {
+ id
pipeline(iid: $iid) {
id
iid
@@ -11,6 +12,7 @@ query getPipelineHeaderData($fullPath: ID!, $iid: ID!) {
updatePipeline
}
detailedStatus {
+ id
detailsPath
icon
group
diff --git a/app/assets/javascripts/pipelines/graphql/queries/get_pipeline_jobs.query.graphql b/app/assets/javascripts/pipelines/graphql/queries/get_pipeline_jobs.query.graphql
new file mode 100644
index 00000000000..5fe47e09d9c
--- /dev/null
+++ b/app/assets/javascripts/pipelines/graphql/queries/get_pipeline_jobs.query.graphql
@@ -0,0 +1,70 @@
+#import "~/graphql_shared/fragments/pageInfo.fragment.graphql"
+
+query getPipelineJobs($fullPath: ID!, $iid: ID!, $after: String) {
+ project(fullPath: $fullPath) {
+ id
+ pipeline(iid: $iid) {
+ id
+ jobs(after: $after, first: 20) {
+ pageInfo {
+ ...PageInfo
+ }
+ nodes {
+ artifacts {
+ nodes {
+ downloadPath
+ fileType
+ }
+ }
+ allowFailure
+ status
+ scheduledAt
+ manualJob
+ triggered
+ createdByTag
+ detailedStatus {
+ id
+ detailsPath
+ group
+ icon
+ label
+ text
+ tooltip
+ action {
+ id
+ buttonTitle
+ icon
+ method
+ path
+ title
+ }
+ }
+ id
+ refName
+ refPath
+ tags
+ shortSha
+ commitPath
+ stage {
+ id
+ name
+ }
+ name
+ duration
+ finishedAt
+ coverage
+ retryable
+ playable
+ cancelable
+ active
+ stuck
+ userPermissions {
+ readBuild
+ readJobArtifacts
+ updateBuild
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/app/assets/javascripts/pipelines/pipeline_details_bundle.js b/app/assets/javascripts/pipelines/pipeline_details_bundle.js
index ee9560e36c4..ae8b2503c79 100644
--- a/app/assets/javascripts/pipelines/pipeline_details_bundle.js
+++ b/app/assets/javascripts/pipelines/pipeline_details_bundle.js
@@ -3,6 +3,7 @@ import { __ } from '~/locale';
import createDagApp from './pipeline_details_dag';
import { createPipelinesDetailApp } from './pipeline_details_graph';
import { createPipelineHeaderApp } from './pipeline_details_header';
+import { createPipelineJobsApp } from './pipeline_details_jobs';
import { apolloProvider } from './pipeline_shared_client';
import { createTestDetails } from './pipeline_test_details';
@@ -11,6 +12,7 @@ const SELECTORS = {
PIPELINE_GRAPH: '#js-pipeline-graph-vue',
PIPELINE_HEADER: '#js-pipeline-header-vue',
PIPELINE_TESTS: '#js-pipeline-tests-detail',
+ PIPELINE_JOBS: '#js-pipeline-jobs-vue',
};
export default async function initPipelineDetailsBundle() {
@@ -55,4 +57,14 @@ export default async function initPipelineDetailsBundle() {
message: __('An error occurred while loading the Test Reports tab.'),
});
}
+
+ try {
+ if (gon.features?.jobsTabVue) {
+ createPipelineJobsApp(SELECTORS.PIPELINE_JOBS);
+ }
+ } catch {
+ createFlash({
+ message: __('An error occurred while loading the Jobs tab.'),
+ });
+ }
}
diff --git a/app/assets/javascripts/pipelines/pipeline_details_jobs.js b/app/assets/javascripts/pipelines/pipeline_details_jobs.js
new file mode 100644
index 00000000000..a1294a484f0
--- /dev/null
+++ b/app/assets/javascripts/pipelines/pipeline_details_jobs.js
@@ -0,0 +1,34 @@
+import { GlToast } from '@gitlab/ui';
+import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+import createDefaultClient from '~/lib/graphql';
+import JobsApp from './components/jobs/jobs_app.vue';
+
+Vue.use(VueApollo);
+Vue.use(GlToast);
+
+const apolloProvider = new VueApollo({
+ defaultClient: createDefaultClient(),
+});
+
+export const createPipelineJobsApp = (selector) => {
+ const containerEl = document.querySelector(selector);
+
+ if (!containerEl) {
+ return false;
+ }
+
+ const { fullPath, pipelineIid } = containerEl.dataset;
+
+ return new Vue({
+ el: containerEl,
+ apolloProvider,
+ provide: {
+ fullPath,
+ pipelineIid,
+ },
+ render(createElement) {
+ return createElement(JobsApp);
+ },
+ });
+};
diff --git a/app/assets/javascripts/projects/commit/constants.js b/app/assets/javascripts/projects/commit/constants.js
index d553bca360e..eb3673461bd 100644
--- a/app/assets/javascripts/projects/commit/constants.js
+++ b/app/assets/javascripts/projects/commit/constants.js
@@ -11,7 +11,7 @@ export const I18N_MODAL = {
'ChangeTypeAction|Your changes will be committed to %{branchName} because a merge request is open.',
),
branchInFork: s__(
- 'ChangeTypeAction|A new branch will be created in your fork and a new merge request will be started.',
+ 'ChangeTypeAction|GitLab will create a branch in your fork and start a merge request.',
),
newMergeRequest: __('new merge request'),
actionCancelText: __('Cancel'),
diff --git a/app/assets/javascripts/projects/commit_box/info/graphql/queries/get_linked_pipelines.query.graphql b/app/assets/javascripts/projects/commit_box/info/graphql/queries/get_linked_pipelines.query.graphql
index ee18c70b6fd..c6a0d48626a 100644
--- a/app/assets/javascripts/projects/commit_box/info/graphql/queries/get_linked_pipelines.query.graphql
+++ b/app/assets/javascripts/projects/commit_box/info/graphql/queries/get_linked_pipelines.query.graphql
@@ -1,15 +1,19 @@
query getLinkedPipelines($fullPath: ID!, $iid: ID!) {
project(fullPath: $fullPath) {
+ id
pipeline(iid: $iid) {
+ id
path
downstream {
nodes {
id
path
project {
+ id
name
}
detailedStatus {
+ id
group
icon
label
@@ -20,9 +24,11 @@ query getLinkedPipelines($fullPath: ID!, $iid: ID!) {
id
path
project {
+ id
name
}
detailedStatus {
+ id
group
icon
label
diff --git a/app/assets/javascripts/projects/new/components/new_project_url_select.vue b/app/assets/javascripts/projects/new/components/new_project_url_select.vue
index e0ba60074af..f4a21c6057c 100644
--- a/app/assets/javascripts/projects/new/components/new_project_url_select.vue
+++ b/app/assets/javascripts/projects/new/components/new_project_url_select.vue
@@ -8,7 +8,7 @@ import {
GlDropdownSectionHeader,
GlSearchBoxByType,
} from '@gitlab/ui';
-import { joinPaths } from '~/lib/utils/url_utility';
+import { joinPaths, PATH_SEPARATOR } from '~/lib/utils/url_utility';
import { MINIMUM_SEARCH_LENGTH } from '~/graphql_shared/constants';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import Tracking from '~/tracking';
@@ -36,7 +36,9 @@ export default {
};
},
skip() {
- return this.search.length > 0 && this.search.length < MINIMUM_SEARCH_LENGTH;
+ const hasNotEnoughSearchCharacters =
+ this.search.length > 0 && this.search.length < MINIMUM_SEARCH_LENGTH;
+ return this.shouldSkipQuery || hasNotEnoughSearchCharacters;
},
debounce: DEBOUNCE_DELAY,
},
@@ -52,7 +54,7 @@ export default {
data() {
return {
currentUser: {},
- groupToFilterBy: undefined,
+ groupPathToFilterBy: undefined,
search: '',
selectedNamespace: this.namespaceId
? {
@@ -63,6 +65,7 @@ export default {
id: this.userNamespaceId,
fullPath: this.userNamespaceFullPath,
},
+ shouldSkipQuery: true,
};
},
computed: {
@@ -73,10 +76,8 @@ export default {
return this.currentUser.namespace || {};
},
filteredGroups() {
- return this.groupToFilterBy
- ? this.userGroups.filter((group) =>
- group.fullPath.startsWith(this.groupToFilterBy.fullPath),
- )
+ return this.groupPathToFilterBy
+ ? this.userGroups.filter((group) => group.fullPath.startsWith(this.groupPathToFilterBy))
: this.userGroups;
},
hasGroupMatches() {
@@ -85,7 +86,7 @@ export default {
hasNamespaceMatches() {
return (
this.userNamespace.fullPath?.toLowerCase().includes(this.search.toLowerCase()) &&
- !this.groupToFilterBy
+ !this.groupPathToFilterBy
);
},
hasNoMatches() {
@@ -99,7 +100,10 @@ export default {
eventHub.$off('select-template', this.handleSelectTemplate);
},
methods: {
- focusInput() {
+ handleDropdownShown() {
+ if (this.shouldSkipQuery) {
+ this.shouldSkipQuery = false;
+ }
this.$refs.search.focusInput();
},
handleDropdownItemClick(namespace) {
@@ -111,13 +115,9 @@ export default {
});
this.setNamespace(namespace);
},
- handleSelectTemplate(groupId) {
- this.groupToFilterBy = this.userGroups.find(
- (group) => getIdFromGraphQLId(group.id) === groupId,
- );
- if (this.groupToFilterBy) {
- this.setNamespace(this.groupToFilterBy);
- }
+ handleSelectTemplate(id, fullPath) {
+ this.groupPathToFilterBy = fullPath.split(PATH_SEPARATOR).shift();
+ this.setNamespace({ id, fullPath });
},
setNamespace({ id, fullPath }) {
this.selectedNamespace = {
@@ -137,7 +137,7 @@ export default {
toggle-class="gl-rounded-top-right-base! gl-rounded-bottom-right-base! gl-w-20"
data-qa-selector="select_namespace_dropdown"
@show="track('activate_form_input', { label: trackLabel, property: 'project_path' })"
- @shown="focusInput"
+ @shown="handleDropdownShown"
>
<gl-search-box-by-type
ref="search"
diff --git a/app/assets/javascripts/projects/new/queries/search_namespaces_where_user_can_create_projects.query.graphql b/app/assets/javascripts/projects/new/queries/search_namespaces_where_user_can_create_projects.query.graphql
index 74febec5a51..568e05d1966 100644
--- a/app/assets/javascripts/projects/new/queries/search_namespaces_where_user_can_create_projects.query.graphql
+++ b/app/assets/javascripts/projects/new/queries/search_namespaces_where_user_can_create_projects.query.graphql
@@ -1,5 +1,6 @@
query searchNamespacesWhereUserCanCreateProjects($search: String) {
currentUser {
+ id
groups(permissionScope: CREATE_PROJECTS, search: $search) {
nodes {
id
diff --git a/app/assets/javascripts/projects/pipelines/charts/components/app.vue b/app/assets/javascripts/projects/pipelines/charts/components/app.vue
index 7379d5caed7..d4b1f7e57d8 100644
--- a/app/assets/javascripts/projects/pipelines/charts/components/app.vue
+++ b/app/assets/javascripts/projects/pipelines/charts/components/app.vue
@@ -1,5 +1,6 @@
<script>
import { GlTabs, GlTab } from '@gitlab/ui';
+import API from '~/api';
import { mergeUrlParams, updateHistory, getParameterValues } from '~/lib/utils/url_utility';
import PipelineCharts from './pipeline_charts.vue';
@@ -13,6 +14,9 @@ export default {
LeadTimeCharts: () => import('ee_component/dora/components/lead_time_charts.vue'),
ProjectQualitySummary: () => import('ee_component/project_quality_summary/app.vue'),
},
+ piplelinesTabEvent: 'p_analytics_ci_cd_pipelines',
+ deploymentFrequencyTabEvent: 'p_analytics_ci_cd_deployment_frequency',
+ leadTimeTabEvent: 'p_analytics_ci_cd_lead_time',
inject: {
shouldRenderDoraCharts: {
type: Boolean,
@@ -60,20 +64,35 @@ export default {
updateHistory({ url: path, title: window.title });
}
},
+ trackTabClick(tab) {
+ API.trackRedisHllUserEvent(tab);
+ },
},
};
</script>
<template>
<div>
<gl-tabs v-if="charts.length > 1" :value="selectedTab" @input="onTabChange">
- <gl-tab :title="__('Pipelines')">
+ <gl-tab
+ :title="__('Pipelines')"
+ data-testid="pipelines-tab"
+ @click="trackTabClick($options.piplelinesTabEvent)"
+ >
<pipeline-charts />
</gl-tab>
<template v-if="shouldRenderDoraCharts">
- <gl-tab :title="__('Deployment frequency')">
+ <gl-tab
+ :title="__('Deployment frequency')"
+ data-testid="deployment-frequency-tab"
+ @click="trackTabClick($options.deploymentFrequencyTabEvent)"
+ >
<deployment-frequency-charts />
</gl-tab>
- <gl-tab :title="__('Lead time')">
+ <gl-tab
+ :title="__('Lead time')"
+ data-testid="lead-time-tab"
+ @click="trackTabClick($options.leadTimeTabEvent)"
+ >
<lead-time-charts />
</gl-tab>
</template>
diff --git a/app/assets/javascripts/projects/pipelines/charts/components/statistics_list.vue b/app/assets/javascripts/projects/pipelines/charts/components/statistics_list.vue
index 7bc3b787f75..5383a6cdddf 100644
--- a/app/assets/javascripts/projects/pipelines/charts/components/statistics_list.vue
+++ b/app/assets/javascripts/projects/pipelines/charts/components/statistics_list.vue
@@ -1,10 +1,19 @@
<script>
+import { GlLink } from '@gitlab/ui';
import { SUPPORTED_FORMATS, getFormatter } from '~/lib/utils/unit_format';
import { s__, n__ } from '~/locale';
const defaultPrecision = 2;
export default {
+ components: {
+ GlLink,
+ },
+ inject: {
+ failedPipelinesLink: {
+ default: '',
+ },
+ },
props: {
counts: {
type: Object,
@@ -27,6 +36,7 @@ export default {
{
title: s__('PipelineCharts|Failed:'),
value: n__('1 pipeline', '%d pipelines', this.counts.failed),
+ link: this.failedPipelinesLink,
},
{
title: s__('PipelineCharts|Success ratio:'),
@@ -39,10 +49,13 @@ export default {
</script>
<template>
<ul>
- <template v-for="({ title, value }, index) in statistics">
+ <template v-for="({ title, value, link }, index) in statistics">
<li :key="index">
<span>{{ title }}</span>
- <strong>{{ value }}</strong>
+ <gl-link v-if="link" :href="link">
+ {{ value }}
+ </gl-link>
+ <strong v-else>{{ value }}</strong>
</li>
</template>
</ul>
diff --git a/app/assets/javascripts/projects/pipelines/charts/graphql/queries/get_pipeline_count_by_status.query.graphql b/app/assets/javascripts/projects/pipelines/charts/graphql/queries/get_pipeline_count_by_status.query.graphql
index d68df689f5f..ac7fe51384c 100644
--- a/app/assets/javascripts/projects/pipelines/charts/graphql/queries/get_pipeline_count_by_status.query.graphql
+++ b/app/assets/javascripts/projects/pipelines/charts/graphql/queries/get_pipeline_count_by_status.query.graphql
@@ -1,5 +1,6 @@
query getPipelineCountByStatus($projectPath: ID!) {
project(fullPath: $projectPath) {
+ id
totalPipelines: pipelines {
count
}
diff --git a/app/assets/javascripts/projects/pipelines/charts/graphql/queries/get_project_pipeline_statistics.query.graphql b/app/assets/javascripts/projects/pipelines/charts/graphql/queries/get_project_pipeline_statistics.query.graphql
index 18b645f8831..46e8a6dc87d 100644
--- a/app/assets/javascripts/projects/pipelines/charts/graphql/queries/get_project_pipeline_statistics.query.graphql
+++ b/app/assets/javascripts/projects/pipelines/charts/graphql/queries/get_project_pipeline_statistics.query.graphql
@@ -1,5 +1,6 @@
query getProjectPipelineStatistics($projectPath: ID!) {
project(fullPath: $projectPath) {
+ id
pipelineAnalytics {
weekPipelinesTotals
weekPipelinesLabels
diff --git a/app/assets/javascripts/projects/pipelines/charts/index.js b/app/assets/javascripts/projects/pipelines/charts/index.js
index 003b61d94b1..94d32609e5d 100644
--- a/app/assets/javascripts/projects/pipelines/charts/index.js
+++ b/app/assets/javascripts/projects/pipelines/charts/index.js
@@ -11,7 +11,7 @@ const apolloProvider = new VueApollo({
});
const mountPipelineChartsApp = (el) => {
- const { projectPath } = el.dataset;
+ const { projectPath, failedPipelinesLink, coverageChartPath, defaultBranch } = el.dataset;
const shouldRenderDoraCharts = parseBoolean(el.dataset.shouldRenderDoraCharts);
const shouldRenderQualitySummary = parseBoolean(el.dataset.shouldRenderQualitySummary);
@@ -25,8 +25,11 @@ const mountPipelineChartsApp = (el) => {
apolloProvider,
provide: {
projectPath,
+ failedPipelinesLink,
shouldRenderDoraCharts,
shouldRenderQualitySummary,
+ coverageChartPath,
+ defaultBranch,
},
render: (createElement) => createElement(ProjectPipelinesCharts, {}),
});
diff --git a/app/assets/javascripts/projects/settings/components/transfer_project_form.vue b/app/assets/javascripts/projects/settings/components/transfer_project_form.vue
new file mode 100644
index 00000000000..b98e1101884
--- /dev/null
+++ b/app/assets/javascripts/projects/settings/components/transfer_project_form.vue
@@ -0,0 +1,63 @@
+<script>
+import { GlFormGroup } from '@gitlab/ui';
+import NamespaceSelect from '~/vue_shared/components/namespace_select/namespace_select.vue';
+import ConfirmDanger from '~/vue_shared/components/confirm_danger/confirm_danger.vue';
+
+export default {
+ name: 'TransferProjectForm',
+ components: {
+ GlFormGroup,
+ NamespaceSelect,
+ ConfirmDanger,
+ },
+ props: {
+ namespaces: {
+ type: Object,
+ required: true,
+ },
+ confirmationPhrase: {
+ type: String,
+ required: true,
+ },
+ confirmButtonText: {
+ type: String,
+ required: true,
+ },
+ },
+ data() {
+ return { selectedNamespace: null };
+ },
+ computed: {
+ hasSelectedNamespace() {
+ return Boolean(this.selectedNamespace?.id);
+ },
+ },
+ methods: {
+ handleSelect(selectedNamespace) {
+ this.selectedNamespace = selectedNamespace;
+ this.$emit('selectNamespace', selectedNamespace.id);
+ },
+ },
+};
+</script>
+<template>
+ <div>
+ <gl-form-group>
+ <namespace-select
+ class="qa-namespaces-list"
+ data-testid="transfer-project-namespace"
+ :full-width="true"
+ :data="namespaces"
+ :selected-namespace="selectedNamespace"
+ @select="handleSelect"
+ />
+ </gl-form-group>
+ <confirm-danger
+ button-class="qa-transfer-button"
+ :disabled="!hasSelectedNamespace"
+ :phrase="confirmationPhrase"
+ :button-text="confirmButtonText"
+ @confirm="$emit('confirm')"
+ />
+ </div>
+</template>
diff --git a/app/assets/javascripts/projects/settings/init_transfer_project_form.js b/app/assets/javascripts/projects/settings/init_transfer_project_form.js
new file mode 100644
index 00000000000..47b49031dc9
--- /dev/null
+++ b/app/assets/javascripts/projects/settings/init_transfer_project_form.js
@@ -0,0 +1,53 @@
+import Vue from 'vue';
+import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
+import TransferProjectForm from './components/transfer_project_form.vue';
+
+const prepareNamespaces = (rawNamespaces = '') => {
+ const data = JSON.parse(rawNamespaces);
+ return {
+ group: data?.group.map(convertObjectPropsToCamelCase),
+ user: data?.user.map(convertObjectPropsToCamelCase),
+ };
+};
+
+export default () => {
+ const el = document.querySelector('.js-transfer-project-form');
+ if (!el) {
+ return false;
+ }
+
+ const {
+ targetFormId = null,
+ targetHiddenInputId = null,
+ buttonText: confirmButtonText = '',
+ phrase: confirmationPhrase = '',
+ confirmDangerMessage = '',
+ namespaces = '',
+ } = el.dataset;
+
+ return new Vue({
+ el,
+ provide: {
+ confirmDangerMessage,
+ },
+ render(createElement) {
+ return createElement(TransferProjectForm, {
+ props: {
+ confirmButtonText,
+ confirmationPhrase,
+ namespaces: prepareNamespaces(namespaces),
+ },
+ on: {
+ selectNamespace: (id) => {
+ if (targetHiddenInputId && document.getElementById(targetHiddenInputId)) {
+ document.getElementById(targetHiddenInputId).value = id;
+ }
+ },
+ confirm: () => {
+ if (targetFormId) document.getElementById(targetFormId)?.submit();
+ },
+ },
+ });
+ },
+ });
+};
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 b8053bf9ab5..e5ddfe82e3b 100644
--- a/app/assets/javascripts/projects/settings_service_desk/components/service_desk_setting.vue
+++ b/app/assets/javascripts/projects/settings_service_desk/components/service_desk_setting.vue
@@ -1,5 +1,15 @@
<script>
-import { GlButton, GlToggle, GlLoadingIcon, GlSprintf, GlFormInput, GlLink } from '@gitlab/ui';
+import {
+ GlButton,
+ GlToggle,
+ GlLoadingIcon,
+ GlSprintf,
+ GlFormInputGroup,
+ GlFormGroup,
+ GlFormInput,
+ GlLink,
+} from '@gitlab/ui';
+import { helpPagePath } from '~/helpers/help_page_helper';
import { __ } from '~/locale';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import ServiceDeskTemplateDropdown from './service_desk_template_dropdown.vue';
@@ -15,6 +25,8 @@ export default {
GlLoadingIcon,
GlSprintf,
GlFormInput,
+ GlFormGroup,
+ GlFormInputGroup,
GlLink,
ServiceDeskTemplateDropdown,
},
@@ -88,6 +100,16 @@ export default {
hasCustomEmail() {
return this.customEmail && this.customEmail !== this.incomingEmail;
},
+ emailSuffixHelpUrl() {
+ return helpPagePath('user/project/service_desk.html', {
+ anchor: 'configuring-a-custom-email-address-suffix',
+ });
+ },
+ customEmailAddressHelpUrl() {
+ return helpPagePath('user/project/service_desk.html', {
+ anchor: 'using-a-custom-email-address',
+ });
+ },
},
methods: {
onCheckboxToggle(isChecked) {
@@ -132,101 +154,122 @@ export default {
</label>
<div v-if="isEnabled" class="row mt-3">
<div class="col-md-9 mb-0">
- <strong
- id="incoming-email-describer"
- class="gl-display-block gl-mb-1"
- data-testid="incoming-email-describer"
+ <gl-form-group
+ :label="__('Email address to use for Support Desk')"
+ label-for="incoming-email"
+ data-testid="incoming-email-label"
>
- {{ __('Email address to use for Support Desk') }}
- </strong>
- <template v-if="email">
- <div class="input-group">
- <input
+ <gl-form-input-group v-if="email">
+ <gl-form-input
+ id="incoming-email"
ref="service-desk-incoming-email"
type="text"
- class="form-control"
data-testid="incoming-email"
:placeholder="__('Incoming email')"
:aria-label="__('Incoming email')"
aria-describedby="incoming-email-describer"
:value="email"
- disabled="true"
+ :disabled="true"
/>
- <div class="input-group-append">
+ <template #append>
<clipboard-button :title="__('Copy')" :text="email" css-class="input-group-text" />
- </div>
- </div>
- <span v-if="hasCustomEmail" class="form-text text-muted">
- <gl-sprintf :message="__('Emails sent to %{email} are also supported.')">
- <template #email>
- <code>{{ incomingEmail }}</code>
+ </template>
+ </gl-form-input-group>
+ <template v-if="email && hasCustomEmail" #description>
+ <span class="gl-mt-2 d-inline-block">
+ <gl-sprintf :message="__('Emails sent to %{email} are also supported.')">
+ <template #email>
+ <code>{{ incomingEmail }}</code>
+ </template>
+ </gl-sprintf>
+ </span>
+ </template>
+ <template v-if="!email">
+ <gl-loading-icon size="sm" :inline="true" />
+ <span class="sr-only">{{ __('Fetching incoming email') }}</span>
+ </template>
+ </gl-form-group>
+
+ <gl-form-group :label="__('Email address suffix')" :state="!projectKeyError">
+ <gl-form-input
+ v-if="hasProjectKeySupport"
+ id="service-desk-project-suffix"
+ v-model.trim="projectKey"
+ data-testid="project-suffix"
+ @blur="validateProjectKey"
+ />
+
+ <template v-if="hasProjectKeySupport" #description>
+ <gl-sprintf
+ :message="
+ __('Add a suffix to Service Desk email address. %{linkStart}Learn more.%{linkEnd}')
+ "
+ >
+ <template #link="{ content }">
+ <gl-link
+ :href="emailSuffixHelpUrl"
+ target="_blank"
+ class="gl-text-blue-600 font-size-inherit"
+ >{{ content }}
+ </gl-link>
</template>
</gl-sprintf>
- </span>
- </template>
- <template v-else>
- <gl-loading-icon size="sm" :inline="true" />
- <span class="sr-only">{{ __('Fetching incoming email') }}</span>
- </template>
+ </template>
+ <template v-else #description>
+ <gl-sprintf
+ :message="
+ __(
+ 'To add a custom suffix, set up a Service Desk email address. %{linkStart}Learn more.%{linkEnd}',
+ )
+ "
+ >
+ <template #link="{ content }">
+ <gl-link
+ :href="customEmailAddressHelpUrl"
+ target="_blank"
+ class="gl-text-blue-600 font-size-inherit"
+ >{{ content }}
+ </gl-link>
+ </template>
+ </gl-sprintf>
+ </template>
+
+ <template v-if="hasProjectKeySupport && projectKeyError" #invalid-feedback>
+ {{ projectKeyError }}
+ </template>
+ </gl-form-group>
- <label for="service-desk-project-suffix" class="mt-3">
- {{ __('Project name suffix') }}
- </label>
- <gl-form-input
- v-if="hasProjectKeySupport"
- id="service-desk-project-suffix"
- v-model.trim="projectKey"
- data-testid="project-suffix"
- class="form-control"
+ <gl-form-group
+ :label="__('Template to append to all Service Desk issues')"
:state="!projectKeyError"
- @blur="validateProjectKey"
- />
- <span v-if="hasProjectKeySupport && projectKeyError" class="form-text text-danger">
- {{ projectKeyError }}
- </span>
- <span
- v-if="hasProjectKeySupport"
- class="form-text text-muted"
- :class="{ 'gl-mt-2!': hasProjectKeySupport && projectKeyError }"
+ class="mt-3"
>
- {{ __('A string appended to the project path to form the Service Desk email address.') }}
- </span>
- <span v-else class="form-text text-muted">
- <gl-sprintf
- :message="
- __(
- 'To add a custom suffix, set up a Service Desk email address. %{linkStart}Learn more.%{linkEnd}',
- )
- "
- >
- <template #link="{ content }">
- <gl-link
- href="https://docs.gitlab.com/ee/user/project/service_desk.html#using-a-custom-email-address"
- target="_blank"
- class="gl-text-blue-600 font-size-inherit"
- >{{ content }}
- </gl-link>
- </template>
- </gl-sprintf>
- </span>
+ <service-desk-template-dropdown
+ :selected-template="selectedTemplate"
+ :selected-file-template-project-id="selectedFileTemplateProjectId"
+ :templates="templates"
+ @change="templateChange"
+ />
+ </gl-form-group>
+
+ <gl-form-group
+ :label="__('Email display name')"
+ label-for="service-desk-email-from-name"
+ :state="!projectKeyError"
+ class="mt-3"
+ >
+ <gl-form-input
+ v-if="hasProjectKeySupport"
+ id="service-desk-email-from-name"
+ v-model.trim="outgoingName"
+ data-testid="email-from-name"
+ />
- <label for="service-desk-template-select" class="mt-3">
- {{ __('Template to append to all Service Desk issues') }}
- </label>
- <service-desk-template-dropdown
- :selected-template="selectedTemplate"
- :selected-file-template-project-id="selectedFileTemplateProjectId"
- :templates="templates"
- @change="templateChange"
- />
+ <template v-if="hasProjectKeySupport" #description>
+ {{ __('Emails sent from Service Desk have this name.') }}
+ </template>
+ </gl-form-group>
- <label for="service-desk-email-from-name" class="mt-3">
- {{ __('Email display name') }}
- </label>
- <input id="service-desk-email-from-name" v-model.trim="outgoingName" class="form-control" />
- <span class="form-text text-muted">
- {{ __('Emails sent from Service Desk have this name.') }}
- </span>
<div class="gl-display-flex gl-justify-content-end">
<gl-button
variant="success"
diff --git a/app/assets/javascripts/projects/storage_counter/components/app.vue b/app/assets/javascripts/projects/storage_counter/components/app.vue
deleted file mode 100644
index 1a911ea3d9b..00000000000
--- a/app/assets/javascripts/projects/storage_counter/components/app.vue
+++ /dev/null
@@ -1,106 +0,0 @@
-<script>
-import { GlAlert, GlLink, GlLoadingIcon } from '@gitlab/ui';
-import { sprintf } from '~/locale';
-import UsageGraph from '~/vue_shared/components/storage_counter/usage_graph.vue';
-import {
- ERROR_MESSAGE,
- LEARN_MORE_LABEL,
- USAGE_QUOTAS_LABEL,
- TOTAL_USAGE_TITLE,
- TOTAL_USAGE_SUBTITLE,
- TOTAL_USAGE_DEFAULT_TEXT,
- HELP_LINK_ARIA_LABEL,
-} from '../constants';
-import getProjectStorageCount from '../queries/project_storage.query.graphql';
-import { parseGetProjectStorageResults } from '../utils';
-import StorageTable from './storage_table.vue';
-
-export default {
- name: 'StorageCounterApp',
- components: {
- GlAlert,
- GlLink,
- GlLoadingIcon,
- StorageTable,
- UsageGraph,
- },
- inject: ['projectPath', 'helpLinks'],
- apollo: {
- project: {
- query: getProjectStorageCount,
- variables() {
- return {
- fullPath: this.projectPath,
- };
- },
- update(data) {
- return parseGetProjectStorageResults(data, this.helpLinks);
- },
- error() {
- this.error = ERROR_MESSAGE;
- },
- },
- },
- data() {
- return {
- project: {},
- error: '',
- };
- },
- computed: {
- totalUsage() {
- return this.project?.storage?.totalUsage || TOTAL_USAGE_DEFAULT_TEXT;
- },
- storageTypes() {
- return this.project?.storage?.storageTypes || [];
- },
- },
- methods: {
- clearError() {
- this.error = '';
- },
- helpLinkAriaLabel(linkTitle) {
- return sprintf(HELP_LINK_ARIA_LABEL, {
- linkTitle,
- });
- },
- },
- LEARN_MORE_LABEL,
- USAGE_QUOTAS_LABEL,
- TOTAL_USAGE_TITLE,
- TOTAL_USAGE_SUBTITLE,
-};
-</script>
-<template>
- <gl-loading-icon v-if="$apollo.queries.project.loading" class="gl-mt-5" size="md" />
- <gl-alert v-else-if="error" variant="danger" @dismiss="clearError">
- {{ error }}
- </gl-alert>
- <div v-else>
- <div class="gl-pt-5 gl-px-3">
- <div class="gl-display-flex gl-justify-content-space-between gl-align-items-center">
- <div>
- <p class="gl-m-0 gl-font-lg gl-font-weight-bold">{{ $options.TOTAL_USAGE_TITLE }}</p>
- <p class="gl-m-0 gl-text-gray-400">
- {{ $options.TOTAL_USAGE_SUBTITLE }}
- <gl-link
- :href="helpLinks.usageQuotasHelpPagePath"
- target="_blank"
- :aria-label="helpLinkAriaLabel($options.USAGE_QUOTAS_LABEL)"
- data-testid="usage-quotas-help-link"
- >
- {{ $options.LEARN_MORE_LABEL }}
- </gl-link>
- </p>
- </div>
- <p class="gl-m-0 gl-font-size-h-display gl-font-weight-bold" data-testid="total-usage">
- {{ totalUsage }}
- </p>
- </div>
- </div>
- <div v-if="project.statistics" class="gl-w-full">
- <usage-graph :root-storage-statistics="project.statistics" :limit="0" />
- </div>
- <storage-table :storage-types="storageTypes" />
- </div>
-</template>
diff --git a/app/assets/javascripts/projects/storage_counter/components/storage_table.vue b/app/assets/javascripts/projects/storage_counter/components/storage_table.vue
deleted file mode 100644
index a42a9711572..00000000000
--- a/app/assets/javascripts/projects/storage_counter/components/storage_table.vue
+++ /dev/null
@@ -1,88 +0,0 @@
-<script>
-import { GlLink, GlIcon, GlTableLite as GlTable, GlSprintf } from '@gitlab/ui';
-import { numberToHumanSize } from '~/lib/utils/number_utils';
-import { thWidthClass } from '~/lib/utils/table_utility';
-import { sprintf } from '~/locale';
-import { PROJECT_TABLE_LABELS, HELP_LINK_ARIA_LABEL } from '../constants';
-import StorageTypeIcon from './storage_type_icon.vue';
-
-export default {
- name: 'StorageTable',
- components: {
- GlLink,
- GlIcon,
- GlTable,
- GlSprintf,
- StorageTypeIcon,
- },
- props: {
- storageTypes: {
- type: Array,
- required: true,
- },
- },
- methods: {
- helpLinkAriaLabel(linkTitle) {
- return sprintf(HELP_LINK_ARIA_LABEL, {
- linkTitle,
- });
- },
- },
- projectTableFields: [
- {
- key: 'storageType',
- label: PROJECT_TABLE_LABELS.STORAGE_TYPE,
- thClass: thWidthClass(90),
- sortable: true,
- },
- {
- key: 'value',
- label: PROJECT_TABLE_LABELS.VALUE,
- thClass: thWidthClass(10),
- sortable: true,
- formatter: (value) => {
- return numberToHumanSize(value, 1);
- },
- },
- ],
-};
-</script>
-<template>
- <gl-table :items="storageTypes" :fields="$options.projectTableFields">
- <template #cell(storageType)="{ item }">
- <div class="gl-display-flex gl-flex-direction-row">
- <storage-type-icon
- :name="item.storageType.id"
- :data-testid="`${item.storageType.id}-icon`"
- />
- <div>
- <p class="gl-font-weight-bold gl-mb-0" :data-testid="`${item.storageType.id}-name`">
- {{ item.storageType.name }}
- <gl-link
- v-if="item.storageType.helpPath"
- :href="item.storageType.helpPath"
- target="_blank"
- :aria-label="helpLinkAriaLabel(item.storageType.name)"
- :data-testid="`${item.storageType.id}-help-link`"
- >
- <gl-icon name="question" :size="12" />
- </gl-link>
- </p>
- <p class="gl-mb-0" :data-testid="`${item.storageType.id}-description`">
- {{ item.storageType.description }}
- </p>
- <p v-if="item.storageType.warningMessage" class="gl-mb-0 gl-font-sm">
- <gl-icon name="warning" :size="12" />
- <gl-sprintf :message="item.storageType.warningMessage">
- <template #warningLink="{ content }">
- <gl-link :href="item.storageType.warningLink" target="_blank" class="gl-font-sm">{{
- content
- }}</gl-link>
- </template>
- </gl-sprintf>
- </p>
- </div>
- </div>
- </template>
- </gl-table>
-</template>
diff --git a/app/assets/javascripts/projects/storage_counter/components/storage_type_icon.vue b/app/assets/javascripts/projects/storage_counter/components/storage_type_icon.vue
deleted file mode 100644
index bc7cd42df1e..00000000000
--- a/app/assets/javascripts/projects/storage_counter/components/storage_type_icon.vue
+++ /dev/null
@@ -1,35 +0,0 @@
-<script>
-import { GlIcon } from '@gitlab/ui';
-
-export default {
- components: { GlIcon },
- props: {
- name: {
- type: String,
- required: false,
- default: '',
- },
- },
- methods: {
- iconName(storageTypeName) {
- const defaultStorageTypeIcon = 'disk';
- const storageTypeIconMap = {
- lfsObjectsSize: 'doc-image',
- snippetsSize: 'snippet',
- uploadsSize: 'upload',
- repositorySize: 'infrastructure-registry',
- packagesSize: 'package',
- };
-
- return storageTypeIconMap[`${storageTypeName}`] ?? defaultStorageTypeIcon;
- },
- },
-};
-</script>
-<template>
- <span
- class="gl-display-inline-flex gl-align-items-flex-start gl-justify-content-center gl-min-w-8 gl-pr-2 gl-pt-1"
- >
- <gl-icon :name="iconName(name)" :size="16" class="gl-mt-1" />
- </span>
-</template>
diff --git a/app/assets/javascripts/projects/storage_counter/constants.js b/app/assets/javascripts/projects/storage_counter/constants.js
deleted file mode 100644
index df4b1800dff..00000000000
--- a/app/assets/javascripts/projects/storage_counter/constants.js
+++ /dev/null
@@ -1,61 +0,0 @@
-import { s__, __ } from '~/locale';
-
-export const PROJECT_STORAGE_TYPES = [
- {
- id: 'buildArtifactsSize',
- name: s__('UsageQuota|Artifacts'),
- description: s__('UsageQuota|Pipeline artifacts and job artifacts, created with CI/CD.'),
- warningMessage: s__(
- 'UsageQuota|Because of a known issue, the artifact total for some projects may be incorrect. For more details, read %{warningLinkStart}the epic%{warningLinkEnd}.',
- ),
- warningLink: 'https://gitlab.com/groups/gitlab-org/-/epics/5380',
- },
- {
- id: 'lfsObjectsSize',
- name: s__('UsageQuota|LFS storage'),
- description: s__('UsageQuota|Audio samples, videos, datasets, and graphics.'),
- },
- {
- id: 'packagesSize',
- name: s__('UsageQuota|Packages'),
- description: s__('UsageQuota|Code packages and container images.'),
- },
- {
- id: 'repositorySize',
- name: s__('UsageQuota|Repository'),
- description: s__('UsageQuota|Git repository.'),
- },
- {
- id: 'snippetsSize',
- name: s__('UsageQuota|Snippets'),
- description: s__('UsageQuota|Shared bits of code and text.'),
- },
- {
- id: 'uploadsSize',
- name: s__('UsageQuota|Uploads'),
- description: s__('UsageQuota|File attachments and smaller design graphics.'),
- },
- {
- id: 'wikiSize',
- name: s__('UsageQuota|Wiki'),
- description: s__('UsageQuota|Wiki content.'),
- },
-];
-
-export const PROJECT_TABLE_LABELS = {
- STORAGE_TYPE: s__('UsageQuota|Storage type'),
- VALUE: s__('UsageQuota|Usage'),
-};
-
-export const ERROR_MESSAGE = s__(
- 'UsageQuota|Something went wrong while fetching project storage statistics',
-);
-
-export const LEARN_MORE_LABEL = __('Learn more.');
-export const USAGE_QUOTAS_LABEL = s__('UsageQuota|Usage Quotas');
-export const HELP_LINK_ARIA_LABEL = s__('UsageQuota|%{linkTitle} help link');
-export const TOTAL_USAGE_DEFAULT_TEXT = __('N/A');
-export const TOTAL_USAGE_TITLE = s__('UsageQuota|Usage breakdown');
-export const TOTAL_USAGE_SUBTITLE = s__(
- 'UsageQuota|Includes artifacts, repositories, wiki, uploads, and other items.',
-);
diff --git a/app/assets/javascripts/projects/storage_counter/index.js b/app/assets/javascripts/projects/storage_counter/index.js
deleted file mode 100644
index 15796bc1870..00000000000
--- a/app/assets/javascripts/projects/storage_counter/index.js
+++ /dev/null
@@ -1,51 +0,0 @@
-import Vue from 'vue';
-import VueApollo from 'vue-apollo';
-import createDefaultClient from '~/lib/graphql';
-import StorageCounterApp from './components/app.vue';
-
-Vue.use(VueApollo);
-
-export default (containerId = 'js-project-storage-count-app') => {
- const el = document.getElementById(containerId);
-
- if (!el) {
- return false;
- }
-
- const {
- projectPath,
- usageQuotasHelpPagePath,
- buildArtifactsHelpPagePath,
- lfsObjectsHelpPagePath,
- packagesHelpPagePath,
- repositoryHelpPagePath,
- snippetsHelpPagePath,
- uploadsHelpPagePath,
- wikiHelpPagePath,
- } = el.dataset;
-
- const apolloProvider = new VueApollo({
- defaultClient: createDefaultClient(),
- });
-
- return new Vue({
- el,
- apolloProvider,
- provide: {
- projectPath,
- helpLinks: {
- usageQuotasHelpPagePath,
- buildArtifactsHelpPagePath,
- lfsObjectsHelpPagePath,
- packagesHelpPagePath,
- repositoryHelpPagePath,
- snippetsHelpPagePath,
- uploadsHelpPagePath,
- wikiHelpPagePath,
- },
- },
- render(createElement) {
- return createElement(StorageCounterApp);
- },
- });
-};
diff --git a/app/assets/javascripts/projects/storage_counter/queries/project_storage.query.graphql b/app/assets/javascripts/projects/storage_counter/queries/project_storage.query.graphql
deleted file mode 100644
index a4f2c529522..00000000000
--- a/app/assets/javascripts/projects/storage_counter/queries/project_storage.query.graphql
+++ /dev/null
@@ -1,16 +0,0 @@
-query getProjectStorageCount($fullPath: ID!) {
- project(fullPath: $fullPath) {
- id
- statistics {
- buildArtifactsSize
- pipelineArtifactsSize
- lfsObjectsSize
- packagesSize
- repositorySize
- snippetsSize
- storageSize
- uploadsSize
- wikiSize
- }
- }
-}
diff --git a/app/assets/javascripts/projects/storage_counter/utils.js b/app/assets/javascripts/projects/storage_counter/utils.js
deleted file mode 100644
index 9fca9d88f46..00000000000
--- a/app/assets/javascripts/projects/storage_counter/utils.js
+++ /dev/null
@@ -1,36 +0,0 @@
-import { numberToHumanSize } from '~/lib/utils/number_utils';
-import { PROJECT_STORAGE_TYPES } from './constants';
-
-/**
- * This method parses the results from `getProjectStorageCount` call.
- *
- * @param {Object} data graphql result
- * @returns {Object}
- */
-export const parseGetProjectStorageResults = (data, helpLinks) => {
- const projectStatistics = data?.project?.statistics;
- if (!projectStatistics) {
- return {};
- }
- const { storageSize, ...storageStatistics } = projectStatistics;
- const storageTypes = PROJECT_STORAGE_TYPES.reduce((types, currentType) => {
- const helpPathKey = currentType.id.replace(`Size`, `HelpPagePath`);
- const helpPath = helpLinks[helpPathKey];
-
- return types.concat({
- storageType: {
- ...currentType,
- helpPath,
- },
- value: storageStatistics[currentType.id],
- });
- }, []);
-
- return {
- storage: {
- totalUsage: numberToHumanSize(storageSize, 1),
- storageTypes,
- },
- statistics: projectStatistics,
- };
-};
diff --git a/app/assets/javascripts/related_issues/components/issue_token.vue b/app/assets/javascripts/related_issues/components/issue_token.vue
index abbd612d3ec..61bd2bf5e8e 100644
--- a/app/assets/javascripts/related_issues/components/issue_token.vue
+++ b/app/assets/javascripts/related_issues/components/issue_token.vue
@@ -1,7 +1,7 @@
<script>
import { GlIcon, GlTooltipDirective } from '@gitlab/ui';
import { __, sprintf } from '~/locale';
-import relatedIssuableMixin from '~/vue_shared/mixins/related_issuable_mixin';
+import relatedIssuableMixin from '~/issuable/mixins/related_issuable_mixin';
export default {
name: 'IssueToken',
diff --git a/app/assets/javascripts/related_issues/components/related_issues_list.vue b/app/assets/javascripts/related_issues/components/related_issues_list.vue
index a21e294a34a..58138655241 100644
--- a/app/assets/javascripts/related_issues/components/related_issues_list.vue
+++ b/app/assets/javascripts/related_issues/components/related_issues_list.vue
@@ -2,7 +2,7 @@
import { GlLoadingIcon } from '@gitlab/ui';
import Sortable from 'sortablejs';
import sortableConfig from '~/sortable/sortable_config';
-import RelatedIssuableItem from '~/vue_shared/components/issue/related_issuable_item.vue';
+import RelatedIssuableItem from '~/issuable/components/related_issuable_item.vue';
export default {
name: 'RelatedIssuesList',
diff --git a/app/assets/javascripts/releases/graphql/fragments/release.fragment.graphql b/app/assets/javascripts/releases/graphql/fragments/release.fragment.graphql
index 3a927dfc756..8a5613c75d2 100644
--- a/app/assets/javascripts/releases/graphql/fragments/release.fragment.graphql
+++ b/app/assets/javascripts/releases/graphql/fragments/release.fragment.graphql
@@ -35,6 +35,7 @@ fragment Release on Release {
__typename
nodes {
__typename
+ id
filepath
collectedAt
sha
@@ -52,12 +53,14 @@ fragment Release on Release {
}
commit {
__typename
+ id
sha
webUrl
title
}
author {
__typename
+ id
webUrl
avatarUrl
username
diff --git a/app/assets/javascripts/releases/graphql/fragments/release_for_editing.fragment.graphql b/app/assets/javascripts/releases/graphql/fragments/release_for_editing.fragment.graphql
index 75a73acb9ae..1823a327350 100644
--- a/app/assets/javascripts/releases/graphql/fragments/release_for_editing.fragment.graphql
+++ b/app/assets/javascripts/releases/graphql/fragments/release_for_editing.fragment.graphql
@@ -18,6 +18,7 @@ fragment ReleaseForEditing on Release {
}
milestones {
nodes {
+ id
title
}
}
diff --git a/app/assets/javascripts/releases/graphql/queries/all_releases.query.graphql b/app/assets/javascripts/releases/graphql/queries/all_releases.query.graphql
index c69481150e0..7f67f7d11a3 100644
--- a/app/assets/javascripts/releases/graphql/queries/all_releases.query.graphql
+++ b/app/assets/javascripts/releases/graphql/queries/all_releases.query.graphql
@@ -16,6 +16,7 @@ query allReleasesDeprecated(
) {
project(fullPath: $fullPath) {
__typename
+ id
releases(first: $first, last: $last, before: $before, after: $after, sort: $sort) {
__typename
nodes {
diff --git a/app/assets/javascripts/releases/graphql/queries/one_release.query.graphql b/app/assets/javascripts/releases/graphql/queries/one_release.query.graphql
index c80d6e753ab..dab92d5d41c 100644
--- a/app/assets/javascripts/releases/graphql/queries/one_release.query.graphql
+++ b/app/assets/javascripts/releases/graphql/queries/one_release.query.graphql
@@ -2,6 +2,7 @@
query oneRelease($fullPath: ID!, $tagName: String!) {
project(fullPath: $fullPath) {
+ id
release(tagName: $tagName) {
...Release
}
diff --git a/app/assets/javascripts/releases/graphql/queries/one_release_for_editing.query.graphql b/app/assets/javascripts/releases/graphql/queries/one_release_for_editing.query.graphql
index 767ba4aeca0..962d554303a 100644
--- a/app/assets/javascripts/releases/graphql/queries/one_release_for_editing.query.graphql
+++ b/app/assets/javascripts/releases/graphql/queries/one_release_for_editing.query.graphql
@@ -2,6 +2,7 @@
query oneReleaseForEditing($fullPath: ID!, $tagName: String!) {
project(fullPath: $fullPath) {
+ id
release(tagName: $tagName) {
...ReleaseForEditing
}
diff --git a/app/assets/javascripts/repository/commits_service.js b/app/assets/javascripts/repository/commits_service.js
index 504efaea8cc..5fd9cfd4e53 100644
--- a/app/assets/javascripts/repository/commits_service.js
+++ b/app/assets/javascripts/repository/commits_service.js
@@ -52,14 +52,9 @@ export const loadCommits = async (projectPath, path, ref, offset) => {
}
// We fetch in batches of 25, so this ensures we don't refetch
- Array.from(Array(COMMIT_BATCH_SIZE)).forEach((_, i) => {
- addRequestedOffset(offset - i);
- addRequestedOffset(offset + i);
- });
+ Array.from(Array(COMMIT_BATCH_SIZE)).forEach((_, i) => addRequestedOffset(offset + i));
- // Since a user could scroll either up or down, we want to support lazy loading in both directions
- const commitsBatchUp = await fetchData(projectPath, path, ref, offset - COMMIT_BATCH_SIZE);
- const commitsBatchDown = await fetchData(projectPath, path, ref, offset);
+ const commits = await fetchData(projectPath, path, ref, offset);
- return commitsBatchUp.concat(commitsBatchDown);
+ return commits;
};
diff --git a/app/assets/javascripts/repository/components/blob_button_group.vue b/app/assets/javascripts/repository/components/blob_button_group.vue
index 4e7ca7b17e4..6f540bf8ece 100644
--- a/app/assets/javascripts/repository/components/blob_button_group.vue
+++ b/app/assets/javascripts/repository/components/blob_button_group.vue
@@ -53,6 +53,10 @@ export default {
type: Boolean,
required: true,
},
+ canPushToBranch: {
+ type: Boolean,
+ required: true,
+ },
emptyRepo: {
type: Boolean,
required: true,
@@ -83,6 +87,9 @@ export default {
deleteModalTitle() {
return sprintf(__('Delete %{name}'), { name: this.name });
},
+ lockBtnQASelector() {
+ return this.canLock ? 'lock_button' : 'disabled_lock_button';
+ },
},
};
</script>
@@ -98,6 +105,7 @@ export default {
:is-locked="isLocked"
:can-lock="canLock"
data-testid="lock"
+ :data-qa-selector="lockBtnQASelector"
/>
<gl-button v-gl-modal="replaceModalId" data-testid="replace">
{{ $options.i18n.replace }}
@@ -125,6 +133,7 @@ export default {
:target-branch="targetBranch || ref"
:original-branch="originalBranch || ref"
:can-push-code="canPushCode"
+ :can-push-to-branch="canPushToBranch"
:empty-repo="emptyRepo"
/>
</div>
diff --git a/app/assets/javascripts/repository/components/blob_content_viewer.vue b/app/assets/javascripts/repository/components/blob_content_viewer.vue
index 2cc5a8a79d2..f3fa4526999 100644
--- a/app/assets/javascripts/repository/components/blob_content_viewer.vue
+++ b/app/assets/javascripts/repository/components/blob_content_viewer.vue
@@ -106,6 +106,7 @@ export default {
ideForkAndEditPath: '',
storedExternally: false,
canModifyBlob: false,
+ canCurrentUserPushToBranch: false,
rawPath: '',
externalStorageUrl: '',
replacePath: '',
@@ -156,11 +157,18 @@ export default {
},
canLock() {
const { pushCode, downloadCode } = this.project.userPermissions;
+ const currentUsername = window.gon?.current_username;
+
+ if (this.pathLockedByUser && this.pathLockedByUser.username !== currentUsername) {
+ return false;
+ }
return pushCode && downloadCode;
},
- isLocked() {
- return this.project.pathLocks.nodes.some((node) => node.path === this.path);
+ pathLockedByUser() {
+ const pathLock = this.project.pathLocks.nodes.find((node) => node.path === this.path);
+
+ return pathLock ? pathLock.user : null;
},
showForkSuggestion() {
const { createMergeRequestIn, forkProject } = this.project.userPermissions;
@@ -266,9 +274,10 @@ export default {
:replace-path="blobInfo.replacePath"
:delete-path="blobInfo.webPath"
:can-push-code="project.userPermissions.pushCode"
+ :can-push-to-branch="blobInfo.canCurrentUserPushToBranch"
:empty-repo="project.repository.empty"
:project-path="projectPath"
- :is-locked="isLocked"
+ :is-locked="Boolean(pathLockedByUser)"
:can-lock="canLock"
/>
</template>
diff --git a/app/assets/javascripts/repository/components/blob_viewers/index.js b/app/assets/javascripts/repository/components/blob_viewers/index.js
index c5209d97abb..8f6f2d15215 100644
--- a/app/assets/javascripts/repository/components/blob_viewers/index.js
+++ b/app/assets/javascripts/repository/components/blob_viewers/index.js
@@ -3,8 +3,11 @@ export const loadViewer = (type) => {
case 'empty':
return () => import(/* webpackChunkName: 'blob_empty_viewer' */ './empty_viewer.vue');
case 'text':
- return gon.features.refactorTextViewer
- ? () => import(/* webpackChunkName: 'blob_text_viewer' */ './text_viewer.vue')
+ return gon.features.highlightJs
+ ? () =>
+ import(
+ /* webpackChunkName: 'blob_text_viewer' */ '~/vue_shared/components/source_viewer.vue'
+ )
: null;
case 'download':
return () => import(/* webpackChunkName: 'blob_download_viewer' */ './download_viewer.vue');
@@ -12,6 +15,8 @@ export const loadViewer = (type) => {
return () => import(/* webpackChunkName: 'blob_image_viewer' */ './image_viewer.vue');
case 'video':
return () => import(/* webpackChunkName: 'blob_video_viewer' */ './video_viewer.vue');
+ case 'pdf':
+ return () => import(/* webpackChunkName: 'blob_pdf_viewer' */ './pdf_viewer.vue');
default:
return null;
}
@@ -21,8 +26,7 @@ export const viewerProps = (type, blob) => {
return {
text: {
content: blob.rawTextBlob,
- fileName: blob.name,
- readOnly: true,
+ autoDetect: true, // We'll eventually disable autoDetect and pass the language explicitly to reduce the footprint (https://gitlab.com/gitlab-org/gitlab/-/issues/348145)
},
download: {
fileName: blob.name,
@@ -36,5 +40,9 @@ export const viewerProps = (type, blob) => {
video: {
url: blob.rawPath,
},
+ pdf: {
+ url: blob.rawPath,
+ fileSize: blob.rawSize,
+ },
}[type];
};
diff --git a/app/assets/javascripts/repository/components/blob_viewers/pdf_viewer.vue b/app/assets/javascripts/repository/components/blob_viewers/pdf_viewer.vue
new file mode 100644
index 00000000000..803a357df52
--- /dev/null
+++ b/app/assets/javascripts/repository/components/blob_viewers/pdf_viewer.vue
@@ -0,0 +1,50 @@
+<script>
+import { GlButton } from '@gitlab/ui';
+import PdfViewer from '~/blob/pdf/pdf_viewer.vue';
+import { __ } from '~/locale';
+import { PDF_MAX_FILE_SIZE, PDF_MAX_PAGE_LIMIT } from '../../constants';
+
+export default {
+ components: { GlButton, PdfViewer },
+ i18n: {
+ tooLargeDescription: __('This PDF is too large to display. Please download to view.'),
+ tooLargeButtonText: __('Download PDF'),
+ },
+ props: {
+ url: {
+ type: String,
+ required: true,
+ },
+ fileSize: {
+ type: Number,
+ required: true,
+ },
+ },
+ data() {
+ return { totalPages: 0 };
+ },
+ computed: {
+ tooLargeToDisplay() {
+ return this.fileSize > PDF_MAX_FILE_SIZE || this.totalPages > PDF_MAX_PAGE_LIMIT;
+ },
+ },
+ methods: {
+ handleOnLoad(totalPages) {
+ this.totalPages = totalPages;
+ },
+ },
+};
+</script>
+<template>
+ <div>
+ <pdf-viewer v-if="!tooLargeToDisplay" :pdf="url" @pdflabload="handleOnLoad" />
+
+ <div v-else class="gl-display-flex gl-flex-direction-column gl-align-items-center gl-p-5">
+ <p>{{ $options.i18n.tooLargeDescription }}</p>
+
+ <gl-button icon="download" category="secondary" variant="confirm" :href="url" download>{{
+ $options.i18n.tooLargeButtonText
+ }}</gl-button>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/repository/components/blob_viewers/text_viewer.vue b/app/assets/javascripts/repository/components/blob_viewers/text_viewer.vue
deleted file mode 100644
index 57fc979a56e..00000000000
--- a/app/assets/javascripts/repository/components/blob_viewers/text_viewer.vue
+++ /dev/null
@@ -1,25 +0,0 @@
-<script>
-export default {
- components: {
- SourceEditor: () =>
- import(/* webpackChunkName: 'SourceEditor' */ '~/vue_shared/components/source_editor.vue'),
- },
- props: {
- content: {
- type: String,
- required: true,
- },
- fileName: {
- type: String,
- required: true,
- },
- readOnly: {
- type: Boolean,
- required: true,
- },
- },
-};
-</script>
-<template>
- <source-editor :value="content" :file-name="fileName" :editor-options="{ readOnly }" />
-</template>
diff --git a/app/assets/javascripts/repository/components/delete_blob_modal.vue b/app/assets/javascripts/repository/components/delete_blob_modal.vue
index 4a8cedb60b4..0d3dc06c2c8 100644
--- a/app/assets/javascripts/repository/components/delete_blob_modal.vue
+++ b/app/assets/javascripts/repository/components/delete_blob_modal.vue
@@ -71,6 +71,10 @@ export default {
type: Boolean,
required: true,
},
+ canPushToBranch: {
+ type: Boolean,
+ required: true,
+ },
emptyRepo: {
type: Boolean,
required: true,
@@ -176,9 +180,12 @@ export default {
</template>
<template v-else>
<input type="hidden" name="original_branch" :value="originalBranch" />
- <!-- Once "push to branch" permission is made available, will need to add to conditional
- Follow-up issue: https://gitlab.com/gitlab-org/gitlab/-/issues/335462 -->
- <input v-if="createNewMr" type="hidden" name="create_merge_request" value="1" />
+ <input
+ v-if="createNewMr || !canPushToBranch"
+ type="hidden"
+ name="create_merge_request"
+ value="1"
+ />
<gl-form-group
:label="$options.i18n.COMMIT_LABEL"
label-for="commit_message"
@@ -188,6 +195,7 @@ export default {
v-model="form.fields['commit_message'].value"
v-validation:[form.showValidation]
name="commit_message"
+ data-qa-selector="commit_message_field"
:state="form.fields['commit_message'].state"
:disabled="loading"
required
diff --git a/app/assets/javascripts/repository/components/last_commit.vue b/app/assets/javascripts/repository/components/last_commit.vue
index 62066973ee6..43e114a91d3 100644
--- a/app/assets/javascripts/repository/components/last_commit.vue
+++ b/app/assets/javascripts/repository/components/last_commit.vue
@@ -111,7 +111,7 @@ export default {
</script>
<template>
- <div class="info-well d-none d-sm-flex project-last-commit commit p-3">
+ <div class="well-segment commit gl-p-5 gl-w-full">
<gl-loading-icon v-if="isLoading" size="md" color="dark" class="m-auto" />
<template v-else-if="commit">
<user-avatar-link
diff --git a/app/assets/javascripts/repository/components/table/row.vue b/app/assets/javascripts/repository/components/table/row.vue
index bd06c064ab7..8fcec5fb893 100644
--- a/app/assets/javascripts/repository/components/table/row.vue
+++ b/app/assets/javascripts/repository/components/table/row.vue
@@ -13,7 +13,7 @@ import {
import { escapeRegExp } from 'lodash';
import paginatedTreeQuery from 'shared_queries/repository/paginated_tree.query.graphql';
import { escapeFileUrl } from '~/lib/utils/url_utility';
-import { TREE_PAGE_SIZE } from '~/repository/constants';
+import { TREE_PAGE_SIZE, ROW_APPEAR_DELAY } from '~/repository/constants';
import FileIcon from '~/vue_shared/components/file_icon.vue';
import TimeagoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
@@ -128,6 +128,7 @@ export default {
return {
commit: null,
hasRowAppeared: false,
+ delayedRowAppear: null,
};
},
computed: {
@@ -202,14 +203,19 @@ export default {
rowAppeared() {
this.hasRowAppeared = true;
+ if (this.commitInfo) {
+ return;
+ }
+
if (this.glFeatures.lazyLoadCommits) {
- this.$emit('row-appear', {
- rowNumber: this.rowNumber,
- hasCommit: Boolean(this.commitInfo),
- });
+ this.delayedRowAppear = setTimeout(
+ () => this.$emit('row-appear', this.rowNumber),
+ ROW_APPEAR_DELAY,
+ );
}
},
rowDisappeared() {
+ clearTimeout(this.delayedRowAppear);
this.hasRowAppeared = false;
},
},
diff --git a/app/assets/javascripts/repository/components/tree_content.vue b/app/assets/javascripts/repository/components/tree_content.vue
index ffe8d5531f8..130ebf77361 100644
--- a/app/assets/javascripts/repository/components/tree_content.vue
+++ b/app/assets/javascripts/repository/components/tree_content.vue
@@ -3,7 +3,12 @@ import paginatedTreeQuery from 'shared_queries/repository/paginated_tree.query.g
import createFlash from '~/flash';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { __ } from '../../locale';
-import { TREE_PAGE_SIZE, TREE_INITIAL_FETCH_COUNT, TREE_PAGE_LIMIT } from '../constants';
+import {
+ TREE_PAGE_SIZE,
+ TREE_INITIAL_FETCH_COUNT,
+ TREE_PAGE_LIMIT,
+ COMMIT_BATCH_SIZE,
+} from '../constants';
import getRefMixin from '../mixins/get_ref';
import projectPathQuery from '../queries/project_path.query.graphql';
import { readmeFile } from '../utils/readme';
@@ -151,11 +156,19 @@ export default {
.concat(data.trees.pageInfo, data.submodules.pageInfo, data.blobs.pageInfo)
.find(({ hasNextPage }) => hasNextPage);
},
- loadCommitData({ rowNumber = 0, hasCommit } = {}) {
- if (!this.glFeatures.lazyLoadCommits || hasCommit || isRequested(rowNumber)) {
+ handleRowAppear(rowNumber) {
+ if (!this.glFeatures.lazyLoadCommits || isRequested(rowNumber)) {
return;
}
+ // Since a user could scroll either up or down, we want to support lazy loading in both directions
+ this.loadCommitData(rowNumber);
+
+ if (rowNumber - COMMIT_BATCH_SIZE >= 0) {
+ this.loadCommitData(rowNumber - COMMIT_BATCH_SIZE);
+ }
+ },
+ loadCommitData(rowNumber) {
loadCommits(this.projectPath, this.path, this.ref, rowNumber)
.then(this.setCommitData)
.catch(() => {});
@@ -182,7 +195,7 @@ export default {
:has-more="hasShowMore"
:commits="commits"
@showMore="handleShowMore"
- @row-appear="loadCommitData"
+ @row-appear="handleRowAppear"
/>
<file-preview v-if="readme" :blob="readme" />
</div>
diff --git a/app/assets/javascripts/repository/components/upload_blob_modal.vue b/app/assets/javascripts/repository/components/upload_blob_modal.vue
index 11e5b5608cb..b56c9ce5247 100644
--- a/app/assets/javascripts/repository/components/upload_blob_modal.vue
+++ b/app/assets/javascripts/repository/components/upload_blob_modal.vue
@@ -24,10 +24,10 @@ import {
} from '../constants';
const PRIMARY_OPTIONS_TEXT = __('Upload file');
-const MODAL_TITLE = __('Upload New File');
+const MODAL_TITLE = __('Upload new file');
const REMOVE_FILE_TEXT = __('Remove file');
const NEW_BRANCH_IN_FORK = __(
- 'A new branch will be created in your fork and a new merge request will be started.',
+ 'GitLab will create a branch in your fork and start a merge request.',
);
const ERROR_MESSAGE = __('Error uploading file. Please try again.');
diff --git a/app/assets/javascripts/repository/constants.js b/app/assets/javascripts/repository/constants.js
index 152fabbd7cc..d01757d6141 100644
--- a/app/assets/javascripts/repository/constants.js
+++ b/app/assets/javascripts/repository/constants.js
@@ -11,7 +11,7 @@ export const COMMIT_LABEL = __('Commit message');
export const TARGET_BRANCH_LABEL = __('Target branch');
export const TOGGLE_CREATE_MR_LABEL = __('Start a new merge request with these changes');
export const NEW_BRANCH_IN_FORK = __(
- 'A new branch will be created in your fork and a new merge request will be started.',
+ 'GitLab will create a branch in your fork and start a merge request.',
);
export const COMMIT_MESSAGE_SUBJECT_MAX_LENGTH = 52;
@@ -20,3 +20,8 @@ export const COMMIT_MESSAGE_BODY_MAX_LENGTH = 72;
export const LIMITED_CONTAINER_WIDTH_CLASS = 'limit-container-width';
export const I18N_COMMIT_DATA_FETCH_ERROR = __('An error occurred while fetching commit data.');
+
+export const PDF_MAX_FILE_SIZE = 10000000; // 10 MB
+export const PDF_MAX_PAGE_LIMIT = 50;
+
+export const ROW_APPEAR_DELAY = 150;
diff --git a/app/assets/javascripts/repository/index.js b/app/assets/javascripts/repository/index.js
index 45e026ad695..197b19387cf 100644
--- a/app/assets/javascripts/repository/index.js
+++ b/app/assets/javascripts/repository/index.js
@@ -188,5 +188,5 @@ export default function setupVueRepositoryList() {
},
});
- return { router, data: dataset };
+ return { router, data: dataset, apolloProvider, projectPath };
}
diff --git a/app/assets/javascripts/repository/mutations/lock_path.mutation.graphql b/app/assets/javascripts/repository/mutations/lock_path.mutation.graphql
index eaebc4ddf17..0851564bb24 100644
--- a/app/assets/javascripts/repository/mutations/lock_path.mutation.graphql
+++ b/app/assets/javascripts/repository/mutations/lock_path.mutation.graphql
@@ -4,6 +4,7 @@ mutation toggleLock($projectPath: ID!, $filePath: String!, $lock: Boolean!) {
id
pathLocks {
nodes {
+ id
path
}
}
diff --git a/app/assets/javascripts/repository/pages/tree.vue b/app/assets/javascripts/repository/pages/tree.vue
index cbdc62624d4..6bf674eb3f1 100644
--- a/app/assets/javascripts/repository/pages/tree.vue
+++ b/app/assets/javascripts/repository/pages/tree.vue
@@ -1,5 +1,5 @@
<script>
-import TreeContent from '../components/tree_content.vue';
+import TreeContent from 'jh_else_ce/repository/components/tree_content.vue';
import preloadMixin from '../mixins/preload';
import { updateElementsVisibility } from '../utils/dom';
diff --git a/app/assets/javascripts/repository/queries/blob_info.query.graphql b/app/assets/javascripts/repository/queries/blob_info.query.graphql
index cf3892802fd..45d1ba80917 100644
--- a/app/assets/javascripts/repository/queries/blob_info.query.graphql
+++ b/app/assets/javascripts/repository/queries/blob_info.query.graphql
@@ -9,13 +9,19 @@ query getBlobInfo($projectPath: ID!, $filePath: String!, $ref: String!) {
}
pathLocks {
nodes {
+ id
path
+ user {
+ id
+ username
+ }
}
}
repository {
empty
blobs(paths: [$filePath], ref: $ref) {
nodes {
+ id
webPath
name
size
@@ -28,6 +34,7 @@ query getBlobInfo($projectPath: ID!, $filePath: String!, $ref: String!) {
forkAndEditPath
ideForkAndEditPath
canModifyBlob
+ canCurrentUserPushToBranch
storedExternally
rawPath
replacePath
diff --git a/app/assets/javascripts/right_sidebar.js b/app/assets/javascripts/right_sidebar.js
index 3c8533dd06d..ee9533bbec3 100644
--- a/app/assets/javascripts/right_sidebar.js
+++ b/app/assets/javascripts/right_sidebar.js
@@ -3,7 +3,6 @@
import $ from 'jquery';
import Cookies from 'js-cookie';
import { hide, fixTitle } from '~/tooltips';
-import { DEBOUNCE_DROPDOWN_DELAY } from '~/vue_shared/components/sidebar/labels_select_widget/constants';
import createFlash from './flash';
import axios from './lib/utils/axios_utils';
import { sprintf, s__, __ } from './locale';
@@ -127,14 +126,6 @@ Sidebar.prototype.openDropdown = function (blockOrName) {
this.setCollapseAfterUpdate($block);
this.toggleSidebar('open');
}
-
- // Wait for the sidebar to trigger('click') open
- // so it doesn't cause our dropdown to close preemptively
- setTimeout(() => {
- if (!gon.features?.labelsWidget && !$block.hasClass('labels-select-wrapper')) {
- $block.find('.js-sidebar-dropdown-toggle').trigger('click');
- }
- }, DEBOUNCE_DROPDOWN_DELAY);
};
Sidebar.prototype.setCollapseAfterUpdate = function ($block) {
diff --git a/app/assets/javascripts/runner/admin_runners/admin_runners_app.vue b/app/assets/javascripts/runner/admin_runners/admin_runners_app.vue
index 3edb658eaf5..f8220553db6 100644
--- a/app/assets/javascripts/runner/admin_runners/admin_runners_app.vue
+++ b/app/assets/javascripts/runner/admin_runners/admin_runners_app.vue
@@ -3,12 +3,12 @@ import { GlBadge, GlLink } from '@gitlab/ui';
import createFlash from '~/flash';
import { fetchPolicies } from '~/lib/graphql';
import { updateHistory } from '~/lib/utils/url_utility';
-import { sprintf, __ } from '~/locale';
import RegistrationDropdown from '../components/registration/registration_dropdown.vue';
import RunnerFilteredSearchBar from '../components/runner_filtered_search_bar.vue';
import RunnerList from '../components/runner_list.vue';
import RunnerName from '../components/runner_name.vue';
+import RunnerOnlineStat from '../components/stat/runner_online_stat.vue';
import RunnerPagination from '../components/runner_pagination.vue';
import RunnerTypeTabs from '../components/runner_type_tabs.vue';
@@ -38,6 +38,7 @@ export default {
RunnerFilteredSearchBar,
RunnerList,
RunnerName,
+ RunnerOnlineStat,
RunnerPagination,
RunnerTypeTabs,
},
@@ -110,17 +111,12 @@ export default {
noRunnersFound() {
return !this.runnersLoading && !this.runners.items.length;
},
- activeRunnersMessage() {
- return sprintf(__('Runners currently online: %{active_runners_count}'), {
- active_runners_count: this.activeRunnersCount,
- });
- },
searchTokens() {
return [
statusTokenConfig,
{
...tagTokenConfig,
- recentTokenValuesStorageKey: `${this.$options.filteredSearchNamespace}-recent-tags`,
+ recentSuggestionsStorageKey: `${this.$options.filteredSearchNamespace}-recent-tags`,
},
];
},
@@ -165,6 +161,8 @@ export default {
</script>
<template>
<div>
+ <runner-online-stat class="gl-py-6 gl-px-5" :value="activeRunnersCount" />
+
<div
class="gl-display-flex gl-align-items-center gl-flex-direction-column-reverse gl-md-flex-direction-row gl-mt-3 gl-md-mt-0"
>
@@ -194,11 +192,7 @@ export default {
v-model="search"
:tokens="searchTokens"
:namespace="$options.filteredSearchNamespace"
- >
- <template #runner-count>
- {{ activeRunnersMessage }}
- </template>
- </runner-filtered-search-bar>
+ />
<div v-if="noRunnersFound" class="gl-text-center gl-p-5">
{{ __('No runners found') }}
diff --git a/app/assets/javascripts/runner/components/cells/runner_actions_cell.vue b/app/assets/javascripts/runner/components/cells/runner_actions_cell.vue
index c4bddb7b398..33f7a67aba4 100644
--- a/app/assets/javascripts/runner/components/cells/runner_actions_cell.vue
+++ b/app/assets/javascripts/runner/components/cells/runner_actions_cell.vue
@@ -1,27 +1,29 @@
<script>
-import { GlButton, GlButtonGroup, GlTooltipDirective } from '@gitlab/ui';
+import { GlButton, GlButtonGroup, GlModalDirective, GlTooltipDirective } from '@gitlab/ui';
import createFlash from '~/flash';
-import { __, s__ } from '~/locale';
+import { __, s__, sprintf } from '~/locale';
import runnerDeleteMutation from '~/runner/graphql/runner_delete.mutation.graphql';
import runnerActionsUpdateMutation from '~/runner/graphql/runner_actions_update.mutation.graphql';
import { captureException } from '~/runner/sentry_utils';
+import { getIdFromGraphQLId } from '~/graphql_shared/utils';
+import RunnerDeleteModal from '../runner_delete_modal.vue';
-const i18n = {
- I18N_EDIT: __('Edit'),
- I18N_PAUSE: __('Pause'),
- I18N_RESUME: __('Resume'),
- I18N_REMOVE: __('Remove'),
- I18N_REMOVE_CONFIRMATION: s__('Runners|Are you sure you want to delete this runner?'),
-};
+const I18N_EDIT = __('Edit');
+const I18N_PAUSE = __('Pause');
+const I18N_RESUME = __('Resume');
+const I18N_DELETE = s__('Runners|Delete runner');
+const I18N_DELETED_TOAST = s__('Runners|Runner %{name} was deleted');
export default {
name: 'RunnerActionsCell',
components: {
GlButton,
GlButtonGroup,
+ RunnerDeleteModal,
},
directives: {
GlTooltip: GlTooltipDirective,
+ GlModal: GlModalDirective,
},
props: {
runner: {
@@ -48,21 +50,29 @@ export default {
// mouseout listeners don't run leaving the tooltip stuck
return '';
}
- return this.isActive ? i18n.I18N_PAUSE : i18n.I18N_RESUME;
+ return this.isActive ? I18N_PAUSE : I18N_RESUME;
},
deleteTitle() {
- // Prevent a "sticky" tooltip: If element gets removed,
- // mouseout listeners don't run and leaving the tooltip stuck
- return this.deleting ? '' : i18n.I18N_REMOVE;
+ if (this.deleting) {
+ // Prevent a "sticky" tooltip: If this button is disabled,
+ // mouseout listeners don't run leaving the tooltip stuck
+ return '';
+ }
+ return I18N_DELETE;
+ },
+ runnerId() {
+ return getIdFromGraphQLId(this.runner.id);
+ },
+ runnerName() {
+ return `#${this.runnerId} (${this.runner.shortSha})`;
+ },
+ runnerDeleteModalId() {
+ return `delete-runner-modal-${this.runnerId}`;
},
},
methods: {
async onToggleActive() {
this.updating = true;
- // TODO In HAML iteration we had a confirmation modal via:
- // data-confirm="_('Are you sure?')"
- // this may not have to ported, this is an easily reversible operation
-
try {
const toggledActive = !this.runner.active;
@@ -91,12 +101,8 @@ export default {
},
async onDelete() {
- // TODO Replace confirmation with gl-modal
- // eslint-disable-next-line no-alert
- if (!window.confirm(i18n.I18N_REMOVE_CONFIRMATION)) {
- return;
- }
-
+ // Deleting stays "true" until this row is removed,
+ // should only change back if the operation fails.
this.deleting = true;
try {
const {
@@ -115,11 +121,13 @@ export default {
});
if (errors && errors.length) {
throw new Error(errors.join(' '));
+ } else {
+ // Use $root to have the toast message stay after this element is removed
+ this.$root.$toast?.show(sprintf(I18N_DELETED_TOAST, { name: this.runnerName }));
}
} catch (e) {
- this.onError(e);
- } finally {
this.deleting = false;
+ this.onError(e);
}
},
@@ -133,14 +141,15 @@ export default {
captureException({ error, component: this.$options.name });
},
},
- i18n,
+ I18N_EDIT,
+ I18N_DELETE,
};
</script>
<template>
<gl-button-group>
<!--
- This button appears for administratos: those with
+ This button appears for administrators: those with
access to the adminUrl. More advanced permissions policies
will allow more granular permissions.
@@ -148,16 +157,14 @@ export default {
-->
<gl-button
v-if="runner.adminUrl"
- v-gl-tooltip.hover.viewport
+ v-gl-tooltip.hover.viewport="$options.I18N_EDIT"
:href="runner.adminUrl"
- :title="$options.i18n.I18N_EDIT"
- :aria-label="$options.i18n.I18N_EDIT"
+ :aria-label="$options.I18N_EDIT"
icon="pencil"
data-testid="edit-runner"
/>
<gl-button
- v-gl-tooltip.hover.viewport
- :title="toggleActiveTitle"
+ v-gl-tooltip.hover.viewport="toggleActiveTitle"
:aria-label="toggleActiveTitle"
:icon="toggleActiveIcon"
:loading="updating"
@@ -165,14 +172,20 @@ export default {
@click="onToggleActive"
/>
<gl-button
- v-gl-tooltip.hover.viewport
- :title="deleteTitle"
+ v-gl-tooltip.hover.viewport="deleteTitle"
+ v-gl-modal="runnerDeleteModalId"
:aria-label="deleteTitle"
icon="close"
:loading="deleting"
variant="danger"
data-testid="delete-runner"
- @click="onDelete"
+ />
+
+ <runner-delete-modal
+ :ref="runnerDeleteModalId"
+ :modal-id="runnerDeleteModalId"
+ :runner-name="runnerName"
+ @primary="onDelete"
/>
</gl-button-group>
</template>
diff --git a/app/assets/javascripts/runner/components/cells/runner_status_cell.vue b/app/assets/javascripts/runner/components/cells/runner_status_cell.vue
index 9ba1192bc8c..473cd7e9794 100644
--- a/app/assets/javascripts/runner/components/cells/runner_status_cell.vue
+++ b/app/assets/javascripts/runner/components/cells/runner_status_cell.vue
@@ -1,14 +1,12 @@
<script>
import { GlTooltipDirective } from '@gitlab/ui';
-import RunnerContactedStateBadge from '../runner_contacted_state_badge.vue';
+import RunnerStatusBadge from '../runner_status_badge.vue';
import RunnerPausedBadge from '../runner_paused_badge.vue';
-import { I18N_LOCKED_RUNNER_DESCRIPTION, I18N_PAUSED_RUNNER_DESCRIPTION } from '../../constants';
-
export default {
components: {
- RunnerContactedStateBadge,
+ RunnerStatusBadge,
RunnerPausedBadge,
},
directives: {
@@ -25,16 +23,12 @@ export default {
return !this.runner.active;
},
},
- i18n: {
- I18N_LOCKED_RUNNER_DESCRIPTION,
- I18N_PAUSED_RUNNER_DESCRIPTION,
- },
};
</script>
<template>
<div>
- <runner-contacted-state-badge :runner="runner" size="sm" />
+ <runner-status-badge :runner="runner" size="sm" />
<runner-paused-badge v-if="paused" size="sm" />
</div>
</template>
diff --git a/app/assets/javascripts/runner/components/cells/runner_summary_cell.vue b/app/assets/javascripts/runner/components/cells/runner_summary_cell.vue
index 3b476997915..937ec631633 100644
--- a/app/assets/javascripts/runner/components/cells/runner_summary_cell.vue
+++ b/app/assets/javascripts/runner/components/cells/runner_summary_cell.vue
@@ -1,7 +1,7 @@
<script>
import { GlIcon, GlTooltipDirective } from '@gitlab/ui';
-import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate.vue';
+import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate/tooltip_on_truncate.vue';
import RunnerName from '../runner_name.vue';
import RunnerTypeBadge from '../runner_type_badge.vue';
diff --git a/app/assets/javascripts/runner/components/runner_delete_modal.vue b/app/assets/javascripts/runner/components/runner_delete_modal.vue
new file mode 100644
index 00000000000..8be216a7eb5
--- /dev/null
+++ b/app/assets/javascripts/runner/components/runner_delete_modal.vue
@@ -0,0 +1,51 @@
+<script>
+import { GlModal } from '@gitlab/ui';
+import { __, s__, sprintf } from '~/locale';
+
+const I18N_TITLE = s__('Runners|Delete runner %{name}?');
+const I18N_BODY = s__(
+ 'Runners|The runner will be permanently deleted and no longer available for projects or groups in the instance. Are you sure you want to continue?',
+);
+const I18N_PRIMARY = s__('Runners|Delete runner');
+const I18N_CANCEL = __('Cancel');
+
+export default {
+ components: {
+ GlModal,
+ },
+ props: {
+ runnerName: {
+ type: String,
+ required: true,
+ },
+ },
+ computed: {
+ title() {
+ return sprintf(I18N_TITLE, { name: this.runnerName });
+ },
+ },
+ methods: {
+ onPrimary() {
+ this.$refs.modal.hide();
+ },
+ },
+ actionPrimary: { text: I18N_PRIMARY, attributes: { variant: 'danger' } },
+ actionCancel: { text: I18N_CANCEL },
+ I18N_BODY,
+};
+</script>
+
+<template>
+ <gl-modal
+ ref="modal"
+ size="sm"
+ :title="title"
+ :action-primary="$options.actionPrimary"
+ :action-cancel="$options.actionCancel"
+ v-bind="$attrs"
+ v-on="$listeners"
+ @primary="onPrimary"
+ >
+ {{ $options.I18N_BODY }}
+ </gl-modal>
+</template>
diff --git a/app/assets/javascripts/runner/components/runner_filtered_search_bar.vue b/app/assets/javascripts/runner/components/runner_filtered_search_bar.vue
index a9dfec35479..f0f8bbdf5df 100644
--- a/app/assets/javascripts/runner/components/runner_filtered_search_bar.vue
+++ b/app/assets/javascripts/runner/components/runner_filtered_search_bar.vue
@@ -76,24 +76,18 @@ export default {
};
</script>
<template>
- <div
+ <filtered-search
class="gl-bg-gray-10 gl-p-5 gl-border-solid gl-border-gray-100 gl-border-0 gl-border-t-1 gl-border-b-1"
- >
- <filtered-search
- v-bind="$attrs"
- :namespace="namespace"
- recent-searches-storage-key="runners-search"
- :sort-options="$options.sortOptions"
- :initial-filter-value="initialFilterValue"
- :tokens="tokens"
- :initial-sort-by="initialSortBy"
- :search-input-placeholder="__('Search or filter results...')"
- data-testid="runners-filtered-search"
- @onFilter="onFilter"
- @onSort="onSort"
- />
- <div class="gl-text-right" data-testid="runner-count">
- <slot name="runner-count"></slot>
- </div>
- </div>
+ v-bind="$attrs"
+ :namespace="namespace"
+ recent-searches-storage-key="runners-search"
+ :sort-options="$options.sortOptions"
+ :initial-filter-value="initialFilterValue"
+ :tokens="tokens"
+ :initial-sort-by="initialSortBy"
+ :search-input-placeholder="__('Search or filter results...')"
+ data-testid="runners-filtered-search"
+ @onFilter="onFilter"
+ @onSort="onSort"
+ />
</template>
diff --git a/app/assets/javascripts/runner/components/runner_list.vue b/app/assets/javascripts/runner/components/runner_list.vue
index f8dbc469c22..023308dbac2 100644
--- a/app/assets/javascripts/runner/components/runner_list.vue
+++ b/app/assets/javascripts/runner/components/runner_list.vue
@@ -1,27 +1,26 @@
<script>
import { GlTable, GlTooltipDirective, GlSkeletonLoader } from '@gitlab/ui';
+import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate/tooltip_on_truncate.vue';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
-import { __, s__ } from '~/locale';
+import { formatNumber, __, s__ } from '~/locale';
import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue';
+import { RUNNER_JOB_COUNT_LIMIT } from '../constants';
import RunnerActionsCell from './cells/runner_actions_cell.vue';
import RunnerSummaryCell from './cells/runner_summary_cell.vue';
import RunnerStatusCell from './cells/runner_status_cell.vue';
import RunnerTags from './runner_tags.vue';
-const tableField = ({ key, label = '', width = 10 }) => {
+const tableField = ({ key, label = '', thClasses = [] }) => {
return {
key,
label,
thClass: [
- `gl-w-${width}p`,
'gl-bg-transparent!',
'gl-border-b-solid!',
'gl-border-b-gray-100!',
- 'gl-py-5!',
- 'gl-px-0!',
'gl-border-b-1!',
+ ...thClasses,
],
- tdClass: ['gl-py-5!', 'gl-px-1!'],
tdAttr: {
'data-testid': `td-${key}`,
},
@@ -32,6 +31,7 @@ export default {
components: {
GlTable,
GlSkeletonLoader,
+ TooltipOnTruncate,
TimeAgo,
RunnerActionsCell,
RunnerSummaryCell,
@@ -53,6 +53,12 @@ export default {
},
},
methods: {
+ formatJobCount(jobCount) {
+ if (jobCount > RUNNER_JOB_COUNT_LIMIT) {
+ return `${formatNumber(RUNNER_JOB_COUNT_LIMIT)}+`;
+ }
+ return formatNumber(jobCount);
+ },
runnerTrAttr(runner) {
if (runner) {
return {
@@ -64,10 +70,11 @@ export default {
},
fields: [
tableField({ key: 'status', label: s__('Runners|Status') }),
- tableField({ key: 'summary', label: s__('Runners|Runner ID'), width: 30 }),
+ tableField({ key: 'summary', label: s__('Runners|Runner ID'), thClasses: ['gl-lg-w-25p'] }),
tableField({ key: 'version', label: __('Version') }),
tableField({ key: 'ipAddress', label: __('IP Address') }),
- tableField({ key: 'tagList', label: __('Tags'), width: 20 }),
+ tableField({ key: 'jobCount', label: __('Jobs') }),
+ tableField({ key: 'tagList', label: __('Tags'), thClasses: ['gl-lg-w-25p'] }),
tableField({ key: 'contactedAt', label: __('Last contact') }),
tableField({ key: 'actions', label: '' }),
],
@@ -82,6 +89,7 @@ export default {
:tbody-tr-attr="runnerTrAttr"
data-testid="runner-list"
stacked="md"
+ primary-key="id"
fixed
>
<template v-if="!runners.length" #table-busy>
@@ -101,11 +109,19 @@ export default {
</template>
<template #cell(version)="{ item: { version } }">
- {{ version }}
+ <tooltip-on-truncate class="gl-display-block gl-text-truncate" :title="version">
+ {{ version }}
+ </tooltip-on-truncate>
</template>
<template #cell(ipAddress)="{ item: { ipAddress } }">
- {{ ipAddress }}
+ <tooltip-on-truncate class="gl-display-block gl-text-truncate" :title="ipAddress">
+ {{ ipAddress }}
+ </tooltip-on-truncate>
+ </template>
+
+ <template #cell(jobCount)="{ item: { jobCount } }">
+ {{ formatJobCount(jobCount) }}
</template>
<template #cell(tagList)="{ item: { tagList } }">
diff --git a/app/assets/javascripts/runner/components/runner_contacted_state_badge.vue b/app/assets/javascripts/runner/components/runner_status_badge.vue
index b4727f832f8..0823876a187 100644
--- a/app/assets/javascripts/runner/components/runner_contacted_state_badge.vue
+++ b/app/assets/javascripts/runner/components/runner_status_badge.vue
@@ -1,14 +1,17 @@
<script>
import { GlBadge, GlTooltipDirective } from '@gitlab/ui';
-import { s__, sprintf } from '~/locale';
+import { __, s__, sprintf } from '~/locale';
import { getTimeago } from '~/lib/utils/datetime_utility';
import {
- I18N_ONLINE_RUNNER_DESCRIPTION,
- I18N_OFFLINE_RUNNER_DESCRIPTION,
+ I18N_ONLINE_RUNNER_TIMEAGO_DESCRIPTION,
I18N_NOT_CONNECTED_RUNNER_DESCRIPTION,
+ I18N_OFFLINE_RUNNER_TIMEAGO_DESCRIPTION,
+ I18N_STALE_RUNNER_DESCRIPTION,
STATUS_ONLINE,
- STATUS_OFFLINE,
STATUS_NOT_CONNECTED,
+ STATUS_NEVER_CONTACTED,
+ STATUS_OFFLINE,
+ STATUS_STALE,
} from '../constants';
export default {
@@ -29,31 +32,39 @@ export default {
if (this.runner.contactedAt) {
return getTimeago().format(this.runner.contactedAt);
}
- return null;
+ // Prevent "just now" from being rendered, in case data is missing.
+ return __('n/a');
},
badge() {
- switch (this.runner.status) {
+ switch (this.runner?.status) {
case STATUS_ONLINE:
return {
variant: 'success',
label: s__('Runners|online'),
- tooltip: sprintf(I18N_ONLINE_RUNNER_DESCRIPTION, {
+ tooltip: sprintf(I18N_ONLINE_RUNNER_TIMEAGO_DESCRIPTION, {
timeAgo: this.contactedAtTimeAgo,
}),
};
+ case STATUS_NOT_CONNECTED:
+ case STATUS_NEVER_CONTACTED:
+ return {
+ variant: 'muted',
+ label: s__('Runners|not connected'),
+ tooltip: I18N_NOT_CONNECTED_RUNNER_DESCRIPTION,
+ };
case STATUS_OFFLINE:
return {
variant: 'muted',
label: s__('Runners|offline'),
- tooltip: sprintf(I18N_OFFLINE_RUNNER_DESCRIPTION, {
+ tooltip: sprintf(I18N_OFFLINE_RUNNER_TIMEAGO_DESCRIPTION, {
timeAgo: this.contactedAtTimeAgo,
}),
};
- case STATUS_NOT_CONNECTED:
+ case STATUS_STALE:
return {
- variant: 'muted',
- label: s__('Runners|not connected'),
- tooltip: I18N_NOT_CONNECTED_RUNNER_DESCRIPTION,
+ variant: 'warning',
+ label: s__('Runners|stale'),
+ tooltip: I18N_STALE_RUNNER_DESCRIPTION,
};
default:
return null;
diff --git a/app/assets/javascripts/runner/components/search_tokens/status_token_config.js b/app/assets/javascripts/runner/components/search_tokens/status_token_config.js
index 9963048ae1d..4b356fa47ed 100644
--- a/app/assets/javascripts/runner/components/search_tokens/status_token_config.js
+++ b/app/assets/javascripts/runner/components/search_tokens/status_token_config.js
@@ -7,6 +7,7 @@ import {
STATUS_ONLINE,
STATUS_OFFLINE,
STATUS_NOT_CONNECTED,
+ STATUS_STALE,
PARAM_KEY_STATUS,
} from '../../constants';
@@ -16,6 +17,7 @@ const options = [
{ value: STATUS_ONLINE, title: s__('Runners|Online') },
{ value: STATUS_OFFLINE, title: s__('Runners|Offline') },
{ value: STATUS_NOT_CONNECTED, title: s__('Runners|Not connected') },
+ { value: STATUS_STALE, title: s__('Runners|Stale') },
];
export const statusTokenConfig = {
diff --git a/app/assets/javascripts/runner/components/search_tokens/tag_token.vue b/app/assets/javascripts/runner/components/search_tokens/tag_token.vue
index ab67ac608e2..7461308ab91 100644
--- a/app/assets/javascripts/runner/components/search_tokens/tag_token.vue
+++ b/app/assets/javascripts/runner/components/search_tokens/tag_token.vue
@@ -68,7 +68,6 @@ export default {
:config="config"
:suggestions-loading="loading"
:suggestions="tags"
- :recent-suggestions-storage-key="config.recentTokenValuesStorageKey"
@fetch-suggestions="fetchTags"
v-on="$listeners"
>
diff --git a/app/assets/javascripts/runner/components/stat/runner_online_stat.vue b/app/assets/javascripts/runner/components/stat/runner_online_stat.vue
new file mode 100644
index 00000000000..b92b9badef0
--- /dev/null
+++ b/app/assets/javascripts/runner/components/stat/runner_online_stat.vue
@@ -0,0 +1,17 @@
+<script>
+import { GlSingleStat } from '@gitlab/ui/dist/charts';
+
+export default {
+ components: {
+ GlSingleStat,
+ },
+};
+</script>
+<template>
+ <gl-single-stat
+ v-bind="$attrs"
+ variant="success"
+ :title="s__('Runners|Online Runners')"
+ :meta-text="s__('Runners|online')"
+ />
+</template>
diff --git a/app/assets/javascripts/runner/constants.js b/app/assets/javascripts/runner/constants.js
index 3952e2398e0..355f3054917 100644
--- a/app/assets/javascripts/runner/constants.js
+++ b/app/assets/javascripts/runner/constants.js
@@ -1,6 +1,7 @@
import { s__ } from '~/locale';
export const RUNNER_PAGE_SIZE = 20;
+export const RUNNER_JOB_COUNT_LIMIT = 1000;
export const GROUP_RUNNER_COUNT_LIMIT = 1000;
export const I18N_FETCH_ERROR = s__('Runners|Something went wrong while fetching runner data.');
@@ -14,15 +15,18 @@ export const I18N_GROUP_RUNNER_DESCRIPTION = s__(
export const I18N_PROJECT_RUNNER_DESCRIPTION = s__('Runners|Associated with one or more projects');
// Status
-export const I18N_ONLINE_RUNNER_DESCRIPTION = s__(
+export const I18N_ONLINE_RUNNER_TIMEAGO_DESCRIPTION = s__(
'Runners|Runner is online; last contact was %{timeAgo}',
);
-export const I18N_OFFLINE_RUNNER_DESCRIPTION = s__(
- 'Runners|No recent contact from this runner; last contact was %{timeAgo}',
-);
export const I18N_NOT_CONNECTED_RUNNER_DESCRIPTION = s__(
'Runners|This runner has never connected to this instance',
);
+export const I18N_OFFLINE_RUNNER_TIMEAGO_DESCRIPTION = s__(
+ 'Runners|No recent contact from this runner; last contact was %{timeAgo}',
+);
+export const I18N_STALE_RUNNER_DESCRIPTION = s__(
+ 'Runners|No contact from this runner in over 3 months',
+);
export const I18N_LOCKED_RUNNER_DESCRIPTION = s__('Runners|You cannot assign to other projects');
export const I18N_PAUSED_RUNNER_DESCRIPTION = s__('Runners|Not available to run jobs');
@@ -54,9 +58,12 @@ export const PROJECT_TYPE = 'PROJECT_TYPE';
export const STATUS_ACTIVE = 'ACTIVE';
export const STATUS_PAUSED = 'PAUSED';
+
export const STATUS_ONLINE = 'ONLINE';
-export const STATUS_OFFLINE = 'OFFLINE';
export const STATUS_NOT_CONNECTED = 'NOT_CONNECTED';
+export const STATUS_NEVER_CONTACTED = 'NEVER_CONTACTED';
+export const STATUS_OFFLINE = 'OFFLINE';
+export const STATUS_STALE = 'STALE';
// CiRunnerAccessLevel
diff --git a/app/assets/javascripts/runner/graphql/get_group_runners.query.graphql b/app/assets/javascripts/runner/graphql/get_group_runners.query.graphql
index 3e5109b1ac4..6da9e276f74 100644
--- a/app/assets/javascripts/runner/graphql/get_group_runners.query.graphql
+++ b/app/assets/javascripts/runner/graphql/get_group_runners.query.graphql
@@ -13,6 +13,7 @@ query getGroupRunners(
$sort: CiRunnerSort
) {
group(fullPath: $groupFullPath) {
+ id
runners(
membership: DESCENDANTS
before: $before
diff --git a/app/assets/javascripts/runner/graphql/get_runner.query.graphql b/app/assets/javascripts/runner/graphql/get_runner.query.graphql
index c294cb9bf22..59c55eae060 100644
--- a/app/assets/javascripts/runner/graphql/get_runner.query.graphql
+++ b/app/assets/javascripts/runner/graphql/get_runner.query.graphql
@@ -1,6 +1,8 @@
#import "ee_else_ce/runner/graphql/runner_details.fragment.graphql"
query getRunner($id: CiRunnerID!) {
+ # We have an id in deeply nested fragment
+ # eslint-disable-next-line @graphql-eslint/require-id-when-available
runner(id: $id) {
...RunnerDetails
}
diff --git a/app/assets/javascripts/runner/graphql/runner_node.fragment.graphql b/app/assets/javascripts/runner/graphql/runner_node.fragment.graphql
index 98f2dab26ca..169f6ffd2ea 100644
--- a/app/assets/javascripts/runner/graphql/runner_node.fragment.graphql
+++ b/app/assets/javascripts/runner/graphql/runner_node.fragment.graphql
@@ -8,7 +8,8 @@ fragment RunnerNode on CiRunner {
ipAddress
active
locked
+ jobCount
tagList
contactedAt
- status
+ status(legacyMode: null)
}
diff --git a/app/assets/javascripts/runner/graphql/runner_update.mutation.graphql b/app/assets/javascripts/runner/graphql/runner_update.mutation.graphql
index ea622fd4958..8d1b75828be 100644
--- a/app/assets/javascripts/runner/graphql/runner_update.mutation.graphql
+++ b/app/assets/javascripts/runner/graphql/runner_update.mutation.graphql
@@ -5,6 +5,8 @@
mutation runnerUpdate($input: RunnerUpdateInput!) {
runnerUpdate(input: $input) {
+ # We have an id in deep nested fragment
+ # eslint-disable-next-line @graphql-eslint/require-id-when-available
runner {
...RunnerDetails
}
diff --git a/app/assets/javascripts/runner/group_runners/group_runners_app.vue b/app/assets/javascripts/runner/group_runners/group_runners_app.vue
index c3dfa885f27..a58a53a6a0d 100644
--- a/app/assets/javascripts/runner/group_runners/group_runners_app.vue
+++ b/app/assets/javascripts/runner/group_runners/group_runners_app.vue
@@ -9,6 +9,7 @@ import RegistrationDropdown from '../components/registration/registration_dropdo
import RunnerFilteredSearchBar from '../components/runner_filtered_search_bar.vue';
import RunnerList from '../components/runner_list.vue';
import RunnerName from '../components/runner_name.vue';
+import RunnerOnlineStat from '../components/stat/runner_online_stat.vue';
import RunnerPagination from '../components/runner_pagination.vue';
import RunnerTypeTabs from '../components/runner_type_tabs.vue';
@@ -35,6 +36,7 @@ export default {
RunnerFilteredSearchBar,
RunnerList,
RunnerName,
+ RunnerOnlineStat,
RunnerPagination,
RunnerTypeTabs,
},
@@ -145,6 +147,8 @@ export default {
<template>
<div>
+ <runner-online-stat class="gl-py-6 gl-px-5" :value="groupRunnersCount" />
+
<div class="gl-display-flex gl-align-items-center">
<runner-type-tabs
v-model="search"
@@ -164,11 +168,7 @@ export default {
v-model="search"
:tokens="searchTokens"
:namespace="filteredSearchNamespace"
- >
- <template #runner-count>
- {{ runnerCountMessage }}
- </template>
- </runner-filtered-search-bar>
+ />
<div v-if="noRunnersFound" class="gl-text-center gl-p-5">
{{ __('No runners found') }}
diff --git a/app/assets/javascripts/security_configuration/components/app.vue b/app/assets/javascripts/security_configuration/components/app.vue
index bc13150c99c..75d2b324623 100644
--- a/app/assets/javascripts/security_configuration/components/app.vue
+++ b/app/assets/javascripts/security_configuration/components/app.vue
@@ -1,12 +1,14 @@
<script>
import { GlTab, GlTabs, GlSprintf, GlLink, GlAlert } from '@gitlab/ui';
import { __, s__ } from '~/locale';
+import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue';
import UserCalloutDismisser from '~/vue_shared/components/user_callout_dismisser.vue';
import AutoDevOpsAlert from './auto_dev_ops_alert.vue';
import AutoDevOpsEnabledAlert from './auto_dev_ops_enabled_alert.vue';
import { AUTO_DEVOPS_ENABLED_ALERT_DISMISSED_STORAGE_KEY } from './constants';
import FeatureCard from './feature_card.vue';
+import TrainingProviderList from './training_provider_list.vue';
import SectionLayout from './section_layout.vue';
import UpgradeBanner from './upgrade_banner.vue';
@@ -23,6 +25,8 @@ export const i18n = {
any subsequent feature branch you create will include the scan.`,
),
securityConfiguration: __('Security Configuration'),
+ vulnerabilityManagement: s__('SecurityConfiguration|Vulnerability Management'),
+ securityTraining: s__('SecurityConfiguration|Security training'),
};
export default {
@@ -40,7 +44,9 @@ export default {
SectionLayout,
UpgradeBanner,
UserCalloutDismisser,
+ TrainingProviderList,
},
+ mixins: [glFeatureFlagsMixin()],
inject: ['projectPath'],
props: {
augmentedSecurityFeatures: {
@@ -231,6 +237,17 @@ export default {
</template>
</section-layout>
</gl-tab>
+ <gl-tab
+ v-if="glFeatures.secureVulnerabilityTraining"
+ data-testid="vulnerability-management-tab"
+ :title="$options.i18n.vulnerabilityManagement"
+ >
+ <section-layout :heading="$options.i18n.securityTraining">
+ <template #features>
+ <training-provider-list />
+ </template>
+ </section-layout>
+ </gl-tab>
</gl-tabs>
</article>
</template>
diff --git a/app/assets/javascripts/security_configuration/components/constants.js b/app/assets/javascripts/security_configuration/components/constants.js
index 9c80506549e..dd8ba72ad1f 100644
--- a/app/assets/javascripts/security_configuration/components/constants.js
+++ b/app/assets/javascripts/security_configuration/components/constants.js
@@ -22,6 +22,7 @@ import configureSecretDetectionMutation from '../graphql/configure_secret_detect
/**
* Translations & helpPagePaths for Security Configuration Page
+ * Make sure to add new scanner translations to the SCANNER_NAMES_MAP below.
*/
export const SAST_NAME = __('Static Application Security Testing (SAST)');
@@ -138,6 +139,18 @@ export const LICENSE_COMPLIANCE_HELP_PATH = helpPagePath(
'user/compliance/license_compliance/index',
);
+export const SCANNER_NAMES_MAP = {
+ SAST: SAST_SHORT_NAME,
+ SAST_IAC: SAST_IAC_NAME,
+ DAST: DAST_SHORT_NAME,
+ API_FUZZING: API_FUZZING_NAME,
+ CONTAINER_SCANNING: CONTAINER_SCANNING_NAME,
+ CLUSTER_IMAGE_SCANNING: CLUSTER_IMAGE_SCANNING_NAME,
+ COVERAGE_FUZZING: COVERAGE_FUZZING_NAME,
+ SECRET_DETECTION: SECRET_DETECTION_NAME,
+ DEPENDENCY_SCANNING: DEPENDENCY_SCANNING_NAME,
+};
+
export const securityFeatures = [
{
name: SAST_NAME,
@@ -156,27 +169,23 @@ export const securityFeatures = [
// https://gitlab.com/gitlab-org/gitlab/-/issues/331621
canEnableByMergeRequest: true,
},
- ...(gon?.features?.configureIacScanningViaMr
- ? [
- {
- name: SAST_IAC_NAME,
- shortName: SAST_IAC_SHORT_NAME,
- description: SAST_IAC_DESCRIPTION,
- helpPath: SAST_IAC_HELP_PATH,
- configurationHelpPath: SAST_IAC_CONFIG_HELP_PATH,
- type: REPORT_TYPE_SAST_IAC,
+ {
+ name: SAST_IAC_NAME,
+ shortName: SAST_IAC_SHORT_NAME,
+ description: SAST_IAC_DESCRIPTION,
+ helpPath: SAST_IAC_HELP_PATH,
+ configurationHelpPath: SAST_IAC_CONFIG_HELP_PATH,
+ type: REPORT_TYPE_SAST_IAC,
- // This field is currently hardcoded because SAST IaC is always available.
- // It will eventually come from the Backend, the progress is tracked in
- // https://gitlab.com/gitlab-org/gitlab/-/issues/331622
- available: true,
+ // This field is currently hardcoded because SAST IaC is always available.
+ // It will eventually come from the Backend, the progress is tracked in
+ // https://gitlab.com/gitlab-org/gitlab/-/issues/331622
+ available: true,
- // This field will eventually come from the backend, the progress is
- // tracked in https://gitlab.com/gitlab-org/gitlab/-/issues/331621
- canEnableByMergeRequest: true,
- },
- ]
- : []),
+ // This field will eventually come from the backend, the progress is
+ // tracked in https://gitlab.com/gitlab-org/gitlab/-/issues/331621
+ canEnableByMergeRequest: true,
+ },
{
name: DAST_NAME,
shortName: DAST_SHORT_NAME,
@@ -278,21 +287,17 @@ export const featureToMutationMap = {
},
}),
},
- ...(gon?.features?.configureIacScanningViaMr
- ? {
- [REPORT_TYPE_SAST_IAC]: {
- mutationId: 'configureSastIac',
- getMutationPayload: (projectPath) => ({
- mutation: configureSastIacMutation,
- variables: {
- input: {
- projectPath,
- },
- },
- }),
+ [REPORT_TYPE_SAST_IAC]: {
+ mutationId: 'configureSastIac',
+ getMutationPayload: (projectPath) => ({
+ mutation: configureSastIacMutation,
+ variables: {
+ input: {
+ projectPath,
},
- }
- : {}),
+ },
+ }),
+ },
[REPORT_TYPE_SECRET_DETECTION]: {
mutationId: 'configureSecretDetection',
getMutationPayload: (projectPath) => ({
diff --git a/app/assets/javascripts/security_configuration/components/training_provider_list.vue b/app/assets/javascripts/security_configuration/components/training_provider_list.vue
new file mode 100644
index 00000000000..509377a63e8
--- /dev/null
+++ b/app/assets/javascripts/security_configuration/components/training_provider_list.vue
@@ -0,0 +1,61 @@
+<script>
+import { GlCard, GlToggle, GlLink, GlSkeletonLoader } from '@gitlab/ui';
+import securityTrainingProvidersQuery from '../graphql/security_training_providers.query.graphql';
+
+export default {
+ components: {
+ GlCard,
+ GlToggle,
+ GlLink,
+ GlSkeletonLoader,
+ },
+ apollo: {
+ securityTrainingProviders: {
+ query: securityTrainingProvidersQuery,
+ },
+ },
+ data() {
+ return {
+ securityTrainingProviders: [],
+ };
+ },
+ computed: {
+ isLoading() {
+ return this.$apollo.queries.securityTrainingProviders.loading;
+ },
+ },
+};
+</script>
+
+<template>
+ <div
+ v-if="isLoading"
+ class="gl-bg-white gl-py-6 gl-rounded-base gl-border-1 gl-border-solid gl-border-gray-100"
+ >
+ <gl-skeleton-loader :width="350" :height="44">
+ <rect width="200" height="8" x="10" y="0" rx="4" />
+ <rect width="300" height="8" x="10" y="15" rx="4" />
+ <rect width="100" height="8" x="10" y="35" rx="4" />
+ </gl-skeleton-loader>
+ </div>
+ <ul v-else class="gl-list-style-none gl-m-0 gl-p-0">
+ <li
+ v-for="{ id, isEnabled, name, description, url } in securityTrainingProviders"
+ :key="id"
+ class="gl-mb-6"
+ >
+ <gl-card>
+ <div class="gl-display-flex">
+ <gl-toggle :value="isEnabled" :label="__('Training mode')" label-position="hidden" />
+ <div class="gl-ml-5">
+ <h3 class="gl-font-lg gl-m-0 gl-mb-2">{{ name }}</h3>
+ <p>
+ {{ description }}
+ <gl-link :href="url" target="_blank">{{ __('Learn more.') }}</gl-link>
+ </p>
+ </div>
+ </div>
+ </gl-card>
+ </li>
+ </ul>
+</template>
diff --git a/app/assets/javascripts/security_configuration/graphql/security_training_providers.query.graphql b/app/assets/javascripts/security_configuration/graphql/security_training_providers.query.graphql
new file mode 100644
index 00000000000..e0c5715ba8e
--- /dev/null
+++ b/app/assets/javascripts/security_configuration/graphql/security_training_providers.query.graphql
@@ -0,0 +1,9 @@
+query Query {
+ securityTrainingProviders @client {
+ name
+ id
+ description
+ isEnabled
+ url
+ }
+}
diff --git a/app/assets/javascripts/security_configuration/index.js b/app/assets/javascripts/security_configuration/index.js
index a8623b468f2..c86ff1a58f2 100644
--- a/app/assets/javascripts/security_configuration/index.js
+++ b/app/assets/javascripts/security_configuration/index.js
@@ -2,10 +2,39 @@ import Vue from 'vue';
import VueApollo from 'vue-apollo';
import createDefaultClient from '~/lib/graphql';
import { parseBooleanDataAttributes } from '~/lib/utils/dom_utils';
+import { __ } from '~/locale';
import SecurityConfigurationApp from './components/app.vue';
import { securityFeatures, complianceFeatures } from './components/constants';
import { augmentFeatures } from './utils';
+// Note: this is behind a feature flag and only a placeholder
+// until the actual GraphQL fields have been added
+// https://gitlab.com/gitlab-org/gi tlab/-/issues/346480
+export const tempResolvers = {
+ Query: {
+ securityTrainingProviders() {
+ return [
+ {
+ __typename: 'SecurityTrainingProvider',
+ id: 101,
+ name: __('Kontra'),
+ description: __('Interactive developer security education.'),
+ url: 'https://application.security/',
+ isEnabled: false,
+ },
+ {
+ __typename: 'SecurityTrainingProvider',
+ id: 102,
+ name: __('SecureCodeWarrior'),
+ description: __('Security training with guide and learning pathways.'),
+ url: 'https://www.securecodewarrior.com/',
+ isEnabled: true,
+ },
+ ];
+ },
+ },
+};
+
export const initSecurityConfiguration = (el) => {
if (!el) {
return null;
@@ -14,7 +43,7 @@ export const initSecurityConfiguration = (el) => {
Vue.use(VueApollo);
const apolloProvider = new VueApollo({
- defaultClient: createDefaultClient(),
+ defaultClient: createDefaultClient(tempResolvers),
});
const {
diff --git a/app/assets/javascripts/security_configuration/utils.js b/app/assets/javascripts/security_configuration/utils.js
index ec6b93c6193..47231497b8f 100644
--- a/app/assets/javascripts/security_configuration/utils.js
+++ b/app/assets/javascripts/security_configuration/utils.js
@@ -1,4 +1,5 @@
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
+import { SCANNER_NAMES_MAP } from '~/security_configuration/components/constants';
export const augmentFeatures = (securityFeatures, complianceFeatures, features = []) => {
const featuresByType = features.reduce((acc, feature) => {
@@ -24,3 +25,13 @@ export const augmentFeatures = (securityFeatures, complianceFeatures, features =
augmentedComplianceFeatures: complianceFeatures.map((feature) => augmentFeature(feature)),
};
};
+
+/**
+ * Converts a list of security scanner IDs (such as SAST_IAC) into a list of their translated
+ * names defined in the SCANNER_NAMES_MAP constant (eg. IaC Scanning).
+ *
+ * @param {String[]} scannerNames
+ * @returns {String[]}
+ */
+export const translateScannerNames = (scannerNames = []) =>
+ scannerNames.map((scannerName) => SCANNER_NAMES_MAP[scannerName] || scannerName);
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 0021fe909e5..e41f3aa5c9d 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
@@ -236,6 +236,8 @@ export default {
},
statusTimeRanges,
safeHtmlConfig: { ADD_TAGS: ['gl-emoji'] },
+ actionPrimary: { text: s__('SetStatusModal|Set status') },
+ actionSecondary: { text: s__('SetStatusModal|Remove status') },
};
</script>
@@ -243,14 +245,13 @@ export default {
<gl-modal
:title="s__('SetStatusModal|Set a status')"
:modal-id="modalId"
- :ok-title="s__('SetStatusModal|Set status')"
- :cancel-title="s__('SetStatusModal|Remove status')"
- ok-variant="success"
+ :action-primary="$options.actionPrimary"
+ :action-secondary="$options.actionSecondary"
modal-class="set-user-status-modal"
@shown="setupEmojiListAndAutocomplete"
@hide="hideEmojiMenu"
- @ok="setStatus"
- @cancel="removeStatus"
+ @primary="setStatus"
+ @secondary="removeStatus"
>
<div>
<input
diff --git a/app/assets/javascripts/shared/milestones/form.js b/app/assets/javascripts/shared/milestones/form.js
deleted file mode 100644
index 3ca9288b156..00000000000
--- a/app/assets/javascripts/shared/milestones/form.js
+++ /dev/null
@@ -1,22 +0,0 @@
-import $ from 'jquery';
-import initDatePicker from '~/behaviors/date_picker';
-import GLForm from '../../gl_form';
-import ZenMode from '../../zen_mode';
-
-export default (initGFM = true) => {
- new ZenMode(); // eslint-disable-line no-new
- initDatePicker();
-
- // eslint-disable-next-line no-new
- new GLForm($('.milestone-form'), {
- emojis: true,
- members: initGFM,
- issues: initGFM,
- mergeRequests: initGFM,
- epics: initGFM,
- milestones: initGFM,
- labels: initGFM,
- snippets: initGFM,
- vulnerabilities: initGFM,
- });
-};
diff --git a/app/assets/javascripts/sidebar/components/assignees/assignees_realtime.vue b/app/assets/javascripts/sidebar/components/assignees/assignees_realtime.vue
index e7ef731eed8..2387fe64b8f 100644
--- a/app/assets/javascripts/sidebar/components/assignees/assignees_realtime.vue
+++ b/app/assets/javascripts/sidebar/components/assignees/assignees_realtime.vue
@@ -1,7 +1,7 @@
<script>
import produce from 'immer';
import { convertToGraphQLId, getIdFromGraphQLId } from '~/graphql_shared/utils';
-import { IssuableType } from '~/issue_show/constants';
+import { IssuableType } from '~/issues/constants';
import { assigneesQueries } from '~/sidebar/constants';
export default {
diff --git a/app/assets/javascripts/sidebar/components/assignees/collapsed_assignee_list.vue b/app/assets/javascripts/sidebar/components/assignees/collapsed_assignee_list.vue
index 20667e695ce..6a74ab83c22 100644
--- a/app/assets/javascripts/sidebar/components/assignees/collapsed_assignee_list.vue
+++ b/app/assets/javascripts/sidebar/components/assignees/collapsed_assignee_list.vue
@@ -110,7 +110,7 @@ export default {
<template>
<div
v-gl-tooltip="tooltipOptions"
- :class="{ 'multiple-users': hasMoreThanOneAssignee }"
+ :class="{ 'multiple-users gl-relative': hasMoreThanOneAssignee }"
:title="tooltipTitle"
class="sidebar-collapsed-icon sidebar-collapsed-user"
>
diff --git a/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees_widget.vue b/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees_widget.vue
index 1b28ba2afd1..5b4dc20e9c8 100644
--- a/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees_widget.vue
+++ b/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees_widget.vue
@@ -3,7 +3,7 @@ import { GlDropdownItem } from '@gitlab/ui';
import { cloneDeep } from 'lodash';
import Vue from 'vue';
import createFlash from '~/flash';
-import { IssuableType } from '~/issue_show/constants';
+import { IssuableType } from '~/issues/constants';
import { __, n__ } from '~/locale';
import SidebarAssigneesRealtime from '~/sidebar/components/assignees/assignees_realtime.vue';
import IssuableAssignees from '~/sidebar/components/assignees/issuable_assignees.vue';
diff --git a/app/assets/javascripts/sidebar/components/assignees/uncollapsed_assignee_list.vue b/app/assets/javascripts/sidebar/components/assignees/uncollapsed_assignee_list.vue
index 8d5c3b2def3..a27dbee31ec 100644
--- a/app/assets/javascripts/sidebar/components/assignees/uncollapsed_assignee_list.vue
+++ b/app/assets/javascripts/sidebar/components/assignees/uncollapsed_assignee_list.vue
@@ -1,6 +1,6 @@
<script>
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
-import { IssuableType } from '~/issue_show/constants';
+import { IssuableType } from '~/issues/constants';
import { __, sprintf } from '~/locale';
import AttentionRequestedToggle from '../attention_requested_toggle.vue';
import AssigneeAvatarLink from './assignee_avatar_link.vue';
diff --git a/app/assets/javascripts/sidebar/components/attention_requested_toggle.vue b/app/assets/javascripts/sidebar/components/attention_requested_toggle.vue
index 38ba468d197..42e56906e2c 100644
--- a/app/assets/javascripts/sidebar/components/attention_requested_toggle.vue
+++ b/app/assets/javascripts/sidebar/components/attention_requested_toggle.vue
@@ -64,7 +64,7 @@ export default {
<gl-button
:loading="loading"
:variant="user.attention_requested ? 'warning' : 'default'"
- :icon="user.attention_requested ? 'star' : 'star-o'"
+ :icon="user.attention_requested ? 'attention-solid' : 'attention'"
:aria-label="tooltipTitle"
size="small"
category="tertiary"
diff --git a/app/assets/javascripts/sidebar/components/confidential/sidebar_confidentiality_form.vue b/app/assets/javascripts/sidebar/components/confidential/sidebar_confidentiality_form.vue
index 1fb4bd26533..209d1cca360 100644
--- a/app/assets/javascripts/sidebar/components/confidential/sidebar_confidentiality_form.vue
+++ b/app/assets/javascripts/sidebar/components/confidential/sidebar_confidentiality_form.vue
@@ -1,7 +1,7 @@
<script>
import { GlSprintf, GlButton } from '@gitlab/ui';
import createFlash from '~/flash';
-import { IssuableType } from '~/issue_show/constants';
+import { IssuableType } from '~/issues/constants';
import { __, sprintf } from '~/locale';
import { confidentialityQueries } from '~/sidebar/constants';
diff --git a/app/assets/javascripts/sidebar/components/crm_contacts/crm_contacts.vue b/app/assets/javascripts/sidebar/components/crm_contacts/crm_contacts.vue
new file mode 100644
index 00000000000..6d4da104952
--- /dev/null
+++ b/app/assets/javascripts/sidebar/components/crm_contacts/crm_contacts.vue
@@ -0,0 +1,131 @@
+<script>
+import { GlIcon, GlPopover, GlTooltipDirective } from '@gitlab/ui';
+import { __, n__, sprintf } from '~/locale';
+import createFlash from '~/flash';
+import { convertToGraphQLId } from '~/graphql_shared/utils';
+import { TYPE_ISSUE } from '~/graphql_shared/constants';
+import getIssueCrmContactsQuery from './queries/get_issue_crm_contacts.query.graphql';
+import issueCrmContactsSubscription from './queries/issue_crm_contacts.subscription.graphql';
+
+export default {
+ components: {
+ GlIcon,
+ GlPopover,
+ },
+ directives: {
+ GlTooltip: GlTooltipDirective,
+ },
+ props: {
+ issueId: {
+ type: String,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ contacts: [],
+ };
+ },
+ apollo: {
+ contacts: {
+ query: getIssueCrmContactsQuery,
+ variables() {
+ return this.queryVariables;
+ },
+ update(data) {
+ return data?.issue?.customerRelationsContacts?.nodes;
+ },
+ error(error) {
+ createFlash({
+ message: __('Something went wrong trying to load issue contacts.'),
+ error,
+ captureError: true,
+ });
+ },
+ subscribeToMore: {
+ document: issueCrmContactsSubscription,
+ variables() {
+ return this.queryVariables;
+ },
+ updateQuery(prev, { subscriptionData }) {
+ const draftData = subscriptionData?.data?.issueCrmContactsUpdated;
+ if (prev && draftData) return { issue: draftData };
+ return prev;
+ },
+ },
+ },
+ },
+ computed: {
+ shouldShowContacts() {
+ return this.contacts?.length;
+ },
+ queryVariables() {
+ return { id: convertToGraphQLId(TYPE_ISSUE, this.issueId) };
+ },
+ contactsLabel() {
+ return sprintf(n__('%{count} contact', '%{count} contacts', this.contactCount), {
+ count: this.contactCount,
+ });
+ },
+ contactCount() {
+ return this.contacts?.length || 0;
+ },
+ },
+ methods: {
+ shouldShowPopover(contact) {
+ return this.popOverData(contact).length > 0;
+ },
+ divider(index) {
+ if (index < this.contactCount - 1) return ',';
+ return '';
+ },
+ popOverData(contact) {
+ return [contact.organization?.name, contact.email, contact.phone, contact.description].filter(
+ Boolean,
+ );
+ },
+ },
+ i18n: {
+ help: __('Work in progress- click here to find out more'),
+ },
+};
+</script>
+
+<template>
+ <div>
+ <div v-gl-tooltip.left.viewport :title="contactsLabel" class="sidebar-collapsed-icon">
+ <gl-icon name="users" />
+ <span> {{ contactCount }} </span>
+ </div>
+ <div
+ v-gl-tooltip.left.viewport="$options.i18n.help"
+ class="hide-collapsed help-button float-right"
+ >
+ <a href="https://gitlab.com/gitlab-org/gitlab/-/issues/2256"><gl-icon name="question-o" /></a>
+ </div>
+ <div class="title hide-collapsed gl-mb-2 gl-line-height-20">
+ {{ contactsLabel }}
+ </div>
+ <div class="hide-collapsed gl-display-flex gl-flex-wrap">
+ <div
+ v-for="(contact, index) in contacts"
+ :id="`contact_container_${index}`"
+ :key="index"
+ class="gl-pr-2"
+ >
+ <span :id="`contact_${index}`" class="gl-font-weight-bold"
+ >{{ contact.firstName }} {{ contact.lastName }}{{ divider(index) }}</span
+ >
+ <gl-popover
+ v-if="shouldShowPopover(contact)"
+ :target="`contact_${index}`"
+ :container="`contact_container_${index}`"
+ triggers="hover focus"
+ placement="top"
+ >
+ <div v-for="row in popOverData(contact)" :key="row">{{ row }}</div>
+ </gl-popover>
+ </div>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/sidebar/components/crm_contacts/queries/get_issue_crm_contacts.query.graphql b/app/assets/javascripts/sidebar/components/crm_contacts/queries/get_issue_crm_contacts.query.graphql
new file mode 100644
index 00000000000..30a0af10d56
--- /dev/null
+++ b/app/assets/javascripts/sidebar/components/crm_contacts/queries/get_issue_crm_contacts.query.graphql
@@ -0,0 +1,7 @@
+#import "./issue_crm_contacts.fragment.graphql"
+
+query issueCrmContacts($id: IssueID!) {
+ issue(id: $id) {
+ ...CrmContacts
+ }
+}
diff --git a/app/assets/javascripts/sidebar/components/crm_contacts/queries/issue_crm_contacts.fragment.graphql b/app/assets/javascripts/sidebar/components/crm_contacts/queries/issue_crm_contacts.fragment.graphql
new file mode 100644
index 00000000000..750e1f1d1af
--- /dev/null
+++ b/app/assets/javascripts/sidebar/components/crm_contacts/queries/issue_crm_contacts.fragment.graphql
@@ -0,0 +1,17 @@
+fragment CrmContacts on Issue {
+ id
+ customerRelationsContacts {
+ nodes {
+ id
+ firstName
+ lastName
+ email
+ phone
+ description
+ organization {
+ id
+ name
+ }
+ }
+ }
+}
diff --git a/app/assets/javascripts/sidebar/components/crm_contacts/queries/issue_crm_contacts.subscription.graphql b/app/assets/javascripts/sidebar/components/crm_contacts/queries/issue_crm_contacts.subscription.graphql
new file mode 100644
index 00000000000..f3b6e4ec06f
--- /dev/null
+++ b/app/assets/javascripts/sidebar/components/crm_contacts/queries/issue_crm_contacts.subscription.graphql
@@ -0,0 +1,9 @@
+#import "./issue_crm_contacts.fragment.graphql"
+
+subscription issueCrmContactsUpdated($id: IssuableID!) {
+ issueCrmContactsUpdated(issuableId: $id) {
+ ... on Issue {
+ ...CrmContacts
+ }
+ }
+}
diff --git a/app/assets/javascripts/sidebar/components/date/sidebar_date_widget.vue b/app/assets/javascripts/sidebar/components/date/sidebar_date_widget.vue
index 1ff24dec884..404bcc3122a 100644
--- a/app/assets/javascripts/sidebar/components/date/sidebar_date_widget.vue
+++ b/app/assets/javascripts/sidebar/components/date/sidebar_date_widget.vue
@@ -1,7 +1,7 @@
<script>
import { GlIcon, GlDatepicker, GlTooltipDirective, GlLink, GlPopover } from '@gitlab/ui';
import createFlash from '~/flash';
-import { IssuableType } from '~/issue_show/constants';
+import { IssuableType } from '~/issues/constants';
import { dateInWords, formatDate, parsePikadayDate } from '~/lib/utils/datetime_utility';
import { __, sprintf } from '~/locale';
import SidebarEditableItem from '~/sidebar/components/sidebar_editable_item.vue';
@@ -124,6 +124,9 @@ export default {
isLoading() {
return this.$apollo.queries.issuable.loading || this.loading;
},
+ initialLoading() {
+ return this.$apollo.queries.issuable.loading;
+ },
hasDate() {
return this.dateValue !== null;
},
@@ -151,7 +154,7 @@ export default {
};
},
dataTestId() {
- return this.dateType === dateTypes.start ? 'start-date' : 'due-date';
+ return this.dateType === dateTypes.start ? 'sidebar-start-date' : 'sidebar-due-date';
},
},
methods: {
@@ -266,15 +269,15 @@ export default {
</gl-popover>
</template>
<template #collapsed>
- <div v-gl-tooltip :title="dateLabel" class="sidebar-collapsed-icon">
+ <div v-gl-tooltip.viewport.left :title="dateLabel" class="sidebar-collapsed-icon">
<gl-icon :size="16" name="calendar" />
<span class="collapse-truncated-title">{{ formattedDate }}</span>
</div>
<sidebar-inherit-date
- v-if="canInherit"
+ v-if="canInherit && !initialLoading"
:issuable="issuable"
- :is-loading="isLoading"
:date-type="dateType"
+ :is-loading="isLoading"
@reset-date="setDate(null)"
@set-date="setFixedDate"
/>
diff --git a/app/assets/javascripts/sidebar/components/date/sidebar_inherit_date.vue b/app/assets/javascripts/sidebar/components/date/sidebar_inherit_date.vue
index b6bfacb2e47..77f8e125dce 100644
--- a/app/assets/javascripts/sidebar/components/date/sidebar_inherit_date.vue
+++ b/app/assets/javascripts/sidebar/components/date/sidebar_inherit_date.vue
@@ -17,8 +17,9 @@ export default {
type: Object,
},
isLoading: {
- required: true,
+ required: false,
type: Boolean,
+ default: false,
},
dateType: {
type: String,
@@ -31,6 +32,7 @@ export default {
return this.issuable?.[dateFields[this.dateType].isDateFixed] || false;
},
set(fixed) {
+ if (fixed === this.issuable[dateFields[this.dateType].isDateFixed]) return;
this.$emit('set-date', fixed);
},
},
diff --git a/app/assets/javascripts/sidebar/components/labels/sidebar_labels.vue b/app/assets/javascripts/sidebar/components/labels/sidebar_labels.vue
deleted file mode 100644
index 5cd4a1a5192..00000000000
--- a/app/assets/javascripts/sidebar/components/labels/sidebar_labels.vue
+++ /dev/null
@@ -1,192 +0,0 @@
-<script>
-import $ from 'jquery';
-import { camelCase, difference, union } from 'lodash';
-import updateIssueLabelsMutation from '~/boards/graphql/issue_set_labels.mutation.graphql';
-import createFlash from '~/flash';
-import { getIdFromGraphQLId, MutationOperationMode } from '~/graphql_shared/utils';
-import { IssuableType } from '~/issue_show/constants';
-import { __ } from '~/locale';
-import updateMergeRequestLabelsMutation from '~/sidebar/queries/update_merge_request_labels.mutation.graphql';
-import { toLabelGid } from '~/sidebar/utils';
-import { DropdownVariant } from '~/vue_shared/components/sidebar/labels_select_vue/constants';
-import LabelsSelect from '~/vue_shared/components/sidebar/labels_select_vue/labels_select_root.vue';
-import LabelsSelectWidget from '~/vue_shared/components/sidebar/labels_select_widget/labels_select_root.vue';
-import { LabelType } from '~/vue_shared/components/sidebar/labels_select_widget/constants';
-import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
-
-const mutationMap = {
- [IssuableType.Issue]: {
- mutation: updateIssueLabelsMutation,
- mutationName: 'updateIssue',
- },
- [IssuableType.MergeRequest]: {
- mutation: updateMergeRequestLabelsMutation,
- mutationName: 'mergeRequestSetLabels',
- },
-};
-
-export default {
- components: {
- LabelsSelect,
- LabelsSelectWidget,
- },
- variant: DropdownVariant.Sidebar,
- mixins: [glFeatureFlagMixin()],
- inject: [
- 'allowLabelCreate',
- 'allowLabelEdit',
- 'allowScopedLabels',
- 'iid',
- 'fullPath',
- 'initiallySelectedLabels',
- 'issuableType',
- 'labelsFetchPath',
- 'labelsManagePath',
- 'projectIssuesPath',
- 'projectPath',
- ],
- data() {
- return {
- isLabelsSelectInProgress: false,
- selectedLabels: this.initiallySelectedLabels,
- LabelType,
- };
- },
- methods: {
- handleDropdownClose() {
- $(this.$el).trigger('hidden.gl.dropdown');
- },
- getUpdateVariables(labels) {
- let labelIds = [];
-
- if (this.glFeatures.labelsWidget) {
- labelIds = labels.map(({ id }) => toLabelGid(id));
- } else {
- const currentLabelIds = this.selectedLabels.map((label) => label.id);
- const userAddedLabelIds = labels.filter((label) => label.set).map((label) => label.id);
- const userRemovedLabelIds = labels.filter((label) => !label.set).map((label) => label.id);
-
- labelIds = difference(union(currentLabelIds, userAddedLabelIds), userRemovedLabelIds).map(
- toLabelGid,
- );
- }
-
- switch (this.issuableType) {
- case IssuableType.Issue:
- return {
- iid: this.iid,
- projectPath: this.projectPath,
- labelIds,
- };
- case IssuableType.MergeRequest:
- return {
- iid: this.iid,
- labelIds,
- operationMode: MutationOperationMode.Replace,
- projectPath: this.projectPath,
- };
- default:
- return {};
- }
- },
- handleUpdateSelectedLabels(dropdownLabels) {
- this.updateSelectedLabels(this.getUpdateVariables(dropdownLabels));
- },
- getRemoveVariables(labelId) {
- switch (this.issuableType) {
- case IssuableType.Issue:
- return {
- iid: this.iid,
- projectPath: this.projectPath,
- removeLabelIds: [labelId],
- };
- case IssuableType.MergeRequest:
- return {
- iid: this.iid,
- labelIds: [toLabelGid(labelId)],
- operationMode: MutationOperationMode.Remove,
- projectPath: this.projectPath,
- };
- default:
- return {};
- }
- },
- handleLabelRemove(labelId) {
- this.updateSelectedLabels(this.getRemoveVariables(labelId));
- },
- updateSelectedLabels(inputVariables) {
- this.isLabelsSelectInProgress = true;
-
- this.$apollo
- .mutate({
- mutation: mutationMap[this.issuableType].mutation,
- variables: { input: inputVariables },
- })
- .then(({ data }) => {
- const { mutationName } = mutationMap[this.issuableType];
-
- if (data[mutationName]?.errors?.length) {
- throw new Error();
- }
-
- const issuableType = camelCase(this.issuableType);
- this.selectedLabels = data[mutationName]?.[issuableType]?.labels?.nodes?.map((label) => ({
- ...label,
- id: getIdFromGraphQLId(label.id),
- }));
- })
- .catch(() => createFlash({ message: __('An error occurred while updating labels.') }))
- .finally(() => {
- this.isLabelsSelectInProgress = false;
- });
- },
- },
-};
-</script>
-
-<template>
- <labels-select-widget
- v-if="glFeatures.labelsWidget"
- class="block labels js-labels-block"
- :iid="iid"
- :full-path="fullPath"
- :allow-label-remove="allowLabelEdit"
- :allow-multiselect="true"
- :footer-create-label-title="__('Create project label')"
- :footer-manage-label-title="__('Manage project labels')"
- :labels-create-title="__('Create project label')"
- :labels-filter-base-path="projectIssuesPath"
- :variant="$options.variant"
- :issuable-type="issuableType"
- workspace-type="project"
- :attr-workspace-path="fullPath"
- :label-create-type="LabelType.project"
- data-qa-selector="labels_block"
- >
- {{ __('None') }}
- </labels-select-widget>
- <labels-select
- v-else
- class="block labels js-labels-block"
- :allow-label-remove="allowLabelEdit"
- :allow-label-create="allowLabelCreate"
- :allow-label-edit="allowLabelEdit"
- :allow-multiselect="true"
- :allow-scoped-labels="allowScopedLabels"
- :footer-create-label-title="__('Create project label')"
- :footer-manage-label-title="__('Manage project labels')"
- :labels-create-title="__('Create project label')"
- :labels-fetch-path="labelsFetchPath"
- :labels-filter-base-path="projectIssuesPath"
- :labels-manage-path="labelsManagePath"
- :labels-select-in-progress="isLabelsSelectInProgress"
- :selected-labels="selectedLabels"
- :variant="$options.sidebar"
- data-qa-selector="labels_block"
- @onDropdownClose="handleDropdownClose"
- @onLabelRemove="handleLabelRemove"
- @updateSelectedLabels="handleUpdateSelectedLabels"
- >
- {{ __('None') }}
- </labels-select>
-</template>
diff --git a/app/assets/javascripts/sidebar/components/lock/mutations/update_issue_lock.mutation.graphql b/app/assets/javascripts/sidebar/components/lock/mutations/update_issue_lock.mutation.graphql
index 2a1bcdf7136..cb9ee6abc9b 100644
--- a/app/assets/javascripts/sidebar/components/lock/mutations/update_issue_lock.mutation.graphql
+++ b/app/assets/javascripts/sidebar/components/lock/mutations/update_issue_lock.mutation.graphql
@@ -1,6 +1,7 @@
mutation updateIssueLocked($input: IssueSetLockedInput!) {
issueSetLocked(input: $input) {
issue {
+ id
discussionLocked
}
errors
diff --git a/app/assets/javascripts/sidebar/components/lock/mutations/update_merge_request_lock.mutation.graphql b/app/assets/javascripts/sidebar/components/lock/mutations/update_merge_request_lock.mutation.graphql
index 8590c8e71a6..11eb3611006 100644
--- a/app/assets/javascripts/sidebar/components/lock/mutations/update_merge_request_lock.mutation.graphql
+++ b/app/assets/javascripts/sidebar/components/lock/mutations/update_merge_request_lock.mutation.graphql
@@ -1,6 +1,7 @@
mutation updateMergeRequestLocked($input: MergeRequestSetLockedInput!) {
mergeRequestSetLocked(input: $input) {
mergeRequest {
+ id
discussionLocked
}
errors
diff --git a/app/assets/javascripts/sidebar/components/reviewers/collapsed_reviewer_list.vue b/app/assets/javascripts/sidebar/components/reviewers/collapsed_reviewer_list.vue
index 9554a98121f..60d8fb4d408 100644
--- a/app/assets/javascripts/sidebar/components/reviewers/collapsed_reviewer_list.vue
+++ b/app/assets/javascripts/sidebar/components/reviewers/collapsed_reviewer_list.vue
@@ -89,7 +89,7 @@ export default {
<template>
<div
v-gl-tooltip="tooltipOptions"
- :class="{ 'multiple-users': hasMoreThanOneReviewer }"
+ :class="{ 'multiple-users gl-relative': hasMoreThanOneReviewer }"
:title="tooltipTitle"
class="sidebar-collapsed-icon sidebar-collapsed-user"
>
diff --git a/app/assets/javascripts/sidebar/components/severity/graphql/mutations/update_issuable_severity.mutation.graphql b/app/assets/javascripts/sidebar/components/severity/graphql/mutations/update_issuable_severity.mutation.graphql
index 750e757971f..c9d36dfdb67 100644
--- a/app/assets/javascripts/sidebar/components/severity/graphql/mutations/update_issuable_severity.mutation.graphql
+++ b/app/assets/javascripts/sidebar/components/severity/graphql/mutations/update_issuable_severity.mutation.graphql
@@ -3,6 +3,7 @@ mutation updateIssuableSeverity($projectPath: ID!, $severity: IssuableSeverity!,
errors
issue {
iid
+ id
severity
}
}
diff --git a/app/assets/javascripts/sidebar/components/severity/sidebar_severity.vue b/app/assets/javascripts/sidebar/components/severity/sidebar_severity.vue
index 5dc93476120..86e46016534 100644
--- a/app/assets/javascripts/sidebar/components/severity/sidebar_severity.vue
+++ b/app/assets/javascripts/sidebar/components/severity/sidebar_severity.vue
@@ -5,7 +5,7 @@ import {
GlLoadingIcon,
GlTooltip,
GlSprintf,
- GlLink,
+ GlButton,
} from '@gitlab/ui';
import createFlash from '~/flash';
import { INCIDENT_SEVERITY, ISSUABLE_TYPES, I18N } from './constants';
@@ -20,7 +20,7 @@ export default {
GlSprintf,
GlDropdown,
GlDropdownItem,
- GlLink,
+ GlButton,
SeverityToken,
},
inject: ['canUpdate'],
@@ -150,23 +150,25 @@ export default {
<div class="hide-collapsed">
<p
- class="gl-line-height-20 gl-mb-0 gl-text-gray-900 gl-display-flex gl-justify-content-space-between"
+ class="gl-line-height-20 gl-mb-2 gl-text-gray-900 gl-display-flex gl-justify-content-space-between"
>
{{ $options.i18n.SEVERITY }}
- <gl-link
+ <gl-button
v-if="canUpdate"
+ category="tertiary"
+ size="small"
data-testid="editButton"
- href="#"
@click="toggleFormDropdown"
@keydown.esc="hideDropdown"
>
{{ $options.i18n.EDIT }}
- </gl-link>
+ </gl-button>
</p>
<gl-dropdown
:class="dropdownClass"
block
+ :header-text="__('Assign severity')"
:text="selectedItem.label"
toggle-class="dropdown-menu-toggle gl-mb-2"
@keydown.esc.native="hideDropdown"
diff --git a/app/assets/javascripts/sidebar/components/sidebar_dropdown_widget.vue b/app/assets/javascripts/sidebar/components/sidebar_dropdown_widget.vue
index 0ba8c4f8907..da792b3a2aa 100644
--- a/app/assets/javascripts/sidebar/components/sidebar_dropdown_widget.vue
+++ b/app/assets/javascripts/sidebar/components/sidebar_dropdown_widget.vue
@@ -12,11 +12,12 @@ import {
} from '@gitlab/ui';
import createFlash from '~/flash';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
-import { IssuableType } from '~/issue_show/constants';
+import { IssuableType } from '~/issues/constants';
import { timeFor } from '~/lib/utils/datetime_utility';
-import { __, s__, sprintf } from '~/locale';
+import { __ } from '~/locale';
import SidebarEditableItem from '~/sidebar/components/sidebar_editable_item.vue';
import {
+ dropdowni18nText,
Tracking,
IssuableAttributeState,
IssuableAttributeType,
@@ -24,14 +25,11 @@ import {
noAttributeId,
defaultEpicSort,
epicIidPattern,
-} from '~/sidebar/constants';
+} from 'ee_else_ce/sidebar/constants';
export default {
noAttributeId,
- IssuableAttributeState,
- issuableAttributesQueries,
i18n: {
- [IssuableAttributeType.Milestone]: __('Milestone'),
expired: __('(expired)'),
none: __('None'),
},
@@ -53,14 +51,24 @@ export default {
isClassicSidebar: {
default: false,
},
+ issuableAttributesQueries: {
+ default: issuableAttributesQueries,
+ },
+ issuableAttributesState: {
+ default: IssuableAttributeState,
+ },
+ widgetTitleText: {
+ default: {
+ [IssuableAttributeType.Milestone]: __('Milestone'),
+ expired: __('(expired)'),
+ none: __('None'),
+ },
+ },
},
props: {
issuableAttribute: {
type: String,
required: true,
- validator(value) {
- return [IssuableAttributeType.Milestone].includes(value);
- },
},
workspacePath: {
required: true,
@@ -132,13 +140,13 @@ export default {
return {
fullPath: this.attrWorkspacePath,
title: this.searchTerm,
- state: this.$options.IssuableAttributeState[this.issuableAttribute],
+ state: this.issuableAttributesState[this.issuableAttribute],
};
}
const variables = {
fullPath: this.attrWorkspacePath,
- state: this.$options.IssuableAttributeState[this.issuableAttribute],
+ state: this.issuableAttributesState[this.issuableAttribute],
sort: defaultEpicSort,
};
@@ -180,7 +188,7 @@ export default {
},
computed: {
issuableAttributeQuery() {
- return this.$options.issuableAttributesQueries[this.issuableAttribute];
+ return this.issuableAttributesQueries[this.issuableAttribute];
},
attributeTitle() {
return this.currentAttribute?.title || this.i18n.noAttribute;
@@ -189,9 +197,7 @@ export default {
return this.currentAttribute?.webUrl;
},
dropdownText() {
- return this.currentAttribute
- ? this.currentAttribute?.title
- : this.$options.i18n[this.issuableAttribute];
+ return this.currentAttribute ? this.currentAttribute?.title : this.attributeTypeTitle;
},
loading() {
return this.$apollo.queries.currentAttribute.loading;
@@ -200,7 +206,7 @@ export default {
return this.attributesList.length === 0;
},
attributeTypeTitle() {
- return this.$options.i18n[this.issuableAttribute];
+ return this.widgetTitleText[this.issuableAttribute];
},
attributeTypeIcon() {
return this.icon || this.issuableAttribute;
@@ -209,37 +215,10 @@ export default {
return timeFor(this.currentAttribute?.dueDate);
},
i18n() {
- return {
- noAttribute: sprintf(s__('DropdownWidget|No %{issuableAttribute}'), {
- issuableAttribute: this.issuableAttribute,
- }),
- assignAttribute: sprintf(s__('DropdownWidget|Assign %{issuableAttribute}'), {
- issuableAttribute: this.issuableAttribute,
- }),
- noAttributesFound: sprintf(s__('DropdownWidget|No %{issuableAttribute} found'), {
- issuableAttribute: this.issuableAttribute,
- }),
- updateError: sprintf(
- s__(
- 'DropdownWidget|Failed to set %{issuableAttribute} on this %{issuableType}. Please try again.',
- ),
- { issuableAttribute: this.issuableAttribute, issuableType: this.issuableType },
- ),
- listFetchError: sprintf(
- s__(
- 'DropdownWidget|Failed to fetch the %{issuableAttribute} for this %{issuableType}. Please try again.',
- ),
- { issuableAttribute: this.issuableAttribute, issuableType: this.issuableType },
- ),
- currentFetchError: sprintf(
- s__(
- 'DropdownWidget|An error occurred while fetching the assigned %{issuableAttribute} of the selected %{issuableType}.',
- ),
- { issuableAttribute: this.issuableAttribute, issuableType: this.issuableType },
- ),
- };
+ return dropdowni18nText(this.issuableAttribute, this.issuableType);
},
isEpic() {
+ // MV to EE https://gitlab.com/gitlab-org/gitlab/-/issues/345311
return this.issuableAttribute === IssuableType.Epic;
},
},
@@ -252,7 +231,7 @@ export default {
const selectedAttribute =
Boolean(attributeId) && this.attributesList.find((p) => p.id === attributeId);
- this.selectedTitle = selectedAttribute ? selectedAttribute.title : this.$options.i18n.none;
+ this.selectedTitle = selectedAttribute ? selectedAttribute.title : this.widgetTitleText.none;
const { current } = this.issuableAttributeQuery;
const { mutation } = current[this.issuableType];
diff --git a/app/assets/javascripts/sidebar/components/subscriptions/sidebar_subscriptions_widget.vue b/app/assets/javascripts/sidebar/components/subscriptions/sidebar_subscriptions_widget.vue
index bc7e377a966..701833c4e95 100644
--- a/app/assets/javascripts/sidebar/components/subscriptions/sidebar_subscriptions_widget.vue
+++ b/app/assets/javascripts/sidebar/components/subscriptions/sidebar_subscriptions_widget.vue
@@ -1,7 +1,7 @@
<script>
import { GlIcon, GlLoadingIcon, GlToggle, GlTooltipDirective } from '@gitlab/ui';
import createFlash from '~/flash';
-import { IssuableType } from '~/issue_show/constants';
+import { IssuableType } from '~/issues/constants';
import { isLoggedIn } from '~/lib/utils/common_utils';
import { __, sprintf } from '~/locale';
import SidebarEditableItem from '~/sidebar/components/sidebar_editable_item.vue';
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 9a9d03353dc..91c67a03dfb 100644
--- a/app/assets/javascripts/sidebar/components/time_tracking/time_tracker.vue
+++ b/app/assets/javascripts/sidebar/components/time_tracking/time_tracker.vue
@@ -1,6 +1,6 @@
<script>
import { GlIcon, GlLink, GlModal, GlModalDirective, GlLoadingIcon } from '@gitlab/ui';
-import { IssuableType } from '~/issue_show/constants';
+import { IssuableType } from '~/issues/constants';
import { s__, __ } from '~/locale';
import { timeTrackingQueries } from '~/sidebar/constants';
diff --git a/app/assets/javascripts/sidebar/constants.js b/app/assets/javascripts/sidebar/constants.js
index ac34a75ac5c..0238fb8e8d5 100644
--- a/app/assets/javascripts/sidebar/constants.js
+++ b/app/assets/javascripts/sidebar/constants.js
@@ -1,5 +1,6 @@
+import { s__, sprintf } from '~/locale';
import updateIssueLabelsMutation from '~/boards/graphql/issue_set_labels.mutation.graphql';
-import { IssuableType, WorkspaceType } from '~/issue_show/constants';
+import { IssuableType, WorkspaceType } from '~/issues/constants';
import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
import epicConfidentialQuery from '~/sidebar/queries/epic_confidential.query.graphql';
import epicDueDateQuery from '~/sidebar/queries/epic_due_date.query.graphql';
@@ -272,3 +273,35 @@ export const todoMutations = {
[TodoMutationTypes.Create]: todoCreateMutation,
[TodoMutationTypes.MarkDone]: todoMarkDoneMutation,
};
+
+export function dropdowni18nText(issuableAttribute, issuableType) {
+ return {
+ noAttribute: sprintf(s__('DropdownWidget|No %{issuableAttribute}'), {
+ issuableAttribute,
+ }),
+ assignAttribute: sprintf(s__('DropdownWidget|Assign %{issuableAttribute}'), {
+ issuableAttribute,
+ }),
+ noAttributesFound: sprintf(s__('DropdownWidget|No %{issuableAttribute} found'), {
+ issuableAttribute,
+ }),
+ updateError: sprintf(
+ s__(
+ 'DropdownWidget|Failed to set %{issuableAttribute} on this %{issuableType}. Please try again.',
+ ),
+ { issuableAttribute, issuableType },
+ ),
+ listFetchError: sprintf(
+ s__(
+ 'DropdownWidget|Failed to fetch the %{issuableAttribute} for this %{issuableType}. Please try again.',
+ ),
+ { issuableAttribute, issuableType },
+ ),
+ currentFetchError: sprintf(
+ s__(
+ 'DropdownWidget|An error occurred while fetching the assigned %{issuableAttribute} of the selected %{issuableType}.',
+ ),
+ { issuableAttribute, issuableType },
+ ),
+ };
+}
diff --git a/app/assets/javascripts/sidebar/graphql.js b/app/assets/javascripts/sidebar/graphql.js
index 6a670db2d38..5b2ce3fe446 100644
--- a/app/assets/javascripts/sidebar/graphql.js
+++ b/app/assets/javascripts/sidebar/graphql.js
@@ -1,7 +1,7 @@
import { IntrospectionFragmentMatcher } from 'apollo-cache-inmemory';
import produce from 'immer';
import VueApollo from 'vue-apollo';
-import getIssueStateQuery from '~/issue_show/queries/get_issue_state.query.graphql';
+import getIssueStateQuery from '~/issues/show/queries/get_issue_state.query.graphql';
import createDefaultClient from '~/lib/graphql';
import introspectionQueryResultData from './fragmentTypes.json';
diff --git a/app/assets/javascripts/sidebar/mount_milestone_sidebar.js b/app/assets/javascripts/sidebar/mount_milestone_sidebar.js
index 270b22fcdf9..1947c4801db 100644
--- a/app/assets/javascripts/sidebar/mount_milestone_sidebar.js
+++ b/app/assets/javascripts/sidebar/mount_milestone_sidebar.js
@@ -1,5 +1,5 @@
import Vue from 'vue';
-import { IssuableType } from '~/issue_show/constants';
+import { IssuableType } from '~/issues/constants';
import { parseBoolean } from '~/lib/utils/common_utils';
import timeTracker from './components/time_tracking/time_tracker.vue';
diff --git a/app/assets/javascripts/sidebar/mount_sidebar.js b/app/assets/javascripts/sidebar/mount_sidebar.js
index 898be4a97ce..cbe40d0bfbe 100644
--- a/app/assets/javascripts/sidebar/mount_sidebar.js
+++ b/app/assets/javascripts/sidebar/mount_sidebar.js
@@ -5,13 +5,14 @@ import { TYPE_ISSUE, TYPE_MERGE_REQUEST } from '~/graphql_shared/constants';
import { convertToGraphQLId } from '~/graphql_shared/utils';
import initInviteMembersModal from '~/invite_members/init_invite_members_modal';
import initInviteMembersTrigger from '~/invite_members/init_invite_members_trigger';
-import { IssuableType } from '~/issue_show/constants';
+import { IssuableType } from '~/issues/constants';
import {
isInIssuePage,
isInDesignPage,
isInIncidentPage,
parseBoolean,
} from '~/lib/utils/common_utils';
+import { __ } from '~/locale';
import CollapsedAssigneeList from '~/sidebar/components/assignees/collapsed_assignee_list.vue';
import SidebarAssigneesWidget from '~/sidebar/components/assignees/sidebar_assignees_widget.vue';
import SidebarConfidentialityWidget from '~/sidebar/components/confidential/sidebar_confidentiality_widget.vue';
@@ -23,10 +24,11 @@ import SidebarTodoWidget from '~/sidebar/components/todo_toggle/sidebar_todo_wid
import { apolloProvider } from '~/sidebar/graphql';
import trackShowInviteMemberLink from '~/sidebar/track_invite_members';
import { DropdownVariant } from '~/vue_shared/components/sidebar/labels_select_vue/constants';
+import LabelsSelectWidget from '~/vue_shared/components/sidebar/labels_select_widget/labels_select_root.vue';
+import { LabelType } from '~/vue_shared/components/sidebar/labels_select_widget/constants';
import Translate from '../vue_shared/translate';
import SidebarAssignees from './components/assignees/sidebar_assignees.vue';
import CopyEmailToClipboard from './components/copy_email_to_clipboard.vue';
-import SidebarLabels from './components/labels/sidebar_labels.vue';
import IssuableLockForm from './components/lock/issuable_lock_form.vue';
import SidebarReviewers from './components/reviewers/sidebar_reviewers.vue';
import SidebarSeverity from './components/severity/sidebar_severity.vue';
@@ -34,6 +36,7 @@ import SidebarSubscriptionsWidget from './components/subscriptions/sidebar_subsc
import SidebarTimeTracking from './components/time_tracking/sidebar_time_tracking.vue';
import { IssuableAttributeType } from './constants';
import SidebarMoveIssue from './lib/sidebar_move_issue';
+import CrmContacts from './components/crm_contacts/crm_contacts.vue';
Vue.use(Translate);
Vue.use(VueApollo);
@@ -205,6 +208,28 @@ function mountReviewersComponent(mediator) {
}
}
+function mountCrmContactsComponent() {
+ const el = document.getElementById('js-issue-crm-contacts');
+
+ if (!el) return;
+
+ const { issueId } = el.dataset;
+ // eslint-disable-next-line no-new
+ new Vue({
+ el,
+ apolloProvider,
+ components: {
+ CrmContacts,
+ },
+ render: (createElement) =>
+ createElement('crm-contacts', {
+ props: {
+ issueId,
+ },
+ }),
+ });
+}
+
function mountMilestoneSelect() {
const el = document.querySelector('.js-milestone-select');
@@ -241,7 +266,6 @@ function mountMilestoneSelect() {
export function mountSidebarLabels() {
const el = document.querySelector('.js-sidebar-labels');
- const { fullPath } = getSidebarOptions();
if (!el) {
return false;
@@ -250,22 +274,43 @@ export function mountSidebarLabels() {
return new Vue({
el,
apolloProvider,
+
+ components: {
+ LabelsSelectWidget,
+ },
provide: {
...el.dataset,
- fullPath,
+ canUpdate: parseBoolean(el.dataset.canEdit),
allowLabelCreate: parseBoolean(el.dataset.allowLabelCreate),
allowLabelEdit: parseBoolean(el.dataset.canEdit),
allowScopedLabels: parseBoolean(el.dataset.allowScopedLabels),
- initiallySelectedLabels: JSON.parse(el.dataset.selectedLabels),
- variant: DropdownVariant.Sidebar,
- canUpdate: parseBoolean(el.dataset.canEdit),
isClassicSidebar: true,
- issuableType:
- isInIssuePage() || isInIncidentPage() || isInDesignPage()
- ? IssuableType.Issue
- : IssuableType.MergeRequest,
},
- render: (createElement) => createElement(SidebarLabels),
+ render: (createElement) =>
+ createElement('labels-select-widget', {
+ props: {
+ iid: String(el.dataset.iid),
+ fullPath: el.dataset.projectPath,
+ allowLabelRemove: parseBoolean(el.dataset.canEdit),
+ allowMultiselect: true,
+ footerCreateLabelTitle: __('Create project label'),
+ footerManageLabelTitle: __('Manage project labels'),
+ labelsCreateTitle: __('Create project label'),
+ labelsFilterBasePath: el.dataset.projectIssuesPath,
+ variant: DropdownVariant.Sidebar,
+ issuableType:
+ isInIssuePage() || isInIncidentPage() || isInDesignPage()
+ ? IssuableType.Issue
+ : IssuableType.MergeRequest,
+ workspaceType: 'project',
+ attrWorkspacePath: el.dataset.projectPath,
+ labelCreateType: LabelType.project,
+ },
+ class: ['block labels js-labels-block'],
+ scopedSlots: {
+ default: () => __('None'),
+ },
+ }),
});
}
@@ -535,6 +580,7 @@ export function mountSidebar(mediator, store) {
mountAssigneesComponentDeprecated(mediator);
}
mountReviewersComponent(mediator);
+ mountCrmContactsComponent();
mountSidebarLabels();
mountMilestoneSelect();
mountConfidentialComponent(mediator);
diff --git a/app/assets/javascripts/sidebar/queries/epic_confidential.query.graphql b/app/assets/javascripts/sidebar/queries/epic_confidential.query.graphql
index 7a1fdb40e93..4998b2af666 100644
--- a/app/assets/javascripts/sidebar/queries/epic_confidential.query.graphql
+++ b/app/assets/javascripts/sidebar/queries/epic_confidential.query.graphql
@@ -1,6 +1,7 @@
query epicConfidential($fullPath: ID!, $iid: ID) {
workspace: group(fullPath: $fullPath) {
__typename
+ id
issuable: epic(iid: $iid) {
__typename
id
diff --git a/app/assets/javascripts/sidebar/queries/epic_due_date.query.graphql b/app/assets/javascripts/sidebar/queries/epic_due_date.query.graphql
index f60f44abebd..00529042e92 100644
--- a/app/assets/javascripts/sidebar/queries/epic_due_date.query.graphql
+++ b/app/assets/javascripts/sidebar/queries/epic_due_date.query.graphql
@@ -1,6 +1,7 @@
query epicDueDate($fullPath: ID!, $iid: ID) {
workspace: group(fullPath: $fullPath) {
__typename
+ id
issuable: epic(iid: $iid) {
__typename
id
diff --git a/app/assets/javascripts/sidebar/queries/epic_participants.query.graphql b/app/assets/javascripts/sidebar/queries/epic_participants.query.graphql
index fbebc50ab08..dada7ffc034 100644
--- a/app/assets/javascripts/sidebar/queries/epic_participants.query.graphql
+++ b/app/assets/javascripts/sidebar/queries/epic_participants.query.graphql
@@ -4,6 +4,7 @@
query epicParticipants($fullPath: ID!, $iid: ID) {
workspace: group(fullPath: $fullPath) {
__typename
+ id
issuable: epic(iid: $iid) {
__typename
id
diff --git a/app/assets/javascripts/sidebar/queries/epic_reference.query.graphql b/app/assets/javascripts/sidebar/queries/epic_reference.query.graphql
index bd10f09aed8..f35ca896ef8 100644
--- a/app/assets/javascripts/sidebar/queries/epic_reference.query.graphql
+++ b/app/assets/javascripts/sidebar/queries/epic_reference.query.graphql
@@ -1,6 +1,7 @@
query epicReference($fullPath: ID!, $iid: ID) {
workspace: group(fullPath: $fullPath) {
__typename
+ id
issuable: epic(iid: $iid) {
__typename
id
diff --git a/app/assets/javascripts/sidebar/queries/epic_start_date.query.graphql b/app/assets/javascripts/sidebar/queries/epic_start_date.query.graphql
index c6c24fd3d95..85fc7de8d02 100644
--- a/app/assets/javascripts/sidebar/queries/epic_start_date.query.graphql
+++ b/app/assets/javascripts/sidebar/queries/epic_start_date.query.graphql
@@ -1,6 +1,7 @@
query epicStartDate($fullPath: ID!, $iid: ID) {
workspace: group(fullPath: $fullPath) {
__typename
+ id
issuable: epic(iid: $iid) {
__typename
id
diff --git a/app/assets/javascripts/sidebar/queries/epic_subscribed.query.graphql b/app/assets/javascripts/sidebar/queries/epic_subscribed.query.graphql
index 9f1967e1685..a8fe6b8ddc3 100644
--- a/app/assets/javascripts/sidebar/queries/epic_subscribed.query.graphql
+++ b/app/assets/javascripts/sidebar/queries/epic_subscribed.query.graphql
@@ -1,6 +1,7 @@
query epicSubscribed($fullPath: ID!, $iid: ID) {
workspace: group(fullPath: $fullPath) {
__typename
+ id
emailsDisabled
issuable: epic(iid: $iid) {
__typename
diff --git a/app/assets/javascripts/sidebar/queries/epic_todo.query.graphql b/app/assets/javascripts/sidebar/queries/epic_todo.query.graphql
index 1e6f9bad5b2..b0ba724e727 100644
--- a/app/assets/javascripts/sidebar/queries/epic_todo.query.graphql
+++ b/app/assets/javascripts/sidebar/queries/epic_todo.query.graphql
@@ -1,6 +1,7 @@
query epicTodos($fullPath: ID!, $iid: ID) {
workspace: group(fullPath: $fullPath) {
__typename
+ id
issuable: epic(iid: $iid) {
__typename
id
diff --git a/app/assets/javascripts/sidebar/queries/issuable_assignees.subscription.graphql b/app/assets/javascripts/sidebar/queries/issuable_assignees.subscription.graphql
index 47ce094418c..a58a04d87c4 100644
--- a/app/assets/javascripts/sidebar/queries/issuable_assignees.subscription.graphql
+++ b/app/assets/javascripts/sidebar/queries/issuable_assignees.subscription.graphql
@@ -3,6 +3,7 @@
subscription issuableAssigneesUpdated($issuableId: IssuableID!) {
issuableAssigneesUpdated(issuableId: $issuableId) {
... on Issue {
+ id
assignees {
nodes {
...User
diff --git a/app/assets/javascripts/sidebar/queries/issue_confidential.query.graphql b/app/assets/javascripts/sidebar/queries/issue_confidential.query.graphql
index 92cabf46af7..e578cf3bda5 100644
--- a/app/assets/javascripts/sidebar/queries/issue_confidential.query.graphql
+++ b/app/assets/javascripts/sidebar/queries/issue_confidential.query.graphql
@@ -1,6 +1,7 @@
query issueConfidential($fullPath: ID!, $iid: String) {
workspace: project(fullPath: $fullPath) {
__typename
+ id
issuable: issue(iid: $iid) {
__typename
id
diff --git a/app/assets/javascripts/sidebar/queries/issue_due_date.query.graphql b/app/assets/javascripts/sidebar/queries/issue_due_date.query.graphql
index 6d3f782bd0a..48cbff252b3 100644
--- a/app/assets/javascripts/sidebar/queries/issue_due_date.query.graphql
+++ b/app/assets/javascripts/sidebar/queries/issue_due_date.query.graphql
@@ -1,6 +1,7 @@
query issueDueDate($fullPath: ID!, $iid: String) {
workspace: project(fullPath: $fullPath) {
__typename
+ id
issuable: issue(iid: $iid) {
__typename
id
diff --git a/app/assets/javascripts/sidebar/queries/issue_reference.query.graphql b/app/assets/javascripts/sidebar/queries/issue_reference.query.graphql
index db4f58a4f69..c3128d6d961 100644
--- a/app/assets/javascripts/sidebar/queries/issue_reference.query.graphql
+++ b/app/assets/javascripts/sidebar/queries/issue_reference.query.graphql
@@ -1,5 +1,6 @@
query issueReference($fullPath: ID!, $iid: String) {
workspace: project(fullPath: $fullPath) {
+ id
__typename
issuable: issue(iid: $iid) {
__typename
diff --git a/app/assets/javascripts/sidebar/queries/issue_subscribed.query.graphql b/app/assets/javascripts/sidebar/queries/issue_subscribed.query.graphql
index 7d38b5d3bd8..e2722fc86a4 100644
--- a/app/assets/javascripts/sidebar/queries/issue_subscribed.query.graphql
+++ b/app/assets/javascripts/sidebar/queries/issue_subscribed.query.graphql
@@ -1,6 +1,7 @@
query issueSubscribed($fullPath: ID!, $iid: String) {
workspace: project(fullPath: $fullPath) {
__typename
+ id
issuable: issue(iid: $iid) {
__typename
id
diff --git a/app/assets/javascripts/sidebar/queries/issue_time_tracking.query.graphql b/app/assets/javascripts/sidebar/queries/issue_time_tracking.query.graphql
index 7ac989b5c63..059361dd370 100644
--- a/app/assets/javascripts/sidebar/queries/issue_time_tracking.query.graphql
+++ b/app/assets/javascripts/sidebar/queries/issue_time_tracking.query.graphql
@@ -1,6 +1,7 @@
query issueTimeTracking($fullPath: ID!, $iid: String) {
workspace: project(fullPath: $fullPath) {
__typename
+ id
issuable: issue(iid: $iid) {
__typename
id
diff --git a/app/assets/javascripts/sidebar/queries/issue_todo.query.graphql b/app/assets/javascripts/sidebar/queries/issue_todo.query.graphql
index 783d36352fe..5cd5d81c439 100644
--- a/app/assets/javascripts/sidebar/queries/issue_todo.query.graphql
+++ b/app/assets/javascripts/sidebar/queries/issue_todo.query.graphql
@@ -1,6 +1,7 @@
query issueTodos($fullPath: ID!, $iid: String!) {
workspace: project(fullPath: $fullPath) {
__typename
+ id
issuable: issue(iid: $iid) {
__typename
id
diff --git a/app/assets/javascripts/sidebar/queries/merge_request_milestone.query.graphql b/app/assets/javascripts/sidebar/queries/merge_request_milestone.query.graphql
index 5c0edf5acee..b0a16677cf2 100644
--- a/app/assets/javascripts/sidebar/queries/merge_request_milestone.query.graphql
+++ b/app/assets/javascripts/sidebar/queries/merge_request_milestone.query.graphql
@@ -3,6 +3,7 @@
query mergeRequestMilestone($fullPath: ID!, $iid: String!) {
workspace: project(fullPath: $fullPath) {
__typename
+ id
issuable: mergeRequest(iid: $iid) {
__typename
id
diff --git a/app/assets/javascripts/sidebar/queries/merge_request_reference.query.graphql b/app/assets/javascripts/sidebar/queries/merge_request_reference.query.graphql
index 7979a1ccb3e..7c78f812b67 100644
--- a/app/assets/javascripts/sidebar/queries/merge_request_reference.query.graphql
+++ b/app/assets/javascripts/sidebar/queries/merge_request_reference.query.graphql
@@ -1,6 +1,7 @@
query mergeRequestReference($fullPath: ID!, $iid: String!) {
workspace: project(fullPath: $fullPath) {
__typename
+ id
issuable: mergeRequest(iid: $iid) {
__typename
id
diff --git a/app/assets/javascripts/sidebar/queries/merge_request_subscribed.query.graphql b/app/assets/javascripts/sidebar/queries/merge_request_subscribed.query.graphql
index 3b54a2e529b..d5e27ca7b69 100644
--- a/app/assets/javascripts/sidebar/queries/merge_request_subscribed.query.graphql
+++ b/app/assets/javascripts/sidebar/queries/merge_request_subscribed.query.graphql
@@ -1,6 +1,7 @@
query mergeRequestSubscribed($fullPath: ID!, $iid: String!) {
workspace: project(fullPath: $fullPath) {
__typename
+ id
issuable: mergeRequest(iid: $iid) {
__typename
id
diff --git a/app/assets/javascripts/sidebar/queries/merge_request_time_tracking.query.graphql b/app/assets/javascripts/sidebar/queries/merge_request_time_tracking.query.graphql
index b1ab1bcbe87..d480ff3d5ba 100644
--- a/app/assets/javascripts/sidebar/queries/merge_request_time_tracking.query.graphql
+++ b/app/assets/javascripts/sidebar/queries/merge_request_time_tracking.query.graphql
@@ -1,6 +1,7 @@
query mergeRequestTimeTracking($fullPath: ID!, $iid: String!) {
workspace: project(fullPath: $fullPath) {
__typename
+ id
issuable: mergeRequest(iid: $iid) {
__typename
id
diff --git a/app/assets/javascripts/sidebar/queries/merge_request_todo.query.graphql b/app/assets/javascripts/sidebar/queries/merge_request_todo.query.graphql
index 93a1c9ea925..65b9ef45260 100644
--- a/app/assets/javascripts/sidebar/queries/merge_request_todo.query.graphql
+++ b/app/assets/javascripts/sidebar/queries/merge_request_todo.query.graphql
@@ -1,6 +1,7 @@
query mergeRequestTodos($fullPath: ID!, $iid: String!) {
workspace: project(fullPath: $fullPath) {
__typename
+ id
issuable: mergeRequest(iid: $iid) {
__typename
id
diff --git a/app/assets/javascripts/sidebar/queries/project_issue_milestone.query.graphql b/app/assets/javascripts/sidebar/queries/project_issue_milestone.query.graphql
index 2bc42a0b011..c7f3adc9aca 100644
--- a/app/assets/javascripts/sidebar/queries/project_issue_milestone.query.graphql
+++ b/app/assets/javascripts/sidebar/queries/project_issue_milestone.query.graphql
@@ -3,6 +3,7 @@
query projectIssueMilestone($fullPath: ID!, $iid: String!) {
workspace: project(fullPath: $fullPath) {
__typename
+ id
issuable: issue(iid: $iid) {
__typename
id
diff --git a/app/assets/javascripts/sidebar/queries/project_milestones.query.graphql b/app/assets/javascripts/sidebar/queries/project_milestones.query.graphql
index a3ab1ebc872..d9eab18628d 100644
--- a/app/assets/javascripts/sidebar/queries/project_milestones.query.graphql
+++ b/app/assets/javascripts/sidebar/queries/project_milestones.query.graphql
@@ -3,6 +3,7 @@
query projectMilestones($fullPath: ID!, $title: String, $state: MilestoneStateEnum) {
workspace: project(fullPath: $fullPath) {
__typename
+ id
attributes: milestones(
searchTitle: $title
state: $state
diff --git a/app/assets/javascripts/sidebar/queries/sidebarDetails.query.graphql b/app/assets/javascripts/sidebar/queries/sidebarDetails.query.graphql
index dd85eb1631b..90d1a7794ea 100644
--- a/app/assets/javascripts/sidebar/queries/sidebarDetails.query.graphql
+++ b/app/assets/javascripts/sidebar/queries/sidebarDetails.query.graphql
@@ -1,6 +1,8 @@
query sidebarDetails($fullPath: ID!, $iid: String!) {
project(fullPath: $fullPath) {
+ id
issue(iid: $iid) {
+ id
iid
}
}
diff --git a/app/assets/javascripts/sidebar/queries/sidebarDetailsMR.query.graphql b/app/assets/javascripts/sidebar/queries/sidebarDetailsMR.query.graphql
index 02498b18832..0505f88773d 100644
--- a/app/assets/javascripts/sidebar/queries/sidebarDetailsMR.query.graphql
+++ b/app/assets/javascripts/sidebar/queries/sidebarDetailsMR.query.graphql
@@ -1,6 +1,8 @@
query mergeRequestSidebarDetails($fullPath: ID!, $iid: String!) {
project(fullPath: $fullPath) {
+ id
mergeRequest(iid: $iid) {
+ id
iid # currently unused.
}
}
diff --git a/app/assets/javascripts/sidebar/queries/update_epic_title.mutation.graphql b/app/assets/javascripts/sidebar/queries/update_epic_title.mutation.graphql
index 2e6bc8c36ba..809cb2c9f76 100644
--- a/app/assets/javascripts/sidebar/queries/update_epic_title.mutation.graphql
+++ b/app/assets/javascripts/sidebar/queries/update_epic_title.mutation.graphql
@@ -1,6 +1,7 @@
mutation updateEpicTitle($input: UpdateEpicInput!) {
updateIssuableTitle: updateEpic(input: $input) {
epic {
+ id
title
}
errors
diff --git a/app/assets/javascripts/sidebar/queries/update_merge_request_labels.mutation.graphql b/app/assets/javascripts/sidebar/queries/update_merge_request_labels.mutation.graphql
index 016c31ea096..a48c9e96fc2 100644
--- a/app/assets/javascripts/sidebar/queries/update_merge_request_labels.mutation.graphql
+++ b/app/assets/javascripts/sidebar/queries/update_merge_request_labels.mutation.graphql
@@ -1,7 +1,7 @@
mutation mergeRequestSetLabels($input: MergeRequestSetLabelsInput!) {
- mergeRequestSetLabels(input: $input) {
+ updateIssuableLabels: mergeRequestSetLabels(input: $input) {
errors
- mergeRequest {
+ issuable: mergeRequest {
id
labels {
nodes {
diff --git a/app/assets/javascripts/sidebar/sidebar_mediator.js b/app/assets/javascripts/sidebar/sidebar_mediator.js
index 86580744ccc..a49ddac8c89 100644
--- a/app/assets/javascripts/sidebar/sidebar_mediator.js
+++ b/app/assets/javascripts/sidebar/sidebar_mediator.js
@@ -79,6 +79,20 @@ export default class SidebarMediator {
}),
);
} else {
+ const currentUserId = gon.current_user_id;
+
+ if (currentUserId !== user.id) {
+ const currentUserReviewerOrAssignee = isReviewer
+ ? this.store.findReviewer({ id: currentUserId })
+ : this.store.findAssignee({ id: currentUserId });
+
+ if (currentUserReviewerOrAssignee?.attention_requested) {
+ // Update current users attention_requested state
+ this.store.updateReviewer(currentUserId, 'attention_requested');
+ this.store.updateAssignee(currentUserId, 'attention_requested');
+ }
+ }
+
toast(sprintf(__('Requested attention from @%{username}'), { username: user.username }));
}
diff --git a/app/assets/javascripts/snippets/components/edit.vue b/app/assets/javascripts/snippets/components/edit.vue
index f07fb9d926a..e3aa29d5f89 100644
--- a/app/assets/javascripts/snippets/components/edit.vue
+++ b/app/assets/javascripts/snippets/components/edit.vue
@@ -230,7 +230,7 @@ export default {
<gl-button
category="primary"
type="submit"
- variant="success"
+ variant="confirm"
:disabled="updatePrevented"
data-qa-selector="submit_button"
data-testid="snippet-submit-btn"
diff --git a/app/assets/javascripts/snippets/components/snippet_blob_view.vue b/app/assets/javascripts/snippets/components/snippet_blob_view.vue
index 8481ac2b9c9..86cbc2c31b3 100644
--- a/app/assets/javascripts/snippets/components/snippet_blob_view.vue
+++ b/app/assets/javascripts/snippets/components/snippet_blob_view.vue
@@ -90,7 +90,7 @@ export default {
};
</script>
<template>
- <article class="file-holder snippet-file-content">
+ <figure class="file-holder snippet-file-content" :aria-label="__('Code snippet')">
<blob-header
:blob="blob"
:active-viewer-type="viewer.type"
@@ -105,5 +105,5 @@ export default {
@[$options.BLOB_RENDER_EVENT_LOAD]="forceQuery"
@[$options.BLOB_RENDER_EVENT_SHOW_SOURCE]="switchViewer"
/>
- </article>
+ </figure>
</template>
diff --git a/app/assets/javascripts/snippets/components/snippet_header.vue b/app/assets/javascripts/snippets/components/snippet_header.vue
index a5c98a7ad90..9b24c8afe37 100644
--- a/app/assets/javascripts/snippets/components/snippet_header.vue
+++ b/app/assets/javascripts/snippets/components/snippet_header.vue
@@ -113,7 +113,7 @@ export default {
href: this.snippet.project
? joinPaths(this.snippet.project.webUrl, '-/snippets/new')
: joinPaths('/', gon.relative_url_root, '/-/snippets/new'),
- variant: 'success',
+ variant: 'confirm',
category: 'secondary',
},
{
diff --git a/app/assets/javascripts/snippets/fragments/project.fragment.graphql b/app/assets/javascripts/snippets/fragments/project.fragment.graphql
deleted file mode 100644
index 64bb2315c1b..00000000000
--- a/app/assets/javascripts/snippets/fragments/project.fragment.graphql
+++ /dev/null
@@ -1,6 +0,0 @@
-fragment SnippetProject on Snippet {
- project {
- fullPath
- webUrl
- }
-}
diff --git a/app/assets/javascripts/snippets/mutations/createSnippet.mutation.graphql b/app/assets/javascripts/snippets/mutations/createSnippet.mutation.graphql
index f688868d1b9..8640c4725f4 100644
--- a/app/assets/javascripts/snippets/mutations/createSnippet.mutation.graphql
+++ b/app/assets/javascripts/snippets/mutations/createSnippet.mutation.graphql
@@ -2,6 +2,7 @@ mutation CreateSnippet($input: CreateSnippetInput!) {
createSnippet(input: $input) {
errors
snippet {
+ id
webUrl
}
}
diff --git a/app/assets/javascripts/snippets/mutations/updateSnippet.mutation.graphql b/app/assets/javascripts/snippets/mutations/updateSnippet.mutation.graphql
index 548725f7357..99242c5d500 100644
--- a/app/assets/javascripts/snippets/mutations/updateSnippet.mutation.graphql
+++ b/app/assets/javascripts/snippets/mutations/updateSnippet.mutation.graphql
@@ -2,6 +2,7 @@ mutation UpdateSnippet($input: UpdateSnippetInput!) {
updateSnippet(input: $input) {
errors
snippet {
+ id
webUrl
}
}
diff --git a/app/assets/javascripts/static_site_editor/graphql/queries/source_content.query.graphql b/app/assets/javascripts/static_site_editor/graphql/queries/source_content.query.graphql
index cfe30c601ed..c8c4195e1cd 100644
--- a/app/assets/javascripts/static_site_editor/graphql/queries/source_content.query.graphql
+++ b/app/assets/javascripts/static_site_editor/graphql/queries/source_content.query.graphql
@@ -1,5 +1,6 @@
query sourceContent($project: ID!, $sourcePath: String!) {
project(fullPath: $project) {
+ id
fullPath
file(path: $sourcePath) @client {
title
diff --git a/app/assets/javascripts/tabs/constants.js b/app/assets/javascripts/tabs/constants.js
new file mode 100644
index 00000000000..3b84d7394d4
--- /dev/null
+++ b/app/assets/javascripts/tabs/constants.js
@@ -0,0 +1,20 @@
+export const ACTIVE_TAB_CLASSES = Object.freeze([
+ 'active',
+ 'gl-tab-nav-item-active',
+ 'gl-tab-nav-item-active-indigo',
+]);
+
+export const ACTIVE_PANEL_CLASS = 'active';
+
+export const KEY_CODE_LEFT = 'ArrowLeft';
+export const KEY_CODE_UP = 'ArrowUp';
+export const KEY_CODE_RIGHT = 'ArrowRight';
+export const KEY_CODE_DOWN = 'ArrowDown';
+
+export const ATTR_ARIA_CONTROLS = 'aria-controls';
+export const ATTR_ARIA_LABELLEDBY = 'aria-labelledby';
+export const ATTR_ARIA_SELECTED = 'aria-selected';
+export const ATTR_ROLE = 'role';
+export const ATTR_TABINDEX = 'tabindex';
+
+export const TAB_SHOWN_EVENT = 'gl-tab-shown';
diff --git a/app/assets/javascripts/tabs/index.js b/app/assets/javascripts/tabs/index.js
new file mode 100644
index 00000000000..44937e593e0
--- /dev/null
+++ b/app/assets/javascripts/tabs/index.js
@@ -0,0 +1,239 @@
+import { uniqueId } from 'lodash';
+import {
+ ACTIVE_TAB_CLASSES,
+ ATTR_ROLE,
+ ATTR_ARIA_CONTROLS,
+ ATTR_TABINDEX,
+ ATTR_ARIA_SELECTED,
+ ATTR_ARIA_LABELLEDBY,
+ ACTIVE_PANEL_CLASS,
+ KEY_CODE_LEFT,
+ KEY_CODE_UP,
+ KEY_CODE_RIGHT,
+ KEY_CODE_DOWN,
+ TAB_SHOWN_EVENT,
+} from './constants';
+
+export { TAB_SHOWN_EVENT };
+
+/**
+ * The `GlTabsBehavior` class adds interactivity to tabs created by the `gl_tabs_nav` and
+ * `gl_tab_link_to` Rails helpers.
+ *
+ * Example using `href` references:
+ *
+ * ```haml
+ * = gl_tabs_nav({ class: 'js-my-tabs' }) do
+ * = gl_tab_link_to '#foo', item_active: true do
+ * = _('Foo')
+ * = gl_tab_link_to '#bar' do
+ * = _('Bar')
+ *
+ * .tab-content
+ * .tab-pane.active#foo
+ * .tab-pane#bar
+ * ```
+ *
+ * ```javascript
+ * import { GlTabsBehavior } from '~/tabs';
+ *
+ * const glTabs = new GlTabsBehavior(document.querySelector('.js-my-tabs'));
+ * ```
+ *
+ * Example using `aria-controls` references:
+ *
+ * ```haml
+ * = gl_tabs_nav({ class: 'js-my-tabs' }) do
+ * = gl_tab_link_to '#', item_active: true, 'aria-controls': 'foo' do
+ * = _('Foo')
+ * = gl_tab_link_to '#', 'aria-controls': 'bar' do
+ * = _('Bar')
+ *
+ * .tab-content
+ * .tab-pane.active#foo
+ * .tab-pane#bar
+ * ```
+ *
+ * ```javascript
+ * import { GlTabsBehavior } from '~/tabs';
+ *
+ * const glTabs = new GlTabsBehavior(document.querySelector('.js-my-tabs'));
+ * ```
+ *
+ * `GlTabsBehavior` can be used to replace Bootstrap tab implementations that cannot
+ * easily be rewritten in Vue.
+ *
+ * NOTE: Do *not* use `GlTabsBehavior` with markup generated by other means, as it may not
+ * work correctly.
+ *
+ * Tab panels must exist somewhere in the page for the tabs to control. Tab panels
+ * must:
+ * - be immediate children of a `.tab-content` element
+ * - have the `tab-pane` class
+ * - if the panel is active, have the `active` class
+ * - have a unique `id` attribute
+ *
+ * In order to associate tabs with panels, the tabs must reference their panel's
+ * `id` by having one of the following attributes:
+ * - `href`, e.g., `href="#the-panel-id"` (note the leading `#` in the value)
+ * - `aria-controls`, e.g., `aria-controls="the-panel-id"` (no leading `#`)
+ *
+ * Exactly one tab/panel must be active in the original markup.
+ *
+ * Call the `destroy` method on an instance to remove event listeners that were
+ * added during construction. Other DOM mutations (like ARIA attributes) are
+ * _not_ reverted.
+ */
+export class GlTabsBehavior {
+ /**
+ * Create a GlTabsBehavior instance.
+ *
+ * @param {HTMLElement} el The element created by the Rails `gl_tabs_nav` helper.
+ */
+ constructor(el) {
+ if (!el) {
+ throw new Error('Cannot instantiate GlTabsBehavior without an element');
+ }
+
+ this.destroyFns = [];
+ this.tabList = el;
+ this.tabs = this.getTabs();
+ this.activeTab = null;
+
+ this.setAccessibilityAttrs();
+ this.bindEvents();
+ }
+
+ setAccessibilityAttrs() {
+ this.tabList.setAttribute(ATTR_ROLE, 'tablist');
+ this.tabs.forEach((tab) => {
+ if (!tab.hasAttribute('id')) {
+ tab.setAttribute('id', uniqueId('gl_tab_nav__tab_'));
+ }
+
+ if (!this.activeTab && tab.classList.contains(ACTIVE_TAB_CLASSES[0])) {
+ this.activeTab = tab;
+ tab.setAttribute(ATTR_ARIA_SELECTED, 'true');
+ tab.removeAttribute(ATTR_TABINDEX);
+ } else {
+ tab.setAttribute(ATTR_ARIA_SELECTED, 'false');
+ tab.setAttribute(ATTR_TABINDEX, '-1');
+ }
+
+ tab.setAttribute(ATTR_ROLE, 'tab');
+ tab.closest('.nav-item').setAttribute(ATTR_ROLE, 'presentation');
+
+ const tabPanel = this.getPanelForTab(tab);
+ if (!tab.hasAttribute(ATTR_ARIA_CONTROLS)) {
+ tab.setAttribute(ATTR_ARIA_CONTROLS, tabPanel.id);
+ }
+
+ tabPanel.setAttribute(ATTR_ROLE, 'tabpanel');
+ tabPanel.setAttribute(ATTR_ARIA_LABELLEDBY, tab.id);
+ });
+ }
+
+ bindEvents() {
+ this.tabs.forEach((tab) => {
+ this.bindEvent(tab, 'click', (event) => {
+ event.preventDefault();
+
+ if (tab !== this.activeTab) {
+ this.activateTab(tab);
+ }
+ });
+
+ this.bindEvent(tab, 'keydown', (event) => {
+ const { code } = event;
+ if (code === KEY_CODE_UP || code === KEY_CODE_LEFT) {
+ event.preventDefault();
+ this.activatePreviousTab();
+ } else if (code === KEY_CODE_DOWN || code === KEY_CODE_RIGHT) {
+ event.preventDefault();
+ this.activateNextTab();
+ }
+ });
+ });
+ }
+
+ bindEvent(el, ...args) {
+ el.addEventListener(...args);
+
+ this.destroyFns.push(() => {
+ el.removeEventListener(...args);
+ });
+ }
+
+ activatePreviousTab() {
+ const currentTabIndex = this.tabs.indexOf(this.activeTab);
+
+ if (currentTabIndex <= 0) return;
+
+ const previousTab = this.tabs[currentTabIndex - 1];
+ this.activateTab(previousTab);
+ previousTab.focus();
+ }
+
+ activateNextTab() {
+ const currentTabIndex = this.tabs.indexOf(this.activeTab);
+
+ if (currentTabIndex >= this.tabs.length - 1) return;
+
+ const nextTab = this.tabs[currentTabIndex + 1];
+ this.activateTab(nextTab);
+ nextTab.focus();
+ }
+
+ getTabs() {
+ return Array.from(this.tabList.querySelectorAll('.gl-tab-nav-item'));
+ }
+
+ // eslint-disable-next-line class-methods-use-this
+ getPanelForTab(tab) {
+ const ariaControls = tab.getAttribute(ATTR_ARIA_CONTROLS);
+
+ if (ariaControls) {
+ return document.querySelector(`#${ariaControls}`);
+ }
+
+ return document.querySelector(tab.getAttribute('href'));
+ }
+
+ activateTab(tabToActivate) {
+ // Deactivate active tab first
+ this.activeTab.setAttribute(ATTR_ARIA_SELECTED, 'false');
+ this.activeTab.setAttribute(ATTR_TABINDEX, '-1');
+ this.activeTab.classList.remove(...ACTIVE_TAB_CLASSES);
+
+ const activePanel = this.getPanelForTab(this.activeTab);
+ activePanel.classList.remove(ACTIVE_PANEL_CLASS);
+
+ // Now activate the given tab/panel
+ tabToActivate.setAttribute(ATTR_ARIA_SELECTED, 'true');
+ tabToActivate.removeAttribute(ATTR_TABINDEX);
+ tabToActivate.classList.add(...ACTIVE_TAB_CLASSES);
+
+ const tabPanel = this.getPanelForTab(tabToActivate);
+ tabPanel.classList.add(ACTIVE_PANEL_CLASS);
+
+ this.activeTab = tabToActivate;
+
+ this.dispatchTabShown(tabToActivate, tabPanel);
+ }
+
+ // eslint-disable-next-line class-methods-use-this
+ dispatchTabShown(tab, activeTabPanel) {
+ const event = new CustomEvent(TAB_SHOWN_EVENT, {
+ bubbles: true,
+ detail: {
+ activeTabPanel,
+ },
+ });
+
+ tab.dispatchEvent(event);
+ }
+
+ destroy() {
+ this.destroyFns.forEach((destroy) => destroy());
+ }
+}
diff --git a/app/assets/javascripts/terraform/graphql/fragments/state_version.fragment.graphql b/app/assets/javascripts/terraform/graphql/fragments/state_version.fragment.graphql
index 70ba5c960be..bb1e7195b17 100644
--- a/app/assets/javascripts/terraform/graphql/fragments/state_version.fragment.graphql
+++ b/app/assets/javascripts/terraform/graphql/fragments/state_version.fragment.graphql
@@ -1,23 +1,23 @@
#import "~/graphql_shared/fragments/user.fragment.graphql"
fragment StateVersion on TerraformStateVersion {
+ id
downloadPath
serial
updatedAt
-
createdByUser {
...User
}
-
job {
+ id
detailedStatus {
+ id
detailsPath
group
icon
label
text
}
-
pipeline {
id
path
diff --git a/app/assets/javascripts/terraform/graphql/queries/get_states.query.graphql b/app/assets/javascripts/terraform/graphql/queries/get_states.query.graphql
index 9453e32b1b5..4d26ea88ddf 100644
--- a/app/assets/javascripts/terraform/graphql/queries/get_states.query.graphql
+++ b/app/assets/javascripts/terraform/graphql/queries/get_states.query.graphql
@@ -3,13 +3,12 @@
query getStates($projectPath: ID!, $first: Int, $last: Int, $before: String, $after: String) {
project(fullPath: $projectPath) {
+ id
terraformStates(first: $first, last: $last, before: $before, after: $after) {
count
-
nodes {
...State
}
-
pageInfo {
...PageInfo
}
diff --git a/app/assets/javascripts/test_utils/simulate_drag.js b/app/assets/javascripts/test_utils/simulate_drag.js
index 321315d531b..4f3f1365f4a 100644
--- a/app/assets/javascripts/test_utils/simulate_drag.js
+++ b/app/assets/javascripts/test_utils/simulate_drag.js
@@ -122,7 +122,6 @@ export default function simulateDrag(options) {
const firstRect = getRect(firstEl);
const lastRect = getRect(lastEl);
- const startTime = new Date().getTime();
const duration = options.duration || 1000;
simulateEvent(fromEl, 'pointerdown', {
@@ -140,8 +139,28 @@ export default function simulateDrag(options) {
toRect.cy = lastRect.y + lastRect.h + 50;
}
- const dragInterval = setInterval(() => {
- const progress = (new Date().getTime() - startTime) / duration;
+ let startTime;
+
+ // Called within dragFn when the drag should finish
+ const finishFn = () => {
+ if (options.ondragend) options.ondragend();
+
+ if (options.performDrop) {
+ simulateEvent(toEl, 'mouseup');
+ }
+
+ window.SIMULATE_DRAG_ACTIVE = 0;
+ };
+
+ const dragFn = (timestamp) => {
+ if (!startTime) {
+ startTime = timestamp;
+ }
+
+ const elapsed = timestamp - startTime;
+
+ // Make sure that progress maxes at 1
+ const progress = Math.min(elapsed / duration, 1);
const x = fromRect.cx + (toRect.cx - fromRect.cx) * progress;
const y = fromRect.cy + (toRect.cy - fromRect.cy + options.extraHeight) * progress;
const overEl = fromEl.ownerDocument.elementFromPoint(x, y);
@@ -152,16 +171,15 @@ export default function simulateDrag(options) {
});
if (progress >= 1) {
- if (options.ondragend) options.ondragend();
-
- if (options.performDrop) {
- simulateEvent(toEl, 'mouseup');
- }
-
- clearInterval(dragInterval);
- window.SIMULATE_DRAG_ACTIVE = 0;
+ // finish on next frame, so we can pause in the correct position for a frame
+ requestAnimationFrame(finishFn);
+ } else {
+ requestAnimationFrame(dragFn);
}
- }, 100);
+ };
+
+ // Start the drag animation
+ requestAnimationFrame(dragFn);
return {
target: fromEl,
diff --git a/app/assets/javascripts/token_access/graphql/queries/get_ci_job_token_scope.query.graphql b/app/assets/javascripts/token_access/graphql/queries/get_ci_job_token_scope.query.graphql
index d4f559c3701..0e5334b468f 100644
--- a/app/assets/javascripts/token_access/graphql/queries/get_ci_job_token_scope.query.graphql
+++ b/app/assets/javascripts/token_access/graphql/queries/get_ci_job_token_scope.query.graphql
@@ -1,5 +1,6 @@
query getCIJobTokenScope($fullPath: ID!) {
project(fullPath: $fullPath) {
+ id
ciCdSettings {
jobTokenScopeEnabled
}
diff --git a/app/assets/javascripts/token_access/graphql/queries/get_projects_with_ci_job_token_scope.query.graphql b/app/assets/javascripts/token_access/graphql/queries/get_projects_with_ci_job_token_scope.query.graphql
index bec0710a1dd..664991bc110 100644
--- a/app/assets/javascripts/token_access/graphql/queries/get_projects_with_ci_job_token_scope.query.graphql
+++ b/app/assets/javascripts/token_access/graphql/queries/get_projects_with_ci_job_token_scope.query.graphql
@@ -1,8 +1,10 @@
query getProjectsWithCIJobTokenScope($fullPath: ID!) {
project(fullPath: $fullPath) {
+ id
ciJobTokenScope {
projects {
nodes {
+ id
name
fullPath
}
diff --git a/app/assets/javascripts/ui_development_kit.js b/app/assets/javascripts/ui_development_kit.js
deleted file mode 100644
index 1a3fd6c77ed..00000000000
--- a/app/assets/javascripts/ui_development_kit.js
+++ /dev/null
@@ -1,28 +0,0 @@
-import $ from 'jquery';
-import initDeprecatedJQueryDropdown from '~/deprecated_jquery_dropdown';
-import Api from './api';
-
-export default () => {
- initDeprecatedJQueryDropdown($('#js-project-dropdown'), {
- data: (term, callback) => {
- Api.projects(
- term,
- {
- order_by: 'last_activity_at',
- },
- (data) => {
- callback(data);
- },
- );
- },
- text: (project) => project.name_with_namespace || project.name,
- selectable: true,
- fieldName: 'author_id',
- filterable: true,
- search: {
- fields: ['name_with_namespace'],
- },
- id: (data) => data.id,
- isSelected: (data) => data.id === 2,
- });
-};
diff --git a/app/assets/javascripts/user_lists/components/add_user_modal.vue b/app/assets/javascripts/user_lists/components/add_user_modal.vue
index a8dde1f681e..e982d10f63b 100644
--- a/app/assets/javascripts/user_lists/components/add_user_modal.vue
+++ b/app/assets/javascripts/user_lists/components/add_user_modal.vue
@@ -19,7 +19,7 @@ export default {
modalOptions: {
actionPrimary: {
text: s__('UserLists|Add'),
- attributes: [{ 'data-testid': 'confirm-add-user-ids' }],
+ attributes: [{ 'data-testid': 'confirm-add-user-ids', variant: 'confirm' }],
},
actionCancel: {
text: s__('UserLists|Cancel'),
diff --git a/app/assets/javascripts/user_lists/components/user_list.vue b/app/assets/javascripts/user_lists/components/user_list.vue
index 4cf3f3010b9..e86b3f81daa 100644
--- a/app/assets/javascripts/user_lists/components/user_list.vue
+++ b/app/assets/javascripts/user_lists/components/user_list.vue
@@ -105,7 +105,7 @@ export default {
<gl-button
v-gl-modal="$options.ADD_USER_MODAL_ID"
data-testid="add-users"
- variant="success"
+ variant="confirm"
>
{{ $options.translations.addUserButtonLabel }}
</gl-button>
diff --git a/app/assets/javascripts/vue_alerts.js b/app/assets/javascripts/vue_alerts.js
index abc1dd75645..b44f787cf30 100644
--- a/app/assets/javascripts/vue_alerts.js
+++ b/app/assets/javascripts/vue_alerts.js
@@ -1,7 +1,17 @@
import Vue from 'vue';
+import Cookies from 'js-cookie';
import { parseBoolean } from '~/lib/utils/common_utils';
import DismissibleAlert from '~/vue_shared/components/dismissible_alert.vue';
+const getCookieExpirationPeriod = (expirationPeriod) => {
+ const defaultExpirationPeriod = 30;
+ const alertExpirationPeriod = Number(expirationPeriod);
+
+ return !expirationPeriod || Number.isNaN(alertExpirationPeriod)
+ ? defaultExpirationPeriod
+ : alertExpirationPeriod;
+};
+
const mountVueAlert = (el) => {
const props = {
html: el.innerHTML,
@@ -10,11 +20,25 @@ const mountVueAlert = (el) => {
...el.dataset,
dismissible: parseBoolean(el.dataset.dismissible),
};
+ const { dismissCookieName, dismissCookieExpire } = el.dataset;
return new Vue({
el,
- render(h) {
- return h(DismissibleAlert, { props, attrs });
+ render(createElement) {
+ return createElement(DismissibleAlert, {
+ props,
+ attrs,
+ on: {
+ alertDismissed() {
+ if (!dismissCookieName) {
+ return;
+ }
+ Cookies.set(dismissCookieName, true, {
+ expires: getCookieExpirationPeriod(dismissCookieExpire),
+ });
+ },
+ },
+ });
},
});
};
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/deployment/deployment_info.vue b/app/assets/javascripts/vue_merge_request_widget/components/deployment/deployment_info.vue
index f4f611dfd1b..e115710b5d1 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/deployment/deployment_info.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/deployment/deployment_info.vue
@@ -1,7 +1,7 @@
<script>
import { GlLink, GlTooltipDirective } from '@gitlab/ui';
import { __ } from '~/locale';
-import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate.vue';
+import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate/tooltip_on_truncate.vue';
import timeagoMixin from '~/vue_shared/mixins/timeago';
import {
MANUAL_DEPLOY,
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/extensions/base.vue b/app/assets/javascripts/vue_merge_request_widget/components/extensions/base.vue
index 6f10f788952..549cf64fb08 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/extensions/base.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/extensions/base.vue
@@ -9,17 +9,20 @@ import {
GlIntersectionObserver,
} from '@gitlab/ui';
import { once } from 'lodash';
+import * as Sentry from '@sentry/browser';
import api from '~/api';
import { sprintf, s__, __ } from '~/locale';
import SmartVirtualList from '~/vue_shared/components/smart_virtual_list.vue';
-import { EXTENSION_ICON_CLASS } from '../../constants';
+import { EXTENSION_ICON_CLASS, EXTENSION_ICONS } from '../../constants';
import StatusIcon from './status_icon.vue';
import Actions from './actions.vue';
+import { generateText } from './utils';
export const LOADING_STATES = {
collapsedLoading: 'collapsedLoading',
collapsedError: 'collapsedError',
expandedLoading: 'expandedLoading',
+ expandedError: 'expandedError',
};
export default {
@@ -40,8 +43,8 @@ export default {
data() {
return {
loadingState: LOADING_STATES.collapsedLoading,
- collapsedData: null,
- fullData: null,
+ collapsedData: {},
+ fullData: [],
isCollapsed: true,
showFade: false,
};
@@ -53,6 +56,9 @@ export default {
widgetLoadingText() {
return this.$options.i18n?.loading || __('Loading...');
},
+ widgetErrorText() {
+ return this.$options.i18n?.error || __('Failed to load');
+ },
isLoadingSummary() {
return this.loadingState === LOADING_STATES.collapsedLoading;
},
@@ -60,11 +66,16 @@ export default {
return this.loadingState === LOADING_STATES.expandedLoading;
},
isCollapsible() {
- if (this.isLoadingSummary) {
- return false;
- }
-
- return true;
+ return !this.isLoadingSummary && this.loadingState !== LOADING_STATES.collapsedError;
+ },
+ hasFullData() {
+ return this.fullData.length > 0;
+ },
+ hasFetchError() {
+ return (
+ this.loadingState === LOADING_STATES.collapsedError ||
+ this.loadingState === LOADING_STATES.expandedError
+ );
},
collapseButtonLabel() {
return sprintf(
@@ -75,6 +86,7 @@ export default {
);
},
statusIconName() {
+ if (this.hasFetchError) return EXTENSION_ICONS.error;
if (this.isLoadingSummary) return null;
return this.statusIcon(this.collapsedData);
@@ -82,6 +94,20 @@ export default {
tertiaryActionsButtons() {
return this.tertiaryButtons ? this.tertiaryButtons() : undefined;
},
+ hydratedSummary() {
+ const structuredOutput = this.summary(this.collapsedData);
+ const summary = {
+ subject: generateText(
+ typeof structuredOutput === 'string' ? structuredOutput : structuredOutput.subject,
+ ),
+ };
+
+ if (structuredOutput.meta) {
+ summary.meta = generateText(structuredOutput.meta);
+ }
+
+ return summary;
+ },
},
watch: {
isCollapsed(newVal) {
@@ -93,15 +119,7 @@ export default {
},
},
mounted() {
- this.fetchCollapsedData(this.$props)
- .then((data) => {
- this.collapsedData = data;
- this.loadingState = null;
- })
- .catch((e) => {
- this.loadingState = LOADING_STATES.collapsedError;
- throw e;
- });
+ this.loadCollapsedData();
},
methods: {
triggerRedisTracking: once(function triggerRedisTracking() {
@@ -114,8 +132,22 @@ export default {
this.triggerRedisTracking();
},
+ loadCollapsedData() {
+ this.loadingState = LOADING_STATES.collapsedLoading;
+
+ this.fetchCollapsedData(this.$props)
+ .then((data) => {
+ this.collapsedData = data;
+ this.loadingState = null;
+ })
+ .catch((e) => {
+ this.loadingState = LOADING_STATES.collapsedError;
+
+ Sentry.captureException(e);
+ });
+ },
loadAllData() {
- if (this.fullData) return;
+ if (this.hasFullData) return;
this.loadingState = LOADING_STATES.expandedLoading;
@@ -125,10 +157,14 @@ export default {
this.fullData = data;
})
.catch((e) => {
- this.loadingState = null;
- throw e;
+ this.loadingState = LOADING_STATES.expandedError;
+
+ Sentry.captureException(e);
});
},
+ isArray(arr) {
+ return Array.isArray(arr);
+ },
appear(index) {
if (index === this.fullData.length - 1) {
this.showFade = false;
@@ -139,6 +175,7 @@ export default {
this.showFade = true;
}
},
+ generateText,
},
EXTENSION_ICON_CLASS,
};
@@ -153,20 +190,29 @@ export default {
:icon-name="statusIconName"
/>
<div
- class="media-body gl-display-flex gl-flex-direction-row!"
+ class="media-body gl-display-flex gl-flex-direction-row! gl-align-self-center"
data-testid="widget-extension-top-level"
>
<div class="gl-flex-grow-1">
<template v-if="isLoadingSummary">{{ widgetLoadingText }}</template>
- <div v-else v-safe-html="summary(collapsedData)"></div>
+ <template v-else-if="hasFetchError">{{ widgetErrorText }}</template>
+ <div v-else>
+ <span v-safe-html="hydratedSummary.subject"></span>
+ <template v-if="hydratedSummary.meta">
+ <br />
+ <span v-safe-html="hydratedSummary.meta" class="gl-font-sm"></span>
+ </template>
+ </div>
</div>
<actions
:widget="$options.label || $options.name"
:tertiary-buttons="tertiaryActionsButtons"
/>
- <div class="gl-border-l-1 gl-border-l-solid gl-border-gray-100 gl-ml-3 gl-pl-3 gl-h-6">
+ <div
+ v-if="isCollapsible"
+ class="gl-border-l-1 gl-border-l-solid gl-border-gray-100 gl-ml-3 gl-pl-3 gl-h-6"
+ >
<gl-button
- v-if="isCollapsible"
v-gl-tooltip
:title="collapseButtonLabel"
:aria-expanded="`${!isCollapsed}`"
@@ -189,7 +235,7 @@ export default {
<gl-loading-icon size="sm" inline /> {{ __('Loading...') }}
</div>
<smart-virtual-list
- v-else-if="fullData"
+ v-else-if="hasFullData"
:length="fullData.length"
:remain="20"
:size="32"
@@ -203,37 +249,64 @@ export default {
:class="{
'gl-border-b-solid gl-border-b-1 gl-border-gray-100': index !== fullData.length - 1,
}"
- class="gl-display-flex gl-align-items-center gl-py-3 gl-pl-7"
+ class="gl-py-3 gl-pl-7"
data-testid="extension-list-item"
>
- <status-icon v-if="data.icon" :icon-name="data.icon.name" :size="12" class="gl-pl-0" />
- <gl-intersection-observer
- :options="{ rootMargin: '100px', thresholds: 0.1 }"
- class="gl-flex-wrap gl-display-flex gl-w-full"
- @appear="appear(index)"
- @disappear="disappear(index)"
- >
- <div
- v-safe-html="data.text"
- class="gl-mr-4 gl-display-flex gl-align-items-center"
- ></div>
- <div v-if="data.link">
- <gl-link :href="data.link.href">{{ data.link.text }}</gl-link>
+ <div class="gl-w-full">
+ <div v-if="data.header" class="gl-mb-2">
+ <template v-if="isArray(data.header)">
+ <component
+ :is="headerI === 0 ? 'strong' : 'span'"
+ v-for="(header, headerI) in data.header"
+ :key="headerI"
+ v-safe-html="generateText(header)"
+ class="gl-display-block"
+ />
+ </template>
+ <strong v-else v-safe-html="generateText(data.header)"></strong>
+ </div>
+ <div class="gl-display-flex">
+ <status-icon
+ v-if="data.icon"
+ :icon-name="data.icon.name"
+ :size="12"
+ class="gl-pl-0"
+ />
+ <gl-intersection-observer
+ :options="{ rootMargin: '100px', thresholds: 0.1 }"
+ class="gl-w-full"
+ @appear="appear(index)"
+ @disappear="disappear(index)"
+ >
+ <div class="gl-flex-wrap gl-display-flex gl-w-full">
+ <div class="gl-mr-4 gl-display-flex gl-align-items-center">
+ <p v-safe-html="generateText(data.text)" class="gl-m-0"></p>
+ </div>
+ <div v-if="data.link">
+ <gl-link :href="data.link.href">{{ data.link.text }}</gl-link>
+ </div>
+ <gl-badge v-if="data.badge" :variant="data.badge.variant || 'info'">
+ {{ data.badge.text }}
+ </gl-badge>
+ <actions
+ :widget="$options.label || $options.name"
+ :tertiary-buttons="data.actions"
+ class="gl-ml-auto"
+ />
+ </div>
+ <p
+ v-if="data.subtext"
+ v-safe-html="generateText(data.subtext)"
+ class="gl-m-0 gl-font-sm"
+ ></p>
+ </gl-intersection-observer>
</div>
- <gl-badge v-if="data.badge" :variant="data.badge.variant || 'info'">
- {{ data.badge.text }}
- </gl-badge>
- <actions
- :widget="$options.label || $options.name"
- :tertiary-buttons="data.actions"
- class="gl-ml-auto"
- />
- </gl-intersection-observer>
+ </div>
</li>
</smart-virtual-list>
<div
:class="{ show: showFade }"
- class="fade mr-extenson-scrim gl-absolute gl-left-0 gl-bottom-0 gl-w-full gl-h-7"
+ class="fade mr-extenson-scrim gl-absolute gl-left-0 gl-bottom-0 gl-w-full gl-h-7 gl-pointer-events-none"
></div>
</div>
</section>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/extensions/utils.js b/app/assets/javascripts/vue_merge_request_widget/components/extensions/utils.js
new file mode 100644
index 00000000000..8ba13cf8252
--- /dev/null
+++ b/app/assets/javascripts/vue_merge_request_widget/components/extensions/utils.js
@@ -0,0 +1,62 @@
+const TEXT_STYLES = {
+ success: {
+ start: '%{success_start}',
+ end: '%{success_end}',
+ },
+ danger: {
+ start: '%{danger_start}',
+ end: '%{danger_end}',
+ },
+ critical: {
+ start: '%{critical_start}',
+ end: '%{critical_end}',
+ },
+ same: {
+ start: '%{same_start}',
+ end: '%{same_end}',
+ },
+ strong: {
+ start: '%{strong_start}',
+ end: '%{strong_end}',
+ },
+ small: {
+ start: '%{small_start}',
+ end: '%{small_end}',
+ },
+};
+
+const getStartTag = (tag) => TEXT_STYLES[tag].start;
+const textStyleTags = {
+ [getStartTag('success')]: '<span class="gl-font-weight-bold gl-text-green-500">',
+ [getStartTag('danger')]: '<span class="gl-font-weight-bold gl-text-red-500">',
+ [getStartTag('critical')]: '<span class="gl-font-weight-bold gl-text-red-800">',
+ [getStartTag('same')]: '<span class="gl-font-weight-bold gl-text-gray-700">',
+ [getStartTag('strong')]: '<span class="gl-font-weight-bold">',
+ [getStartTag('small')]: '<span class="gl-font-sm">',
+};
+
+export const generateText = (text) => {
+ if (typeof text !== 'string') return null;
+
+ return text
+ .replace(
+ new RegExp(
+ `(${Object.values(TEXT_STYLES)
+ .reduce((acc, i) => [...acc, ...Object.values(i)], [])
+ .join('|')})`,
+ 'gi',
+ ),
+ (replace) => {
+ const replacement = textStyleTags[replace];
+
+ // If the replacement tag ends with a `_end` then we can just return `</span>`
+ // unless we have a replacement, for cases were we want to change the HTML tag
+ if (!replacement && replace.endsWith('_end}')) {
+ return '</span>';
+ }
+
+ return replacement;
+ },
+ )
+ .replace(/%{([a-z]|_)+}/g, ''); // Filter out any tags we don't know about
+};
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 9070cb1fe65..235a200b747 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
@@ -13,7 +13,7 @@ import {
import { constructWebIDEPath } from '~/lib/utils/url_utility';
import { s__ } from '~/locale';
import clipboardButton from '~/vue_shared/components/clipboard_button.vue';
-import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate.vue';
+import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate/tooltip_on_truncate.vue';
import WebIdeLink from '~/vue_shared/components/web_ide_link.vue';
import MrWidgetHowToMergeModal from './mr_widget_how_to_merge_modal.vue';
import MrWidgetIcon from './mr_widget_icon.vue';
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 f7c952f9ef6..c0b80eef082 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
@@ -15,7 +15,7 @@ import PipelineMiniGraph from '~/pipelines/components/pipelines_list/pipeline_mi
import PipelineArtifacts from '~/pipelines/components/pipelines_list/pipelines_artifacts.vue';
import CiIcon from '~/vue_shared/components/ci_icon.vue';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
-import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate.vue';
+import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate/tooltip_on_truncate.vue';
import { MT_MERGE_STRATEGY } from '../constants';
export default {
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_related_links.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_related_links.vue
index c314261d3f5..730d11b1208 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_related_links.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_related_links.vue
@@ -1,9 +1,13 @@
<script>
+import { GlSafeHtmlDirective as SafeHtml } from '@gitlab/ui';
import { s__, n__ } from '~/locale';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
export default {
name: 'MRWidgetRelatedLinks',
+ directives: {
+ SafeHtml,
+ },
mixins: [glFeatureFlagMixin()],
props: {
relatedLinks: {
@@ -43,14 +47,14 @@ export default {
:class="{ 'gl-display-line gl-m-0': glFeatures.restructuredMrWidget }"
>
{{ closesText }}
- <span v-html="relatedLinks.closing /* eslint-disable-line vue/no-v-html */"></span>
+ <span v-safe-html="relatedLinks.closing"></span>
</p>
<p
v-if="relatedLinks.mentioned"
:class="{ 'gl-display-line gl-m-0': glFeatures.restructuredMrWidget }"
>
{{ n__('mrWidget|Mentions issue', 'mrWidget|Mentions issues', relatedLinks.mentionedCount) }}
- <span v-html="relatedLinks.mentioned /* eslint-disable-line vue/no-v-html */"></span>
+ <span v-safe-html="relatedLinks.mentioned"></span>
</p>
<p
v-if="relatedLinks.assignToMe && showAssignToMe"
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/commit_edit.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/commit_edit.vue
index 3eda2828e97..18761d04c2e 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/commit_edit.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/commit_edit.vue
@@ -41,7 +41,6 @@ export default {
rows="7"
@input="$emit('input', $event.target.value)"
></textarea>
- <slot name="text-muted"></slot>
</div>
</li>
</template>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/merge_checks_failed.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/merge_checks_failed.vue
index 503ddf8a396..ce572f8b0bf 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/merge_checks_failed.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/merge_checks_failed.vue
@@ -9,7 +9,7 @@ export default {
pipelineFailed: s__(
'mrWidget|The pipeline for this merge request did not complete. Push a new commit to fix the failure.',
),
- approvalNeeded: s__('mrWidget|You can only merge once this merge request is approved.'),
+ approvalNeeded: s__('mrWidget|Merge blocked: this merge request must be approved.'),
unresolvedDiscussions: s__('mrWidget|Merge blocked: all threads must be resolved.'),
},
components: {
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_archived.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_archived.vue
index 29c26f4fb3e..13b1e49f44e 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_archived.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_archived.vue
@@ -20,7 +20,7 @@ export default {
</div>
<div class="media-body">
<span class="bold">
- {{ s__('mrWidget|This project is archived, write access has been disabled') }}
+ {{ s__('mrWidget|Merge unavailable: merge requests are read-only on archived projects.') }}
</span>
</div>
</div>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_conflicts.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_conflicts.vue
index 1596f852b74..7a002d41ac0 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_conflicts.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_conflicts.vue
@@ -117,11 +117,12 @@ export default {
</span>
<template v-else>
<span class="bold">
- {{ s__('mrWidget|There are merge conflicts') }}<span v-if="!canMerge">.</span>
+ {{ s__('mrWidget|Merge blocked: merge conflicts must be resolved.') }}
<span v-if="!canMerge">
{{
- s__(`mrWidget|Resolve these conflicts or ask someone
- with write access to this repository to merge it locally`)
+ s__(
+ `mrWidget|Users who can write to the source or target branches can resolve the conflicts.`,
+ )
}}
</span>
</span>
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 9f2870d8d69..01e8303f513 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
@@ -121,9 +121,6 @@ export default {
if (res.merge_error && res.merge_error.length) {
this.rebasingError = res.merge_error;
- createFlash({
- message: __('Something went wrong. Please try again.'),
- });
}
eventHub.$emit('MRWidgetRebaseSuccess');
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 d2cc99302a9..8830128b7d6 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
@@ -181,9 +181,16 @@ export default {
return this.mr.canRemoveSourceBranch;
},
commitTemplateHelpPage() {
- return helpPagePath('user/project/merge_requests/commit_templates.md', {
- anchor: 'merge-commit-message-template',
- });
+ return helpPagePath('user/project/merge_requests/commit_templates.md');
+ },
+ commitTemplateHintText() {
+ if (this.shouldShowSquashEdit && this.shouldShowMergeEdit) {
+ return this.$options.i18n.mergeAndSquashCommitTemplatesHintText;
+ }
+ if (this.shouldShowSquashEdit) {
+ return this.$options.i18n.squashCommitTemplateHintText;
+ }
+ return this.$options.i18n.mergeCommitTemplateHintText;
},
commits() {
if (this.glFeatures.mergeRequestWidgetGraphql) {
@@ -287,7 +294,7 @@ export default {
return false;
}
- return enableSquashBeforeMerge && this.commitsCount > 1;
+ return enableSquashBeforeMerge;
},
shouldShowMergeControls() {
if (this.glFeatures.restructuredMrWidget) {
@@ -509,6 +516,12 @@ export default {
mergeCommitTemplateHintText: s__(
'mrWidget|To change this default message, edit the template for merge commit messages. %{linkStart}Learn more.%{linkEnd}',
),
+ squashCommitTemplateHintText: s__(
+ 'mrWidget|To change this default message, edit the template for squash commit messages. %{linkStart}Learn more.%{linkEnd}',
+ ),
+ mergeAndSquashCommitTemplatesHintText: s__(
+ 'mrWidget|To change these default messages, edit the templates for both the merge and squash commit messages. %{linkStart}Learn more.%{linkEnd}',
+ ),
},
};
</script>
@@ -590,13 +603,7 @@ export default {
:class="{ 'gl-w-full gl-order-n1 gl-mb-5': glFeatures.restructuredMrWidget }"
class="gl-display-flex gl-align-items-center gl-flex-wrap"
>
- <merge-train-helper-icon
- v-if="shouldRenderMergeTrainHelperIcon"
- :merge-train-when-pipeline-succeeds-docs-path="
- mr.mergeTrainWhenPipelineSucceedsDocsPath
- "
- class="gl-mx-3"
- />
+ <merge-train-helper-icon v-if="shouldRenderMergeTrainHelperIcon" class="gl-mx-3" />
<gl-form-checkbox
v-if="canRemoveSourceBranch"
@@ -680,23 +687,22 @@ export default {
:label="__('Merge commit message')"
input-id="merge-message-edit"
class="gl-m-0! gl-p-0!"
- >
- <template #text-muted>
- <p class="form-text text-muted">
- <gl-sprintf :message="$options.i18n.mergeCommitTemplateHintText">
- <template #link="{ content }">
- <gl-link
- :href="commitTemplateHelpPage"
- class="inline-link"
- target="_blank"
- >
- {{ content }}
- </gl-link>
- </template>
- </gl-sprintf>
- </p>
- </template>
- </commit-edit>
+ />
+ <li class="gl-m-0! gl-p-0!">
+ <p class="form-text text-muted">
+ <gl-sprintf :message="commitTemplateHintText">
+ <template #link="{ content }">
+ <gl-link
+ :href="commitTemplateHelpPage"
+ class="inline-link"
+ target="_blank"
+ >
+ {{ content }}
+ </gl-link>
+ </template>
+ </gl-sprintf>
+ </p>
+ </li>
</ul>
</div>
<div
@@ -798,19 +804,18 @@ export default {
v-model="commitMessage"
:label="__('Merge commit message')"
input-id="merge-message-edit"
- >
- <template #text-muted>
- <p class="form-text text-muted">
- <gl-sprintf :message="$options.i18n.mergeCommitTemplateHintText">
- <template #link="{ content }">
- <gl-link :href="commitTemplateHelpPage" class="inline-link" target="_blank">
- {{ content }}
- </gl-link>
- </template>
- </gl-sprintf>
- </p>
- </template>
- </commit-edit>
+ />
+ <li>
+ <p class="form-text text-muted">
+ <gl-sprintf :message="commitTemplateHintText">
+ <template #link="{ content }">
+ <gl-link :href="commitTemplateHelpPage" class="inline-link" target="_blank">
+ {{ content }}
+ </gl-link>
+ </template>
+ </gl-sprintf>
+ </p>
+ </li>
</ul>
</commits-header>
</template>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/work_in_progress.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/work_in_progress.vue
index fa4f8b76cb9..ba831a33b73 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/work_in_progress.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/work_in_progress.vue
@@ -165,13 +165,12 @@ export default {
<div class="mr-widget-body media">
<status-icon :show-disabled-button="canUpdate" status="warning" />
<div class="media-body">
- <div class="gl-ml-3 float-left">
+ <div class="float-left">
<span class="gl-font-weight-bold">
- {{ __('This merge request is still a draft.') }}
+ {{
+ __("Merge blocked: merge request must be marked as ready. It's still marked as draft.")
+ }}
</span>
- <span class="gl-display-block text-muted">{{
- __("Draft merge requests can't be merged.")
- }}</span>
</div>
<gl-button
v-if="canUpdate"
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 87a310efe78..1e5f7361966 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
@@ -20,8 +20,8 @@ export default {
'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.'),
+ namedReportFailed: s__('Terraform|The job %{name} failed to generate a report.'),
+ namedReportGenerated: s__('Terraform|The job %{name} generated a report.'),
reportFailed: s__('Terraform|A report failed to generate.'),
reportGenerated: s__('Terraform|A report was generated in your pipelines.'),
},
diff --git a/app/assets/javascripts/vue_merge_request_widget/constants.js b/app/assets/javascripts/vue_merge_request_widget/constants.js
index d0c6cf12e25..2edccce7f4e 100644
--- a/app/assets/javascripts/vue_merge_request_widget/constants.js
+++ b/app/assets/javascripts/vue_merge_request_widget/constants.js
@@ -50,6 +50,18 @@ export const MERGE_ACTIVE_STATUS_PHRASES = [
message: s__('mrWidget|Merging! This is going to be great…'),
emoji: 'heart_eyes',
},
+ {
+ message: s__('mrWidget|Merging! Lift-off in 5… 4… 3…'),
+ emoji: 'rocket',
+ },
+ {
+ message: s__('mrWidget|Merging! The changes are leaving the station…'),
+ emoji: 'bullettrain_front',
+ },
+ {
+ message: s__('mrWidget|Merging! Take a deep breath and relax…'),
+ emoji: 'sunglasses',
+ },
];
const STATE_MACHINE = {
@@ -146,4 +158,7 @@ export const EXTENSION_ICON_CLASS = {
severityUnknown: 'gl-text-gray-400',
};
+export const EXTENSION_SUMMARY_FAILED_CLASS = 'gl-text-red-500';
+export const EXTENSION_SUMMARY_NEUTRAL_CLASS = 'gl-text-gray-700';
+
export { STATE_MACHINE };
diff --git a/app/assets/javascripts/vue_merge_request_widget/extensions/issues.js b/app/assets/javascripts/vue_merge_request_widget/extensions/issues.js
index 9cbc0b0e5d1..ba3336df2eb 100644
--- a/app/assets/javascripts/vue_merge_request_widget/extensions/issues.js
+++ b/app/assets/javascripts/vue_merge_request_widget/extensions/issues.js
@@ -2,6 +2,7 @@
import { EXTENSION_ICONS } from '../constants';
import issuesCollapsedQuery from './issues_collapsed.query.graphql';
import issuesQuery from './issues.query.graphql';
+import { n__, sprintf } from '~/locale';
export default {
// Give the extension a name
@@ -20,7 +21,14 @@ export default {
// Small summary text to be displayed in the collapsed state
// Receives the collapsed data as an argument
summary(count) {
- return 'Summary text<br/>Second line';
+ return sprintf(
+ n__(
+ 'ciReport|Load performance test metrics detected %{strong_start}%{changesFound}%{strong_end} change',
+ 'ciReport|Load performance test metrics detected %{strong_start}%{changesFound}%{strong_end} changes',
+ changesFound,
+ ),
+ { changesFound },
+ );
},
// Status icon to be used next to the summary text
// Receives the collapsed data as an argument
@@ -57,9 +65,13 @@ export default {
.query({ query: issuesQuery, variables: { projectPath: targetProjectFullPath } })
.then(({ data }) => {
// Return some transformed data to be rendered in the expanded state
- return data.project.issues.nodes.map((issue) => ({
+ return data.project.issues.nodes.map((issue, i) => ({
id: issue.id, // Required: The ID of the object
- text: issue.title, // Required: The text to get used on each row
+ header: ['New', 'This is an %{strong_start}issue%{strong_end} row'],
+ text:
+ '%{critical_start}1 Critical%{critical_end}, %{danger_start}1 High%{danger_end}, and %{strong_start}1 Other%{strong_end}. %{small_start}Some smaller text%{small_end}', // Required: The text to get used on each row
+ subtext:
+ 'Reported resource changes: %{strong_start}2%{strong_end} to add, 0 to change, 0 to delete', // Optional: The sub-text to get displayed below each rows main content
// Icon to get rendered on the side of each row
icon: {
// Required: Name maps to an icon in GitLabs SVG
diff --git a/app/assets/javascripts/vue_merge_request_widget/extensions/issues.query.graphql b/app/assets/javascripts/vue_merge_request_widget/extensions/issues.query.graphql
index 690f571c083..5c54560bd02 100644
--- a/app/assets/javascripts/vue_merge_request_widget/extensions/issues.query.graphql
+++ b/app/assets/javascripts/vue_merge_request_widget/extensions/issues.query.graphql
@@ -1,5 +1,6 @@
query getAllIssues($projectPath: ID!) {
project(fullPath: $projectPath) {
+ id
issues {
nodes {
id
diff --git a/app/assets/javascripts/vue_merge_request_widget/extensions/issues_collapsed.query.graphql b/app/assets/javascripts/vue_merge_request_widget/extensions/issues_collapsed.query.graphql
index da1cace4598..bf278e1ea85 100644
--- a/app/assets/javascripts/vue_merge_request_widget/extensions/issues_collapsed.query.graphql
+++ b/app/assets/javascripts/vue_merge_request_widget/extensions/issues_collapsed.query.graphql
@@ -1,5 +1,6 @@
query getProjectIssues($projectPath: ID!) {
project(fullPath: $projectPath) {
+ id
issues {
count
}
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 83789f10285..fa618756bb5 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
@@ -1,6 +1,8 @@
import { __ } from '~/locale';
-export const MERGE_DISABLED_TEXT = __('You can only merge once the items above are resolved.');
+export const MERGE_DISABLED_TEXT = __(
+ 'Merge blocked: all merge request dependencies must be merged or closed.',
+);
export const MERGE_DISABLED_SKIPPED_PIPELINE_TEXT = __(
"Merge blocked: pipeline must succeed. It's waiting for a manual job to continue.",
);
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 bfb1517be81..0b8396b4461 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
@@ -1,9 +1,11 @@
query getState($projectPath: ID!, $iid: String!) {
project(fullPath: $projectPath) {
+ id
archived
onlyAllowMergeIfPipelineSucceeds
mergeRequest(iid: $iid) {
+ id
autoMergeEnabled
commitCount
conflicts
diff --git a/app/assets/javascripts/vue_merge_request_widget/queries/permissions.query.graphql b/app/assets/javascripts/vue_merge_request_widget/queries/permissions.query.graphql
index ae2a67440fe..7ca3ff39fbe 100644
--- a/app/assets/javascripts/vue_merge_request_widget/queries/permissions.query.graphql
+++ b/app/assets/javascripts/vue_merge_request_widget/queries/permissions.query.graphql
@@ -1,6 +1,8 @@
query userPermissionsQuery($projectPath: ID!, $iid: String!) {
project(fullPath: $projectPath) {
+ id
mergeRequest(iid: $iid) {
+ id
userPermissions {
canMerge
pushToSourceBranch
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 ad715599eb1..fc25e699e39 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,4 +1,5 @@
fragment autoMergeEnabled on MergeRequest {
+ id
autoMergeStrategy
mergeUser {
id
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 e0215fbd969..2d79d35cf24 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
@@ -2,6 +2,7 @@
query autoMergeEnabled($projectPath: ID!, $iid: String!) {
project(fullPath: $projectPath) {
+ id
mergeRequest(iid: $iid) {
...autoMergeEnabled
}
diff --git a/app/assets/javascripts/vue_merge_request_widget/queries/states/auto_merge_failed.query.graphql b/app/assets/javascripts/vue_merge_request_widget/queries/states/auto_merge_failed.query.graphql
index 2fe0d174b67..da8aeab9dcb 100644
--- a/app/assets/javascripts/vue_merge_request_widget/queries/states/auto_merge_failed.query.graphql
+++ b/app/assets/javascripts/vue_merge_request_widget/queries/states/auto_merge_failed.query.graphql
@@ -1,6 +1,8 @@
query autoMergeFailedQuery($projectPath: ID!, $iid: String!) {
project(fullPath: $projectPath) {
+ id
mergeRequest(iid: $iid) {
+ id
mergeError
}
}
diff --git a/app/assets/javascripts/vue_merge_request_widget/queries/states/conflicts.query.graphql b/app/assets/javascripts/vue_merge_request_widget/queries/states/conflicts.query.graphql
index e66ac01ab12..faf21b28f86 100644
--- a/app/assets/javascripts/vue_merge_request_widget/queries/states/conflicts.query.graphql
+++ b/app/assets/javascripts/vue_merge_request_widget/queries/states/conflicts.query.graphql
@@ -1,6 +1,8 @@
query workInProgress($projectPath: ID!, $iid: String!) {
project(fullPath: $projectPath) {
+ id
mergeRequest(iid: $iid) {
+ id
shouldBeRebased
sourceBranchProtected
}
diff --git a/app/assets/javascripts/vue_merge_request_widget/queries/states/draft.query.graphql b/app/assets/javascripts/vue_merge_request_widget/queries/states/draft.query.graphql
index 0983c28448e..54f2233439f 100644
--- a/app/assets/javascripts/vue_merge_request_widget/queries/states/draft.query.graphql
+++ b/app/assets/javascripts/vue_merge_request_widget/queries/states/draft.query.graphql
@@ -1,6 +1,8 @@
query mrUserPermission($projectPath: ID!, $iid: String!) {
project(fullPath: $projectPath) {
+ id
mergeRequest(iid: $iid) {
+ id
userPermissions {
updateMergeRequest
}
diff --git a/app/assets/javascripts/vue_merge_request_widget/queries/states/missing_branch.query.graphql b/app/assets/javascripts/vue_merge_request_widget/queries/states/missing_branch.query.graphql
index ea95218aec6..4d87d55f671 100644
--- a/app/assets/javascripts/vue_merge_request_widget/queries/states/missing_branch.query.graphql
+++ b/app/assets/javascripts/vue_merge_request_widget/queries/states/missing_branch.query.graphql
@@ -1,6 +1,8 @@
query missingBranchQuery($projectPath: ID!, $iid: String!) {
project(fullPath: $projectPath) {
+ id
mergeRequest(iid: $iid) {
+ id
sourceBranchExists
}
}
diff --git a/app/assets/javascripts/vue_merge_request_widget/queries/states/new_ready_to_merge.query.graphql b/app/assets/javascripts/vue_merge_request_widget/queries/states/new_ready_to_merge.query.graphql
index 21c3ffd8321..73c9e77b7bc 100644
--- a/app/assets/javascripts/vue_merge_request_widget/queries/states/new_ready_to_merge.query.graphql
+++ b/app/assets/javascripts/vue_merge_request_widget/queries/states/new_ready_to_merge.query.graphql
@@ -1,6 +1,8 @@
query getReadyToMergeStatus($projectPath: ID!, $iid: String!) {
project(fullPath: $projectPath) {
+ id
mergeRequest(iid: $iid) {
+ id
userPermissions {
canMerge
}
diff --git a/app/assets/javascripts/vue_merge_request_widget/queries/states/ready_to_merge.fragment.graphql b/app/assets/javascripts/vue_merge_request_widget/queries/states/ready_to_merge.fragment.graphql
index b2a1be5c5a9..d85794f7245 100644
--- a/app/assets/javascripts/vue_merge_request_widget/queries/states/ready_to_merge.fragment.graphql
+++ b/app/assets/javascripts/vue_merge_request_widget/queries/states/ready_to_merge.fragment.graphql
@@ -1,8 +1,10 @@
fragment ReadyToMerge on Project {
+ id
onlyAllowMergeIfPipelineSucceeds
mergeRequestsFfOnlyEnabled
squashReadOnly
mergeRequest(iid: $iid) {
+ id
autoMergeEnabled
shouldRemoveSourceBranch
forceRemoveSourceBranch
@@ -26,6 +28,7 @@ fragment ReadyToMerge on Project {
mergeError
commitsWithoutMergeCommits {
nodes {
+ id
sha
shortId
title
diff --git a/app/assets/javascripts/vue_merge_request_widget/queries/states/rebase.query.graphql b/app/assets/javascripts/vue_merge_request_widget/queries/states/rebase.query.graphql
index a8c7d2610bf..283177267d4 100644
--- a/app/assets/javascripts/vue_merge_request_widget/queries/states/rebase.query.graphql
+++ b/app/assets/javascripts/vue_merge_request_widget/queries/states/rebase.query.graphql
@@ -1,6 +1,8 @@
query rebaseQuery($projectPath: ID!, $iid: String!) {
project(fullPath: $projectPath) {
+ id
mergeRequest(iid: $iid) {
+ id
rebaseInProgress
targetBranch
userPermissions {
diff --git a/app/assets/javascripts/vue_merge_request_widget/queries/toggle_draft.mutation.graphql b/app/assets/javascripts/vue_merge_request_widget/queries/toggle_draft.mutation.graphql
index 200fb1b7ca5..022629bb802 100644
--- a/app/assets/javascripts/vue_merge_request_widget/queries/toggle_draft.mutation.graphql
+++ b/app/assets/javascripts/vue_merge_request_widget/queries/toggle_draft.mutation.graphql
@@ -1,6 +1,7 @@
mutation toggleDraftStatus($projectPath: ID!, $iid: String!, $draft: Boolean!) {
mergeRequestSetDraft(input: { projectPath: $projectPath, iid: $iid, draft: $draft }) {
mergeRequest {
+ id
mergeableDiscussionsState
title
draft
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 10a2907c81a..57af869a0ba 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
@@ -59,7 +59,6 @@ export default class MergeRequestStore {
this.sourceBranch = data.source_branch;
this.sourceBranchProtected = data.source_branch_protected;
this.conflictsDocsPath = data.conflicts_docs_path;
- this.mergeTrainWhenPipelineSucceedsDocsPath = data.merge_train_when_pipeline_succeeds_docs_path;
this.commitMessage = data.default_merge_commit_message;
this.shortMergeCommitSha = data.short_merged_commit_sha;
this.mergeCommitSha = data.merged_commit_sha;
diff --git a/app/assets/javascripts/vue_shared/alert_details/components/sidebar/sidebar_assignees.vue b/app/assets/javascripts/vue_shared/alert_details/components/sidebar/sidebar_assignees.vue
index c24318cb9ad..489d4afa41f 100644
--- a/app/assets/javascripts/vue_shared/alert_details/components/sidebar/sidebar_assignees.vue
+++ b/app/assets/javascripts/vue_shared/alert_details/components/sidebar/sidebar_assignees.vue
@@ -220,16 +220,17 @@ export default {
class="gl-text-gray-900 gl-mb-2 gl-line-height-20 gl-display-flex gl-justify-content-space-between"
>
{{ __('Assignee') }}
- <a
+ <gl-button
v-if="isEditable"
ref="editButton"
- class="btn-link"
- href="#"
+ category="tertiary"
+ size="small"
+ class="gl-text-black-normal!"
@click="toggleFormDropdown"
@keydown.esc="hideDropdown"
>
{{ __('Edit') }}
- </a>
+ </gl-button>
</p>
<gl-dropdown
diff --git a/app/assets/javascripts/vue_shared/alert_details/components/sidebar/sidebar_status.vue b/app/assets/javascripts/vue_shared/alert_details/components/sidebar/sidebar_status.vue
index eaa5fc5af04..c512585b980 100644
--- a/app/assets/javascripts/vue_shared/alert_details/components/sidebar/sidebar_status.vue
+++ b/app/assets/javascripts/vue_shared/alert_details/components/sidebar/sidebar_status.vue
@@ -100,7 +100,8 @@ export default {
<gl-button
v-if="isEditable"
class="gl-text-black-normal!"
- variant="link"
+ category="tertiary"
+ size="small"
@click="toggleFormDropdown"
@keydown.esc="hideDropdown"
>
diff --git a/app/assets/javascripts/vue_shared/alert_details/graphql/mutations/alert_issue_create.mutation.graphql b/app/assets/javascripts/vue_shared/alert_details/graphql/mutations/alert_issue_create.mutation.graphql
index f0095abfca1..0460d250f75 100644
--- a/app/assets/javascripts/vue_shared/alert_details/graphql/mutations/alert_issue_create.mutation.graphql
+++ b/app/assets/javascripts/vue_shared/alert_details/graphql/mutations/alert_issue_create.mutation.graphql
@@ -2,6 +2,7 @@ mutation createAlertIssue($projectPath: ID!, $iid: String!) {
createAlertIssue(input: { iid: $iid, projectPath: $projectPath }) {
errors
issue {
+ id
iid
webUrl
}
diff --git a/app/assets/javascripts/vue_shared/alert_details/graphql/queries/alert_sidebar_details.query.graphql b/app/assets/javascripts/vue_shared/alert_details/graphql/queries/alert_sidebar_details.query.graphql
index 0c26fcc0ab2..0ea209ffd39 100644
--- a/app/assets/javascripts/vue_shared/alert_details/graphql/queries/alert_sidebar_details.query.graphql
+++ b/app/assets/javascripts/vue_shared/alert_details/graphql/queries/alert_sidebar_details.query.graphql
@@ -3,6 +3,7 @@
query alertDetailsAssignees($fullPath: ID!, $alertId: String) {
project(fullPath: $fullPath) {
+ id
alertManagementAlerts(iid: $alertId) {
nodes {
...AlertDetailItem
diff --git a/app/assets/javascripts/vue_shared/components/chronic_duration_input.vue b/app/assets/javascripts/vue_shared/components/chronic_duration_input.vue
new file mode 100644
index 00000000000..ffbcdefc924
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/chronic_duration_input.vue
@@ -0,0 +1,133 @@
+<script>
+import * as Sentry from '@sentry/browser';
+import { GlFormInput } from '@gitlab/ui';
+import {
+ DurationParseError,
+ outputChronicDuration,
+ parseChronicDuration,
+} from '~/chronic_duration';
+import { __ } from '~/locale';
+
+export default {
+ components: {
+ GlFormInput,
+ },
+ model: {
+ prop: 'value',
+ event: 'change',
+ },
+ props: {
+ value: {
+ type: Number,
+ required: false,
+ default: null,
+ },
+ name: {
+ type: String,
+ required: false,
+ default: null,
+ },
+ integerRequired: {
+ type: Boolean,
+ required: false,
+ default: true,
+ },
+ },
+ data() {
+ return {
+ numberData: this.value,
+ humanReadableData: this.convertDuration(this.value),
+ isValueValid: this.value === null ? null : true,
+ };
+ },
+ computed: {
+ numberValue: {
+ get() {
+ return this.numberData;
+ },
+ set(value) {
+ if (this.numberData !== value) {
+ this.numberData = value;
+ this.humanReadableData = this.convertDuration(value);
+ this.isValueValid = value === null ? null : true;
+ }
+ this.emitEvents();
+ },
+ },
+ humanReadableValue: {
+ get() {
+ return this.humanReadableData;
+ },
+ set(value) {
+ this.humanReadableData = value;
+ try {
+ if (value === '') {
+ this.numberData = null;
+ this.isValueValid = null;
+ } else {
+ this.numberData = parseChronicDuration(value, {
+ keepZero: true,
+ raiseExceptions: true,
+ });
+ this.isValueValid = true;
+ }
+ } catch (e) {
+ if (e instanceof DurationParseError) {
+ this.isValueValid = false;
+ } else {
+ Sentry.captureException(e);
+ }
+ }
+ this.emitEvents(true);
+ },
+ },
+ isValidDecimal() {
+ return !this.integerRequired || this.numberData === null || Number.isInteger(this.numberData);
+ },
+ feedback() {
+ if (this.isValueValid === false) {
+ return this.$options.i18n.INVALID_INPUT_FEEDBACK;
+ }
+ if (!this.isValidDecimal) {
+ return this.$options.i18n.INVALID_DECIMAL_FEEDBACK;
+ }
+ return '';
+ },
+ },
+ i18n: {
+ INVALID_INPUT_FEEDBACK: __('Please enter a valid time interval'),
+ INVALID_DECIMAL_FEEDBACK: __('An integer value is required for seconds'),
+ },
+ watch: {
+ value() {
+ this.numberValue = this.value;
+ },
+ },
+ mounted() {
+ this.emitEvents();
+ },
+ methods: {
+ convertDuration(value) {
+ return value === null ? '' : outputChronicDuration(value);
+ },
+ emitEvents(emitChange = false) {
+ if (emitChange && this.isValueValid !== false && this.isValidDecimal) {
+ this.$emit('change', this.numberData);
+ }
+ const { feedback } = this;
+ this.$refs.text.$el.setCustomValidity(feedback);
+ this.$refs.hidden.setCustomValidity(feedback);
+ this.$emit('valid', {
+ valid: this.isValueValid && this.isValidDecimal,
+ feedback,
+ });
+ },
+ },
+};
+</script>
+<template>
+ <div>
+ <gl-form-input ref="text" v-bind="$attrs" v-model="humanReadableValue" />
+ <input ref="hidden" type="hidden" :name="name" :value="numberValue" />
+ </div>
+</template>
diff --git a/app/assets/javascripts/vue_shared/components/clipboard_button.vue b/app/assets/javascripts/vue_shared/components/clipboard_button.vue
index fe329b18f30..400be3ef688 100644
--- a/app/assets/javascripts/vue_shared/components/clipboard_button.vue
+++ b/app/assets/javascripts/vue_shared/components/clipboard_button.vue
@@ -66,6 +66,11 @@ export default {
required: false,
default: 'medium',
},
+ variant: {
+ type: String,
+ required: false,
+ default: 'default',
+ },
},
computed: {
clipboardText() {
@@ -92,6 +97,7 @@ export default {
:size="size"
icon="copy-to-clipboard"
:aria-label="__('Copy this value')"
+ :variant="variant"
v-on="$listeners"
>
<slot></slot>
diff --git a/app/assets/javascripts/vue_shared/components/commit.vue b/app/assets/javascripts/vue_shared/components/commit.vue
index 5f50a699034..ebbc1bfb037 100644
--- a/app/assets/javascripts/vue_shared/components/commit.vue
+++ b/app/assets/javascripts/vue_shared/components/commit.vue
@@ -2,7 +2,7 @@
import { GlTooltipDirective, GlLink, GlIcon } from '@gitlab/ui';
import { isString, isEmpty } from 'lodash';
import { __, sprintf } from '~/locale';
-import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate.vue';
+import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate/tooltip_on_truncate.vue';
import UserAvatarLink from './user_avatar/user_avatar_link.vue';
export default {
diff --git a/app/assets/javascripts/vue_shared/components/confirm_danger/confirm_danger.vue b/app/assets/javascripts/vue_shared/components/confirm_danger/confirm_danger.vue
index 4c07cf44fed..f93415ced45 100644
--- a/app/assets/javascripts/vue_shared/components/confirm_danger/confirm_danger.vue
+++ b/app/assets/javascripts/vue_shared/components/confirm_danger/confirm_danger.vue
@@ -26,6 +26,11 @@ export default {
type: String,
required: true,
},
+ buttonClass: {
+ type: String,
+ required: false,
+ default: '',
+ },
buttonTestid: {
type: String,
required: false,
@@ -39,7 +44,7 @@ export default {
<div>
<gl-button
v-gl-modal="$options.modalId"
- class="gl-button"
+ :class="buttonClass"
variant="danger"
:disabled="disabled"
:data-testid="buttonTestid"
diff --git a/app/assets/javascripts/vue_shared/components/confirm_danger/confirm_danger_modal.vue b/app/assets/javascripts/vue_shared/components/confirm_danger/confirm_danger_modal.vue
index 30c96daf7e3..5bbe44b20b3 100644
--- a/app/assets/javascripts/vue_shared/components/confirm_danger/confirm_danger_modal.vue
+++ b/app/assets/javascripts/vue_shared/components/confirm_danger/confirm_danger_modal.vue
@@ -47,7 +47,7 @@ export default {
actionPrimary() {
return {
text: this.confirmButtonText,
- attributes: [{ variant: 'danger', disabled: !this.isValid }],
+ attributes: [{ variant: 'danger', disabled: !this.isValid, class: 'qa-confirm-button' }],
};
},
},
@@ -95,7 +95,7 @@ export default {
<gl-form-input
id="confirm_name_input"
v-model="confirmationPhrase"
- class="form-control"
+ class="form-control qa-confirm-input"
data-testid="confirm-danger-input"
type="text"
/>
diff --git a/app/assets/javascripts/vue_shared/components/confirm_modal.vue b/app/assets/javascripts/vue_shared/components/confirm_modal.vue
index 7c1d3772acd..72504e5bc50 100644
--- a/app/assets/javascripts/vue_shared/components/confirm_modal.vue
+++ b/app/assets/javascripts/vue_shared/components/confirm_modal.vue
@@ -2,10 +2,13 @@
import { GlModal, GlSafeHtmlDirective as SafeHtml } from '@gitlab/ui';
import { uniqueId } from 'lodash';
import csrf from '~/lib/utils/csrf';
+import eventHub, { EVENT_OPEN_CONFIRM_MODAL } from './confirm_modal_eventhub';
+import DomElementListener from './dom_element_listener.vue';
export default {
components: {
GlModal,
+ DomElementListener,
},
directives: {
SafeHtml,
@@ -30,18 +33,35 @@ export default {
};
},
mounted() {
- document.querySelectorAll(this.selector).forEach((button) => {
- button.addEventListener('click', (e) => {
- e.preventDefault();
-
- this.path = button.dataset.path;
- this.method = button.dataset.method;
- this.modalAttributes = JSON.parse(button.dataset.modalAttributes);
- this.openModal();
- });
- });
+ eventHub.$on(EVENT_OPEN_CONFIRM_MODAL, this.onOpenEvent);
+ },
+ destroyed() {
+ eventHub.$off(EVENT_OPEN_CONFIRM_MODAL, this.onOpenEvent);
},
methods: {
+ onButtonPress(e) {
+ const element = e.currentTarget;
+
+ if (!element.dataset.path) {
+ return;
+ }
+
+ const modalAttributes = element.dataset.modalAttributes
+ ? JSON.parse(element.dataset.modalAttributes)
+ : {};
+
+ this.onOpenEvent({
+ path: element.dataset.path,
+ method: element.dataset.method,
+ modalAttributes,
+ });
+ },
+ onOpenEvent({ path, method, modalAttributes }) {
+ this.path = path;
+ this.method = method;
+ this.modalAttributes = modalAttributes;
+ this.openModal();
+ },
openModal() {
this.$refs.modal.show();
},
@@ -61,21 +81,23 @@ export default {
</script>
<template>
- <gl-modal
- ref="modal"
- :modal-id="modalId"
- v-bind="modalAttributes"
- @primary="submitModal"
- @cancel="closeModal"
- >
- <form ref="form" :action="path" method="post">
- <!-- Rails workaround for <form method="delete" />
+ <dom-element-listener :selector="selector" @click.prevent="onButtonPress">
+ <gl-modal
+ ref="modal"
+ :modal-id="modalId"
+ v-bind="modalAttributes"
+ @primary="submitModal"
+ @cancel="closeModal"
+ >
+ <form ref="form" :action="path" method="post">
+ <!-- Rails workaround for <form method="delete" />
https://github.com/rails/rails/blob/master/actionview/app/assets/javascripts/rails-ujs/features/method.coffee
-->
- <input type="hidden" name="_method" :value="method" />
- <input type="hidden" name="authenticity_token" :value="$options.csrf.token" />
- <div v-if="modalAttributes.messageHtml" v-safe-html="modalAttributes.messageHtml"></div>
- <div v-else>{{ modalAttributes.message }}</div>
- </form>
- </gl-modal>
+ <input type="hidden" name="_method" :value="method" />
+ <input type="hidden" name="authenticity_token" :value="$options.csrf.token" />
+ <div v-if="modalAttributes.messageHtml" v-safe-html="modalAttributes.messageHtml"></div>
+ <div v-else>{{ modalAttributes.message }}</div>
+ </form>
+ </gl-modal>
+ </dom-element-listener>
</template>
diff --git a/app/assets/javascripts/vue_shared/components/confirm_modal_eventhub.js b/app/assets/javascripts/vue_shared/components/confirm_modal_eventhub.js
new file mode 100644
index 00000000000..f8d9d410ace
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/confirm_modal_eventhub.js
@@ -0,0 +1,5 @@
+import createEventHub from '~/helpers/event_hub_factory';
+
+export default createEventHub();
+
+export const EVENT_OPEN_CONFIRM_MODAL = Symbol('OPEN');
diff --git a/app/assets/javascripts/vue_shared/components/date_time_picker/date_time_picker.vue b/app/assets/javascripts/vue_shared/components/date_time_picker/date_time_picker.vue
index 1a96cabf755..e546ca57c5e 100644
--- a/app/assets/javascripts/vue_shared/components/date_time_picker/date_time_picker.vue
+++ b/app/assets/javascripts/vue_shared/components/date_time_picker/date_time_picker.vue
@@ -3,7 +3,7 @@ import { GlIcon, GlButton, GlDropdown, GlDropdownItem, GlFormGroup } from '@gitl
import { convertToFixedRange, isEqualTimeRanges, findTimeRange } from '~/lib/utils/datetime_range';
import { __, sprintf } from '~/locale';
-import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate.vue';
+import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate/tooltip_on_truncate.vue';
import DateTimePickerInput from './date_time_picker_input.vue';
import {
defaultTimeRanges,
diff --git a/app/assets/javascripts/design_management/components/design_note_pin.vue b/app/assets/javascripts/vue_shared/components/design_management/design_note_pin.vue
index 320e0654aab..cb038a8c4e1 100644
--- a/app/assets/javascripts/design_management/components/design_note_pin.vue
+++ b/app/assets/javascripts/vue_shared/components/design_management/design_note_pin.vue
@@ -10,13 +10,24 @@ export default {
props: {
position: {
type: Object,
- required: true,
+ required: false,
+ default: null,
},
label: {
type: Number,
required: false,
default: null,
},
+ isResolved: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ isInactive: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
},
computed: {
isNewNote() {
@@ -36,10 +47,13 @@ export default {
:style="position"
:aria-label="pinLabel"
:class="{
- 'btn-transparent comment-indicator gl-p-0': isNewNote,
- 'js-image-badge badge badge-pill': !isNewNote,
+ 'btn-transparent comment-indicator': isNewNote,
+ 'js-image-badge design-note-pin': !isNewNote,
+ resolved: isResolved,
+ inactive: isInactive,
+ 'gl-absolute': position,
}"
- class="gl-absolute gl-display-flex gl-align-items-center gl-justify-content-center gl-font-lg gl-outline-0!"
+ class="gl-display-flex gl-align-items-center gl-justify-content-center gl-font-sm"
type="button"
@mousedown="$emit('mousedown', $event)"
@mouseup="$emit('mouseup', $event)"
diff --git a/app/assets/javascripts/vue_shared/components/dismissible_alert.vue b/app/assets/javascripts/vue_shared/components/dismissible_alert.vue
index 52371e42ba1..0621ec14c6c 100644
--- a/app/assets/javascripts/vue_shared/components/dismissible_alert.vue
+++ b/app/assets/javascripts/vue_shared/components/dismissible_alert.vue
@@ -24,6 +24,7 @@ export default {
methods: {
dismiss() {
this.isDismissed = true;
+ this.$emit('alertDismissed');
},
},
};
diff --git a/app/assets/javascripts/vue_shared/components/dom_element_listener.vue b/app/assets/javascripts/vue_shared/components/dom_element_listener.vue
new file mode 100644
index 00000000000..ca427ed4897
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/dom_element_listener.vue
@@ -0,0 +1,28 @@
+<script>
+export default {
+ props: {
+ selector: {
+ type: String,
+ required: true,
+ },
+ },
+ mounted() {
+ this.disposables = Array.from(document.querySelectorAll(this.selector)).flatMap((button) => {
+ return Object.entries(this.$listeners).map(([key, value]) => {
+ button.addEventListener(key, value);
+ return () => {
+ button.removeEventListener(key, value);
+ };
+ });
+ });
+ },
+ destroyed() {
+ this.disposables.forEach((x) => {
+ x();
+ });
+ },
+ render() {
+ return this.$slots.default;
+ },
+};
+</script>
diff --git a/app/assets/javascripts/vue_shared/components/file_icon/file_icon_map.js b/app/assets/javascripts/vue_shared/components/file_icon/file_icon_map.js
index e1e71639115..8686d317c8a 100644
--- a/app/assets/javascripts/vue_shared/components/file_icon/file_icon_map.js
+++ b/app/assets/javascripts/vue_shared/components/file_icon/file_icon_map.js
@@ -6,15 +6,10 @@ const fileExtensionIcons = {
jade: 'pug',
pug: 'pug',
md: 'markdown',
- 'md.rendered': 'markdown',
markdown: 'markdown',
- 'markdown.rendered': 'markdown',
mdown: 'markdown',
- 'mdown.rendered': 'markdown',
mkd: 'markdown',
- 'mkd.rendered': 'markdown',
mkdn: 'markdown',
- 'mkdn.rendered': 'markdown',
rst: 'markdown',
blink: 'blink',
css: 'css',
@@ -23,7 +18,6 @@ const fileExtensionIcons = {
less: 'less',
json: 'json',
yaml: 'yaml',
- 'YAML-tmLanguage': 'yaml',
yml: 'yaml',
xml: 'xml',
plist: 'xml',
@@ -85,10 +79,7 @@ const fileExtensionIcons = {
props: 'settings',
toml: 'settings',
prefs: 'settings',
- 'sln.dotsettings': 'settings',
- 'sln.dotsettings.user': 'settings',
ts: 'typescript',
- 'd.ts': 'typescript-def',
marko: 'markojs',
pdf: 'pdf',
xlsx: 'table',
@@ -99,7 +90,6 @@ const fileExtensionIcons = {
vscodeignore: 'vscode',
vsixmanifest: 'vscode',
vsix: 'vscode',
- 'code-workplace': 'vscode',
suo: 'visualstudio',
sln: 'visualstudio',
csproj: 'visualstudio',
@@ -118,7 +108,6 @@ const fileExtensionIcons = {
xz: 'zip',
bzip2: 'zip',
gzip: 'zip',
- '7z': 'zip',
rar: 'zip',
tgz: 'zip',
exe: 'exe',
@@ -129,7 +118,6 @@ const fileExtensionIcons = {
c: 'c',
m: 'c',
h: 'h',
- 'c++': 'cpp',
cc: 'cpp',
cpp: 'cpp',
mm: 'cpp',
@@ -231,7 +219,6 @@ const fileExtensionIcons = {
m2v: 'movie',
vdi: 'virtual',
vbox: 'virtual',
- 'vbox-prev': 'virtual',
ics: 'email',
mp3: 'music',
flac: 'music',
@@ -277,44 +264,12 @@ const fileExtensionIcons = {
ml: 'ocaml',
mli: 'ocaml',
cmx: 'ocaml',
- 'js.map': 'javascript-map',
- 'css.map': 'css-map',
lock: 'lock',
hbs: 'handlebars',
mustache: 'handlebars',
pl: 'perl',
pm: 'perl',
hx: 'haxe',
- 'spec.ts': 'test-ts',
- 'test.ts': 'test-ts',
- 'ts.snap': 'test-ts',
- 'spec.tsx': 'test-jsx',
- 'test.tsx': 'test-jsx',
- 'tsx.snap': 'test-jsx',
- 'spec.jsx': 'test-jsx',
- 'test.jsx': 'test-jsx',
- 'jsx.snap': 'test-jsx',
- 'spec.js': 'test-js',
- 'test.js': 'test-js',
- 'js.snap': 'test-js',
- 'routing.ts': 'angular-routing',
- 'routing.js': 'angular-routing',
- 'module.ts': 'angular',
- 'module.js': 'angular',
- 'ng-template': 'angular',
- 'component.ts': 'angular-component',
- 'component.js': 'angular-component',
- 'guard.ts': 'angular-guard',
- 'guard.js': 'angular-guard',
- 'service.ts': 'angular-service',
- 'service.js': 'angular-service',
- 'pipe.ts': 'angular-pipe',
- 'pipe.js': 'angular-pipe',
- 'filter.js': 'angular-pipe',
- 'directive.ts': 'angular-directive',
- 'directive.js': 'angular-directive',
- 'resolver.ts': 'angular-resolver',
- 'resolver.js': 'angular-resolver',
pp: 'puppet',
ex: 'elixir',
exs: 'elixir',
@@ -345,11 +300,8 @@ const fileExtensionIcons = {
haml: 'haml',
yang: 'yang',
tf: 'terraform',
- 'tf.json': 'terraform',
tfvars: 'terraform',
tfstate: 'terraform',
- 'blade.php': 'laravel',
- 'inky.php': 'laravel',
applescript: 'applescript',
cake: 'cake',
feature: 'cucumber',
@@ -376,16 +328,68 @@ const fileExtensionIcons = {
kv: 'kivy',
graphcool: 'graphcool',
sbt: 'sbt',
+ cr: 'crystal',
+ cu: 'cuda',
+ cuh: 'cuda',
+ log: 'log',
+};
+
+const twoFileExtensionIcons = {
+ 'gradle.kts': 'gradle',
+ 'md.rendered': 'markdown',
+ 'markdown.rendered': 'markdown',
+ 'mdown.rendered': 'markdown',
+ 'mkd.rendered': 'markdown',
+ 'mkdn.rendered': 'markdown',
+ 'YAML-tmLanguage': 'yaml',
+ 'sln.dotsettings': 'settings',
+ 'sln.dotsettings.user': 'settings',
+ 'd.ts': 'typescript-def',
+ 'code-workplace': 'vscode',
+ '7z': 'zip',
+ 'c++': 'cpp',
+ 'vbox-prev': 'virtual',
+ 'js.map': 'javascript-map',
+ 'css.map': 'css-map',
+ 'spec.ts': 'test-ts',
+ 'test.ts': 'test-ts',
+ 'ts.snap': 'test-ts',
+ 'spec.tsx': 'test-jsx',
+ 'test.tsx': 'test-jsx',
+ 'tsx.snap': 'test-jsx',
+ 'spec.jsx': 'test-jsx',
+ 'test.jsx': 'test-jsx',
+ 'jsx.snap': 'test-jsx',
+ 'spec.js': 'test-js',
+ 'test.js': 'test-js',
+ 'js.snap': 'test-js',
+ 'routing.ts': 'angular-routing',
+ 'routing.js': 'angular-routing',
+ 'module.ts': 'angular',
+ 'module.js': 'angular',
+ 'ng-template': 'angular',
+ 'component.ts': 'angular-component',
+ 'component.js': 'angular-component',
+ 'guard.ts': 'angular-guard',
+ 'guard.js': 'angular-guard',
+ 'service.ts': 'angular-service',
+ 'service.js': 'angular-service',
+ 'pipe.ts': 'angular-pipe',
+ 'pipe.js': 'angular-pipe',
+ 'filter.js': 'angular-pipe',
+ 'directive.ts': 'angular-directive',
+ 'directive.js': 'angular-directive',
+ 'resolver.ts': 'angular-resolver',
+ 'resolver.js': 'angular-resolver',
+ 'tf.json': 'terraform',
+ 'blade.php': 'laravel',
+ 'inky.php': 'laravel',
'reducer.ts': 'ngrx-reducer',
'rootReducer.ts': 'ngrx-reducer',
'state.ts': 'ngrx-state',
'actions.ts': 'ngrx-actions',
'effects.ts': 'ngrx-effects',
- cr: 'crystal',
'drone.yml': 'drone',
- cu: 'cuda',
- cuh: 'cuda',
- log: 'log',
};
const fileNameIcons = {
@@ -598,6 +602,9 @@ const fileNameIcons = {
export default function getIconForFile(name) {
return (
- fileNameIcons[name] || fileExtensionIcons[name ? name.split('.').pop().toLowerCase() : ''] || ''
+ fileNameIcons[name] ||
+ twoFileExtensionIcons[name ? name.split('.').slice(-2).join('.') : ''] ||
+ fileExtensionIcons[name ? name.split('.').pop().toLowerCase() : ''] ||
+ ''
);
}
diff --git a/app/assets/javascripts/vue_shared/components/file_row.vue b/app/assets/javascripts/vue_shared/components/file_row.vue
index 0b0a416b7ef..2227047a909 100644
--- a/app/assets/javascripts/vue_shared/components/file_row.vue
+++ b/app/assets/javascripts/vue_shared/components/file_row.vue
@@ -146,6 +146,7 @@ export default {
ref="textOutput"
:style="levelIndentation"
class="file-row-name"
+ :title="file.name"
data-qa-selector="file_name_content"
:data-qa-file-name="file.name"
data-testid="file-row-name-container"
diff --git a/app/assets/javascripts/vue_shared/components/filtered_search_bar/constants.js b/app/assets/javascripts/vue_shared/components/filtered_search_bar/constants.js
index d9290e86bca..810d9f782b9 100644
--- a/app/assets/javascripts/vue_shared/components/filtered_search_bar/constants.js
+++ b/app/assets/javascripts/vue_shared/components/filtered_search_bar/constants.js
@@ -2,7 +2,6 @@ import { __ } from '~/locale';
export const DEBOUNCE_DELAY = 200;
export const MAX_RECENT_TOKENS_SIZE = 3;
-export const WEIGHT_TOKEN_SUGGESTIONS_SIZE = 21;
export const FILTER_NONE = 'None';
export const FILTER_ANY = 'Any';
@@ -24,22 +23,11 @@ export const DEFAULT_LABEL_NONE = { value: FILTER_NONE, text: __('None'), title:
export const DEFAULT_LABEL_ANY = { value: FILTER_ANY, text: __('Any'), title: __('Any') };
export const DEFAULT_NONE_ANY = [DEFAULT_LABEL_NONE, DEFAULT_LABEL_ANY];
-export const DEFAULT_ITERATIONS = DEFAULT_NONE_ANY.concat([
- { value: FILTER_CURRENT, text: __('Current') },
-]);
-
export const DEFAULT_MILESTONES = DEFAULT_NONE_ANY.concat([
{ value: FILTER_UPCOMING, text: __('Upcoming'), title: __('Upcoming') },
{ value: FILTER_STARTED, text: __('Started'), title: __('Started') },
]);
-export const DEFAULT_MILESTONES_GRAPHQL = [
- { value: 'any', text: __('Any'), title: __('Any') },
- { value: 'none', text: __('None'), title: __('None') },
- { value: '#upcoming', text: __('Upcoming'), title: __('Upcoming') },
- { value: '#started', text: __('Started'), title: __('Started') },
-];
-
export const SortDirection = {
descending: 'descending',
ascending: 'ascending',
@@ -56,6 +44,3 @@ export const TOKEN_TITLE_TYPE = __('Type');
export const TOKEN_TITLE_RELEASE = __('Release');
export const TOKEN_TITLE_MY_REACTION = __('My-Reaction');
export const TOKEN_TITLE_CONFIDENTIAL = __('Confidential');
-export const TOKEN_TITLE_ITERATION = __('Iteration');
-export const TOKEN_TITLE_EPIC = __('Epic');
-export const TOKEN_TITLE_WEIGHT = __('Weight');
diff --git a/app/assets/javascripts/vue_shared/components/filtered_search_bar/queries/epic.fragment.graphql b/app/assets/javascripts/vue_shared/components/filtered_search_bar/queries/epic.fragment.graphql
deleted file mode 100644
index 9e9bda8ad3e..00000000000
--- a/app/assets/javascripts/vue_shared/components/filtered_search_bar/queries/epic.fragment.graphql
+++ /dev/null
@@ -1,15 +0,0 @@
-fragment EpicNode on Epic {
- id
- iid
- group {
- fullPath
- }
- title
- state
- reference
- referencePath: reference(full: true)
- webPath
- webUrl
- createdAt
- closedAt
-}
diff --git a/app/assets/javascripts/vue_shared/components/filtered_search_bar/queries/search_epics.query.graphql b/app/assets/javascripts/vue_shared/components/filtered_search_bar/queries/search_epics.query.graphql
deleted file mode 100644
index 4bb4b586fc9..00000000000
--- a/app/assets/javascripts/vue_shared/components/filtered_search_bar/queries/search_epics.query.graphql
+++ /dev/null
@@ -1,16 +0,0 @@
-#import "./epic.fragment.graphql"
-
-query searchEpics($fullPath: ID!, $search: String, $state: EpicState) {
- group(fullPath: $fullPath) {
- epics(
- search: $search
- state: $state
- includeAncestorGroups: true
- includeDescendantGroups: false
- ) {
- nodes {
- ...EpicNode
- }
- }
- }
-}
diff --git a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/author_token.vue b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/author_token.vue
index b3b3d5c88c6..06478a89721 100644
--- a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/author_token.vue
+++ b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/author_token.vue
@@ -87,7 +87,6 @@ export default {
:get-active-token-value="getActiveAuthor"
:default-suggestions="defaultAuthors"
:preloaded-suggestions="preloadedAuthors"
- :recent-suggestions-storage-key="config.recentSuggestionsStorageKey"
@fetch-suggestions="fetchAuthors"
v-on="$listeners"
>
diff --git a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/base_token.vue b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/base_token.vue
index cee7c40aa83..bbc1888bc0b 100644
--- a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/base_token.vue
+++ b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/base_token.vue
@@ -4,12 +4,17 @@ import {
GlFilteredSearchSuggestion,
GlDropdownDivider,
GlDropdownSectionHeader,
+ GlDropdownText,
GlLoadingIcon,
} from '@gitlab/ui';
import { debounce } from 'lodash';
import { DEBOUNCE_DELAY, FILTER_NONE_ANY, OPERATOR_IS_NOT } from '../constants';
-import { getRecentlyUsedSuggestions, setTokenValueToRecentlyUsed } from '../filtered_search_utils';
+import {
+ getRecentlyUsedSuggestions,
+ setTokenValueToRecentlyUsed,
+ stripQuotes,
+} from '../filtered_search_utils';
export default {
components: {
@@ -17,6 +22,7 @@ export default {
GlFilteredSearchSuggestion,
GlDropdownDivider,
GlDropdownSectionHeader,
+ GlDropdownText,
GlLoadingIcon,
},
props: {
@@ -57,11 +63,6 @@ export default {
required: false,
default: () => [],
},
- recentSuggestionsStorageKey: {
- type: String,
- required: false,
- default: '',
- },
valueIdentifier: {
type: String,
required: false,
@@ -76,14 +77,14 @@ export default {
data() {
return {
searchKey: '',
- recentSuggestions: this.recentSuggestionsStorageKey
- ? getRecentlyUsedSuggestions(this.recentSuggestionsStorageKey)
+ recentSuggestions: this.config.recentSuggestionsStorageKey
+ ? getRecentlyUsedSuggestions(this.config.recentSuggestionsStorageKey)
: [],
};
},
computed: {
isRecentSuggestionsEnabled() {
- return Boolean(this.recentSuggestionsStorageKey);
+ return Boolean(this.config.recentSuggestionsStorageKey);
},
recentTokenIds() {
return this.recentSuggestions.map((tokenValue) => tokenValue[this.valueIdentifier]);
@@ -119,6 +120,9 @@ export default {
showDefaultSuggestions() {
return this.availableDefaultSuggestions.length > 0;
},
+ showNoMatchesText() {
+ return this.searchKey && !this.availableSuggestions.length;
+ },
showRecentSuggestions() {
return (
this.isRecentSuggestionsEnabled && this.recentSuggestions.length > 0 && !this.searchKey
@@ -163,11 +167,20 @@ export default {
this.searchKey = data;
if (!this.suggestionsLoading && !this.activeTokenValue) {
- const search = this.searchTerm ? this.searchTerm : data;
+ let search = this.searchTerm ? this.searchTerm : data;
+
+ if (search.startsWith('"') && search.endsWith('"')) {
+ search = stripQuotes(search);
+ } else if (search.startsWith('"')) {
+ search = search.slice(1, search.length);
+ }
+
this.$emit('fetch-suggestions', search);
}
}, DEBOUNCE_DELAY),
- handleTokenValueSelected(activeTokenValue) {
+ handleTokenValueSelected(selectedValue) {
+ const activeTokenValue = this.getActiveTokenValue(this.suggestions, selectedValue);
+
// Make sure that;
// 1. Recently used values feature is enabled
// 2. User has actually selected a value
@@ -177,7 +190,7 @@ export default {
activeTokenValue &&
!this.preloadedTokenIds.includes(activeTokenValue[this.valueIdentifier])
) {
- setTokenValueToRecentlyUsed(this.recentSuggestionsStorageKey, activeTokenValue);
+ setTokenValueToRecentlyUsed(this.config.recentSuggestionsStorageKey, activeTokenValue);
}
},
},
@@ -192,7 +205,7 @@ export default {
v-bind="$attrs"
v-on="$listeners"
@input="handleInput"
- @select="handleTokenValueSelected(activeTokenValue)"
+ @select="handleTokenValueSelected"
>
<template #view-token="viewTokenProps">
<slot name="view-token" :view-token-props="{ ...viewTokenProps, activeTokenValue }"></slot>
@@ -222,6 +235,9 @@ export default {
:suggestions="preloadedSuggestions"
></slot>
<gl-loading-icon v-if="suggestionsLoading" size="sm" />
+ <gl-dropdown-text v-else-if="showNoMatchesText">
+ {{ __('No matches found') }}
+ </gl-dropdown-text>
<template v-else>
<slot name="suggestions-list" :suggestions="availableSuggestions"></slot>
</template>
diff --git a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/epic_token.vue b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/epic_token.vue
deleted file mode 100644
index 9c2f5306654..00000000000
--- a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/epic_token.vue
+++ /dev/null
@@ -1,129 +0,0 @@
-<script>
-import { GlFilteredSearchSuggestion } from '@gitlab/ui';
-import createFlash from '~/flash';
-import { getIdFromGraphQLId } from '~/graphql_shared/utils';
-import { __ } from '~/locale';
-import { DEFAULT_NONE_ANY, FILTER_NONE_ANY, OPERATOR_IS_NOT } from '../constants';
-import searchEpicsQuery from '../queries/search_epics.query.graphql';
-
-import BaseToken from './base_token.vue';
-
-export default {
- prefix: '&',
- separator: '::',
- components: {
- BaseToken,
- GlFilteredSearchSuggestion,
- },
- props: {
- config: {
- type: Object,
- required: true,
- },
- value: {
- type: Object,
- required: true,
- },
- active: {
- type: Boolean,
- required: true,
- },
- },
- data() {
- return {
- epics: this.config.initialEpics || [],
- loading: false,
- };
- },
- computed: {
- idProperty() {
- return this.config.idProperty || 'iid';
- },
- currentValue() {
- const epicIid = Number(this.value.data);
- if (epicIid) {
- return epicIid;
- }
- return this.value.data;
- },
- defaultEpics() {
- return this.config.defaultEpics || DEFAULT_NONE_ANY;
- },
- availableDefaultEpics() {
- if (this.value.operator === OPERATOR_IS_NOT) {
- return this.defaultEpics.filter(
- (suggestion) => !FILTER_NONE_ANY.includes(suggestion.value),
- );
- }
- return this.defaultEpics;
- },
- },
- methods: {
- fetchEpics(search = '') {
- return this.$apollo
- .query({
- query: searchEpicsQuery,
- variables: { fullPath: this.config.fullPath, search },
- })
- .then(({ data }) => data.group?.epics.nodes);
- },
- fetchEpicsBySearchTerm(search) {
- this.loading = true;
- this.fetchEpics(search)
- .then((response) => {
- this.epics = Array.isArray(response) ? response : response?.data;
- })
- .catch(() => createFlash({ message: __('There was a problem fetching epics.') }))
- .finally(() => {
- this.loading = false;
- });
- },
- getActiveEpic(epics, data) {
- if (data && epics.length) {
- return epics.find((epic) => this.getValue(epic) === data);
- }
- return undefined;
- },
- getValue(epic) {
- return this.getEpicIdProperty(epic).toString();
- },
- displayValue(epic) {
- return `${this.$options.prefix}${this.getEpicIdProperty(epic)}${this.$options.separator}${
- epic?.title
- }`;
- },
- getEpicIdProperty(epic) {
- return getIdFromGraphQLId(epic[this.idProperty]);
- },
- },
-};
-</script>
-
-<template>
- <base-token
- :config="config"
- :value="value"
- :active="active"
- :suggestions-loading="loading"
- :suggestions="epics"
- :get-active-token-value="getActiveEpic"
- :default-suggestions="availableDefaultEpics"
- :recent-suggestions-storage-key="config.recentSuggestionsStorageKey"
- search-by="title"
- @fetch-suggestions="fetchEpicsBySearchTerm"
- v-on="$listeners"
- >
- <template #view="{ viewTokenProps: { inputValue, activeTokenValue } }">
- {{ activeTokenValue ? displayValue(activeTokenValue) : inputValue }}
- </template>
- <template #suggestions-list="{ suggestions }">
- <gl-filtered-search-suggestion
- v-for="epic in suggestions"
- :key="epic.id"
- :value="getValue(epic)"
- >
- {{ epic.title }}
- </gl-filtered-search-suggestion>
- </template>
- </base-token>
-</template>
diff --git a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/iteration_token.vue b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/iteration_token.vue
deleted file mode 100644
index aff93ebc9c0..00000000000
--- a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/iteration_token.vue
+++ /dev/null
@@ -1,138 +0,0 @@
-<script>
-import { GlDropdownDivider, GlDropdownSectionHeader, GlFilteredSearchSuggestion } from '@gitlab/ui';
-import createFlash from '~/flash';
-import { getIdFromGraphQLId } from '~/graphql_shared/utils';
-import { __ } from '~/locale';
-import BaseToken from '~/vue_shared/components/filtered_search_bar/tokens/base_token.vue';
-import { formatDate } from '~/lib/utils/datetime_utility';
-import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
-import { DEFAULT_ITERATIONS } from '../constants';
-
-export default {
- components: {
- BaseToken,
- GlDropdownDivider,
- GlDropdownSectionHeader,
- GlFilteredSearchSuggestion,
- },
- mixins: [glFeatureFlagMixin()],
- props: {
- active: {
- type: Boolean,
- required: true,
- },
- config: {
- type: Object,
- required: true,
- },
- value: {
- type: Object,
- required: true,
- },
- },
- data() {
- return {
- iterations: this.config.initialIterations || [],
- loading: false,
- };
- },
- computed: {
- defaultIterations() {
- return this.config.defaultIterations || DEFAULT_ITERATIONS;
- },
- },
- methods: {
- getActiveIteration(iterations, data) {
- return iterations.find((iteration) => this.getValue(iteration) === data);
- },
- groupIterationsByCadence(iterations) {
- const cadences = [];
- iterations.forEach((iteration) => {
- if (!iteration.iterationCadence) {
- return;
- }
- const { title } = iteration.iterationCadence;
- const cadenceIteration = {
- id: iteration.id,
- title: iteration.title,
- period: this.getIterationPeriod(iteration),
- };
- const cadence = cadences.find((cad) => cad.title === title);
- if (cadence) {
- cadence.iterations.push(cadenceIteration);
- } else {
- cadences.push({ title, iterations: [cadenceIteration] });
- }
- });
- return cadences;
- },
- fetchIterations(searchTerm) {
- this.loading = true;
- this.config
- .fetchIterations(searchTerm)
- .then((response) => {
- this.iterations = Array.isArray(response) ? response : response.data;
- })
- .catch(() => {
- createFlash({ message: __('There was a problem fetching iterations.') });
- })
- .finally(() => {
- this.loading = false;
- });
- },
- getValue(iteration) {
- return String(getIdFromGraphQLId(iteration.id));
- },
- /**
- * TODO: https://gitlab.com/gitlab-org/gitlab/-/issues/344619
- * This method also exists as a utility function in ee/../iterations/utils.js
- * Remove the duplication when iteration token is moved to EE.
- */
- getIterationPeriod({ startDate, dueDate }) {
- const start = formatDate(startDate, 'mmm d, yyyy', true);
- const due = formatDate(dueDate, 'mmm d, yyyy', true);
- return `${start} - ${due}`;
- },
- },
-};
-</script>
-
-<template>
- <base-token
- :active="active"
- :config="config"
- :value="value"
- :default-suggestions="defaultIterations"
- :suggestions="iterations"
- :suggestions-loading="loading"
- :get-active-token-value="getActiveIteration"
- @fetch-suggestions="fetchIterations"
- v-on="$listeners"
- >
- <template #view="{ viewTokenProps: { inputValue, activeTokenValue } }">
- {{ activeTokenValue ? activeTokenValue.title : inputValue }}
- </template>
- <template #suggestions-list="{ suggestions }">
- <template v-for="(cadence, index) in groupIterationsByCadence(suggestions)">
- <gl-dropdown-divider v-if="index !== 0" :key="index" />
- <gl-dropdown-section-header
- :key="cadence.title"
- class="gl-overflow-hidden"
- :title="cadence.title"
- >
- {{ cadence.title }}
- </gl-dropdown-section-header>
- <gl-filtered-search-suggestion
- v-for="iteration in cadence.iterations"
- :key="iteration.id"
- :value="getValue(iteration)"
- >
- {{ iteration.title }}
- <div v-if="glFeatures.iterationCadences" class="gl-text-gray-400">
- {{ iteration.period }}
- </div>
- </gl-filtered-search-suggestion>
- </template>
- </template>
- </base-token>
-</template>
diff --git a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/label_token.vue b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/label_token.vue
index c31f3a25fb1..3f7a8920f48 100644
--- a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/label_token.vue
+++ b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/label_token.vue
@@ -104,7 +104,6 @@ export default {
:suggestions="labels"
:get-active-token-value="getActiveLabel"
:default-suggestions="defaultLabels"
- :recent-suggestions-storage-key="config.recentSuggestionsStorageKey"
@fetch-suggestions="fetchLabels"
v-on="$listeners"
>
diff --git a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/milestone_token.vue b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/milestone_token.vue
index 523438f459c..0d3394788fa 100644
--- a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/milestone_token.vue
+++ b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/milestone_token.vue
@@ -2,7 +2,7 @@
import { GlFilteredSearchSuggestion } from '@gitlab/ui';
import createFlash from '~/flash';
import { __ } from '~/locale';
-import { sortMilestonesByDueDate } from '~/milestones/milestone_utils';
+import { sortMilestonesByDueDate } from '~/milestones/utils';
import BaseToken from '~/vue_shared/components/filtered_search_bar/tokens/base_token.vue';
import { DEFAULT_MILESTONES } from '../constants';
import { stripQuotes } from '../filtered_search_utils';
diff --git a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/weight_token.vue b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/weight_token.vue
deleted file mode 100644
index 280fb234576..00000000000
--- a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/weight_token.vue
+++ /dev/null
@@ -1,66 +0,0 @@
-<script>
-import { GlFilteredSearchSuggestion } from '@gitlab/ui';
-import BaseToken from '~/vue_shared/components/filtered_search_bar/tokens/base_token.vue';
-import { DEFAULT_NONE_ANY, WEIGHT_TOKEN_SUGGESTIONS_SIZE } from '../constants';
-
-const weights = Array.from(Array(WEIGHT_TOKEN_SUGGESTIONS_SIZE), (_, index) => index.toString());
-
-export default {
- components: {
- BaseToken,
- GlFilteredSearchSuggestion,
- },
- props: {
- active: {
- type: Boolean,
- required: true,
- },
- config: {
- type: Object,
- required: true,
- },
- value: {
- type: Object,
- required: true,
- },
- },
- data() {
- return {
- weights,
- };
- },
- computed: {
- defaultWeights() {
- return this.config.defaultWeights || DEFAULT_NONE_ANY;
- },
- },
- methods: {
- getActiveWeight(weightSuggestions, data) {
- return weightSuggestions.find((weight) => weight === data);
- },
- updateWeights(searchTerm) {
- const weight = parseInt(searchTerm, 10);
- this.weights = Number.isNaN(weight) ? weights : [String(weight)];
- },
- },
-};
-</script>
-
-<template>
- <base-token
- :active="active"
- :config="config"
- :value="value"
- :default-suggestions="defaultWeights"
- :suggestions="weights"
- :get-active-token-value="getActiveWeight"
- @fetch-suggestions="updateWeights"
- v-on="$listeners"
- >
- <template #suggestions-list="{ suggestions }">
- <gl-filtered-search-suggestion v-for="weight of suggestions" :key="weight" :value="weight">
- {{ weight }}
- </gl-filtered-search-suggestion>
- </template>
- </base-token>
-</template>
diff --git a/app/assets/javascripts/vue_shared/components/form/input_copy_toggle_visibility.stories.js b/app/assets/javascripts/vue_shared/components/form/input_copy_toggle_visibility.stories.js
new file mode 100644
index 00000000000..cdd7a074f34
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/form/input_copy_toggle_visibility.stories.js
@@ -0,0 +1,27 @@
+import InputCopyToggleVisibility from './input_copy_toggle_visibility.vue';
+
+export default {
+ component: InputCopyToggleVisibility,
+ title: 'vue_shared/components/form/input_copy_toggle_visibility',
+};
+
+const defaultProps = {
+ value: 'hR8x1fuJbzwu5uFKLf9e',
+ formInputGroupProps: { class: 'gl-form-input-xl' },
+};
+
+const Template = (args, { argTypes }) => ({
+ components: { InputCopyToggleVisibility },
+ props: Object.keys(argTypes),
+ template: `<input-copy-toggle-visibility
+ :value="value"
+ :initial-visibility="initialVisibility"
+ :show-toggle-visibility-button="showToggleVisibilityButton"
+ :show-copy-button="showCopyButton"
+ :form-input-group-props="formInputGroupProps"
+ :copy-button-title="copyButtonTitle"
+ />`,
+});
+
+export const Default = Template.bind({});
+Default.args = defaultProps;
diff --git a/app/assets/javascripts/vue_shared/components/form/input_copy_toggle_visibility.vue b/app/assets/javascripts/vue_shared/components/form/input_copy_toggle_visibility.vue
new file mode 100644
index 00000000000..06949b59823
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/form/input_copy_toggle_visibility.vue
@@ -0,0 +1,127 @@
+<script>
+import { GlFormInputGroup, GlFormGroup, GlButton, GlTooltipDirective } from '@gitlab/ui';
+
+import { __ } from '~/locale';
+import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
+
+export default {
+ name: 'InputCopyToggleVisibility',
+ i18n: {
+ toggleVisibilityLabelHide: __('Click to hide'),
+ toggleVisibilityLabelReveal: __('Click to reveal'),
+ },
+ components: {
+ GlFormInputGroup,
+ GlFormGroup,
+ GlButton,
+ ClipboardButton,
+ },
+ directives: {
+ GlTooltip: GlTooltipDirective,
+ },
+ props: {
+ value: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ initialVisibility: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ showToggleVisibilityButton: {
+ type: Boolean,
+ required: false,
+ default: true,
+ },
+ showCopyButton: {
+ type: Boolean,
+ required: false,
+ default: true,
+ },
+ copyButtonTitle: {
+ type: String,
+ required: false,
+ default: __('Copy'),
+ },
+ formInputGroupProps: {
+ type: Object,
+ required: false,
+ default() {
+ return {};
+ },
+ },
+ },
+ data() {
+ return {
+ valueIsVisible: this.initialVisibility,
+ };
+ },
+ computed: {
+ toggleVisibilityLabel() {
+ return this.valueIsVisible
+ ? this.$options.i18n.toggleVisibilityLabelHide
+ : this.$options.i18n.toggleVisibilityLabelReveal;
+ },
+ toggleVisibilityIcon() {
+ return this.valueIsVisible ? 'eye-slash' : 'eye';
+ },
+ computedValueIsVisible() {
+ return !this.showToggleVisibilityButton || this.valueIsVisible;
+ },
+ displayedValue() {
+ return this.computedValueIsVisible ? this.value : '*'.repeat(this.value.length || 20);
+ },
+ },
+ methods: {
+ handleToggleVisibilityButtonClick() {
+ this.valueIsVisible = !this.valueIsVisible;
+
+ this.$emit('visibility-change', this.valueIsVisible);
+ },
+ handleCopyButtonClick() {
+ this.$emit('copy');
+ },
+ handleFormInputCopy(event) {
+ if (this.computedValueIsVisible) {
+ return;
+ }
+
+ event.clipboardData.setData('text/plain', this.value);
+ event.preventDefault();
+ },
+ },
+};
+</script>
+<template>
+ <gl-form-group v-bind="$attrs">
+ <gl-form-input-group
+ :value="displayedValue"
+ input-class="gl-font-monospace! gl-cursor-default!"
+ select-on-click
+ readonly
+ v-bind="formInputGroupProps"
+ @copy="handleFormInputCopy"
+ >
+ <template v-if="showToggleVisibilityButton || showCopyButton" #append>
+ <gl-button
+ v-if="showToggleVisibilityButton"
+ v-gl-tooltip.hover="toggleVisibilityLabel"
+ :aria-label="toggleVisibilityLabel"
+ :icon="toggleVisibilityIcon"
+ @click="handleToggleVisibilityButtonClick"
+ />
+ <clipboard-button
+ v-if="showCopyButton"
+ :text="value"
+ :title="copyButtonTitle"
+ @click="handleCopyButtonClick"
+ />
+ </template>
+ </gl-form-input-group>
+ <template v-for="slot in Object.keys($slots)" #[slot]>
+ <slot :name="slot"></slot>
+ </template>
+ </gl-form-group>
+</template>
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 6ace0bd88f8..9bff469b670 100644
--- a/app/assets/javascripts/vue_shared/components/header_ci_component.vue
+++ b/app/assets/javascripts/vue_shared/components/header_ci_component.vue
@@ -5,6 +5,7 @@ import {
GlSafeHtmlDirective,
GlAvatarLink,
GlAvatarLabeled,
+ GlTooltip,
} from '@gitlab/ui';
import { isGid, getIdFromGraphQLId } from '~/graphql_shared/utils';
import { glEmojiTag } from '../../emoji';
@@ -26,6 +27,7 @@ export default {
GlButton,
GlAvatarLink,
GlAvatarLabeled,
+ GlTooltip,
},
directives: {
GlTooltip: GlTooltipDirective,
diff --git a/app/assets/javascripts/vue_shared/components/issuable/init_issuable_header_warning.js b/app/assets/javascripts/vue_shared/components/issuable/init_issuable_header_warning.js
deleted file mode 100644
index 28aa93d6680..00000000000
--- a/app/assets/javascripts/vue_shared/components/issuable/init_issuable_header_warning.js
+++ /dev/null
@@ -1,22 +0,0 @@
-import Vue from 'vue';
-import { parseBoolean } from '~/lib/utils/common_utils';
-import IssuableHeaderWarnings from './issuable_header_warnings.vue';
-
-export default function issuableHeaderWarnings(store) {
- const el = document.getElementById('js-issuable-header-warnings');
-
- if (!el) {
- return false;
- }
-
- const { hidden } = el.dataset;
-
- return new Vue({
- el,
- store,
- provide: { hidden: parseBoolean(hidden) },
- render(createElement) {
- return createElement(IssuableHeaderWarnings);
- },
- });
-}
diff --git a/app/assets/javascripts/vue_shared/components/line_numbers.vue b/app/assets/javascripts/vue_shared/components/line_numbers.vue
new file mode 100644
index 00000000000..7e17cca3dcc
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/line_numbers.vue
@@ -0,0 +1,57 @@
+<script>
+import { GlIcon, GlLink } from '@gitlab/ui';
+
+export default {
+ components: {
+ GlIcon,
+ GlLink,
+ },
+ props: {
+ lines: {
+ type: Number,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ currentlyHighlightedLine: null,
+ };
+ },
+ mounted() {
+ this.scrollToLine();
+ },
+ methods: {
+ scrollToLine(hash = window.location.hash) {
+ const lineToHighlight = hash && this.$el.querySelector(hash);
+
+ if (!lineToHighlight) {
+ return;
+ }
+
+ if (this.currentlyHighlightedLine) {
+ this.currentlyHighlightedLine.classList.remove('hll');
+ }
+
+ lineToHighlight.classList.add('hll');
+ this.currentlyHighlightedLine = lineToHighlight;
+ lineToHighlight.scrollIntoView({ behavior: 'smooth', block: 'center' });
+ },
+ },
+};
+</script>
+<template>
+ <div class="line-numbers">
+ <gl-link
+ v-for="line in lines"
+ :id="`L${line}`"
+ :key="line"
+ class="diff-line-num"
+ :href="`#L${line}`"
+ :data-line-number="line"
+ @click="scrollToLine(`#L${line}`)"
+ >
+ <gl-icon :size="12" name="link" />
+ {{ line }}
+ </gl-link>
+ </div>
+</template>
diff --git a/app/assets/javascripts/vue_shared/components/markdown/suggestions.vue b/app/assets/javascripts/vue_shared/components/markdown/suggestions.vue
index e36cfb3b275..2f6776f835e 100644
--- a/app/assets/javascripts/vue_shared/components/markdown/suggestions.vue
+++ b/app/assets/javascripts/vue_shared/components/markdown/suggestions.vue
@@ -165,6 +165,6 @@ export default {
<template>
<div>
<div class="flash-container js-suggestions-flash"></div>
- <div v-show="isRendered" ref="container" v-safe-html="noteHtml" class="md"></div>
+ <div v-show="isRendered" ref="container" v-safe-html="noteHtml" class="md suggestions"></div>
</div>
</template>
diff --git a/app/assets/javascripts/vue_shared/components/markdown/toolbar.vue b/app/assets/javascripts/vue_shared/components/markdown/toolbar.vue
index 912aa8ce294..f1c293c87f4 100644
--- a/app/assets/javascripts/vue_shared/components/markdown/toolbar.vue
+++ b/app/assets/javascripts/vue_shared/components/markdown/toolbar.vue
@@ -1,18 +1,13 @@
<script>
import { GlButton, GlLink, GlLoadingIcon, GlSprintf, GlIcon } from '@gitlab/ui';
-import { isExperimentVariant } from '~/experimentation/utils';
-import InviteMembersTrigger from '~/invite_members/components/invite_members_trigger.vue';
-import { INVITE_MEMBERS_IN_COMMENT } from '~/invite_members/constants';
export default {
- inviteMembersInComment: INVITE_MEMBERS_IN_COMMENT,
components: {
GlButton,
GlLink,
GlLoadingIcon,
GlSprintf,
GlIcon,
- InviteMembersTrigger,
},
props: {
markdownDocsPath: {
@@ -34,9 +29,6 @@ export default {
hasQuickActionsDocsPath() {
return this.quickActionsDocsPath !== '';
},
- inviteCommentEnabled() {
- return isExperimentVariant(INVITE_MEMBERS_IN_COMMENT, 'invite_member_link');
- },
},
};
</script>
@@ -67,16 +59,6 @@ export default {
</template>
</div>
<span v-if="canAttachFile" class="uploading-container">
- <invite-members-trigger
- v-if="inviteCommentEnabled"
- classes="gl-mr-3 gl-vertical-align-text-bottom"
- :display-text="s__('InviteMember|Invite Member')"
- icon="assignee"
- variant="link"
- :track-experiment="$options.inviteMembersInComment"
- :trigger-source="$options.inviteMembersInComment"
- data-track-action="comment_invite_click"
- />
<span class="uploading-progress-container hide">
<gl-icon name="media" />
<span class="attaching-file-message"></span>
diff --git a/app/assets/javascripts/vue_shared/components/namespace_select/namespace_select.vue b/app/assets/javascripts/vue_shared/components/namespace_select/namespace_select.vue
new file mode 100644
index 00000000000..7d2af7983d1
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/namespace_select/namespace_select.vue
@@ -0,0 +1,93 @@
+<script>
+import { GlDropdown, GlDropdownItem, GlDropdownSectionHeader, GlSearchBoxByType } from '@gitlab/ui';
+import { __ } from '~/locale';
+
+export const i18n = {
+ DEFAULT_TEXT: __('Select a new namespace'),
+ GROUPS: __('Groups'),
+ USERS: __('Users'),
+};
+
+const filterByName = (data, searchTerm = '') =>
+ data.filter((d) => d.humanName.toLowerCase().includes(searchTerm));
+
+export default {
+ name: 'NamespaceSelect',
+ components: {
+ GlDropdown,
+ GlDropdownItem,
+ GlDropdownSectionHeader,
+ GlSearchBoxByType,
+ },
+ props: {
+ data: {
+ type: Object,
+ required: true,
+ },
+ fullWidth: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ },
+ data() {
+ return {
+ searchTerm: '',
+ selectedNamespace: null,
+ };
+ },
+ computed: {
+ hasUserNamespaces() {
+ return this.data.user?.length;
+ },
+ hasGroupNamespaces() {
+ return this.data.group?.length;
+ },
+ filteredGroupNamespaces() {
+ if (!this.hasGroupNamespaces) return [];
+ return filterByName(this.data.group, this.searchTerm);
+ },
+ filteredUserNamespaces() {
+ if (!this.hasUserNamespaces) return [];
+ return filterByName(this.data.user, this.searchTerm);
+ },
+ selectedNamespaceText() {
+ return this.selectedNamespace?.humanName || this.$options.i18n.DEFAULT_TEXT;
+ },
+ },
+ methods: {
+ handleSelect(item) {
+ this.selectedNamespace = item;
+ this.$emit('select', item);
+ },
+ },
+ i18n,
+};
+</script>
+<template>
+ <gl-dropdown :text="selectedNamespaceText" :block="fullWidth">
+ <template #header>
+ <gl-search-box-by-type v-model.trim="searchTerm" />
+ </template>
+ <div v-if="hasGroupNamespaces" class="qa-namespaces-list-groups">
+ <gl-dropdown-section-header>{{ $options.i18n.GROUPS }}</gl-dropdown-section-header>
+ <gl-dropdown-item
+ v-for="item in filteredGroupNamespaces"
+ :key="item.id"
+ class="qa-namespaces-list-item"
+ @click="handleSelect(item)"
+ >{{ item.humanName }}</gl-dropdown-item
+ >
+ </div>
+ <div v-if="hasUserNamespaces" class="qa-namespaces-list-users">
+ <gl-dropdown-section-header>{{ $options.i18n.USERS }}</gl-dropdown-section-header>
+ <gl-dropdown-item
+ v-for="item in filteredUserNamespaces"
+ :key="item.id"
+ class="qa-namespaces-list-item"
+ @click="handleSelect(item)"
+ >{{ item.humanName }}</gl-dropdown-item
+ >
+ </div>
+ </gl-dropdown>
+</template>
diff --git a/app/assets/javascripts/vue_shared/components/notes/placeholder_note.vue b/app/assets/javascripts/vue_shared/components/notes/placeholder_note.vue
index 9ea14ed506c..624dbcc6d8e 100644
--- a/app/assets/javascripts/vue_shared/components/notes/placeholder_note.vue
+++ b/app/assets/javascripts/vue_shared/components/notes/placeholder_note.vue
@@ -39,6 +39,11 @@ export default {
required: false,
default: null,
},
+ isOverviewTab: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
},
computed: {
...mapGetters(['getUserData']),
@@ -46,9 +51,10 @@ export default {
return renderMarkdown(this.note.body);
},
avatarSize() {
- if (this.line) {
- return 16;
+ if (this.line && !this.isOverviewTab) {
+ return 24;
}
+
return 40;
},
},
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 8877cfa39fb..1963d1aa7fe 100644
--- a/app/assets/javascripts/vue_shared/components/notes/system_note.vue
+++ b/app/assets/javascripts/vue_shared/components/notes/system_note.vue
@@ -141,6 +141,7 @@ export default {
variant="link"
:icon="descriptionVersionToggleIcon"
data-testid="compare-btn"
+ class="gl-vertical-align-text-bottom"
@click="toggleDescriptionVersion"
>{{ __('Compare with previous version') }}</gl-button
>
@@ -149,6 +150,7 @@ export default {
:icon="showLines ? 'chevron-up' : 'chevron-down'"
variant="link"
data-testid="outdated-lines-change-btn"
+ class="gl-vertical-align-text-bottom"
@click="toggleDiff"
>
{{ __('Compare changes') }}
diff --git a/app/assets/javascripts/vue_shared/components/pagination_bar/pagination_bar.stories.js b/app/assets/javascripts/vue_shared/components/pagination_bar/pagination_bar.stories.js
new file mode 100644
index 00000000000..e31446f4bb8
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/pagination_bar/pagination_bar.stories.js
@@ -0,0 +1,40 @@
+/* eslint-disable @gitlab/require-i18n-strings */
+import PaginationBar from './pagination_bar.vue';
+
+export default {
+ component: PaginationBar,
+ title: 'vue_shared/components/pagination_bar/pagination_bar',
+};
+
+const Template = (args, { argTypes }) => ({
+ components: { PaginationBar },
+ props: Object.keys(argTypes),
+ template: `<pagination-bar v-bind="$props" v-on="{ 'set-page-size': setPageSize, 'set-page': setPage }" />`,
+});
+
+export const Default = Template.bind({});
+
+Default.args = {
+ pageInfo: {
+ perPage: 20,
+ page: 2,
+ total: 83,
+ totalPages: 5,
+ },
+ pageSizes: [20, 50, 100],
+};
+
+Default.argTypes = {
+ pageInfo: {
+ description: 'Page info object',
+ control: { type: 'object' },
+ },
+ pageSizes: {
+ description: 'Array of possible page sizes',
+ control: { type: 'array' },
+ },
+
+ // events
+ setPageSize: { action: 'set-page-size' },
+ setPage: { action: 'set-page' },
+};
diff --git a/app/assets/javascripts/import_entities/components/pagination_bar.vue b/app/assets/javascripts/vue_shared/components/pagination_bar/pagination_bar.vue
index 33bd3e08bb1..b4d565991f5 100644
--- a/app/assets/javascripts/import_entities/components/pagination_bar.vue
+++ b/app/assets/javascripts/vue_shared/components/pagination_bar/pagination_bar.vue
@@ -23,10 +23,6 @@ export default {
type: Array,
default: () => DEFAULT_PAGE_SIZES,
},
- itemsCount: {
- required: true,
- type: Number,
- },
},
computed: {
@@ -35,9 +31,10 @@ export default {
},
paginationInfo() {
- const { page, perPage } = this.pageInfo;
+ const { page, perPage, totalPages, total } = this.pageInfo;
+ const itemsCount = page === totalPages ? total - (page - 1) * perPage : perPage;
const start = (page - 1) * perPage + 1;
- const end = start + this.itemsCount - 1;
+ const end = start + itemsCount - 1;
return { start, end };
},
@@ -45,8 +42,24 @@ export default {
methods: {
setPage(page) {
+ // eslint-disable-next-line spaced-comment
+ /**
+ * Emitted when selected page is updated
+ *
+ * @event set-page
+ **/
this.$emit('set-page', page);
},
+
+ setPageSize(pageSize) {
+ // eslint-disable-next-line spaced-comment
+ /**
+ * Emitted when page size is updated
+ *
+ * @event set-page-size
+ **/
+ this.$emit('set-page-size', pageSize);
+ },
},
};
</script>
@@ -54,7 +67,7 @@ export default {
<template>
<div class="gl-display-flex gl-align-items-center">
<pagination-links :change="setPage" :page-info="pageInfo" class="gl-m-0" />
- <gl-dropdown category="tertiary" class="gl-ml-auto">
+ <gl-dropdown category="tertiary" class="gl-ml-auto" data-testid="page-size">
<template #button-content>
<span class="gl-font-weight-bold">
<gl-sprintf :message="__('%{count} items per page')">
@@ -65,7 +78,7 @@ export default {
</span>
<gl-icon class="gl-button-icon dropdown-chevron" name="chevron-down" />
</template>
- <gl-dropdown-item v-for="size in pageSizes" :key="size" @click="$emit('set-page-size', size)">
+ <gl-dropdown-item v-for="size in pageSizes" :key="size" @click="setPageSize(size)">
<gl-sprintf :message="__('%{count} items per page')">
<template #count>
{{ size }}
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 933a215112b..6bb321713d5 100644
--- a/app/assets/javascripts/vue_shared/components/registry/list_item.vue
+++ b/app/assets/javascripts/vue_shared/components/registry/list_item.vue
@@ -54,10 +54,10 @@ 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-3 gl-px-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-sm-display-flex gl-justify-content-start gl-pl-2"
+ class="gl-w-7 gl-display-flex gl-justify-content-start gl-pl-2"
>
<slot name="left-action"></slot>
</div>
@@ -105,7 +105,7 @@ export default {
</div>
<div
v-if="$slots['right-action']"
- class="gl-w-9 gl-display-none gl-sm-display-flex gl-justify-content-end gl-pr-1"
+ class="gl-w-9 gl-display-flex gl-justify-content-end gl-pr-1"
>
<slot name="right-action"></slot>
</div>
diff --git a/app/assets/javascripts/vue_shared/components/registry/metadata_item.vue b/app/assets/javascripts/vue_shared/components/registry/metadata_item.vue
index 93396219a54..4c2816b63b2 100644
--- a/app/assets/javascripts/vue_shared/components/registry/metadata_item.vue
+++ b/app/assets/javascripts/vue_shared/components/registry/metadata_item.vue
@@ -1,6 +1,6 @@
<script>
import { GlIcon, GlLink, GlTooltipDirective } from '@gitlab/ui';
-import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate.vue';
+import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate/tooltip_on_truncate.vue';
export default {
name: 'MetadataItem',
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/collapsed_grouped_date_picker.vue b/app/assets/javascripts/vue_shared/components/sidebar/collapsed_grouped_date_picker.vue
deleted file mode 100644
index a1dca65a423..00000000000
--- a/app/assets/javascripts/vue_shared/components/sidebar/collapsed_grouped_date_picker.vue
+++ /dev/null
@@ -1,101 +0,0 @@
-<script>
-import { dateInWords, timeFor } from '~/lib/utils/datetime_utility';
-import { __ } from '~/locale';
-import timeagoMixin from '~/vue_shared/mixins/timeago';
-import collapsedCalendarIcon from './collapsed_calendar_icon.vue';
-
-export default {
- name: 'SidebarCollapsedGroupedDatePicker',
- components: {
- collapsedCalendarIcon,
- },
- mixins: [timeagoMixin],
- props: {
- collapsed: {
- type: Boolean,
- required: false,
- default: true,
- },
- minDate: {
- type: Date,
- required: false,
- default: null,
- },
- maxDate: {
- type: Date,
- required: false,
- default: null,
- },
- disableClickableIcons: {
- type: Boolean,
- required: false,
- default: false,
- },
- },
- computed: {
- hasMinAndMaxDates() {
- return this.minDate && this.maxDate;
- },
- hasNoMinAndMaxDates() {
- return !this.minDate && !this.maxDate;
- },
- showMinDateBlock() {
- return this.minDate || this.hasNoMinAndMaxDates;
- },
- showFromText() {
- return !this.maxDate && this.minDate;
- },
- iconClass() {
- const disabledClass = this.disableClickableIcons ? 'disabled' : '';
- return `sidebar-collapsed-icon calendar-icon ${disabledClass}`;
- },
- },
- methods: {
- toggleSidebar() {
- this.$emit('toggleCollapse');
- },
- dateText(dateType = 'min') {
- const date = this[`${dateType}Date`];
- const dateWords = dateInWords(date, true);
- const parsedDateWords = dateWords ? dateWords.replace(',', '') : dateWords;
-
- return date ? parsedDateWords : __('None');
- },
- tooltipText(dateType = 'min') {
- const defaultText = dateType === 'min' ? __('Start date') : __('Due date');
- const date = this[`${dateType}Date`];
- const timeAgo = dateType === 'min' ? this.timeFormatted(date) : timeFor(date);
- const dateText = date ? [this.dateText(dateType), `(${timeAgo})`].join(' ') : '';
-
- if (date) {
- return [defaultText, dateText].join('<br />');
- }
- return __('Start and due date');
- },
- },
-};
-</script>
-
-<template>
- <div class="block sidebar-grouped-item gl-cursor-pointer" role="button" @click="toggleSidebar">
- <collapsed-calendar-icon
- v-if="showMinDateBlock"
- :container-class="iconClass"
- :tooltip-text="tooltipText('min')"
- >
- <span class="sidebar-collapsed-value">
- <span v-if="showFromText">{{ __('From') }}</span> <span>{{ dateText('min') }}</span>
- </span>
- </collapsed-calendar-icon>
- <div v-if="hasMinAndMaxDates" class="text-center sidebar-collapsed-divider">-</div>
- <collapsed-calendar-icon
- v-if="maxDate"
- :container-class="iconClass"
- :tooltip-text="tooltipText('max')"
- >
- <span class="sidebar-collapsed-value">
- <span v-if="!minDate">{{ __('Until') }}</span> <span>{{ dateText('max') }}</span>
- </span>
- </collapsed-calendar-icon>
- </div>
-</template>
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 4234bc72f3a..7e259cb8b96 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
@@ -179,6 +179,8 @@ export default {
document.addEventListener('mousedown', this.handleDocumentMousedown);
document.addEventListener('click', this.handleDocumentClick);
+
+ this.updateLabelsSetState();
},
beforeDestroy() {
document.removeEventListener('mousedown', this.handleDocumentMousedown);
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents.vue
index f7485de0342..13a6dd43207 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents.vue
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents.vue
@@ -172,6 +172,13 @@ export default {
showDropdown() {
this.$refs.dropdown.show();
},
+ clearSearch() {
+ if (!this.allowMultiselect || this.isStandalone) {
+ return;
+ }
+ this.searchKey = '';
+ this.setFocus();
+ },
},
};
</script>
@@ -188,12 +195,12 @@ export default {
>
<template #header>
<dropdown-header
- v-if="!isStandalone"
ref="header"
- v-model="searchKey"
+ :search-key="searchKey"
:labels-create-title="labelsCreateTitle"
:labels-list-title="labelsListTitle"
:show-dropdown-contents-create-view="showDropdownContentsCreateView"
+ :is-standalone="isStandalone"
@toggleDropdownContentsCreateView="toggleDropdownContent"
@closeDropdown="$emit('closeDropdown')"
@input="debouncedSearchKeyUpdate"
@@ -210,6 +217,7 @@ export default {
:attr-workspace-path="attrWorkspacePath"
:label-create-type="labelCreateType"
@hideCreateView="toggleDropdownContent"
+ @input="clearSearch"
/>
</template>
<template #footer>
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_header.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_header.vue
index 10064b01648..7a0f20b0c83 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_header.vue
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_header.vue
@@ -6,9 +6,6 @@ export default {
GlButton,
GlSearchBoxByType,
},
- model: {
- prop: 'searchKey',
- },
props: {
labelsCreateTitle: {
type: String,
@@ -31,6 +28,11 @@ export default {
type: String,
required: true,
},
+ isStandalone: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
},
computed: {
dropdownTitle() {
@@ -47,7 +49,11 @@ export default {
<template>
<div data-testid="dropdown-header">
- <div class="dropdown-title gl-display-flex gl-align-items-center gl-pt-0 gl-pb-3!">
+ <div
+ v-if="!isStandalone"
+ class="dropdown-title gl-display-flex gl-align-items-center gl-pt-0 gl-pb-3!"
+ data-testid="dropdown-header-title"
+ >
<gl-button
v-if="showDropdownContentsCreateView"
:aria-label="__('Go back')"
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_value.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_value.vue
index aed5bc303ee..57ee816c4c7 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_value.vue
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_value.vue
@@ -1,10 +1,15 @@
<script>
-import { GlLabel } from '@gitlab/ui';
+import { GlIcon, GlLabel, GlTooltipDirective } from '@gitlab/ui';
import { sortBy } from 'lodash';
import { isScopedLabel } from '~/lib/utils/common_utils';
+import { s__, sprintf } from '~/locale';
export default {
+ directives: {
+ GlTooltip: GlTooltipDirective,
+ },
components: {
+ GlIcon,
GlLabel,
},
inject: ['allowScopedLabels'],
@@ -35,6 +40,23 @@ export default {
sortedSelectedLabels() {
return sortBy(this.selectedLabels, (label) => (isScopedLabel(label) ? 0 : 1));
},
+ labelsList() {
+ const labelsString = this.selectedLabels.length
+ ? this.selectedLabels
+ .slice(0, 5)
+ .map((label) => label.title)
+ .join(', ')
+ : s__('LabelSelect|Labels');
+
+ if (this.selectedLabels.length > 5) {
+ return sprintf(s__('LabelSelect|%{labelsString}, and %{remainingLabelCount} more'), {
+ labelsString,
+ remainingLabelCount: this.selectedLabels.length - 5,
+ });
+ }
+
+ return labelsString;
+ },
},
methods: {
labelFilterUrl(label) {
@@ -48,6 +70,9 @@ export default {
removeLabel(labelId) {
this.$emit('onLabelRemove', labelId);
},
+ handleCollapsedClick() {
+ this.$emit('onCollapsedValueClick');
+ },
},
};
</script>
@@ -57,16 +82,30 @@ export default {
:class="{
'has-labels': selectedLabels.length,
}"
- class="hide-collapsed value issuable-show-labels js-value"
+ class="value issuable-show-labels js-value"
data-testid="value-wrapper"
>
- <span v-if="!selectedLabels.length" class="text-secondary" data-testid="empty-placeholder">
+ <div
+ v-gl-tooltip.left.viewport
+ :title="labelsList"
+ class="sidebar-collapsed-icon"
+ @click="handleCollapsedClick"
+ >
+ <gl-icon name="labels" />
+ <span class="gl-font-base gl-line-height-24">{{ selectedLabels.length }}</span>
+ </div>
+ <span
+ v-if="!selectedLabels.length"
+ class="text-secondary hide-collapsed"
+ data-testid="empty-placeholder"
+ >
<slot></slot>
</span>
<template v-else>
<gl-label
v-for="label in sortedSelectedLabels"
:key="label.id"
+ class="hide-collapsed"
data-qa-selector="selected_label_content"
:data-qa-label-name="label.title"
:title="label.title"
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_value_collapsed.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_value_collapsed.vue
deleted file mode 100644
index 122250d1ce7..00000000000
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_value_collapsed.vue
+++ /dev/null
@@ -1,55 +0,0 @@
-<script>
-import { GlIcon, GlTooltipDirective } from '@gitlab/ui';
-import { s__, sprintf } from '~/locale';
-
-export default {
- directives: {
- GlTooltip: GlTooltipDirective,
- },
- components: {
- GlIcon,
- },
- props: {
- labels: {
- type: Array,
- required: true,
- },
- },
- computed: {
- labelsList() {
- const labelsString = this.labels.length
- ? this.labels
- .slice(0, 5)
- .map((label) => label.title)
- .join(', ')
- : s__('LabelSelect|Labels');
-
- if (this.labels.length > 5) {
- return sprintf(s__('LabelSelect|%{labelsString}, and %{remainingLabelCount} more'), {
- labelsString,
- remainingLabelCount: this.labels.length - 5,
- });
- }
-
- return labelsString;
- },
- },
- methods: {
- handleClick() {
- this.$emit('onValueClick');
- },
- },
-};
-</script>
-
-<template>
- <div
- v-gl-tooltip.left.viewport
- :title="labelsList"
- class="sidebar-collapsed-icon"
- @click="handleClick"
- >
- <gl-icon name="labels" />
- <span>{{ labels.length }}</span>
- </div>
-</template>
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/graphql/epic_labels.query.graphql b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/graphql/epic_labels.query.graphql
index c130cc426dc..c442c17eb88 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/graphql/epic_labels.query.graphql
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/graphql/epic_labels.query.graphql
@@ -2,6 +2,7 @@
query epicLabels($fullPath: ID!, $iid: ID) {
workspace: group(fullPath: $fullPath) {
+ id
issuable: epic(iid: $iid) {
id
labels {
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/graphql/epic_update_labels.mutation.graphql b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/graphql/epic_update_labels.mutation.graphql
index 45fcb50732e..cb054e2968f 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/graphql/epic_update_labels.mutation.graphql
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/graphql/epic_update_labels.mutation.graphql
@@ -1,8 +1,8 @@
#import "~/graphql_shared/fragments/label.fragment.graphql"
mutation updateEpicLabels($input: UpdateEpicInput!) {
- updateEpic(input: $input) {
- epic {
+ updateIssuableLabels: updateEpic(input: $input) {
+ issuable: epic {
id
labels {
nodes {
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/graphql/issue_labels.query.graphql b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/graphql/issue_labels.query.graphql
index e471d279b24..2904857270e 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/graphql/issue_labels.query.graphql
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/graphql/issue_labels.query.graphql
@@ -2,6 +2,7 @@
query issueLabels($fullPath: ID!, $iid: String) {
workspace: project(fullPath: $fullPath) {
+ id
issuable: issue(iid: $iid) {
id
labels {
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/graphql/merge_request_labels.query.graphql b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/graphql/merge_request_labels.query.graphql
index dd80e89c8a7..e0cdfd91658 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/graphql/merge_request_labels.query.graphql
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/graphql/merge_request_labels.query.graphql
@@ -2,6 +2,7 @@
query mergeRequestLabels($fullPath: ID!, $iid: String!) {
workspace: project(fullPath: $fullPath) {
+ id
issuable: mergeRequest(iid: $iid) {
id
labels {
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/labels_select_root.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/labels_select_root.vue
index 97a65c13933..3adda69b892 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/labels_select_root.vue
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/labels_select_root.vue
@@ -2,14 +2,13 @@
import { debounce } from 'lodash';
import { MutationOperationMode, getIdFromGraphQLId } from '~/graphql_shared/utils';
import createFlash from '~/flash';
-import { IssuableType } from '~/issue_show/constants';
+import { IssuableType } from '~/issues/constants';
import { __ } from '~/locale';
import SidebarEditableItem from '~/sidebar/components/sidebar_editable_item.vue';
import { issuableLabelsQueries } from '~/sidebar/constants';
import { DEBOUNCE_DROPDOWN_DELAY, DropdownVariant } from './constants';
import DropdownContents from './dropdown_contents.vue';
import DropdownValue from './dropdown_value.vue';
-import DropdownValueCollapsed from './dropdown_value_collapsed.vue';
import {
isDropdownVariantSidebar,
isDropdownVariantStandalone,
@@ -20,7 +19,6 @@ export default {
components: {
DropdownValue,
DropdownContents,
- DropdownValueCollapsed,
SidebarEditableItem,
},
inject: {
@@ -225,15 +223,13 @@ export default {
variables: { input: inputVariables },
})
.then(({ data }) => {
- const { mutationName } = issuableLabelsQueries[this.issuableType];
-
- if (data[mutationName]?.errors?.length) {
+ if (data.updateIssuableLabels?.errors?.length) {
throw new Error();
}
this.$emit('updateSelectedLabels', {
- id: data[mutationName]?.[this.issuableType]?.id,
- labels: data[mutationName]?.[this.issuableType]?.labels?.nodes,
+ id: data.updateIssuableLabels?.issuable?.id,
+ labels: data.updateIssuableLabels?.issuable?.labels?.nodes,
});
})
.catch((error) =>
@@ -288,18 +284,14 @@ export default {
<template>
<div
- class="labels-select-wrapper position-relative"
+ class="labels-select-wrapper gl-relative"
:class="{
'is-standalone': isDropdownVariantStandalone(variant),
'is-embedded': isDropdownVariantEmbedded(variant),
}"
+ data-qa-selector="labels_block"
>
<template v-if="isDropdownVariantSidebar(variant)">
- <dropdown-value-collapsed
- ref="dropdownButtonCollapsed"
- :labels="issuableLabels"
- @onValueClick="handleCollapsedValueClick"
- />
<sidebar-editable-item
ref="editable"
:title="__('Labels')"
@@ -315,6 +307,7 @@ export default {
:labels-filter-base-path="labelsFilterBasePath"
:labels-filter-param="labelsFilterParam"
@onLabelRemove="handleLabelRemove"
+ @onCollapsedValueClick="handleCollapsedValueClick"
>
<slot></slot>
</dropdown-value>
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/queries/get_alert_assignees.query.graphql b/app/assets/javascripts/vue_shared/components/sidebar/queries/get_alert_assignees.query.graphql
index d99fc125012..bb6c7181e5c 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/queries/get_alert_assignees.query.graphql
+++ b/app/assets/javascripts/vue_shared/components/sidebar/queries/get_alert_assignees.query.graphql
@@ -7,6 +7,7 @@ query alertAssignees(
$iid: String!
) {
workspace: project(fullPath: $fullPath) {
+ id
issuable: alertManagementAlert(domain: $domain, iid: $iid) {
iid
assignees {
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/queries/get_issue_assignees.query.graphql b/app/assets/javascripts/vue_shared/components/sidebar/queries/get_issue_assignees.query.graphql
index 93b9833bb7d..be270e440ed 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/queries/get_issue_assignees.query.graphql
+++ b/app/assets/javascripts/vue_shared/components/sidebar/queries/get_issue_assignees.query.graphql
@@ -4,6 +4,7 @@
query issueAssignees($fullPath: ID!, $iid: String!) {
workspace: project(fullPath: $fullPath) {
__typename
+ id
issuable: issue(iid: $iid) {
__typename
id
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/queries/get_issue_participants.query.graphql b/app/assets/javascripts/vue_shared/components/sidebar/queries/get_issue_participants.query.graphql
index 48787305459..96a40e597ee 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/queries/get_issue_participants.query.graphql
+++ b/app/assets/javascripts/vue_shared/components/sidebar/queries/get_issue_participants.query.graphql
@@ -4,6 +4,7 @@
query issueParticipants($fullPath: ID!, $iid: String!) {
workspace: project(fullPath: $fullPath) {
__typename
+ id
issuable: issue(iid: $iid) {
__typename
id
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/queries/get_mr_assignees.query.graphql b/app/assets/javascripts/vue_shared/components/sidebar/queries/get_mr_assignees.query.graphql
index 53f7381760e..81e19e48d75 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/queries/get_mr_assignees.query.graphql
+++ b/app/assets/javascripts/vue_shared/components/sidebar/queries/get_mr_assignees.query.graphql
@@ -3,6 +3,7 @@
query getMrAssignees($fullPath: ID!, $iid: String!) {
workspace: project(fullPath: $fullPath) {
+ id
issuable: mergeRequest(iid: $iid) {
id
assignees {
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/queries/get_mr_participants.query.graphql b/app/assets/javascripts/vue_shared/components/sidebar/queries/get_mr_participants.query.graphql
index 6adbd4098f2..3496d5f4a2e 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/queries/get_mr_participants.query.graphql
+++ b/app/assets/javascripts/vue_shared/components/sidebar/queries/get_mr_participants.query.graphql
@@ -3,6 +3,7 @@
query getMrParticipants($fullPath: ID!, $iid: String!) {
workspace: project(fullPath: $fullPath) {
+ id
issuable: mergeRequest(iid: $iid) {
id
participants {
diff --git a/app/assets/javascripts/vue_shared/components/source_editor.vue b/app/assets/javascripts/vue_shared/components/source_editor.vue
index fdf0c9baee3..8a0fef36079 100644
--- a/app/assets/javascripts/vue_shared/components/source_editor.vue
+++ b/app/assets/javascripts/vue_shared/components/source_editor.vue
@@ -96,6 +96,7 @@ export default {
:id="`source-editor-${fileGlobalId}`"
ref="editor"
data-editor-loading
+ data-qa-selector="source_editor_container"
@[$options.readyEvent]="$emit($options.readyEvent)"
>
<pre class="editor-loading-content">{{ value }}</pre>
diff --git a/app/assets/javascripts/vue_shared/components/source_viewer.vue b/app/assets/javascripts/vue_shared/components/source_viewer.vue
new file mode 100644
index 00000000000..8f0d051543f
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/source_viewer.vue
@@ -0,0 +1,88 @@
+<script>
+import { GlSafeHtmlDirective } from '@gitlab/ui';
+import LineNumbers from '~/vue_shared/components/line_numbers.vue';
+
+export default {
+ components: {
+ LineNumbers,
+ },
+ directives: {
+ SafeHtml: GlSafeHtmlDirective,
+ },
+ props: {
+ content: {
+ type: String,
+ required: true,
+ },
+ language: {
+ type: String,
+ required: false,
+ default: 'plaintext',
+ },
+ autoDetect: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ },
+ data() {
+ return {
+ languageDefinition: null,
+ hljs: null,
+ };
+ },
+ computed: {
+ lineNumbers() {
+ return this.content.split('\n').length;
+ },
+ highlightedContent() {
+ let highlightedContent;
+
+ if (this.hljs) {
+ if (this.autoDetect) {
+ highlightedContent = this.hljs.highlightAuto(this.content).value;
+ } else if (this.languageDefinition) {
+ highlightedContent = this.hljs.highlight(this.content, { language: this.language }).value;
+ }
+ }
+
+ return highlightedContent;
+ },
+ },
+ async mounted() {
+ this.hljs = await this.loadHighlightJS();
+
+ if (!this.autoDetect) {
+ this.languageDefinition = await this.loadLanguage();
+ }
+ },
+ methods: {
+ loadHighlightJS() {
+ // With auto-detect enabled we load all common languages else we load only the core (smallest footprint)
+ return this.autoDetect ? import('highlight.js/lib/common') : import('highlight.js/lib/core');
+ },
+ async loadLanguage() {
+ let languageDefinition;
+
+ try {
+ languageDefinition = await import(`highlight.js/lib/languages/${this.language}`);
+ this.hljs.registerLanguage(this.language, languageDefinition.default);
+ } catch (message) {
+ this.$emit('error', message);
+ }
+
+ return languageDefinition;
+ },
+ },
+ userColorScheme: window.gon.user_color_scheme,
+};
+</script>
+<template>
+ <div class="file-content code" :class="$options.userColorScheme">
+ <line-numbers :lines="lineNumbers" />
+ <pre
+ class="code gl-pl-3!"
+ ><code v-safe-html="highlightedContent" class="gl-white-space-pre-wrap!"></code>
+ </pre>
+ </div>
+</template>
diff --git a/app/assets/javascripts/vue_shared/components/storage_counter/usage_graph.stories.js b/app/assets/javascripts/vue_shared/components/storage_counter/usage_graph.stories.js
deleted file mode 100644
index 00aa5519ec6..00000000000
--- a/app/assets/javascripts/vue_shared/components/storage_counter/usage_graph.stories.js
+++ /dev/null
@@ -1,38 +0,0 @@
-/* eslint-disable @gitlab/require-i18n-strings */
-import '@gitlab/ui/dist/utility_classes.css';
-import UsageGraph from './usage_graph.vue';
-
-export default {
- component: UsageGraph,
- title: 'vue_shared/components/storage_counter/usage_graph',
-};
-
-const Template = (args, { argTypes }) => ({
- components: { UsageGraph },
- props: Object.keys(argTypes),
- template: '<usage-graph v-bind="$props" />',
-});
-
-export const Default = Template.bind({});
-Default.argTypes = {
- rootStorageStatistics: {
- description: 'The statistics object with all its fields',
- type: { name: 'object', required: true },
- defaultValue: {
- buildArtifactsSize: 400000,
- pipelineArtifactsSize: 38000,
- lfsObjectsSize: 4800000,
- packagesSize: 3800000,
- repositorySize: 39000000,
- snippetsSize: 2000112,
- storageSize: 39930000,
- uploadsSize: 7000,
- wikiSize: 300000,
- },
- },
- limit: {
- description:
- 'When a limit is set, users will see how much of their storage usage (limit) is used. In case the limit is 0 or the current usage exceeds the limit, it just renders the distribution',
- defaultValue: 0,
- },
-};
diff --git a/app/assets/javascripts/vue_shared/components/storage_counter/usage_graph.vue b/app/assets/javascripts/vue_shared/components/storage_counter/usage_graph.vue
deleted file mode 100644
index c33d065ff4b..00000000000
--- a/app/assets/javascripts/vue_shared/components/storage_counter/usage_graph.vue
+++ /dev/null
@@ -1,148 +0,0 @@
-<script>
-import { GlIcon, GlTooltipDirective } from '@gitlab/ui';
-import { numberToHumanSize } from '~/lib/utils/number_utils';
-import { s__ } from '~/locale';
-
-export default {
- components: {
- GlIcon,
- },
- directives: {
- GlTooltip: GlTooltipDirective,
- },
- props: {
- rootStorageStatistics: {
- required: true,
- type: Object,
- },
- limit: {
- required: true,
- type: Number,
- },
- },
- computed: {
- storageTypes() {
- const {
- buildArtifactsSize,
- pipelineArtifactsSize,
- lfsObjectsSize,
- packagesSize,
- repositorySize,
- storageSize,
- wikiSize,
- snippetsSize,
- uploadsSize,
- } = this.rootStorageStatistics;
- const artifactsSize = buildArtifactsSize + pipelineArtifactsSize;
-
- if (storageSize === 0) {
- return null;
- }
-
- return [
- {
- name: s__('UsageQuota|Repositories'),
- style: this.usageStyle(this.barRatio(repositorySize)),
- class: 'gl-bg-data-viz-blue-500',
- size: repositorySize,
- },
- {
- name: s__('UsageQuota|LFS Objects'),
- style: this.usageStyle(this.barRatio(lfsObjectsSize)),
- class: 'gl-bg-data-viz-orange-600',
- size: lfsObjectsSize,
- },
- {
- name: s__('UsageQuota|Packages'),
- style: this.usageStyle(this.barRatio(packagesSize)),
- class: 'gl-bg-data-viz-aqua-500',
- size: packagesSize,
- },
- {
- name: s__('UsageQuota|Artifacts'),
- style: this.usageStyle(this.barRatio(artifactsSize)),
- class: 'gl-bg-data-viz-green-600',
- size: artifactsSize,
- tooltip: s__('UsageQuota|Artifacts is a sum of build and pipeline artifacts.'),
- },
- {
- name: s__('UsageQuota|Wikis'),
- style: this.usageStyle(this.barRatio(wikiSize)),
- class: 'gl-bg-data-viz-magenta-500',
- size: wikiSize,
- },
- {
- name: s__('UsageQuota|Snippets'),
- style: this.usageStyle(this.barRatio(snippetsSize)),
- class: 'gl-bg-data-viz-orange-800',
- size: snippetsSize,
- },
- {
- name: s__('UsageQuota|Uploads'),
- style: this.usageStyle(this.barRatio(uploadsSize)),
- class: 'gl-bg-data-viz-aqua-700',
- size: uploadsSize,
- },
- ]
- .filter((data) => data.size !== 0)
- .sort((a, b) => b.size - a.size);
- },
- },
- methods: {
- formatSize(size) {
- return numberToHumanSize(size);
- },
- usageStyle(ratio) {
- return { flex: ratio };
- },
- barRatio(size) {
- let max = this.rootStorageStatistics.storageSize;
-
- if (this.limit !== 0 && max <= this.limit) {
- max = this.limit;
- }
-
- return size / max;
- },
- },
-};
-</script>
-<template>
- <div v-if="storageTypes" class="gl-display-flex gl-flex-direction-column w-100">
- <div class="gl-h-6 gl-my-5 gl-bg-gray-50 gl-rounded-base gl-display-flex">
- <div
- v-for="storageType in storageTypes"
- :key="storageType.name"
- class="storage-type-usage gl-h-full gl-display-inline-block"
- :class="storageType.class"
- :style="storageType.style"
- data-testid="storage-type-usage"
- ></div>
- </div>
- <div class="row py-0">
- <div
- v-for="storageType in storageTypes"
- :key="storageType.name"
- class="col-md-auto gl-display-flex gl-align-items-center"
- data-testid="storage-type-legend"
- >
- <div class="gl-h-2 gl-w-5 gl-mr-2 gl-display-inline-block" :class="storageType.class"></div>
- <span class="gl-mr-2 gl-font-weight-bold gl-font-sm">
- {{ storageType.name }}
- </span>
- <span class="gl-text-gray-500 gl-font-sm">
- {{ formatSize(storageType.size) }}
- </span>
- <span
- v-if="storageType.tooltip"
- v-gl-tooltip
- :title="storageType.tooltip"
- :aria-label="storageType.tooltip"
- class="gl-ml-2"
- >
- <gl-icon name="question" :size="12" />
- </span>
- </div>
- </div>
- </div>
-</template>
diff --git a/app/assets/javascripts/vue_shared/components/tooltip_on_truncate.vue b/app/assets/javascripts/vue_shared/components/tooltip_on_truncate.vue
deleted file mode 100644
index c5fdb5fc242..00000000000
--- a/app/assets/javascripts/vue_shared/components/tooltip_on_truncate.vue
+++ /dev/null
@@ -1,69 +0,0 @@
-<script>
-import { GlTooltipDirective as GlTooltip } from '@gitlab/ui';
-import { isFunction } from 'lodash';
-import { hasHorizontalOverflow } from '~/lib/utils/dom_utils';
-
-export default {
- directives: {
- GlTooltip,
- },
- props: {
- title: {
- type: String,
- required: false,
- default: '',
- },
- placement: {
- type: String,
- required: false,
- default: 'top',
- },
- truncateTarget: {
- type: [String, Function],
- required: false,
- default: '',
- },
- },
- data() {
- return {
- showTooltip: false,
- };
- },
- watch: {
- title() {
- // Wait on $nextTick in case of slot width changes
- this.$nextTick(this.updateTooltip);
- },
- },
- mounted() {
- this.updateTooltip();
- },
- methods: {
- selectTarget() {
- if (isFunction(this.truncateTarget)) {
- return this.truncateTarget(this.$el);
- } else if (this.truncateTarget === 'child') {
- return this.$el.childNodes[0];
- }
-
- return this.$el;
- },
- updateTooltip() {
- const target = this.selectTarget();
- this.showTooltip = hasHorizontalOverflow(target);
- },
- },
-};
-</script>
-
-<template>
- <span
- v-if="showTooltip"
- v-gl-tooltip="{ placement }"
- :title="title"
- class="js-show-tooltip gl-min-w-0"
- >
- <slot></slot>
- </span>
- <span v-else class="gl-min-w-0"> <slot></slot> </span>
-</template>
diff --git a/app/assets/javascripts/vue_shared/components/tooltip_on_truncate/tooltip_on_truncate.stories.js b/app/assets/javascripts/vue_shared/components/tooltip_on_truncate/tooltip_on_truncate.stories.js
new file mode 100644
index 00000000000..f27901a30a9
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/tooltip_on_truncate/tooltip_on_truncate.stories.js
@@ -0,0 +1,88 @@
+/* eslint-disable @gitlab/require-i18n-strings */
+import TooltipOnTruncate from './tooltip_on_truncate.vue';
+
+const defaultWidth = '250px';
+
+export default {
+ component: TooltipOnTruncate,
+ title: 'vue_shared/components/tooltip_on_truncate/tooltip_on_truncate.vue',
+};
+
+const createStory = ({ ...options }) => {
+ return (_, { argTypes }) => {
+ const comp = {
+ components: { TooltipOnTruncate },
+ props: Object.keys(argTypes),
+ template: `
+ <div class="gl-bg-blue-50" :style="{ width }">
+ <tooltip-on-truncate :title="title" :placement="placement" class="gl-display-block gl-text-truncate">
+ {{title}}
+ </tooltip-on-truncate>
+ </div>
+ `,
+ ...options,
+ };
+
+ return comp;
+ };
+};
+
+export const Default = createStory();
+Default.args = {
+ width: defaultWidth,
+ title: 'Hover on this text to see the content in a tooltip.',
+};
+
+export const NoOverflow = createStory();
+NoOverflow.args = {
+ width: defaultWidth,
+ title: "Short text doesn't need a tooltip.",
+};
+
+export const Placement = createStory();
+Placement.args = {
+ width: defaultWidth,
+ title: 'Use `placement="right"` to display this tooltip at the right.',
+ placement: 'right',
+};
+
+const TIMEOUT_S = 3;
+
+export const LiveUpdates = createStory({
+ props: ['width', 'placement'],
+ data() {
+ return {
+ title: `(loading in ${TIMEOUT_S}s)`,
+ };
+ },
+ mounted() {
+ setTimeout(() => {
+ this.title = 'Content updated! The content is now overflowing so we use a tooltip!';
+ }, TIMEOUT_S * 1000);
+ },
+});
+LiveUpdates.args = {
+ width: defaultWidth,
+};
+LiveUpdates.argTypes = {
+ title: {
+ control: false,
+ },
+};
+
+export const TruncateTarget = createStory({
+ template: `
+ <div class="gl-bg-black" :style="{ width }">
+ <tooltip-on-truncate class="gl-display-flex" :truncate-target="truncateTarget" :title="title">
+ <div class="gl-m-5 gl-bg-blue-50 gl-text-truncate">
+ {{ title }}
+ </div>
+ </tooltip-on-truncate>
+ </div>
+ `,
+});
+TruncateTarget.args = {
+ width: defaultWidth,
+ truncateTarget: 'child',
+ title: 'Wrap in container and use `truncate-target="child"` prop.',
+};
diff --git a/app/assets/javascripts/vue_shared/components/tooltip_on_truncate/tooltip_on_truncate.vue b/app/assets/javascripts/vue_shared/components/tooltip_on_truncate/tooltip_on_truncate.vue
new file mode 100644
index 00000000000..09414e679bb
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/tooltip_on_truncate/tooltip_on_truncate.vue
@@ -0,0 +1,85 @@
+<script>
+import { GlTooltipDirective, GlResizeObserverDirective } from '@gitlab/ui';
+import { isFunction, debounce } from 'lodash';
+import { hasHorizontalOverflow } from '~/lib/utils/dom_utils';
+
+const UPDATE_TOOLTIP_DEBOUNCED_WAIT_MS = 300;
+
+export default {
+ directives: {
+ GlTooltip: GlTooltipDirective,
+ GlResizeObserver: GlResizeObserverDirective,
+ },
+ props: {
+ title: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ placement: {
+ type: String,
+ required: false,
+ default: 'top',
+ },
+ truncateTarget: {
+ type: [String, Function],
+ required: false,
+ default: '',
+ },
+ },
+ data() {
+ return {
+ tooltipDisabled: true,
+ };
+ },
+ computed: {
+ classes() {
+ if (this.tooltipDisabled) {
+ return '';
+ }
+ return 'js-show-tooltip';
+ },
+ tooltip() {
+ return {
+ title: this.title,
+ placement: this.placement,
+ disabled: this.tooltipDisabled,
+ };
+ },
+ },
+ watch: {
+ title() {
+ // Wait on $nextTick in case the slot width changes
+ this.$nextTick(this.updateTooltip);
+ },
+ },
+ created() {
+ this.updateTooltipDebounced = debounce(this.updateTooltip, UPDATE_TOOLTIP_DEBOUNCED_WAIT_MS);
+ },
+ mounted() {
+ this.updateTooltip();
+ },
+ methods: {
+ selectTarget() {
+ if (isFunction(this.truncateTarget)) {
+ return this.truncateTarget(this.$el);
+ } else if (this.truncateTarget === 'child') {
+ return this.$el.childNodes[0];
+ }
+ return this.$el;
+ },
+ updateTooltip() {
+ this.tooltipDisabled = !hasHorizontalOverflow(this.selectTarget());
+ },
+ onResize() {
+ this.updateTooltipDebounced();
+ },
+ },
+};
+</script>
+
+<template>
+ <span v-gl-tooltip="tooltip" v-gl-resize-observer="onResize" :class="classes" class="gl-min-w-0">
+ <slot></slot>
+ </span>
+</template>
diff --git a/app/assets/javascripts/issuable_create/components/issuable_create_root.vue b/app/assets/javascripts/vue_shared/issuable/create/components/issuable_create_root.vue
index f4cbaba9313..f4cbaba9313 100644
--- a/app/assets/javascripts/issuable_create/components/issuable_create_root.vue
+++ b/app/assets/javascripts/vue_shared/issuable/create/components/issuable_create_root.vue
diff --git a/app/assets/javascripts/issuable_create/components/issuable_form.vue b/app/assets/javascripts/vue_shared/issuable/create/components/issuable_form.vue
index c216a05bdb0..c216a05bdb0 100644
--- a/app/assets/javascripts/issuable_create/components/issuable_form.vue
+++ b/app/assets/javascripts/vue_shared/issuable/create/components/issuable_form.vue
diff --git a/app/assets/javascripts/issuable_list/components/issuable_bulk_edit_sidebar.vue b/app/assets/javascripts/vue_shared/issuable/list/components/issuable_bulk_edit_sidebar.vue
index 5ca9e50d854..5ca9e50d854 100644
--- a/app/assets/javascripts/issuable_list/components/issuable_bulk_edit_sidebar.vue
+++ b/app/assets/javascripts/vue_shared/issuable/list/components/issuable_bulk_edit_sidebar.vue
diff --git a/app/assets/javascripts/issuable_list/components/issuable_item.vue b/app/assets/javascripts/vue_shared/issuable/list/components/issuable_item.vue
index ab04c6a38a5..0bb0e0d9fb0 100644
--- a/app/assets/javascripts/issuable_list/components/issuable_item.vue
+++ b/app/assets/javascripts/vue_shared/issuable/list/components/issuable_item.vue
@@ -6,7 +6,7 @@ import { isScopedLabel } from '~/lib/utils/common_utils';
import { differenceInSeconds, getTimeago, SECONDS_IN_DAY } from '~/lib/utils/datetime_utility';
import { isExternal, setUrlFragment } from '~/lib/utils/url_utility';
import { __, n__, sprintf } from '~/locale';
-import IssuableAssignees from '~/vue_shared/components/issue/issue_assignees.vue';
+import IssuableAssignees from '~/issuable/components/issue_assignees.vue';
import timeagoMixin from '~/vue_shared/mixins/timeago';
export default {
@@ -166,6 +166,7 @@ export default {
class="issue gl-display-flex! gl-px-5!"
:class="{ closed: issuable.closedAt, today: createdInPastDay }"
:data-labels="labelIdsString"
+ :data-qa-issue-id="issuableId"
>
<gl-form-checkbox
v-if="showCheckbox"
@@ -185,6 +186,13 @@ export default {
:title="__('Confidential')"
:aria-label="__('Confidential')"
/>
+ <gl-icon
+ v-if="issuable.hidden"
+ v-gl-tooltip
+ name="spam"
+ :title="__('This issue is hidden because its author has been banned')"
+ :aria-label="__('Hidden')"
+ />
<gl-link class="issue-title-text" dir="auto" :href="webUrl" v-bind="issuableTitleProps">
{{ issuable.title }}
<gl-icon v-if="isIssuableUrlExternal" name="external-link" class="gl-ml-2" />
@@ -202,7 +210,7 @@ export default {
<span v-else data-testid="issuable-reference" class="issuable-reference">
{{ reference }}
</span>
- <span class="gl-display-none gl-sm-display-inline-block">
+ <span class="gl-display-none gl-sm-display-inline">
<span aria-hidden="true">&middot;</span>
<span class="issuable-authored gl-mr-3">
<gl-sprintf :message="__('created %{timeAgo} by %{author}')">
diff --git a/app/assets/javascripts/issuable_list/components/issuable_list_root.vue b/app/assets/javascripts/vue_shared/issuable/list/components/issuable_list_root.vue
index c1082987146..2f8401b45f0 100644
--- a/app/assets/javascripts/issuable_list/components/issuable_list_root.vue
+++ b/app/assets/javascripts/vue_shared/issuable/list/components/issuable_list_root.vue
@@ -1,5 +1,5 @@
<script>
-import { GlKeysetPagination, GlSkeletonLoading, GlPagination } from '@gitlab/ui';
+import { GlAlert, GlKeysetPagination, GlSkeletonLoading, GlPagination } from '@gitlab/ui';
import { uniqueId } from 'lodash';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { updateHistory, setUrlParams } from '~/lib/utils/url_utility';
@@ -19,6 +19,7 @@ export default {
tag: 'ul',
},
components: {
+ GlAlert,
GlKeysetPagination,
GlSkeletonLoading,
IssuableTabs,
@@ -156,6 +157,11 @@ export default {
required: false,
default: false,
},
+ error: {
+ type: String,
+ required: false,
+ default: '',
+ },
},
data() {
return {
@@ -272,10 +278,12 @@ export default {
:show-checkbox="showBulkEditSidebar"
:checkbox-checked="allIssuablesChecked"
class="gl-flex-grow-1 gl-border-t-none row-content-block"
+ data-qa-selector="issuable_search_container"
@checked-input="handleAllIssuablesCheckedInput"
@onFilter="$emit('filter', $event)"
@onSort="$emit('sort', $event)"
/>
+ <gl-alert v-if="error" variant="danger" @dismiss="$emit('dismiss-alert')">{{ error }}</gl-alert>
<issuable-bulk-edit-sidebar :expanded="showBulkEditSidebar">
<template #bulk-edit-actions>
<slot name="bulk-edit-actions" :checked-issuables="bulkEditIssuables"></slot>
@@ -302,6 +310,8 @@ export default {
v-for="issuable in issuables"
:key="issuableId(issuable)"
:class="{ 'gl-cursor-grab': isManualOrdering }"
+ data-qa-selector="issuable_container"
+ :data-qa-issuable-title="issuable.title"
:issuable-symbol="issuableSymbol"
:issuable="issuable"
:enable-label-permalinks="enableLabelPermalinks"
diff --git a/app/assets/javascripts/issuable_list/components/issuable_tabs.vue b/app/assets/javascripts/vue_shared/issuable/list/components/issuable_tabs.vue
index 96b07031a11..3ff87ba3c4f 100644
--- a/app/assets/javascripts/issuable_list/components/issuable_tabs.vue
+++ b/app/assets/javascripts/vue_shared/issuable/list/components/issuable_tabs.vue
@@ -46,7 +46,9 @@ export default {
@click="$emit('click', tab.name)"
>
<template #title>
- <span :title="tab.titleTooltip">{{ tab.title }}</span>
+ <span :title="tab.titleTooltip" :data-qa-selector="`${tab.name}_issuables_tab`">
+ {{ tab.title }}
+ </span>
<gl-badge
v-if="tabCounts && isTabCountNumeric(tab)"
variant="muted"
diff --git a/app/assets/javascripts/issuable_list/constants.js b/app/assets/javascripts/vue_shared/issuable/list/constants.js
index 773ad0f8e93..773ad0f8e93 100644
--- a/app/assets/javascripts/issuable_list/constants.js
+++ b/app/assets/javascripts/vue_shared/issuable/list/constants.js
diff --git a/app/assets/javascripts/issuable_show/components/issuable_body.vue b/app/assets/javascripts/vue_shared/issuable/show/components/issuable_body.vue
index 05dc1650379..05dc1650379 100644
--- a/app/assets/javascripts/issuable_show/components/issuable_body.vue
+++ b/app/assets/javascripts/vue_shared/issuable/show/components/issuable_body.vue
diff --git a/app/assets/javascripts/issuable_show/components/issuable_description.vue b/app/assets/javascripts/vue_shared/issuable/show/components/issuable_description.vue
index f57b5b2deb4..f57b5b2deb4 100644
--- a/app/assets/javascripts/issuable_show/components/issuable_description.vue
+++ b/app/assets/javascripts/vue_shared/issuable/show/components/issuable_description.vue
diff --git a/app/assets/javascripts/issuable_show/components/issuable_discussion.vue b/app/assets/javascripts/vue_shared/issuable/show/components/issuable_discussion.vue
index 5858af6cc51..5858af6cc51 100644
--- a/app/assets/javascripts/issuable_show/components/issuable_discussion.vue
+++ b/app/assets/javascripts/vue_shared/issuable/show/components/issuable_discussion.vue
diff --git a/app/assets/javascripts/issuable_show/components/issuable_edit_form.vue b/app/assets/javascripts/vue_shared/issuable/show/components/issuable_edit_form.vue
index 33dca3e9332..33dca3e9332 100644
--- a/app/assets/javascripts/issuable_show/components/issuable_edit_form.vue
+++ b/app/assets/javascripts/vue_shared/issuable/show/components/issuable_edit_form.vue
diff --git a/app/assets/javascripts/issuable_show/components/issuable_header.vue b/app/assets/javascripts/vue_shared/issuable/show/components/issuable_header.vue
index d7da533d055..d7da533d055 100644
--- a/app/assets/javascripts/issuable_show/components/issuable_header.vue
+++ b/app/assets/javascripts/vue_shared/issuable/show/components/issuable_header.vue
diff --git a/app/assets/javascripts/issuable_show/components/issuable_show_root.vue b/app/assets/javascripts/vue_shared/issuable/show/components/issuable_show_root.vue
index 011db52cbe3..8849af2a52e 100644
--- a/app/assets/javascripts/issuable_show/components/issuable_show_root.vue
+++ b/app/assets/javascripts/vue_shared/issuable/show/components/issuable_show_root.vue
@@ -1,5 +1,5 @@
<script>
-import IssuableSidebar from '~/issuable_sidebar/components/issuable_sidebar_root.vue';
+import IssuableSidebar from '~/vue_shared/issuable/sidebar/components/issuable_sidebar_root.vue';
import IssuableBody from './issuable_body.vue';
import IssuableDiscussion from './issuable_discussion.vue';
@@ -100,7 +100,7 @@ export default {
</script>
<template>
- <div class="issuable-show-container">
+ <div class="issuable-show-container" data-qa-selector="issuable_show_container">
<issuable-header
:status-badge-class="statusBadgeClass"
:status-icon="statusIcon"
diff --git a/app/assets/javascripts/issuable_show/components/issuable_title.vue b/app/assets/javascripts/vue_shared/issuable/show/components/issuable_title.vue
index b96ce0c43f7..b96ce0c43f7 100644
--- a/app/assets/javascripts/issuable_show/components/issuable_title.vue
+++ b/app/assets/javascripts/vue_shared/issuable/show/components/issuable_title.vue
diff --git a/app/assets/javascripts/issuable_show/constants.js b/app/assets/javascripts/vue_shared/issuable/show/constants.js
index 346f45c7d90..346f45c7d90 100644
--- a/app/assets/javascripts/issuable_show/constants.js
+++ b/app/assets/javascripts/vue_shared/issuable/show/constants.js
diff --git a/app/assets/javascripts/pages/projects/labels/event_hub.js b/app/assets/javascripts/vue_shared/issuable/show/event_hub.js
index e31806ad199..e31806ad199 100644
--- a/app/assets/javascripts/pages/projects/labels/event_hub.js
+++ b/app/assets/javascripts/vue_shared/issuable/show/event_hub.js
diff --git a/app/assets/javascripts/issuable_sidebar/components/issuable_sidebar_root.vue b/app/assets/javascripts/vue_shared/issuable/sidebar/components/issuable_sidebar_root.vue
index 99dcccd12ed..99dcccd12ed 100644
--- a/app/assets/javascripts/issuable_sidebar/components/issuable_sidebar_root.vue
+++ b/app/assets/javascripts/vue_shared/issuable/sidebar/components/issuable_sidebar_root.vue
diff --git a/app/assets/javascripts/issuable_sidebar/constants.js b/app/assets/javascripts/vue_shared/issuable/sidebar/constants.js
index 4f4b6341a1c..4f4b6341a1c 100644
--- a/app/assets/javascripts/issuable_sidebar/constants.js
+++ b/app/assets/javascripts/vue_shared/issuable/sidebar/constants.js
diff --git a/app/assets/javascripts/vue_shared/mixins/issuable.js b/app/assets/javascripts/vue_shared/mixins/issuable.js
deleted file mode 100644
index fab0919d96e..00000000000
--- a/app/assets/javascripts/vue_shared/mixins/issuable.js
+++ /dev/null
@@ -1,14 +0,0 @@
-export default {
- props: {
- issuableType: {
- required: true,
- type: String,
- },
- },
-
- computed: {
- issuableDisplayName() {
- return this.issuableType.replace(/_/g, ' ');
- },
- },
-};
diff --git a/app/assets/javascripts/vue_shared/security_configuration/components/manage_via_mr.vue b/app/assets/javascripts/vue_shared/security_configuration/components/manage_via_mr.vue
index 42272c222fc..d1630c9ac13 100644
--- a/app/assets/javascripts/vue_shared/security_configuration/components/manage_via_mr.vue
+++ b/app/assets/javascripts/vue_shared/security_configuration/components/manage_via_mr.vue
@@ -85,7 +85,7 @@ export default {
);
},
i18n: {
- buttonLabel: s__('SecurityConfiguration|Configure via Merge Request'),
+ buttonLabel: s__('SecurityConfiguration|Configure with a merge request'),
noSuccessPathError: s__(
'SecurityConfiguration|%{featureName} merge request creation mutation failed',
),
diff --git a/app/assets/javascripts/vue_shared/security_reports/graphql/fragments/job_artifacts.fragment.graphql b/app/assets/javascripts/vue_shared/security_reports/graphql/fragments/job_artifacts.fragment.graphql
index ae77a2ce5e4..829b9d9f9d8 100644
--- a/app/assets/javascripts/vue_shared/security_reports/graphql/fragments/job_artifacts.fragment.graphql
+++ b/app/assets/javascripts/vue_shared/security_reports/graphql/fragments/job_artifacts.fragment.graphql
@@ -1,6 +1,8 @@
fragment JobArtifacts on Pipeline {
+ id
jobs(securityReportTypes: $reportTypes) {
nodes {
+ id
name
artifacts {
nodes {
diff --git a/app/assets/javascripts/vue_shared/security_reports/graphql/queries/security_report_merge_request_download_paths.query.graphql b/app/assets/javascripts/vue_shared/security_reports/graphql/queries/security_report_merge_request_download_paths.query.graphql
index 4ce13827da2..2e80db30e9a 100644
--- a/app/assets/javascripts/vue_shared/security_reports/graphql/queries/security_report_merge_request_download_paths.query.graphql
+++ b/app/assets/javascripts/vue_shared/security_reports/graphql/queries/security_report_merge_request_download_paths.query.graphql
@@ -4,11 +4,14 @@ query securityReportDownloadPaths(
$reportTypes: [SecurityReportTypeEnum!]
) {
project(fullPath: $projectPath) {
+ id
mergeRequest(iid: $iid) {
+ id
headPipeline {
id
jobs(securityReportTypes: $reportTypes) {
nodes {
+ id
name
artifacts {
nodes {
diff --git a/app/assets/javascripts/vue_shared/security_reports/graphql/queries/security_report_pipeline_download_paths.query.graphql b/app/assets/javascripts/vue_shared/security_reports/graphql/queries/security_report_pipeline_download_paths.query.graphql
index e1f3c55a886..e4f0c392b91 100644
--- a/app/assets/javascripts/vue_shared/security_reports/graphql/queries/security_report_pipeline_download_paths.query.graphql
+++ b/app/assets/javascripts/vue_shared/security_reports/graphql/queries/security_report_pipeline_download_paths.query.graphql
@@ -2,8 +2,8 @@
query getPipelineCorpuses($projectPath: ID!, $iid: ID, $reportTypes: [SecurityReportTypeEnum!]) {
project(fullPath: $projectPath) {
+ id
pipeline(iid: $iid) {
- id
...JobArtifacts
}
}
diff --git a/app/assets/javascripts/work_items/components/item_title.vue b/app/assets/javascripts/work_items/components/item_title.vue
new file mode 100644
index 00000000000..5e9e50a94f0
--- /dev/null
+++ b/app/assets/javascripts/work_items/components/item_title.vue
@@ -0,0 +1,71 @@
+<script>
+import { escape } from 'lodash';
+import { __ } from '~/locale';
+
+export default {
+ props: {
+ initialTitle: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ placeholder: {
+ type: String,
+ required: false,
+ default: __('Add a title...'),
+ },
+ disabled: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ },
+ data() {
+ return {
+ title: this.initialTitle,
+ };
+ },
+ methods: {
+ getSanitizedTitle(inputEl) {
+ const { innerText } = inputEl;
+ return escape(innerText);
+ },
+ handleBlur({ target }) {
+ this.$emit('title-changed', this.getSanitizedTitle(target));
+ },
+ handleInput({ target }) {
+ this.$emit('title-input', this.getSanitizedTitle(target));
+ },
+ handleSubmit() {
+ this.$refs.titleEl.blur();
+ },
+ },
+};
+</script>
+
+<template>
+ <h2
+ class="gl-font-weight-normal gl-sm-font-weight-bold gl-my-5 gl-display-inline-block"
+ :class="{ 'gl-cursor-not-allowed': disabled }"
+ data-testid="title"
+ aria-labelledby="item-title"
+ >
+ <span
+ id="item-title"
+ ref="titleEl"
+ role="textbox"
+ :aria-label="__('Title')"
+ :data-placeholder="placeholder"
+ :contenteditable="!disabled"
+ class="gl-pseudo-placeholder"
+ @blur="handleBlur"
+ @keyup="handleInput"
+ @keydown.enter.exact="handleSubmit"
+ @keydown.ctrl.u.prevent
+ @keydown.meta.u.prevent
+ @keydown.ctrl.b.prevent
+ @keydown.meta.b.prevent
+ >{{ title }}</span
+ >
+ </h2>
+</template>
diff --git a/app/assets/javascripts/work_items/graphql/create_work_item.mutation.graphql b/app/assets/javascripts/work_items/graphql/create_work_item.mutation.graphql
new file mode 100644
index 00000000000..2f302dae7d7
--- /dev/null
+++ b/app/assets/javascripts/work_items/graphql/create_work_item.mutation.graphql
@@ -0,0 +1,18 @@
+#import './widget.fragment.graphql'
+
+mutation createWorkItem($input: LocalCreateWorkItemInput) {
+ localCreateWorkItem(input: $input) @client {
+ workItem {
+ id
+ type
+ widgets {
+ nodes {
+ ...WidgetBase
+ ... on LocalTitleWidget {
+ contentText
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/app/assets/javascripts/work_items/graphql/fragmentTypes.json b/app/assets/javascripts/work_items/graphql/fragmentTypes.json
index c048ac34ac0..3b837e84ee9 100644
--- a/app/assets/javascripts/work_items/graphql/fragmentTypes.json
+++ b/app/assets/javascripts/work_items/graphql/fragmentTypes.json
@@ -1 +1 @@
-{"__schema":{"types":[{"kind":"INTERFACE","name":"WorkItemWidget","possibleTypes":[{"name":"TitleWidget"}]}]}}
+{"__schema":{"types":[{"kind":"INTERFACE","name":"LocalWorkItemWidget","possibleTypes":[{"name":"LocalTitleWidget"}]}]}}
diff --git a/app/assets/javascripts/work_items/graphql/provider.js b/app/assets/javascripts/work_items/graphql/provider.js
index 083735336ce..fb536a425c0 100644
--- a/app/assets/javascripts/work_items/graphql/provider.js
+++ b/app/assets/javascripts/work_items/graphql/provider.js
@@ -4,6 +4,7 @@ import { IntrospectionFragmentMatcher } from 'apollo-cache-inmemory';
import createDefaultClient from '~/lib/graphql';
import workItemQuery from './work_item.query.graphql';
import introspectionQueryResultData from './fragmentTypes.json';
+import { resolvers } from './resolvers';
import typeDefs from './typedefs.graphql';
const fragmentMatcher = new IntrospectionFragmentMatcher({
@@ -13,15 +14,12 @@ const fragmentMatcher = new IntrospectionFragmentMatcher({
export function createApolloProvider() {
Vue.use(VueApollo);
- const defaultClient = createDefaultClient(
- {},
- {
- cacheConfig: {
- fragmentMatcher,
- },
- typeDefs,
+ const defaultClient = createDefaultClient(resolvers, {
+ cacheConfig: {
+ fragmentMatcher,
},
- );
+ typeDefs,
+ });
defaultClient.cache.writeQuery({
query: workItemQuery,
@@ -30,14 +28,14 @@ export function createApolloProvider() {
},
data: {
workItem: {
- __typename: 'WorkItem',
+ __typename: 'LocalWorkItem',
id: '1',
type: 'FEATURE',
widgets: {
- __typename: 'WorkItemWidgetConnection',
+ __typename: 'LocalWorkItemWidgetConnection',
nodes: [
{
- __typename: 'TitleWidget',
+ __typename: 'LocalTitleWidget',
type: 'TITLE',
enabled: true,
// eslint-disable-next-line @gitlab/require-i18n-strings
diff --git a/app/assets/javascripts/work_items/graphql/resolvers.js b/app/assets/javascripts/work_items/graphql/resolvers.js
index e69de29bb2d..63d5234d083 100644
--- a/app/assets/javascripts/work_items/graphql/resolvers.js
+++ b/app/assets/javascripts/work_items/graphql/resolvers.js
@@ -0,0 +1,58 @@
+import { uuids } from '~/lib/utils/uuids';
+import workItemQuery from './work_item.query.graphql';
+
+export const resolvers = {
+ Mutation: {
+ localCreateWorkItem(_, { input }, { cache }) {
+ const id = uuids()[0];
+ const workItem = {
+ __typename: 'LocalWorkItem',
+ type: 'FEATURE',
+ id,
+ widgets: {
+ __typename: 'LocalWorkItemWidgetConnection',
+ nodes: [
+ {
+ __typename: 'LocalTitleWidget',
+ type: 'TITLE',
+ enabled: true,
+ contentText: input.title,
+ },
+ ],
+ },
+ };
+
+ cache.writeQuery({ query: workItemQuery, variables: { id }, data: { workItem } });
+
+ return {
+ __typename: 'LocalCreateWorkItemPayload',
+ workItem,
+ };
+ },
+
+ localUpdateWorkItem(_, { input }, { cache }) {
+ const workItemTitle = {
+ __typename: 'LocalTitleWidget',
+ type: 'TITLE',
+ enabled: true,
+ contentText: input.title,
+ };
+ const workItem = {
+ __typename: 'LocalWorkItem',
+ type: 'FEATURE',
+ id: input.id,
+ widgets: {
+ __typename: 'LocalWorkItemWidgetConnection',
+ nodes: [workItemTitle],
+ },
+ };
+
+ cache.writeQuery({ query: workItemQuery, variables: { id: input.id }, data: { workItem } });
+
+ return {
+ __typename: 'LocalUpdateWorkItemPayload',
+ workItem,
+ };
+ },
+ },
+};
diff --git a/app/assets/javascripts/work_items/graphql/typedefs.graphql b/app/assets/javascripts/work_items/graphql/typedefs.graphql
index 4a6e4aeed60..177eea00322 100644
--- a/app/assets/javascripts/work_items/graphql/typedefs.graphql
+++ b/app/assets/javascripts/work_items/graphql/typedefs.graphql
@@ -1,38 +1,60 @@
-enum WorkItemType {
+enum LocalWorkItemType {
FEATURE
}
-enum WidgetType {
+enum LocalWidgetType {
TITLE
}
-interface WorkItemWidget {
- type: WidgetType!
+interface LocalWorkItemWidget {
+ type: LocalWidgetType!
}
# Replicating Relay connection type for client schema
-type WorkItemWidgetEdge {
+type LocalWorkItemWidgetEdge {
cursor: String!
- node: WorkItemWidget
+ node: LocalWorkItemWidget
}
-type WorkItemWidgetConnection {
- edges: [WorkItemWidgetEdge]
- nodes: [WorkItemWidget]
+type LocalWorkItemWidgetConnection {
+ edges: [LocalWorkItemWidgetEdge]
+ nodes: [LocalWorkItemWidget]
pageInfo: PageInfo!
}
-type TitleWidget implements WorkItemWidget {
- type: WidgetType!
+type LocalTitleWidget implements LocalWorkItemWidget {
+ type: LocalWidgetType!
contentText: String!
}
-type WorkItem {
+type LocalWorkItem {
id: ID!
- type: WorkItemType!
- widgets: [WorkItemWidgetConnection]
+ type: LocalWorkItemType!
+ widgets: [LocalWorkItemWidgetConnection]
+}
+
+input LocalCreateWorkItemInput {
+ title: String!
+}
+
+input LocalUpdateWorkItemInput {
+ id: ID!
+ title: String
+}
+
+type LocalCreateWorkItemPayload {
+ workItem: LocalWorkItem!
+}
+
+type LocalUpdateWorkItemPayload {
+ workItem: LocalWorkItem!
}
extend type Query {
- workItem(id: ID!): WorkItem!
+ workItem(id: ID!): LocalWorkItem!
+}
+
+extend type Mutation {
+ localCreateWorkItem(input: LocalCreateWorkItemInput!): LocalCreateWorkItemPayload!
+ localUpdateWorkItem(input: LocalUpdateWorkItemInput!): LocalUpdateWorkItemPayload!
}
diff --git a/app/assets/javascripts/work_items/graphql/update_work_item.mutation.graphql b/app/assets/javascripts/work_items/graphql/update_work_item.mutation.graphql
new file mode 100644
index 00000000000..f0563f099b2
--- /dev/null
+++ b/app/assets/javascripts/work_items/graphql/update_work_item.mutation.graphql
@@ -0,0 +1,18 @@
+#import './widget.fragment.graphql'
+
+mutation updateWorkItem($input: LocalUpdateWorkItemInput) {
+ localUpdateWorkItem(input: $input) @client {
+ workItem {
+ id
+ type
+ widgets {
+ nodes {
+ ...WidgetBase
+ ... on LocalTitleWidget {
+ contentText
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/app/assets/javascripts/work_items/graphql/widget.fragment.graphql b/app/assets/javascripts/work_items/graphql/widget.fragment.graphql
index d7608c26052..154367dc0d8 100644
--- a/app/assets/javascripts/work_items/graphql/widget.fragment.graphql
+++ b/app/assets/javascripts/work_items/graphql/widget.fragment.graphql
@@ -1,3 +1,3 @@
-fragment WidgetBase on WorkItemWidget {
+fragment WidgetBase on LocalWorkItemWidget {
type
}
diff --git a/app/assets/javascripts/work_items/graphql/work_item.query.graphql b/app/assets/javascripts/work_items/graphql/work_item.query.graphql
index 549e4f8c65a..9f173f7c302 100644
--- a/app/assets/javascripts/work_items/graphql/work_item.query.graphql
+++ b/app/assets/javascripts/work_items/graphql/work_item.query.graphql
@@ -7,7 +7,7 @@ query WorkItem($id: ID!) {
widgets {
nodes {
...WidgetBase
- ... on TitleWidget {
+ ... on LocalTitleWidget {
contentText
}
}
diff --git a/app/assets/javascripts/work_items/pages/create_work_item.vue b/app/assets/javascripts/work_items/pages/create_work_item.vue
new file mode 100644
index 00000000000..12bad5606d4
--- /dev/null
+++ b/app/assets/javascripts/work_items/pages/create_work_item.vue
@@ -0,0 +1,71 @@
+<script>
+import { GlButton, GlAlert } from '@gitlab/ui';
+import createWorkItemMutation from '../graphql/create_work_item.mutation.graphql';
+
+import ItemTitle from '../components/item_title.vue';
+
+export default {
+ components: {
+ GlButton,
+ GlAlert,
+ ItemTitle,
+ },
+ data() {
+ return {
+ title: '',
+ error: false,
+ };
+ },
+ methods: {
+ async createWorkItem() {
+ try {
+ const response = await this.$apollo.mutate({
+ mutation: createWorkItemMutation,
+ variables: {
+ input: {
+ title: this.title,
+ },
+ },
+ });
+
+ const {
+ data: {
+ localCreateWorkItem: {
+ workItem: { id },
+ },
+ },
+ } = response;
+ this.$router.push({ name: 'workItem', params: { id } });
+ } catch {
+ this.error = true;
+ }
+ },
+ handleTitleInput(title) {
+ this.title = title;
+ },
+ },
+};
+</script>
+
+<template>
+ <form @submit.prevent="createWorkItem">
+ <gl-alert v-if="error" variant="danger" @dismiss="error = false">{{
+ __('Something went wrong when creating a work item. Please try again')
+ }}</gl-alert>
+ <item-title data-testid="title-input" @title-input="handleTitleInput" />
+ <div class="gl-bg-gray-10 gl-py-5 gl-px-6">
+ <gl-button
+ variant="confirm"
+ :disabled="title.length === 0"
+ class="gl-mr-3"
+ data-testid="create-button"
+ type="submit"
+ >
+ {{ __('Create') }}
+ </gl-button>
+ <gl-button type="button" data-testid="cancel-button" @click="$router.go(-1)">
+ {{ __('Cancel') }}
+ </gl-button>
+ </div>
+ </form>
+</template>
diff --git a/app/assets/javascripts/work_items/pages/work_item_root.vue b/app/assets/javascripts/work_items/pages/work_item_root.vue
index 493ee0aba01..479274baf3a 100644
--- a/app/assets/javascripts/work_items/pages/work_item_root.vue
+++ b/app/assets/javascripts/work_items/pages/work_item_root.vue
@@ -1,8 +1,16 @@
<script>
+import { GlAlert } from '@gitlab/ui';
import workItemQuery from '../graphql/work_item.query.graphql';
+import updateWorkItemMutation from '../graphql/update_work_item.mutation.graphql';
import { widgetTypes } from '../constants';
+import ItemTitle from '../components/item_title.vue';
+
export default {
+ components: {
+ ItemTitle,
+ GlAlert,
+ },
props: {
id: {
type: String,
@@ -12,6 +20,7 @@ export default {
data() {
return {
workItem: null,
+ error: false,
};
},
apollo: {
@@ -29,20 +38,39 @@ export default {
return this.workItem?.widgets?.nodes?.find((widget) => widget.type === widgetTypes.title);
},
},
+ methods: {
+ async updateWorkItem(title) {
+ try {
+ await this.$apollo.mutate({
+ mutation: updateWorkItemMutation,
+ variables: {
+ input: {
+ id: this.id,
+ title,
+ },
+ },
+ });
+ } catch {
+ this.error = true;
+ }
+ },
+ },
};
</script>
<template>
<section>
+ <gl-alert v-if="error" variant="danger" @dismiss="error = false">{{
+ __('Something went wrong while updating work item. Please try again')
+ }}</gl-alert>
<!-- Title widget placeholder -->
<div>
- <h2
+ <item-title
v-if="titleWidgetData"
- class="gl-font-weight-normal gl-sm-font-weight-bold gl-my-5"
+ :initial-title="titleWidgetData.contentText"
data-testid="title"
- >
- {{ titleWidgetData.contentText }}
- </h2>
+ @title-changed="updateWorkItem"
+ />
</div>
</section>
</template>
diff --git a/app/assets/javascripts/work_items/router/routes.js b/app/assets/javascripts/work_items/router/routes.js
index a3cf44ad4ca..95772bbd026 100644
--- a/app/assets/javascripts/work_items/router/routes.js
+++ b/app/assets/javascripts/work_items/router/routes.js
@@ -1,7 +1,12 @@
export const routes = [
{
+ path: '/new',
+ name: 'createWorkItem',
+ component: () => import('../pages/create_work_item.vue'),
+ },
+ {
path: '/:id',
- name: 'work_item',
+ name: 'workItem',
component: () => import('../pages/work_item_root.vue'),
props: true,
},