summaryrefslogtreecommitdiff
path: root/app/assets/javascripts
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2020-09-19 01:45:44 +0000
committerGitLab Bot <gitlab-bot@gitlab.com>2020-09-19 01:45:44 +0000
commit85dc423f7090da0a52c73eb66faf22ddb20efff9 (patch)
tree9160f299afd8c80c038f08e1545be119f5e3f1e1 /app/assets/javascripts
parent15c2c8c66dbe422588e5411eee7e68f1fa440bb8 (diff)
downloadgitlab-ce-85dc423f7090da0a52c73eb66faf22ddb20efff9.tar.gz
Add latest changes from gitlab-org/gitlab@13-4-stable-ee
Diffstat (limited to 'app/assets/javascripts')
-rw-r--r--app/assets/javascripts/admin/application_settings/setup_metrics_and_profiling.js7
-rw-r--r--app/assets/javascripts/admin/cohorts/components/usage_ping_disabled.vue48
-rw-r--r--app/assets/javascripts/admin/dev_ops_report/components/usage_ping_disabled.vue53
-rw-r--r--app/assets/javascripts/admin/statistics_panel/store/getters.js1
-rw-r--r--app/assets/javascripts/ajax_loading_spinner.js34
-rw-r--r--app/assets/javascripts/alert_handler.js13
-rw-r--r--app/assets/javascripts/alert_management/components/alert_details.vue42
-rw-r--r--app/assets/javascripts/alert_management/components/alert_management_table.vue61
-rw-r--r--app/assets/javascripts/alert_management/components/alert_status.vue6
-rw-r--r--app/assets/javascripts/alert_management/components/sidebar/sidebar_assignees.vue53
-rw-r--r--app/assets/javascripts/alert_management/components/sidebar/sidebar_todo.vue18
-rw-r--r--app/assets/javascripts/alert_management/components/system_notes/system_note.vue3
-rw-r--r--app/assets/javascripts/alert_management/constants.js2
-rw-r--r--app/assets/javascripts/alert_management/details.js9
-rw-r--r--app/assets/javascripts/alert_management/graphql/fragments/list_item.fragment.graphql3
-rw-r--r--app/assets/javascripts/alert_management/graphql/mutations/alert_set_assignees.mutation.graphql3
-rw-r--r--app/assets/javascripts/api.js8
-rw-r--r--app/assets/javascripts/authentication/mount_2fa.js19
-rw-r--r--app/assets/javascripts/authentication/u2f/authenticate.js3
-rw-r--r--app/assets/javascripts/authentication/u2f/register.js26
-rw-r--r--app/assets/javascripts/authentication/webauthn/authenticate.js69
-rw-r--r--app/assets/javascripts/authentication/webauthn/error.js28
-rw-r--r--app/assets/javascripts/authentication/webauthn/flow.js24
-rw-r--r--app/assets/javascripts/authentication/webauthn/index.js13
-rw-r--r--app/assets/javascripts/authentication/webauthn/register.js78
-rw-r--r--app/assets/javascripts/authentication/webauthn/util.js120
-rw-r--r--app/assets/javascripts/badges/components/badge.vue9
-rw-r--r--app/assets/javascripts/badges/components/badge_form.vue30
-rw-r--r--app/assets/javascripts/badges/components/badge_list_row.vue9
-rw-r--r--app/assets/javascripts/batch_comments/components/draft_note.vue1
-rw-r--r--app/assets/javascripts/batch_comments/components/preview_dropdown.vue33
-rw-r--r--app/assets/javascripts/batch_comments/components/preview_item.vue9
-rw-r--r--app/assets/javascripts/batch_comments/components/review_bar.vue1
-rw-r--r--app/assets/javascripts/batch_comments/index.js1
-rw-r--r--app/assets/javascripts/behaviors/autosize.js11
-rw-r--r--app/assets/javascripts/behaviors/index.js22
-rw-r--r--app/assets/javascripts/behaviors/markdown/gfm_auto_complete.js5
-rw-r--r--app/assets/javascripts/behaviors/markdown/render_gfm.js21
-rw-r--r--app/assets/javascripts/behaviors/markdown/render_math.js3
-rw-r--r--app/assets/javascripts/behaviors/shortcuts.js6
-rw-r--r--app/assets/javascripts/behaviors/shortcuts/shortcuts.js99
-rw-r--r--app/assets/javascripts/behaviors/shortcuts/shortcuts_find_file.js17
-rw-r--r--app/assets/javascripts/blob/components/blob_edit_content.vue2
-rw-r--r--app/assets/javascripts/blob/components/blob_embeddable.vue41
-rw-r--r--app/assets/javascripts/blob/file_template_mediator.js8
-rw-r--r--app/assets/javascripts/blob/file_template_selector.js8
-rw-r--r--app/assets/javascripts/blob/pipeline_tour_success_modal.vue45
-rw-r--r--app/assets/javascripts/blob/suggest_gitlab_ci_yml/components/popover.vue6
-rw-r--r--app/assets/javascripts/blob/suggest_gitlab_ci_yml/index.js1
-rw-r--r--app/assets/javascripts/blob/suggest_web_ide_ci/components/web_ide_alert.vue50
-rw-r--r--app/assets/javascripts/blob/suggest_web_ide_ci/index.js20
-rw-r--r--app/assets/javascripts/blob/template_selector.js4
-rw-r--r--app/assets/javascripts/blob/template_selectors/ci_yaml_selector.js3
-rw-r--r--app/assets/javascripts/blob/template_selectors/dockerfile_selector.js3
-rw-r--r--app/assets/javascripts/blob/template_selectors/gitignore_selector.js3
-rw-r--r--app/assets/javascripts/blob/template_selectors/license_selector.js3
-rw-r--r--app/assets/javascripts/blob/template_selectors/metrics_dashboard_selector.js3
-rw-r--r--app/assets/javascripts/blob/template_selectors/type_selector.js3
-rw-r--r--app/assets/javascripts/blob/utils.js8
-rw-r--r--app/assets/javascripts/blob_edit/blob_bundle.js11
-rw-r--r--app/assets/javascripts/blob_edit/edit_blob.js15
-rw-r--r--app/assets/javascripts/boards/boards_util.js64
-rw-r--r--app/assets/javascripts/boards/components/board_blank_state.vue18
-rw-r--r--app/assets/javascripts/boards/components/board_card.vue112
-rw-r--r--app/assets/javascripts/boards/components/board_card_layout.vue93
-rw-r--r--app/assets/javascripts/boards/components/board_column.vue63
-rw-r--r--app/assets/javascripts/boards/components/board_content.vue70
-rw-r--r--app/assets/javascripts/boards/components/board_form.vue6
-rw-r--r--app/assets/javascripts/boards/components/board_list.vue27
-rw-r--r--app/assets/javascripts/boards/components/board_list_header.vue60
-rw-r--r--app/assets/javascripts/boards/components/board_new_issue.vue47
-rw-r--r--app/assets/javascripts/boards/components/board_settings_sidebar.vue30
-rw-r--r--app/assets/javascripts/boards/components/board_sidebar.js4
-rw-r--r--app/assets/javascripts/boards/components/boards_selector.vue10
-rw-r--r--app/assets/javascripts/boards/components/issuable_title.vue21
-rw-r--r--app/assets/javascripts/boards/components/issue_card_inner.vue24
-rw-r--r--app/assets/javascripts/boards/components/issue_due_date.vue7
-rw-r--r--app/assets/javascripts/boards/components/issue_time_estimate.vue7
-rw-r--r--app/assets/javascripts/boards/components/modal/empty_state.vue24
-rw-r--r--app/assets/javascripts/boards/components/modal/footer.vue10
-rw-r--r--app/assets/javascripts/boards/components/modal/header.vue26
-rw-r--r--app/assets/javascripts/boards/components/modal/index.vue25
-rw-r--r--app/assets/javascripts/boards/components/modal/list.vue16
-rw-r--r--app/assets/javascripts/boards/components/modal/lists_dropdown.vue7
-rw-r--r--app/assets/javascripts/boards/components/new_list_dropdown.js3
-rw-r--r--app/assets/javascripts/boards/components/project_select.vue22
-rw-r--r--app/assets/javascripts/boards/components/sidebar/board_editable_item.vue79
-rw-r--r--app/assets/javascripts/boards/constants.js3
-rw-r--r--app/assets/javascripts/boards/filtered_search_boards.js5
-rw-r--r--app/assets/javascripts/boards/index.js82
-rw-r--r--app/assets/javascripts/boards/models/issue.js2
-rw-r--r--app/assets/javascripts/boards/models/list.js2
-rw-r--r--app/assets/javascripts/boards/mount_multiple_boards_switcher.js2
-rw-r--r--app/assets/javascripts/boards/queries/board_list_create.mutation.graphql10
-rw-r--r--app/assets/javascripts/boards/queries/board_list_shared.fragment.graphql2
-rw-r--r--app/assets/javascripts/boards/queries/board_list_update.mutation.graphql10
-rw-r--r--app/assets/javascripts/boards/queries/group_lists_issues.query.graphql18
-rw-r--r--app/assets/javascripts/boards/queries/issue.fragment.graphql6
-rw-r--r--app/assets/javascripts/boards/queries/issue_move_list.mutation.graphql28
-rw-r--r--app/assets/javascripts/boards/queries/lists_issues.query.graphql39
-rw-r--r--app/assets/javascripts/boards/queries/project_lists_issues.query.graphql18
-rw-r--r--app/assets/javascripts/boards/stores/actions.js270
-rw-r--r--app/assets/javascripts/boards/stores/boards_store.js34
-rw-r--r--app/assets/javascripts/boards/stores/getters.js22
-rw-r--r--app/assets/javascripts/boards/stores/mutation_types.js21
-rw-r--r--app/assets/javascripts/boards/stores/mutations.js135
-rw-r--r--app/assets/javascripts/boards/stores/state.js10
-rw-r--r--app/assets/javascripts/branches/ajax_loading_spinner.js31
-rw-r--r--app/assets/javascripts/ci_lint/components/ci_lint.vue14
-rw-r--r--app/assets/javascripts/ci_lint/components/ci_lint_results.vue9
-rw-r--r--app/assets/javascripts/ci_lint/index.js18
-rw-r--r--app/assets/javascripts/ci_variable_list/ci_variable_list.js2
-rw-r--r--app/assets/javascripts/ci_variable_list/components/ci_environments_dropdown.vue2
-rw-r--r--app/assets/javascripts/ci_variable_list/components/ci_variable_table.vue1
-rw-r--r--app/assets/javascripts/ci_variable_list/constants.js1
-rw-r--r--app/assets/javascripts/ci_variable_list/store/getters.js3
-rw-r--r--app/assets/javascripts/clusters/clusters_bundle.js2
-rw-r--r--app/assets/javascripts/clusters/components/application_row.vue45
-rw-r--r--app/assets/javascripts/clusters/components/knative_domain_editor.vue17
-rw-r--r--app/assets/javascripts/clusters/components/new_cluster.vue34
-rw-r--r--app/assets/javascripts/clusters/components/remove_cluster_confirmation.vue33
-rw-r--r--app/assets/javascripts/clusters/components/uninstall_application_button.vue8
-rw-r--r--app/assets/javascripts/clusters/components/uninstall_application_confirmation_modal.vue1
-rw-r--r--app/assets/javascripts/clusters/components/update_application_confirmation_modal.vue1
-rw-r--r--app/assets/javascripts/clusters/new_cluster.js19
-rw-r--r--app/assets/javascripts/clusters/stores/new_cluster/index.js12
-rw-r--r--app/assets/javascripts/clusters/stores/new_cluster/state.js3
-rw-r--r--app/assets/javascripts/clusters_list/components/clusters.vue2
-rw-r--r--app/assets/javascripts/commit/pipelines/pipelines_table.vue2
-rw-r--r--app/assets/javascripts/commons/jquery.js3
-rw-r--r--app/assets/javascripts/compare_autocomplete.js3
-rw-r--r--app/assets/javascripts/confidential_merge_request/components/dropdown.vue11
-rw-r--r--app/assets/javascripts/confidential_merge_request/components/project_form_group.vue5
-rw-r--r--app/assets/javascripts/contributors/stores/actions.js1
-rw-r--r--app/assets/javascripts/create_cluster/eks_cluster/components/eks_cluster_configuration_form.vue1
-rw-r--r--app/assets/javascripts/create_cluster/eks_cluster/components/service_credentials_form.vue16
-rw-r--r--app/assets/javascripts/create_cluster/eks_cluster/constants.js8
-rw-r--r--app/assets/javascripts/create_cluster/eks_cluster/store/actions.js1
-rw-r--r--app/assets/javascripts/create_cluster/eks_cluster/store/getters.js1
-rw-r--r--app/assets/javascripts/create_cluster/eks_cluster/store/state.js2
-rw-r--r--app/assets/javascripts/create_item_dropdown.js8
-rw-r--r--app/assets/javascripts/custom_metrics/components/custom_metrics_form_fields.vue14
-rw-r--r--app/assets/javascripts/cycle_analytics/components/banner.vue7
-rw-r--r--app/assets/javascripts/cycle_analytics/components/stage_review_component.vue4
-rw-r--r--app/assets/javascripts/cycle_analytics/components/stage_staging_component.vue7
-rw-r--r--app/assets/javascripts/cycle_analytics/components/stage_test_component.vue7
-rw-r--r--app/assets/javascripts/cycle_analytics/cycle_analytics_store.js3
-rw-r--r--app/assets/javascripts/deploy_keys/components/app.vue9
-rw-r--r--app/assets/javascripts/deploy_keys/components/key.vue16
-rw-r--r--app/assets/javascripts/deploy_keys/service/index.js10
-rw-r--r--app/assets/javascripts/deprecated_jquery_dropdown/gl_dropdown.js (renamed from app/assets/javascripts/gl_dropdown.js)252
-rw-r--r--app/assets/javascripts/deprecated_jquery_dropdown/gl_dropdown_filter.js135
-rw-r--r--app/assets/javascripts/deprecated_jquery_dropdown/gl_dropdown_input.js44
-rw-r--r--app/assets/javascripts/deprecated_jquery_dropdown/gl_dropdown_remote.js42
-rw-r--r--app/assets/javascripts/deprecated_jquery_dropdown/index.js11
-rw-r--r--app/assets/javascripts/deprecated_jquery_dropdown/render.js (renamed from app/assets/javascripts/gl_dropdown/render.js)16
-rw-r--r--app/assets/javascripts/design_management/components/delete_button.vue3
-rw-r--r--app/assets/javascripts/design_management/components/design_notes/design_discussion.vue92
-rw-r--r--app/assets/javascripts/design_management/components/design_notes/design_note.vue45
-rw-r--r--app/assets/javascripts/design_management/components/design_notes/design_reply_form.vue21
-rw-r--r--app/assets/javascripts/design_management/components/design_overlay.vue49
-rw-r--r--app/assets/javascripts/design_management/components/design_sidebar.vue59
-rw-r--r--app/assets/javascripts/design_management/components/design_todo_button.vue168
-rw-r--r--app/assets/javascripts/design_management/components/list/item.vue29
-rw-r--r--app/assets/javascripts/design_management/components/upload/design_version_dropdown.vue14
-rw-r--r--app/assets/javascripts/design_management/constants.js1
-rw-r--r--app/assets/javascripts/design_management/graphql.js59
-rw-r--r--app/assets/javascripts/design_management/graphql/fragments/design_list.fragment.graphql5
-rw-r--r--app/assets/javascripts/design_management/graphql/fragments/design_note.fragment.graphql5
-rw-r--r--app/assets/javascripts/design_management/graphql/mutations/create_design_todo.mutation.graphql17
-rw-r--r--app/assets/javascripts/design_management/graphql/queries/get_design.query.graphql1
-rw-r--r--app/assets/javascripts/design_management/index.js2
-rw-r--r--app/assets/javascripts/design_management/pages/design/index.vue29
-rw-r--r--app/assets/javascripts/design_management/pages/index.vue11
-rw-r--r--app/assets/javascripts/design_management/utils/cache_update.js283
-rw-r--r--app/assets/javascripts/design_management/utils/design_management_utils.js34
-rw-r--r--app/assets/javascripts/design_management/utils/error_messages.js8
-rw-r--r--app/assets/javascripts/design_management/utils/tracking.js1
-rw-r--r--app/assets/javascripts/design_management_legacy/components/app.vue3
-rw-r--r--app/assets/javascripts/design_management_legacy/components/delete_button.vue64
-rw-r--r--app/assets/javascripts/design_management_legacy/components/design_destroyer.vue66
-rw-r--r--app/assets/javascripts/design_management_legacy/components/design_note_pin.vue61
-rw-r--r--app/assets/javascripts/design_management_legacy/components/design_notes/design_discussion.vue297
-rw-r--r--app/assets/javascripts/design_management_legacy/components/design_notes/design_note.vue156
-rw-r--r--app/assets/javascripts/design_management_legacy/components/design_notes/design_reply_form.vue141
-rw-r--r--app/assets/javascripts/design_management_legacy/components/design_notes/toggle_replies_widget.vue70
-rw-r--r--app/assets/javascripts/design_management_legacy/components/design_overlay.vue287
-rw-r--r--app/assets/javascripts/design_management_legacy/components/design_presentation.vue322
-rw-r--r--app/assets/javascripts/design_management_legacy/components/design_scaler.vue65
-rw-r--r--app/assets/javascripts/design_management_legacy/components/design_sidebar.vue178
-rw-r--r--app/assets/javascripts/design_management_legacy/components/image.vue110
-rw-r--r--app/assets/javascripts/design_management_legacy/components/list/item.vue174
-rw-r--r--app/assets/javascripts/design_management_legacy/components/toolbar/index.vue126
-rw-r--r--app/assets/javascripts/design_management_legacy/components/toolbar/pagination.vue83
-rw-r--r--app/assets/javascripts/design_management_legacy/components/toolbar/pagination_button.vue48
-rw-r--r--app/assets/javascripts/design_management_legacy/components/upload/button.vue58
-rw-r--r--app/assets/javascripts/design_management_legacy/components/upload/design_dropzone.vue134
-rw-r--r--app/assets/javascripts/design_management_legacy/components/upload/design_version_dropdown.vue76
-rw-r--r--app/assets/javascripts/design_management_legacy/constants.js16
-rw-r--r--app/assets/javascripts/design_management_legacy/graphql.js45
-rw-r--r--app/assets/javascripts/design_management_legacy/graphql/fragments/design.fragment.graphql24
-rw-r--r--app/assets/javascripts/design_management_legacy/graphql/fragments/design_list.fragment.graphql8
-rw-r--r--app/assets/javascripts/design_management_legacy/graphql/fragments/design_note.fragment.graphql29
-rw-r--r--app/assets/javascripts/design_management_legacy/graphql/fragments/diff_refs.fragment.graphql5
-rw-r--r--app/assets/javascripts/design_management_legacy/graphql/fragments/discussion_resolved_status.fragment.graphql9
-rw-r--r--app/assets/javascripts/design_management_legacy/graphql/fragments/note_permissions.fragment.graphql3
-rw-r--r--app/assets/javascripts/design_management_legacy/graphql/fragments/version.fragment.graphql4
-rw-r--r--app/assets/javascripts/design_management_legacy/graphql/mutations/create_image_diff_note.mutation.graphql21
-rw-r--r--app/assets/javascripts/design_management_legacy/graphql/mutations/create_note.mutation.graphql10
-rw-r--r--app/assets/javascripts/design_management_legacy/graphql/mutations/destroy_design.mutation.graphql10
-rw-r--r--app/assets/javascripts/design_management_legacy/graphql/mutations/toggle_resolve_discussion.mutation.graphql17
-rw-r--r--app/assets/javascripts/design_management_legacy/graphql/mutations/update_active_discussion.mutation.graphql3
-rw-r--r--app/assets/javascripts/design_management_legacy/graphql/mutations/update_image_diff_note.mutation.graphql10
-rw-r--r--app/assets/javascripts/design_management_legacy/graphql/mutations/update_note.mutation.graphql10
-rw-r--r--app/assets/javascripts/design_management_legacy/graphql/mutations/upload_design.mutation.graphql21
-rw-r--r--app/assets/javascripts/design_management_legacy/graphql/queries/active_discussion.query.graphql6
-rw-r--r--app/assets/javascripts/design_management_legacy/graphql/queries/app_data.query.graphql4
-rw-r--r--app/assets/javascripts/design_management_legacy/graphql/queries/design_permissions.query.graphql10
-rw-r--r--app/assets/javascripts/design_management_legacy/graphql/queries/get_design.query.graphql31
-rw-r--r--app/assets/javascripts/design_management_legacy/graphql/queries/get_design_list.query.graphql26
-rw-r--r--app/assets/javascripts/design_management_legacy/graphql/typedefs.graphql12
-rw-r--r--app/assets/javascripts/design_management_legacy/index.js61
-rw-r--r--app/assets/javascripts/design_management_legacy/mixins/all_designs.js49
-rw-r--r--app/assets/javascripts/design_management_legacy/mixins/all_versions.js62
-rw-r--r--app/assets/javascripts/design_management_legacy/pages/design/index.vue378
-rw-r--r--app/assets/javascripts/design_management_legacy/pages/index.vue323
-rw-r--r--app/assets/javascripts/design_management_legacy/router/constants.js3
-rw-r--r--app/assets/javascripts/design_management_legacy/router/index.js35
-rw-r--r--app/assets/javascripts/design_management_legacy/router/routes.js44
-rw-r--r--app/assets/javascripts/design_management_legacy/utils/cache_update.js276
-rw-r--r--app/assets/javascripts/design_management_legacy/utils/design_management_utils.js128
-rw-r--r--app/assets/javascripts/design_management_legacy/utils/error_messages.js95
-rw-r--r--app/assets/javascripts/design_management_legacy/utils/tracking.js27
-rw-r--r--app/assets/javascripts/diff_notes/components/jump_to_discussion.js1
-rw-r--r--app/assets/javascripts/diffs/components/app.vue231
-rw-r--r--app/assets/javascripts/diffs/components/collapsed_files_warning.vue71
-rw-r--r--app/assets/javascripts/diffs/components/commit_item.vue14
-rw-r--r--app/assets/javascripts/diffs/components/compare_dropdown_layout.vue6
-rw-r--r--app/assets/javascripts/diffs/components/compare_versions.vue35
-rw-r--r--app/assets/javascripts/diffs/components/diff_content.vue17
-rw-r--r--app/assets/javascripts/diffs/components/diff_discussions.vue6
-rw-r--r--app/assets/javascripts/diffs/components/diff_expansion_cell.vue52
-rw-r--r--app/assets/javascripts/diffs/components/diff_file.vue53
-rw-r--r--app/assets/javascripts/diffs/components/diff_file_header.vue59
-rw-r--r--app/assets/javascripts/diffs/components/diff_file_row.vue24
-rw-r--r--app/assets/javascripts/diffs/components/diff_gutter_avatars.vue7
-rw-r--r--app/assets/javascripts/diffs/components/diff_stats.vue6
-rw-r--r--app/assets/javascripts/diffs/components/edit_button.vue7
-rw-r--r--app/assets/javascripts/diffs/components/image_diff_overlay.vue8
-rw-r--r--app/assets/javascripts/diffs/components/inline_diff_table_row.vue156
-rw-r--r--app/assets/javascripts/diffs/components/inline_diff_view.vue3
-rw-r--r--app/assets/javascripts/diffs/components/merge_conflict_warning.vue72
-rw-r--r--app/assets/javascripts/diffs/components/no_changes.vue28
-rw-r--r--app/assets/javascripts/diffs/components/parallel_diff_table_row.vue273
-rw-r--r--app/assets/javascripts/diffs/components/settings_dropdown.vue138
-rw-r--r--app/assets/javascripts/diffs/components/tree_list.vue12
-rw-r--r--app/assets/javascripts/diffs/constants.js6
-rw-r--r--app/assets/javascripts/diffs/diff_file.js1
-rw-r--r--app/assets/javascripts/diffs/i18n.js14
-rw-r--r--app/assets/javascripts/diffs/store/actions.js75
-rw-r--r--app/assets/javascripts/diffs/store/getters.js9
-rw-r--r--app/assets/javascripts/diffs/store/getters_versions_dropdowns.js31
-rw-r--r--app/assets/javascripts/diffs/store/modules/diff_state.js2
-rw-r--r--app/assets/javascripts/diffs/store/mutation_types.js2
-rw-r--r--app/assets/javascripts/diffs/store/mutations.js18
-rw-r--r--app/assets/javascripts/diffs/store/utils.js80
-rw-r--r--app/assets/javascripts/diffs/utils/uuids.js3
-rw-r--r--app/assets/javascripts/due_date_select.js3
-rw-r--r--app/assets/javascripts/editor/constants.js7
-rw-r--r--app/assets/javascripts/editor/editor_file_template_ext.js7
-rw-r--r--app/assets/javascripts/editor/editor_lite.js113
-rw-r--r--app/assets/javascripts/editor/editor_markdown_ext.js14
-rw-r--r--app/assets/javascripts/environments/components/confirm_rollback_modal.vue1
-rw-r--r--app/assets/javascripts/environments/components/environment_actions.vue33
-rw-r--r--app/assets/javascripts/environments/components/environment_delete.vue7
-rw-r--r--app/assets/javascripts/environments/components/environment_item.vue11
-rw-r--r--app/assets/javascripts/environments/components/environment_monitoring.vue16
-rw-r--r--app/assets/javascripts/environments/components/environment_pin.vue18
-rw-r--r--app/assets/javascripts/environments/components/environment_rollback.vue24
-rw-r--r--app/assets/javascripts/environments/components/environment_terminal_button.vue7
-rw-r--r--app/assets/javascripts/environments/components/environments_table.vue9
-rw-r--r--app/assets/javascripts/environments/components/stop_environment_modal.vue2
-rw-r--r--app/assets/javascripts/environments/folder/environments_folder_bundle.js22
-rw-r--r--app/assets/javascripts/environments/folder/environments_folder_view.vue3
-rw-r--r--app/assets/javascripts/environments/index.js21
-rw-r--r--app/assets/javascripts/environments/mixins/canary_callout_mixin.js2
-rw-r--r--app/assets/javascripts/environments/stores/helpers.js1
-rw-r--r--app/assets/javascripts/error_tracking/components/error_details.vue6
-rw-r--r--app/assets/javascripts/error_tracking/components/stacktrace_entry.vue8
-rw-r--r--app/assets/javascripts/error_tracking/store/details/actions.js1
-rw-r--r--app/assets/javascripts/error_tracking/store/details/getters.js1
-rw-r--r--app/assets/javascripts/error_tracking_settings/components/error_tracking_form.vue7
-rw-r--r--app/assets/javascripts/filtered_search/issuable_filtered_search_token_keys.js5
-rw-r--r--app/assets/javascripts/flash.js2
-rw-r--r--app/assets/javascripts/frequent_items/components/frequent_items_list_item.vue2
-rw-r--r--app/assets/javascripts/frequent_items/components/frequent_items_search_input.vue6
-rw-r--r--app/assets/javascripts/frequent_items/index.js12
-rw-r--r--app/assets/javascripts/frequent_items/store/getters.js1
-rw-r--r--app/assets/javascripts/gfm_auto_complete.js22
-rw-r--r--app/assets/javascripts/gpg_badges.js3
-rw-r--r--app/assets/javascripts/grafana_integration/components/grafana_integration.vue7
-rw-r--r--app/assets/javascripts/graphql_shared/fragments/author.fragment.graphql1
-rw-r--r--app/assets/javascripts/graphql_shared/fragments/epic.fragment.graphql10
-rw-r--r--app/assets/javascripts/graphql_shared/fragments/label.fragment.graphql7
-rw-r--r--app/assets/javascripts/graphql_shared/mutations/todo_mark_done.mutation.graphql (renamed from app/assets/javascripts/alert_management/graphql/mutations/alert_todo_mark_done.mutation.graphql)0
-rw-r--r--app/assets/javascripts/groups/components/app.vue1
-rw-r--r--app/assets/javascripts/groups/components/group_item.vue1
-rw-r--r--app/assets/javascripts/groups/components/invite_members_banner.vue76
-rw-r--r--app/assets/javascripts/groups/components/item_actions.vue8
-rw-r--r--app/assets/javascripts/groups/components/item_caret.vue6
-rw-r--r--app/assets/javascripts/groups/components/item_stats_value.vue6
-rw-r--r--app/assets/javascripts/groups/components/item_type_icon.vue6
-rw-r--r--app/assets/javascripts/groups/init_invite_members_banner.js23
-rw-r--r--app/assets/javascripts/groups/members/components/app.vue11
-rw-r--r--app/assets/javascripts/groups/members/index.js30
-rw-r--r--app/assets/javascripts/groups/transfer_dropdown.js3
-rw-r--r--app/assets/javascripts/groups_select.js11
-rw-r--r--app/assets/javascripts/header.js79
-rw-r--r--app/assets/javascripts/helpers/startup_css_helper.js46
-rw-r--r--app/assets/javascripts/ide/components/activity_bar.vue14
-rw-r--r--app/assets/javascripts/ide/components/branches/item.vue6
-rw-r--r--app/assets/javascripts/ide/components/branches/search_list.vue7
-rw-r--r--app/assets/javascripts/ide/components/commit_sidebar/actions.vue1
-rw-r--r--app/assets/javascripts/ide/components/commit_sidebar/form.vue46
-rw-r--r--app/assets/javascripts/ide/components/commit_sidebar/list.vue9
-rw-r--r--app/assets/javascripts/ide/components/commit_sidebar/list_collapsed.vue10
-rw-r--r--app/assets/javascripts/ide/components/commit_sidebar/list_item.vue6
-rw-r--r--app/assets/javascripts/ide/components/commit_sidebar/message_field.vue6
-rw-r--r--app/assets/javascripts/ide/components/commit_sidebar/success_message.vue1
-rw-r--r--app/assets/javascripts/ide/components/error_message.vue1
-rw-r--r--app/assets/javascripts/ide/components/file_row_extra.vue6
-rw-r--r--app/assets/javascripts/ide/components/ide.vue3
-rw-r--r--app/assets/javascripts/ide/components/ide_file_row.vue12
-rw-r--r--app/assets/javascripts/ide/components/ide_review.vue4
-rw-r--r--app/assets/javascripts/ide/components/ide_side_bar.vue2
-rw-r--r--app/assets/javascripts/ide/components/ide_status_bar.vue7
-rw-r--r--app/assets/javascripts/ide/components/ide_status_list.vue7
-rw-r--r--app/assets/javascripts/ide/components/ide_tree.vue4
-rw-r--r--app/assets/javascripts/ide/components/ide_tree_list.vue2
-rw-r--r--app/assets/javascripts/ide/components/jobs/detail.vue15
-rw-r--r--app/assets/javascripts/ide/components/jobs/detail/description.vue14
-rw-r--r--app/assets/javascripts/ide/components/jobs/detail/scroll_button.vue6
-rw-r--r--app/assets/javascripts/ide/components/jobs/stage.vue7
-rw-r--r--app/assets/javascripts/ide/components/merge_requests/item.vue6
-rw-r--r--app/assets/javascripts/ide/components/merge_requests/list.vue9
-rw-r--r--app/assets/javascripts/ide/components/mr_file_icon.vue6
-rw-r--r--app/assets/javascripts/ide/components/nav_dropdown_button.vue8
-rw-r--r--app/assets/javascripts/ide/components/new_dropdown/button.vue6
-rw-r--r--app/assets/javascripts/ide/components/new_dropdown/index.vue6
-rw-r--r--app/assets/javascripts/ide/components/new_dropdown/modal.vue12
-rw-r--r--app/assets/javascripts/ide/components/new_dropdown/upload.vue3
-rw-r--r--app/assets/javascripts/ide/components/panes/collapsible_sidebar.vue2
-rw-r--r--app/assets/javascripts/ide/components/pipelines/list.vue12
-rw-r--r--app/assets/javascripts/ide/components/preview/navigator.vue11
-rw-r--r--app/assets/javascripts/ide/components/repo_editor.vue16
-rw-r--r--app/assets/javascripts/ide/components/repo_file_status_icon.vue6
-rw-r--r--app/assets/javascripts/ide/components/repo_tab.vue13
-rw-r--r--app/assets/javascripts/ide/components/repo_tabs.vue7
-rw-r--r--app/assets/javascripts/ide/components/shared/tokened_input.vue6
-rw-r--r--app/assets/javascripts/ide/components/terminal/empty_state.vue1
-rw-r--r--app/assets/javascripts/ide/components/terminal_sync/terminal_sync_status.vue7
-rw-r--r--app/assets/javascripts/ide/ide_router.js1
-rw-r--r--app/assets/javascripts/ide/lib/diff/diff.js2
-rw-r--r--app/assets/javascripts/ide/lib/editor.js7
-rw-r--r--app/assets/javascripts/ide/lib/editorconfig/parser.js1
-rw-r--r--app/assets/javascripts/ide/lib/errors.js39
-rw-r--r--app/assets/javascripts/ide/lib/files.js19
-rw-r--r--app/assets/javascripts/ide/lib/schemas/index.js4
-rw-r--r--app/assets/javascripts/ide/lib/schemas/json/index.js8
-rw-r--r--app/assets/javascripts/ide/lib/schemas/yaml/gitlab_ci.js4
-rw-r--r--app/assets/javascripts/ide/lib/schemas/yaml/index.js12
-rw-r--r--app/assets/javascripts/ide/services/gql.js1
-rw-r--r--app/assets/javascripts/ide/services/index.js6
-rw-r--r--app/assets/javascripts/ide/stores/actions.js22
-rw-r--r--app/assets/javascripts/ide/stores/actions/file.js14
-rw-r--r--app/assets/javascripts/ide/stores/actions/tree.js6
-rw-r--r--app/assets/javascripts/ide/stores/getters.js19
-rw-r--r--app/assets/javascripts/ide/stores/modules/commit/actions.js28
-rw-r--r--app/assets/javascripts/ide/stores/modules/commit/mutation_types.js3
-rw-r--r--app/assets/javascripts/ide/stores/modules/commit/mutations.js6
-rw-r--r--app/assets/javascripts/ide/stores/modules/commit/state.js1
-rw-r--r--app/assets/javascripts/ide/stores/modules/pane/getters.js1
-rw-r--r--app/assets/javascripts/ide/stores/modules/pipelines/actions.js22
-rw-r--r--app/assets/javascripts/ide/stores/modules/pipelines/constants.js1
-rw-r--r--app/assets/javascripts/ide/stores/modules/pipelines/mutation_types.js6
-rw-r--r--app/assets/javascripts/ide/stores/modules/pipelines/mutations.js6
-rw-r--r--app/assets/javascripts/ide/stores/modules/pipelines/utils.js1
-rw-r--r--app/assets/javascripts/ide/stores/modules/router/actions.js1
-rw-r--r--app/assets/javascripts/ide/stores/modules/router/mutation_types.js1
-rw-r--r--app/assets/javascripts/ide/stores/modules/terminal/getters.js1
-rw-r--r--app/assets/javascripts/ide/stores/mutations.js19
-rw-r--r--app/assets/javascripts/ide/stores/utils.js17
-rw-r--r--app/assets/javascripts/ide/sync_router_and_store.js1
-rw-r--r--app/assets/javascripts/ide/utils.js31
-rw-r--r--app/assets/javascripts/import_projects/components/import_projects_table.vue185
-rw-r--r--app/assets/javascripts/import_projects/components/imported_project_table_row.vue59
-rw-r--r--app/assets/javascripts/import_projects/components/incompatible_repo_table_row.vue32
-rw-r--r--app/assets/javascripts/import_projects/components/page_query_param_sync.vue39
-rw-r--r--app/assets/javascripts/import_projects/components/provider_repo_table_row.vue48
-rw-r--r--app/assets/javascripts/import_projects/store/actions.js39
-rw-r--r--app/assets/javascripts/import_projects/store/getters.js13
-rw-r--r--app/assets/javascripts/import_projects/store/mutations.js116
-rw-r--r--app/assets/javascripts/import_projects/store/state.js2
-rw-r--r--app/assets/javascripts/import_projects/utils.js12
-rw-r--r--app/assets/javascripts/incidents/components/incidents_list.vue67
-rw-r--r--app/assets/javascripts/incidents/constants.js3
-rw-r--r--app/assets/javascripts/incidents/graphql/fragments/incident_fields.fragment.graphql3
-rw-r--r--app/assets/javascripts/incidents/graphql/queries/get_incidents.query.graphql4
-rw-r--r--app/assets/javascripts/incidents_settings/components/alerts_form.vue23
-rw-r--r--app/assets/javascripts/incidents_settings/constants.js3
-rw-r--r--app/assets/javascripts/incidents_settings/index.js2
-rw-r--r--app/assets/javascripts/init_changes_dropdown.js3
-rw-r--r--app/assets/javascripts/init_issuable_sidebar.js4
-rw-r--r--app/assets/javascripts/integrations/edit/components/active_checkbox.vue (renamed from app/assets/javascripts/integrations/edit/components/active_toggle.vue)30
-rw-r--r--app/assets/javascripts/integrations/edit/components/dynamic_field.vue1
-rw-r--r--app/assets/javascripts/integrations/edit/components/integration_form.vue59
-rw-r--r--app/assets/javascripts/integrations/edit/components/jira_issues_fields.vue7
-rw-r--r--app/assets/javascripts/integrations/edit/components/override_dropdown.vue39
-rw-r--r--app/assets/javascripts/integrations/edit/constants.js17
-rw-r--r--app/assets/javascripts/integrations/edit/index.js26
-rw-r--r--app/assets/javascripts/integrations/edit/store/actions.js3
-rw-r--r--app/assets/javascripts/integrations/edit/store/getters.js6
-rw-r--r--app/assets/javascripts/integrations/edit/store/index.js1
-rw-r--r--app/assets/javascripts/integrations/edit/store/mutation_types.js3
-rw-r--r--app/assets/javascripts/integrations/edit/store/mutations.js6
-rw-r--r--app/assets/javascripts/integrations/edit/store/state.js8
-rw-r--r--app/assets/javascripts/integrations/integration_settings_form.js116
-rw-r--r--app/assets/javascripts/issuable_bulk_update_sidebar.js2
-rw-r--r--app/assets/javascripts/issuable_create/components/issuable_create_root.vue44
-rw-r--r--app/assets/javascripts/issuable_create/components/issuable_form.vue127
-rw-r--r--app/assets/javascripts/issuable_form.js1
-rw-r--r--app/assets/javascripts/issuable_list/components/issuable_item.vue140
-rw-r--r--app/assets/javascripts/issuable_list/components/issuable_list_root.vue153
-rw-r--r--app/assets/javascripts/issuable_list/components/issuable_tabs.vue53
-rw-r--r--app/assets/javascripts/issuable_suggestions/components/app.vue7
-rw-r--r--app/assets/javascripts/issuable_suggestions/components/item.vue11
-rw-r--r--app/assets/javascripts/issuables_list/service_desk_helper.js55
-rw-r--r--app/assets/javascripts/issue.js21
-rw-r--r--app/assets/javascripts/issue_show/components/app.vue25
-rw-r--r--app/assets/javascripts/issue_show/components/description.vue19
-rw-r--r--app/assets/javascripts/issue_show/components/edit_actions.vue35
-rw-r--r--app/assets/javascripts/issue_show/components/fields/description_template.vue21
-rw-r--r--app/assets/javascripts/issue_show/components/incidents/graphql/queries/get_alert.graphql20
-rw-r--r--app/assets/javascripts/issue_show/components/incidents/highlight_bar.vue42
-rw-r--r--app/assets/javascripts/issue_show/components/incidents/incident_tabs.vue71
-rw-r--r--app/assets/javascripts/issue_show/components/locked_warning.vue1
-rw-r--r--app/assets/javascripts/issue_show/components/pinned_links.vue19
-rw-r--r--app/assets/javascripts/issue_show/components/title.vue30
-rw-r--r--app/assets/javascripts/issue_show/incident.js36
-rw-r--r--app/assets/javascripts/issue_show/issue.js (renamed from app/assets/javascripts/issue_show/index.js)5
-rw-r--r--app/assets/javascripts/issue_show/stores/index.js5
-rw-r--r--app/assets/javascripts/issue_show/utils/parse_data.js14
-rw-r--r--app/assets/javascripts/issue_status_select.js3
-rw-r--r--app/assets/javascripts/issues_list/components/issuable.vue (renamed from app/assets/javascripts/issuables_list/components/issuable.vue)14
-rw-r--r--app/assets/javascripts/issues_list/components/issuables_list_app.vue (renamed from app/assets/javascripts/issuables_list/components/issuables_list_app.vue)2
-rw-r--r--app/assets/javascripts/issues_list/components/jira_issues_list_root.vue (renamed from app/assets/javascripts/issuables_list/components/issuable_list_root_app.vue)2
-rw-r--r--app/assets/javascripts/issues_list/constants.js (renamed from app/assets/javascripts/issuables_list/constants.js)0
-rw-r--r--app/assets/javascripts/issues_list/eventhub.js (renamed from app/assets/javascripts/issuables_list/eventhub.js)0
-rw-r--r--app/assets/javascripts/issues_list/index.js (renamed from app/assets/javascripts/issuables_list/index.js)8
-rw-r--r--app/assets/javascripts/issues_list/queries/get_issues_list_details.query.graphql (renamed from app/assets/javascripts/issuables_list/queries/get_issues_list_details.query.graphql)0
-rw-r--r--app/assets/javascripts/issues_list/service_desk_helper.js111
-rw-r--r--app/assets/javascripts/jira_connect.js56
-rw-r--r--app/assets/javascripts/jira_import/components/jira_import_form.vue26
-rw-r--r--app/assets/javascripts/jira_import/utils/jira_import_utils.js2
-rw-r--r--app/assets/javascripts/jobs/components/artifacts_block.vue16
-rw-r--r--app/assets/javascripts/jobs/components/job_app.vue14
-rw-r--r--app/assets/javascripts/jobs/components/job_container_item.vue9
-rw-r--r--app/assets/javascripts/jobs/components/job_log_controllers.vue34
-rw-r--r--app/assets/javascripts/jobs/components/log/line.vue2
-rw-r--r--app/assets/javascripts/jobs/components/log/line_header.vue8
-rw-r--r--app/assets/javascripts/jobs/components/manual_variables_form.vue26
-rw-r--r--app/assets/javascripts/jobs/components/sidebar.vue22
-rw-r--r--app/assets/javascripts/jobs/components/sidebar_detail_row.vue5
-rw-r--r--app/assets/javascripts/jobs/index.js2
-rw-r--r--app/assets/javascripts/labels_select.js9
-rw-r--r--app/assets/javascripts/layout_nav.js13
-rw-r--r--app/assets/javascripts/lib/graphql.js33
-rw-r--r--app/assets/javascripts/lib/utils/axios_startup_calls.js73
-rw-r--r--app/assets/javascripts/lib/utils/datetime_utility.js38
-rw-r--r--app/assets/javascripts/lib/utils/forms.js37
-rw-r--r--app/assets/javascripts/lib/utils/image_utility.js2
-rw-r--r--app/assets/javascripts/lib/utils/jquery_at_who.js3
-rw-r--r--app/assets/javascripts/lib/utils/poll.js6
-rw-r--r--app/assets/javascripts/lib/utils/set.js1
-rw-r--r--app/assets/javascripts/lib/utils/simple_poll.js4
-rw-r--r--app/assets/javascripts/lib/utils/text_markdown.js77
-rw-r--r--app/assets/javascripts/lib/utils/text_utility.js75
-rw-r--r--app/assets/javascripts/lib/utils/type_utility.js1
-rw-r--r--app/assets/javascripts/lib/utils/url_utility.js27
-rw-r--r--app/assets/javascripts/lib/utils/webpack.js1
-rw-r--r--app/assets/javascripts/line_highlighter.js1
-rw-r--r--app/assets/javascripts/logs/stores/getters.js14
-rw-r--r--app/assets/javascripts/main.js31
-rw-r--r--app/assets/javascripts/members.js3
-rw-r--r--app/assets/javascripts/merge_request.js8
-rw-r--r--app/assets/javascripts/merge_request_tabs.js1
-rw-r--r--app/assets/javascripts/milestone_select.js162
-rw-r--r--app/assets/javascripts/milestones/project_milestone_combobox.vue76
-rw-r--r--app/assets/javascripts/monitoring/components/alert_widget.vue2
-rw-r--r--app/assets/javascripts/monitoring/components/alert_widget_form.vue10
-rw-r--r--app/assets/javascripts/monitoring/components/charts/empty_chart.vue1
-rw-r--r--app/assets/javascripts/monitoring/components/charts/time_series.vue7
-rw-r--r--app/assets/javascripts/monitoring/components/dashboard.vue7
-rw-r--r--app/assets/javascripts/monitoring/components/dashboard_actions_menu.vue50
-rw-r--r--app/assets/javascripts/monitoring/components/dashboard_header.vue30
-rw-r--r--app/assets/javascripts/monitoring/components/dashboard_panel.vue6
-rw-r--r--app/assets/javascripts/monitoring/components/dashboards_dropdown.vue48
-rw-r--r--app/assets/javascripts/monitoring/components/embeds/embed_group.vue11
-rw-r--r--app/assets/javascripts/monitoring/components/group_empty_state.vue1
-rw-r--r--app/assets/javascripts/monitoring/components/refresh_button.vue26
-rw-r--r--app/assets/javascripts/monitoring/components/variables_section.vue7
-rw-r--r--app/assets/javascripts/monitoring/csv_export.js1
-rw-r--r--app/assets/javascripts/monitoring/stores/embed_group/actions.js1
-rw-r--r--app/assets/javascripts/monitoring/stores/embed_group/getters.js1
-rw-r--r--app/assets/javascripts/monitoring/stores/embed_group/mutation_types.js1
-rw-r--r--app/assets/javascripts/mr_popover/components/mr_popover.vue2
-rw-r--r--app/assets/javascripts/namespace_select.js4
-rw-r--r--app/assets/javascripts/notebook/cells/markdown.vue1
-rw-r--r--app/assets/javascripts/notebook/cells/output/html.vue1
-rw-r--r--app/assets/javascripts/notes.js5
-rw-r--r--app/assets/javascripts/notes/components/comment_form.vue51
-rw-r--r--app/assets/javascripts/notes/components/diff_discussion_header.vue1
-rw-r--r--app/assets/javascripts/notes/components/diff_with_note.vue3
-rw-r--r--app/assets/javascripts/notes/components/discussion_counter.vue13
-rw-r--r--app/assets/javascripts/notes/components/discussion_filter.vue6
-rw-r--r--app/assets/javascripts/notes/components/discussion_filter_note.vue8
-rw-r--r--app/assets/javascripts/notes/components/discussion_jump_to_next_button.vue7
-rw-r--r--app/assets/javascripts/notes/components/discussion_locked_widget.vue7
-rw-r--r--app/assets/javascripts/notes/components/discussion_resolve_button.vue9
-rw-r--r--app/assets/javascripts/notes/components/note_actions.vue80
-rw-r--r--app/assets/javascripts/notes/components/note_actions/reply_button.vue18
-rw-r--r--app/assets/javascripts/notes/components/note_body.vue1
-rw-r--r--app/assets/javascripts/notes/components/note_form.vue3
-rw-r--r--app/assets/javascripts/notes/components/note_header.vue13
-rw-r--r--app/assets/javascripts/notes/components/note_signed_out_widget.vue6
-rw-r--r--app/assets/javascripts/notes/components/noteable_discussion.vue5
-rw-r--r--app/assets/javascripts/notes/components/noteable_note.vue14
-rw-r--r--app/assets/javascripts/notes/components/toggle_replies_widget.vue8
-rw-r--r--app/assets/javascripts/notes/constants.js2
-rw-r--r--app/assets/javascripts/notes/stores/actions.js22
-rw-r--r--app/assets/javascripts/notes/stores/modules/index.js1
-rw-r--r--app/assets/javascripts/notes/stores/mutation_types.js1
-rw-r--r--app/assets/javascripts/notes/stores/mutations.js3
-rw-r--r--app/assets/javascripts/packages/details/components/additional_metadata.vue2
-rw-r--r--app/assets/javascripts/packages/details/components/app.vue63
-rw-r--r--app/assets/javascripts/packages/details/components/code_instruction.vue63
-rw-r--r--app/assets/javascripts/packages/details/components/composer_installation.vue18
-rw-r--r--app/assets/javascripts/packages/details/components/conan_installation.vue16
-rw-r--r--app/assets/javascripts/packages/details/components/dependency_row.vue2
-rw-r--r--app/assets/javascripts/packages/details/components/maven_installation.vue17
-rw-r--r--app/assets/javascripts/packages/details/components/npm_installation.vue17
-rw-r--r--app/assets/javascripts/packages/details/components/nuget_installation.vue15
-rw-r--r--app/assets/javascripts/packages/details/components/package_history.vue24
-rw-r--r--app/assets/javascripts/packages/details/components/package_title.vue112
-rw-r--r--app/assets/javascripts/packages/details/components/pypi_installation.vue12
-rw-r--r--app/assets/javascripts/packages/details/store/actions.js13
-rw-r--r--app/assets/javascripts/packages/details/store/getters.js4
-rw-r--r--app/assets/javascripts/packages/details/store/index.js6
-rw-r--r--app/assets/javascripts/packages/list/components/packages_list.vue6
-rw-r--r--app/assets/javascripts/packages/list/components/packages_list_app.vue20
-rw-r--r--app/assets/javascripts/packages/list/constants.js1
-rw-r--r--app/assets/javascripts/packages/list/stores/actions.js2
-rw-r--r--app/assets/javascripts/packages/shared/components/package_list_row.vue54
-rw-r--r--app/assets/javascripts/packages/shared/components/package_tags.vue6
-rw-r--r--app/assets/javascripts/packages/shared/components/packages_list_loader.vue58
-rw-r--r--app/assets/javascripts/packages/shared/components/publish_method.vue22
-rw-r--r--app/assets/javascripts/packages/shared/constants.js5
-rw-r--r--app/assets/javascripts/pages/admin/application_settings/metrics_and_profiling/index.js2
-rw-r--r--app/assets/javascripts/pages/admin/application_settings/payload_previewer.js4
-rw-r--r--app/assets/javascripts/pages/admin/clusters/new/index.js5
-rw-r--r--app/assets/javascripts/pages/admin/cohorts/index.js22
-rw-r--r--app/assets/javascripts/pages/admin/dev_ops_report/index.js27
-rw-r--r--app/assets/javascripts/pages/admin/projects/index/components/delete_project_modal.vue69
-rw-r--r--app/assets/javascripts/pages/admin/projects/index/index.js21
-rw-r--r--app/assets/javascripts/pages/admin/services/index/index.js6
-rw-r--r--app/assets/javascripts/pages/admin/users/components/delete_user_modal.vue46
-rw-r--r--app/assets/javascripts/pages/admin/users/components/user_operation_confirmation_modal.vue1
-rw-r--r--app/assets/javascripts/pages/constants.js2
-rw-r--r--app/assets/javascripts/pages/dashboard/projects/index/components/customize_homepage_banner.vue36
-rw-r--r--app/assets/javascripts/pages/dashboard/todos/index/todos.js4
-rw-r--r--app/assets/javascripts/pages/groups/clusters/new/index.js5
-rw-r--r--app/assets/javascripts/pages/groups/group_members/index.js6
-rw-r--r--app/assets/javascripts/pages/groups/issues/index.js2
-rw-r--r--app/assets/javascripts/pages/groups/shared/group_details.js2
-rw-r--r--app/assets/javascripts/pages/instance_statistics/dev_ops_score/index.js3
-rw-r--r--app/assets/javascripts/pages/milestones/shared/components/delete_milestone_modal.vue6
-rw-r--r--app/assets/javascripts/pages/projects/blob/show/index.js31
-rw-r--r--app/assets/javascripts/pages/projects/branches/index/index.js2
-rw-r--r--app/assets/javascripts/pages/projects/ci/lints/ci_lint_editor.js47
-rw-r--r--app/assets/javascripts/pages/projects/ci/lints/new/index.js10
-rw-r--r--app/assets/javascripts/pages/projects/clusters/new/index.js5
-rw-r--r--app/assets/javascripts/pages/projects/commit/show/index.js30
-rw-r--r--app/assets/javascripts/pages/projects/constants.js2
-rw-r--r--app/assets/javascripts/pages/projects/environments/show/index.js2
-rw-r--r--app/assets/javascripts/pages/projects/forks/new/components/fork_groups_list_item.vue4
-rw-r--r--app/assets/javascripts/pages/projects/graphs/charts/index.js269
-rw-r--r--app/assets/javascripts/pages/projects/integrations/jira/issues/index/index.js2
-rw-r--r--app/assets/javascripts/pages/projects/issues/index/index.js2
-rw-r--r--app/assets/javascripts/pages/projects/issues/service_desk/index.js2
-rw-r--r--app/assets/javascripts/pages/projects/issues/show.js22
-rw-r--r--app/assets/javascripts/pages/projects/issues/show/index.js2
-rw-r--r--app/assets/javascripts/pages/projects/merge_requests/creations/new/target_project_dropdown.js5
-rw-r--r--app/assets/javascripts/pages/projects/merge_requests/init_merge_request_show.js2
-rw-r--r--app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/pipeline_schedules_callout.vue7
-rw-r--r--app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/target_branch_dropdown.js3
-rw-r--r--app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/timezone_dropdown.js4
-rw-r--r--app/assets/javascripts/pages/projects/project.js86
-rw-r--r--app/assets/javascripts/pages/projects/shared/permissions/components/project_setting_row.vue7
-rw-r--r--app/assets/javascripts/pages/search/show/index.js6
-rw-r--r--app/assets/javascripts/pages/search/show/search.js6
-rw-r--r--app/assets/javascripts/pages/shared/wikis/components/delete_wiki_modal.vue11
-rw-r--r--app/assets/javascripts/pdf/page/index.vue10
-rw-r--r--app/assets/javascripts/performance_bar/components/detailed_metric.vue6
-rw-r--r--app/assets/javascripts/performance_bar/components/performance_bar_app.vue1
-rw-r--r--app/assets/javascripts/performance_bar/components/request_selector.vue1
-rw-r--r--app/assets/javascripts/performance_bar/components/request_warning.vue1
-rw-r--r--app/assets/javascripts/performance_bar/index.js30
-rw-r--r--app/assets/javascripts/performance_bar/performance_bar_log.js28
-rw-r--r--app/assets/javascripts/performance_bar/services/performance_bar_service.js18
-rw-r--r--app/assets/javascripts/performance_bar/stores/performance_bar_store.js5
-rw-r--r--app/assets/javascripts/pipeline_new/components/pipeline_new_form.vue91
-rw-r--r--app/assets/javascripts/pipeline_new/index.js2
-rw-r--r--app/assets/javascripts/pipelines/components/graph/action_component.vue7
-rw-r--r--app/assets/javascripts/pipelines/components/graph/graph_component.vue19
-rw-r--r--app/assets/javascripts/pipelines/components/graph/job_group_dropdown.vue4
-rw-r--r--app/assets/javascripts/pipelines/components/graph/job_item.vue18
-rw-r--r--app/assets/javascripts/pipelines/components/graph/job_name_component.vue4
-rw-r--r--app/assets/javascripts/pipelines/components/graph/linked_pipeline.vue65
-rw-r--r--app/assets/javascripts/pipelines/components/graph/linked_pipelines_column.vue4
-rw-r--r--app/assets/javascripts/pipelines/components/graph/stage_column_component.vue6
-rw-r--r--app/assets/javascripts/pipelines/components/pipeline_graph/gitlab_ci_yaml_visualization.vue76
-rw-r--r--app/assets/javascripts/pipelines/components/pipeline_graph/job_pill.vue24
-rw-r--r--app/assets/javascripts/pipelines/components/pipeline_graph/pipeline_graph.vue57
-rw-r--r--app/assets/javascripts/pipelines/components/pipeline_graph/stage_pill.vue35
-rw-r--r--app/assets/javascripts/pipelines/components/pipelines_list/nav_controls.vue22
-rw-r--r--app/assets/javascripts/pipelines/components/pipelines_list/pipeline_stop_modal.vue1
-rw-r--r--app/assets/javascripts/pipelines/components/pipelines_list/pipelines.vue8
-rw-r--r--app/assets/javascripts/pipelines/components/pipelines_list/pipelines_actions.vue32
-rw-r--r--app/assets/javascripts/pipelines/components/pipelines_list/pipelines_artifacts.vue7
-rw-r--r--app/assets/javascripts/pipelines/components/pipelines_list/pipelines_table_row.vue29
-rw-r--r--app/assets/javascripts/pipelines/components/pipelines_list/stage.vue7
-rw-r--r--app/assets/javascripts/pipelines/components/pipelines_list/time_ago.vue13
-rw-r--r--app/assets/javascripts/pipelines/components/test_reports/test_reports.vue7
-rw-r--r--app/assets/javascripts/pipelines/components/test_reports/test_suite_table.vue99
-rw-r--r--app/assets/javascripts/pipelines/components/test_reports/test_summary.vue19
-rw-r--r--app/assets/javascripts/pipelines/components/test_reports/test_summary_table.vue122
-rw-r--r--app/assets/javascripts/pipelines/pipeline_details_bundle.js22
-rw-r--r--app/assets/javascripts/pipelines/stores/test_reports/getters.js4
-rw-r--r--app/assets/javascripts/pipelines/stores/test_reports/utils.js26
-rw-r--r--app/assets/javascripts/pipelines/utils.js44
-rw-r--r--app/assets/javascripts/profile/account/components/delete_account_modal.vue95
-rw-r--r--app/assets/javascripts/profile/account/components/update_username.vue1
-rw-r--r--app/assets/javascripts/profile/account/index.js3
-rw-r--r--app/assets/javascripts/project_select.js11
-rw-r--r--app/assets/javascripts/projects/commits/components/author_select.vue38
-rw-r--r--app/assets/javascripts/projects/components/project_delete_button.vue4
-rw-r--r--app/assets/javascripts/projects/components/shared/delete_button.vue2
-rw-r--r--app/assets/javascripts/projects/experiment_new_project_creation/components/app.vue1
-rw-r--r--app/assets/javascripts/projects/experiment_new_project_creation/components/welcome.vue1
-rw-r--r--app/assets/javascripts/projects/project_new.js9
-rw-r--r--app/assets/javascripts/projects/settings/access_dropdown.js3
-rw-r--r--app/assets/javascripts/projects/settings_service_desk/components/service_desk_root.vue5
-rw-r--r--app/assets/javascripts/projects/settings_service_desk/components/service_desk_setting.vue18
-rw-r--r--app/assets/javascripts/prometheus_alerts/components/reset_key.vue1
-rw-r--r--app/assets/javascripts/protected_tags/protected_tag_access_dropdown.js3
-rw-r--r--app/assets/javascripts/protected_tags/protected_tag_create.js2
-rw-r--r--app/assets/javascripts/ref/components/ref_results_section.vue14
-rw-r--r--app/assets/javascripts/ref/components/ref_selector.vue44
-rw-r--r--app/assets/javascripts/ref_select_dropdown.js4
-rw-r--r--app/assets/javascripts/registry/explorer/components/delete_button.vue1
-rw-r--r--app/assets/javascripts/registry/explorer/components/details_page/details_header.vue11
-rw-r--r--app/assets/javascripts/registry/explorer/components/details_page/tags_list.vue1
-rw-r--r--app/assets/javascripts/registry/explorer/components/details_page/tags_list_row.vue4
-rw-r--r--app/assets/javascripts/registry/explorer/components/list_page/cli_commands.vue102
-rw-r--r--app/assets/javascripts/registry/explorer/components/list_page/image_list.vue1
-rw-r--r--app/assets/javascripts/registry/explorer/components/list_page/image_list_row.vue2
-rw-r--r--app/assets/javascripts/registry/explorer/components/list_page/registry_header.vue66
-rw-r--r--app/assets/javascripts/registry/explorer/components/registry_breadcrumb.vue6
-rw-r--r--app/assets/javascripts/registry/explorer/stores/index.js1
-rw-r--r--app/assets/javascripts/registry/explorer/utils.js1
-rw-r--r--app/assets/javascripts/registry/settings/components/settings_form.vue13
-rw-r--r--app/assets/javascripts/related_issues/components/add_issuable_form.vue207
-rw-r--r--app/assets/javascripts/related_issues/components/issue_token.vue115
-rw-r--r--app/assets/javascripts/related_issues/components/related_issuable_input.vue231
-rw-r--r--app/assets/javascripts/related_issues/components/related_issues_block.vue215
-rw-r--r--app/assets/javascripts/related_issues/components/related_issues_list.vue146
-rw-r--r--app/assets/javascripts/related_issues/components/related_issues_root.vue247
-rw-r--r--app/assets/javascripts/related_issues/constants.js106
-rw-r--r--app/assets/javascripts/related_issues/index.js27
-rw-r--r--app/assets/javascripts/related_issues/services/related_issues_service.js34
-rw-r--r--app/assets/javascripts/related_issues/stores/related_issues_store.js50
-rw-r--r--app/assets/javascripts/related_merge_requests/components/related_merge_requests.vue7
-rw-r--r--app/assets/javascripts/releases/components/app_edit_new.vue3
-rw-r--r--app/assets/javascripts/releases/components/app_index.vue39
-rw-r--r--app/assets/javascripts/releases/components/app_show.vue2
-rw-r--r--app/assets/javascripts/releases/components/evidence_block.vue4
-rw-r--r--app/assets/javascripts/releases/components/release_block.vue1
-rw-r--r--app/assets/javascripts/releases/components/release_block_assets.vue8
-rw-r--r--app/assets/javascripts/releases/components/release_block_footer.vue9
-rw-r--r--app/assets/javascripts/releases/components/release_block_header.vue9
-rw-r--r--app/assets/javascripts/releases/components/release_block_metadata.vue9
-rw-r--r--app/assets/javascripts/releases/components/release_block_milestones.vue7
-rw-r--r--app/assets/javascripts/releases/components/releases_pagination.vue20
-rw-r--r--app/assets/javascripts/releases/components/releases_pagination_graphql.vue35
-rw-r--r--app/assets/javascripts/releases/components/releases_pagination_rest.vue24
-rw-r--r--app/assets/javascripts/releases/mount_edit.js3
-rw-r--r--app/assets/javascripts/releases/mount_index.js17
-rw-r--r--app/assets/javascripts/releases/mount_new.js3
-rw-r--r--app/assets/javascripts/releases/mount_show.js3
-rw-r--r--app/assets/javascripts/releases/queries/all_releases.query.graphql69
-rw-r--r--app/assets/javascripts/releases/stores/index.js3
-rw-r--r--app/assets/javascripts/releases/stores/modules/list/actions.js30
-rw-r--r--app/assets/javascripts/releases/stores/modules/list/index.js8
-rw-r--r--app/assets/javascripts/releases/stores/modules/list/state.js14
-rw-r--r--app/assets/javascripts/releases/util.js89
-rw-r--r--app/assets/javascripts/reports/accessibility_report/store/index.js15
-rw-r--r--app/assets/javascripts/reports/codequality_report/store/index.js15
-rw-r--r--app/assets/javascripts/reports/components/issue_status_icon.vue6
-rw-r--r--app/assets/javascripts/reports/store/getters.js1
-rw-r--r--app/assets/javascripts/reports/store/index.js15
-rw-r--r--app/assets/javascripts/repository/components/breadcrumbs.vue8
-rw-r--r--app/assets/javascripts/repository/components/last_commit.vue8
-rw-r--r--app/assets/javascripts/repository/components/preview/index.vue1
-rw-r--r--app/assets/javascripts/repository/components/table/index.vue26
-rw-r--r--app/assets/javascripts/repository/components/table/row.vue3
-rw-r--r--app/assets/javascripts/repository/components/tree_content.vue19
-rw-r--r--app/assets/javascripts/repository/components/web_ide_link.vue47
-rw-r--r--app/assets/javascripts/repository/graphql.js1
-rw-r--r--app/assets/javascripts/repository/index.js28
-rw-r--r--app/assets/javascripts/repository/log_tree.js13
-rw-r--r--app/assets/javascripts/repository/router.js3
-rw-r--r--app/assets/javascripts/repository/utils/commit.js1
-rw-r--r--app/assets/javascripts/repository/utils/icon.js1
-rw-r--r--app/assets/javascripts/repository/utils/readme.js1
-rw-r--r--app/assets/javascripts/search/state_filter/components/state_filter.vue94
-rw-r--r--app/assets/javascripts/search/state_filter/constants.js39
-rw-r--r--app/assets/javascripts/search/state_filter/index.js34
-rw-r--r--app/assets/javascripts/search_autocomplete.js12
-rw-r--r--app/assets/javascripts/self_monitor/components/self_monitor_form.vue1
-rw-r--r--app/assets/javascripts/serverless/components/functions.vue1
-rw-r--r--app/assets/javascripts/set_status_modal/event_hub.js3
-rw-r--r--app/assets/javascripts/set_status_modal/set_status_modal_trigger.vue27
-rw-r--r--app/assets/javascripts/set_status_modal/set_status_modal_wrapper.vue20
-rw-r--r--app/assets/javascripts/shared/milestones/form.js1
-rw-r--r--app/assets/javascripts/sidebar/components/assignees/assignee_avatar_link.vue2
-rw-r--r--app/assets/javascripts/sidebar/components/assignees/collapsed_assignee_list.vue5
-rw-r--r--app/assets/javascripts/sidebar/components/assignees/issuable_assignees.vue37
-rw-r--r--app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.vue2
-rw-r--r--app/assets/javascripts/sidebar/components/assignees/uncollapsed_assignee_list.vue6
-rw-r--r--app/assets/javascripts/sidebar/components/confidential/confidential_issue_sidebar.vue18
-rw-r--r--app/assets/javascripts/sidebar/components/labels/sidebar_labels.vue97
-rw-r--r--app/assets/javascripts/sidebar/components/lock/issuable_lock_form.vue8
-rw-r--r--app/assets/javascripts/sidebar/components/participants/participants.vue5
-rw-r--r--app/assets/javascripts/sidebar/components/severity/constants.js41
-rw-r--r--app/assets/javascripts/sidebar/components/severity/graphql/mutations/update_issuable_severity.mutation.graphql9
-rw-r--r--app/assets/javascripts/sidebar/components/severity/severity.vue42
-rw-r--r--app/assets/javascripts/sidebar/components/severity/sidebar_severity.vue187
-rw-r--r--app/assets/javascripts/sidebar/components/subscriptions/subscriptions.vue6
-rw-r--r--app/assets/javascripts/sidebar/components/time_tracking/collapsed_state.vue6
-rw-r--r--app/assets/javascripts/sidebar/components/time_tracking/help_state.vue1
-rw-r--r--app/assets/javascripts/sidebar/components/time_tracking/spent_only_pane.vue1
-rw-r--r--app/assets/javascripts/sidebar/components/time_tracking/time_tracker.vue2
-rw-r--r--app/assets/javascripts/sidebar/components/todo_toggle/todo.vue8
-rw-r--r--app/assets/javascripts/sidebar/event_hub.js4
-rw-r--r--app/assets/javascripts/sidebar/lib/sidebar_move_issue.js4
-rw-r--r--app/assets/javascripts/sidebar/mount_sidebar.js61
-rw-r--r--app/assets/javascripts/sidebar/queries/sidebarDetailsForHealthStatusFeatureFlag.query.graphql7
-rw-r--r--app/assets/javascripts/sidebar/services/sidebar_service.js7
-rw-r--r--app/assets/javascripts/snippet/snippet_edit.js1
-rw-r--r--app/assets/javascripts/snippet/snippet_show.js2
-rw-r--r--app/assets/javascripts/snippets/components/edit.vue36
-rw-r--r--app/assets/javascripts/snippets/components/embed_dropdown.vue78
-rw-r--r--app/assets/javascripts/snippets/components/show.vue12
-rw-r--r--app/assets/javascripts/snippets/components/snippet_blob_edit.vue7
-rw-r--r--app/assets/javascripts/snippets/components/snippet_description_view.vue1
-rw-r--r--app/assets/javascripts/snippets/components/snippet_header.vue7
-rw-r--r--app/assets/javascripts/snippets/components/snippet_visibility_edit.vue50
-rw-r--r--app/assets/javascripts/snippets/constants.js12
-rw-r--r--app/assets/javascripts/snippets/index.js18
-rw-r--r--app/assets/javascripts/snippets/mixins/snippets.js1
-rw-r--r--app/assets/javascripts/snippets/queries/snippet_visibility.query.graphql5
-rw-r--r--app/assets/javascripts/snippets/utils/blob.js15
-rw-r--r--app/assets/javascripts/static_site_editor/components/edit_area.vue43
-rw-r--r--app/assets/javascripts/static_site_editor/components/edit_drawer.vue32
-rw-r--r--app/assets/javascripts/static_site_editor/components/front_matter_controls.vue57
-rw-r--r--app/assets/javascripts/static_site_editor/components/publish_toolbar.vue16
-rw-r--r--app/assets/javascripts/static_site_editor/services/formatter.js44
-rw-r--r--app/assets/javascripts/static_site_editor/services/image_service.js1
-rw-r--r--app/assets/javascripts/static_site_editor/services/parse_source_file.js66
-rw-r--r--app/assets/javascripts/subscription_select.js3
-rw-r--r--app/assets/javascripts/templates/issuable_template_selector.js4
-rw-r--r--app/assets/javascripts/tooltips/components/tooltips.vue116
-rw-r--r--app/assets/javascripts/tooltips/index.js120
-rw-r--r--app/assets/javascripts/tracking.js12
-rw-r--r--app/assets/javascripts/ui_development_kit.js3
-rw-r--r--app/assets/javascripts/users_select/index.js197
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/approvals/approvals_summary_optional.vue7
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/deployment/constants.js6
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/deployment/deployment_action_button.vue23
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/deployment/deployment_actions.vue22
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/deployment/deployment_view_button.vue77
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/deployment/memory_usage.vue66
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/mr_collapsible_extension.vue7
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/mr_widget_alert_message.vue7
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.vue106
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/mr_widget_icon.vue6
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue51
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline_container.vue1
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/mr_widget_related_links.vue1
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/mr_widget_suggest_pipeline.vue4
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/review_app_link.vue18
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/source_branch_removal_status.vue10
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/commits_header.vue22
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_failed_to_merge.vue10
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.vue39
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_missing_branch.vue4
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_rebase.vue1
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/nothing_to_merge.vue1
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue13
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/squash_before_merge.vue6
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/work_in_progress.vue8
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/terraform/mr_widget_terraform_container.vue2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue34
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/queries/get_state.query.graphql22
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/services/mr_widget_service.js8
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/stores/artifacts_list/getters.js1
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/stores/get_state_key.js2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js63
-rw-r--r--app/assets/javascripts/vue_shared/components/actions_button.vue90
-rw-r--r--app/assets/javascripts/vue_shared/components/alert_details_table.vue70
-rw-r--r--app/assets/javascripts/vue_shared/components/awards_list.vue1
-rw-r--r--app/assets/javascripts/vue_shared/components/blob_viewers/rich_viewer.vue6
-rw-r--r--app/assets/javascripts/vue_shared/components/blob_viewers/simple_viewer.vue1
-rw-r--r--app/assets/javascripts/vue_shared/components/changed_file_icon.vue7
-rw-r--r--app/assets/javascripts/vue_shared/components/ci_badge_link.vue7
-rw-r--r--app/assets/javascripts/vue_shared/components/ci_icon.vue6
-rw-r--r--app/assets/javascripts/vue_shared/components/clone_dropdown.vue16
-rw-r--r--app/assets/javascripts/vue_shared/components/commit.vue15
-rw-r--r--app/assets/javascripts/vue_shared/components/content_viewer/viewers/download_viewer.vue7
-rw-r--r--app/assets/javascripts/vue_shared/components/content_viewer/viewers/markdown_viewer.vue3
-rw-r--r--app/assets/javascripts/vue_shared/components/date_time_picker/date_time_picker.vue35
-rw-r--r--app/assets/javascripts/vue_shared/components/dismissible_alert.vue1
-rw-r--r--app/assets/javascripts/vue_shared/components/dropdown/dropdown_search_input.vue7
-rw-r--r--app/assets/javascripts/vue_shared/components/droplab_dropdown_button.vue7
-rw-r--r--app/assets/javascripts/vue_shared/components/file_finder/index.vue40
-rw-r--r--app/assets/javascripts/vue_shared/components/file_finder/item.vue8
-rw-r--r--app/assets/javascripts/vue_shared/components/file_row.vue18
-rw-r--r--app/assets/javascripts/vue_shared/components/filtered_search_bar/constants.js19
-rw-r--r--app/assets/javascripts/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue43
-rw-r--r--app/assets/javascripts/vue_shared/components/filtered_search_bar/filtered_search_utils.js166
-rw-r--r--app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/author_token.vue18
-rw-r--r--app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/branch_token.vue115
-rw-r--r--app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/label_token.vue18
-rw-r--r--app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/milestone_token.vue13
-rw-r--r--app/assets/javascripts/vue_shared/components/filtered_search_dropdown.vue154
-rw-r--r--app/assets/javascripts/vue_shared/components/gl_mentions.vue16
-rw-r--r--app/assets/javascripts/vue_shared/components/header_ci_component.vue1
-rw-r--r--app/assets/javascripts/vue_shared/components/help_popover.vue6
-rw-r--r--app/assets/javascripts/vue_shared/components/icon.vue72
-rw-r--r--app/assets/javascripts/vue_shared/components/issue/issue_milestone.vue7
-rw-r--r--app/assets/javascripts/vue_shared/components/issue/related_issuable_item.vue36
-rw-r--r--app/assets/javascripts/vue_shared/components/lib/utils/diff_utils.js2
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/field.vue10
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/header.vue40
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/suggestion_diff_header.vue23
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/suggestion_diff_row.vue12
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/suggestions.vue6
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/toolbar_button.vue28
-rw-r--r--app/assets/javascripts/vue_shared/components/modal_copy_button.vue7
-rw-r--r--app/assets/javascripts/vue_shared/components/notes/noteable_warning.vue8
-rw-r--r--app/assets/javascripts/vue_shared/components/notes/skeleton_note.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/notes/system_note.vue48
-rw-r--r--app/assets/javascripts/vue_shared/components/project_selector/project_list_item.vue25
-rw-r--r--app/assets/javascripts/vue_shared/components/project_selector/project_selector.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/recaptcha_modal.vue1
-rw-r--r--app/assets/javascripts/vue_shared/components/registry/code_instruction.vue82
-rw-r--r--app/assets/javascripts/vue_shared/components/registry/details_row.vue (renamed from app/assets/javascripts/registry/shared/components/details_row.vue)0
-rw-r--r--app/assets/javascripts/vue_shared/components/registry/history_item.vue (renamed from app/assets/javascripts/packages/details/components/history_element.vue)7
-rw-r--r--app/assets/javascripts/vue_shared/components/registry/list_item.vue (renamed from app/assets/javascripts/registry/explorer/components/list_item.vue)51
-rw-r--r--app/assets/javascripts/vue_shared/components/registry/metadata_item.vue63
-rw-r--r--app/assets/javascripts/vue_shared/components/registry/title_area.vue66
-rw-r--r--app/assets/javascripts/vue_shared/components/resizable_chart/constants.js6
-rw-r--r--app/assets/javascripts/vue_shared/components/resizable_chart/skeleton_loader.vue64
-rw-r--r--app/assets/javascripts/vue_shared/components/rich_content_editor/services/build_custom_renderer.js15
-rw-r--r--app/assets/javascripts/vue_shared/components/rich_content_editor/services/build_html_to_markdown_renderer.js19
-rw-r--r--app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/render_attribute_definition.js7
-rw-r--r--app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/render_heading.js6
-rw-r--r--app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/render_identifier_paragraph.js31
-rw-r--r--app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/render_kramdown_list.js24
-rw-r--r--app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/render_kramdown_text.js9
-rw-r--r--app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/render_list_item.js6
-rw-r--r--app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/render_utils.js28
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/collapsed_calendar_icon.vue6
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select/base.vue23
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_create_label.vue55
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_header.vue16
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed.vue6
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/labels_select_root.vue12
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/store/actions.js3
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/store/mutation_types.js1
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/store/mutations.js4
-rw-r--r--app/assets/javascripts/vue_shared/components/timezone_dropdown.vue8
-rw-r--r--app/assets/javascripts/vue_shared/components/todo_button.vue28
-rw-r--r--app/assets/javascripts/vue_shared/components/toggle_button.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/tooltip_on_truncate.vue4
-rw-r--r--app/assets/javascripts/vue_shared/components/url_sync.vue4
-rw-r--r--app/assets/javascripts/vue_shared/components/user_popover/user_popover.vue12
-rw-r--r--app/assets/javascripts/vue_shared/components/web_ide_link.vue118
-rw-r--r--app/assets/javascripts/vue_shared/mixins/related_issuable_mixin.js4
-rw-r--r--app/assets/javascripts/vuex_shared/bindings.js1
-rw-r--r--app/assets/javascripts/vuex_shared/modules/members/index.js6
-rw-r--r--app/assets/javascripts/vuex_shared/modules/members/state.js5
-rw-r--r--app/assets/javascripts/whats_new/components/app.vue56
-rw-r--r--app/assets/javascripts/whats_new/components/trigger.vue19
-rw-r--r--app/assets/javascripts/whats_new/index.js46
915 files changed, 14444 insertions, 10382 deletions
diff --git a/app/assets/javascripts/admin/application_settings/setup_metrics_and_profiling.js b/app/assets/javascripts/admin/application_settings/setup_metrics_and_profiling.js
index b4803be4d52..f89533aeb1d 100644
--- a/app/assets/javascripts/admin/application_settings/setup_metrics_and_profiling.js
+++ b/app/assets/javascripts/admin/application_settings/setup_metrics_and_profiling.js
@@ -1,8 +1,7 @@
import PayloadPreviewer from '~/pages/admin/application_settings/payload_previewer';
export default () => {
- new PayloadPreviewer(
- document.querySelector('.js-usage-ping-payload-trigger'),
- document.querySelector('.js-usage-ping-payload'),
- ).init();
+ Array.from(document.querySelectorAll('.js-payload-preview-trigger')).forEach(trigger => {
+ new PayloadPreviewer(trigger).init();
+ });
};
diff --git a/app/assets/javascripts/admin/cohorts/components/usage_ping_disabled.vue b/app/assets/javascripts/admin/cohorts/components/usage_ping_disabled.vue
new file mode 100644
index 00000000000..2ea55d44420
--- /dev/null
+++ b/app/assets/javascripts/admin/cohorts/components/usage_ping_disabled.vue
@@ -0,0 +1,48 @@
+<script>
+import { GlEmptyState, GlSprintf, GlLink } from '@gitlab/ui';
+
+export default {
+ components: {
+ GlEmptyState,
+ GlSprintf,
+ GlLink,
+ },
+ inject: {
+ svgPath: {
+ type: String,
+ },
+ docsLink: {
+ type: String,
+ },
+ primaryButtonPath: {
+ type: String,
+ },
+ },
+};
+</script>
+<template>
+ <gl-empty-state
+ class="js-empty-state"
+ :title="__('Activate user activity analysis')"
+ :svg-path="svgPath"
+ :primary-button-text="__('Turn on usage ping')"
+ :primary-button-link="primaryButtonPath"
+ >
+ <template #description>
+ <gl-sprintf
+ :message="
+ __(
+ 'Turn on %{strongStart}usage ping%{strongEnd} to activate analysis of user activity, known as %{docLinkStart}Cohorts%{docLinkEnd}.',
+ )
+ "
+ >
+ <template #docLink="{content}">
+ <gl-link :href="docsLink" target="_blank">{{ content }}</gl-link>
+ </template>
+ <template #strong="{ content }"
+ ><strong>{{ content }}</strong></template
+ >
+ </gl-sprintf>
+ </template>
+ </gl-empty-state>
+</template>
diff --git a/app/assets/javascripts/admin/dev_ops_report/components/usage_ping_disabled.vue b/app/assets/javascripts/admin/dev_ops_report/components/usage_ping_disabled.vue
new file mode 100644
index 00000000000..5429ec403d3
--- /dev/null
+++ b/app/assets/javascripts/admin/dev_ops_report/components/usage_ping_disabled.vue
@@ -0,0 +1,53 @@
+<script>
+import { GlEmptyState, GlSprintf, GlLink, GlButton } from '@gitlab/ui';
+
+export default {
+ components: {
+ GlEmptyState,
+ GlSprintf,
+ GlLink,
+ GlButton,
+ },
+ inject: {
+ isAdmin: {
+ type: Boolean,
+ },
+ svgPath: {
+ type: String,
+ },
+ docsLink: {
+ type: String,
+ },
+ primaryButtonPath: {
+ type: String,
+ },
+ },
+};
+</script>
+<template>
+ <gl-empty-state class="js-empty-state" :title="__('Usage ping is off')" :svg-path="svgPath">
+ <template #description>
+ <gl-sprintf
+ v-if="!isAdmin"
+ :message="
+ __(
+ 'To view instance-level analytics, ask an admin to turn on %{docLinkStart}usage ping%{docLinkEnd}.',
+ )
+ "
+ >
+ <template #docLink="{content}">
+ <gl-link :href="docsLink" target="_blank">{{ content }}</gl-link>
+ </template>
+ </gl-sprintf>
+ <template v-else
+ ><p>
+ {{ __('Turn on usage ping to review instance-level analytics.') }}
+ </p>
+
+ <gl-button category="primary" variant="success" :href="primaryButtonPath">
+ {{ __('Turn on usage ping') }}</gl-button
+ >
+ </template>
+ </template>
+ </gl-empty-state>
+</template>
diff --git a/app/assets/javascripts/admin/statistics_panel/store/getters.js b/app/assets/javascripts/admin/statistics_panel/store/getters.js
index 2aa34b8f38e..1c1868b5bca 100644
--- a/app/assets/javascripts/admin/statistics_panel/store/getters.js
+++ b/app/assets/javascripts/admin/statistics_panel/store/getters.js
@@ -3,7 +3,6 @@
* and returns an array of the following form:
* [{ key: "forks", label: "Forks", value: 50 }]
*/
-// eslint-disable-next-line import/prefer-default-export
export const getStatistics = state => labels =>
Object.keys(labels).map(key => {
const result = {
diff --git a/app/assets/javascripts/ajax_loading_spinner.js b/app/assets/javascripts/ajax_loading_spinner.js
deleted file mode 100644
index 54e86f329e4..00000000000
--- a/app/assets/javascripts/ajax_loading_spinner.js
+++ /dev/null
@@ -1,34 +0,0 @@
-import $ from 'jquery';
-
-export default class AjaxLoadingSpinner {
- static init() {
- const $elements = $('.js-ajax-loading-spinner');
-
- $elements.on('ajax:beforeSend', AjaxLoadingSpinner.ajaxBeforeSend);
- $elements.on('ajax:complete', AjaxLoadingSpinner.ajaxComplete);
- }
-
- static ajaxBeforeSend(e) {
- e.target.setAttribute('disabled', '');
- const iconElement = e.target.querySelector('i');
- // get first fa- icon
- const originalIcon = iconElement.className.match(/(fa-)([^\s]+)/g)[0];
- iconElement.dataset.icon = originalIcon;
- AjaxLoadingSpinner.toggleLoadingIcon(iconElement);
- $(e.target).off('ajax:beforeSend', AjaxLoadingSpinner.ajaxBeforeSend);
- }
-
- static ajaxComplete(e) {
- e.target.removeAttribute('disabled');
- const iconElement = e.target.querySelector('i');
- AjaxLoadingSpinner.toggleLoadingIcon(iconElement);
- $(e.target).off('ajax:complete', AjaxLoadingSpinner.ajaxComplete);
- }
-
- static toggleLoadingIcon(iconElement) {
- const { classList } = iconElement;
- classList.toggle(iconElement.dataset.icon);
- classList.toggle('fa-spinner');
- classList.toggle('fa-spin');
- }
-}
diff --git a/app/assets/javascripts/alert_handler.js b/app/assets/javascripts/alert_handler.js
new file mode 100644
index 00000000000..8fffb61d1dd
--- /dev/null
+++ b/app/assets/javascripts/alert_handler.js
@@ -0,0 +1,13 @@
+// This allows us to dismiss alerts that we've migrated from bootstrap
+// Note: This ONLY works on alerts that are created on page load
+// You can follow this effort in the following epic
+// https://gitlab.com/groups/gitlab-org/-/epics/4070
+
+export default function initAlertHandler() {
+ const ALERT_SELECTOR = '.gl-alert';
+ const CLOSE_SELECTOR = '.gl-alert-dismiss';
+
+ const dismissAlert = ({ target }) => target.closest(ALERT_SELECTOR).remove();
+ const closeButtons = document.querySelectorAll(`${ALERT_SELECTOR} ${CLOSE_SELECTOR}`);
+ closeButtons.forEach(alert => alert.addEventListener('click', dismissAlert));
+}
diff --git a/app/assets/javascripts/alert_management/components/alert_details.vue b/app/assets/javascripts/alert_management/components/alert_details.vue
index 5d260fcc200..c6605452616 100644
--- a/app/assets/javascripts/alert_management/components/alert_details.vue
+++ b/app/assets/javascripts/alert_management/components/alert_details.vue
@@ -1,4 +1,5 @@
<script>
+/* eslint-disable vue/no-v-html */
import * as Sentry from '@sentry/browser';
import {
GlAlert,
@@ -9,7 +10,6 @@ import {
GlTabs,
GlTab,
GlButton,
- GlTable,
} from '@gitlab/ui';
import { s__ } from '~/locale';
import alertQuery from '../graphql/queries/details.query.graphql';
@@ -27,6 +27,7 @@ import { toggleContainerClasses } from '~/lib/utils/dom_utils';
import SystemNote from './system_notes/system_note.vue';
import AlertSidebar from './alert_sidebar.vue';
import AlertMetrics from './alert_metrics.vue';
+import AlertDetailsTable from '~/vue_shared/components/alert_details_table.vue';
const containerEl = document.querySelector('.page-with-contextual-sidebar');
@@ -42,18 +43,19 @@ export default {
tabsConfig: [
{
id: 'overview',
- title: s__('AlertManagement|Overview'),
- },
- {
- id: 'fullDetails',
title: s__('AlertManagement|Alert details'),
},
{
id: 'metrics',
title: s__('AlertManagement|Metrics'),
},
+ {
+ id: 'activity',
+ title: s__('AlertManagement|Activity feed'),
+ },
],
components: {
+ AlertDetailsTable,
GlBadge,
GlAlert,
GlIcon,
@@ -62,7 +64,6 @@ export default {
GlTab,
GlTabs,
GlButton,
- GlTable,
TimeAgoTooltip,
AlertSidebar,
SystemNote,
@@ -330,32 +331,17 @@ export default {
</div>
<div class="gl-pl-2" data-testid="runbook">{{ alert.runbook }}</div>
</div>
- <template>
- <div v-if="alert.notes.nodes" class="issuable-discussion py-5">
- <ul class="notes main-notes-list timeline">
- <system-note v-for="note in alert.notes.nodes" :key="note.id" :note="note" />
- </ul>
- </div>
- </template>
+ <alert-details-table :alert="alert" :loading="loading" />
</gl-tab>
<gl-tab :data-testid="$options.tabsConfig[1].id" :title="$options.tabsConfig[1].title">
- <gl-table
- class="alert-management-details-table"
- :items="[{ key: 'Value', ...alert }]"
- :show-empty="true"
- :busy="loading"
- stacked
- >
- <template #empty>
- {{ s__('AlertManagement|No alert data to display.') }}
- </template>
- <template #table-busy>
- <gl-loading-icon size="lg" color="dark" class="mt-3" />
- </template>
- </gl-table>
+ <alert-metrics :dashboard-url="alert.metricsDashboardUrl" />
</gl-tab>
<gl-tab :data-testid="$options.tabsConfig[2].id" :title="$options.tabsConfig[2].title">
- <alert-metrics :dashboard-url="alert.metricsDashboardUrl" />
+ <div v-if="alert.notes.nodes.length > 0" class="issuable-discussion">
+ <ul class="notes main-notes-list timeline">
+ <system-note v-for="note in alert.notes.nodes" :key="note.id" :note="note" />
+ </ul>
+ </div>
</gl-tab>
</gl-tabs>
<alert-sidebar
diff --git a/app/assets/javascripts/alert_management/components/alert_management_table.vue b/app/assets/javascripts/alert_management/components/alert_management_table.vue
index 92fd85c6217..0fd00fe90eb 100644
--- a/app/assets/javascripts/alert_management/components/alert_management_table.vue
+++ b/app/assets/javascripts/alert_management/components/alert_management_table.vue
@@ -1,8 +1,12 @@
<script>
+/* eslint-disable vue/no-v-html */
import {
GlLoadingIcon,
GlTable,
GlAlert,
+ GlAvatarsInline,
+ GlAvatarLink,
+ GlAvatar,
GlIcon,
GlLink,
GlTabs,
@@ -11,6 +15,7 @@ import {
GlPagination,
GlSearchBoxByType,
GlSprintf,
+ GlTooltipDirective,
} from '@gitlab/ui';
import { debounce, trim } from 'lodash';
import { __, s__ } from '~/locale';
@@ -35,6 +40,7 @@ const tdClass =
const thClass = 'gl-hover-bg-blue-50';
const bodyTrClass =
'gl-border-1 gl-border-t-solid gl-border-gray-100 gl-hover-bg-blue-50 gl-hover-cursor-pointer gl-hover-border-b-solid gl-hover-border-blue-200';
+const TH_TEST_ID = { 'data-testid': 'alert-management-severity-sort' };
const initialPaginationState = {
currentPage: 1,
@@ -55,12 +61,14 @@ export default {
"AlertManagement|There was an error displaying the alerts. Confirm your endpoint's configuration details to ensure alerts appear.",
),
searchPlaceholder: __('Search or filter results...'),
+ unassigned: __('Unassigned'),
},
fields: [
{
key: 'severity',
label: s__('AlertManagement|Severity'),
thClass: `${thClass} gl-w-eighth`,
+ thAttr: TH_TEST_ID,
tdClass: `${tdClass} rounded-top text-capitalize sortable-cell`,
sortable: true,
},
@@ -72,7 +80,7 @@ export default {
sortable: true,
},
{
- key: 'title',
+ key: 'alertLabel',
label: s__('AlertManagement|Alert'),
thClass: `gl-pointer-events-none`,
tdClass,
@@ -110,6 +118,9 @@ export default {
GlLoadingIcon,
GlTable,
GlAlert,
+ GlAvatarsInline,
+ GlAvatarLink,
+ GlAvatar,
TimeAgo,
GlIcon,
GlLink,
@@ -121,6 +132,9 @@ export default {
GlSprintf,
AlertStatus,
},
+ directives: {
+ GlTooltip: GlTooltipDirective,
+ },
props: {
projectPath: {
type: String,
@@ -264,11 +278,8 @@ export default {
const { category, action, label } = trackAlertStatusUpdateOptions;
Tracking.event(category, action, { label, property: status });
},
- getAssignees(assignees) {
- // TODO: Update to show list of assignee(s) after https://gitlab.com/gitlab-org/gitlab/-/issues/218405
- return assignees.nodes?.length > 0
- ? assignees.nodes[0]?.username
- : s__('AlertManagement|Unassigned');
+ hasAssignees(assignees) {
+ return Boolean(assignees.nodes?.length);
},
getIssueLink(item) {
return joinPaths('/', this.projectPath, '-', 'issues', item.issueIid);
@@ -397,8 +408,14 @@ export default {
{{ item.eventCount }}
</template>
- <template #cell(title)="{ item }">
- <div class="gl-max-w-full text-truncate" :title="item.title">{{ item.title }}</div>
+ <template #cell(alertLabel)="{ item }">
+ <div
+ class="gl-max-w-full text-truncate"
+ :title="`${item.iid} - ${item.title}`"
+ data-testid="idField"
+ >
+ #{{ item.iid }} {{ item.title }}
+ </div>
</template>
<template #cell(issue)="{ item }">
@@ -409,8 +426,32 @@ export default {
</template>
<template #cell(assignees)="{ item }">
- <div class="gl-max-w-full text-truncate" data-testid="assigneesField">
- {{ getAssignees(item.assignees) }}
+ <div data-testid="assigneesField">
+ <template v-if="hasAssignees(item.assignees)">
+ <gl-avatars-inline
+ :avatars="item.assignees.nodes"
+ :collapsed="true"
+ :max-visible="4"
+ :avatar-size="24"
+ badge-tooltip-prop="name"
+ :badge-tooltip-max-chars="100"
+ >
+ <template #avatar="{ avatar }">
+ <gl-avatar-link
+ :key="avatar.username"
+ v-gl-tooltip
+ target="_blank"
+ :href="avatar.webUrl"
+ :title="avatar.name"
+ >
+ <gl-avatar :src="avatar.avatarUrl" :label="avatar.name" :size="24" />
+ </gl-avatar-link>
+ </template>
+ </gl-avatars-inline>
+ </template>
+ <template v-else>
+ {{ $options.i18n.unassigned }}
+ </template>
</div>
</template>
diff --git a/app/assets/javascripts/alert_management/components/alert_status.vue b/app/assets/javascripts/alert_management/components/alert_status.vue
index 8531ca1374e..ff71b348cc9 100644
--- a/app/assets/javascripts/alert_management/components/alert_status.vue
+++ b/app/assets/javascripts/alert_management/components/alert_status.vue
@@ -101,12 +101,12 @@ export default {
@keydown.esc.native="$emit('hide-dropdown')"
@hide="$emit('hide-dropdown')"
>
- <div v-if="isSidebar" class="dropdown-title text-center">
- <span class="alert-title">{{ s__('AlertManagement|Assign status') }}</span>
+ <div v-if="isSidebar" class="dropdown-title gl-display-flex">
+ <span class="alert-title gl-ml-auto">{{ s__('AlertManagement|Assign status') }}</span>
<gl-button
:aria-label="__('Close')"
variant="link"
- class="dropdown-title-button dropdown-menu-close"
+ class="dropdown-title-button dropdown-menu-close gl-ml-auto gl-text-black-normal!"
icon="close"
@click="$emit('hide-dropdown')"
/>
diff --git a/app/assets/javascripts/alert_management/components/sidebar/sidebar_assignees.vue b/app/assets/javascripts/alert_management/components/sidebar/sidebar_assignees.vue
index 4af5c83b30c..0f354e85e96 100644
--- a/app/assets/javascripts/alert_management/components/sidebar/sidebar_assignees.vue
+++ b/app/assets/javascripts/alert_management/components/sidebar/sidebar_assignees.vue
@@ -12,7 +12,7 @@ import {
} from '@gitlab/ui';
import { debounce } from 'lodash';
import axios from '~/lib/utils/axios_utils';
-import { s__, __ } from '~/locale';
+import { s__ } from '~/locale';
import alertSetAssignees from '../../graphql/mutations/alert_set_assignees.mutation.graphql';
import SidebarAssignee from './sidebar_assignee.vue';
@@ -82,8 +82,11 @@ export default {
userName() {
return this.alert?.assignees?.nodes[0]?.username;
},
- assignedUser() {
- return this.userName || __('None');
+ userFullName() {
+ return this.alert?.assignees?.nodes[0]?.name;
+ },
+ userImg() {
+ return this.alert?.assignees?.nodes[0]?.avatarUrl;
},
sortedUsers() {
return this.users
@@ -184,15 +187,15 @@ export default {
</script>
<template>
- <div class="block alert-status">
- <div ref="status" class="sidebar-collapsed-icon" @click="$emit('toggle-sidebar')">
+ <div class="block alert-assignees ">
+ <div ref="assignees" class="sidebar-collapsed-icon" @click="$emit('toggle-sidebar')">
<gl-icon name="user" :size="14" />
<gl-loading-icon v-if="isUpdating" />
</div>
- <gl-tooltip :target="() => $refs.status" boundary="viewport" placement="left">
+ <gl-tooltip :target="() => $refs.assignees" boundary="viewport" placement="left">
<gl-sprintf :message="$options.i18n.ASSIGNEES_BLOCK">
<template #assignees>
- {{ assignedUser }}
+ {{ userName }}
</template>
</gl-sprintf>
</gl-tooltip>
@@ -215,19 +218,19 @@ export default {
<div class="dropdown dropdown-menu-selectable" :class="dropdownClass">
<gl-deprecated-dropdown
ref="dropdown"
- :text="assignedUser"
+ :text="userName"
class="w-100"
toggle-class="dropdown-menu-toggle"
variant="outline-default"
@keydown.esc.native="hideDropdown"
@hide="hideDropdown"
>
- <div class="dropdown-title">
- <span class="alert-title">{{ __('Assign To') }}</span>
+ <div class="dropdown-title gl-display-flex">
+ <span class="alert-title gl-ml-auto">{{ __('Assign To') }}</span>
<gl-button
:aria-label="__('Close')"
variant="link"
- class="dropdown-title-button dropdown-menu-close"
+ class="dropdown-title-button dropdown-menu-close gl-ml-auto gl-text-black-normal!"
icon="close"
@click="hideDropdown"
/>
@@ -272,14 +275,28 @@ export default {
</div>
<gl-loading-icon v-if="isUpdating" :inline="true" />
- <p v-else-if="!isDropdownShowing" class="value gl-m-0" :class="{ 'no-value': !userName }">
- <span v-if="userName" class="gl-text-gray-500" data-testid="assigned-users">{{
- assignedUser
- }}</span>
- <span v-else class="gl-display-flex gl-align-items-center">
+ <div v-else-if="!isDropdownShowing" class="value gl-m-0" :class="{ 'no-value': !userName }">
+ <div v-if="userName" class="gl-display-inline-flex gl-mt-2" data-testid="assigned-users">
+ <span class="gl-relative mr-2">
+ <img
+ :alt="userName"
+ :src="userImg"
+ :width="32"
+ class="avatar avatar-inline gl-m-0 s32"
+ data-qa-selector="avatar_image"
+ />
+ </span>
+ <span class="gl-display-flex gl-flex-direction-column gl-overflow-hidden">
+ <strong class="dropdown-menu-user-full-name">
+ {{ userFullName }}
+ </strong>
+ <span class="dropdown-menu-user-username">{{ userName }}</span>
+ </span>
+ </div>
+ <span v-else class="gl-display-flex gl-align-items-center gl-line-height-normal">
{{ __('None') }} -
<gl-button
- class="gl-pl-2"
+ class="gl-ml-2"
href="#"
variant="link"
data-testid="unassigned-users"
@@ -288,7 +305,7 @@ export default {
{{ __('assign yourself') }}
</gl-button>
</span>
- </p>
+ </div>
</div>
</div>
</template>
diff --git a/app/assets/javascripts/alert_management/components/sidebar/sidebar_todo.vue b/app/assets/javascripts/alert_management/components/sidebar/sidebar_todo.vue
index 5bd69a1f0ec..84d54466a10 100644
--- a/app/assets/javascripts/alert_management/components/sidebar/sidebar_todo.vue
+++ b/app/assets/javascripts/alert_management/components/sidebar/sidebar_todo.vue
@@ -1,8 +1,9 @@
<script>
+import produce from 'immer';
import { s__ } from '~/locale';
import Todo from '~/sidebar/components/todo_toggle/todo.vue';
-import createAlertTodo from '../../graphql/mutations/alert_todo_create.mutation.graphql';
-import todoMarkDone from '../../graphql/mutations/alert_todo_mark_done.mutation.graphql';
+import createAlertTodoMutation from '../../graphql/mutations/alert_todo_create.mutation.graphql';
+import todoMarkDoneMutation from '~/graphql_shared/mutations/todo_mark_done.mutation.graphql';
import alertQuery from '../../graphql/queries/details.query.graphql';
export default {
@@ -52,7 +53,7 @@ export default {
},
methods: {
updateToDoCount(add) {
- const oldCount = parseInt(document.querySelector('.todos-count').innerText, 10);
+ const oldCount = parseInt(document.querySelector('.js-todos-count').innerText, 10);
const count = add ? oldCount + 1 : oldCount - 1;
const headerTodoEvent = new CustomEvent('todo:toggle', {
detail: {
@@ -66,7 +67,7 @@ export default {
this.isUpdating = true;
return this.$apollo
.mutate({
- mutation: createAlertTodo,
+ mutation: createAlertTodoMutation,
variables: {
iid: this.alert.iid,
projectPath: this.projectPath,
@@ -89,7 +90,7 @@ export default {
this.isUpdating = true;
return this.$apollo
.mutate({
- mutation: todoMarkDone,
+ mutation: todoMarkDoneMutation,
variables: {
id: this.firstToDoId,
},
@@ -109,12 +110,15 @@ export default {
});
},
updateCache(store) {
- const data = store.readQuery({
+ const sourceData = store.readQuery({
query: alertQuery,
variables: this.getAlertQueryVariables,
});
- data.project.alertManagementAlerts.nodes[0].todos.nodes.shift();
+ const data = produce(sourceData, draftData => {
+ // eslint-disable-next-line no-param-reassign
+ draftData.project.alertManagementAlerts.nodes[0].todos.nodes = [];
+ });
store.writeQuery({
query: alertQuery,
diff --git a/app/assets/javascripts/alert_management/components/system_notes/system_note.vue b/app/assets/javascripts/alert_management/components/system_notes/system_note.vue
index 39717ab609f..0b206ce42f4 100644
--- a/app/assets/javascripts/alert_management/components/system_notes/system_note.vue
+++ b/app/assets/javascripts/alert_management/components/system_notes/system_note.vue
@@ -1,4 +1,5 @@
<script>
+/* eslint-disable vue/no-v-html */
import NoteHeader from '~/notes/components/note_header.vue';
import { spriteIcon } from '~/lib/utils/common_utils';
@@ -31,7 +32,7 @@ export default {
</script>
<template>
- <li :id="noteAnchorId" class="timeline-entry note system-note note-wrapper">
+ <li :id="noteAnchorId" class="timeline-entry note system-note note-wrapper gl-px-0!">
<div class="timeline-entry-inner">
<div class="timeline-icon" v-html="iconHtml"></div>
<div class="timeline-content">
diff --git a/app/assets/javascripts/alert_management/constants.js b/app/assets/javascripts/alert_management/constants.js
index b9670466c0f..73cb5ecdf98 100644
--- a/app/assets/javascripts/alert_management/constants.js
+++ b/app/assets/javascripts/alert_management/constants.js
@@ -64,4 +64,4 @@ export const trackAlertStatusUpdateOptions = {
label: 'Status',
};
-export const DEFAULT_PAGE_SIZE = 10;
+export const DEFAULT_PAGE_SIZE = 20;
diff --git a/app/assets/javascripts/alert_management/details.js b/app/assets/javascripts/alert_management/details.js
index dccf990f0b4..c2020dfcbe3 100644
--- a/app/assets/javascripts/alert_management/details.js
+++ b/app/assets/javascripts/alert_management/details.js
@@ -1,5 +1,6 @@
import Vue from 'vue';
import VueApollo from 'vue-apollo';
+import produce from 'immer';
import { defaultDataIdFromObject } from 'apollo-cache-inmemory';
import createDefaultClient from '~/lib/graphql';
import createRouter from './router';
@@ -16,8 +17,11 @@ export default selector => {
const resolvers = {
Mutation: {
toggleSidebarStatus: (_, __, { cache }) => {
- const data = cache.readQuery({ query: sidebarStatusQuery });
- data.sidebarStatus = !data.sidebarStatus;
+ const sourceData = cache.readQuery({ query: sidebarStatusQuery });
+ const data = produce(sourceData, draftData => {
+ // eslint-disable-next-line no-param-reassign
+ draftData.sidebarStatus = !draftData.sidebarStatus;
+ });
cache.writeQuery({ query: sidebarStatusQuery, data });
},
},
@@ -34,6 +38,7 @@ export default selector => {
return defaultDataIdFromObject(object);
},
},
+ assumeImmutableResults: true,
}),
});
diff --git a/app/assets/javascripts/alert_management/graphql/fragments/list_item.fragment.graphql b/app/assets/javascripts/alert_management/graphql/fragments/list_item.fragment.graphql
index c37f29c74fc..62119177887 100644
--- a/app/assets/javascripts/alert_management/graphql/fragments/list_item.fragment.graphql
+++ b/app/assets/javascripts/alert_management/graphql/fragments/list_item.fragment.graphql
@@ -8,7 +8,10 @@ fragment AlertListItem on AlertManagementAlert {
issueIid
assignees {
nodes {
+ name
username
+ avatarUrl
+ webUrl
}
}
}
diff --git a/app/assets/javascripts/alert_management/graphql/mutations/alert_set_assignees.mutation.graphql b/app/assets/javascripts/alert_management/graphql/mutations/alert_set_assignees.mutation.graphql
index 40b4b6ae854..5008bfa5e1b 100644
--- a/app/assets/javascripts/alert_management/graphql/mutations/alert_set_assignees.mutation.graphql
+++ b/app/assets/javascripts/alert_management/graphql/mutations/alert_set_assignees.mutation.graphql
@@ -10,6 +10,9 @@ mutation alertSetAssignees($projectPath: ID!, $assigneeUsernames: [String!]!, $i
assignees {
nodes {
username
+ name
+ avatarUrl
+ webUrl
}
}
notes {
diff --git a/app/assets/javascripts/api.js b/app/assets/javascripts/api.js
index 1d8fb1fc5a6..dbc7ff67d9d 100644
--- a/app/assets/javascripts/api.js
+++ b/app/assets/javascripts/api.js
@@ -20,6 +20,7 @@ const Api = {
projectPath: '/api/:version/projects/:id',
forkedProjectsPath: '/api/:version/projects/:id/forks',
projectLabelsPath: '/:namespace_path/:project_path/-/labels',
+ projectFileSchemaPath: '/:namespace_path/:project_path/-/schema/:ref/:filename',
projectUsersPath: '/api/:version/projects/:id/users',
projectMergeRequestsPath: '/api/:version/projects/:id/merge_requests',
projectMergeRequestPath: '/api/:version/projects/:id/merge_requests/:mrid',
@@ -42,7 +43,6 @@ const Api = {
userPostStatusPath: '/api/:version/user/status',
commitPath: '/api/:version/projects/:id/repository/commits/:sha',
commitsPath: '/api/:version/projects/:id/repository/commits',
-
applySuggestionPath: '/api/:version/suggestions/:id/apply',
applySuggestionBatchPath: '/api/:version/suggestions/batch_apply',
commitPipelinesPath: '/:project_id/commit/:sha/pipelines',
@@ -309,10 +309,12 @@ const Api = {
});
},
- projectMilestones(id) {
+ projectMilestones(id, params = {}) {
const url = Api.buildUrl(Api.projectMilestonesPath).replace(':id', encodeURIComponent(id));
- return axios.get(url);
+ return axios.get(url, {
+ params,
+ });
},
mergeRequests(params = {}) {
diff --git a/app/assets/javascripts/authentication/mount_2fa.js b/app/assets/javascripts/authentication/mount_2fa.js
index 9917151ac81..dd5a42fa5fc 100644
--- a/app/assets/javascripts/authentication/mount_2fa.js
+++ b/app/assets/javascripts/authentication/mount_2fa.js
@@ -1,14 +1,23 @@
import $ from 'jquery';
import initU2F from './u2f';
+import initWebauthn from './webauthn';
import U2FRegister from './u2f/register';
+import WebAuthnRegister from './webauthn/register';
export const mount2faAuthentication = () => {
- // Soon this will conditionally mount a webauthn app (see https://gitlab.com/gitlab-org/gitlab/-/merge_requests/26692)
- initU2F();
+ if (gon.webauthn) {
+ initWebauthn();
+ } else {
+ initU2F();
+ }
};
export const mount2faRegistration = () => {
- // Soon this will conditionally mount a webauthn app (see https://gitlab.com/gitlab-org/gitlab/-/merge_requests/26692)
- const u2fRegister = new U2FRegister($('#js-register-u2f'), gon.u2f);
- u2fRegister.start();
+ if (gon.webauthn) {
+ const webauthnRegister = new WebAuthnRegister($('#js-register-token-2fa'), gon.webauthn);
+ webauthnRegister.start();
+ } else {
+ const u2fRegister = new U2FRegister($('#js-register-token-2fa'), gon.u2f);
+ u2fRegister.start();
+ }
};
diff --git a/app/assets/javascripts/authentication/u2f/authenticate.js b/app/assets/javascripts/authentication/u2f/authenticate.js
index 201cd5c2e61..f9b5ca3e5b4 100644
--- a/app/assets/javascripts/authentication/u2f/authenticate.js
+++ b/app/assets/javascripts/authentication/u2f/authenticate.js
@@ -40,7 +40,6 @@ export default class U2FAuthenticate {
this.signRequests = u2fParams.sign_requests.map(request => omit(request, 'challenge'));
this.templates = {
- setup: '#js-authenticate-token-2fa-setup',
inProgress: '#js-authenticate-token-2fa-in-progress',
error: '#js-authenticate-token-2fa-error',
authenticated: '#js-authenticate-token-2fa-authenticated',
@@ -86,7 +85,7 @@ export default class U2FAuthenticate {
renderError(error) {
this.renderTemplate('error', {
error_message: error.message(),
- error_code: error.errorCode,
+ error_name: error.errorCode,
});
return this.container.find('#js-token-2fa-try-again').on('click', this.renderInProgress);
}
diff --git a/app/assets/javascripts/authentication/u2f/register.js b/app/assets/javascripts/authentication/u2f/register.js
index 52c0ce1fc04..9773a9185f8 100644
--- a/app/assets/javascripts/authentication/u2f/register.js
+++ b/app/assets/javascripts/authentication/u2f/register.js
@@ -1,5 +1,6 @@
import $ from 'jquery';
import { template as lodashTemplate } from 'lodash';
+import { __ } from '~/locale';
import importU2FLibrary from './util';
import U2FError from './error';
@@ -24,11 +25,10 @@ export default class U2FRegister {
this.signRequests = u2fParams.sign_requests;
this.templates = {
- notSupported: '#js-register-u2f-not-supported',
- setup: '#js-register-u2f-setup',
- inProgress: '#js-register-u2f-in-progress',
- error: '#js-register-u2f-error',
- registered: '#js-register-u2f-registered',
+ message: '#js-register-2fa-message',
+ setup: '#js-register-token-2fa-setup',
+ error: '#js-register-token-2fa-error',
+ registered: '#js-register-token-2fa-registered',
};
}
@@ -65,18 +65,22 @@ export default class U2FRegister {
renderSetup() {
this.renderTemplate('setup');
- return this.container.find('#js-setup-u2f-device').on('click', this.renderInProgress);
+ return this.container.find('#js-setup-token-2fa-device').on('click', this.renderInProgress);
}
renderInProgress() {
- this.renderTemplate('inProgress');
+ this.renderTemplate('message', {
+ message: __(
+ 'Trying to communicate with your device. Plug it in (if needed) and press the button on the device now.',
+ ),
+ });
return this.register();
}
renderError(error) {
this.renderTemplate('error', {
error_message: error.message(),
- error_code: error.errorCode,
+ error_name: error.errorCode,
});
return this.container.find('#js-token-2fa-try-again').on('click', this.renderSetup);
}
@@ -89,6 +93,10 @@ export default class U2FRegister {
}
renderNotSupported() {
- return this.renderTemplate('notSupported');
+ return this.renderTemplate('message', {
+ message: __(
+ "Your browser doesn't support U2F. Please use Google Chrome desktop (version 41 or newer).",
+ ),
+ });
}
}
diff --git a/app/assets/javascripts/authentication/webauthn/authenticate.js b/app/assets/javascripts/authentication/webauthn/authenticate.js
new file mode 100644
index 00000000000..42c4c2b63bd
--- /dev/null
+++ b/app/assets/javascripts/authentication/webauthn/authenticate.js
@@ -0,0 +1,69 @@
+import WebAuthnError from './error';
+import WebAuthnFlow from './flow';
+import { supported, convertGetParams, convertGetResponse } from './util';
+
+// Authenticate WebAuthn devices for users to authenticate with.
+//
+// State Flow #1: setup -> in_progress -> authenticated -> POST to server
+// State Flow #2: setup -> in_progress -> error -> setup
+export default class WebAuthnAuthenticate {
+ constructor(container, form, webauthnParams, fallbackButton, fallbackUI) {
+ this.container = container;
+ this.webauthnParams = convertGetParams(JSON.parse(webauthnParams.options));
+ this.renderInProgress = this.renderInProgress.bind(this);
+
+ this.form = form;
+ this.fallbackButton = fallbackButton;
+ this.fallbackUI = fallbackUI;
+ if (this.fallbackButton) {
+ this.fallbackButton.addEventListener('click', this.switchToFallbackUI.bind(this));
+ }
+
+ this.flow = new WebAuthnFlow(container, {
+ inProgress: '#js-authenticate-token-2fa-in-progress',
+ error: '#js-authenticate-token-2fa-error',
+ authenticated: '#js-authenticate-token-2fa-authenticated',
+ });
+
+ this.container.on('click', '#js-token-2fa-try-again', this.renderInProgress);
+ }
+
+ start() {
+ if (!supported()) {
+ this.switchToFallbackUI();
+ } else {
+ this.renderInProgress();
+ }
+ }
+
+ authenticate() {
+ navigator.credentials
+ .get({ publicKey: this.webauthnParams })
+ .then(resp => {
+ const convertedResponse = convertGetResponse(resp);
+ this.renderAuthenticated(JSON.stringify(convertedResponse));
+ })
+ .catch(err => {
+ this.flow.renderError(new WebAuthnError(err, 'authenticate'));
+ });
+ }
+
+ renderInProgress() {
+ this.flow.renderTemplate('inProgress');
+ this.authenticate();
+ }
+
+ renderAuthenticated(deviceResponse) {
+ this.flow.renderTemplate('authenticated');
+ const container = this.container[0];
+ container.querySelector('#js-device-response').value = deviceResponse;
+ container.querySelector(this.form).submit();
+ this.fallbackButton.classList.add('hidden');
+ }
+
+ switchToFallbackUI() {
+ this.fallbackButton.classList.add('hidden');
+ this.container[0].classList.add('hidden');
+ this.fallbackUI.classList.remove('hidden');
+ }
+}
diff --git a/app/assets/javascripts/authentication/webauthn/error.js b/app/assets/javascripts/authentication/webauthn/error.js
new file mode 100644
index 00000000000..a1a3f861c25
--- /dev/null
+++ b/app/assets/javascripts/authentication/webauthn/error.js
@@ -0,0 +1,28 @@
+import { __ } from '~/locale';
+import { isHTTPS, FLOW_AUTHENTICATE, FLOW_REGISTER } from './util';
+
+export default class WebAuthnError {
+ constructor(error, flowType) {
+ this.error = error;
+ this.errorName = error.name || 'UnknownError';
+ this.message = this.message.bind(this);
+ this.httpsDisabled = !isHTTPS();
+ this.flowType = flowType;
+ }
+
+ message() {
+ if (this.errorName === 'NotSupportedError') {
+ return __('Your device is not compatible with GitLab. Please try another device');
+ } else if (this.errorName === 'InvalidStateError' && this.flowType === FLOW_AUTHENTICATE) {
+ return __('This device has not been registered with us.');
+ } else if (this.errorName === 'InvalidStateError' && this.flowType === FLOW_REGISTER) {
+ return __('This device has already been registered with us.');
+ } else if (this.errorName === 'SecurityError' && this.httpsDisabled) {
+ return __(
+ 'WebAuthn only works with HTTPS-enabled websites. Contact your administrator for more details.',
+ );
+ }
+
+ return __('There was a problem communicating with your device.');
+ }
+}
diff --git a/app/assets/javascripts/authentication/webauthn/flow.js b/app/assets/javascripts/authentication/webauthn/flow.js
new file mode 100644
index 00000000000..10a1debc876
--- /dev/null
+++ b/app/assets/javascripts/authentication/webauthn/flow.js
@@ -0,0 +1,24 @@
+import { template } from 'lodash';
+
+/**
+ * Generic abstraction for WebAuthnFlows, especially for register / authenticate
+ */
+export default class WebAuthnFlow {
+ constructor(container, templates) {
+ this.container = container;
+ this.templates = templates;
+ }
+
+ renderTemplate(name, params) {
+ const templateString = document.querySelector(this.templates[name]).innerHTML;
+ const compiledTemplate = template(templateString);
+ this.container.html(compiledTemplate(params));
+ }
+
+ renderError(error) {
+ this.renderTemplate('error', {
+ error_message: error.message(),
+ error_name: error.errorName,
+ });
+ }
+}
diff --git a/app/assets/javascripts/authentication/webauthn/index.js b/app/assets/javascripts/authentication/webauthn/index.js
new file mode 100644
index 00000000000..bbf694c7698
--- /dev/null
+++ b/app/assets/javascripts/authentication/webauthn/index.js
@@ -0,0 +1,13 @@
+import $ from 'jquery';
+import WebAuthnAuthenticate from './authenticate';
+
+export default () => {
+ const webauthnAuthenticate = new WebAuthnAuthenticate(
+ $('#js-authenticate-token-2fa'),
+ '#js-login-token-2fa-form',
+ gon.webauthn,
+ document.querySelector('#js-login-2fa-device'),
+ document.querySelector('.js-2fa-form'),
+ );
+ webauthnAuthenticate.start();
+};
diff --git a/app/assets/javascripts/authentication/webauthn/register.js b/app/assets/javascripts/authentication/webauthn/register.js
new file mode 100644
index 00000000000..06e4ffd6f3e
--- /dev/null
+++ b/app/assets/javascripts/authentication/webauthn/register.js
@@ -0,0 +1,78 @@
+import { __ } from '~/locale';
+import WebAuthnError from './error';
+import WebAuthnFlow from './flow';
+import { supported, isHTTPS, convertCreateParams, convertCreateResponse } from './util';
+
+// Register WebAuthn devices for users to authenticate with.
+//
+// State Flow #1: setup -> in_progress -> registered -> POST to server
+// State Flow #2: setup -> in_progress -> error -> setup
+export default class WebAuthnRegister {
+ constructor(container, webauthnParams) {
+ this.container = container;
+ this.renderInProgress = this.renderInProgress.bind(this);
+ this.webauthnOptions = convertCreateParams(webauthnParams.options);
+
+ this.flow = new WebAuthnFlow(container, {
+ message: '#js-register-2fa-message',
+ setup: '#js-register-token-2fa-setup',
+ error: '#js-register-token-2fa-error',
+ registered: '#js-register-token-2fa-registered',
+ });
+
+ this.container.on('click', '#js-token-2fa-try-again', this.renderInProgress);
+ }
+
+ start() {
+ if (!supported()) {
+ // we show a special error message when the user visits the site
+ // using a non-ssl connection as this makes WebAuthn unavailable in
+ // any case, regardless of the used browser
+ this.renderNotSupported(!isHTTPS());
+ } else {
+ this.renderSetup();
+ }
+ }
+
+ register() {
+ navigator.credentials
+ .create({
+ publicKey: this.webauthnOptions,
+ })
+ .then(cred => this.renderRegistered(JSON.stringify(convertCreateResponse(cred))))
+ .catch(err => this.flow.renderError(new WebAuthnError(err, 'register')));
+ }
+
+ renderSetup() {
+ this.flow.renderTemplate('setup');
+ this.container.find('#js-setup-token-2fa-device').on('click', this.renderInProgress);
+ }
+
+ renderInProgress() {
+ this.flow.renderTemplate('message', {
+ message: __(
+ 'Trying to communicate with your device. Plug it in (if needed) and press the button on the device now.',
+ ),
+ });
+ return this.register();
+ }
+
+ renderRegistered(deviceResponse) {
+ this.flow.renderTemplate('registered');
+ // Prefer to do this instead of interpolating using Underscore templates
+ // because of JSON escaping issues.
+ this.container.find('#js-device-response').val(deviceResponse);
+ }
+
+ renderNotSupported(noHttps) {
+ const message = noHttps
+ ? __(
+ 'WebAuthn only works with HTTPS-enabled websites. Contact your administrator for more details.',
+ )
+ : __(
+ "Your browser doesn't support WebAuthn. Please use a supported browser, e.g. Chrome (67+) or Firefox (60+).",
+ );
+
+ this.flow.renderTemplate('message', { message });
+ }
+}
diff --git a/app/assets/javascripts/authentication/webauthn/util.js b/app/assets/javascripts/authentication/webauthn/util.js
new file mode 100644
index 00000000000..5f06c000afe
--- /dev/null
+++ b/app/assets/javascripts/authentication/webauthn/util.js
@@ -0,0 +1,120 @@
+export function supported() {
+ return Boolean(
+ navigator.credentials &&
+ navigator.credentials.create &&
+ navigator.credentials.get &&
+ window.PublicKeyCredential,
+ );
+}
+
+export function isHTTPS() {
+ return window.location.protocol.startsWith('https');
+}
+
+export const FLOW_AUTHENTICATE = 'authenticate';
+export const FLOW_REGISTER = 'register';
+
+// adapted from https://stackoverflow.com/a/21797381/8204697
+function base64ToBuffer(base64) {
+ const binaryString = window.atob(base64);
+ const len = binaryString.length;
+ const bytes = new Uint8Array(len);
+ for (let i = 0; i < len; i += 1) {
+ bytes[i] = binaryString.charCodeAt(i);
+ }
+ return bytes.buffer;
+}
+
+// adapted from https://stackoverflow.com/a/9458996/8204697
+function bufferToBase64(buffer) {
+ if (typeof buffer === 'string') {
+ return buffer;
+ }
+
+ let binary = '';
+ const bytes = new Uint8Array(buffer);
+ const len = bytes.byteLength;
+ for (let i = 0; i < len; i += 1) {
+ binary += String.fromCharCode(bytes[i]);
+ }
+ return window.btoa(binary);
+}
+
+/**
+ * Returns a copy of the given object with the id property converted to buffer
+ *
+ * @param {Object} param
+ */
+function convertIdToBuffer({ id, ...rest }) {
+ return {
+ ...rest,
+ id: base64ToBuffer(id),
+ };
+}
+
+/**
+ * Returns a copy of the given array with all `id`s of the items converted to buffer
+ *
+ * @param {Array} items
+ */
+function convertIdsToBuffer(items) {
+ return items.map(convertIdToBuffer);
+}
+
+/**
+ * Returns an object with keys of the given props, and values from the given object converted to base64
+ *
+ * @param {String} obj
+ * @param {Array} props
+ */
+function convertPropertiesToBase64(obj, props) {
+ return props.reduce(
+ (acc, property) => Object.assign(acc, { [property]: bufferToBase64(obj[property]) }),
+ {},
+ );
+}
+
+export function convertGetParams({ allowCredentials, challenge, ...rest }) {
+ return {
+ ...rest,
+ ...(allowCredentials ? { allowCredentials: convertIdsToBuffer(allowCredentials) } : {}),
+ challenge: base64ToBuffer(challenge),
+ };
+}
+
+export function convertGetResponse(webauthnResponse) {
+ return {
+ type: webauthnResponse.type,
+ id: webauthnResponse.id,
+ rawId: bufferToBase64(webauthnResponse.rawId),
+ response: convertPropertiesToBase64(webauthnResponse.response, [
+ 'clientDataJSON',
+ 'authenticatorData',
+ 'signature',
+ 'userHandle',
+ ]),
+ clientExtensionResults: webauthnResponse.getClientExtensionResults(),
+ };
+}
+
+export function convertCreateParams({ challenge, user, excludeCredentials, ...rest }) {
+ return {
+ ...rest,
+ challenge: base64ToBuffer(challenge),
+ user: convertIdToBuffer(user),
+ ...(excludeCredentials ? { excludeCredentials: convertIdsToBuffer(excludeCredentials) } : {}),
+ };
+}
+
+export function convertCreateResponse(webauthnResponse) {
+ return {
+ type: webauthnResponse.type,
+ id: webauthnResponse.id,
+ rawId: bufferToBase64(webauthnResponse.rawId),
+ clientExtensionResults: webauthnResponse.getClientExtensionResults(),
+ response: convertPropertiesToBase64(webauthnResponse.response, [
+ 'clientDataJSON',
+ 'attestationObject',
+ ]),
+ };
+}
diff --git a/app/assets/javascripts/badges/components/badge.vue b/app/assets/javascripts/badges/components/badge.vue
index 3242993b06a..3dd05f73841 100644
--- a/app/assets/javascripts/badges/components/badge.vue
+++ b/app/assets/javascripts/badges/components/badge.vue
@@ -1,13 +1,12 @@
<script>
-import { GlLoadingIcon, GlTooltipDirective } from '@gitlab/ui';
-import Icon from '~/vue_shared/components/icon.vue';
+import { GlLoadingIcon, GlTooltipDirective, GlIcon } from '@gitlab/ui';
export default {
// name: 'Badge' is a false positive: https://gitlab.com/gitlab-org/frontend/eslint-plugin-i18n/issues/25
// eslint-disable-next-line @gitlab/require-i18n-strings
name: 'Badge',
components: {
- Icon,
+ GlIcon,
GlLoadingIcon,
},
directives: {
@@ -84,7 +83,7 @@ export default {
<div v-show="hasError" class="btn-group">
<div class="btn btn-default btn-sm disabled">
- <icon :size="16" class="gl-ml-3 gl-mr-3" name="doc-image" aria-hidden="true" />
+ <gl-icon :size="16" class="gl-ml-3 gl-mr-3" name="doc-image" aria-hidden="true" />
</div>
<div class="btn btn-default btn-sm disabled">
<span class="gl-ml-3 gl-mr-3">{{ s__('Badges|No badge image') }}</span>
@@ -99,7 +98,7 @@ export default {
type="button"
@click="reloadImage"
>
- <icon :size="16" name="retry" />
+ <gl-icon :size="16" name="retry" />
</button>
</div>
</template>
diff --git a/app/assets/javascripts/badges/components/badge_form.vue b/app/assets/javascripts/badges/components/badge_form.vue
index 08834df0a9b..6afb10dd2ad 100644
--- a/app/assets/javascripts/badges/components/badge_form.vue
+++ b/app/assets/javascripts/badges/components/badge_form.vue
@@ -1,10 +1,10 @@
<script>
+/* eslint-disable vue/no-v-html */
import { escape, debounce } from 'lodash';
import { mapActions, mapState } from 'vuex';
-import { GlLoadingIcon, GlFormInput, GlFormGroup } from '@gitlab/ui';
+import { GlLoadingIcon, GlFormInput, GlFormGroup, GlButton } from '@gitlab/ui';
import { deprecatedCreateFlash as createFlash } from '~/flash';
import { s__, sprintf } from '~/locale';
-import LoadingButton from '~/vue_shared/components/loading_button.vue';
import createEmptyBadge from '../empty_badge';
import Badge from './badge.vue';
@@ -14,7 +14,7 @@ export default {
name: 'BadgeForm',
components: {
Badge,
- LoadingButton,
+ GlButton,
GlLoadingIcon,
GlFormInput,
GlFormGroup,
@@ -219,23 +219,23 @@ export default {
</div>
<div v-if="isEditing" class="row-content-block gl-display-flex gl-justify-content-end">
- <button class="btn btn-cancel gl-mr-4" type="button" @click="onCancel">
+ <gl-button class="btn-cancel gl-mr-4" data-testid="cancelEditing" @click="onCancel">
{{ __('Cancel') }}
- </button>
- <loading-button
+ </gl-button>
+ <gl-button
:loading="isSaving"
- :label="s__('Badges|Save changes')"
type="submit"
- container-class="btn btn-success"
- />
+ variant="success"
+ category="primary"
+ data-testid="saveEditing"
+ >
+ {{ s__('Badges|Save changes') }}
+ </gl-button>
</div>
<div v-else class="gl-display-flex gl-justify-content-end form-group">
- <loading-button
- :loading="isSaving"
- :label="s__('Badges|Add badge')"
- type="submit"
- container-class="btn btn-success"
- />
+ <gl-button :loading="isSaving" type="submit" variant="success" category="primary">
+ {{ s__('Badges|Add badge') }}
+ </gl-button>
</div>
</form>
</template>
diff --git a/app/assets/javascripts/badges/components/badge_list_row.vue b/app/assets/javascripts/badges/components/badge_list_row.vue
index bad14666bb2..3343634ecad 100644
--- a/app/assets/javascripts/badges/components/badge_list_row.vue
+++ b/app/assets/javascripts/badges/components/badge_list_row.vue
@@ -1,8 +1,7 @@
<script>
import { mapActions, mapState } from 'vuex';
-import { GlLoadingIcon } from '@gitlab/ui';
+import { GlLoadingIcon, GlIcon } from '@gitlab/ui';
import { s__ } from '~/locale';
-import Icon from '~/vue_shared/components/icon.vue';
import { PROJECT_BADGE } from '../constants';
import Badge from './badge.vue';
@@ -10,7 +9,7 @@ export default {
name: 'BadgeListRow',
components: {
Badge,
- Icon,
+ GlIcon,
GlLoadingIcon,
},
props: {
@@ -58,7 +57,7 @@ export default {
type="button"
@click="editBadge(badge)"
>
- <icon :size="16" :aria-label="__('Edit')" name="pencil" />
+ <gl-icon :size="16" :aria-label="__('Edit')" name="pencil" />
</button>
<button
:disabled="badge.isDeleting"
@@ -68,7 +67,7 @@ export default {
data-target="#delete-badge-modal"
@click="updateBadgeInModal(badge)"
>
- <icon :size="16" :aria-label="__('Delete')" name="remove" />
+ <gl-icon :size="16" :aria-label="__('Delete')" name="remove" />
</button>
<gl-loading-icon v-show="badge.isDeleting" :inline="true" />
</div>
diff --git a/app/assets/javascripts/batch_comments/components/draft_note.vue b/app/assets/javascripts/batch_comments/components/draft_note.vue
index 39c1b8decee..a6cd36caede 100644
--- a/app/assets/javascripts/batch_comments/components/draft_note.vue
+++ b/app/assets/javascripts/batch_comments/components/draft_note.vue
@@ -1,4 +1,5 @@
<script>
+/* eslint-disable vue/no-v-html */
import { mapActions, mapGetters, mapState } from 'vuex';
import { GlButton } from '@gitlab/ui';
import NoteableNote from '~/notes/components/noteable_note.vue';
diff --git a/app/assets/javascripts/batch_comments/components/preview_dropdown.vue b/app/assets/javascripts/batch_comments/components/preview_dropdown.vue
index 7520cc2401b..2b37ed19176 100644
--- a/app/assets/javascripts/batch_comments/components/preview_dropdown.vue
+++ b/app/assets/javascripts/batch_comments/components/preview_dropdown.vue
@@ -1,16 +1,16 @@
<script>
import { mapActions, mapGetters, mapState } from 'vuex';
-import { GlLoadingIcon } from '@gitlab/ui';
+import { GlButton, GlLoadingIcon, GlIcon } from '@gitlab/ui';
import { sprintf, n__ } from '~/locale';
-import Icon from '~/vue_shared/components/icon.vue';
import DraftsCount from './drafts_count.vue';
import PublishButton from './publish_button.vue';
import PreviewItem from './preview_item.vue';
export default {
components: {
+ GlButton,
GlLoadingIcon,
- Icon,
+ GlIcon,
DraftsCount,
PublishButton,
PreviewItem,
@@ -29,7 +29,7 @@ export default {
watch: {
showPreviewDropdown() {
if (this.showPreviewDropdown && this.$refs.dropdown) {
- this.$nextTick(() => this.$refs.dropdown.focus());
+ this.$nextTick(() => this.$refs.dropdown.$el.focus());
}
},
},
@@ -63,32 +63,35 @@ export default {
show: showPreviewDropdown,
}"
>
- <button
+ <gl-button
ref="dropdown"
type="button"
- class="btn btn-success review-preview-dropdown-toggle qa-review-preview-toggle"
+ category="primary"
+ variant="success"
+ class="review-preview-dropdown-toggle qa-review-preview-toggle"
@click="toggleReviewDropdown"
>
{{ __('Finish review') }}
<drafts-count />
- <icon name="angle-up" />
- </button>
+ <gl-icon name="angle-up" />
+ </gl-button>
<div
class="dropdown-menu dropdown-menu-large dropdown-menu-right dropdown-open-top"
:class="{
show: showPreviewDropdown,
}"
>
- <div class="dropdown-title">
- {{ dropdownTitle }}
- <button
+ <div class="dropdown-title gl-display-flex gl-align-items-center">
+ <span class="gl-ml-auto">{{ dropdownTitle }}</span>
+ <gl-button
:aria-label="__('Close')"
type="button"
- class="dropdown-title-button dropdown-menu-close"
+ category="tertiary"
+ size="small"
+ class="dropdown-title-button gl-ml-auto gl-p-0!"
+ icon="close"
@click="toggleReviewDropdown"
- >
- <icon name="close" />
- </button>
+ />
</div>
<div class="dropdown-content">
<ul v-if="isNotesFetched">
diff --git a/app/assets/javascripts/batch_comments/components/preview_item.vue b/app/assets/javascripts/batch_comments/components/preview_item.vue
index 982fb01f49a..c89a6b537ef 100644
--- a/app/assets/javascripts/batch_comments/components/preview_item.vue
+++ b/app/assets/javascripts/batch_comments/components/preview_item.vue
@@ -1,9 +1,8 @@
<script>
import { mapActions, mapGetters } from 'vuex';
-import { GlSprintf } from '@gitlab/ui';
+import { GlSprintf, GlIcon } from '@gitlab/ui';
import { IMAGE_DIFF_POSITION_TYPE } from '~/diffs/constants';
import { sprintf, __ } from '~/locale';
-import Icon from '~/vue_shared/components/icon.vue';
import resolvedStatusMixin from '../mixins/resolved_status';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import {
@@ -14,7 +13,7 @@ import {
export default {
components: {
- Icon,
+ GlIcon,
GlSprintf,
},
mixins: [resolvedStatusMixin, glFeatureFlagsMixin()],
@@ -101,7 +100,7 @@ export default {
@click="scrollToDraft(draft)"
>
<span class="review-preview-item-header">
- <icon class="flex-shrink-0" :name="iconName" />
+ <gl-icon class="flex-shrink-0" :name="iconName" />
<span
class="bold text-nowrap"
:class="{ 'gl-align-items-center': glFeatures.multilineComments }"
@@ -138,7 +137,7 @@ export default {
v-if="draft.discussion_id && resolvedStatusMessage"
class="review-preview-item-footer draft-note-resolution p-0"
>
- <icon class="gl-mr-3" name="status_success" /> {{ resolvedStatusMessage }}
+ <gl-icon class="gl-mr-3" name="status_success" /> {{ resolvedStatusMessage }}
</span>
</button>
</template>
diff --git a/app/assets/javascripts/batch_comments/components/review_bar.vue b/app/assets/javascripts/batch_comments/components/review_bar.vue
index 2d7b86d2431..e51888eabc1 100644
--- a/app/assets/javascripts/batch_comments/components/review_bar.vue
+++ b/app/assets/javascripts/batch_comments/components/review_bar.vue
@@ -1,4 +1,5 @@
<script>
+/* eslint-disable vue/no-v-html */
import { mapActions, mapState, mapGetters } from 'vuex';
import { GlModal, GlModalDirective, GlButton } from '@gitlab/ui';
import { sprintf, s__ } from '~/locale';
diff --git a/app/assets/javascripts/batch_comments/index.js b/app/assets/javascripts/batch_comments/index.js
index e06285c0b37..9c763e70d63 100644
--- a/app/assets/javascripts/batch_comments/index.js
+++ b/app/assets/javascripts/batch_comments/index.js
@@ -3,7 +3,6 @@ import { mapActions } from 'vuex';
import store from '~/mr_notes/stores';
import ReviewBar from './components/review_bar.vue';
-// eslint-disable-next-line import/prefer-default-export
export const initReviewBar = () => {
const el = document.getElementById('js-review-bar');
diff --git a/app/assets/javascripts/behaviors/autosize.js b/app/assets/javascripts/behaviors/autosize.js
index add43b81f6d..d61797b7ae4 100644
--- a/app/assets/javascripts/behaviors/autosize.js
+++ b/app/assets/javascripts/behaviors/autosize.js
@@ -1,8 +1,13 @@
import Autosize from 'autosize';
+import { waitForCSSLoaded } from '../helpers/startup_css_helper';
document.addEventListener('DOMContentLoaded', () => {
- const autosizeEls = document.querySelectorAll('.js-autosize');
+ waitForCSSLoaded(() => {
+ const autosizeEls = document.querySelectorAll('.js-autosize');
- Autosize(autosizeEls);
- Autosize.update(autosizeEls);
+ Autosize(autosizeEls);
+ Autosize.update(autosizeEls);
+
+ autosizeEls.forEach(el => el.classList.add('js-autosize-initialized'));
+ });
});
diff --git a/app/assets/javascripts/behaviors/index.js b/app/assets/javascripts/behaviors/index.js
index 8060938c72a..fd12c282b62 100644
--- a/app/assets/javascripts/behaviors/index.js
+++ b/app/assets/javascripts/behaviors/index.js
@@ -1,7 +1,7 @@
+import $ from 'jquery';
import './autosize';
import './bind_in_out';
import './markdown/render_gfm';
-import initGFMInput from './markdown/gfm_auto_complete';
import initCopyAsGFM from './markdown/copy_as_gfm';
import initCopyToClipboard from './copy_to_clipboard';
import './details_behavior';
@@ -15,9 +15,27 @@ import initCollapseSidebarOnWindowResize from './collapse_sidebar_on_window_resi
import initSelect2Dropdowns from './select2';
installGlEmojiElement();
-initGFMInput();
+
initCopyAsGFM();
initCopyToClipboard();
+
initPageShortcuts();
initCollapseSidebarOnWindowResize();
initSelect2Dropdowns();
+
+document.addEventListener('DOMContentLoaded', () => {
+ window.requestIdleCallback(
+ () => {
+ // Check if we have to Load GFM Input
+ const $gfmInputs = $('.js-gfm-input:not(.js-gfm-input-initialized)');
+ if ($gfmInputs.length) {
+ import(/* webpackChunkName: 'initGFMInput' */ './markdown/gfm_auto_complete')
+ .then(({ default: initGFMInput }) => {
+ initGFMInput($gfmInputs);
+ })
+ .catch(() => {});
+ }
+ },
+ { timeout: 500 },
+ );
+});
diff --git a/app/assets/javascripts/behaviors/markdown/gfm_auto_complete.js b/app/assets/javascripts/behaviors/markdown/gfm_auto_complete.js
index 6bbd2133344..d712c90242c 100644
--- a/app/assets/javascripts/behaviors/markdown/gfm_auto_complete.js
+++ b/app/assets/javascripts/behaviors/markdown/gfm_auto_complete.js
@@ -2,8 +2,8 @@ import $ from 'jquery';
import GfmAutoComplete from 'ee_else_ce/gfm_auto_complete';
import { parseBoolean } from '~/lib/utils/common_utils';
-export default function initGFMInput() {
- $('.js-gfm-input:not(.js-vue-textarea)').each((i, el) => {
+export default function initGFMInput($els) {
+ $els.each((i, el) => {
const gfm = new GfmAutoComplete(gl.GfmAutoComplete && gl.GfmAutoComplete.dataSources);
const enableGFM = parseBoolean(el.dataset.supportsAutocomplete);
@@ -14,6 +14,7 @@ export default function initGFMInput() {
milestones: enableGFM,
mergeRequests: enableGFM,
labels: enableGFM,
+ vulnerabilities: enableGFM,
});
});
}
diff --git a/app/assets/javascripts/behaviors/markdown/render_gfm.js b/app/assets/javascripts/behaviors/markdown/render_gfm.js
index 01627b7206d..5e9d80e1529 100644
--- a/app/assets/javascripts/behaviors/markdown/render_gfm.js
+++ b/app/assets/javascripts/behaviors/markdown/render_gfm.js
@@ -5,7 +5,6 @@ import renderMermaid from './render_mermaid';
import renderMetrics from './render_metrics';
import highlightCurrentUser from './highlight_current_user';
import initUserPopovers from '../../user_popovers';
-import initMRPopovers from '../../mr_popover';
// Render GitLab flavoured Markdown
//
@@ -17,9 +16,25 @@ $.fn.renderGFM = function renderGFM() {
renderMermaid(this.find('.js-render-mermaid'));
highlightCurrentUser(this.find('.gfm-project_member').get());
initUserPopovers(this.find('.js-user-link').get());
- initMRPopovers(this.find('.gfm-merge_request').get());
+
+ const mrPopoverElements = this.find('.gfm-merge_request').get();
+ if (mrPopoverElements.length) {
+ import(/* webpackChunkName: 'MrPopoverBundle' */ '../../mr_popover')
+ .then(({ default: initMRPopovers }) => {
+ initMRPopovers(mrPopoverElements);
+ })
+ .catch(() => {});
+ }
+
renderMetrics(this.find('.js-render-metrics').get());
return this;
};
-$(() => $('body').renderGFM());
+$(() => {
+ window.requestIdleCallback(
+ () => {
+ $('body').renderGFM();
+ },
+ { timeout: 500 },
+ );
+});
diff --git a/app/assets/javascripts/behaviors/markdown/render_math.js b/app/assets/javascripts/behaviors/markdown/render_math.js
index 03d9955f8fc..30783562da9 100644
--- a/app/assets/javascripts/behaviors/markdown/render_math.js
+++ b/app/assets/javascripts/behaviors/markdown/render_math.js
@@ -1,5 +1,6 @@
import { deprecatedCreateFlash as flash } from '~/flash';
import { s__, sprintf } from '~/locale';
+import { differenceInMilliseconds } from '~/lib/utils/datetime_utility';
// Renders math using KaTeX in any element with the
// `js-render-math` class
@@ -111,7 +112,7 @@ class SafeMathRenderer {
// Give the browser time to reflow the svg
waitForReflow(() => {
- const deltaTime = Date.now() - this.startTime;
+ const deltaTime = differenceInMilliseconds(this.startTime);
this.totalMS += deltaTime;
this.renderElement();
diff --git a/app/assets/javascripts/behaviors/shortcuts.js b/app/assets/javascripts/behaviors/shortcuts.js
index 7987a533ae5..7352be0dbd5 100644
--- a/app/assets/javascripts/behaviors/shortcuts.js
+++ b/app/assets/javascripts/behaviors/shortcuts.js
@@ -1,5 +1,3 @@
-import Shortcuts from './shortcuts/shortcuts';
-
export default function initPageShortcuts() {
const { page } = document.body.dataset;
const pagesWithCustomShortcuts = [
@@ -29,7 +27,9 @@ export default function initPageShortcuts() {
// the pages above have their own shortcuts sub-classes instantiated elsewhere
// TODO: replace this whitelist with something more automated/maintainable
if (page && !pagesWithCustomShortcuts.includes(page)) {
- return new Shortcuts();
+ import(/* webpackChunkName: 'shortcutsBundle' */ './shortcuts/shortcuts')
+ .then(({ default: Shortcuts }) => new Shortcuts())
+ .catch(() => {});
}
return false;
}
diff --git a/app/assets/javascripts/behaviors/shortcuts/shortcuts.js b/app/assets/javascripts/behaviors/shortcuts/shortcuts.js
index 85636f3e5d2..8a8b61a57cd 100644
--- a/app/assets/javascripts/behaviors/shortcuts/shortcuts.js
+++ b/app/assets/javascripts/behaviors/shortcuts/shortcuts.js
@@ -2,6 +2,7 @@ import $ from 'jquery';
import Cookies from 'js-cookie';
import Mousetrap from 'mousetrap';
import Vue from 'vue';
+import { flatten } from 'lodash';
import { disableShortcuts, shouldDisableShortcuts } from './shortcuts_toggle';
import ShortcutsToggle from './shortcuts_toggle.vue';
import axios from '../../lib/utils/axios_utils';
@@ -9,13 +10,13 @@ import { refreshCurrentPage, visitUrl } from '../../lib/utils/url_utility';
import findAndFollowLink from '../../lib/utils/navigation_utility';
import { parseBoolean, getCspNonceValue } from '~/lib/utils/common_utils';
-const defaultStopCallback = Mousetrap.stopCallback;
-Mousetrap.stopCallback = (e, element, combo) => {
+const defaultStopCallback = Mousetrap.prototype.stopCallback;
+Mousetrap.prototype.stopCallback = function customStopCallback(e, element, combo) {
if (['ctrl+shift+p', 'command+shift+p'].indexOf(combo) !== -1) {
return false;
}
- return defaultStopCallback(e, element, combo);
+ return defaultStopCallback.call(this, e, element, combo);
};
function initToggleButton() {
@@ -27,6 +28,39 @@ function initToggleButton() {
});
}
+/**
+ * The key used to save and fetch the local Mousetrap instance
+ * attached to a `<textarea>` element using `jQuery.data`
+ */
+const LOCAL_MOUSETRAP_DATA_KEY = 'local-mousetrap-instance';
+
+/**
+ * Gets a mapping of toolbar button => keyboard shortcuts
+ * associated to the given markdown editor `<textarea>` element
+ *
+ * @param {HTMLTextAreaElement} $textarea The jQuery-wrapped `<textarea>`
+ * element to extract keyboard shortcuts from
+ *
+ * @returns A Map with keys that are jQuery-wrapped toolbar buttons
+ * (i.e. `$toolbarBtn`) and values that are arrays of string
+ * keyboard shortcuts (e.g. `['command+k', 'ctrl+k]`).
+ */
+function getToolbarBtnToShortcutsMap($textarea) {
+ const $allToolbarBtns = $textarea.closest('.md-area').find('.js-md');
+ const map = new Map();
+
+ $allToolbarBtns.each(function attachToolbarBtnHandler() {
+ const $toolbarBtn = $(this);
+ const keyboardShortcuts = $toolbarBtn.data('md-shortcuts');
+
+ if (keyboardShortcuts?.length) {
+ map.set($toolbarBtn, keyboardShortcuts);
+ }
+ });
+
+ return map;
+}
+
export default class Shortcuts {
constructor() {
this.onToggleHelp = this.onToggleHelp.bind(this);
@@ -34,6 +68,7 @@ export default class Shortcuts {
Mousetrap.bind('?', this.onToggleHelp);
Mousetrap.bind('s', Shortcuts.focusSearch);
+ Mousetrap.bind('/', Shortcuts.focusSearch);
Mousetrap.bind('f', this.focusFilter.bind(this));
Mousetrap.bind('p b', Shortcuts.onTogglePerfBar);
@@ -143,4 +178,62 @@ export default class Shortcuts {
e.preventDefault();
}
}
+
+ /**
+ * Initializes markdown editor shortcuts on the provided `<textarea>` element
+ *
+ * @param {JQuery} $textarea The jQuery-wrapped `<textarea>` element
+ * where markdown shortcuts should be enabled
+ * @param {Function} handler The handler to call when a
+ * keyboard shortcut is pressed inside the markdown `<textarea>`
+ */
+ static initMarkdownEditorShortcuts($textarea, handler) {
+ const toolbarBtnToShortcutsMap = getToolbarBtnToShortcutsMap($textarea);
+
+ const localMousetrap = new Mousetrap($textarea[0]);
+
+ // Save a reference to the local mousetrap instance on the <textarea>
+ // so that it can be retrieved when unbinding shortcut handlers
+ $textarea.data(LOCAL_MOUSETRAP_DATA_KEY, localMousetrap);
+
+ toolbarBtnToShortcutsMap.forEach((keyboardShortcuts, $toolbarBtn) => {
+ localMousetrap.bind(keyboardShortcuts, e => {
+ e.preventDefault();
+
+ handler($toolbarBtn);
+ });
+ });
+
+ // Get an array of all shortcut strings that have been added above
+ const allShortcuts = flatten([...toolbarBtnToShortcutsMap.values()]);
+
+ const originalStopCallback = Mousetrap.prototype.stopCallback;
+ localMousetrap.stopCallback = function newStopCallback(e, element, combo) {
+ if (allShortcuts.includes(combo)) {
+ return false;
+ }
+
+ return originalStopCallback.call(this, e, element, combo);
+ };
+ }
+
+ /**
+ * Removes markdown editor shortcut handlers originally attached
+ * with `initMarkdownEditorShortcuts`.
+ *
+ * Note: it is safe to call this function even if `initMarkdownEditorShortcuts`
+ * has _not_ yet been called on the given `<textarea>`.
+ *
+ * @param {JQuery} $textarea The jQuery-wrapped `<textarea>`
+ * to remove shortcut handlers from
+ */
+ static removeMarkdownEditorShortcuts($textarea) {
+ const localMousetrap = $textarea.data(LOCAL_MOUSETRAP_DATA_KEY);
+
+ if (localMousetrap) {
+ getToolbarBtnToShortcutsMap($textarea).forEach(keyboardShortcuts => {
+ localMousetrap.unbind(keyboardShortcuts);
+ });
+ }
+ }
}
diff --git a/app/assets/javascripts/behaviors/shortcuts/shortcuts_find_file.js b/app/assets/javascripts/behaviors/shortcuts/shortcuts_find_file.js
index 8658081c6c2..f0d2ecfd210 100644
--- a/app/assets/javascripts/behaviors/shortcuts/shortcuts_find_file.js
+++ b/app/assets/javascripts/behaviors/shortcuts/shortcuts_find_file.js
@@ -5,12 +5,11 @@ export default class ShortcutsFindFile extends ShortcutsNavigation {
constructor(projectFindFile) {
super();
- const oldStopCallback = Mousetrap.stopCallback;
- this.projectFindFile = projectFindFile;
+ const oldStopCallback = Mousetrap.prototype.stopCallback;
- Mousetrap.stopCallback = (e, element, combo) => {
+ Mousetrap.prototype.stopCallback = function customStopCallback(e, element, combo) {
if (
- element === this.projectFindFile.inputElement[0] &&
+ element === projectFindFile.inputElement[0] &&
(combo === 'up' || combo === 'down' || combo === 'esc' || combo === 'enter')
) {
// when press up/down key in textbox, cursor prevent to move to home/end
@@ -18,12 +17,12 @@ export default class ShortcutsFindFile extends ShortcutsNavigation {
return false;
}
- return oldStopCallback(e, element, combo);
+ return oldStopCallback.call(this, e, element, combo);
};
- Mousetrap.bind('up', this.projectFindFile.selectRowUp);
- Mousetrap.bind('down', this.projectFindFile.selectRowDown);
- Mousetrap.bind('esc', this.projectFindFile.goToTree);
- Mousetrap.bind('enter', this.projectFindFile.goToBlob);
+ Mousetrap.bind('up', projectFindFile.selectRowUp);
+ Mousetrap.bind('down', projectFindFile.selectRowDown);
+ Mousetrap.bind('esc', projectFindFile.goToTree);
+ Mousetrap.bind('enter', projectFindFile.goToBlob);
}
}
diff --git a/app/assets/javascripts/blob/components/blob_edit_content.vue b/app/assets/javascripts/blob/components/blob_edit_content.vue
index 26ba7b98a39..6293f3bed1c 100644
--- a/app/assets/javascripts/blob/components/blob_edit_content.vue
+++ b/app/assets/javascripts/blob/components/blob_edit_content.vue
@@ -46,7 +46,7 @@ export default {
blobGlobalId: this.fileGlobalId,
});
- this.editor.onChangeContent(debounce(this.onFileChange.bind(this), 250));
+ this.editor.onDidChangeModelContent(debounce(this.onFileChange.bind(this), 250));
window.requestAnimationFrame(() => {
if (!performance.getEntriesByName(SNIPPET_MARK_BLOBS_CONTENT).length) {
diff --git a/app/assets/javascripts/blob/components/blob_embeddable.vue b/app/assets/javascripts/blob/components/blob_embeddable.vue
deleted file mode 100644
index 00b915ec8bd..00000000000
--- a/app/assets/javascripts/blob/components/blob_embeddable.vue
+++ /dev/null
@@ -1,41 +0,0 @@
-<script>
-import { GlFormInputGroup, GlDeprecatedButton, GlIcon } from '@gitlab/ui';
-import { __ } from '~/locale';
-
-export default {
- components: {
- GlFormInputGroup,
- GlDeprecatedButton,
- GlIcon,
- },
- props: {
- url: {
- type: String,
- required: true,
- },
- },
- data() {
- return {
- optionValues: [
- // eslint-disable-next-line no-useless-escape
- { name: __('Embed'), value: `<script src='${this.url}.js'><\/script>` },
- { name: __('Share'), value: this.url },
- ],
- };
- },
-};
-</script>
-<template>
- <gl-form-input-group
- id="embeddable-text"
- :predefined-options="optionValues"
- readonly
- select-on-click
- >
- <template #append>
- <gl-deprecated-button new-style data-clipboard-target="#embeddable-text">
- <gl-icon name="copy-to-clipboard" :title="__('Copy')" />
- </gl-deprecated-button>
- </template>
- </gl-form-input-group>
-</template>
diff --git a/app/assets/javascripts/blob/file_template_mediator.js b/app/assets/javascripts/blob/file_template_mediator.js
index 4409d7a33cc..5058ca7122d 100644
--- a/app/assets/javascripts/blob/file_template_mediator.js
+++ b/app/assets/javascripts/blob/file_template_mediator.js
@@ -1,16 +1,18 @@
import $ from 'jquery';
+
import Api from '~/api';
+import toast from '~/vue_shared/plugins/global_toast';
+import { __ } from '~/locale';
+import initPopover from '~/blob/suggest_gitlab_ci_yml';
import { deprecatedCreateFlash as Flash } from '../flash';
+
import FileTemplateTypeSelector from './template_selectors/type_selector';
import BlobCiYamlSelector from './template_selectors/ci_yaml_selector';
import DockerfileSelector from './template_selectors/dockerfile_selector';
import GitignoreSelector from './template_selectors/gitignore_selector';
import LicenseSelector from './template_selectors/license_selector';
import MetricsDashboardSelector from './template_selectors/metrics_dashboard_selector';
-import toast from '~/vue_shared/plugins/global_toast';
-import { __ } from '~/locale';
-import initPopover from '~/blob/suggest_gitlab_ci_yml';
export default class FileTemplateMediator {
constructor({ editor, currentAction, projectId }) {
diff --git a/app/assets/javascripts/blob/file_template_selector.js b/app/assets/javascripts/blob/file_template_selector.js
index 476901aae75..bd39aa2e16f 100644
--- a/app/assets/javascripts/blob/file_template_selector.js
+++ b/app/assets/javascripts/blob/file_template_selector.js
@@ -45,11 +45,15 @@ export default class FileTemplateSelector {
}
renderLoading() {
- this.$loadingIcon.addClass('fa-spinner fa-spin').removeClass('fa-chevron-down');
+ this.$loadingIcon
+ .addClass('gl-spinner gl-spinner-orange gl-spinner-sm')
+ .removeClass('fa-chevron-down');
}
renderLoaded() {
- this.$loadingIcon.addClass('fa-chevron-down').removeClass('fa-spinner fa-spin');
+ this.$loadingIcon
+ .addClass('fa-chevron-down')
+ .removeClass('gl-spinner gl-spinner-orange gl-spinner-sm');
}
reportSelection(options) {
diff --git a/app/assets/javascripts/blob/pipeline_tour_success_modal.vue b/app/assets/javascripts/blob/pipeline_tour_success_modal.vue
index 90eafb75758..411241b72d5 100644
--- a/app/assets/javascripts/blob/pipeline_tour_success_modal.vue
+++ b/app/assets/javascripts/blob/pipeline_tour_success_modal.vue
@@ -1,5 +1,5 @@
<script>
-import { GlModal, GlSprintf, GlLink } from '@gitlab/ui';
+import { GlModal, GlSprintf, GlLink, GlButton } from '@gitlab/ui';
import Cookies from 'js-cookie';
import { sprintf, s__, __ } from '~/locale';
import { glEmojiTag } from '~/emoji';
@@ -18,6 +18,8 @@ export default {
helpMessage: s__(
`MR widget|Take a look at our %{beginnerLinkStart}Beginner's Guide to Continuous Integration%{beginnerLinkEnd} and our %{exampleLinkStart}examples of GitLab CI/CD%{exampleLinkEnd} to learn more.`,
),
+ pipelinesButton: s__('MR widget|See your pipeline in action'),
+ mergeRequestButton: s__('MR widget|Back to the Merge request'),
modalTitle: sprintf(
__("That's it, well done!%{celebrate}"),
{
@@ -25,11 +27,13 @@ export default {
},
false,
),
- goToTrackValue: 10,
+ goToTrackValuePipelines: 10,
+ goToTrackValueMergeRequest: 20,
trackEvent: 'click_button',
components: {
GlModal,
GlSprintf,
+ GlButton,
GlLink,
},
mixins: [trackingMixin],
@@ -38,6 +42,11 @@ export default {
type: String,
required: true,
},
+ projectMergeRequestsPath: {
+ type: String,
+ required: false,
+ default: '',
+ },
commitCookie: {
type: String,
required: true,
@@ -59,6 +68,15 @@ export default {
property: this.humanAccess,
};
},
+ goToMergeRequestPath() {
+ return this.commitCookiePath || this.projectMergeRequestsPath;
+ },
+ commitCookiePath() {
+ const cookieVal = Cookies.get(this.commitCookie);
+
+ if (cookieVal !== 'true') return cookieVal;
+ return '';
+ },
},
mounted() {
this.track();
@@ -100,17 +118,28 @@ export default {
</template>
</gl-sprintf>
<template #modal-footer>
- <a
- ref="goto"
+ <gl-button
+ v-if="projectMergeRequestsPath"
+ ref="goToMergeRequest"
+ :href="goToMergeRequestPath"
+ :data-track-property="humanAccess"
+ :data-track-value="$options.goToTrackValueMergeRequest"
+ :data-track-event="$options.trackEvent"
+ :data-track-label="trackLabel"
+ >
+ {{ $options.mergeRequestButton }}
+ </gl-button>
+ <gl-button
+ ref="goToPipelines"
:href="goToPipelinesPath"
- class="btn btn-success"
+ variant="success"
:data-track-property="humanAccess"
- :data-track-value="$options.goToTrackValue"
+ :data-track-value="$options.goToTrackValuePipelines"
:data-track-event="$options.trackEvent"
:data-track-label="trackLabel"
>
- {{ __('See your pipeline in action') }}
- </a>
+ {{ $options.pipelinesButton }}
+ </gl-button>
</template>
</gl-modal>
</template>
diff --git a/app/assets/javascripts/blob/suggest_gitlab_ci_yml/components/popover.vue b/app/assets/javascripts/blob/suggest_gitlab_ci_yml/components/popover.vue
index aff6a56cb0b..06f436adb8e 100644
--- a/app/assets/javascripts/blob/suggest_gitlab_ci_yml/components/popover.vue
+++ b/app/assets/javascripts/blob/suggest_gitlab_ci_yml/components/popover.vue
@@ -49,6 +49,10 @@ export default {
type: String,
required: true,
},
+ mergeRequestPath: {
+ type: String,
+ required: true,
+ },
},
data() {
return {
@@ -109,7 +113,7 @@ export default {
:css-classes="['suggest-gitlab-ci-yml', 'ml-4']"
>
<template #title>
- <span v-html="suggestTitle"></span>
+ <span>{{ suggestTitle }}</span>
<span class="ml-auto">
<gl-button
:aria-label="__('Close')"
diff --git a/app/assets/javascripts/blob/suggest_gitlab_ci_yml/index.js b/app/assets/javascripts/blob/suggest_gitlab_ci_yml/index.js
index 3b67b3dd259..55edb852ee6 100644
--- a/app/assets/javascripts/blob/suggest_gitlab_ci_yml/index.js
+++ b/app/assets/javascripts/blob/suggest_gitlab_ci_yml/index.js
@@ -10,6 +10,7 @@ export default el =>
target: el.dataset.target,
trackLabel: el.dataset.trackLabel,
dismissKey: el.dataset.dismissKey,
+ mergeRequestPath: el.dataset.mergeRequestPath,
humanAccess: el.dataset.humanAccess,
},
});
diff --git a/app/assets/javascripts/blob/suggest_web_ide_ci/components/web_ide_alert.vue b/app/assets/javascripts/blob/suggest_web_ide_ci/components/web_ide_alert.vue
new file mode 100644
index 00000000000..1308ca53e74
--- /dev/null
+++ b/app/assets/javascripts/blob/suggest_web_ide_ci/components/web_ide_alert.vue
@@ -0,0 +1,50 @@
+<script>
+import { GlAlert, GlButton } from '@gitlab/ui';
+import axios from '~/lib/utils/axios_utils';
+
+export default {
+ components: {
+ GlAlert,
+ GlButton,
+ },
+ props: {
+ dismissEndpoint: {
+ type: String,
+ required: true,
+ },
+ featureId: {
+ type: String,
+ required: true,
+ },
+ editPath: {
+ type: String,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ showAlert: true,
+ };
+ },
+ methods: {
+ dismissAlert() {
+ this.showAlert = false;
+
+ return axios.post(this.dismissEndpoint, {
+ feature_name: this.featureId,
+ });
+ },
+ },
+};
+</script>
+
+<template>
+ <gl-alert v-if="showAlert" class="gl-mt-5" @dismiss="dismissAlert">
+ {{ __('The Web IDE offers advanced syntax highlighting capabilities and more.') }}
+ <div class="gl-mt-5">
+ <gl-button :href="editPath" category="primary" variant="info">{{
+ __('Open Web IDE')
+ }}</gl-button>
+ </div>
+ </gl-alert>
+</template>
diff --git a/app/assets/javascripts/blob/suggest_web_ide_ci/index.js b/app/assets/javascripts/blob/suggest_web_ide_ci/index.js
new file mode 100644
index 00000000000..eadf3cd6216
--- /dev/null
+++ b/app/assets/javascripts/blob/suggest_web_ide_ci/index.js
@@ -0,0 +1,20 @@
+import Vue from 'vue';
+import WebIdeAlert from './components/web_ide_alert.vue';
+
+export default el => {
+ const { dismissEndpoint, featureId, editPath } = el.dataset;
+
+ // eslint-disable-next-line no-new
+ new Vue({
+ el,
+ render(createElement) {
+ return createElement(WebIdeAlert, {
+ props: {
+ dismissEndpoint,
+ featureId,
+ editPath,
+ },
+ });
+ },
+ });
+};
diff --git a/app/assets/javascripts/blob/template_selector.js b/app/assets/javascripts/blob/template_selector.js
index 2427e25a17d..257458138dc 100644
--- a/app/assets/javascripts/blob/template_selector.js
+++ b/app/assets/javascripts/blob/template_selector.js
@@ -1,7 +1,7 @@
/* eslint-disable class-methods-use-this */
import $ from 'jquery';
-import '~/gl_dropdown';
+import initDeprecatedJQueryDropdown from '~/deprecated_jquery_dropdown';
export default class TemplateSelector {
constructor({ dropdown, data, pattern, wrapper, editor, $input } = {}) {
@@ -19,7 +19,7 @@ export default class TemplateSelector {
}
initDropdown(dropdown, data) {
- return $(dropdown).glDropdown({
+ return initDeprecatedJQueryDropdown($(dropdown), {
data,
filterable: true,
selectable: true,
diff --git a/app/assets/javascripts/blob/template_selectors/ci_yaml_selector.js b/app/assets/javascripts/blob/template_selectors/ci_yaml_selector.js
index d819452df68..3a4e86fe572 100644
--- a/app/assets/javascripts/blob/template_selectors/ci_yaml_selector.js
+++ b/app/assets/javascripts/blob/template_selectors/ci_yaml_selector.js
@@ -1,4 +1,5 @@
import FileTemplateSelector from '../file_template_selector';
+import initDeprecatedJQueryDropdown from '~/deprecated_jquery_dropdown';
export default class BlobCiYamlSelector extends FileTemplateSelector {
constructor({ mediator }) {
@@ -15,7 +16,7 @@ export default class BlobCiYamlSelector extends FileTemplateSelector {
initDropdown() {
// maybe move to super class as well
- this.$dropdown.glDropdown({
+ initDeprecatedJQueryDropdown(this.$dropdown, {
data: this.$dropdown.data('data'),
filterable: true,
selectable: true,
diff --git a/app/assets/javascripts/blob/template_selectors/dockerfile_selector.js b/app/assets/javascripts/blob/template_selectors/dockerfile_selector.js
index 7d5e98889d3..3cb4bb83930 100644
--- a/app/assets/javascripts/blob/template_selectors/dockerfile_selector.js
+++ b/app/assets/javascripts/blob/template_selectors/dockerfile_selector.js
@@ -1,5 +1,6 @@
import FileTemplateSelector from '../file_template_selector';
import { __ } from '~/locale';
+import initDeprecatedJQueryDropdown from '~/deprecated_jquery_dropdown';
export default class DockerfileSelector extends FileTemplateSelector {
constructor({ mediator }) {
@@ -16,7 +17,7 @@ export default class DockerfileSelector extends FileTemplateSelector {
initDropdown() {
// maybe move to super class as well
- this.$dropdown.glDropdown({
+ initDeprecatedJQueryDropdown(this.$dropdown, {
data: this.$dropdown.data('data'),
filterable: true,
selectable: true,
diff --git a/app/assets/javascripts/blob/template_selectors/gitignore_selector.js b/app/assets/javascripts/blob/template_selectors/gitignore_selector.js
index 39a8937641d..1721230dcb7 100644
--- a/app/assets/javascripts/blob/template_selectors/gitignore_selector.js
+++ b/app/assets/javascripts/blob/template_selectors/gitignore_selector.js
@@ -1,4 +1,5 @@
import FileTemplateSelector from '../file_template_selector';
+import initDeprecatedJQueryDropdown from '~/deprecated_jquery_dropdown';
export default class BlobGitignoreSelector extends FileTemplateSelector {
constructor({ mediator }) {
@@ -14,7 +15,7 @@ export default class BlobGitignoreSelector extends FileTemplateSelector {
}
initDropdown() {
- this.$dropdown.glDropdown({
+ initDeprecatedJQueryDropdown(this.$dropdown, {
data: this.$dropdown.data('data'),
filterable: true,
selectable: true,
diff --git a/app/assets/javascripts/blob/template_selectors/license_selector.js b/app/assets/javascripts/blob/template_selectors/license_selector.js
index f4041835a7d..dafde82b1e0 100644
--- a/app/assets/javascripts/blob/template_selectors/license_selector.js
+++ b/app/assets/javascripts/blob/template_selectors/license_selector.js
@@ -1,4 +1,5 @@
import FileTemplateSelector from '../file_template_selector';
+import initDeprecatedJQueryDropdown from '~/deprecated_jquery_dropdown';
export default class BlobLicenseSelector extends FileTemplateSelector {
constructor({ mediator }) {
@@ -14,7 +15,7 @@ export default class BlobLicenseSelector extends FileTemplateSelector {
}
initDropdown() {
- this.$dropdown.glDropdown({
+ initDeprecatedJQueryDropdown(this.$dropdown, {
data: this.$dropdown.data('data'),
filterable: true,
selectable: true,
diff --git a/app/assets/javascripts/blob/template_selectors/metrics_dashboard_selector.js b/app/assets/javascripts/blob/template_selectors/metrics_dashboard_selector.js
index b4accaadfa3..9e698bfea5d 100644
--- a/app/assets/javascripts/blob/template_selectors/metrics_dashboard_selector.js
+++ b/app/assets/javascripts/blob/template_selectors/metrics_dashboard_selector.js
@@ -1,4 +1,5 @@
import FileTemplateSelector from '../file_template_selector';
+import initDeprecatedJQueryDropdown from '~/deprecated_jquery_dropdown';
export default class MetricsDashboardSelector extends FileTemplateSelector {
constructor({ mediator }) {
@@ -14,7 +15,7 @@ export default class MetricsDashboardSelector extends FileTemplateSelector {
}
initDropdown() {
- this.$dropdown.glDropdown({
+ initDeprecatedJQueryDropdown(this.$dropdown, {
data: this.$dropdown.data('data'),
filterable: true,
selectable: true,
diff --git a/app/assets/javascripts/blob/template_selectors/type_selector.js b/app/assets/javascripts/blob/template_selectors/type_selector.js
index cb4e1aaa9ac..01625911815 100644
--- a/app/assets/javascripts/blob/template_selectors/type_selector.js
+++ b/app/assets/javascripts/blob/template_selectors/type_selector.js
@@ -1,4 +1,5 @@
import FileTemplateSelector from '../file_template_selector';
+import initDeprecatedJQueryDropdown from '~/deprecated_jquery_dropdown';
export default class FileTemplateTypeSelector extends FileTemplateSelector {
constructor({ mediator, dropdownData }) {
@@ -12,7 +13,7 @@ export default class FileTemplateTypeSelector extends FileTemplateSelector {
}
initDropdown() {
- this.$dropdown.glDropdown({
+ initDeprecatedJQueryDropdown(this.$dropdown, {
data: this.config.dropdownData,
filterable: false,
selectable: true,
diff --git a/app/assets/javascripts/blob/utils.js b/app/assets/javascripts/blob/utils.js
index a0211c8bb8e..8043c0bbc07 100644
--- a/app/assets/javascripts/blob/utils.js
+++ b/app/assets/javascripts/blob/utils.js
@@ -1,20 +1,16 @@
import Editor from '~/editor/editor_lite';
export function initEditorLite({ el, ...args }) {
- if (!el) {
- throw new Error(`"el" parameter is required to initialize Editor`);
- }
const editor = new Editor({
scrollbar: {
alwaysConsumeMouseWheel: false,
},
});
- editor.createInstance({
+
+ return editor.createInstance({
el,
...args,
});
-
- return editor;
}
export default () => ({});
diff --git a/app/assets/javascripts/blob_edit/blob_bundle.js b/app/assets/javascripts/blob_edit/blob_bundle.js
index 9b9ade28623..c9972f0b43c 100644
--- a/app/assets/javascripts/blob_edit/blob_bundle.js
+++ b/app/assets/javascripts/blob_edit/blob_bundle.js
@@ -7,12 +7,14 @@ import BlobFileDropzone from '../blob/blob_file_dropzone';
import initPopover from '~/blob/suggest_gitlab_ci_yml';
import { disableButtonIfEmptyField, setCookie } from '~/lib/utils/common_utils';
import Tracking from '~/tracking';
+import initWebIdeAlert from '~/blob/suggest_web_ide_ci';
export default () => {
const editBlobForm = $('.js-edit-blob-form');
const uploadBlobForm = $('.js-upload-blob-form');
const deleteBlobForm = $('.js-delete-blob-form');
const suggestEl = document.querySelector('.js-suggest-gitlab-ci-yml');
+ const alertEl = document.getElementById('js-suggest-web-ide-ci');
if (editBlobForm.length) {
const urlRoot = editBlobForm.data('relativeUrlRoot');
@@ -65,12 +67,15 @@ export default () => {
if (commitButton) {
const { dismissKey, humanAccess } = suggestEl.dataset;
+ const urlParams = new URLSearchParams(window.location.search);
+ const mergeRequestPath = urlParams.get('mr_path') || true;
+
const commitCookieName = `suggest_gitlab_ci_yml_commit_${dismissKey}`;
const commitTrackLabel = 'suggest_gitlab_ci_yml_commit_changes';
const commitTrackValue = '20';
commitButton.addEventListener('click', () => {
- setCookie(commitCookieName, true);
+ setCookie(commitCookieName, mergeRequestPath);
Tracking.event(undefined, 'click_button', {
label: commitTrackLabel,
@@ -80,4 +85,8 @@ export default () => {
});
}
}
+
+ if (alertEl) {
+ initWebIdeAlert(alertEl);
+ }
};
diff --git a/app/assets/javascripts/blob_edit/edit_blob.js b/app/assets/javascripts/blob_edit/edit_blob.js
index e22c9b0d4c4..2a4ab4b8827 100644
--- a/app/assets/javascripts/blob_edit/edit_blob.js
+++ b/app/assets/javascripts/blob_edit/edit_blob.js
@@ -40,9 +40,10 @@ export default class EditBlob {
const MarkdownExtensionPromise = this.options.isMarkdown
? import('~/editor/editor_markdown_ext')
: Promise.resolve(false);
+ const FileTemplateExtensionPromise = import('~/editor/editor_file_template_ext');
- return Promise.all([EditorPromise, MarkdownExtensionPromise])
- .then(([EditorModule, MarkdownExtension]) => {
+ return Promise.all([EditorPromise, MarkdownExtensionPromise, FileTemplateExtensionPromise])
+ .then(([EditorModule, MarkdownExtension, FileTemplateExtension]) => {
const EditorLite = EditorModule.default;
const editorEl = document.getElementById('editor');
const fileNameEl =
@@ -50,18 +51,16 @@ export default class EditBlob {
const fileContentEl = document.getElementById('file-content');
const form = document.querySelector('.js-edit-blob-form');
- this.editor = new EditorLite();
+ const rootEditor = new EditorLite();
- if (MarkdownExtension) {
- this.editor.use(MarkdownExtension.default);
- }
-
- this.editor.createInstance({
+ this.editor = rootEditor.createInstance({
el: editorEl,
blobPath: fileNameEl.value,
blobContent: editorEl.innerText,
});
+ rootEditor.use([MarkdownExtension.default, FileTemplateExtension.default], this.editor);
+
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 384a386d69c..5c8df94ca90 100644
--- a/app/assets/javascripts/boards/boards_util.js
+++ b/app/assets/javascripts/boards/boards_util.js
@@ -1,28 +1,72 @@
+import { sortBy } from 'lodash';
import ListIssue from 'ee_else_ce/boards/models/issue';
+import { ListType } from './constants';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
export function getMilestone() {
return null;
}
+export function formatIssue(issue) {
+ return new ListIssue({
+ ...issue,
+ labels: issue.labels?.nodes || [],
+ assignees: issue.assignees?.nodes || [],
+ });
+}
+
export function formatListIssues(listIssues) {
- return listIssues.nodes.reduce((map, list) => {
+ const issues = {};
+
+ const listData = listIssues.nodes.reduce((map, list) => {
+ const sortedIssues = sortBy(list.issues.nodes, 'relativePosition');
return {
...map,
- [list.id]: list.issues.nodes.map(
- i =>
- new ListIssue({
- ...i,
- id: getIdFromGraphQLId(i.id),
- labels: i.labels?.nodes || [],
- assignees: i.assignees?.nodes || [],
- }),
- ),
+ [list.id]: sortedIssues.map(i => {
+ const id = getIdFromGraphQLId(i.id);
+
+ const listIssue = new ListIssue({
+ ...i,
+ id,
+ labels: i.labels?.nodes || [],
+ assignees: i.assignees?.nodes || [],
+ });
+
+ issues[id] = listIssue;
+
+ return id;
+ }),
};
}, {});
+
+ return { listData, issues };
+}
+
+export function fullBoardId(boardId) {
+ return `gid://gitlab/Board/${boardId}`;
+}
+
+export function moveIssueListHelper(issue, fromList, toList) {
+ if (toList.type === ListType.label) {
+ issue.addLabel(toList.label);
+ }
+ if (fromList && fromList.type === ListType.label) {
+ issue.removeLabel(fromList.label);
+ }
+
+ if (toList.type === ListType.assignee) {
+ issue.addAssignee(toList.assignee);
+ }
+ if (fromList && fromList.type === ListType.assignee) {
+ issue.removeAssignee(fromList.assignee);
+ }
+
+ return issue;
}
export default {
getMilestone,
+ formatIssue,
formatListIssues,
+ fullBoardId,
};
diff --git a/app/assets/javascripts/boards/components/board_blank_state.vue b/app/assets/javascripts/boards/components/board_blank_state.vue
index afdf0290e8e..55e3e4a6329 100644
--- a/app/assets/javascripts/boards/components/board_blank_state.vue
+++ b/app/assets/javascripts/boards/components/board_blank_state.vue
@@ -1,10 +1,14 @@
<script>
+import { GlButton } from '@gitlab/ui';
import Cookies from 'js-cookie';
import { __ } from '~/locale';
import ListLabel from '~/boards/models/label';
import boardsStore from '../stores/boards_store';
export default {
+ components: {
+ GlButton,
+ },
data() {
return {
predefinedLabels: [
@@ -84,15 +88,17 @@ export default {
)
}}
</p>
- <button
- class="btn btn-success btn-inverted btn-block"
- type="button"
+ <gl-button
+ category="secondary"
+ variant="success"
+ block="block"
+ class="gl-mb-0"
@click.stop="addDefaultLists"
>
{{ s__('BoardBlankState|Add default lists') }}
- </button>
- <button class="btn btn-default btn-block" type="button" @click.stop="clearBlankState">
+ </gl-button>
+ <gl-button category="secondary" variant="default" block="block" @click.stop="clearBlankState">
{{ s__("BoardBlankState|Nevermind, I'll use my own") }}
- </button>
+ </gl-button>
</div>
</template>
diff --git a/app/assets/javascripts/boards/components/board_card.vue b/app/assets/javascripts/boards/components/board_card.vue
index 246d3b9dcd1..31050eef83d 100644
--- a/app/assets/javascripts/boards/components/board_card.vue
+++ b/app/assets/javascripts/boards/components/board_card.vue
@@ -1,6 +1,5 @@
<script>
-/* eslint-disable vue/require-default-prop */
-import IssueCardInner from './issue_card_inner.vue';
+import BoardCardLayout from './board_card_layout.vue';
import eventHub from '../eventhub';
import sidebarEventHub from '~/sidebar/event_hub';
import boardsStore from '../stores/boards_store';
@@ -8,7 +7,7 @@ import boardsStore from '../stores/boards_store';
export default {
name: 'BoardsIssueCard',
components: {
- IssueCardInner,
+ BoardCardLayout,
},
props: {
list: {
@@ -21,80 +20,29 @@ export default {
default: () => ({}),
required: false,
},
- issueLinkBase: {
- type: String,
- default: '',
- required: false,
- },
- disabled: {
- type: Boolean,
- default: false,
- required: false,
- },
- index: {
- type: Number,
- default: 0,
- required: false,
- },
- rootPath: {
- type: String,
- default: '',
- required: false,
- },
- groupId: {
- type: Number,
- required: false,
- },
- },
- data() {
- return {
- showDetail: false,
- detailIssue: boardsStore.detail,
- multiSelect: boardsStore.multiSelect,
- };
- },
- computed: {
- issueDetailVisible() {
- return this.detailIssue.issue && this.detailIssue.issue.id === this.issue.id;
- },
- multiSelectVisible() {
- return this.multiSelect.list.findIndex(issue => issue.id === this.issue.id) > -1;
- },
- canMultiSelect() {
- return gon.features && gon.features.multiSelectBoard;
- },
},
methods: {
- mouseDown() {
- this.showDetail = true;
+ // These are methods instead of computed's, because boardsStore is not reactive.
+ isActive() {
+ return this.getActiveId() === this.issue.id;
},
- mouseMove() {
- this.showDetail = false;
+ getActiveId() {
+ return boardsStore.detail?.issue?.id;
},
- showIssue(e) {
- if (e.target.classList.contains('js-no-trigger')) return;
-
+ showIssue({ isMultiSelect }) {
// If no issues are opened, close all sidebars first
- if (!boardsStore.detail?.issue?.id) {
+ if (!this.getActiveId()) {
sidebarEventHub.$emit('sidebar.closeAll');
}
+ if (this.isActive()) {
+ eventHub.$emit('clearDetailIssue', isMultiSelect);
- // If CMD or CTRL is clicked
- const isMultiSelect = this.canMultiSelect && (e.ctrlKey || e.metaKey);
-
- if (this.showDetail || isMultiSelect) {
- this.showDetail = false;
-
- if (boardsStore.detail.issue && boardsStore.detail.issue.id === this.issue.id) {
- eventHub.$emit('clearDetailIssue', isMultiSelect);
-
- if (isMultiSelect) {
- eventHub.$emit('newDetailIssue', this.issue, isMultiSelect);
- }
- } else {
+ if (isMultiSelect) {
eventHub.$emit('newDetailIssue', this.issue, isMultiSelect);
- boardsStore.setListDetail(this.list);
}
+ } else {
+ eventHub.$emit('newDetailIssue', this.issue, isMultiSelect);
+ boardsStore.setListDetail(this.list);
}
},
},
@@ -102,28 +50,12 @@ export default {
</script>
<template>
- <li
- :class="{
- 'multi-select': multiSelectVisible,
- 'user-can-drag': !disabled && issue.id,
- 'is-disabled': disabled || !issue.id,
- 'is-active': issueDetailVisible,
- }"
- :index="index"
- :data-issue-id="issue.id"
+ <board-card-layout
data-qa-selector="board_card"
- class="board-card p-3 rounded"
- @mousedown="mouseDown"
- @mousemove="mouseMove"
- @mouseup="showIssue($event)"
- >
- <issue-card-inner
- :list="list"
- :issue="issue"
- :issue-link-base="issueLinkBase"
- :group-id="groupId"
- :root-path="rootPath"
- :update-filters="true"
- />
- </li>
+ :issue="issue"
+ :list="list"
+ :is-active="isActive()"
+ v-bind="$attrs"
+ @show="showIssue"
+ />
</template>
diff --git a/app/assets/javascripts/boards/components/board_card_layout.vue b/app/assets/javascripts/boards/components/board_card_layout.vue
new file mode 100644
index 00000000000..072dd87861a
--- /dev/null
+++ b/app/assets/javascripts/boards/components/board_card_layout.vue
@@ -0,0 +1,93 @@
+<script>
+import IssueCardInner from './issue_card_inner.vue';
+import boardsStore from '../stores/boards_store';
+
+export default {
+ name: 'BoardsIssueCard',
+ components: {
+ IssueCardInner,
+ },
+ props: {
+ list: {
+ type: Object,
+ default: () => ({}),
+ required: false,
+ },
+ issue: {
+ type: Object,
+ default: () => ({}),
+ required: false,
+ },
+ disabled: {
+ type: Boolean,
+ default: false,
+ required: false,
+ },
+ index: {
+ type: Number,
+ default: 0,
+ required: false,
+ },
+ isActive: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ },
+ data() {
+ return {
+ showDetail: false,
+ multiSelect: boardsStore.multiSelect,
+ };
+ },
+ computed: {
+ multiSelectVisible() {
+ return this.multiSelect.list.findIndex(issue => issue.id === this.issue.id) > -1;
+ },
+ canMultiSelect() {
+ return gon.features && gon.features.multiSelectBoard;
+ },
+ },
+ methods: {
+ mouseDown() {
+ this.showDetail = true;
+ },
+ mouseMove() {
+ this.showDetail = false;
+ },
+ showIssue(e) {
+ // Don't do anything if this happened on a no trigger element
+ if (e.target.classList.contains('js-no-trigger')) return;
+
+ const isMultiSelect = this.canMultiSelect && (e.ctrlKey || e.metaKey);
+
+ if (this.showDetail || isMultiSelect) {
+ this.showDetail = false;
+ this.$emit('show', { event: e, isMultiSelect });
+ }
+ },
+ },
+};
+</script>
+
+<template>
+ <li
+ :class="{
+ 'multi-select': multiSelectVisible,
+ 'user-can-drag': !disabled && issue.id,
+ 'is-disabled': disabled || !issue.id,
+ 'is-active': isActive,
+ }"
+ :index="index"
+ :data-issue-id="issue.id"
+ :data-issue-iid="issue.iid"
+ :data-issue-path="issue.referencePath"
+ data-testid="board_card"
+ class="board-card p-3 rounded"
+ @mousedown="mouseDown"
+ @mousemove="mouseMove"
+ @mouseup="showIssue($event)"
+ >
+ <issue-card-inner :list="list" :issue="issue" :update-filters="true" />
+ </li>
+</template>
diff --git a/app/assets/javascripts/boards/components/board_column.vue b/app/assets/javascripts/boards/components/board_column.vue
index dae24338e45..6d216911798 100644
--- a/app/assets/javascripts/boards/components/board_column.vue
+++ b/app/assets/javascripts/boards/components/board_column.vue
@@ -1,9 +1,11 @@
<script>
+import { mapGetters, mapActions } from 'vuex';
import Sortable from 'sortablejs';
import isWipLimitsOn from 'ee_else_ce/boards/mixins/is_wip_limits';
import BoardListHeader from 'ee_else_ce/boards/components/board_list_header.vue';
import Tooltip from '~/vue_shared/directives/tooltip';
import EmptyComponent from '~/vue_shared/components/empty_component';
+import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import BoardBlankState from './board_blank_state.vue';
import BoardList from './board_list.vue';
import boardsStore from '../stores/boards_store';
@@ -21,7 +23,7 @@ export default {
directives: {
Tooltip,
},
- mixins: [isWipLimitsOn],
+ mixins: [isWipLimitsOn, glFeatureFlagMixin()],
props: {
list: {
type: Object,
@@ -32,27 +34,15 @@ export default {
type: Boolean,
required: true,
},
- issueLinkBase: {
- type: String,
- required: true,
- },
- rootPath: {
- type: String,
- required: true,
- },
- boardId: {
- type: String,
- required: true,
- },
canAdminList: {
type: Boolean,
required: false,
default: false,
},
- groupId: {
- type: Number,
- required: false,
- default: null,
+ },
+ inject: {
+ boardId: {
+ type: String,
},
},
data() {
@@ -62,6 +52,7 @@ export default {
};
},
computed: {
+ ...mapGetters(['getIssues']),
showBoardListAndBoardInfo() {
return this.list.type !== ListType.blank && this.list.type !== ListType.promotion;
},
@@ -69,19 +60,36 @@ export default {
// eslint-disable-next-line @gitlab/require-i18n-strings
return `boards.${this.boardId}.${this.list.type}.${this.list.id}`;
},
+ listIssues() {
+ if (!this.glFeatures.graphqlBoardLists) {
+ return this.list.issues;
+ }
+ return this.getIssues(this.list.id);
+ },
+ shouldFetchIssues() {
+ return this.glFeatures.graphqlBoardLists && this.list.type !== ListType.blank;
+ },
},
watch: {
filter: {
handler() {
- this.list.page = 1;
- this.list.getIssues(true).catch(() => {
- // TODO: handle request error
- });
+ if (this.shouldFetchIssues) {
+ this.fetchIssuesForList(this.list.id);
+ } else {
+ this.list.page = 1;
+ this.list.getIssues(true).catch(() => {
+ // TODO: handle request error
+ });
+ }
},
deep: true,
},
},
mounted() {
+ if (this.shouldFetchIssues) {
+ this.fetchIssuesForList(this.list.id);
+ }
+
const instance = this;
const sortableOptions = getBoardSortableDefaultOptions({
@@ -108,6 +116,7 @@ export default {
Sortable.create(this.$el.parentNode, sortableOptions);
},
methods: {
+ ...mapActions(['fetchIssuesForList']),
showListNewIssueForm(listId) {
eventHub.$emit('showForm', listId);
},
@@ -130,22 +139,14 @@ export default {
<div
class="board-inner gl-display-flex gl-flex-direction-column gl-relative gl-h-full gl-rounded-base"
>
- <board-list-header
- :can-admin-list="canAdminList"
- :list="list"
- :disabled="disabled"
- :board-id="boardId"
- />
+ <board-list-header :can-admin-list="canAdminList" :list="list" :disabled="disabled" />
<board-list
v-if="showBoardListAndBoardInfo"
ref="board-list"
:disabled="disabled"
- :group-id="groupId || null"
- :issue-link-base="issueLinkBase"
- :issues="list.issues"
+ :issues="listIssues"
:list="list"
:loading="list.loading"
- :root-path="rootPath"
/>
<board-blank-state v-if="canAdminList && list.id === 'blank'" />
diff --git a/app/assets/javascripts/boards/components/board_content.vue b/app/assets/javascripts/boards/components/board_content.vue
index c42295792f1..c7b3da0e672 100644
--- a/app/assets/javascripts/boards/components/board_content.vue
+++ b/app/assets/javascripts/boards/components/board_content.vue
@@ -1,13 +1,15 @@
<script>
-import { mapState } from 'vuex';
+import { mapState, mapGetters, mapActions } from 'vuex';
import BoardColumn from 'ee_else_ce/boards/components/board_column.vue';
-import EpicsSwimlanes from 'ee_component/boards/components/epics_swimlanes.vue';
+import { GlAlert } from '@gitlab/ui';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
export default {
components: {
BoardColumn,
- EpicsSwimlanes,
+ BoardContentSidebar: () => import('ee_component/boards/components/board_content_sidebar.vue'),
+ EpicsSwimlanes: () => import('ee_component/boards/components/epics_swimlanes.vue'),
+ GlAlert,
},
mixins: [glFeatureFlagMixin()],
props: {
@@ -19,66 +21,58 @@ export default {
type: Boolean,
required: true,
},
- groupId: {
- type: Number,
- required: false,
- default: null,
- },
disabled: {
type: Boolean,
required: true,
},
- issueLinkBase: {
- type: String,
- required: true,
- },
- rootPath: {
- type: String,
- required: true,
- },
- boardId: {
- type: String,
- required: true,
- },
},
computed: {
- ...mapState(['isShowingEpicsSwimlanes', 'boardLists']),
- isSwimlanesOn() {
- return this.glFeatures.boardsWithSwimlanes && this.isShowingEpicsSwimlanes;
+ ...mapState(['boardLists', 'error']),
+ ...mapGetters(['isSwimlanesOn']),
+ boardListsToUse() {
+ return this.glFeatures.graphqlBoardLists ? this.boardLists : this.lists;
},
},
+ mounted() {
+ if (this.glFeatures.graphqlBoardLists) {
+ this.fetchLists();
+ this.showPromotionList();
+ }
+ },
+ methods: {
+ ...mapActions(['fetchLists', 'showPromotionList']),
+ },
};
</script>
<template>
<div>
+ <gl-alert v-if="error" variant="danger" :dismissible="false">
+ {{ error }}
+ </gl-alert>
<div
v-if="!isSwimlanesOn"
class="boards-list gl-w-full gl-py-5 gl-px-3 gl-white-space-nowrap"
data-qa-selector="boards_list"
>
<board-column
- v-for="list in lists"
+ v-for="list in boardListsToUse"
:key="list.id"
ref="board"
:can-admin-list="canAdminList"
- :group-id="groupId"
:list="list"
:disabled="disabled"
- :issue-link-base="issueLinkBase"
- :root-path="rootPath"
- :board-id="boardId"
/>
</div>
- <epics-swimlanes
- v-else
- ref="swimlanes"
- :lists="boardLists"
- :can-admin-list="canAdminList"
- :disabled="disabled"
- :board-id="boardId"
- :group-id="groupId"
- :root-path="rootPath"
- />
+
+ <template v-else>
+ <epics-swimlanes
+ ref="swimlanes"
+ :lists="boardLists"
+ :can-admin-list="canAdminList"
+ :disabled="disabled"
+ />
+ <board-content-sidebar />
+ </template>
</div>
</template>
diff --git a/app/assets/javascripts/boards/components/board_form.vue b/app/assets/javascripts/boards/components/board_form.vue
index 231059b895e..385dd5fdc71 100644
--- a/app/assets/javascripts/boards/components/board_form.vue
+++ b/app/assets/javascripts/boards/components/board_form.vue
@@ -25,11 +25,11 @@ export default {
type: Boolean,
required: true,
},
- milestonePath: {
+ labelsPath: {
type: String,
required: true,
},
- labelsPath: {
+ labelsWebUrl: {
type: String,
required: true,
},
@@ -201,8 +201,8 @@ export default {
:collapse-scope="isNewForm"
:board="board"
:can-admin-board="canAdminBoard"
- :milestone-path="milestonePath"
:labels-path="labelsPath"
+ :labels-web-url="labelsWebUrl"
:enable-scoped-labels="enableScopedLabels"
:project-id="projectId"
:group-id="groupId"
diff --git a/app/assets/javascripts/boards/components/board_list.vue b/app/assets/javascripts/boards/components/board_list.vue
index 1a26782f6f0..25f8ffca633 100644
--- a/app/assets/javascripts/boards/components/board_list.vue
+++ b/app/assets/javascripts/boards/components/board_list.vue
@@ -6,6 +6,7 @@ import boardCard from './board_card.vue';
import eventHub from '../eventhub';
import boardsStore from '../stores/boards_store';
import { sprintf, __ } from '~/locale';
+import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { deprecatedCreateFlash as createFlash } from '~/flash';
import {
getBoardSortableDefaultOptions,
@@ -24,12 +25,8 @@ export default {
boardNewIssue,
GlLoadingIcon,
},
+ mixins: [glFeatureFlagMixin()],
props: {
- groupId: {
- type: Number,
- required: false,
- default: 0,
- },
disabled: {
type: Boolean,
required: true,
@@ -46,14 +43,6 @@ export default {
type: Boolean,
required: true,
},
- issueLinkBase: {
- type: String,
- required: true,
- },
- rootPath: {
- type: String,
- required: true,
- },
},
data() {
return {
@@ -83,6 +72,7 @@ export default {
deep: true,
},
issues() {
+ if (this.glFeatures.graphqlBoardLists) return;
this.$nextTick(() => {
if (
this.scrollHeight() <= this.listHeight() &&
@@ -413,6 +403,8 @@ export default {
this.showIssueForm = !this.showIssueForm;
},
onScroll() {
+ if (this.glFeatures.graphqlBoardLists) return;
+
if (!this.list.loadingMore && this.scrollTop() > this.scrollHeight() - this.scrollOffset) {
this.loadNextPage();
}
@@ -430,11 +422,7 @@ export default {
<div v-if="loading" class="board-list-loading text-center" :aria-label="__('Loading issues')">
<gl-loading-icon />
</div>
- <board-new-issue
- v-if="list.type !== 'closed' && showIssueForm"
- :group-id="groupId"
- :list="list"
- />
+ <board-new-issue v-if="list.type !== 'closed' && showIssueForm" :list="list" />
<ul
v-show="!loading"
ref="list"
@@ -450,9 +438,6 @@ export default {
:index="index"
:list="list"
:issue="issue"
- :issue-link-base="issueLinkBase"
- :group-id="groupId"
- :root-path="rootPath"
:disabled="disabled"
/>
<li v-if="showCount" class="board-list-count text-center" data-issue-id="-1">
diff --git a/app/assets/javascripts/boards/components/board_list_header.vue b/app/assets/javascripts/boards/components/board_list_header.vue
index bafe07afb48..361fe252afb 100644
--- a/app/assets/javascripts/boards/components/board_list_header.vue
+++ b/app/assets/javascripts/boards/components/board_list_header.vue
@@ -1,4 +1,5 @@
<script>
+import { mapActions } from 'vuex';
import {
GlButton,
GlButtonGroup,
@@ -17,6 +18,7 @@ import boardsStore from '../stores/boards_store';
import eventHub from '../eventhub';
import { ListType } from '../constants';
import { isScopedLabel } from '~/lib/utils/common_utils';
+import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
export default {
components: {
@@ -32,7 +34,7 @@ export default {
directives: {
GlTooltip: GlTooltipDirective,
},
- mixins: [isWipLimitsOn],
+ mixins: [isWipLimitsOn, glFeatureFlagMixin()],
props: {
list: {
type: Object,
@@ -43,10 +45,6 @@ export default {
type: Boolean,
required: true,
},
- boardId: {
- type: String,
- required: true,
- },
canAdminList: {
type: Boolean,
required: false,
@@ -58,6 +56,11 @@ export default {
default: false,
},
},
+ inject: {
+ boardId: {
+ type: String,
+ },
+ },
data() {
return {
weightFeatureAvailable: false,
@@ -94,10 +97,11 @@ export default {
showAssigneeListDetails() {
return this.list.type === 'assignee' && (this.list.isExpanded || !this.isSwimlanesHeader);
},
+ issuesCount() {
+ return this.list.issuesSize;
+ },
issuesTooltipLabel() {
- const { issuesSize } = this.list;
-
- return n__(`%d issue`, `%d issues`, issuesSize);
+ return n__(`%d issue`, `%d issues`, this.issuesCount);
},
chevronTooltip() {
return this.list.isExpanded ? s__('Boards|Collapse') : s__('Boards|Expand');
@@ -126,8 +130,12 @@ export default {
collapsedTooltipTitle() {
return this.listTitle || this.listAssignee;
},
+ shouldDisplaySwimlanes() {
+ return this.glFeatures.boardsWithSwimlanes && this.isSwimlanesOn;
+ },
},
methods: {
+ ...mapActions(['updateList']),
showScopedLabels(label) {
return boardsStore.scopedLabels.enabled && isScopedLabel(label);
},
@@ -136,20 +144,28 @@ export default {
eventHub.$emit(`toggle-issue-form-${this.list.id}`);
},
toggleExpanded() {
- if (this.list.isExpandable) {
- this.list.isExpanded = !this.list.isExpanded;
-
- if (AccessorUtilities.isLocalStorageAccessSafe() && !this.isLoggedIn) {
- localStorage.setItem(`${this.uniqueKey}.expanded`, this.list.isExpanded);
- }
+ this.list.isExpanded = !this.list.isExpanded;
- if (this.isLoggedIn) {
- this.list.update();
- }
+ if (!this.isLoggedIn) {
+ this.addToLocalStorage();
+ } else {
+ this.updateListFunction();
+ }
- // When expanding/collapsing, the tooltip on the caret button sometimes stays open.
- // Close all tooltips manually to prevent dangling tooltips.
- this.$root.$emit('bv::hide::tooltip');
+ // When expanding/collapsing, the tooltip on the caret button sometimes stays open.
+ // Close all tooltips manually to prevent dangling tooltips.
+ this.$root.$emit('bv::hide::tooltip');
+ },
+ addToLocalStorage() {
+ if (AccessorUtilities.isLocalStorageAccessSafe()) {
+ localStorage.setItem(`${this.uniqueKey}.expanded`, this.list.isExpanded);
+ }
+ },
+ updateListFunction() {
+ if (this.shouldDisplaySwimlanes || this.glFeatures.graphqlBoardLists) {
+ this.updateList({ listId: this.list.id, collapsed: !this.list.isExpanded });
+ } else {
+ this.list.update();
}
},
},
@@ -172,7 +188,7 @@ export default {
<h3
:class="{
'user-can-drag': !disabled && !list.preset,
- 'gl-py-3': !list.isExpanded && !isSwimlanesHeader,
+ 'gl-py-3 gl-h-full': !list.isExpanded && !isSwimlanesHeader,
'gl-border-b-0': !list.isExpanded || isSwimlanesHeader,
'gl-py-2': !list.isExpanded && isSwimlanesHeader,
}"
@@ -288,7 +304,7 @@ export default {
<gl-tooltip :target="() => $refs.issueCount" :title="issuesTooltipLabel" />
<span ref="issueCount" class="issue-count-badge-count">
<gl-icon class="gl-mr-2" name="issues" />
- <issue-count :issues-size="list.issuesSize" :max-issue-count="list.maxIssueCount" />
+ <issue-count :issues-size="issuesCount" :max-issue-count="list.maxIssueCount" />
</span>
<!-- The following is only true in EE. -->
<template v-if="weightFeatureAvailable">
diff --git a/app/assets/javascripts/boards/components/board_new_issue.vue b/app/assets/javascripts/boards/components/board_new_issue.vue
index 34e8438ba4c..348d485ff37 100644
--- a/app/assets/javascripts/boards/components/board_new_issue.vue
+++ b/app/assets/javascripts/boards/components/board_new_issue.vue
@@ -1,11 +1,13 @@
<script>
import $ from 'jquery';
+import { mapActions, mapGetters } from 'vuex';
import { GlButton } from '@gitlab/ui';
import { getMilestone } from 'ee_else_ce/boards/boards_util';
import ListIssue from 'ee_else_ce/boards/models/issue';
import eventHub from '../eventhub';
import ProjectSelect from './project_select.vue';
import boardsStore from '../stores/boards_store';
+import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
export default {
name: 'BoardNewIssue',
@@ -13,17 +15,18 @@ export default {
ProjectSelect,
GlButton,
},
+ mixins: [glFeatureFlagMixin()],
props: {
- groupId: {
- type: Number,
- required: false,
- default: 0,
- },
list: {
type: Object,
required: true,
},
},
+ inject: {
+ groupId: {
+ type: Number,
+ },
+ },
data() {
return {
title: '',
@@ -32,18 +35,23 @@ export default {
};
},
computed: {
+ ...mapGetters(['isSwimlanesOn']),
disabled() {
if (this.groupId) {
return this.title === '' || !this.selectedProject.name;
}
return this.title === '';
},
+ shouldDisplaySwimlanes() {
+ return this.glFeatures.boardsWithSwimlanes && this.isSwimlanesOn;
+ },
},
mounted() {
this.$refs.input.focus();
eventHub.$on('setSelectedProject', this.setSelectedProject);
},
methods: {
+ ...mapActions(['addListIssue', 'addListIssueFailure']),
submit(e) {
e.preventDefault();
if (this.title.trim() === '') return Promise.resolve();
@@ -70,21 +78,31 @@ export default {
eventHub.$emit(`scroll-board-list-${this.list.id}`);
this.cancel();
+ if (this.shouldDisplaySwimlanes || this.glFeatures.graphqlBoardLists) {
+ this.addListIssue({ list: this.list, issue, position: 0 });
+ }
+
return this.list
.newIssue(issue)
.then(() => {
// Need this because our jQuery very kindly disables buttons on ALL form submissions
$(this.$refs.submitButton).enable();
- boardsStore.setIssueDetail(issue);
- boardsStore.setListDetail(this.list);
+ if (!this.shouldDisplaySwimlanes && !this.glFeatures.graphqlBoardLists) {
+ boardsStore.setIssueDetail(issue);
+ boardsStore.setListDetail(this.list);
+ }
})
.catch(() => {
// Need this because our jQuery very kindly disables buttons on ALL form submissions
$(this.$refs.submitButton).enable();
// Remove the issue
- this.list.removeIssue(issue);
+ if (this.shouldDisplaySwimlanes || this.glFeatures.graphqlBoardLists) {
+ this.addListIssueFailure({ list: this.list, issue });
+ } else {
+ this.list.removeIssue(issue);
+ }
// Show error message
this.error = true;
@@ -121,7 +139,7 @@ export default {
<project-select v-if="groupId" :group-id="groupId" :list="list" />
<div class="clearfix gl-mt-3">
<gl-button
- ref="submit-button"
+ ref="submitButton"
:disabled="disabled"
class="float-left"
variant="success"
@@ -129,9 +147,14 @@ export default {
type="submit"
>{{ __('Submit issue') }}</gl-button
>
- <gl-button class="float-right" type="button" variant="default" @click="cancel">{{
- __('Cancel')
- }}</gl-button>
+ <gl-button
+ ref="cancelButton"
+ class="float-right"
+ type="button"
+ variant="default"
+ @click="cancel"
+ >{{ __('Cancel') }}</gl-button
+ >
</div>
</form>
</div>
diff --git a/app/assets/javascripts/boards/components/board_settings_sidebar.vue b/app/assets/javascripts/boards/components/board_settings_sidebar.vue
index 3149762ecdf..e2600883e89 100644
--- a/app/assets/javascripts/boards/components/board_settings_sidebar.vue
+++ b/app/assets/javascripts/boards/components/board_settings_sidebar.vue
@@ -1,11 +1,12 @@
<script>
import { GlDrawer, GlLabel } from '@gitlab/ui';
-import { mapActions, mapState } from 'vuex';
+import { mapActions, mapState, mapGetters } from 'vuex';
import { __ } from '~/locale';
import boardsStore from '~/boards/stores/boards_store';
import eventHub from '~/sidebar/event_hub';
import { isScopedLabel } from '~/lib/utils/common_utils';
-import { inactiveId } from '~/boards/constants';
+import { LIST } from '~/boards/constants';
+import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
// NOTE: need to revisit how we handle headerHeight, because we have so many different header and footer options.
export default {
@@ -23,18 +24,20 @@ export default {
BoardSettingsListTypes: () =>
import('ee_component/boards/components/board_settings_list_types.vue'),
},
+ mixins: [glFeatureFlagMixin()],
computed: {
- ...mapState(['activeId']),
+ ...mapGetters(['isSidebarOpen']),
+ ...mapState(['activeId', 'sidebarType', 'boardLists']),
activeList() {
/*
Warning: Though a computed property it is not reactive because we are
referencing a List Model class. Reactivity only applies to plain JS objects
*/
+ if (this.glFeatures.graphqlBoardLists) {
+ return this.boardLists.find(({ id }) => id === this.activeId);
+ }
return boardsStore.state.lists.find(({ id }) => id === this.activeId);
},
- isSidebarOpen() {
- return this.activeId !== inactiveId;
- },
activeListLabel() {
return this.activeList.label;
},
@@ -44,18 +47,18 @@ export default {
listTypeTitle() {
return this.$options.labelListText;
},
+ showSidebar() {
+ return this.sidebarType === LIST;
+ },
},
created() {
- eventHub.$on('sidebar.closeAll', this.closeSidebar);
+ eventHub.$on('sidebar.closeAll', this.unsetActiveId);
},
beforeDestroy() {
- eventHub.$off('sidebar.closeAll', this.closeSidebar);
+ eventHub.$off('sidebar.closeAll', this.unsetActiveId);
},
methods: {
- ...mapActions(['setActiveId']),
- closeSidebar() {
- this.setActiveId(inactiveId);
- },
+ ...mapActions(['unsetActiveId']),
showScopedLabels(label) {
return boardsStore.scopedLabels.enabled && isScopedLabel(label);
},
@@ -65,10 +68,11 @@ export default {
<template>
<gl-drawer
+ v-if="showSidebar"
class="js-board-settings-sidebar"
:open="isSidebarOpen"
:header-height="$options.headerHeight"
- @close="closeSidebar"
+ @close="unsetActiveId"
>
<template #header>{{ $options.listSettingsText }}</template>
<template v-if="isSidebarOpen">
diff --git a/app/assets/javascripts/boards/components/board_sidebar.js b/app/assets/javascripts/boards/components/board_sidebar.js
index 3790c494085..d26f15c1723 100644
--- a/app/assets/javascripts/boards/components/board_sidebar.js
+++ b/app/assets/javascripts/boards/components/board_sidebar.js
@@ -83,7 +83,7 @@ export default Vue.extend({
$('.js-issue-board-sidebar', this.$el).each((i, el) => {
$(el)
- .data('glDropdown')
+ .data('deprecatedJQueryDropdown')
.clearMenu();
});
}
@@ -95,7 +95,7 @@ export default Vue.extend({
},
},
created() {
- // Get events from glDropdown
+ // Get events from deprecatedJQueryDropdown
eventHub.$on('sidebar.removeAssignee', this.removeAssignee);
eventHub.$on('sidebar.addAssignee', this.addAssignee);
eventHub.$on('sidebar.removeAllAssignees', this.removeAllAssignees);
diff --git a/app/assets/javascripts/boards/components/boards_selector.vue b/app/assets/javascripts/boards/components/boards_selector.vue
index 48f6ba6cfc7..271e1fc4b5f 100644
--- a/app/assets/javascripts/boards/components/boards_selector.vue
+++ b/app/assets/javascripts/boards/components/boards_selector.vue
@@ -36,10 +36,6 @@ export default {
type: Object,
required: true,
},
- milestonePath: {
- type: String,
- required: true,
- },
throttleDuration: {
type: Number,
default: 200,
@@ -65,6 +61,10 @@ export default {
type: String,
required: true,
},
+ labelsWebUrl: {
+ type: String,
+ required: true,
+ },
projectId: {
type: Number,
required: true,
@@ -335,8 +335,8 @@ export default {
<board-form
v-if="currentPage"
- :milestone-path="milestonePath"
:labels-path="labelsPath"
+ :labels-web-url="labelsWebUrl"
:project-id="projectId"
:group-id="groupId"
:can-admin-board="canAdminBoard"
diff --git a/app/assets/javascripts/boards/components/issuable_title.vue b/app/assets/javascripts/boards/components/issuable_title.vue
new file mode 100644
index 00000000000..40627a9fab8
--- /dev/null
+++ b/app/assets/javascripts/boards/components/issuable_title.vue
@@ -0,0 +1,21 @@
+<script>
+export default {
+ props: {
+ title: {
+ type: String,
+ required: true,
+ },
+ refPath: {
+ type: String,
+ required: true,
+ },
+ },
+};
+</script>
+
+<template>
+ <div data-testid="issue-title">
+ <p class="gl-font-weight-bold">{{ title }}</p>
+ <p class="gl-mb-0">{{ refPath }}</p>
+ </div>
+</template>
diff --git a/app/assets/javascripts/boards/components/issue_card_inner.vue b/app/assets/javascripts/boards/components/issue_card_inner.vue
index d90928f35b6..8658f51e5cf 100644
--- a/app/assets/javascripts/boards/components/issue_card_inner.vue
+++ b/app/assets/javascripts/boards/components/issue_card_inner.vue
@@ -1,10 +1,9 @@
<script>
import { sortBy } from 'lodash';
import { mapState } from 'vuex';
-import { GlLabel, GlTooltipDirective } from '@gitlab/ui';
+import { GlLabel, GlTooltipDirective, GlIcon } from '@gitlab/ui';
import issueCardInner from 'ee_else_ce/boards/mixins/issue_card_inner';
import { sprintf, __ } from '~/locale';
-import Icon from '~/vue_shared/components/icon.vue';
import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate.vue';
import UserAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue';
import IssueDueDate from './issue_due_date.vue';
@@ -15,7 +14,7 @@ import { isScopedLabel } from '~/lib/utils/common_utils';
export default {
components: {
GlLabel,
- Icon,
+ GlIcon,
UserAvatarLink,
TooltipOnTruncate,
IssueDueDate,
@@ -31,28 +30,23 @@ export default {
type: Object,
required: true,
},
- issueLinkBase: {
- type: String,
- required: true,
- },
list: {
type: Object,
required: false,
default: () => ({}),
},
- rootPath: {
- type: String,
- required: true,
- },
updateFilters: {
type: Boolean,
required: false,
default: false,
},
+ },
+ inject: {
groupId: {
type: Number,
- required: false,
- default: null,
+ },
+ rootPath: {
+ type: String,
},
},
data() {
@@ -148,7 +142,7 @@ export default {
<div>
<div class="d-flex board-card-header" dir="auto">
<h4 class="board-card-title gl-mb-0 gl-mt-0">
- <icon
+ <gl-icon
v-if="issue.blocked"
v-gl-tooltip
name="issue-block"
@@ -156,7 +150,7 @@ export default {
class="issue-blocked-icon gl-mr-2"
:aria-label="__('Blocked issue')"
/>
- <icon
+ <gl-icon
v-if="issue.confidential"
v-gl-tooltip
name="eye-slash"
diff --git a/app/assets/javascripts/boards/components/issue_due_date.vue b/app/assets/javascripts/boards/components/issue_due_date.vue
index 4add5ee646a..fb45de6e14d 100644
--- a/app/assets/javascripts/boards/components/issue_due_date.vue
+++ b/app/assets/javascripts/boards/components/issue_due_date.vue
@@ -1,7 +1,6 @@
<script>
import dateFormat from 'dateformat';
-import { GlTooltip } from '@gitlab/ui';
-import Icon from '~/vue_shared/components/icon.vue';
+import { GlTooltip, GlIcon } from '@gitlab/ui';
import { __ } from '~/locale';
import {
getDayDifference,
@@ -12,7 +11,7 @@ import {
export default {
components: {
- Icon,
+ GlIcon,
GlTooltip,
},
props: {
@@ -87,7 +86,7 @@ export default {
<template>
<span>
<span ref="issueDueDate" :class="cssClass" class="board-card-info card-number">
- <icon :class="{ 'text-danger': isPastDue }" class="board-card-info-icon" name="calendar" />
+ <gl-icon :class="{ 'text-danger': isPastDue }" class="board-card-info-icon" name="calendar" />
<time :class="{ 'text-danger': isPastDue }" datetime="date" class="board-card-info-text">{{
body
}}</time>
diff --git a/app/assets/javascripts/boards/components/issue_time_estimate.vue b/app/assets/javascripts/boards/components/issue_time_estimate.vue
index e8b7689da13..fe56833016e 100644
--- a/app/assets/javascripts/boards/components/issue_time_estimate.vue
+++ b/app/assets/javascripts/boards/components/issue_time_estimate.vue
@@ -1,12 +1,11 @@
<script>
-import { GlTooltip } from '@gitlab/ui';
-import Icon from '~/vue_shared/components/icon.vue';
+import { GlTooltip, GlIcon } from '@gitlab/ui';
import { parseSeconds, stringifyTime } from '~/lib/utils/datetime_utility';
import boardsStore from '../stores/boards_store';
export default {
components: {
- Icon,
+ GlIcon,
GlTooltip,
},
props: {
@@ -34,7 +33,7 @@ export default {
<template>
<span>
<span ref="issueTimeEstimate" class="board-card-info card-number">
- <icon name="hourglass" class="board-card-info-icon" /><time class="board-card-info-text">{{
+ <gl-icon name="hourglass" class="board-card-info-icon" /><time class="board-card-info-text">{{
timeEstimate
}}</time>
</span>
diff --git a/app/assets/javascripts/boards/components/modal/empty_state.vue b/app/assets/javascripts/boards/components/modal/empty_state.vue
index 66f59009714..cd4512f320f 100644
--- a/app/assets/javascripts/boards/components/modal/empty_state.vue
+++ b/app/assets/javascripts/boards/components/modal/empty_state.vue
@@ -1,9 +1,14 @@
<script>
+/* eslint-disable vue/no-v-html */
+import { GlButton } from '@gitlab/ui';
import { __, sprintf } from '~/locale';
import ModalStore from '../../stores/modal_store';
import modalMixin from '../../mixins/modal_mixins';
export default {
+ components: {
+ GlButton,
+ },
mixins: [modalMixin],
props: {
newIssuePath: {
@@ -53,17 +58,22 @@ export default {
<div class="text-content">
<h4>{{ contents.title }}</h4>
<p v-html="contents.content"></p>
- <a v-if="activeTab === 'all'" :href="newIssuePath" class="btn btn-success btn-inverted">{{
- __('New issue')
- }}</a>
- <button
+ <gl-button
+ v-if="activeTab === 'all'"
+ :href="newIssuePath"
+ category="secondary"
+ variant="success"
+ >
+ {{ __('New issue') }}
+ </gl-button>
+ <gl-button
v-if="activeTab === 'selected'"
- class="btn btn-default"
- type="button"
+ category="primary"
+ variant="default"
@click="changeTab('all')"
>
{{ __('Open issues') }}
- </button>
+ </gl-button>
</div>
</div>
</div>
diff --git a/app/assets/javascripts/boards/components/modal/footer.vue b/app/assets/javascripts/boards/components/modal/footer.vue
index c4953dda793..d28a03da97f 100644
--- a/app/assets/javascripts/boards/components/modal/footer.vue
+++ b/app/assets/javascripts/boards/components/modal/footer.vue
@@ -1,4 +1,5 @@
<script>
+import { GlButton } from '@gitlab/ui';
import footerEEMixin from 'ee_else_ce/boards/mixins/modal_footer';
import { deprecatedCreateFlash as Flash } from '../../../flash';
import { __, n__ } from '../../../locale';
@@ -10,6 +11,7 @@ import boardsStore from '../../stores/boards_store';
export default {
components: {
ListsDropdown,
+ GlButton,
},
mixins: [modalMixin, footerEEMixin],
data() {
@@ -65,14 +67,14 @@ export default {
<template>
<footer class="form-actions add-issues-footer">
<div class="float-left">
- <button :disabled="submitDisabled" class="btn btn-success" type="button" @click="addIssues">
+ <gl-button :disabled="submitDisabled" category="primary" variant="success" @click="addIssues">
{{ submitText }}
- </button>
+ </gl-button>
<span class="inline add-issues-footer-to-list">{{ __('to list') }}</span>
<lists-dropdown />
</div>
- <button class="btn btn-default float-right" type="button" @click="toggleModal(false)">
+ <gl-button class="float-right" @click="toggleModal(false)">
{{ __('Cancel') }}
- </button>
+ </gl-button>
</footer>
</template>
diff --git a/app/assets/javascripts/boards/components/modal/header.vue b/app/assets/javascripts/boards/components/modal/header.vue
index 573284d2b44..3e96ecca24c 100644
--- a/app/assets/javascripts/boards/components/modal/header.vue
+++ b/app/assets/javascripts/boards/components/modal/header.vue
@@ -1,5 +1,6 @@
<script>
/* eslint-disable @gitlab/vue-require-i18n-strings */
+import { GlButton } from '@gitlab/ui';
import { __ } from '~/locale';
import ModalFilters from './filters';
import ModalTabs from './tabs.vue';
@@ -10,6 +11,7 @@ export default {
components: {
ModalTabs,
ModalFilters,
+ GlButton,
},
mixins: [modalMixin],
props: {
@@ -17,10 +19,6 @@ export default {
type: Number,
required: true,
},
- milestonePath: {
- type: String,
- required: true,
- },
labelPath: {
type: String,
required: true,
@@ -43,7 +41,7 @@ export default {
},
methods: {
toggleAll() {
- this.$refs.selectAllBtn.blur();
+ this.$refs.selectAllBtn.$el.blur();
ModalStore.toggleAll();
},
@@ -55,28 +53,28 @@ export default {
<header class="add-issues-header border-top-0 form-actions">
<h2 class="m-0">
Add issues
- <button
- type="button"
+ <gl-button
+ category="tertiary"
+ icon="close"
class="close"
data-dismiss="modal"
:aria-label="__('Close')"
@click="toggleModal(false)"
- >
- <span aria-hidden="true">×</span>
- </button>
+ />
</h2>
</header>
<modal-tabs v-if="!loading && issuesCount > 0" />
<div v-if="showSearch" class="d-flex gl-mb-3">
<modal-filters :store="filter" />
- <button
+ <gl-button
ref="selectAllBtn"
- type="button"
- class="btn btn-success btn-inverted gl-ml-3"
+ category="secondary"
+ variant="success"
+ class="gl-ml-3"
@click="toggleAll"
>
{{ selectAllText }}
- </button>
+ </gl-button>
</div>
</div>
</template>
diff --git a/app/assets/javascripts/boards/components/modal/index.vue b/app/assets/javascripts/boards/components/modal/index.vue
index 20344b66140..817b3bdddb0 100644
--- a/app/assets/javascripts/boards/components/modal/index.vue
+++ b/app/assets/javascripts/boards/components/modal/index.vue
@@ -26,22 +26,10 @@ export default {
type: String,
required: true,
},
- issueLinkBase: {
- type: String,
- required: true,
- },
- rootPath: {
- type: String,
- required: true,
- },
projectId: {
type: Number,
required: true,
},
- milestonePath: {
- type: String,
- required: true,
- },
labelPath: {
type: String,
required: true,
@@ -149,17 +137,8 @@ export default {
class="add-issues-modal d-flex position-fixed position-top-0 position-bottom-0 position-left-0 position-right-0 h-100"
>
<div class="add-issues-container d-flex flex-column m-auto rounded">
- <modal-header
- :project-id="projectId"
- :milestone-path="milestonePath"
- :label-path="labelPath"
- />
- <modal-list
- v-if="!loading && showList && !filterLoading"
- :issue-link-base="issueLinkBase"
- :root-path="rootPath"
- :empty-state-svg="emptyStateSvg"
- />
+ <modal-header :project-id="projectId" :label-path="labelPath" />
+ <modal-list v-if="!loading && showList && !filterLoading" :empty-state-svg="emptyStateSvg" />
<empty-state
v-if="showEmptyState"
:new-issue-path="newIssuePath"
diff --git a/app/assets/javascripts/boards/components/modal/list.vue b/app/assets/javascripts/boards/components/modal/list.vue
index 78e3351a79e..219263bd9b9 100644
--- a/app/assets/javascripts/boards/components/modal/list.vue
+++ b/app/assets/javascripts/boards/components/modal/list.vue
@@ -1,23 +1,15 @@
<script>
import { GlBreakpointInstance as bp } from '@gitlab/ui/dist/utils';
-import Icon from '~/vue_shared/components/icon.vue';
+import { GlIcon } from '@gitlab/ui';
import ModalStore from '../../stores/modal_store';
import IssueCardInner from '../issue_card_inner.vue';
export default {
components: {
IssueCardInner,
- Icon,
+ GlIcon,
},
props: {
- issueLinkBase: {
- type: String,
- required: true,
- },
- rootPath: {
- type: String,
- required: true,
- },
emptyStateSvg: {
type: String,
required: true,
@@ -134,8 +126,8 @@ export default {
class="board-card position-relative p-3 rounded"
@click="toggleIssue($event, issue)"
>
- <issue-card-inner :issue="issue" :issue-link-base="issueLinkBase" :root-path="rootPath" />
- <icon
+ <issue-card-inner :issue="issue" />
+ <gl-icon
v-if="issue.selected"
:aria-label="'Issue #' + issue.id + ' selected'"
name="mobile-issue-close"
diff --git a/app/assets/javascripts/boards/components/modal/lists_dropdown.vue b/app/assets/javascripts/boards/components/modal/lists_dropdown.vue
index 3fbe8fe1be7..fe10e7fb856 100644
--- a/app/assets/javascripts/boards/components/modal/lists_dropdown.vue
+++ b/app/assets/javascripts/boards/components/modal/lists_dropdown.vue
@@ -1,13 +1,12 @@
<script>
-import { GlLink } from '@gitlab/ui';
-import Icon from '~/vue_shared/components/icon.vue';
+import { GlLink, GlIcon } from '@gitlab/ui';
import ModalStore from '../../stores/modal_store';
import boardsStore from '../../stores/boards_store';
export default {
components: {
GlLink,
- Icon,
+ GlIcon,
},
data() {
return {
@@ -29,7 +28,7 @@ export default {
<div class="dropdown inline">
<button class="dropdown-menu-toggle" type="button" data-toggle="dropdown" aria-expanded="false">
<span :style="{ backgroundColor: selected.label.color }" class="dropdown-label-box"> </span>
- {{ selected.title }} <icon name="chevron-down" />
+ {{ selected.title }} <gl-icon name="chevron-down" class="dropdown-menu-toggle-icon" />
</button>
<div class="dropdown-menu dropdown-menu-selectable dropdown-menu-drop-up">
<ul>
diff --git a/app/assets/javascripts/boards/components/new_list_dropdown.js b/app/assets/javascripts/boards/components/new_list_dropdown.js
index 2b9fdf11b37..2e356f1353a 100644
--- a/app/assets/javascripts/boards/components/new_list_dropdown.js
+++ b/app/assets/javascripts/boards/components/new_list_dropdown.js
@@ -6,6 +6,7 @@ import axios from '~/lib/utils/axios_utils';
import { deprecatedCreateFlash as flash } from '~/flash';
import CreateLabelDropdown from '../../create_label';
import boardsStore from '../stores/boards_store';
+import initDeprecatedJQueryDropdown from '~/deprecated_jquery_dropdown';
$(document)
.off('created.label')
@@ -36,7 +37,7 @@ export default function initNewListDropdown() {
$dropdownToggle.data('projectPath'),
);
- $dropdownToggle.glDropdown({
+ initDeprecatedJQueryDropdown($dropdownToggle, {
data(term, callback) {
axios
.get($dropdownToggle.attr('data-list-labels-path'))
diff --git a/app/assets/javascripts/boards/components/project_select.vue b/app/assets/javascripts/boards/components/project_select.vue
index 598e92726c1..59e7620962a 100644
--- a/app/assets/javascripts/boards/components/project_select.vue
+++ b/app/assets/javascripts/boards/components/project_select.vue
@@ -1,30 +1,30 @@
<script>
import $ from 'jquery';
import { escape } from 'lodash';
-import { GlLoadingIcon } from '@gitlab/ui';
-import Icon from '~/vue_shared/components/icon.vue';
+import { GlLoadingIcon, GlIcon } from '@gitlab/ui';
import { __ } from '~/locale';
import eventHub from '../eventhub';
import Api from '../../api';
import { featureAccessLevel } from '~/pages/projects/shared/permissions/constants';
+import initDeprecatedJQueryDropdown from '~/deprecated_jquery_dropdown';
export default {
name: 'BoardProjectSelect',
components: {
- Icon,
+ GlIcon,
GlLoadingIcon,
},
props: {
- groupId: {
- type: Number,
- required: true,
- default: 0,
- },
list: {
type: Object,
required: true,
},
},
+ inject: {
+ groupId: {
+ type: Number,
+ },
+ },
data() {
return {
loading: true,
@@ -37,7 +37,7 @@ export default {
},
},
mounted() {
- $(this.$refs.projectsDropdown).glDropdown({
+ initDeprecatedJQueryDropdown($(this.$refs.projectsDropdown), {
filterable: true,
filterRemote: true,
search: {
@@ -105,13 +105,13 @@ export default {
data-toggle="dropdown"
aria-expanded="false"
>
- {{ selectedProjectName }} <icon name="chevron-down" />
+ {{ selectedProjectName }} <gl-icon name="chevron-down" class="dropdown-menu-toggle-icon" />
</button>
<div class="dropdown-menu dropdown-menu-selectable dropdown-menu-full-width">
<div class="dropdown-title">{{ __('Projects') }}</div>
<div class="dropdown-input">
<input class="dropdown-input-field" type="search" :placeholder="__('Search projects')" />
- <icon name="search" class="dropdown-input-search" data-hidden="true" />
+ <gl-icon name="search" class="dropdown-input-search" data-hidden="true" />
</div>
<div class="dropdown-content"></div>
<div class="dropdown-loading"><gl-loading-icon /></div>
diff --git a/app/assets/javascripts/boards/components/sidebar/board_editable_item.vue b/app/assets/javascripts/boards/components/sidebar/board_editable_item.vue
new file mode 100644
index 00000000000..8df03ea581f
--- /dev/null
+++ b/app/assets/javascripts/boards/components/sidebar/board_editable_item.vue
@@ -0,0 +1,79 @@
+<script>
+import { GlButton, GlLoadingIcon } from '@gitlab/ui';
+
+export default {
+ components: { GlButton, GlLoadingIcon },
+ props: {
+ title: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ loading: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ },
+ inject: ['canUpdate'],
+ data() {
+ return {
+ edit: false,
+ };
+ },
+ destroyed() {
+ window.removeEventListener('click', this.collapseWhenOffClick);
+ },
+ methods: {
+ collapseWhenOffClick({ target }) {
+ if (!this.$el.contains(target)) {
+ this.collapse();
+ }
+ },
+ expand() {
+ if (this.edit) {
+ return;
+ }
+
+ this.edit = true;
+ this.$emit('changed', this.edit);
+ window.addEventListener('click', this.collapseWhenOffClick);
+ },
+ collapse() {
+ if (!this.edit) {
+ return;
+ }
+
+ this.edit = false;
+ this.$emit('changed', this.edit);
+ window.removeEventListener('click', this.collapseWhenOffClick);
+ },
+ },
+};
+</script>
+
+<template>
+ <div>
+ <div class="gl-display-flex gl-justify-content-space-between gl-mb-3">
+ <span class="gl-vertical-align-middle">
+ <span data-testid="title">{{ title }}</span>
+ <gl-loading-icon v-if="loading" inline class="gl-ml-2" />
+ </span>
+ <gl-button
+ v-if="canUpdate"
+ variant="link"
+ class="gl-text-gray-900!"
+ data-testid="edit-button"
+ @click="expand()"
+ >
+ {{ __('Edit') }}
+ </gl-button>
+ </div>
+ <div v-show="!edit" class="gl-text-gray-400" data-testid="collapsed-content">
+ <slot name="collapsed">{{ __('None') }}</slot>
+ </div>
+ <div v-show="edit" data-testid="expanded-content">
+ <slot></slot>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/boards/constants.js b/app/assets/javascripts/boards/constants.js
index 35c52558cac..2f64014a949 100644
--- a/app/assets/javascripts/boards/constants.js
+++ b/app/assets/javascripts/boards/constants.js
@@ -15,6 +15,9 @@ export const ListType = {
export const inactiveId = 0;
+export const ISSUABLE = 'issuable';
+export const LIST = 'list';
+
export default {
BoardType,
ListType,
diff --git a/app/assets/javascripts/boards/filtered_search_boards.js b/app/assets/javascripts/boards/filtered_search_boards.js
index b7966dd869d..fff89832bf0 100644
--- a/app/assets/javascripts/boards/filtered_search_boards.js
+++ b/app/assets/javascripts/boards/filtered_search_boards.js
@@ -27,6 +27,11 @@ export default class FilteredSearchBoards extends FilteredSearchManager {
updateObject(path) {
this.store.path = path.substr(1);
+ if (gon.features.boardsWithSwimlanes || gon.features.graphqlBoardLists) {
+ boardsStore.updateFiltersUrl();
+ boardsStore.performSearch();
+ }
+
if (this.updateUrl) {
boardsStore.updateFiltersUrl();
}
diff --git a/app/assets/javascripts/boards/index.js b/app/assets/javascripts/boards/index.js
index 971edd71eec..1173c6d0578 100644
--- a/app/assets/javascripts/boards/index.js
+++ b/app/assets/javascripts/boards/index.js
@@ -1,6 +1,6 @@
import $ from 'jquery';
import Vue from 'vue';
-import { mapActions } from 'vuex';
+import { mapActions, mapState } from 'vuex';
import 'ee_else_ce/boards/models/issue';
import 'ee_else_ce/boards/models/list';
@@ -24,7 +24,6 @@ import { deprecatedCreateFlash as Flash } from '~/flash';
import { __ } from '~/locale';
import './models/label';
import './models/assignee';
-import { BoardType } from './constants';
import toggleFocusMode from '~/boards/toggle_focus';
import FilteredSearchBoards from '~/boards/filtered_search_boards';
@@ -42,11 +41,9 @@ import {
NavigationType,
convertObjectPropsToCamelCase,
parseBoolean,
+ urlParamsToObject,
} from '~/lib/utils/common_utils';
-import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import mountMultipleBoardsSwitcher from './mount_multiple_boards_switcher';
-import projectBoardQuery from './queries/project_board.query.graphql';
-import groupQuery from './queries/group_board.query.graphql';
Vue.use(VueApollo);
@@ -85,6 +82,11 @@ export default () => {
BoardAddIssuesModal,
BoardSettingsSidebar: () => import('~/boards/components/board_settings_sidebar.vue'),
},
+ provide: {
+ boardId: $boardApp.dataset.boardId,
+ groupId: Number($boardApp.dataset.groupId) || null,
+ rootPath: $boardApp.dataset.rootPath,
+ },
store,
apolloProvider,
data() {
@@ -94,16 +96,14 @@ export default () => {
boardsEndpoint: $boardApp.dataset.boardsEndpoint,
recentBoardsEndpoint: $boardApp.dataset.recentBoardsEndpoint,
listsEndpoint: $boardApp.dataset.listsEndpoint,
- boardId: $boardApp.dataset.boardId,
disabled: parseBoolean($boardApp.dataset.disabled),
- issueLinkBase: $boardApp.dataset.issueLinkBase,
- rootPath: $boardApp.dataset.rootPath,
bulkUpdatePath: $boardApp.dataset.bulkUpdatePath,
detailIssue: boardsStore.detail,
parent: $boardApp.dataset.parent,
};
},
computed: {
+ ...mapState(['isShowingEpicsSwimlanes']),
detailIssueVisible() {
return Object.keys(this.detailIssue.issue).length;
},
@@ -114,10 +114,15 @@ export default () => {
recentBoardsEndpoint: this.recentBoardsEndpoint,
listsEndpoint: this.listsEndpoint,
bulkUpdatePath: this.bulkUpdatePath,
- boardId: this.boardId,
+ boardId: $boardApp.dataset.boardId,
fullPath: $boardApp.dataset.fullPath,
};
- this.setInitialBoardData({ ...endpoints, boardType: this.parent });
+ this.setInitialBoardData({
+ ...endpoints,
+ boardType: this.parent,
+ disabled: this.disabled,
+ showPromotion: parseBoolean($boardApp.getAttribute('data-show-promotion')),
+ });
boardsStore.setEndpoints(endpoints);
boardsStore.rootPath = this.boardsEndpoint;
@@ -125,55 +130,24 @@ export default () => {
eventHub.$on('newDetailIssue', this.updateDetailIssue);
eventHub.$on('clearDetailIssue', this.clearDetailIssue);
sidebarEventHub.$on('toggleSubscription', this.toggleSubscription);
+ eventHub.$on('performSearch', this.performSearch);
},
beforeDestroy() {
eventHub.$off('updateTokens', this.updateTokens);
eventHub.$off('newDetailIssue', this.updateDetailIssue);
eventHub.$off('clearDetailIssue', this.clearDetailIssue);
sidebarEventHub.$off('toggleSubscription', this.toggleSubscription);
+ eventHub.$off('performSearch', this.performSearch);
},
mounted() {
this.filterManager = new FilteredSearchBoards(boardsStore.filter, true, boardsStore.cantEdit);
this.filterManager.setup();
- boardsStore.disabled = this.disabled;
-
- if (gon.features.graphqlBoardLists) {
- this.$apollo.addSmartQuery('lists', {
- query() {
- return this.parent === BoardType.group ? groupQuery : projectBoardQuery;
- },
- variables() {
- return {
- fullPath: this.state.endpoints.fullPath,
- boardId: `gid://gitlab/Board/${this.boardId}`,
- };
- },
- update(data) {
- return this.getNodes(data);
- },
- result({ data, error }) {
- if (error) {
- throw error;
- }
-
- const lists = this.getNodes(data);
+ this.performSearch();
- lists.forEach(list =>
- boardsStore.addList({
- ...list,
- id: getIdFromGraphQLId(list.id),
- }),
- );
+ boardsStore.disabled = this.disabled;
- boardsStore.addBlankState();
- setPromotionState(boardsStore);
- },
- error() {
- Flash(__('An error occurred while fetching the board lists. Please try again.'));
- },
- });
- } else {
+ if (!gon.features.graphqlBoardLists) {
boardsStore
.all()
.then(res => res.data)
@@ -189,10 +163,22 @@ export default () => {
}
},
methods: {
- ...mapActions(['setInitialBoardData']),
+ ...mapActions([
+ 'setInitialBoardData',
+ 'setFilters',
+ 'fetchEpicsSwimlanes',
+ 'fetchIssuesForAllLists',
+ ]),
updateTokens() {
this.filterManager.updateTokens();
},
+ performSearch() {
+ this.setFilters(convertObjectPropsToCamelCase(urlParamsToObject(window.location.search)));
+ if (gon.features.boardsWithSwimlanes && this.isShowingEpicsSwimlanes) {
+ this.fetchEpicsSwimlanes(false);
+ this.fetchIssuesForAllLists();
+ }
+ },
updateDetailIssue(newIssue, multiSelect = false) {
const { sidebarInfoEndpoint } = newIssue;
if (sidebarInfoEndpoint && newIssue.subscribed === undefined) {
@@ -354,6 +340,8 @@ export default () => {
class="btn btn-success gl-ml-3"
type="button"
data-placement="bottom"
+ data-track-event="click_button"
+ data-track-label="board_add_issues"
ref="addIssuesButton"
:class="{ 'disabled': disabled }"
:title="tooltipTitle"
diff --git a/app/assets/javascripts/boards/models/issue.js b/app/assets/javascripts/boards/models/issue.js
index 98eac35b2ed..822e6d62ab3 100644
--- a/app/assets/javascripts/boards/models/issue.js
+++ b/app/assets/javascripts/boards/models/issue.js
@@ -15,7 +15,7 @@ class ListIssue {
this.labels = [];
this.assignees = [];
this.selected = false;
- this.position = obj.position || obj.relative_position || Infinity;
+ this.position = obj.position || obj.relative_position || obj.relativePosition || Infinity;
this.isFetching = {
subscriptions: true,
};
diff --git a/app/assets/javascripts/boards/models/list.js b/app/assets/javascripts/boards/models/list.js
index b8b30c958a9..2f6caffbf84 100644
--- a/app/assets/javascripts/boards/models/list.js
+++ b/app/assets/javascripts/boards/models/list.js
@@ -47,7 +47,7 @@ class List {
this.loading = true;
this.loadingMore = false;
this.issues = obj.issues || [];
- this.issuesSize = obj.issuesSize ? obj.issuesSize : 0;
+ this.issuesSize = obj.issuesSize || obj.issuesCount || 0;
this.maxIssueCount = obj.maxIssueCount || obj.max_issue_count || 0;
if (obj.label) {
diff --git a/app/assets/javascripts/boards/mount_multiple_boards_switcher.js b/app/assets/javascripts/boards/mount_multiple_boards_switcher.js
index 73d37459bfe..51bb72b7657 100644
--- a/app/assets/javascripts/boards/mount_multiple_boards_switcher.js
+++ b/app/assets/javascripts/boards/mount_multiple_boards_switcher.js
@@ -27,7 +27,7 @@ export default () => {
hasMissingBoards: parseBoolean(dataset.hasMissingBoards),
canAdminBoard: parseBoolean(dataset.canAdminBoard),
multipleIssueBoardsAvailable: parseBoolean(dataset.multipleIssueBoardsAvailable),
- projectId: Number(dataset.projectId),
+ projectId: dataset.projectId ? Number(dataset.projectId) : 0,
groupId: Number(dataset.groupId),
scopedIssueBoardFeatureEnabled: parseBoolean(dataset.scopedIssueBoardFeatureEnabled),
weights: JSON.parse(dataset.weights),
diff --git a/app/assets/javascripts/boards/queries/board_list_create.mutation.graphql b/app/assets/javascripts/boards/queries/board_list_create.mutation.graphql
new file mode 100644
index 00000000000..dcfe69222a0
--- /dev/null
+++ b/app/assets/javascripts/boards/queries/board_list_create.mutation.graphql
@@ -0,0 +1,10 @@
+#import "./board_list.fragment.graphql"
+
+mutation CreateBoardList($boardId: BoardID!, $backlog: Boolean) {
+ boardListCreate(input: { boardId: $boardId, backlog: $backlog }) {
+ list {
+ ...BoardListFragment
+ }
+ errors
+ }
+}
diff --git a/app/assets/javascripts/boards/queries/board_list_shared.fragment.graphql b/app/assets/javascripts/boards/queries/board_list_shared.fragment.graphql
index 8abd79332fb..d85b736720b 100644
--- a/app/assets/javascripts/boards/queries/board_list_shared.fragment.graphql
+++ b/app/assets/javascripts/boards/queries/board_list_shared.fragment.graphql
@@ -4,7 +4,7 @@ fragment BoardListShared on BoardList {
position
listType
collapsed
- maxIssueCount
+ issuesCount
label {
id
title
diff --git a/app/assets/javascripts/boards/queries/board_list_update.mutation.graphql b/app/assets/javascripts/boards/queries/board_list_update.mutation.graphql
new file mode 100644
index 00000000000..b474c9acb93
--- /dev/null
+++ b/app/assets/javascripts/boards/queries/board_list_update.mutation.graphql
@@ -0,0 +1,10 @@
+#import "./board_list.fragment.graphql"
+
+mutation UpdateBoardList($listId: ID!, $position: Int, $collapsed: Boolean) {
+ updateBoardList(input: { listId: $listId, position: $position, collapsed: $collapsed }) {
+ list {
+ ...BoardListFragment
+ }
+ errors
+ }
+}
diff --git a/app/assets/javascripts/boards/queries/group_lists_issues.query.graphql b/app/assets/javascripts/boards/queries/group_lists_issues.query.graphql
deleted file mode 100644
index 724c7884c58..00000000000
--- a/app/assets/javascripts/boards/queries/group_lists_issues.query.graphql
+++ /dev/null
@@ -1,18 +0,0 @@
-#import "./issue.fragment.graphql"
-
-query GroupListIssues($fullPath: ID!, $boardId: ID!) {
- group(fullPath: $fullPath) {
- board(id: $boardId) {
- lists {
- nodes {
- id
- issues {
- nodes {
- ...IssueNode
- }
- }
- }
- }
- }
- }
-}
diff --git a/app/assets/javascripts/boards/queries/issue.fragment.graphql b/app/assets/javascripts/boards/queries/issue.fragment.graphql
index 89d56b895a4..4b429f875a6 100644
--- a/app/assets/javascripts/boards/queries/issue.fragment.graphql
+++ b/app/assets/javascripts/boards/queries/issue.fragment.graphql
@@ -7,14 +7,10 @@ fragment IssueNode on Issue {
referencePath: reference(full: true)
dueDate
timeEstimate
- weight
confidential
webUrl
subscribed
- blocked
- epic {
- id
- }
+ relativePosition
assignees {
nodes {
...User
diff --git a/app/assets/javascripts/boards/queries/issue_move_list.mutation.graphql b/app/assets/javascripts/boards/queries/issue_move_list.mutation.graphql
new file mode 100644
index 00000000000..ff6aa597f48
--- /dev/null
+++ b/app/assets/javascripts/boards/queries/issue_move_list.mutation.graphql
@@ -0,0 +1,28 @@
+#import "ee_else_ce/boards/queries/issue.fragment.graphql"
+
+mutation IssueMoveList(
+ $projectPath: ID!
+ $iid: String!
+ $boardId: ID!
+ $fromListId: ID
+ $toListId: ID
+ $moveBeforeId: ID
+ $moveAfterId: ID
+) {
+ issueMoveList(
+ input: {
+ projectPath: $projectPath
+ iid: $iid
+ boardId: $boardId
+ fromListId: $fromListId
+ toListId: $toListId
+ moveBeforeId: $moveBeforeId
+ moveAfterId: $moveAfterId
+ }
+ ) {
+ issue {
+ ...IssueNode
+ }
+ errors
+ }
+}
diff --git a/app/assets/javascripts/boards/queries/lists_issues.query.graphql b/app/assets/javascripts/boards/queries/lists_issues.query.graphql
new file mode 100644
index 00000000000..c66cdf68cf4
--- /dev/null
+++ b/app/assets/javascripts/boards/queries/lists_issues.query.graphql
@@ -0,0 +1,39 @@
+#import "ee_else_ce/boards/queries/issue.fragment.graphql"
+
+query ListIssues(
+ $fullPath: ID!
+ $boardId: ID!
+ $id: ID
+ $filters: BoardIssueInput
+ $isGroup: Boolean = false
+ $isProject: Boolean = false
+) {
+ group(fullPath: $fullPath) @include(if: $isGroup) {
+ board(id: $boardId) {
+ lists(id: $id) {
+ nodes {
+ id
+ issues(filters: $filters) {
+ nodes {
+ ...IssueNode
+ }
+ }
+ }
+ }
+ }
+ }
+ project(fullPath: $fullPath) @include(if: $isProject) {
+ board(id: $boardId) {
+ lists(id: $id) {
+ nodes {
+ id
+ issues(filters: $filters) {
+ nodes {
+ ...IssueNode
+ }
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/app/assets/javascripts/boards/queries/project_lists_issues.query.graphql b/app/assets/javascripts/boards/queries/project_lists_issues.query.graphql
deleted file mode 100644
index 149b76848ef..00000000000
--- a/app/assets/javascripts/boards/queries/project_lists_issues.query.graphql
+++ /dev/null
@@ -1,18 +0,0 @@
-#import "./issue.fragment.graphql"
-
-query ProjectListIssues($fullPath: ID!, $boardId: ID!) {
- project(fullPath: $fullPath) {
- board(id: $boardId) {
- lists {
- nodes {
- id
- issues {
- nodes {
- ...IssueNode
- }
- }
- }
- }
- }
- }
-}
diff --git a/app/assets/javascripts/boards/stores/actions.js b/app/assets/javascripts/boards/stores/actions.js
index b4be7546252..4b81d9c73ef 100644
--- a/app/assets/javascripts/boards/stores/actions.js
+++ b/app/assets/javascripts/boards/stores/actions.js
@@ -1,66 +1,248 @@
-import * as types from './mutation_types';
+import Cookies from 'js-cookie';
+import { sortBy, pick } from 'lodash';
+import createFlash from '~/flash';
+import { __ } from '~/locale';
+import { parseBoolean } from '~/lib/utils/common_utils';
import createDefaultClient from '~/lib/graphql';
-import { BoardType } from '~/boards/constants';
-import { formatListIssues } from '../boards_util';
-import groupListsIssuesQuery from '../queries/group_lists_issues.query.graphql';
-import projectListsIssuesQuery from '../queries/project_lists_issues.query.graphql';
+import { getIdFromGraphQLId } from '~/graphql_shared/utils';
+import { BoardType, ListType, inactiveId } from '~/boards/constants';
+import * as types from './mutation_types';
+import { formatListIssues, fullBoardId } from '../boards_util';
+import boardStore from '~/boards/stores/boards_store';
-const gqlClient = createDefaultClient();
+import listsIssuesQuery from '../queries/lists_issues.query.graphql';
+import projectBoardQuery from '../queries/project_board.query.graphql';
+import groupBoardQuery from '../queries/group_board.query.graphql';
+import createBoardListMutation from '../queries/board_list_create.mutation.graphql';
+import updateBoardListMutation from '../queries/board_list_update.mutation.graphql';
+import issueMoveListMutation from '../queries/issue_move_list.mutation.graphql';
const notImplemented = () => {
/* eslint-disable-next-line @gitlab/require-i18n-strings */
throw new Error('Not implemented!');
};
+export const gqlClient = createDefaultClient();
+
export default {
setInitialBoardData: ({ commit }, data) => {
commit(types.SET_INITIAL_BOARD_DATA, data);
},
- setActiveId({ commit }, id) {
- commit(types.SET_ACTIVE_ID, id);
+ setActiveId({ commit }, { id, sidebarType }) {
+ commit(types.SET_ACTIVE_ID, { id, sidebarType });
},
- fetchLists: () => {
- notImplemented();
+ unsetActiveId({ dispatch }) {
+ dispatch('setActiveId', { id: inactiveId, sidebarType: '' });
},
+ setFilters: ({ commit }, filters) => {
+ const filterParams = pick(filters, [
+ 'assigneeUsername',
+ 'authorUsername',
+ 'labelName',
+ 'milestoneTitle',
+ 'releaseTag',
+ 'search',
+ ]);
+ commit(types.SET_FILTERS, filterParams);
+ },
+
+ fetchLists: ({ commit, state, dispatch }) => {
+ const { endpoints, boardType } = state;
+ const { fullPath, boardId } = endpoints;
+
+ let query;
+ if (boardType === BoardType.group) {
+ query = groupBoardQuery;
+ } else if (boardType === BoardType.project) {
+ query = projectBoardQuery;
+ } else {
+ createFlash(__('Invalid board'));
+ return Promise.reject();
+ }
+
+ const variables = {
+ fullPath,
+ boardId: fullBoardId(boardId),
+ };
+
+ return gqlClient
+ .query({
+ query,
+ variables,
+ })
+ .then(({ data }) => {
+ let { lists } = data[boardType]?.board;
+ // Temporarily using positioning logic from boardStore
+ lists = lists.nodes.map(list =>
+ boardStore.updateListPosition({
+ ...list,
+ doNotFetchIssues: true,
+ }),
+ );
+ commit(types.RECEIVE_BOARD_LISTS_SUCCESS, sortBy(lists, 'position'));
+ // Backlog list needs to be created if it doesn't exist
+ if (!lists.find(l => l.type === ListType.backlog)) {
+ dispatch('createList', { backlog: true });
+ }
+ dispatch('showWelcomeList');
+ })
+ .catch(() => {
+ createFlash(
+ __('An error occurred while fetching the board lists. Please reload the page.'),
+ );
+ });
+ },
+
+ // This action only supports backlog list creation at this stage
+ // Future iterations will add the ability to create other list types
+ createList: ({ state, commit, dispatch }, { backlog = false }) => {
+ const { boardId } = state.endpoints;
+ gqlClient
+ .mutate({
+ mutation: createBoardListMutation,
+ variables: {
+ boardId: fullBoardId(boardId),
+ backlog,
+ },
+ })
+ .then(({ data }) => {
+ if (data?.boardListCreate?.errors.length) {
+ commit(types.CREATE_LIST_FAILURE);
+ } else {
+ const list = data.boardListCreate?.list;
+ dispatch('addList', list);
+ }
+ })
+ .catch(() => {
+ commit(types.CREATE_LIST_FAILURE);
+ });
+ },
+
+ addList: ({ state, commit }, list) => {
+ const lists = state.boardLists;
+ // Temporarily using positioning logic from boardStore
+ lists.push(boardStore.updateListPosition({ ...list, doNotFetchIssues: true }));
+ commit(types.RECEIVE_BOARD_LISTS_SUCCESS, sortBy(lists, 'position'));
+ },
+
+ showWelcomeList: ({ state, dispatch }) => {
+ if (state.disabled) {
+ return;
+ }
+ if (
+ state.boardLists.find(list => list.type !== ListType.backlog && list.type !== ListType.closed)
+ ) {
+ return;
+ }
+ if (parseBoolean(Cookies.get('issue_board_welcome_hidden'))) {
+ return;
+ }
+
+ dispatch('addList', {
+ id: 'blank',
+ listType: ListType.blank,
+ title: __('Welcome to your issue board!'),
+ position: 0,
+ });
+ },
+
+ showPromotionList: () => {},
+
generateDefaultLists: () => {
notImplemented();
},
- createList: () => {
- notImplemented();
+ moveList: ({ state, commit, dispatch }, { listId, newIndex, adjustmentValue }) => {
+ const { boardLists } = state;
+ const backupList = [...boardLists];
+ const movedList = boardLists.find(({ id }) => id === listId);
+
+ const newPosition = newIndex - 1;
+ const listAtNewIndex = boardLists[newIndex];
+
+ movedList.position = newPosition;
+ listAtNewIndex.position += adjustmentValue;
+ commit(types.MOVE_LIST, {
+ movedList,
+ listAtNewIndex,
+ });
+
+ dispatch('updateList', { listId, position: newPosition, backupList });
},
- updateList: () => {
- notImplemented();
+ updateList: ({ commit }, { listId, position, collapsed, backupList }) => {
+ gqlClient
+ .mutate({
+ mutation: updateBoardListMutation,
+ variables: {
+ listId,
+ position,
+ collapsed,
+ },
+ })
+ .then(({ data }) => {
+ if (data?.updateBoardList?.errors.length) {
+ commit(types.UPDATE_LIST_FAILURE, backupList);
+ }
+ })
+ .catch(() => {
+ commit(types.UPDATE_LIST_FAILURE, backupList);
+ });
},
deleteList: () => {
notImplemented();
},
- fetchIssuesForList: () => {
- notImplemented();
+ fetchIssuesForList: ({ state, commit }, listId) => {
+ const { endpoints, boardType, filterParams } = state;
+ const { fullPath, boardId } = endpoints;
+
+ const variables = {
+ fullPath,
+ boardId: fullBoardId(boardId),
+ id: listId,
+ filters: filterParams,
+ isGroup: boardType === BoardType.group,
+ isProject: boardType === BoardType.project,
+ };
+
+ return gqlClient
+ .query({
+ query: listsIssuesQuery,
+ context: {
+ isSingleRequest: true,
+ },
+ variables,
+ })
+ .then(({ data }) => {
+ const { lists } = data[boardType]?.board;
+ const listIssues = formatListIssues(lists);
+ commit(types.RECEIVE_ISSUES_FOR_LIST_SUCCESS, { listIssues, listId });
+ })
+ .catch(() => commit(types.RECEIVE_ISSUES_FOR_LIST_FAILURE, listId));
},
fetchIssuesForAllLists: ({ state, commit }) => {
commit(types.REQUEST_ISSUES_FOR_ALL_LISTS);
- const { endpoints, boardType } = state;
+ const { endpoints, boardType, filterParams } = state;
const { fullPath, boardId } = endpoints;
- const query = boardType === BoardType.group ? groupListsIssuesQuery : projectListsIssuesQuery;
-
const variables = {
fullPath,
- boardId: `gid://gitlab/Board/${boardId}`,
+ boardId: fullBoardId(boardId),
+ filters: filterParams,
+ isGroup: boardType === BoardType.group,
+ isProject: boardType === BoardType.project,
};
return gqlClient
.query({
- query,
+ query: listsIssuesQuery,
variables,
})
.then(({ data }) => {
@@ -71,14 +253,56 @@ export default {
.catch(() => commit(types.RECEIVE_ISSUES_FOR_ALL_LISTS_FAILURE));
},
- moveIssue: () => {
- notImplemented();
+ moveIssue: (
+ { state, commit },
+ { issueId, issueIid, issuePath, fromListId, toListId, moveBeforeId, moveAfterId },
+ ) => {
+ const originalIssue = state.issues[issueId];
+ const fromList = state.issuesByListId[fromListId];
+ const originalIndex = fromList.indexOf(Number(issueId));
+ commit(types.MOVE_ISSUE, { originalIssue, fromListId, toListId, moveBeforeId, moveAfterId });
+
+ const { boardId } = state.endpoints;
+ const [fullProjectPath] = issuePath.split(/[#]/);
+
+ gqlClient
+ .mutate({
+ mutation: issueMoveListMutation,
+ variables: {
+ projectPath: fullProjectPath,
+ boardId: fullBoardId(boardId),
+ iid: issueIid,
+ fromListId: getIdFromGraphQLId(fromListId),
+ toListId: getIdFromGraphQLId(toListId),
+ moveBeforeId,
+ moveAfterId,
+ },
+ })
+ .then(({ data }) => {
+ if (data?.issueMoveList?.errors.length) {
+ commit(types.MOVE_ISSUE_FAILURE, { originalIssue, fromListId, toListId, originalIndex });
+ } else {
+ const issue = data.issueMoveList?.issue;
+ commit(types.MOVE_ISSUE_SUCCESS, { issue });
+ }
+ })
+ .catch(() =>
+ commit(types.MOVE_ISSUE_FAILURE, { originalIssue, fromListId, toListId, originalIndex }),
+ );
},
createNewIssue: () => {
notImplemented();
},
+ addListIssue: ({ commit }, { list, issue, position }) => {
+ commit(types.ADD_ISSUE_TO_LIST, { list, issue, position });
+ },
+
+ addListIssueFailure: ({ commit }, { list, issue }) => {
+ commit(types.ADD_ISSUE_TO_LIST_FAILURE, { list, issue });
+ },
+
fetchBacklog: () => {
notImplemented();
},
diff --git a/app/assets/javascripts/boards/stores/boards_store.js b/app/assets/javascripts/boards/stores/boards_store.js
index 30c71d64085..faf4f9ebfd3 100644
--- a/app/assets/javascripts/boards/stores/boards_store.js
+++ b/app/assets/javascripts/boards/stores/boards_store.js
@@ -15,6 +15,7 @@ import {
import { __ } from '~/locale';
import axios from '~/lib/utils/axios_utils';
import { mergeUrlParams } from '~/lib/utils/url_utility';
+import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import eventHub from '../eventhub';
import { ListType } from '../constants';
import IssueProject from '../models/project';
@@ -303,7 +304,11 @@ const boardsStore = {
onNewListIssueResponse(list, issue, data) {
issue.refreshData(data);
- if (list.issuesSize > 1) {
+ if (
+ !gon.features.boardsWithSwimlanes &&
+ !gon.features.graphqlBoardLists &&
+ list.issues.length > 1
+ ) {
const moveBeforeId = list.issues[1].id;
this.moveIssue(issue.id, null, null, null, moveBeforeId);
}
@@ -513,6 +518,10 @@ const boardsStore = {
eventHub.$emit('updateTokens');
},
+ performSearch() {
+ eventHub.$emit('performSearch');
+ },
+
setListDetail(newList) {
this.detail.list = newList;
},
@@ -706,6 +715,10 @@ const boardsStore = {
},
newIssue(id, issue) {
+ if (typeof id === 'string') {
+ id = getIdFromGraphQLId(id);
+ }
+
return axios.post(this.generateIssuesPath(id), {
issue,
});
@@ -714,6 +727,10 @@ const boardsStore = {
newListIssue(list, issue) {
list.addIssue(issue, null, 0);
list.issuesSize += 1;
+ let listId = list.id;
+ if (typeof listId === 'string') {
+ listId = getIdFromGraphQLId(listId);
+ }
return this.newIssue(list.id, issue)
.then(res => res.data)
@@ -854,21 +871,6 @@ const boardsStore = {
},
refreshIssueData(issue, obj) {
- // issue.id = obj.id;
- // issue.iid = obj.iid;
- // issue.title = obj.title;
- // issue.confidential = obj.confidential;
- // issue.dueDate = obj.due_date || obj.dueDate;
- // issue.sidebarInfoEndpoint = obj.issue_sidebar_endpoint;
- // issue.referencePath = obj.reference_path || obj.referencePath;
- // issue.path = obj.real_path || obj.webUrl;
- // issue.toggleSubscriptionEndpoint = obj.toggle_subscription_endpoint;
- // issue.project_id = obj.project_id;
- // issue.timeEstimate = obj.time_estimate || obj.timeEstimate;
- // issue.assignableLabelsEndpoint = obj.assignable_labels_endpoint;
- // issue.blocked = obj.blocked;
- // issue.epic = obj.epic;
-
const convertedObj = convertObjectPropsToCamelCase(obj, {
dropKeys: ['issue_sidebar_endpoint', 'real_path', 'webUrl'],
});
diff --git a/app/assets/javascripts/boards/stores/getters.js b/app/assets/javascripts/boards/stores/getters.js
index 4de1576099d..3688476dc5f 100644
--- a/app/assets/javascripts/boards/stores/getters.js
+++ b/app/assets/javascripts/boards/stores/getters.js
@@ -1,3 +1,25 @@
+import { inactiveId } from '../constants';
+
export default {
getLabelToggleState: state => (state.isShowingLabels ? 'on' : 'off'),
+ isSidebarOpen: state => state.activeId !== inactiveId,
+ isSwimlanesOn: state => {
+ if (!gon?.features?.boardsWithSwimlanes) {
+ return false;
+ }
+
+ return state.isShowingEpicsSwimlanes;
+ },
+ getIssueById: state => id => {
+ return state.issues[id] || {};
+ },
+
+ getIssues: (state, getters) => listId => {
+ const listIssueIds = state.issuesByListId[listId] || [];
+ return listIssueIds.map(id => getters.getIssueById(id));
+ },
+
+ getActiveIssue: state => {
+ return state.issues[state.activeId] || {};
+ },
};
diff --git a/app/assets/javascripts/boards/stores/mutation_types.js b/app/assets/javascripts/boards/stores/mutation_types.js
index 0f96dc2e287..f0a283f6161 100644
--- a/app/assets/javascripts/boards/stores/mutation_types.js
+++ b/app/assets/javascripts/boards/stores/mutation_types.js
@@ -1,25 +1,34 @@
export const SET_INITIAL_BOARD_DATA = 'SET_INITIAL_BOARD_DATA';
+export const SET_FILTERS = 'SET_FILTERS';
+export const CREATE_LIST_SUCCESS = 'CREATE_LIST_SUCCESS';
+export const CREATE_LIST_FAILURE = 'CREATE_LIST_FAILURE';
+export const RECEIVE_BOARD_LISTS_SUCCESS = 'RECEIVE_BOARD_LISTS_SUCCESS';
+export const SHOW_PROMOTION_LIST = 'SHOW_PROMOTION_LIST';
export const REQUEST_ADD_LIST = 'REQUEST_ADD_LIST';
export const RECEIVE_ADD_LIST_SUCCESS = 'RECEIVE_ADD_LIST_SUCCESS';
export const RECEIVE_ADD_LIST_ERROR = 'RECEIVE_ADD_LIST_ERROR';
-export const REQUEST_UPDATE_LIST = 'REQUEST_UPDATE_LIST';
-export const RECEIVE_UPDATE_LIST_SUCCESS = 'RECEIVE_UPDATE_LIST_SUCCESS';
-export const RECEIVE_UPDATE_LIST_ERROR = 'RECEIVE_UPDATE_LIST_ERROR';
+export const MOVE_LIST = 'MOVE_LIST';
+export const UPDATE_LIST_FAILURE = 'UPDATE_LIST_FAILURE';
export const REQUEST_REMOVE_LIST = 'REQUEST_REMOVE_LIST';
export const RECEIVE_REMOVE_LIST_SUCCESS = 'RECEIVE_REMOVE_LIST_SUCCESS';
export const RECEIVE_REMOVE_LIST_ERROR = 'RECEIVE_REMOVE_LIST_ERROR';
export const REQUEST_ISSUES_FOR_ALL_LISTS = 'REQUEST_ISSUES_FOR_ALL_LISTS';
+export const RECEIVE_ISSUES_FOR_LIST_FAILURE = 'RECEIVE_ISSUES_FOR_LIST_FAILURE';
+export const RECEIVE_ISSUES_FOR_LIST_SUCCESS = 'RECEIVE_ISSUES_FOR_LIST_SUCCESS';
export const RECEIVE_ISSUES_FOR_ALL_LISTS_SUCCESS = 'RECEIVE_ISSUES_FOR_ALL_LISTS_SUCCESS';
export const RECEIVE_ISSUES_FOR_ALL_LISTS_FAILURE = 'RECEIVE_ISSUES_FOR_ALL_LISTS_FAILURE';
export const REQUEST_ADD_ISSUE = 'REQUEST_ADD_ISSUE';
export const RECEIVE_ADD_ISSUE_SUCCESS = 'RECEIVE_ADD_ISSUE_SUCCESS';
export const RECEIVE_ADD_ISSUE_ERROR = 'RECEIVE_ADD_ISSUE_ERROR';
-export const REQUEST_MOVE_ISSUE = 'REQUEST_MOVE_ISSUE';
-export const RECEIVE_MOVE_ISSUE_SUCCESS = 'RECEIVE_MOVE_ISSUE_SUCCESS';
-export const RECEIVE_MOVE_ISSUE_ERROR = 'RECEIVE_MOVE_ISSUE_ERROR';
+export const MOVE_ISSUE = 'MOVE_ISSUE';
+export const MOVE_ISSUE_SUCCESS = 'MOVE_ISSUE_SUCCESS';
+export const MOVE_ISSUE_FAILURE = 'MOVE_ISSUE_FAILURE';
export const REQUEST_UPDATE_ISSUE = 'REQUEST_UPDATE_ISSUE';
export const RECEIVE_UPDATE_ISSUE_SUCCESS = 'RECEIVE_UPDATE_ISSUE_SUCCESS';
export const RECEIVE_UPDATE_ISSUE_ERROR = 'RECEIVE_UPDATE_ISSUE_ERROR';
+export const ADD_ISSUE_TO_LIST = 'ADD_ISSUE_TO_LIST';
+export const ADD_ISSUE_TO_LIST_FAILURE = 'ADD_ISSUE_TO_LIST_FAILURE';
export const SET_CURRENT_PAGE = 'SET_CURRENT_PAGE';
export const TOGGLE_EMPTY_STATE = 'TOGGLE_EMPTY_STATE';
export const SET_ACTIVE_ID = 'SET_ACTIVE_ID';
+export const UPDATE_ISSUE_BY_ID = 'UPDATE_ISSUE_BY_ID';
diff --git a/app/assets/javascripts/boards/stores/mutations.js b/app/assets/javascripts/boards/stores/mutations.js
index ca9b911ce5b..faeb3e25a71 100644
--- a/app/assets/javascripts/boards/stores/mutations.js
+++ b/app/assets/javascripts/boards/stores/mutations.js
@@ -1,19 +1,55 @@
+import Vue from 'vue';
+import { sortBy, pull } from 'lodash';
+import { formatIssue, moveIssueListHelper } from '../boards_util';
import * as mutationTypes from './mutation_types';
+import { __ } from '~/locale';
+import { getIdFromGraphQLId } from '~/graphql_shared/utils';
const notImplemented = () => {
/* eslint-disable-next-line @gitlab/require-i18n-strings */
throw new Error('Not implemented!');
};
+const removeIssueFromList = (state, listId, issueId) => {
+ Vue.set(state.issuesByListId, listId, pull(state.issuesByListId[listId], issueId));
+};
+
+const addIssueToList = ({ state, listId, issueId, moveBeforeId, moveAfterId, atIndex }) => {
+ const listIssues = state.issuesByListId[listId];
+ let newIndex = atIndex || 0;
+ if (moveBeforeId) {
+ newIndex = listIssues.indexOf(moveBeforeId) + 1;
+ } else if (moveAfterId) {
+ newIndex = listIssues.indexOf(moveAfterId);
+ }
+ listIssues.splice(newIndex, 0, issueId);
+ Vue.set(state.issuesByListId, listId, listIssues);
+};
+
export default {
- [mutationTypes.SET_INITIAL_BOARD_DATA]: (state, data) => {
- const { boardType, ...endpoints } = data;
+ [mutationTypes.SET_INITIAL_BOARD_DATA](state, data) {
+ const { boardType, disabled, showPromotion, ...endpoints } = data;
state.endpoints = endpoints;
state.boardType = boardType;
+ state.disabled = disabled;
+ state.showPromotion = showPromotion;
},
- [mutationTypes.SET_ACTIVE_ID](state, id) {
+ [mutationTypes.RECEIVE_BOARD_LISTS_SUCCESS]: (state, lists) => {
+ state.boardLists = lists;
+ },
+
+ [mutationTypes.SET_ACTIVE_ID](state, { id, sidebarType }) {
state.activeId = id;
+ state.sidebarType = sidebarType;
+ },
+
+ [mutationTypes.SET_FILTERS](state, filterParams) {
+ state.filterParams = filterParams;
+ },
+
+ [mutationTypes.CREATE_LIST_FAILURE]: state => {
+ state.error = __('An error occurred while creating the list. Please try again.');
},
[mutationTypes.REQUEST_ADD_LIST]: () => {
@@ -28,16 +64,17 @@ export default {
notImplemented();
},
- [mutationTypes.REQUEST_UPDATE_LIST]: () => {
- notImplemented();
- },
-
- [mutationTypes.RECEIVE_UPDATE_LIST_SUCCESS]: () => {
- notImplemented();
+ [mutationTypes.MOVE_LIST]: (state, { movedList, listAtNewIndex }) => {
+ const { boardLists } = state;
+ const movedListIndex = state.boardLists.findIndex(l => l.id === movedList.id);
+ Vue.set(boardLists, movedListIndex, movedList);
+ Vue.set(boardLists, movedListIndex.position + 1, listAtNewIndex);
+ Vue.set(state, 'boardLists', sortBy(boardLists, 'position'));
},
- [mutationTypes.RECEIVE_UPDATE_LIST_ERROR]: () => {
- notImplemented();
+ [mutationTypes.UPDATE_LIST_FAILURE]: (state, backupList) => {
+ state.error = __('An error occurred while updating the list. Please try again.');
+ Vue.set(state, 'boardLists', backupList);
},
[mutationTypes.REQUEST_REMOVE_LIST]: () => {
@@ -52,17 +89,41 @@ export default {
notImplemented();
},
+ [mutationTypes.RECEIVE_ISSUES_FOR_LIST_SUCCESS]: (state, { listIssues, listId }) => {
+ const { listData, issues } = listIssues;
+ Vue.set(state, 'issues', { ...state.issues, ...issues });
+ Vue.set(state.issuesByListId, listId, listData[listId]);
+ const listIndex = state.boardLists.findIndex(l => l.id === listId);
+ Vue.set(state.boardLists[listIndex], 'loading', false);
+ },
+
+ [mutationTypes.RECEIVE_ISSUES_FOR_LIST_FAILURE]: (state, listId) => {
+ state.error = __('An error occurred while fetching the board issues. Please reload the page.');
+ const listIndex = state.boardLists.findIndex(l => l.id === listId);
+ Vue.set(state.boardLists[listIndex], 'loading', false);
+ },
+
[mutationTypes.REQUEST_ISSUES_FOR_ALL_LISTS]: state => {
state.isLoadingIssues = true;
},
- [mutationTypes.RECEIVE_ISSUES_FOR_ALL_LISTS_SUCCESS]: (state, listIssues) => {
- state.issuesByListId = listIssues;
+ [mutationTypes.RECEIVE_ISSUES_FOR_ALL_LISTS_SUCCESS]: (state, { listData, issues }) => {
+ state.issuesByListId = listData;
+ state.issues = issues;
state.isLoadingIssues = false;
},
+ [mutationTypes.UPDATE_ISSUE_BY_ID]: (state, { issueId, prop, value }) => {
+ if (!state.issues[issueId]) {
+ /* eslint-disable-next-line @gitlab/require-i18n-strings */
+ throw new Error('No issue found.');
+ }
+
+ Vue.set(state.issues[issueId], prop, value);
+ },
+
[mutationTypes.RECEIVE_ISSUES_FOR_ALL_LISTS_FAILURE]: state => {
- state.listIssueFetchFailure = true;
+ state.error = __('An error occurred while fetching the board issues. Please reload the page.');
state.isLoadingIssues = false;
},
@@ -78,16 +139,38 @@ export default {
notImplemented();
},
- [mutationTypes.REQUEST_MOVE_ISSUE]: () => {
- notImplemented();
+ [mutationTypes.MOVE_ISSUE]: (
+ state,
+ { originalIssue, fromListId, toListId, moveBeforeId, moveAfterId },
+ ) => {
+ const fromList = state.boardLists.find(l => l.id === fromListId);
+ const toList = state.boardLists.find(l => l.id === toListId);
+
+ const issue = moveIssueListHelper(originalIssue, fromList, toList);
+ Vue.set(state.issues, issue.id, issue);
+
+ removeIssueFromList(state, fromListId, issue.id);
+ addIssueToList({ state, listId: toListId, issueId: issue.id, moveBeforeId, moveAfterId });
},
- [mutationTypes.RECEIVE_MOVE_ISSUE_SUCCESS]: () => {
- notImplemented();
+ [mutationTypes.MOVE_ISSUE_SUCCESS]: (state, { issue }) => {
+ const issueId = getIdFromGraphQLId(issue.id);
+ Vue.set(state.issues, issueId, formatIssue({ ...issue, id: issueId }));
},
- [mutationTypes.RECEIVE_MOVE_ISSUE_ERROR]: () => {
- notImplemented();
+ [mutationTypes.MOVE_ISSUE_FAILURE]: (
+ state,
+ { originalIssue, fromListId, toListId, originalIndex },
+ ) => {
+ state.error = __('An error occurred while moving the issue. Please try again.');
+ Vue.set(state.issues, originalIssue.id, originalIssue);
+ removeIssueFromList(state, toListId, originalIssue.id);
+ addIssueToList({
+ state,
+ listId: fromListId,
+ issueId: originalIssue.id,
+ atIndex: originalIndex,
+ });
},
[mutationTypes.REQUEST_UPDATE_ISSUE]: () => {
@@ -102,6 +185,18 @@ export default {
notImplemented();
},
+ [mutationTypes.ADD_ISSUE_TO_LIST]: (state, { list, issue, position }) => {
+ const listIssues = state.issuesByListId[list.id];
+ listIssues.splice(position, 0, issue.id);
+ Vue.set(state.issuesByListId, list.id, listIssues);
+ Vue.set(state.issues, issue.id, issue);
+ },
+
+ [mutationTypes.ADD_ISSUE_TO_LIST_FAILURE]: (state, { list, issue }) => {
+ state.error = __('An error occurred while creating the issue. Please try again.');
+ removeIssueFromList(state, list.id, issue.id);
+ },
+
[mutationTypes.SET_CURRENT_PAGE]: () => {
notImplemented();
},
diff --git a/app/assets/javascripts/boards/stores/state.js b/app/assets/javascripts/boards/stores/state.js
index cb6930774ed..be937d68c6c 100644
--- a/app/assets/javascripts/boards/stores/state.js
+++ b/app/assets/javascripts/boards/stores/state.js
@@ -3,9 +3,17 @@ import { inactiveId } from '~/boards/constants';
export default () => ({
endpoints: {},
boardType: null,
+ disabled: false,
+ showPromotion: false,
isShowingLabels: true,
activeId: inactiveId,
+ sidebarType: '',
+ boardLists: [],
issuesByListId: {},
+ issues: {},
isLoadingIssues: false,
- listIssueFetchFailure: false,
+ filterParams: {},
+ error: undefined,
+ // TODO: remove after ce/ee split of board_content.vue
+ isShowingEpicsSwimlanes: false,
});
diff --git a/app/assets/javascripts/branches/ajax_loading_spinner.js b/app/assets/javascripts/branches/ajax_loading_spinner.js
new file mode 100644
index 00000000000..79f4f919f3d
--- /dev/null
+++ b/app/assets/javascripts/branches/ajax_loading_spinner.js
@@ -0,0 +1,31 @@
+import $ from 'jquery';
+
+export default class AjaxLoadingSpinner {
+ static init() {
+ const $elements = $('.js-ajax-loading-spinner');
+ $elements.on('ajax:beforeSend', AjaxLoadingSpinner.ajaxBeforeSend);
+ }
+
+ static ajaxBeforeSend(e) {
+ const button = e.target;
+ const newButton = document.createElement('button');
+ newButton.classList.add('btn', 'btn-default', 'disabled', 'gl-button');
+ newButton.setAttribute('disabled', 'disabled');
+
+ const spinner = document.createElement('span');
+ spinner.classList.add('align-text-bottom', 'gl-spinner', 'gl-spinner-sm', 'gl-spinner-orange');
+ newButton.appendChild(spinner);
+
+ button.classList.add('hidden');
+ button.parentNode.insertBefore(newButton, button.nextSibling);
+
+ $(button).one('ajax:error', () => {
+ newButton.remove();
+ button.classList.remove('hidden');
+ });
+
+ $(button).one('ajax:success', () => {
+ $(button).off('ajax:beforeSend', AjaxLoadingSpinner.ajaxBeforeSend);
+ });
+ }
+}
diff --git a/app/assets/javascripts/ci_lint/components/ci_lint.vue b/app/assets/javascripts/ci_lint/components/ci_lint.vue
new file mode 100644
index 00000000000..135d02e4f76
--- /dev/null
+++ b/app/assets/javascripts/ci_lint/components/ci_lint.vue
@@ -0,0 +1,14 @@
+<script>
+export default {
+ props: {
+ endpoint: {
+ type: String,
+ required: true,
+ },
+ },
+};
+</script>
+
+<template
+ ><div></div
+></template>
diff --git a/app/assets/javascripts/ci_lint/components/ci_lint_results.vue b/app/assets/javascripts/ci_lint/components/ci_lint_results.vue
new file mode 100644
index 00000000000..9fd1bd30c49
--- /dev/null
+++ b/app/assets/javascripts/ci_lint/components/ci_lint_results.vue
@@ -0,0 +1,9 @@
+<script>
+export default {
+ props: {},
+};
+</script>
+
+<template
+ ><div></div
+></template>
diff --git a/app/assets/javascripts/ci_lint/index.js b/app/assets/javascripts/ci_lint/index.js
new file mode 100644
index 00000000000..ed2cf1fe714
--- /dev/null
+++ b/app/assets/javascripts/ci_lint/index.js
@@ -0,0 +1,18 @@
+import Vue from 'vue';
+import CILint from './components/ci_lint.vue';
+
+export default (containerId = '#js-ci-lint') => {
+ const containerEl = document.querySelector(containerId);
+ const { endpoint } = containerEl.dataset;
+
+ return new Vue({
+ el: containerEl,
+ render(createElement) {
+ return createElement(CILint, {
+ props: {
+ endpoint,
+ },
+ });
+ },
+ });
+};
diff --git a/app/assets/javascripts/ci_variable_list/ci_variable_list.js b/app/assets/javascripts/ci_variable_list/ci_variable_list.js
index 5c79f245f6d..cb1935c863d 100644
--- a/app/assets/javascripts/ci_variable_list/ci_variable_list.js
+++ b/app/assets/javascripts/ci_variable_list/ci_variable_list.js
@@ -53,7 +53,7 @@ export default class VariableList {
},
environment_scope: {
// We can't use a `.js-` class here because
- // gl_dropdown replaces the <input> and doesn't copy over the class
+ // deprecated_jquery_dropdown replaces the <input> and doesn't copy over the class
// See https://gitlab.com/gitlab-org/gitlab-foss/issues/42458
selector: `input[name="${this.formField}[variables_attributes][][environment_scope]"]`,
default: '*',
diff --git a/app/assets/javascripts/ci_variable_list/components/ci_environments_dropdown.vue b/app/assets/javascripts/ci_variable_list/components/ci_environments_dropdown.vue
index d22fef27964..0e09ae108ea 100644
--- a/app/assets/javascripts/ci_variable_list/components/ci_environments_dropdown.vue
+++ b/app/assets/javascripts/ci_variable_list/components/ci_environments_dropdown.vue
@@ -67,7 +67,7 @@ export default {
</script>
<template>
<gl-deprecated-dropdown :text="value">
- <gl-search-box-by-type v-model.trim="searchTerm" class="m-2" />
+ <gl-search-box-by-type v-model.trim="searchTerm" class="gl-m-3" />
<gl-deprecated-dropdown-item
v-for="environment in filteredResults"
:key="environment"
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 018704bff74..501c82b419e 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
@@ -46,6 +46,7 @@ export default {
{
key: 'actions',
label: '',
+ tdClass: 'text-right',
customStyle: { width: '35px' },
},
],
diff --git a/app/assets/javascripts/ci_variable_list/constants.js b/app/assets/javascripts/ci_variable_list/constants.js
index ef304c7ccee..564e1d01242 100644
--- a/app/assets/javascripts/ci_variable_list/constants.js
+++ b/app/assets/javascripts/ci_variable_list/constants.js
@@ -1,6 +1,5 @@
import { __ } from '~/locale';
-// eslint-disable import/prefer-default-export
export const ADD_CI_VARIABLE_MODAL_ID = 'add-ci-variable';
export const displayText = {
diff --git a/app/assets/javascripts/ci_variable_list/store/getters.js b/app/assets/javascripts/ci_variable_list/store/getters.js
index 14b728302f9..619ad54cad6 100644
--- a/app/assets/javascripts/ci_variable_list/store/getters.js
+++ b/app/assets/javascripts/ci_variable_list/store/getters.js
@@ -1,6 +1,3 @@
-/* eslint-disable import/prefer-default-export */
-// Disabling import/prefer-default-export can be
-// removed once a second getter is added to this file
import { uniq } from 'lodash';
export const joinedEnvironments = state => {
diff --git a/app/assets/javascripts/clusters/clusters_bundle.js b/app/assets/javascripts/clusters/clusters_bundle.js
index 92517203972..a75646db162 100644
--- a/app/assets/javascripts/clusters/clusters_bundle.js
+++ b/app/assets/javascripts/clusters/clusters_bundle.js
@@ -237,7 +237,7 @@ export default class Clusters {
}
addBannerCloseHandler(el, status) {
- el.querySelector('.js-close-banner').addEventListener('click', () => {
+ el.querySelector('.js-close').addEventListener('click', () => {
el.classList.add('hidden');
this.setBannerDismissedState(status, true);
});
diff --git a/app/assets/javascripts/clusters/components/application_row.vue b/app/assets/javascripts/clusters/components/application_row.vue
index c86db28515f..412260da958 100644
--- a/app/assets/javascripts/clusters/components/application_row.vue
+++ b/app/assets/javascripts/clusters/components/application_row.vue
@@ -1,9 +1,8 @@
<script>
-import { GlLink, GlModalDirective, GlSprintf } from '@gitlab/ui';
+import { GlLink, GlModalDirective, GlSprintf, GlButton, GlAlert } from '@gitlab/ui';
import { s__, __, sprintf } from '~/locale';
import eventHub from '../event_hub';
import identicon from '../../vue_shared/components/identicon.vue';
-import loadingButton from '../../vue_shared/components/loading_button.vue';
import UninstallApplicationButton from './uninstall_application_button.vue';
import UninstallApplicationConfirmationModal from './uninstall_application_confirmation_modal.vue';
import UpdateApplicationConfirmationModal from './update_application_confirmation_modal.vue';
@@ -12,9 +11,10 @@ import { APPLICATION_STATUS, ELASTIC_STACK } from '../constants';
export default {
components: {
- loadingButton,
+ GlButton,
identicon,
GlLink,
+ GlAlert,
GlSprintf,
UninstallApplicationButton,
UninstallApplicationConfirmationModal,
@@ -382,24 +382,28 @@ export default {
</template>
</div>
- <div
+ <gl-alert
v-if="updateFailed && !isUpdating"
- class="bs-callout bs-callout-danger cluster-application-banner mt-2 mb-0 js-cluster-application-update-details"
+ variant="danger"
+ :dismissible="false"
+ class="gl-mt-3 gl-mb-0 js-cluster-application-update-details"
>
{{ updateFailureDescription }}
- </div>
+ </gl-alert>
<template v-if="updateAvailable || updateFailed || isUpdating">
<template v-if="updatingNeedsConfirmation">
- <loading-button
+ <gl-button
v-gl-modal-directive="updateModalId"
- class="btn btn-primary js-cluster-application-update-button mt-2"
+ class="js-cluster-application-update-button mt-2"
+ variant="info"
+ category="primary"
:loading="isUpdating"
:disabled="isUpdating"
- :label="updateButtonLabel"
data-qa-selector="update_button_with_confirmation"
:data-qa-application="id"
- />
-
+ >
+ {{ updateButtonLabel }}
+ </gl-button>
<update-application-confirmation-modal
:application="id"
:application-title="title"
@@ -407,16 +411,19 @@ export default {
/>
</template>
- <loading-button
+ <gl-button
v-else
- class="btn btn-primary js-cluster-application-update-button mt-2"
+ class="js-cluster-application-update-button mt-2"
+ variant="info"
+ category="primary"
:loading="isUpdating"
:disabled="isUpdating"
- :label="updateButtonLabel"
data-qa-selector="update_button"
:data-qa-application="id"
@click="updateConfirmed"
- />
+ >
+ {{ updateButtonLabel }}
+ </gl-button>
</template>
</div>
</div>
@@ -431,16 +438,18 @@ export default {
}}</a>
</div>
<div class="btn-group table-action-buttons">
- <loading-button
+ <gl-button
v-if="displayInstallButton"
:loading="installButtonLoading"
:disabled="disabled || installButtonDisabled"
- :label="installButtonLabel"
class="js-cluster-application-install-button"
+ variant="default"
data-qa-selector="install_button"
:data-qa-application="id"
@click="installClicked"
- />
+ >
+ {{ installButtonLabel }}
+ </gl-button>
<uninstall-application-button
v-if="displayUninstallButton"
v-gl-modal-directive="uninstallModalId"
diff --git a/app/assets/javascripts/clusters/components/knative_domain_editor.vue b/app/assets/javascripts/clusters/components/knative_domain_editor.vue
index 1236d2a46c9..2617ea0bdea 100644
--- a/app/assets/javascripts/clusters/components/knative_domain_editor.vue
+++ b/app/assets/javascripts/clusters/components/knative_domain_editor.vue
@@ -6,8 +6,8 @@ import {
GlLoadingIcon,
GlSearchBoxByType,
GlSprintf,
+ GlButton,
} from '@gitlab/ui';
-import LoadingButton from '~/vue_shared/components/loading_button.vue';
import ClipboardButton from '../../vue_shared/components/clipboard_button.vue';
import { __, s__ } from '~/locale';
@@ -17,7 +17,7 @@ const { UPDATING, UNINSTALLING } = APPLICATION_STATUS;
export default {
components: {
- LoadingButton,
+ GlButton,
ClipboardButton,
GlLoadingIcon,
GlDeprecatedDropdown,
@@ -130,7 +130,7 @@ export default {
<gl-search-box-by-type
v-model.trim="searchQuery"
:placeholder="s__('ClusterIntegration|Search domains')"
- class="m-2"
+ class="gl-m-3"
/>
<gl-deprecated-dropdown-item
v-for="domain in filteredDomains"
@@ -215,13 +215,16 @@ export default {
}}
</p>
- <loading-button
- class="btn-success js-knative-save-domain-button mt-3 ml-3"
+ <gl-button
+ class="js-knative-save-domain-button gl-mt-5 gl-ml-5"
+ variant="success"
+ category="primary"
:loading="saving"
:disabled="saveButtonDisabled"
- :label="saveButtonLabel"
@click="$emit('save')"
- />
+ >
+ {{ saveButtonLabel }}
+ </gl-button>
</template>
</div>
</template>
diff --git a/app/assets/javascripts/clusters/components/new_cluster.vue b/app/assets/javascripts/clusters/components/new_cluster.vue
new file mode 100644
index 00000000000..2e74ad073c5
--- /dev/null
+++ b/app/assets/javascripts/clusters/components/new_cluster.vue
@@ -0,0 +1,34 @@
+<script>
+import { GlLink, GlSprintf } from '@gitlab/ui';
+import { mapState } from 'vuex';
+import { s__ } from '~/locale';
+
+export default {
+ i18n: {
+ title: s__('ClusterIntegration|Enter the details for your Kubernetes cluster'),
+ information: s__(
+ 'ClusterIntegration|Please enter access information for your Kubernetes cluster. If you need help, you can read our %{linkStart}documentation%{linkEnd} on Kubernetes',
+ ),
+ },
+ components: {
+ GlLink,
+ GlSprintf,
+ },
+ computed: {
+ ...mapState(['clusterConnectHelpPath']),
+ },
+};
+</script>
+
+<template>
+ <div>
+ <h4>{{ $options.i18n.title }}</h4>
+ <p>
+ <gl-sprintf :message="$options.i18n.information">
+ <template #link="{ content }">
+ <gl-link :href="clusterConnectHelpPath" target="_blank">{{ content }}</gl-link>
+ </template>
+ </gl-sprintf>
+ </p>
+ </div>
+</template>
diff --git a/app/assets/javascripts/clusters/components/remove_cluster_confirmation.vue b/app/assets/javascripts/clusters/components/remove_cluster_confirmation.vue
index 3e3b102f0aa..c157b04b4f5 100644
--- a/app/assets/javascripts/clusters/components/remove_cluster_confirmation.vue
+++ b/app/assets/javascripts/clusters/components/remove_cluster_confirmation.vue
@@ -1,6 +1,7 @@
<script>
+/* eslint-disable vue/no-v-html */
import { escape } from 'lodash';
-import { GlModal, GlButton, GlDeprecatedButton, GlFormInput, GlSprintf } from '@gitlab/ui';
+import { GlModal, GlButton, GlFormInput, GlSprintf } from '@gitlab/ui';
import SplitButton from '~/vue_shared/components/split_button.vue';
import { s__, sprintf } from '~/locale';
import csrf from '~/lib/utils/csrf';
@@ -28,7 +29,6 @@ export default {
SplitButton,
GlModal,
GlButton,
- GlDeprecatedButton,
GlFormInput,
GlSprintf,
},
@@ -174,24 +174,31 @@ export default {
}}</span>
</template>
<template #modal-footer>
- <gl-deprecated-button variant="secondary" @click="handleCancel">{{
- s__('Cancel')
- }}</gl-deprecated-button>
+ <gl-button variant="secondary" @click="handleCancel">{{ s__('Cancel') }}</gl-button>
<template v-if="confirmCleanup">
- <gl-deprecated-button :disabled="!canSubmit" variant="warning" @click="handleSubmit">{{
- s__('ClusterIntegration|Remove integration')
- }}</gl-deprecated-button>
- <gl-deprecated-button
+ <gl-button
+ :disabled="!canSubmit"
+ variant="warning"
+ category="primary"
+ @click="handleSubmit"
+ >{{ s__('ClusterIntegration|Remove integration') }}</gl-button
+ >
+ <gl-button
:disabled="!canSubmit"
variant="danger"
+ category="primary"
@click="handleSubmit(true)"
- >{{ s__('ClusterIntegration|Remove integration and resources') }}</gl-deprecated-button
+ >{{ s__('ClusterIntegration|Remove integration and resources') }}</gl-button
>
</template>
<template v-else>
- <gl-deprecated-button :disabled="!canSubmit" variant="danger" @click="handleSubmit">{{
- s__('ClusterIntegration|Remove integration')
- }}</gl-deprecated-button>
+ <gl-button
+ :disabled="!canSubmit"
+ variant="danger"
+ category="primary"
+ @click="handleSubmit"
+ >{{ s__('ClusterIntegration|Remove integration') }}</gl-button
+ >
</template>
</template>
</gl-modal>
diff --git a/app/assets/javascripts/clusters/components/uninstall_application_button.vue b/app/assets/javascripts/clusters/components/uninstall_application_button.vue
index 8465312d84d..73191d6d84d 100644
--- a/app/assets/javascripts/clusters/components/uninstall_application_button.vue
+++ b/app/assets/javascripts/clusters/components/uninstall_application_button.vue
@@ -1,5 +1,5 @@
<script>
-import LoadingButton from '~/vue_shared/components/loading_button.vue';
+import { GlButton } from '@gitlab/ui';
import { APPLICATION_STATUS } from '~/clusters/constants';
import { __ } from '~/locale';
@@ -7,7 +7,7 @@ const { UPDATING, UNINSTALLING } = APPLICATION_STATUS;
export default {
components: {
- LoadingButton,
+ GlButton,
},
props: {
status: {
@@ -30,5 +30,7 @@ export default {
</script>
<template>
- <loading-button :label="label" :disabled="disabled" :loading="loading" />
+ <gl-button :disabled="disabled" variant="default" :loading="loading">
+ {{ label }}
+ </gl-button>
</template>
diff --git a/app/assets/javascripts/clusters/components/uninstall_application_confirmation_modal.vue b/app/assets/javascripts/clusters/components/uninstall_application_confirmation_modal.vue
index e33431d2ea1..f82f4dd5012 100644
--- a/app/assets/javascripts/clusters/components/uninstall_application_confirmation_modal.vue
+++ b/app/assets/javascripts/clusters/components/uninstall_application_confirmation_modal.vue
@@ -1,4 +1,5 @@
<script>
+/* eslint-disable vue/no-v-html */
import { GlModal } from '@gitlab/ui';
import trackUninstallButtonClickMixin from 'ee_else_ce/clusters/mixins/track_uninstall_button_click';
import { sprintf, s__ } from '~/locale';
diff --git a/app/assets/javascripts/clusters/components/update_application_confirmation_modal.vue b/app/assets/javascripts/clusters/components/update_application_confirmation_modal.vue
index 04aa28e9b74..0aedc6e84fa 100644
--- a/app/assets/javascripts/clusters/components/update_application_confirmation_modal.vue
+++ b/app/assets/javascripts/clusters/components/update_application_confirmation_modal.vue
@@ -1,4 +1,5 @@
<script>
+/* eslint-disable vue/no-v-html */
import { GlModal } from '@gitlab/ui';
import { sprintf, s__ } from '~/locale';
import { ELASTIC_STACK } from '../constants';
diff --git a/app/assets/javascripts/clusters/new_cluster.js b/app/assets/javascripts/clusters/new_cluster.js
new file mode 100644
index 00000000000..71f585fd307
--- /dev/null
+++ b/app/assets/javascripts/clusters/new_cluster.js
@@ -0,0 +1,19 @@
+import Vue from 'vue';
+import NewCluster from './components/new_cluster.vue';
+import { createStore } from './stores/new_cluster';
+
+export default () => {
+ const entryPoint = document.querySelector('#js-cluster-new');
+
+ if (!entryPoint) {
+ return null;
+ }
+
+ return new Vue({
+ el: '#js-cluster-new',
+ store: createStore(entryPoint.dataset),
+ render(createElement) {
+ return createElement(NewCluster);
+ },
+ });
+};
diff --git a/app/assets/javascripts/clusters/stores/new_cluster/index.js b/app/assets/javascripts/clusters/stores/new_cluster/index.js
new file mode 100644
index 00000000000..ae082c07f26
--- /dev/null
+++ b/app/assets/javascripts/clusters/stores/new_cluster/index.js
@@ -0,0 +1,12 @@
+import Vue from 'vue';
+import Vuex from 'vuex';
+import state from './state';
+
+Vue.use(Vuex);
+
+export const createStore = initialState =>
+ new Vuex.Store({
+ state: state(initialState),
+ });
+
+export default createStore;
diff --git a/app/assets/javascripts/clusters/stores/new_cluster/state.js b/app/assets/javascripts/clusters/stores/new_cluster/state.js
new file mode 100644
index 00000000000..1ca1ac8de18
--- /dev/null
+++ b/app/assets/javascripts/clusters/stores/new_cluster/state.js
@@ -0,0 +1,3 @@
+export default (initialState = {}) => ({
+ clusterConnectHelpPath: initialState.clusterConnectHelpPath,
+});
diff --git a/app/assets/javascripts/clusters_list/components/clusters.vue b/app/assets/javascripts/clusters_list/components/clusters.vue
index 09d7c0329a9..7b53020fc49 100644
--- a/app/assets/javascripts/clusters_list/components/clusters.vue
+++ b/app/assets/javascripts/clusters_list/components/clusters.vue
@@ -5,7 +5,7 @@ import {
GlLink,
GlLoadingIcon,
GlPagination,
- GlSkeletonLoading,
+ GlDeprecatedSkeletonLoading as GlSkeletonLoading,
GlSprintf,
GlTable,
} from '@gitlab/ui';
diff --git a/app/assets/javascripts/commit/pipelines/pipelines_table.vue b/app/assets/javascripts/commit/pipelines/pipelines_table.vue
index 1526d994770..2f4118c1717 100644
--- a/app/assets/javascripts/commit/pipelines/pipelines_table.vue
+++ b/app/assets/javascripts/commit/pipelines/pipelines_table.vue
@@ -247,7 +247,7 @@ export default {
}}
</p>
<gl-link
- href="/help/ci/merge_request_pipelines/index.html#create-pipelines-in-the-parent-project-for-merge-requests-from-a-forked-project"
+ href="/help/ci/merge_request_pipelines/index.html#run-pipelines-in-the-parent-project-for-merge-requests-from-a-forked-project"
target="_blank"
>
{{ s__('Pipelines|More Information') }}
diff --git a/app/assets/javascripts/commons/jquery.js b/app/assets/javascripts/commons/jquery.js
index c4d663dfc8d..334f95bb27f 100644
--- a/app/assets/javascripts/commons/jquery.js
+++ b/app/assets/javascripts/commons/jquery.js
@@ -2,6 +2,3 @@ import 'jquery';
// common jQuery plugins
import 'jquery-ujs';
-import 'jquery.caret'; // must be imported before at.js
-import '@gitlab/at.js';
-import 'vendor/jquery.scrollTo';
diff --git a/app/assets/javascripts/compare_autocomplete.js b/app/assets/javascripts/compare_autocomplete.js
index 34322755fe9..45ac1bafd61 100644
--- a/app/assets/javascripts/compare_autocomplete.js
+++ b/app/assets/javascripts/compare_autocomplete.js
@@ -5,6 +5,7 @@ import { __ } from './locale';
import axios from './lib/utils/axios_utils';
import { deprecatedCreateFlash as flash } from './flash';
import { capitalizeFirstCharacter } from './lib/utils/text_utility';
+import initDeprecatedJQueryDropdown from '~/deprecated_jquery_dropdown';
export default function initCompareAutocomplete(limitTo = null, clickHandler = () => {}) {
$('.js-compare-dropdown').each(function() {
@@ -13,7 +14,7 @@ export default function initCompareAutocomplete(limitTo = null, clickHandler = (
const $dropdownContainer = $dropdown.closest('.dropdown');
const $fieldInput = $(`input[name="${$dropdown.data('fieldName')}"]`, $dropdownContainer);
const $filterInput = $('input[type="search"]', $dropdownContainer);
- $dropdown.glDropdown({
+ initDeprecatedJQueryDropdown($dropdown, {
data(term, callback) {
const params = {
ref: $dropdown.data('ref'),
diff --git a/app/assets/javascripts/confidential_merge_request/components/dropdown.vue b/app/assets/javascripts/confidential_merge_request/components/dropdown.vue
index b8163ecfab2..5b4bdca46e4 100644
--- a/app/assets/javascripts/confidential_merge_request/components/dropdown.vue
+++ b/app/assets/javascripts/confidential_merge_request/components/dropdown.vue
@@ -1,13 +1,12 @@
<script>
-import { GlDeprecatedDropdown, GlDeprecatedDropdownItem } from '@gitlab/ui';
+import { GlDeprecatedDropdown, GlDeprecatedDropdownItem, GlIcon } from '@gitlab/ui';
import { __ } from '~/locale';
-import Icon from '~/vue_shared/components/icon.vue';
export default {
components: {
GlDeprecatedDropdown,
GlDeprecatedDropdownItem,
- Icon,
+ GlIcon,
},
props: {
projects: {
@@ -41,17 +40,17 @@ export default {
<gl-deprecated-dropdown toggle-class="d-flex align-items-center w-100" class="w-100">
<template #button-content>
<span class="str-truncated-100 mr-2">
- <icon name="lock" />
+ <gl-icon name="lock" />
{{ dropdownText }}
</span>
- <icon name="chevron-down" class="ml-auto" />
+ <gl-icon name="chevron-down" class="ml-auto" />
</template>
<gl-deprecated-dropdown-item
v-for="project in projects"
:key="project.id"
@click="selectProject(project)"
>
- <icon
+ <gl-icon
name="mobile-issue-close"
:class="{ icon: project.id !== selectedProject.id }"
class="js-active-project-check"
diff --git a/app/assets/javascripts/confidential_merge_request/components/project_form_group.vue b/app/assets/javascripts/confidential_merge_request/components/project_form_group.vue
index 88459d5962e..3a6707bc573 100644
--- a/app/assets/javascripts/confidential_merge_request/components/project_form_group.vue
+++ b/app/assets/javascripts/confidential_merge_request/components/project_form_group.vue
@@ -1,5 +1,5 @@
<script>
-import { GlLink, GlSprintf } from '@gitlab/ui';
+import { GlIcon, GlLink, GlSprintf } from '@gitlab/ui';
import { __ } from '../../locale';
import { deprecatedCreateFlash as createFlash } from '../../flash';
import Api from '../../api';
@@ -8,6 +8,7 @@ import Dropdown from './dropdown.vue';
export default {
components: {
+ GlIcon,
GlLink,
GlSprintf,
Dropdown,
@@ -136,7 +137,7 @@ export default {
target="_blank"
>
<span class="sr-only">{{ __('Read more') }}</span>
- <i class="fa fa-question-circle" aria-hidden="true"></i>
+ <gl-icon name="question-o" />
</gl-link>
</p>
</div>
diff --git a/app/assets/javascripts/contributors/stores/actions.js b/app/assets/javascripts/contributors/stores/actions.js
index 393af932fb0..f941c5aa944 100644
--- a/app/assets/javascripts/contributors/stores/actions.js
+++ b/app/assets/javascripts/contributors/stores/actions.js
@@ -3,7 +3,6 @@ import { __ } from '~/locale';
import service from '../services/contributors_service';
import * as types from './mutation_types';
-// eslint-disable-next-line import/prefer-default-export
export const fetchChartData = ({ commit }, endpoint) => {
commit(types.SET_LOADING_STATE, true);
diff --git a/app/assets/javascripts/create_cluster/eks_cluster/components/eks_cluster_configuration_form.vue b/app/assets/javascripts/create_cluster/eks_cluster/components/eks_cluster_configuration_form.vue
index d6c402fcb5d..a653e228e3f 100644
--- a/app/assets/javascripts/create_cluster/eks_cluster/components/eks_cluster_configuration_form.vue
+++ b/app/assets/javascripts/create_cluster/eks_cluster/components/eks_cluster_configuration_form.vue
@@ -1,4 +1,5 @@
<script>
+/* eslint-disable vue/no-v-html */
import { createNamespacedHelpers, mapState, mapActions, mapGetters } from 'vuex';
import { escape } from 'lodash';
import { GlFormInput, GlFormCheckbox } from '@gitlab/ui';
diff --git a/app/assets/javascripts/create_cluster/eks_cluster/components/service_credentials_form.vue b/app/assets/javascripts/create_cluster/eks_cluster/components/service_credentials_form.vue
index e063f9edfd9..5c13cbb2775 100644
--- a/app/assets/javascripts/create_cluster/eks_cluster/components/service_credentials_form.vue
+++ b/app/assets/javascripts/create_cluster/eks_cluster/components/service_credentials_form.vue
@@ -1,15 +1,15 @@
<script>
-import { GlFormInput } from '@gitlab/ui';
+/* eslint-disable vue/no-v-html */
+import { GlFormInput, GlButton } from '@gitlab/ui';
import { escape } from 'lodash';
import { mapState, mapActions } from 'vuex';
import { sprintf, s__, __ } from '~/locale';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
-import LoadingButton from '~/vue_shared/components/loading_button.vue';
export default {
components: {
GlFormInput,
- LoadingButton,
+ GlButton,
ClipboardButton,
},
props: {
@@ -130,13 +130,15 @@ export default {
<gl-form-input id="eks-provision-role-arn" v-model="roleArn" />
<p class="form-text text-muted" v-html="provisionRoleArnHelpText"></p>
</div>
- <loading-button
- class="js-submit-service-credentials btn-success"
+ <gl-button
+ variant="success"
+ category="primary"
type="submit"
:disabled="submitButtonDisabled"
:loading="isCreatingRole"
- :label="submitButtonLabel"
@click.prevent="createRole({ roleArn, externalId })"
- />
+ >
+ {{ submitButtonLabel }}
+ </gl-button>
</form>
</template>
diff --git a/app/assets/javascripts/create_cluster/eks_cluster/constants.js b/app/assets/javascripts/create_cluster/eks_cluster/constants.js
index a850ba89818..471d6e1f0aa 100644
--- a/app/assets/javascripts/create_cluster/eks_cluster/constants.js
+++ b/app/assets/javascripts/create_cluster/eks_cluster/constants.js
@@ -1,2 +1,6 @@
-// eslint-disable-next-line import/prefer-default-export
-export const KUBERNETES_VERSIONS = [{ name: '1.14', value: '1.14' }];
+export const KUBERNETES_VERSIONS = [
+ { name: '1.14', value: '1.14' },
+ { name: '1.15', value: '1.15' },
+ { name: '1.16', value: '1.16', default: true },
+ { name: '1.17', value: '1.17' },
+];
diff --git a/app/assets/javascripts/create_cluster/eks_cluster/store/actions.js b/app/assets/javascripts/create_cluster/eks_cluster/store/actions.js
index caf2729a4c7..5abff3c7831 100644
--- a/app/assets/javascripts/create_cluster/eks_cluster/store/actions.js
+++ b/app/assets/javascripts/create_cluster/eks_cluster/store/actions.js
@@ -56,6 +56,7 @@ export const createCluster = ({ dispatch, state }) => {
environment_scope: state.environmentScope,
managed: state.gitlabManagedCluster,
provider_aws_attributes: {
+ kubernetes_version: state.kubernetesVersion,
region: state.selectedRegion,
vpc_id: state.selectedVpc,
subnet_ids: state.selectedSubnet,
diff --git a/app/assets/javascripts/create_cluster/eks_cluster/store/getters.js b/app/assets/javascripts/create_cluster/eks_cluster/store/getters.js
index bbe4930c191..d8489ca31cf 100644
--- a/app/assets/javascripts/create_cluster/eks_cluster/store/getters.js
+++ b/app/assets/javascripts/create_cluster/eks_cluster/store/getters.js
@@ -1,3 +1,2 @@
-// eslint-disable-next-line import/prefer-default-export
export const subnetValid = ({ selectedSubnet }) =>
Array.isArray(selectedSubnet) && selectedSubnet.length >= 2;
diff --git a/app/assets/javascripts/create_cluster/eks_cluster/store/state.js b/app/assets/javascripts/create_cluster/eks_cluster/store/state.js
index d1337e7ea4a..ed51e95e434 100644
--- a/app/assets/javascripts/create_cluster/eks_cluster/store/state.js
+++ b/app/assets/javascripts/create_cluster/eks_cluster/store/state.js
@@ -1,6 +1,6 @@
import { KUBERNETES_VERSIONS } from '../constants';
-const [{ value: kubernetesVersion }] = KUBERNETES_VERSIONS;
+const kubernetesVersion = KUBERNETES_VERSIONS.find(version => version.default).value;
export default () => ({
createRolePath: null,
diff --git a/app/assets/javascripts/create_item_dropdown.js b/app/assets/javascripts/create_item_dropdown.js
index ec09dafebcb..75e8523adfa 100644
--- a/app/assets/javascripts/create_item_dropdown.js
+++ b/app/assets/javascripts/create_item_dropdown.js
@@ -1,5 +1,5 @@
import { escape } from 'lodash';
-import '~/gl_dropdown';
+import initDeprecatedJQueryDropdown from '~/deprecated_jquery_dropdown';
export default class CreateItemDropdown {
/**
@@ -28,7 +28,7 @@ export default class CreateItemDropdown {
}
buildDropdown() {
- this.$dropdown.glDropdown({
+ initDeprecatedJQueryDropdown(this.$dropdown, {
data: this.getData.bind(this),
filterable: true,
filterRemote: this.getDataRemote,
@@ -67,12 +67,12 @@ export default class CreateItemDropdown {
e.preventDefault();
this.refreshData();
- this.$dropdown.data('glDropdown').selectRowAtIndex();
+ this.$dropdown.data('deprecatedJQueryDropdown').selectRowAtIndex();
}
refreshData() {
// Refresh the dropdown's data, which ends up calling `getData`
- this.$dropdown.data('glDropdown').remote.execute();
+ this.$dropdown.data('deprecatedJQueryDropdown').remote.execute();
}
getData(term, callback) {
diff --git a/app/assets/javascripts/custom_metrics/components/custom_metrics_form_fields.vue b/app/assets/javascripts/custom_metrics/components/custom_metrics_form_fields.vue
index f5207b47f69..6f8455e4bcf 100644
--- a/app/assets/javascripts/custom_metrics/components/custom_metrics_form_fields.vue
+++ b/app/assets/javascripts/custom_metrics/components/custom_metrics_form_fields.vue
@@ -1,8 +1,14 @@
<script>
-import { GlFormInput, GlLink, GlFormGroup, GlFormRadioGroup, GlLoadingIcon } from '@gitlab/ui';
+import {
+ GlFormInput,
+ GlLink,
+ GlFormGroup,
+ GlFormRadioGroup,
+ GlLoadingIcon,
+ GlIcon,
+} from '@gitlab/ui';
import { debounce } from 'lodash';
import { __, s__ } from '~/locale';
-import Icon from '~/vue_shared/components/icon.vue';
import csrf from '~/lib/utils/csrf';
import axios from '~/lib/utils/axios_utils';
import statusCodes from '~/lib/utils/http_status';
@@ -37,7 +43,7 @@ export default {
GlFormGroup,
GlFormRadioGroup,
GlLoadingIcon,
- Icon,
+ GlIcon,
},
props: {
formOperation: {
@@ -229,7 +235,7 @@ export default {
{{ s__('Metrics|Must be a valid PromQL query.') }}
<gl-link href="https://prometheus.io/docs/prometheus/latest/querying/basics/" tabindex="-1">
{{ s__('Metrics|Prometheus Query Documentation') }}
- <icon name="external-link" :size="12" />
+ <gl-icon name="external-link" :size="12" />
</gl-link>
</span>
</gl-form-group>
diff --git a/app/assets/javascripts/cycle_analytics/components/banner.vue b/app/assets/javascripts/cycle_analytics/components/banner.vue
index 0db9d2dbcf9..4448d909c9b 100644
--- a/app/assets/javascripts/cycle_analytics/components/banner.vue
+++ b/app/assets/javascripts/cycle_analytics/components/banner.vue
@@ -1,10 +1,11 @@
<script>
+/* eslint-disable vue/no-v-html */
import iconCycleAnalyticsSplash from 'icons/_icon_cycle_analytics_splash.svg';
-import Icon from '~/vue_shared/components/icon.vue';
+import { GlIcon } from '@gitlab/ui';
export default {
components: {
- Icon,
+ GlIcon,
},
props: {
documentationLink: {
@@ -32,7 +33,7 @@ export default {
type="button"
@click="dismissOverviewDialog"
>
- <icon name="close" />
+ <gl-icon name="close" />
</button>
<div class="svg-container" v-html="iconCycleAnalyticsSplash"></div>
<div class="inner-content">
diff --git a/app/assets/javascripts/cycle_analytics/components/stage_review_component.vue b/app/assets/javascripts/cycle_analytics/components/stage_review_component.vue
index dc13f409462..33b4e649ab0 100644
--- a/app/assets/javascripts/cycle_analytics/components/stage_review_component.vue
+++ b/app/assets/javascripts/cycle_analytics/components/stage_review_component.vue
@@ -3,14 +3,12 @@ import { GlIcon } from '@gitlab/ui';
import userAvatarImage from '../../vue_shared/components/user_avatar/user_avatar_image.vue';
import limitWarning from './limit_warning_component.vue';
import totalTime from './total_time_component.vue';
-import icon from '../../vue_shared/components/icon.vue';
export default {
components: {
userAvatarImage,
totalTime,
limitWarning,
- icon,
GlIcon,
},
props: {
@@ -60,7 +58,7 @@ export default {
</template>
<template v-else>
<span v-if="mergeRequest.branch" class="merge-request-branch">
- <icon :size="16" name="fork" />
+ <gl-icon :size="16" name="fork" />
<a :href="mergeRequest.branch.url"> {{ mergeRequest.branch.name }} </a>
</span>
</template>
diff --git a/app/assets/javascripts/cycle_analytics/components/stage_staging_component.vue b/app/assets/javascripts/cycle_analytics/components/stage_staging_component.vue
index 2a507b7e601..ba2be2e5167 100644
--- a/app/assets/javascripts/cycle_analytics/components/stage_staging_component.vue
+++ b/app/assets/javascripts/cycle_analytics/components/stage_staging_component.vue
@@ -1,16 +1,17 @@
<script>
+/* eslint-disable vue/no-v-html */
+import { GlIcon } from '@gitlab/ui';
import userAvatarImage from '../../vue_shared/components/user_avatar/user_avatar_image.vue';
import iconBranch from '../svg/icon_branch.svg';
import limitWarning from './limit_warning_component.vue';
import totalTime from './total_time_component.vue';
-import icon from '../../vue_shared/components/icon.vue';
export default {
components: {
userAvatarImage,
totalTime,
limitWarning,
- icon,
+ GlIcon,
},
props: {
items: {
@@ -44,7 +45,7 @@ export default {
<user-avatar-image :img-src="build.author.avatarUrl" />
<h5 class="item-title">
<a :href="build.url" class="pipeline-id"> #{{ build.id }} </a>
- <icon :size="16" name="fork" />
+ <gl-icon :size="16" name="fork" />
<a :href="build.branch.url" class="ref-name"> {{ build.branch.name }} </a>
<span class="icon-branch" v-html="iconBranch"> </span>
<a :href="build.commitUrl" class="commit-sha"> {{ build.shortSha }} </a>
diff --git a/app/assets/javascripts/cycle_analytics/components/stage_test_component.vue b/app/assets/javascripts/cycle_analytics/components/stage_test_component.vue
index caff6f9c349..cd49b3c5222 100644
--- a/app/assets/javascripts/cycle_analytics/components/stage_test_component.vue
+++ b/app/assets/javascripts/cycle_analytics/components/stage_test_component.vue
@@ -1,15 +1,16 @@
<script>
+/* eslint-disable vue/no-v-html */
+import { GlIcon } from '@gitlab/ui';
import iconBuildStatus from '../svg/icon_build_status.svg';
import iconBranch from '../svg/icon_branch.svg';
import limitWarning from './limit_warning_component.vue';
import totalTime from './total_time_component.vue';
-import icon from '../../vue_shared/components/icon.vue';
export default {
components: {
totalTime,
limitWarning,
- icon,
+ GlIcon,
},
props: {
items: {
@@ -46,7 +47,7 @@ export default {
<span class="icon-build-status" v-html="iconBuildStatus"> </span>
<a :href="build.url" class="item-build-name"> {{ build.name }} </a> &middot;
<a :href="build.url" class="pipeline-id"> #{{ build.id }} </a>
- <icon :size="16" name="fork" />
+ <gl-icon :size="16" name="fork" />
<a :href="build.branch.url" class="ref-name"> {{ build.branch.name }} </a>
<span class="icon-branch" v-html="iconBranch"> </span>
<a :href="build.commitUrl" class="commit-sha"> {{ build.shortSha }} </a>
diff --git a/app/assets/javascripts/cycle_analytics/cycle_analytics_store.js b/app/assets/javascripts/cycle_analytics/cycle_analytics_store.js
index 4f9069f61a5..3a160d0532c 100644
--- a/app/assets/javascripts/cycle_analytics/cycle_analytics_store.js
+++ b/app/assets/javascripts/cycle_analytics/cycle_analytics_store.js
@@ -23,9 +23,6 @@ const EMPTY_STAGE_TEXTS = {
staging: __(
'The staging stage shows the time between merging the MR and deploying code to the production environment. The data will be automatically added once you deploy to production for the first time.',
),
- production: __(
- 'The total stage shows the time it takes between creating an issue and deploying the code to production. The data will be automatically added once you have completed the full idea to production cycle.',
- ),
};
export default {
diff --git a/app/assets/javascripts/deploy_keys/components/app.vue b/app/assets/javascripts/deploy_keys/components/app.vue
index a03a7114b40..0ac16e6b6a0 100644
--- a/app/assets/javascripts/deploy_keys/components/app.vue
+++ b/app/assets/javascripts/deploy_keys/components/app.vue
@@ -1,5 +1,5 @@
<script>
-import { GlLoadingIcon } from '@gitlab/ui';
+import { GlLoadingIcon, GlIcon } from '@gitlab/ui';
import { s__ } from '~/locale';
import { deprecatedCreateFlash as Flash } from '~/flash';
import NavigationTabs from '~/vue_shared/components/navigation_tabs.vue';
@@ -7,14 +7,13 @@ import eventHub from '../eventhub';
import DeployKeysService from '../service';
import DeployKeysStore from '../store';
import KeysPanel from './keys_panel.vue';
-import Icon from '~/vue_shared/components/icon.vue';
export default {
components: {
KeysPanel,
NavigationTabs,
GlLoadingIcon,
- Icon,
+ GlIcon,
},
props: {
endpoint: {
@@ -125,8 +124,8 @@ export default {
/>
<template v-else-if="hasKeys">
<div class="top-area scrolling-tabs-container inner-page-scroll-tabs">
- <div class="fade-left"><icon name="chevron-lg-left" :size="12" /></div>
- <div class="fade-right"><icon name="chevron-lg-right" :size="12" /></div>
+ <div class="fade-left"><gl-icon name="chevron-lg-left" :size="12" /></div>
+ <div class="fade-right"><gl-icon name="chevron-lg-right" :size="12" /></div>
<navigation-tabs :tabs="tabs" scope="deployKeys" @onChangeTab="onChangeTab" />
</div>
diff --git a/app/assets/javascripts/deploy_keys/components/key.vue b/app/assets/javascripts/deploy_keys/components/key.vue
index 585b091bc51..5b41d23bd27 100644
--- a/app/assets/javascripts/deploy_keys/components/key.vue
+++ b/app/assets/javascripts/deploy_keys/components/key.vue
@@ -1,7 +1,7 @@
<script>
import { head, tail } from 'lodash';
+import { GlIcon } from '@gitlab/ui';
import { s__, sprintf } from '~/locale';
-import icon from '~/vue_shared/components/icon.vue';
import tooltip from '~/vue_shared/directives/tooltip';
import timeagoMixin from '~/vue_shared/mixins/timeago';
@@ -10,7 +10,7 @@ import actionBtn from './action_btn.vue';
export default {
components: {
actionBtn,
- icon,
+ GlIcon,
},
directives: {
tooltip,
@@ -130,7 +130,7 @@ export default {
class="label deploy-project-label"
>
<span> {{ firstProject.project.full_name }} </span>
- <icon :name="firstProject.can_push ? 'lock-open' : 'lock'" />
+ <gl-icon :name="firstProject.can_push ? 'lock-open' : 'lock'" />
</a>
<a
v-if="isExpandable"
@@ -151,7 +151,7 @@ export default {
class="label deploy-project-label"
>
<span> {{ deployKeysProject.project.full_name }} </span>
- <icon :name="deployKeysProject.can_push ? 'lock-open' : 'lock'" />
+ <gl-icon :name="deployKeysProject.can_push ? 'lock-open' : 'lock'" />
</a>
</template>
<span v-else class="text-secondary">{{ __('None') }}</span>
@@ -161,7 +161,7 @@ export default {
<div role="rowheader" class="table-mobile-header">{{ __('Created') }}</div>
<div class="table-mobile-content text-secondary key-created-at">
<span v-tooltip :title="tooltipTitle(deployKey.created_at)">
- <icon name="calendar" /> <span>{{ timeFormatted(deployKey.created_at) }}</span>
+ <gl-icon name="calendar" /> <span>{{ timeFormatted(deployKey.created_at) }}</span>
</span>
</div>
</div>
@@ -178,7 +178,7 @@ export default {
class="btn btn-default text-secondary"
data-container="body"
>
- <icon name="pencil" />
+ <gl-icon name="pencil" />
</a>
<action-btn
v-if="isRemovable"
@@ -189,7 +189,7 @@ export default {
type="remove"
data-container="body"
>
- <icon name="remove" />
+ <gl-icon name="remove" />
</action-btn>
<action-btn
v-else-if="isEnabled"
@@ -200,7 +200,7 @@ export default {
type="disable"
data-container="body"
>
- <icon name="cancel" />
+ <gl-icon name="cancel" />
</action-btn>
</div>
</div>
diff --git a/app/assets/javascripts/deploy_keys/service/index.js b/app/assets/javascripts/deploy_keys/service/index.js
index 268a37008c5..10333752936 100644
--- a/app/assets/javascripts/deploy_keys/service/index.js
+++ b/app/assets/javascripts/deploy_keys/service/index.js
@@ -2,20 +2,18 @@ import axios from '~/lib/utils/axios_utils';
export default class DeployKeysService {
constructor(endpoint) {
- this.axios = axios.create({
- baseURL: endpoint,
- });
+ this.endpoint = endpoint;
}
getKeys() {
- return this.axios.get().then(response => response.data);
+ return axios.get(this.endpoint).then(response => response.data);
}
enableKey(id) {
- return this.axios.put(`${id}/enable`).then(response => response.data);
+ return axios.put(`${this.endpoint}/${id}/enable`).then(response => response.data);
}
disableKey(id) {
- return this.axios.put(`${id}/disable`).then(response => response.data);
+ return axios.put(`${this.endpoint}/${id}/disable`).then(response => response.data);
}
}
diff --git a/app/assets/javascripts/gl_dropdown.js b/app/assets/javascripts/deprecated_jquery_dropdown/gl_dropdown.js
index ec0d0cf6aef..c17f2d2efe4 100644
--- a/app/assets/javascripts/gl_dropdown.js
+++ b/app/assets/javascripts/deprecated_jquery_dropdown/gl_dropdown.js
@@ -1,16 +1,13 @@
-/* eslint-disable max-classes-per-file, one-var, consistent-return */
-
+/* eslint-disable consistent-return */
import $ from 'jquery';
import { escape } from 'lodash';
import fuzzaldrinPlus from 'fuzzaldrin-plus';
-import axios from './lib/utils/axios_utils';
import { visitUrl } from '~/lib/utils/url_utility';
-import { isObject } from './lib/utils/type_utility';
-import renderItem from './gl_dropdown/render';
-
-const BLUR_KEYCODES = [27, 40];
-
-const HAS_VALUE_CLASS = 'has-value';
+import { isObject } from '~/lib/utils/type_utility';
+import renderItem from './render';
+import { GitLabDropdownRemote } from './gl_dropdown_remote';
+import { GitLabDropdownInput } from './gl_dropdown_input';
+import { GitLabDropdownFilter } from './gl_dropdown_filter';
const LOADING_CLASS = 'is-loading';
@@ -32,216 +29,10 @@ const FILTER_INPUT = '.dropdown-input .dropdown-input-field:not(.dropdown-no-fil
const NO_FILTER_INPUT = '.dropdown-input .dropdown-input-field.dropdown-no-filter';
-class GitLabDropdownInput {
- constructor(input, options) {
- this.input = input;
- this.options = options;
- this.fieldName = this.options.fieldName || 'field-name';
- const $inputContainer = this.input.parent();
- const $clearButton = $inputContainer.find('.js-dropdown-input-clear');
- $clearButton.on('click', e => {
- // Clear click
- e.preventDefault();
- e.stopPropagation();
- return this.input
- .val('')
- .trigger('input')
- .focus();
- });
-
- this.input
- .on('keydown', e => {
- const keyCode = e.which;
- if (keyCode === 13 && !options.elIsInput) {
- e.preventDefault();
- }
- })
- .on('input', e => {
- let val = e.currentTarget.value || this.options.inputFieldName;
- val = val
- .split(' ')
- .join('-') // replaces space with dash
- .replace(/[^a-zA-Z0-9 -]/g, '')
- .toLowerCase() // replace non alphanumeric
- .replace(/(-)\1+/g, '-'); // replace repeated dashes
- this.cb(this.options.fieldName, val, {}, true);
- this.input
- .closest('.dropdown')
- .find('.dropdown-toggle-text')
- .text(val);
- });
- }
-
- onInput(cb) {
- this.cb = cb;
- }
-}
-
-class GitLabDropdownFilter {
- constructor(input, options) {
- let ref, timeout;
- this.input = input;
- this.options = options;
- // eslint-disable-next-line no-cond-assign
- this.filterInputBlur = (ref = this.options.filterInputBlur) != null ? ref : true;
- const $inputContainer = this.input.parent();
- const $clearButton = $inputContainer.find('.js-dropdown-input-clear');
- $clearButton.on('click', e => {
- // Clear click
- e.preventDefault();
- e.stopPropagation();
- return this.input
- .val('')
- .trigger('input')
- .focus();
- });
- // Key events
- timeout = '';
- this.input
- .on('keydown', e => {
- const keyCode = e.which;
- if (keyCode === 13 && !options.elIsInput) {
- e.preventDefault();
- }
- })
- .on('input', () => {
- if (this.input.val() !== '' && !$inputContainer.hasClass(HAS_VALUE_CLASS)) {
- $inputContainer.addClass(HAS_VALUE_CLASS);
- } else if (this.input.val() === '' && $inputContainer.hasClass(HAS_VALUE_CLASS)) {
- $inputContainer.removeClass(HAS_VALUE_CLASS);
- }
- // Only filter asynchronously only if option remote is set
- if (this.options.remote) {
- clearTimeout(timeout);
- // eslint-disable-next-line no-return-assign
- return (timeout = setTimeout(() => {
- $inputContainer.parent().addClass('is-loading');
-
- return this.options.query(this.input.val(), data => {
- $inputContainer.parent().removeClass('is-loading');
- return this.options.callback(data);
- });
- }, 250));
- }
- return this.filter(this.input.val());
- });
- }
-
- static shouldBlur(keyCode) {
- return BLUR_KEYCODES.indexOf(keyCode) !== -1;
- }
-
- filter(searchText) {
- let group, results, tmp;
- if (this.options.onFilter) {
- this.options.onFilter(searchText);
- }
- const data = this.options.data();
- if (data != null && !this.options.filterByText) {
- results = data;
- if (searchText !== '') {
- // When data is an array of objects therefore [object Array] e.g.
- // [
- // { prop: 'foo' },
- // { prop: 'baz' }
- // ]
- if (Array.isArray(data)) {
- results = fuzzaldrinPlus.filter(data, searchText, {
- key: this.options.keys,
- });
- }
- // If data is grouped therefore an [object Object]. e.g.
- // {
- // groupName1: [
- // { prop: 'foo' },
- // { prop: 'baz' }
- // ],
- // groupName2: [
- // { prop: 'abc' },
- // { prop: 'def' }
- // ]
- // }
- else if (isObject(data)) {
- results = {};
- Object.keys(data).forEach(key => {
- group = data[key];
- tmp = fuzzaldrinPlus.filter(group, searchText, {
- key: this.options.keys,
- });
- if (tmp.length) {
- results[key] = tmp.map(item => item);
- }
- });
- }
- }
- return this.options.callback(results);
- }
- const elements = this.options.elements();
- if (searchText) {
- // eslint-disable-next-line func-names
- elements.each(function() {
- const $el = $(this);
- const matches = fuzzaldrinPlus.match($el.text().trim(), searchText);
- if (!$el.is('.dropdown-header')) {
- if (matches.length) {
- return $el.show().removeClass('option-hidden');
- }
- return $el.hide().addClass('option-hidden');
- }
- });
- } else {
- elements.show().removeClass('option-hidden');
- }
-
- elements
- .parent()
- .find('.dropdown-menu-empty-item')
- .toggleClass('hidden', elements.is(':visible'));
- }
-}
-
-class GitLabDropdownRemote {
- constructor(dataEndpoint, options) {
- this.dataEndpoint = dataEndpoint;
- this.options = options;
- }
-
- execute() {
- if (typeof this.dataEndpoint === 'string') {
- return this.fetchData();
- } else if (typeof this.dataEndpoint === 'function') {
- if (this.options.beforeSend) {
- this.options.beforeSend();
- }
- return this.dataEndpoint('', data => {
- // Fetch the data by calling the data function
- if (this.options.success) {
- this.options.success(data);
- }
- if (this.options.beforeSend) {
- return this.options.beforeSend();
- }
- });
- }
- }
-
- fetchData() {
- if (this.options.beforeSend) {
- this.options.beforeSend();
- }
-
- // Fetch the data through ajax if the data is a string
- return axios.get(this.dataEndpoint).then(({ data }) => {
- if (this.options.success) {
- return this.options.success(data);
- }
- });
- }
-}
-
-class GitLabDropdown {
+export class GitLabDropdown {
constructor(el1, options) {
- let selector, self;
+ let selector;
+ let self;
this.el = el1;
this.options = options;
this.updateLabel = this.updateLabel.bind(this);
@@ -354,7 +145,8 @@ class GitLabDropdown {
}
});
this.dropdown.on('blur', 'a', e => {
- let $dropdownMenu, $relatedTarget;
+ let $dropdownMenu;
+ let $relatedTarget;
if (e.relatedTarget != null) {
$relatedTarget = $(e.relatedTarget);
$dropdownMenu = $relatedTarget.closest('.dropdown-menu');
@@ -421,7 +213,8 @@ class GitLabDropdown {
}
parseData(data) {
- let groupData, html;
+ let groupData;
+ let html;
this.renderedData = data;
if (this.options.filterable && data.length === 0) {
// render no matching results
@@ -636,7 +429,11 @@ class GitLabDropdown {
}
rowClicked(el) {
- let field, groupName, selectedIndex, selectedObject, isMarking;
+ let field;
+ let groupName;
+ let selectedIndex;
+ let selectedObject;
+ let isMarking;
const { fieldName } = this.options;
const isInput = $(this.el).is('input');
if (this.renderedData) {
@@ -790,7 +587,8 @@ class GitLabDropdown {
selector = `.dropdown-page-one ${selector}`;
}
return $('body').on('keydown', e => {
- let $listItems, PREV_INDEX;
+ let $listItems;
+ let PREV_INDEX;
const currentKeyCode = e.which;
if (ARROW_KEY_CODES.indexOf(currentKeyCode) !== -1) {
e.preventDefault();
@@ -889,13 +687,3 @@ class GitLabDropdown {
return isInput ? field.val('') : field.remove();
}
}
-
-// eslint-disable-next-line func-names
-$.fn.glDropdown = function(opts) {
- // eslint-disable-next-line func-names
- return this.each(function() {
- if (!$.data(this, 'glDropdown')) {
- return $.data(this, 'glDropdown', new GitLabDropdown(this, opts));
- }
- });
-};
diff --git a/app/assets/javascripts/deprecated_jquery_dropdown/gl_dropdown_filter.js b/app/assets/javascripts/deprecated_jquery_dropdown/gl_dropdown_filter.js
new file mode 100644
index 00000000000..89ffb5f5f79
--- /dev/null
+++ b/app/assets/javascripts/deprecated_jquery_dropdown/gl_dropdown_filter.js
@@ -0,0 +1,135 @@
+/* eslint-disable consistent-return */
+
+import $ from 'jquery';
+import fuzzaldrinPlus from 'fuzzaldrin-plus';
+import { isObject } from '~/lib/utils/type_utility';
+
+const BLUR_KEYCODES = [27, 40];
+
+const HAS_VALUE_CLASS = 'has-value';
+
+export class GitLabDropdownFilter {
+ constructor(input, options) {
+ let ref;
+ let timeout;
+ this.input = input;
+ this.options = options;
+ // eslint-disable-next-line no-cond-assign
+ this.filterInputBlur = (ref = this.options.filterInputBlur) != null ? ref : true;
+ const $inputContainer = this.input.parent();
+ const $clearButton = $inputContainer.find('.js-dropdown-input-clear');
+ $clearButton.on('click', e => {
+ // Clear click
+ e.preventDefault();
+ e.stopPropagation();
+ return this.input
+ .val('')
+ .trigger('input')
+ .focus();
+ });
+ // Key events
+ timeout = '';
+ this.input
+ .on('keydown', e => {
+ const keyCode = e.which;
+ if (keyCode === 13 && !options.elIsInput) {
+ e.preventDefault();
+ }
+ })
+ .on('input', () => {
+ if (this.input.val() !== '' && !$inputContainer.hasClass(HAS_VALUE_CLASS)) {
+ $inputContainer.addClass(HAS_VALUE_CLASS);
+ } else if (this.input.val() === '' && $inputContainer.hasClass(HAS_VALUE_CLASS)) {
+ $inputContainer.removeClass(HAS_VALUE_CLASS);
+ }
+ // Only filter asynchronously only if option remote is set
+ if (this.options.remote) {
+ clearTimeout(timeout);
+ // eslint-disable-next-line no-return-assign
+ return (timeout = setTimeout(() => {
+ $inputContainer.parent().addClass('is-loading');
+
+ return this.options.query(this.input.val(), data => {
+ $inputContainer.parent().removeClass('is-loading');
+ return this.options.callback(data);
+ });
+ }, 250));
+ }
+ return this.filter(this.input.val());
+ });
+ }
+
+ static shouldBlur(keyCode) {
+ return BLUR_KEYCODES.indexOf(keyCode) !== -1;
+ }
+
+ filter(searchText) {
+ let group;
+ let results;
+ let tmp;
+ if (this.options.onFilter) {
+ this.options.onFilter(searchText);
+ }
+ const data = this.options.data();
+ if (data != null && !this.options.filterByText) {
+ results = data;
+ if (searchText !== '') {
+ // When data is an array of objects therefore [object Array] e.g.
+ // [
+ // { prop: 'foo' },
+ // { prop: 'baz' }
+ // ]
+ if (Array.isArray(data)) {
+ results = fuzzaldrinPlus.filter(data, searchText, {
+ key: this.options.keys,
+ });
+ }
+ // If data is grouped therefore an [object Object]. e.g.
+ // {
+ // groupName1: [
+ // { prop: 'foo' },
+ // { prop: 'baz' }
+ // ],
+ // groupName2: [
+ // { prop: 'abc' },
+ // { prop: 'def' }
+ // ]
+ // }
+ else if (isObject(data)) {
+ results = {};
+ Object.keys(data).forEach(key => {
+ group = data[key];
+ tmp = fuzzaldrinPlus.filter(group, searchText, {
+ key: this.options.keys,
+ });
+ if (tmp.length) {
+ results[key] = tmp.map(item => item);
+ }
+ });
+ }
+ }
+ return this.options.callback(results);
+ }
+ const elements = this.options.elements();
+ if (searchText) {
+ // eslint-disable-next-line func-names
+ elements.each(function() {
+ const $el = $(this);
+ const matches = fuzzaldrinPlus.match($el.text().trim(), searchText);
+ if (!$el.is('.dropdown-header')) {
+ if (matches.length) {
+ return $el.show().removeClass('option-hidden');
+ }
+ return $el.hide().addClass('option-hidden');
+ }
+ });
+ } else {
+ elements.show().removeClass('option-hidden');
+ }
+
+ elements
+ .parent()
+ .find('.dropdown-menu-empty-item')
+ .toggleClass('hidden', elements.is(':visible'));
+ }
+}
diff --git a/app/assets/javascripts/deprecated_jquery_dropdown/gl_dropdown_input.js b/app/assets/javascripts/deprecated_jquery_dropdown/gl_dropdown_input.js
new file mode 100644
index 00000000000..d857071d05f
--- /dev/null
+++ b/app/assets/javascripts/deprecated_jquery_dropdown/gl_dropdown_input.js
@@ -0,0 +1,44 @@
+export class GitLabDropdownInput {
+ constructor(input, options) {
+ this.input = input;
+ this.options = options;
+ this.fieldName = this.options.fieldName || 'field-name';
+ const $inputContainer = this.input.parent();
+ const $clearButton = $inputContainer.find('.js-dropdown-input-clear');
+ $clearButton.on('click', e => {
+ // Clear click
+ e.preventDefault();
+ e.stopPropagation();
+ return this.input
+ .val('')
+ .trigger('input')
+ .focus();
+ });
+
+ this.input
+ .on('keydown', e => {
+ const keyCode = e.which;
+ if (keyCode === 13 && !options.elIsInput) {
+ e.preventDefault();
+ }
+ })
+ .on('input', e => {
+ let val = e.currentTarget.value || this.options.inputFieldName;
+ val = val
+ .split(' ')
+ .join('-') // replaces space with dash
+ .replace(/[^a-zA-Z0-9 -]/g, '')
+ .toLowerCase() // replace non alphanumeric
+ .replace(/(-)\1+/g, '-'); // replace repeated dashes
+ this.cb(this.options.fieldName, val, {}, true);
+ this.input
+ .closest('.dropdown')
+ .find('.dropdown-toggle-text')
+ .text(val);
+ });
+ }
+
+ onInput(cb) {
+ this.cb = cb;
+ }
+}
diff --git a/app/assets/javascripts/deprecated_jquery_dropdown/gl_dropdown_remote.js b/app/assets/javascripts/deprecated_jquery_dropdown/gl_dropdown_remote.js
new file mode 100644
index 00000000000..1f6a2e1f646
--- /dev/null
+++ b/app/assets/javascripts/deprecated_jquery_dropdown/gl_dropdown_remote.js
@@ -0,0 +1,42 @@
+/* eslint-disable consistent-return */
+
+import axios from '../lib/utils/axios_utils';
+
+export class GitLabDropdownRemote {
+ constructor(dataEndpoint, options) {
+ this.dataEndpoint = dataEndpoint;
+ this.options = options;
+ }
+
+ execute() {
+ if (typeof this.dataEndpoint === 'string') {
+ return this.fetchData();
+ } else if (typeof this.dataEndpoint === 'function') {
+ if (this.options.beforeSend) {
+ this.options.beforeSend();
+ }
+ return this.dataEndpoint('', data => {
+ // Fetch the data by calling the data function
+ if (this.options.success) {
+ this.options.success(data);
+ }
+ if (this.options.beforeSend) {
+ return this.options.beforeSend();
+ }
+ });
+ }
+ }
+
+ fetchData() {
+ if (this.options.beforeSend) {
+ this.options.beforeSend();
+ }
+
+ // Fetch the data through ajax if the data is a string
+ return axios.get(this.dataEndpoint).then(({ data }) => {
+ if (this.options.success) {
+ return this.options.success(data);
+ }
+ });
+ }
+}
diff --git a/app/assets/javascripts/deprecated_jquery_dropdown/index.js b/app/assets/javascripts/deprecated_jquery_dropdown/index.js
new file mode 100644
index 00000000000..90e7f15b5b7
--- /dev/null
+++ b/app/assets/javascripts/deprecated_jquery_dropdown/index.js
@@ -0,0 +1,11 @@
+import $ from 'jquery';
+import { GitLabDropdown } from './gl_dropdown';
+
+export default function initDeprecatedJQueryDropdown($el, opts) {
+ // eslint-disable-next-line func-names
+ return $el.each(function() {
+ if (!$.data(this, 'deprecatedJQueryDropdown')) {
+ $.data(this, 'deprecatedJQueryDropdown', new GitLabDropdown(this, opts));
+ }
+ });
+}
diff --git a/app/assets/javascripts/gl_dropdown/render.js b/app/assets/javascripts/deprecated_jquery_dropdown/render.js
index 66546aa834f..167bc4c286e 100644
--- a/app/assets/javascripts/gl_dropdown/render.js
+++ b/app/assets/javascripts/deprecated_jquery_dropdown/render.js
@@ -1,3 +1,5 @@
+import { slugify } from '~/lib/utils/text_utility';
+
const renderersByType = {
divider(element) {
element.classList.add('divider');
@@ -95,15 +97,22 @@ function checkSelected(data, options) {
return options.parent.querySelector(`input[name='${options.fieldName}']`) == null;
}
-function createLink(url, selected, options) {
+function createLink(data, selected, options, index) {
const link = document.createElement('a');
- link.href = url;
+ link.href = getPropertyWithDefault(data, options, 'url', '#');
if (options.icon) {
link.classList.add('d-flex', 'align-items-center');
}
+ if (options.trackSuggestionClickedLabel) {
+ link.setAttribute('data-track-event', 'click_text');
+ link.setAttribute('data-track-label', options.trackSuggestionClickedLabel);
+ link.setAttribute('data-track-value', index);
+ link.setAttribute('data-track-property', slugify(data.category || 'no-category'));
+ }
+
link.classList.toggle('is-active', selected);
return link;
@@ -123,8 +132,7 @@ function assignTextToLink(el, data, options) {
function renderLink(row, data, { options, group, index }) {
const selected = checkSelected(data, options);
- const url = getPropertyWithDefault(data, options, 'url', '#');
- const link = createLink(url, selected, options);
+ const link = createLink(data, selected, options, index);
assignTextToLink(link, data, options);
diff --git a/app/assets/javascripts/design_management/components/delete_button.vue b/app/assets/javascripts/design_management/components/delete_button.vue
index 37686dd5a46..970197ef41b 100644
--- a/app/assets/javascripts/design_management/components/delete_button.vue
+++ b/app/assets/javascripts/design_management/components/delete_button.vue
@@ -98,6 +98,7 @@ export default {
:loading="loading"
:icon="buttonIcon"
:disabled="isDeleting || !hasSelectedDesigns"
- />
+ ><slot></slot
+ ></gl-button>
</div>
</template>
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 6a20517eed7..845f1aec8cf 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,19 +1,20 @@
<script>
import { ApolloMutation } from 'vue-apollo';
-import { GlTooltipDirective, GlIcon, GlLoadingIcon, GlLink } from '@gitlab/ui';
+import { GlTooltipDirective, GlIcon, GlLoadingIcon, GlLink, GlBadge } from '@gitlab/ui';
import { s__ } from '~/locale';
+import createFlash from '~/flash';
import ReplyPlaceholder from '~/notes/components/discussion_reply_placeholder.vue';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
import allVersionsMixin from '../../mixins/all_versions';
import createNoteMutation from '../../graphql/mutations/create_note.mutation.graphql';
import toggleResolveDiscussionMutation from '../../graphql/mutations/toggle_resolve_discussion.mutation.graphql';
-import getDesignQuery from '../../graphql/queries/get_design.query.graphql';
import activeDiscussionQuery from '../../graphql/queries/active_discussion.query.graphql';
import DesignNote from './design_note.vue';
import DesignReplyForm from './design_reply_form.vue';
-import { updateStoreAfterAddDiscussionComment } from '../../utils/cache_update';
import { ACTIVE_DISCUSSION_SOURCE_TYPES } from '../../constants';
import ToggleRepliesWidget from './toggle_replies_widget.vue';
+import { hasErrors } from '../../utils/cache_update';
+import { ADD_DISCUSSION_COMMENT_ERROR } from '../../utils/error_messages';
export default {
components: {
@@ -26,6 +27,7 @@ export default {
GlLink,
ToggleRepliesWidget,
TimeAgoTooltip,
+ GlBadge,
},
directives: {
GlTooltip: GlTooltipDirective,
@@ -62,22 +64,20 @@ export default {
activeDiscussion: {
query: activeDiscussionQuery,
result({ data }) {
- const discussionId = data.activeDiscussion.id;
if (this.discussion.resolved && !this.resolvedDiscussionsExpanded) {
return;
}
- // We watch any changes to the active discussion from the design pins and scroll to this discussion if it exists
- // We don't want scrollIntoView to be triggered from the discussion click itself
- if (
- discussionId &&
- data.activeDiscussion.source === ACTIVE_DISCUSSION_SOURCE_TYPES.pin &&
- discussionId === this.discussion.notes[0].id
- ) {
- this.$el.scrollIntoView({
- behavior: 'smooth',
- inline: 'start',
- });
- }
+
+ this.$nextTick(() => {
+ // We watch any changes to the active discussion from the design pins and scroll to this discussion if it exists.
+ // We don't want scrollIntoView to be triggered from the discussion click itself.
+ if (this.$el && this.shouldScrollToDiscussion(data.activeDiscussion)) {
+ this.$el.scrollIntoView({
+ behavior: 'smooth',
+ inline: 'start',
+ });
+ }
+ });
},
},
},
@@ -107,8 +107,8 @@ export default {
atVersion: this.designsVersion,
};
},
- isDiscussionHighlighted() {
- return this.discussion.notes[0].id === this.activeDiscussion.id;
+ isDiscussionActive() {
+ return this.discussion.notes.some(({ id }) => id === this.activeDiscussion.id);
},
resolveCheckboxText() {
return this.discussion.resolved
@@ -138,21 +138,10 @@ export default {
},
},
methods: {
- addDiscussionComment(
- store,
- {
- data: { createNote },
- },
- ) {
- updateStoreAfterAddDiscussionComment(
- store,
- createNote,
- getDesignQuery,
- this.designVariables,
- this.discussion.id,
- );
- },
- onDone() {
+ onDone({ data: { createNote } }) {
+ if (hasErrors(createNote)) {
+ createFlash({ message: ADD_DISCUSSION_COMMENT_ERROR });
+ }
this.discussionComment = '';
this.hideForm();
if (this.shouldChangeResolvedStatus) {
@@ -160,14 +149,14 @@ export default {
}
},
onCreateNoteError(err) {
- this.$emit('createNoteError', err);
+ this.$emit('create-note-error', err);
},
hideForm() {
this.isFormRendered = false;
this.discussionComment = '';
},
showForm() {
- this.$emit('openForm', this.discussion.id);
+ this.$emit('open-form', this.discussion.id);
this.isFormRendered = true;
},
toggleResolvedStatus() {
@@ -179,16 +168,24 @@ export default {
})
.then(({ data }) => {
if (data.errors?.length > 0) {
- this.$emit('resolveDiscussionError', data.errors[0]);
+ this.$emit('resolve-discussion-error', data.errors[0]);
}
})
.catch(err => {
- this.$emit('resolveDiscussionError', err);
+ this.$emit('resolve-discussion-error', err);
})
.finally(() => {
this.isResolving = false;
});
},
+ shouldScrollToDiscussion(activeDiscussion) {
+ const ALLOWED_ACTIVE_DISCUSSION_SOURCES = [
+ ACTIVE_DISCUSSION_SOURCE_TYPES.pin,
+ ACTIVE_DISCUSSION_SOURCE_TYPES.url,
+ ];
+ const { source } = activeDiscussion;
+ return ALLOWED_ACTIVE_DISCUSSION_SOURCES.includes(source) && this.isDiscussionActive;
+ },
},
createNoteMutation,
};
@@ -196,13 +193,12 @@ export default {
<template>
<div class="design-discussion-wrapper">
- <div
- class="badge badge-pill gl-display-flex gl-align-items-center gl-justify-content-center"
+ <gl-badge
+ class="gl-display-flex gl-align-items-center gl-justify-content-center gl-cursor-pointer"
:class="{ resolved: discussion.resolved }"
- type="button"
>
{{ discussion.index }}
- </div>
+ </gl-badge>
<ul
class="design-discussion bordered-box gl-relative gl-p-0 gl-list-style-none"
data-qa-selector="design_discussion_content"
@@ -211,8 +207,8 @@ export default {
:note="firstNote"
:markdown-preview-path="markdownPreviewPath"
:is-resolving="isResolving"
- :class="{ 'gl-bg-blue-50': isDiscussionHighlighted }"
- @error="$emit('updateNoteError', $event)"
+ :class="{ 'gl-bg-blue-50': isDiscussionActive }"
+ @error="$emit('update-note-error', $event)"
>
<template v-if="discussion.resolvable" #resolveDiscussion>
<button
@@ -220,7 +216,6 @@ export default {
:class="{ 'is-active': discussion.resolved }"
:title="resolveCheckboxText"
:aria-label="resolveCheckboxText"
- type="button"
class="line-resolve-btn note-action-button gl-mr-3"
data-testid="resolve-button"
@click.stop="toggleResolvedStatus"
@@ -255,8 +250,8 @@ export default {
:note="note"
:markdown-preview-path="markdownPreviewPath"
:is-resolving="isResolving"
- :class="{ 'gl-bg-blue-50': isDiscussionHighlighted }"
- @error="$emit('updateNoteError', $event)"
+ :class="{ 'gl-bg-blue-50': isDiscussionActive }"
+ @error="$emit('update-note-error', $event)"
/>
<li v-show="isReplyPlaceholderVisible" class="reply-wrapper">
<reply-placeholder
@@ -272,7 +267,6 @@ export default {
:variables="{
input: mutationPayload,
}"
- :update="addDiscussionComment"
@done="onDone"
@error="onCreateNoteError"
>
@@ -280,8 +274,8 @@ export default {
v-model="discussionComment"
:is-saving="loading"
:markdown-preview-path="markdownPreviewPath"
- @submitForm="mutate"
- @cancelForm="hideForm"
+ @submit-form="mutate"
+ @cancel-form="hideForm"
>
<template v-if="discussion.resolvable" #resolveCheckbox>
<label data-testid="resolve-checkbox">
diff --git a/app/assets/javascripts/design_management/components/design_notes/design_note.vue b/app/assets/javascripts/design_management/components/design_notes/design_note.vue
index 172e61920ef..7f4b3b31024 100644
--- a/app/assets/javascripts/design_management/components/design_notes/design_note.vue
+++ b/app/assets/javascripts/design_management/components/design_notes/design_note.vue
@@ -1,12 +1,12 @@
<script>
import { ApolloMutation } from 'vue-apollo';
-import { GlTooltipDirective, GlIcon } from '@gitlab/ui';
+import { GlTooltipDirective, GlIcon, GlLink, GlSafeHtmlDirective } from '@gitlab/ui';
import updateNoteMutation from '../../graphql/mutations/update_note.mutation.graphql';
import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue';
import TimelineEntryItem from '~/vue_shared/components/notes/timeline_entry_item.vue';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
import DesignReplyForm from './design_reply_form.vue';
-import { findNoteId } from '../../utils/design_management_utils';
+import { findNoteId, extractDesignNoteId } from '../../utils/design_management_utils';
import { hasErrors } from '../../utils/cache_update';
export default {
@@ -17,9 +17,11 @@ export default {
DesignReplyForm,
ApolloMutation,
GlIcon,
+ GlLink,
},
directives: {
GlTooltip: GlTooltipDirective,
+ SafeHtml: GlSafeHtmlDirective,
},
props: {
note: {
@@ -46,7 +48,7 @@ export default {
return findNoteId(this.note.id);
},
isNoteLinked() {
- return this.$route.hash === `#note_${this.noteAnchorId}`;
+ return extractDesignNoteId(this.$route.hash) === this.noteAnchorId;
},
mutationPayload() {
return {
@@ -58,11 +60,6 @@ export default {
return !this.isEditing && this.note.userPermissions.adminNote;
},
},
- mounted() {
- if (this.isNoteLinked) {
- this.$el.scrollIntoView({ behavior: 'smooth', inline: 'start' });
- }
- },
methods: {
hideForm() {
this.isEditing = false;
@@ -87,30 +84,30 @@ export default {
:img-alt="author.username"
:img-size="40"
/>
- <div class="d-flex justify-content-between">
+ <div class="gl-display-flex gl-justify-content-space-between">
<div>
- <a
+ <gl-link
v-once
:href="author.webUrl"
class="js-user-link"
:data-user-id="author.id"
:data-username="author.username"
>
- <span class="note-header-author-name bold">{{ author.name }}</span>
- <span v-if="author.status_tooltip_html" v-html="author.status_tooltip_html"></span>
+ <span class="note-header-author-name gl-font-weight-bold">{{ author.name }}</span>
+ <span v-if="author.status_tooltip_html" v-safe-html="author.status_tooltip_html"></span>
<span class="note-headline-light">@{{ author.username }}</span>
- </a>
+ </gl-link>
<span class="note-headline-light note-headline-meta">
<span class="system-note-message"> <slot></slot> </span>
- <template v-if="note.createdAt">
- <span class="system-note-separator"></span>
- <a class="note-timestamp system-note-separator" :href="`#note_${noteAnchorId}`">
- <time-ago-tooltip :time="note.createdAt" tooltip-placement="bottom" />
- </a>
- </template>
+ <gl-link
+ class="note-timestamp system-note-separator gl-display-block gl-mb-2"
+ :href="`#note_${noteAnchorId}`"
+ >
+ <time-ago-tooltip :time="note.createdAt" tooltip-placement="bottom" />
+ </gl-link>
</span>
</div>
- <div class="gl-display-flex">
+ <div class="gl-display-flex gl-align-items-baseline">
<slot name="resolveDiscussion"></slot>
<button
v-if="isEditButtonVisible"
@@ -126,9 +123,9 @@ export default {
</div>
<template v-if="!isEditing">
<div
+ v-safe-html="note.bodyHtml"
class="note-text js-note-text md"
data-qa-selector="note_content"
- v-html="note.bodyHtml"
></div>
<slot name="resolvedStatus"></slot>
</template>
@@ -147,9 +144,9 @@ export default {
:is-saving="loading"
:markdown-preview-path="markdownPreviewPath"
:is-new-comment="false"
- class="mt-5"
- @submitForm="mutate"
- @cancelForm="hideForm"
+ class="gl-mt-5"
+ @submit-form="mutate"
+ @cancel-form="hideForm"
/>
</apollo-mutation>
</timeline-entry-item>
diff --git a/app/assets/javascripts/design_management/components/design_notes/design_reply_form.vue b/app/assets/javascripts/design_management/components/design_notes/design_reply_form.vue
index 969034909f2..3754e1dbbc1 100644
--- a/app/assets/javascripts/design_management/components/design_notes/design_reply_form.vue
+++ b/app/assets/javascripts/design_management/components/design_notes/design_reply_form.vue
@@ -1,5 +1,5 @@
<script>
-import { GlDeprecatedButton, GlModal } from '@gitlab/ui';
+import { GlButton, GlModal } from '@gitlab/ui';
import MarkdownField from '~/vue_shared/components/markdown/field.vue';
import { s__ } from '~/locale';
@@ -7,7 +7,7 @@ export default {
name: 'DesignReplyForm',
components: {
MarkdownField,
- GlDeprecatedButton,
+ GlButton,
GlModal,
},
props: {
@@ -66,13 +66,13 @@ export default {
},
methods: {
submitForm() {
- if (this.hasValue) this.$emit('submitForm');
+ if (this.hasValue) this.$emit('submit-form');
},
cancelComment() {
if (this.hasValue && this.formText !== this.value) {
this.$refs.cancelCommentModal.show();
} else {
- this.$emit('cancelForm');
+ this.$emit('cancel-form');
}
},
focusInput() {
@@ -112,20 +112,21 @@ export default {
</markdown-field>
<slot name="resolveCheckbox"></slot>
<div class="note-form-actions gl-display-flex gl-justify-content-space-between">
- <gl-deprecated-button
+ <gl-button
ref="submitButton"
:disabled="!hasValue || isSaving"
+ category="primary"
variant="success"
type="submit"
data-track-event="click_button"
data-qa-selector="save_comment_button"
- @click="$emit('submitForm')"
+ @click="$emit('submit-form')"
>
{{ buttonText }}
- </gl-deprecated-button>
- <gl-deprecated-button ref="cancelButton" @click="cancelComment">{{
+ </gl-button>
+ <gl-button ref="cancelButton" variant="default" category="primary" @click="cancelComment">{{
__('Cancel')
- }}</gl-deprecated-button>
+ }}</gl-button>
</div>
<gl-modal
ref="cancelCommentModal"
@@ -134,7 +135,7 @@ export default {
:ok-title="modalSettings.okTitle"
:cancel-title="modalSettings.cancelTitle"
modal-id="cancel-comment-modal"
- @ok="$emit('cancelForm')"
+ @ok="$emit('cancel-form')"
>{{ modalSettings.content }}
</gl-modal>
</form>
diff --git a/app/assets/javascripts/design_management/components/design_overlay.vue b/app/assets/javascripts/design_management/components/design_overlay.vue
index 926e7c74802..5c4a3ab5f94 100644
--- a/app/assets/javascripts/design_management/components/design_overlay.vue
+++ b/app/assets/javascripts/design_management/components/design_overlay.vue
@@ -1,4 +1,5 @@
<script>
+import { __ } from '~/locale';
import activeDiscussionQuery from '../graphql/queries/active_discussion.query.graphql';
import updateActiveDiscussionMutation from '../graphql/mutations/update_active_discussion.mutation.graphql';
import DesignNotePin from './design_note_pin.vue';
@@ -236,18 +237,26 @@ export default {
});
},
isNoteInactive(note) {
- return this.activeDiscussion.id && this.activeDiscussion.id !== note.id;
+ const discussionNotes = note.discussion.notes.nodes || [];
+
+ return (
+ this.activeDiscussion.id &&
+ !discussionNotes.some(({ id }) => id === this.activeDiscussion.id)
+ );
},
designPinClass(note) {
return { inactive: this.isNoteInactive(note), resolved: note.resolved };
},
},
+ i18n: {
+ newCommentButtonLabel: __('Add comment to design'),
+ },
};
</script>
<template>
<div
- class="position-absolute image-diff-overlay frame"
+ class="gl-absolute gl-top-0 gl-left-0 frame"
:style="overlayStyle"
@mousemove="onOverlayMousemove"
@mouseleave="onNoteMouseup"
@@ -255,26 +264,28 @@ export default {
<button
v-show="!disableCommenting"
type="button"
- class="btn-transparent position-absolute image-diff-overlay-add-comment w-100 h-100 js-add-image-diff-note-button"
+ role="button"
+ :aria-label="$options.i18n.newCommentButtonLabel"
+ class="gl-absolute gl-w-full gl-h-full gl-p-0 gl-top-0 gl-left-0 gl-outline-0! btn-transparent design-detail-overlay-add-comment"
data-qa-selector="design_image_button"
@mouseup="onAddCommentMouseup"
></button>
- <template v-for="note in notes">
- <design-note-pin
- v-if="resolvedDiscussionsExpanded || !note.resolved"
- :key="note.id"
- :label="note.index"
- :repositioning="isMovingNote(note.id)"
- :position="
- isMovingNote(note.id) && movingNoteNewPosition
- ? getNotePositionStyle(movingNoteNewPosition)
- : getNotePositionStyle(note.position)
- "
- :class="designPinClass(note)"
- @mousedown.stop="onNoteMousedown($event, note)"
- @mouseup.stop="onNoteMouseup(note)"
- />
- </template>
+
+ <design-note-pin
+ v-for="note in notes"
+ v-if="resolvedDiscussionsExpanded || !note.resolved"
+ :key="note.id"
+ :label="note.index"
+ :repositioning="isMovingNote(note.id)"
+ :position="
+ isMovingNote(note.id) && movingNoteNewPosition
+ ? getNotePositionStyle(movingNoteNewPosition)
+ : getNotePositionStyle(note.position)
+ "
+ :class="designPinClass(note)"
+ @mousedown.stop="onNoteMousedown($event, note)"
+ @mouseup.stop="onNoteMouseup(note)"
+ />
<design-note-pin
v-if="currentCommentForm"
diff --git a/app/assets/javascripts/design_management/components/design_sidebar.vue b/app/assets/javascripts/design_management/components/design_sidebar.vue
index e5a3590877e..df425e3b96d 100644
--- a/app/assets/javascripts/design_management/components/design_sidebar.vue
+++ b/app/assets/javascripts/design_management/components/design_sidebar.vue
@@ -8,6 +8,8 @@ import { extractDiscussions, extractParticipants } from '../utils/design_managem
import { ACTIVE_DISCUSSION_SOURCE_TYPES } from '../constants';
import DesignDiscussion from './design_notes/design_discussion.vue';
import Participants from '~/sidebar/components/participants/participants.vue';
+import DesignTodoButton from './design_todo_button.vue';
+import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
export default {
components: {
@@ -16,7 +18,9 @@ export default {
GlCollapse,
GlButton,
GlPopover,
+ DesignTodoButton,
},
+ mixins: [glFeatureFlagsMixin()],
props: {
design: {
type: Object,
@@ -37,6 +41,14 @@ export default {
discussionWithOpenForm: '',
};
},
+ inject: {
+ projectPath: {
+ default: '',
+ },
+ issueIid: {
+ default: '',
+ },
+ },
computed: {
discussions() {
return extractDiscussions(this.design.discussions);
@@ -59,6 +71,26 @@ export default {
resolvedCommentsToggleIcon() {
return this.resolvedDiscussionsExpanded ? 'chevron-down' : 'chevron-right';
},
+ showTodoButton() {
+ return this.glFeatures.designManagementTodoButton;
+ },
+ sidebarWrapperClass() {
+ return {
+ 'gl-pt-0': this.showTodoButton,
+ };
+ },
+ },
+ watch: {
+ isResolvedCommentsPopoverHidden(newVal) {
+ if (!newVal) {
+ this.$refs.resolvedComments.scrollIntoView();
+ }
+ },
+ },
+ mounted() {
+ if (!this.isResolvedCommentsPopoverHidden && this.$refs.resolvedComments) {
+ this.$refs.resolvedComments.$el.scrollIntoView();
+ }
},
methods: {
handleSidebarClick() {
@@ -89,7 +121,14 @@ export default {
</script>
<template>
- <div class="image-notes" @click="handleSidebarClick">
+ <div class="image-notes" :class="sidebarWrapperClass" @click="handleSidebarClick">
+ <div
+ v-if="showTodoButton"
+ class="gl-py-4 gl-mb-4 gl-display-flex gl-justify-content-space-between gl-align-items-center gl-border-b-1 gl-border-b-solid gl-border-b-gray-100"
+ >
+ <span>{{ __('To-Do') }}</span>
+ <design-todo-button :design="design" @error="$emit('todoError', $event)" />
+ </div>
<h2 class="gl-font-weight-bold gl-mt-0">
{{ issue.title }}
</h2>
@@ -120,15 +159,16 @@ export default {
:resolved-discussions-expanded="resolvedDiscussionsExpanded"
:discussion-with-open-form="discussionWithOpenForm"
data-testid="unresolved-discussion"
- @createNoteError="$emit('onDesignDiscussionError', $event)"
- @updateNoteError="$emit('updateNoteError', $event)"
- @resolveDiscussionError="$emit('resolveDiscussionError', $event)"
+ @create-note-error="$emit('onDesignDiscussionError', $event)"
+ @update-note-error="$emit('updateNoteError', $event)"
+ @resolve-discussion-error="$emit('resolveDiscussionError', $event)"
@click.native.stop="updateActiveDiscussion(discussion.notes[0].id)"
- @openForm="updateDiscussionWithOpenForm"
+ @open-form="updateDiscussionWithOpenForm"
/>
<template v-if="resolvedDiscussions.length > 0">
<gl-button
id="resolved-comments"
+ ref="resolvedComments"
data-testid="resolved-comments"
:icon="resolvedCommentsToggleIcon"
variant="link"
@@ -151,9 +191,12 @@ export default {
)
}}
</p>
- <a href="#" rel="noopener noreferrer" target="_blank">{{
- s__('DesignManagement|Learn more about resolving comments')
- }}</a>
+ <a
+ href="https://docs.gitlab.com/ee/user/project/issues/design_management.html#resolve-design-threads"
+ rel="noopener noreferrer"
+ target="_blank"
+ >{{ s__('DesignManagement|Learn more about resolving comments') }}</a
+ >
</gl-popover>
<gl-collapse :visible="resolvedDiscussionsExpanded" class="gl-mt-3">
<design-discussion
diff --git a/app/assets/javascripts/design_management/components/design_todo_button.vue b/app/assets/javascripts/design_management/components/design_todo_button.vue
new file mode 100644
index 00000000000..aff4f348d15
--- /dev/null
+++ b/app/assets/javascripts/design_management/components/design_todo_button.vue
@@ -0,0 +1,168 @@
+<script>
+import todoMarkDoneMutation from '~/graphql_shared/mutations/todo_mark_done.mutation.graphql';
+import getDesignQuery from '../graphql/queries/get_design.query.graphql';
+import createDesignTodoMutation from '../graphql/mutations/create_design_todo.mutation.graphql';
+import TodoButton from '~/vue_shared/components/todo_button.vue';
+import allVersionsMixin from '../mixins/all_versions';
+import { updateStoreAfterDeleteDesignTodo } from '../utils/cache_update';
+import { findIssueId, findDesignId } from '../utils/design_management_utils';
+import { CREATE_DESIGN_TODO_ERROR, DELETE_DESIGN_TODO_ERROR } from '../utils/error_messages';
+
+export default {
+ components: {
+ TodoButton,
+ },
+ mixins: [allVersionsMixin],
+ props: {
+ design: {
+ type: Object,
+ required: true,
+ },
+ },
+ inject: {
+ projectPath: {
+ default: '',
+ },
+ issueIid: {
+ default: '',
+ },
+ },
+ data() {
+ return {
+ todoLoading: false,
+ };
+ },
+ computed: {
+ designVariables() {
+ return {
+ fullPath: this.projectPath,
+ iid: this.issueIid,
+ filenames: [this.$route.params.id],
+ atVersion: this.designsVersion,
+ };
+ },
+ designTodoVariables() {
+ return {
+ projectPath: this.projectPath,
+ issueId: findIssueId(this.design.issue.id),
+ designId: findDesignId(this.design.id),
+ issueIid: this.issueIid,
+ filenames: [this.$route.params.id],
+ atVersion: this.designsVersion,
+ };
+ },
+ pendingTodo() {
+ // TODO data structure pending BE MR: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/40555#note_405732940
+ return this.design.currentUserTodos?.nodes[0];
+ },
+ hasPendingTodo() {
+ return Boolean(this.pendingTodo);
+ },
+ },
+ methods: {
+ updateGlobalTodoCount(additionalTodoCount) {
+ const currentCount = parseInt(document.querySelector('.js-todos-count').innerText, 10);
+ const todoToggleEvent = new CustomEvent('todo:toggle', {
+ detail: {
+ count: Math.max(currentCount + additionalTodoCount, 0),
+ },
+ });
+
+ document.dispatchEvent(todoToggleEvent);
+ },
+ incrementGlobalTodoCount() {
+ this.updateGlobalTodoCount(1);
+ },
+ decrementGlobalTodoCount() {
+ this.updateGlobalTodoCount(-1);
+ },
+ createTodo() {
+ this.todoLoading = true;
+ return this.$apollo
+ .mutate({
+ mutation: createDesignTodoMutation,
+ variables: this.designTodoVariables,
+ update: (store, { data: { createDesignTodo } }) => {
+ // because this is a @client mutation,
+ // we control what is in errors, and therefore
+ // we are certain that there is at most 1 item in the array
+ const createDesignTodoError = (createDesignTodo.errors || [])[0];
+ if (createDesignTodoError) {
+ this.$emit('error', Error(createDesignTodoError.message));
+ }
+ },
+ })
+ .then(() => {
+ this.incrementGlobalTodoCount();
+ })
+ .catch(err => {
+ this.$emit('error', Error(CREATE_DESIGN_TODO_ERROR));
+ throw err;
+ })
+ .finally(() => {
+ this.todoLoading = false;
+ });
+ },
+ deleteTodo() {
+ if (!this.hasPendingTodo) return Promise.reject();
+
+ const { id } = this.pendingTodo;
+ const { designVariables } = this;
+
+ this.todoLoading = true;
+ return this.$apollo
+ .mutate({
+ mutation: todoMarkDoneMutation,
+ variables: {
+ id,
+ },
+ update(
+ store,
+ {
+ data: { todoMarkDone },
+ },
+ ) {
+ const todoMarkDoneFirstError = (todoMarkDone.errors || [])[0];
+ if (todoMarkDoneFirstError) {
+ this.$emit('error', Error(todoMarkDoneFirstError));
+ } else {
+ updateStoreAfterDeleteDesignTodo(
+ store,
+ todoMarkDone,
+ getDesignQuery,
+ designVariables,
+ );
+ }
+ },
+ })
+ .then(() => {
+ this.decrementGlobalTodoCount();
+ })
+ .catch(err => {
+ this.$emit('error', Error(DELETE_DESIGN_TODO_ERROR));
+ throw err;
+ })
+ .finally(() => {
+ this.todoLoading = false;
+ });
+ },
+ toggleTodo() {
+ if (this.hasPendingTodo) {
+ return this.deleteTodo();
+ }
+
+ return this.createTodo();
+ },
+ },
+};
+</script>
+
+<template>
+ <todo-button
+ issuable-type="design"
+ :issuable-id="design.iid"
+ :is-todo="hasPendingTodo"
+ :loading="todoLoading"
+ @click.stop.prevent="toggleTodo"
+ />
+</template>
diff --git a/app/assets/javascripts/design_management/components/list/item.vue b/app/assets/javascripts/design_management/components/list/item.vue
index 292b6e09055..36ea812d92e 100644
--- a/app/assets/javascripts/design_management/components/list/item.vue
+++ b/app/assets/javascripts/design_management/components/list/item.vue
@@ -1,6 +1,5 @@
<script>
import { GlLoadingIcon, GlIcon, GlIntersectionObserver } from '@gitlab/ui';
-import Icon from '~/vue_shared/components/icon.vue';
import Timeago from '~/vue_shared/components/time_ago_tooltip.vue';
import { n__, __ } from '~/locale';
import { DESIGN_ROUTE_NAME } from '../../router/constants';
@@ -10,7 +9,6 @@ export default {
GlLoadingIcon,
GlIntersectionObserver,
GlIcon,
- Icon,
Timeago,
},
props: {
@@ -127,12 +125,14 @@ export default {
params: { id: filename },
query: $route.query,
}"
- class="card cursor-pointer text-plain js-design-list-item design-list-item design-list-item-new"
+ class="card gl-cursor-pointer text-plain js-design-list-item design-list-item design-list-item-new"
>
- <div class="card-body p-0 d-flex-center overflow-hidden position-relative">
- <div v-if="icon.name" data-testid="designEvent" class="design-event position-absolute">
+ <div
+ class="card-body gl-p-0 gl-display-flex gl-align-items-center gl-justify-content-center gl-overflow-hidden gl-relative"
+ >
+ <div v-if="icon.name" data-testid="designEvent" class="design-event gl-absolute">
<span :title="icon.tooltip" :aria-label="icon.tooltip">
- <icon :name="icon.name" :size="18" :class="icon.classes" />
+ <gl-icon :name="icon.name" :size="18" :class="icon.classes" />
</span>
</div>
<gl-intersection-observer @appear="onAppear">
@@ -147,25 +147,28 @@ export default {
v-show="showImage"
:src="imageLink"
:alt="filename"
- class="block mx-auto mw-100 mh-100 design-img"
+ class="gl-display-block gl-mx-auto gl-max-w-full mh-100 design-img"
data-qa-selector="design_image"
@load="onImageLoad"
@error="onImageError"
/>
</gl-intersection-observer>
</div>
- <div class="card-footer d-flex w-100">
- <div class="d-flex flex-column str-truncated-100">
- <span class="bold str-truncated-100" data-qa-selector="design_file_name">{{
+ <div class="card-footer gl-display-flex gl-w-full">
+ <div class="gl-display-flex gl-flex-direction-column str-truncated-100">
+ <span class="gl-font-weight-bold str-truncated-100" data-qa-selector="design_file_name">{{
filename
}}</span>
<span v-if="updatedAt" class="str-truncated-100">
{{ __('Updated') }} <timeago :time="updatedAt" tooltip-placement="bottom" />
</span>
</div>
- <div v-if="notesCount" class="ml-auto d-flex align-items-center text-secondary">
- <icon name="comments" class="ml-1" />
- <span :aria-label="notesLabel" class="ml-1">
+ <div
+ v-if="notesCount"
+ class="gl-ml-auto gl-display-flex gl-align-items-center gl-text-gray-500"
+ >
+ <gl-icon name="comments" class="gl-ml-2" />
+ <span :aria-label="notesLabel" class="gl-ml-2">
{{ notesCount }}
</span>
</div>
diff --git a/app/assets/javascripts/design_management/components/upload/design_version_dropdown.vue b/app/assets/javascripts/design_management/components/upload/design_version_dropdown.vue
index a03982cb91b..4a1be7b720a 100644
--- a/app/assets/javascripts/design_management/components/upload/design_version_dropdown.vue
+++ b/app/assets/javascripts/design_management/components/upload/design_version_dropdown.vue
@@ -1,13 +1,13 @@
<script>
-import { GlNewDropdown, GlNewDropdownItem, GlSprintf } from '@gitlab/ui';
+import { GlDropdown, GlDropdownItem, GlSprintf } from '@gitlab/ui';
import { __, sprintf } from '~/locale';
import allVersionsMixin from '../../mixins/all_versions';
import { findVersionId } from '../../utils/design_management_utils';
export default {
components: {
- GlNewDropdown,
- GlNewDropdownItem,
+ GlDropdown,
+ GlDropdownItem,
GlSprintf,
},
mixins: [allVersionsMixin],
@@ -63,8 +63,8 @@ export default {
</script>
<template>
- <gl-new-dropdown :text="dropdownText" size="small">
- <gl-new-dropdown-item
+ <gl-dropdown :text="dropdownText" size="small">
+ <gl-dropdown-item
v-for="(version, index) in allVersions"
:key="version.id"
:is-check-item="true"
@@ -76,6 +76,6 @@ export default {
{{ allVersions.length - index }}
</template>
</gl-sprintf>
- </gl-new-dropdown-item>
- </gl-new-dropdown>
+ </gl-dropdown-item>
+ </gl-dropdown>
</template>
diff --git a/app/assets/javascripts/design_management/constants.js b/app/assets/javascripts/design_management/constants.js
index 21ff361a277..63a92ef5ec0 100644
--- a/app/assets/javascripts/design_management/constants.js
+++ b/app/assets/javascripts/design_management/constants.js
@@ -11,6 +11,7 @@ export const VALID_DATA_TRANSFER_TYPE = 'Files';
export const ACTIVE_DISCUSSION_SOURCE_TYPES = {
pin: 'pin',
discussion: 'discussion',
+ url: 'url',
};
export const DESIGN_DETAIL_LAYOUT_CLASSLIST = ['design-detail-layout', 'overflow-hidden', 'm-0'];
diff --git a/app/assets/javascripts/design_management/graphql.js b/app/assets/javascripts/design_management/graphql.js
index fae337aa75b..d1fe977b969 100644
--- a/app/assets/javascripts/design_management/graphql.js
+++ b/app/assets/javascripts/design_management/graphql.js
@@ -1,24 +1,70 @@
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import { uniqueId } from 'lodash';
+import produce from 'immer';
import { defaultDataIdFromObject } from 'apollo-cache-inmemory';
+import axios from '~/lib/utils/axios_utils';
import createDefaultClient from '~/lib/graphql';
import activeDiscussionQuery from './graphql/queries/active_discussion.query.graphql';
+import getDesignQuery from './graphql/queries/get_design.query.graphql';
import typeDefs from './graphql/typedefs.graphql';
+import { extractTodoIdFromDeletePath, createPendingTodo } from './utils/design_management_utils';
+import { CREATE_DESIGN_TODO_EXISTS_ERROR } from './utils/error_messages';
+import { addPendingTodoToStore } from './utils/cache_update';
Vue.use(VueApollo);
const resolvers = {
Mutation: {
updateActiveDiscussion: (_, { id = null, source }, { cache }) => {
- const data = cache.readQuery({ query: activeDiscussionQuery });
- data.activeDiscussion = {
- __typename: 'ActiveDiscussion',
- id,
- source,
- };
+ const sourceData = cache.readQuery({ query: activeDiscussionQuery });
+
+ const data = produce(sourceData, draftData => {
+ // eslint-disable-next-line no-param-reassign
+ draftData.activeDiscussion = {
+ __typename: 'ActiveDiscussion',
+ id,
+ source,
+ };
+ });
+
cache.writeQuery({ query: activeDiscussionQuery, data });
},
+ createDesignTodo: (
+ _,
+ { projectPath, issueId, designId, issueIid, filenames, atVersion },
+ { cache },
+ ) => {
+ return axios
+ .post(`/${projectPath}/todos`, {
+ issue_id: issueId,
+ issuable_id: designId,
+ issuable_type: 'design',
+ })
+ .then(({ data }) => {
+ const { delete_path } = data;
+ const todoId = extractTodoIdFromDeletePath(delete_path);
+ if (!todoId) {
+ return {
+ errors: [
+ {
+ message: CREATE_DESIGN_TODO_EXISTS_ERROR,
+ },
+ ],
+ };
+ }
+
+ const pendingTodo = createPendingTodo(todoId);
+ addPendingTodoToStore(cache, pendingTodo, getDesignQuery, {
+ fullPath: projectPath,
+ iid: issueIid,
+ filenames,
+ atVersion,
+ });
+
+ return pendingTodo;
+ });
+ },
},
};
@@ -37,6 +83,7 @@ const defaultClient = createDefaultClient(
},
},
typeDefs,
+ assumeImmutableResults: true,
},
);
diff --git a/app/assets/javascripts/design_management/graphql/fragments/design_list.fragment.graphql b/app/assets/javascripts/design_management/graphql/fragments/design_list.fragment.graphql
index bc3132f9b42..9bd70e7e886 100644
--- a/app/assets/javascripts/design_management/graphql/fragments/design_list.fragment.graphql
+++ b/app/assets/javascripts/design_management/graphql/fragments/design_list.fragment.graphql
@@ -5,4 +5,9 @@ fragment DesignListItem on Design {
notesCount
image
imageV432x230
+ currentUserTodos(state: pending) {
+ nodes {
+ id
+ }
+ }
}
diff --git a/app/assets/javascripts/design_management/graphql/fragments/design_note.fragment.graphql b/app/assets/javascripts/design_management/graphql/fragments/design_note.fragment.graphql
index 26edd2c0be1..28224671326 100644
--- a/app/assets/javascripts/design_management/graphql/fragments/design_note.fragment.graphql
+++ b/app/assets/javascripts/design_management/graphql/fragments/design_note.fragment.graphql
@@ -25,5 +25,10 @@ fragment DesignNote on Note {
}
discussion {
id
+ notes {
+ nodes {
+ id
+ }
+ }
}
}
diff --git a/app/assets/javascripts/design_management/graphql/mutations/create_design_todo.mutation.graphql b/app/assets/javascripts/design_management/graphql/mutations/create_design_todo.mutation.graphql
new file mode 100644
index 00000000000..0c989b2fdde
--- /dev/null
+++ b/app/assets/javascripts/design_management/graphql/mutations/create_design_todo.mutation.graphql
@@ -0,0 +1,17 @@
+mutation createDesignTodo(
+ $projectPath: String!
+ $issueId: String!
+ $designId: String!
+ $issueIid: String!
+ $filenames: [String]!
+ $atVersion: String
+) {
+ createDesignTodo(
+ projectPath: $projectPath
+ issueId: $issueId
+ designId: $designId
+ issueIid: $issueIid
+ filenames: $filenames
+ atVersion: $atVersion
+ ) @client
+}
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 ab987dda525..96869a404b1 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,6 +10,7 @@ query getDesign($fullPath: ID!, $iid: String!, $atVersion: ID, $filenames: [Stri
nodes {
...DesignItem
issue {
+ id
title
webPath
webUrl
diff --git a/app/assets/javascripts/design_management/index.js b/app/assets/javascripts/design_management/index.js
index 20c9cacf83f..1a87dd38137 100644
--- a/app/assets/javascripts/design_management/index.js
+++ b/app/assets/javascripts/design_management/index.js
@@ -4,7 +4,7 @@ import App from './components/app.vue';
import apolloProvider from './graphql';
export default () => {
- const el = document.querySelector('.js-design-management-new');
+ const el = document.querySelector('.js-design-management');
const { issueIid, projectPath, issuePath } = el.dataset;
const router = createRouter(issuePath);
diff --git a/app/assets/javascripts/design_management/pages/design/index.vue b/app/assets/javascripts/design_management/pages/design/index.vue
index 17b72e73127..c6225c516e2 100644
--- a/app/assets/javascripts/design_management/pages/design/index.vue
+++ b/app/assets/javascripts/design_management/pages/design/index.vue
@@ -19,6 +19,8 @@ import {
extractDiscussions,
extractDesign,
updateImageDiffNoteOptimisticResponse,
+ toDiffNoteGid,
+ extractDesignNoteId,
} from '../../utils/design_management_utils';
import {
updateStoreAfterAddImageDiffNote,
@@ -31,6 +33,7 @@ import {
DESIGN_NOT_FOUND_ERROR,
DESIGN_VERSION_NOT_EXIST_ERROR,
UPDATE_NOTE_ERROR,
+ TOGGLE_TODO_ERROR,
designDeletionError,
} from '../../utils/error_messages';
import { trackDesignDetailView } from '../../utils/tracking';
@@ -145,8 +148,11 @@ export default {
mounted() {
Mousetrap.bind('esc', this.closeDesign);
this.trackEvent();
- // We need to reset the active discussion when opening a new design
- this.updateActiveDiscussion();
+
+ // Set active discussion immediately.
+ // This will ensure that, if a note is specified in the URL hash,
+ // the browser will scroll to, and highlight, the note in the UI
+ this.updateActiveDiscussionFromUrl();
},
beforeDestroy() {
Mousetrap.unbind('esc', this.closeDesign);
@@ -221,7 +227,7 @@ export default {
},
onError(message, e) {
this.errorMessage = message;
- throw e;
+ if (e) throw e;
},
onCreateImageDiffNoteError(e) {
this.onError(ADD_IMAGE_DIFF_NOTE_ERROR, e);
@@ -241,6 +247,9 @@ export default {
onResolveDiscussionError(e) {
this.onError(UPDATE_IMAGE_DIFF_NOTE_ERROR, e);
},
+ onTodoError(e) {
+ this.onError(e?.message || TOGGLE_TODO_ERROR, e);
+ },
openCommentForm(annotationCoordinates) {
this.annotationCoordinates = annotationCoordinates;
if (this.$refs.newDiscussionForm) {
@@ -266,15 +275,20 @@ export default {
this.isLatestVersion,
);
},
- updateActiveDiscussion(id) {
+ updateActiveDiscussion(id, source = ACTIVE_DISCUSSION_SOURCE_TYPES.discussion) {
this.$apollo.mutate({
mutation: updateActiveDiscussionMutation,
variables: {
id,
- source: ACTIVE_DISCUSSION_SOURCE_TYPES.discussion,
+ source,
},
});
},
+ updateActiveDiscussionFromUrl() {
+ const noteId = extractDesignNoteId(this.$route.hash);
+ const diffNoteGid = noteId ? toDiffNoteGid(noteId) : undefined;
+ return this.updateActiveDiscussion(diffNoteGid, ACTIVE_DISCUSSION_SOURCE_TYPES.url);
+ },
toggleResolvedComments() {
this.resolvedDiscussionsExpanded = !this.resolvedDiscussionsExpanded;
},
@@ -339,6 +353,7 @@ export default {
@updateNoteError="onUpdateNoteError"
@resolveDiscussionError="onResolveDiscussionError"
@toggleResolvedComments="toggleResolvedComments"
+ @todoError="onTodoError"
>
<template #replyForm>
<apollo-mutation
@@ -357,8 +372,8 @@ export default {
v-model="comment"
:is-saving="loading"
:markdown-preview-path="markdownPreviewPath"
- @submitForm="mutate"
- @cancelForm="closeCommentForm"
+ @submit-form="mutate"
+ @cancel-form="closeCommentForm"
/> </apollo-mutation
></template>
</design-sidebar>
diff --git a/app/assets/javascripts/design_management/pages/index.vue b/app/assets/javascripts/design_management/pages/index.vue
index cd68e9d6c5b..6c4c8c75054 100644
--- a/app/assets/javascripts/design_management/pages/index.vue
+++ b/app/assets/javascripts/design_management/pages/index.vue
@@ -281,13 +281,8 @@ export default {
.mutate({
mutation: moveDesignMutation,
variables: this.designMoveVariables(newIndex, element),
- update: (store, { data: { designManagementMove } }) => {
- return updateDesignsOnStoreAfterReorder(
- store,
- designManagementMove,
- this.projectQueryBody,
- );
- },
+ update: (store, { data: { designManagementMove } }) =>
+ updateDesignsOnStoreAfterReorder(store, designManagementMove, this.projectQueryBody),
optimisticResponse: moveDesignOptimisticResponse(this.reorderedDesigns),
})
.catch(() => {
@@ -327,7 +322,7 @@ export default {
v-if="isLatestVersion"
variant="link"
size="small"
- class="gl-mr-3 js-select-all"
+ class="gl-mr-4 js-select-all"
@click="toggleDesignsSelection"
>{{ selectAllButtonText }}
</gl-button>
diff --git a/app/assets/javascripts/design_management/utils/cache_update.js b/app/assets/javascripts/design_management/utils/cache_update.js
index b79df9d01d5..ff41136fd54 100644
--- a/app/assets/javascripts/design_management/utils/cache_update.js
+++ b/app/assets/javascripts/design_management/utils/cache_update.js
@@ -1,22 +1,27 @@
/* eslint-disable @gitlab/require-i18n-strings */
import { groupBy } from 'lodash';
+import produce from 'immer';
import { deprecatedCreateFlash as createFlash } from '~/flash';
-import { extractCurrentDiscussion, extractDesign } from './design_management_utils';
+import { extractCurrentDiscussion, extractDesign, extractDesigns } from './design_management_utils';
import {
ADD_IMAGE_DIFF_NOTE_ERROR,
UPDATE_IMAGE_DIFF_NOTE_ERROR,
- ADD_DISCUSSION_COMMENT_ERROR,
+ DELETE_DESIGN_TODO_ERROR,
designDeletionError,
} from './error_messages';
+const designsOf = data => data.project.issue.designCollection.designs;
+
const deleteDesignsFromStore = (store, query, selectedDesigns) => {
- const data = store.readQuery(query);
+ const sourceData = store.readQuery(query);
- const changedDesigns = data.project.issue.designCollection.designs.nodes.filter(
- node => !selectedDesigns.includes(node.filename),
- );
- data.project.issue.designCollection.designs.nodes = [...changedDesigns];
+ const data = produce(sourceData, draftData => {
+ const changedDesigns = designsOf(sourceData).nodes.filter(
+ design => !selectedDesigns.includes(design.filename),
+ );
+ designsOf(draftData).nodes = [...changedDesigns];
+ });
store.writeQuery({
...query,
@@ -33,13 +38,15 @@ const deleteDesignsFromStore = (store, query, selectedDesigns) => {
*/
const addNewVersionToStore = (store, query, version) => {
if (!version) return;
+ const sourceData = store.readQuery(query);
- const data = store.readQuery(query);
-
- data.project.issue.designCollection.versions.nodes = [
- version,
- ...data.project.issue.designCollection.versions.nodes,
- ];
+ const data = produce(sourceData, draftData => {
+ // eslint-disable-next-line no-param-reassign
+ draftData.project.issue.designCollection.versions.nodes = [
+ version,
+ ...draftData.project.issue.designCollection.versions.nodes,
+ ];
+ });
store.writeQuery({
...query,
@@ -47,47 +54,12 @@ const addNewVersionToStore = (store, query, version) => {
});
};
-const addDiscussionCommentToStore = (store, createNote, query, queryVariables, discussionId) => {
- const data = store.readQuery({
- query,
- variables: queryVariables,
- });
-
- const design = extractDesign(data);
- const currentDiscussion = extractCurrentDiscussion(design.discussions, discussionId);
- currentDiscussion.notes.nodes = [...currentDiscussion.notes.nodes, createNote.note];
-
- design.notesCount += 1;
- if (
- !design.issue.participants.nodes.some(
- participant => participant.username === createNote.note.author.username,
- )
- ) {
- design.issue.participants.nodes = [
- ...design.issue.participants.nodes,
- {
- __typename: 'User',
- ...createNote.note.author,
- },
- ];
- }
- store.writeQuery({
- query,
- variables: queryVariables,
- data: {
- ...data,
- design: {
- ...design,
- },
- },
- });
-};
-
const addImageDiffNoteToStore = (store, createImageDiffNote, query, variables) => {
- const data = store.readQuery({
+ const sourceData = store.readQuery({
query,
variables,
});
+
const newDiscussion = {
__typename: 'Discussion',
id: createImageDiffNote.note.discussion.id,
@@ -101,100 +73,100 @@ const addImageDiffNoteToStore = (store, createImageDiffNote, query, variables) =
nodes: [createImageDiffNote.note],
},
};
- const design = extractDesign(data);
- const notesCount = design.notesCount + 1;
- design.discussions.nodes = [...design.discussions.nodes, newDiscussion];
- if (
- !design.issue.participants.nodes.some(
- participant => participant.username === createImageDiffNote.note.author.username,
- )
- ) {
- design.issue.participants.nodes = [
- ...design.issue.participants.nodes,
- {
- __typename: 'User',
- ...createImageDiffNote.note.author,
- },
- ];
- }
+
+ const data = produce(sourceData, draftData => {
+ const design = extractDesign(draftData);
+ design.notesCount += 1;
+ design.discussions.nodes = [...design.discussions.nodes, newDiscussion];
+
+ if (
+ !design.issue.participants.nodes.some(
+ participant => participant.username === createImageDiffNote.note.author.username,
+ )
+ ) {
+ design.issue.participants.nodes = [
+ ...design.issue.participants.nodes,
+ {
+ __typename: 'User',
+ ...createImageDiffNote.note.author,
+ },
+ ];
+ }
+ });
+
store.writeQuery({
query,
variables,
- data: {
- ...data,
- design: {
- ...design,
- notesCount,
- },
- },
+ data,
});
};
const updateImageDiffNoteInStore = (store, updateImageDiffNote, query, variables) => {
- const data = store.readQuery({
+ const sourceData = store.readQuery({
query,
variables,
});
- const design = extractDesign(data);
- const discussion = extractCurrentDiscussion(
- design.discussions,
- updateImageDiffNote.note.discussion.id,
- );
-
- discussion.notes = {
- ...discussion.notes,
- nodes: [updateImageDiffNote.note, ...discussion.notes.nodes.slice(1)],
- };
+ const data = produce(sourceData, draftData => {
+ const design = extractDesign(draftData);
+ const discussion = extractCurrentDiscussion(
+ design.discussions,
+ updateImageDiffNote.note.discussion.id,
+ );
+
+ discussion.notes = {
+ ...discussion.notes,
+ nodes: [updateImageDiffNote.note, ...discussion.notes.nodes.slice(1)],
+ };
+ });
store.writeQuery({
query,
variables,
- data: {
- ...data,
- design,
- },
+ data,
});
};
const addNewDesignToStore = (store, designManagementUpload, query) => {
- const data = store.readQuery(query);
+ const sourceData = store.readQuery(query);
- const currentDesigns = data.project.issue.designCollection.designs.nodes;
- const existingDesigns = groupBy(currentDesigns, 'filename');
- const newDesigns = currentDesigns.concat(
- designManagementUpload.designs.filter(d => !existingDesigns[d.filename]),
- );
+ const data = produce(sourceData, draftData => {
+ const currentDesigns = extractDesigns(draftData);
+ const existingDesigns = groupBy(currentDesigns, 'filename');
+ const newDesigns = currentDesigns.concat(
+ designManagementUpload.designs.filter(d => !existingDesigns[d.filename]),
+ );
- let newVersionNode;
- const findNewVersions = designManagementUpload.designs.find(design => design.versions);
+ let newVersionNode;
+ const findNewVersions = designManagementUpload.designs.find(design => design.versions);
- if (findNewVersions) {
- const findNewVersionsNodes = findNewVersions.versions.nodes;
+ if (findNewVersions) {
+ const findNewVersionsNodes = findNewVersions.versions.nodes;
- if (findNewVersionsNodes && findNewVersionsNodes.length) {
- newVersionNode = [findNewVersionsNodes[0]];
+ if (findNewVersionsNodes && findNewVersionsNodes.length) {
+ newVersionNode = [findNewVersionsNodes[0]];
+ }
}
- }
-
- const newVersions = [
- ...(newVersionNode || []),
- ...data.project.issue.designCollection.versions.nodes,
- ];
- const updatedDesigns = {
- __typename: 'DesignCollection',
- designs: {
- __typename: 'DesignConnection',
- nodes: newDesigns,
- },
- versions: {
- __typename: 'DesignVersionConnection',
- nodes: newVersions,
- },
- };
+ const newVersions = [
+ ...(newVersionNode || []),
+ ...draftData.project.issue.designCollection.versions.nodes,
+ ];
- data.project.issue.designCollection = updatedDesigns;
+ const updatedDesigns = {
+ __typename: 'DesignCollection',
+ designs: {
+ __typename: 'DesignConnection',
+ nodes: newDesigns,
+ },
+ versions: {
+ __typename: 'DesignVersionConnection',
+ nodes: newVersions,
+ },
+ };
+ // eslint-disable-next-line no-param-reassign
+ draftData.project.issue.designCollection = updatedDesigns;
+ });
store.writeQuery({
...query,
@@ -203,14 +175,63 @@ const addNewDesignToStore = (store, designManagementUpload, query) => {
};
const moveDesignInStore = (store, designManagementMove, query) => {
- const data = store.readQuery(query);
- data.project.issue.designCollection.designs = designManagementMove.designCollection.designs;
+ const sourceData = store.readQuery(query);
+
+ const data = produce(sourceData, draftData => {
+ // eslint-disable-next-line no-param-reassign
+ draftData.project.issue.designCollection.designs =
+ designManagementMove.designCollection.designs;
+ });
+
store.writeQuery({
...query,
data,
});
};
+export const addPendingTodoToStore = (store, pendingTodo, query, queryVariables) => {
+ const sourceData = store.readQuery({
+ query,
+ variables: queryVariables,
+ });
+
+ const data = produce(sourceData, draftData => {
+ const design = extractDesign(draftData);
+ const existingTodos = design.currentUserTodos?.nodes || [];
+ const newTodoNodes = [...existingTodos, { ...pendingTodo, __typename: 'Todo' }];
+
+ if (!design.currentUserTodos) {
+ design.currentUserTodos = {
+ __typename: 'TodoConnection',
+ nodes: newTodoNodes,
+ };
+ } else {
+ design.currentUserTodos.nodes = newTodoNodes;
+ }
+ });
+
+ store.writeQuery({ query, variables: queryVariables, data });
+};
+
+export const deletePendingTodoFromStore = (store, todoMarkDone, query, queryVariables) => {
+ const sourceData = store.readQuery({
+ query,
+ variables: queryVariables,
+ });
+
+ const {
+ todo: { id: todoId },
+ } = todoMarkDone;
+ const data = produce(sourceData, draftData => {
+ const design = extractDesign(draftData);
+ const existingTodos = design.currentUserTodos?.nodes || [];
+
+ design.currentUserTodos.nodes = existingTodos.filter(({ id }) => id !== todoId);
+ });
+
+ store.writeQuery({ query, variables: queryVariables, data });
+};
+
const onError = (data, message) => {
createFlash(message);
throw new Error(data.errors);
@@ -235,20 +256,6 @@ export const updateStoreAfterDesignsDelete = (store, data, query, designs) => {
}
};
-export const updateStoreAfterAddDiscussionComment = (
- store,
- data,
- query,
- queryVariables,
- discussionId,
-) => {
- if (hasErrors(data)) {
- onError(data, ADD_DISCUSSION_COMMENT_ERROR);
- } else {
- addDiscussionCommentToStore(store, data, query, queryVariables, discussionId);
- }
-};
-
export const updateStoreAfterAddImageDiffNote = (store, data, query, queryVariables) => {
if (hasErrors(data)) {
onError(data, ADD_IMAGE_DIFF_NOTE_ERROR);
@@ -280,3 +287,11 @@ export const updateDesignsOnStoreAfterReorder = (store, data, query) => {
moveDesignInStore(store, data, query);
}
};
+
+export const updateStoreAfterDeleteDesignTodo = (store, data, query, queryVariables) => {
+ if (hasErrors(data)) {
+ onError(data, DELETE_DESIGN_TODO_ERROR);
+ } else {
+ deletePendingTodoFromStore(store, data, query, queryVariables);
+ }
+};
diff --git a/app/assets/javascripts/design_management/utils/design_management_utils.js b/app/assets/javascripts/design_management/utils/design_management_utils.js
index da8f89ff960..93e4d6060c3 100644
--- a/app/assets/javascripts/design_management/utils/design_management_utils.js
+++ b/app/assets/javascripts/design_management/utils/design_management_utils.js
@@ -30,10 +30,25 @@ export const findVersionId = id => (id.match('::Version/(.+$)') || [])[1];
export const findNoteId = id => (id.match('DiffNote/(.+$)') || [])[1];
+export const findIssueId = id => (id.match('Issue/(.+$)') || [])[1];
+
+export const findDesignId = id => (id.match('Design/(.+$)') || [])[1];
+
export const extractDesigns = data => data.project.issue.designCollection.designs.nodes;
export const extractDesign = data => (extractDesigns(data) || [])[0];
+export const toDiffNoteGid = noteId => `gid://gitlab/DiffNote/${noteId}`;
+
+/**
+ * Return the note ID from a URL hash parameter
+ * @param {String} urlHash URL hash, including `#` prefix
+ */
+export const extractDesignNoteId = urlHash => {
+ const [, noteId] = urlHash.match('#note_([0-9]+$)') || [];
+ return noteId || null;
+};
+
/**
* Generates optimistic response for a design upload mutation
* @param {Array<File>} files
@@ -135,3 +150,22 @@ const normalizeAuthor = author => ({
export const extractParticipants = users => users.map(node => normalizeAuthor(node));
export const getPageLayoutElement = () => document.querySelector('.layout-page');
+
+/**
+ * Extract the ID of the To-Do for a given 'delete' path
+ * Example of todoDeletePath: /delete/1234
+ * @param {String} todoDeletePath delete_path from REST API response
+ */
+export const extractTodoIdFromDeletePath = todoDeletePath =>
+ (todoDeletePath.match('todos/([0-9]+$)') || [])[1];
+
+const createTodoGid = todoId => {
+ return `gid://gitlab/Todo/${todoId}`;
+};
+
+export const createPendingTodo = todoId => {
+ return {
+ __typename: 'Todo', // eslint-disable-line @gitlab/require-i18n-strings
+ id: createTodoGid(todoId),
+ };
+};
diff --git a/app/assets/javascripts/design_management/utils/error_messages.js b/app/assets/javascripts/design_management/utils/error_messages.js
index c815b11737d..bd21d711462 100644
--- a/app/assets/javascripts/design_management/utils/error_messages.js
+++ b/app/assets/javascripts/design_management/utils/error_messages.js
@@ -44,6 +44,14 @@ export const MOVE_DESIGN_ERROR = __(
'Something went wrong when reordering designs. Please try again',
);
+export const CREATE_DESIGN_TODO_ERROR = __('Failed to create To-Do for the design.');
+
+export const CREATE_DESIGN_TODO_EXISTS_ERROR = __('There is already a To-Do for this design.');
+
+export const DELETE_DESIGN_TODO_ERROR = __('Failed to remove To-Do for the design.');
+
+export const TOGGLE_TODO_ERROR = __('Failed to toggle To-Do for the design.');
+
const MAX_SKIPPED_FILES_LISTINGS = 5;
const oneDesignSkippedMessage = filename =>
diff --git a/app/assets/javascripts/design_management/utils/tracking.js b/app/assets/javascripts/design_management/utils/tracking.js
index b3ecc1453a6..49fa306914c 100644
--- a/app/assets/javascripts/design_management/utils/tracking.js
+++ b/app/assets/javascripts/design_management/utils/tracking.js
@@ -5,7 +5,6 @@ const DESIGN_TRACKING_CONTEXT_SCHEMA = 'iglu:com.gitlab/design_management_contex
const DESIGN_TRACKING_PAGE_NAME = 'projects:issues:design';
const DESIGN_TRACKING_EVENT_NAME = 'view_design';
-// eslint-disable-next-line import/prefer-default-export
export function trackDesignDetailView(
referer = '',
owner = '',
diff --git a/app/assets/javascripts/design_management_legacy/components/app.vue b/app/assets/javascripts/design_management_legacy/components/app.vue
deleted file mode 100644
index 98240aef810..00000000000
--- a/app/assets/javascripts/design_management_legacy/components/app.vue
+++ /dev/null
@@ -1,3 +0,0 @@
-<template>
- <router-view />
-</template>
diff --git a/app/assets/javascripts/design_management_legacy/components/delete_button.vue b/app/assets/javascripts/design_management_legacy/components/delete_button.vue
deleted file mode 100644
index 1fd902c9ed7..00000000000
--- a/app/assets/javascripts/design_management_legacy/components/delete_button.vue
+++ /dev/null
@@ -1,64 +0,0 @@
-<script>
-import { GlDeprecatedButton, GlModal, GlModalDirective } from '@gitlab/ui';
-import { uniqueId } from 'lodash';
-
-export default {
- name: 'DeleteButton',
- components: {
- GlDeprecatedButton,
- GlModal,
- },
- directives: {
- GlModalDirective,
- },
- props: {
- isDeleting: {
- type: Boolean,
- required: false,
- default: false,
- },
- buttonClass: {
- type: String,
- required: false,
- default: '',
- },
- buttonVariant: {
- type: String,
- required: false,
- default: '',
- },
- hasSelectedDesigns: {
- type: Boolean,
- required: false,
- default: true,
- },
- },
- data() {
- return {
- modalId: uniqueId('design-deletion-confirmation-'),
- };
- },
-};
-</script>
-
-<template>
- <div>
- <gl-modal
- :modal-id="modalId"
- :title="s__('DesignManagement|Delete designs confirmation')"
- :ok-title="s__('DesignManagement|Delete')"
- ok-variant="danger"
- @ok="$emit('deleteSelectedDesigns')"
- >
- <p>{{ s__('DesignManagement|Are you sure you want to delete the selected designs?') }}</p>
- </gl-modal>
- <gl-deprecated-button
- v-gl-modal-directive="modalId"
- :variant="buttonVariant"
- :disabled="isDeleting || !hasSelectedDesigns"
- :class="buttonClass"
- >
- <slot></slot>
- </gl-deprecated-button>
- </div>
-</template>
diff --git a/app/assets/javascripts/design_management_legacy/components/design_destroyer.vue b/app/assets/javascripts/design_management_legacy/components/design_destroyer.vue
deleted file mode 100644
index 62460ca551c..00000000000
--- a/app/assets/javascripts/design_management_legacy/components/design_destroyer.vue
+++ /dev/null
@@ -1,66 +0,0 @@
-<script>
-import { ApolloMutation } from 'vue-apollo';
-import getDesignListQuery from '../graphql/queries/get_design_list.query.graphql';
-import destroyDesignMutation from '../graphql/mutations/destroy_design.mutation.graphql';
-import { updateStoreAfterDesignsDelete } from '../utils/cache_update';
-
-export default {
- components: {
- ApolloMutation,
- },
- props: {
- filenames: {
- type: Array,
- required: true,
- },
- projectPath: {
- type: String,
- required: true,
- },
- iid: {
- type: String,
- required: true,
- },
- },
- computed: {
- projectQueryBody() {
- return {
- query: getDesignListQuery,
- variables: { fullPath: this.projectPath, iid: this.iid, atVersion: null },
- };
- },
- },
- methods: {
- updateStoreAfterDelete(
- store,
- {
- data: { designManagementDelete },
- },
- ) {
- updateStoreAfterDesignsDelete(
- store,
- designManagementDelete,
- this.projectQueryBody,
- this.filenames,
- );
- },
- },
- destroyDesignMutation,
-};
-</script>
-
-<template>
- <apollo-mutation
- #default="{ mutate, loading, error }"
- :mutation="$options.destroyDesignMutation"
- :variables="{
- filenames,
- projectPath,
- iid,
- }"
- :update="updateStoreAfterDelete"
- v-on="$listeners"
- >
- <slot v-bind="{ mutate, loading, error }"></slot>
- </apollo-mutation>
-</template>
diff --git a/app/assets/javascripts/design_management_legacy/components/design_note_pin.vue b/app/assets/javascripts/design_management_legacy/components/design_note_pin.vue
deleted file mode 100644
index 2b5e62c2870..00000000000
--- a/app/assets/javascripts/design_management_legacy/components/design_note_pin.vue
+++ /dev/null
@@ -1,61 +0,0 @@
-<script>
-import { GlIcon } from '@gitlab/ui';
-import { __, sprintf } from '~/locale';
-
-export default {
- name: 'DesignNotePin',
- components: {
- GlIcon,
- },
- props: {
- position: {
- type: Object,
- required: true,
- },
- label: {
- type: Number,
- required: false,
- default: null,
- },
- repositioning: {
- type: Boolean,
- required: false,
- default: false,
- },
- },
- computed: {
- isNewNote() {
- return this.label === null;
- },
- pinStyle() {
- return this.repositioning ? { ...this.position, cursor: 'move' } : this.position;
- },
- pinLabel() {
- return this.isNewNote
- ? __('Comment form position')
- : sprintf(__("Comment '%{label}' position"), { label: this.label });
- },
- },
-};
-</script>
-
-<template>
- <button
- :style="pinStyle"
- :aria-label="pinLabel"
- :class="{
- 'btn-transparent comment-indicator': isNewNote,
- 'js-image-badge badge badge-pill': !isNewNote,
- }"
- class="design-pin gl-absolute gl-display-flex gl-align-items-center gl-justify-content-center gl-p-0"
- type="button"
- @mousedown="$emit('mousedown', $event)"
- @mouseup="$emit('mouseup', $event)"
- @click="$emit('click', $event)"
- >
- <gl-icon v-if="isNewNote" name="image-comment-dark" :size="24" />
- <template v-else>
- {{ label }}
- </template>
- </button>
-</template>
diff --git a/app/assets/javascripts/design_management_legacy/components/design_notes/design_discussion.vue b/app/assets/javascripts/design_management_legacy/components/design_notes/design_discussion.vue
deleted file mode 100644
index 6a20517eed7..00000000000
--- a/app/assets/javascripts/design_management_legacy/components/design_notes/design_discussion.vue
+++ /dev/null
@@ -1,297 +0,0 @@
-<script>
-import { ApolloMutation } from 'vue-apollo';
-import { GlTooltipDirective, GlIcon, GlLoadingIcon, GlLink } from '@gitlab/ui';
-import { s__ } from '~/locale';
-import ReplyPlaceholder from '~/notes/components/discussion_reply_placeholder.vue';
-import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
-import allVersionsMixin from '../../mixins/all_versions';
-import createNoteMutation from '../../graphql/mutations/create_note.mutation.graphql';
-import toggleResolveDiscussionMutation from '../../graphql/mutations/toggle_resolve_discussion.mutation.graphql';
-import getDesignQuery from '../../graphql/queries/get_design.query.graphql';
-import activeDiscussionQuery from '../../graphql/queries/active_discussion.query.graphql';
-import DesignNote from './design_note.vue';
-import DesignReplyForm from './design_reply_form.vue';
-import { updateStoreAfterAddDiscussionComment } from '../../utils/cache_update';
-import { ACTIVE_DISCUSSION_SOURCE_TYPES } from '../../constants';
-import ToggleRepliesWidget from './toggle_replies_widget.vue';
-
-export default {
- components: {
- ApolloMutation,
- DesignNote,
- ReplyPlaceholder,
- DesignReplyForm,
- GlIcon,
- GlLoadingIcon,
- GlLink,
- ToggleRepliesWidget,
- TimeAgoTooltip,
- },
- directives: {
- GlTooltip: GlTooltipDirective,
- },
- mixins: [allVersionsMixin],
- props: {
- discussion: {
- type: Object,
- required: true,
- },
- noteableId: {
- type: String,
- required: true,
- },
- designId: {
- type: String,
- required: true,
- },
- markdownPreviewPath: {
- type: String,
- required: false,
- default: '',
- },
- resolvedDiscussionsExpanded: {
- type: Boolean,
- required: true,
- },
- discussionWithOpenForm: {
- type: String,
- required: true,
- },
- },
- apollo: {
- activeDiscussion: {
- query: activeDiscussionQuery,
- result({ data }) {
- const discussionId = data.activeDiscussion.id;
- if (this.discussion.resolved && !this.resolvedDiscussionsExpanded) {
- return;
- }
- // We watch any changes to the active discussion from the design pins and scroll to this discussion if it exists
- // We don't want scrollIntoView to be triggered from the discussion click itself
- if (
- discussionId &&
- data.activeDiscussion.source === ACTIVE_DISCUSSION_SOURCE_TYPES.pin &&
- discussionId === this.discussion.notes[0].id
- ) {
- this.$el.scrollIntoView({
- behavior: 'smooth',
- inline: 'start',
- });
- }
- },
- },
- },
- data() {
- return {
- discussionComment: '',
- isFormRendered: false,
- activeDiscussion: {},
- isResolving: false,
- shouldChangeResolvedStatus: false,
- areRepliesCollapsed: this.discussion.resolved,
- };
- },
- computed: {
- mutationPayload() {
- return {
- noteableId: this.noteableId,
- body: this.discussionComment,
- discussionId: this.discussion.id,
- };
- },
- designVariables() {
- return {
- fullPath: this.projectPath,
- iid: this.issueIid,
- filenames: [this.$route.params.id],
- atVersion: this.designsVersion,
- };
- },
- isDiscussionHighlighted() {
- return this.discussion.notes[0].id === this.activeDiscussion.id;
- },
- resolveCheckboxText() {
- return this.discussion.resolved
- ? s__('DesignManagement|Unresolve thread')
- : s__('DesignManagement|Resolve thread');
- },
- firstNote() {
- return this.discussion.notes[0];
- },
- discussionReplies() {
- return this.discussion.notes.slice(1);
- },
- areRepliesShown() {
- return !this.discussion.resolved || !this.areRepliesCollapsed;
- },
- resolveIconName() {
- return this.discussion.resolved ? 'check-circle-filled' : 'check-circle';
- },
- isRepliesWidgetVisible() {
- return this.discussion.resolved && this.discussionReplies.length > 0;
- },
- isReplyPlaceholderVisible() {
- return this.areRepliesShown || !this.discussionReplies.length;
- },
- isFormVisible() {
- return this.isFormRendered && this.discussionWithOpenForm === this.discussion.id;
- },
- },
- methods: {
- addDiscussionComment(
- store,
- {
- data: { createNote },
- },
- ) {
- updateStoreAfterAddDiscussionComment(
- store,
- createNote,
- getDesignQuery,
- this.designVariables,
- this.discussion.id,
- );
- },
- onDone() {
- this.discussionComment = '';
- this.hideForm();
- if (this.shouldChangeResolvedStatus) {
- this.toggleResolvedStatus();
- }
- },
- onCreateNoteError(err) {
- this.$emit('createNoteError', err);
- },
- hideForm() {
- this.isFormRendered = false;
- this.discussionComment = '';
- },
- showForm() {
- this.$emit('openForm', this.discussion.id);
- this.isFormRendered = true;
- },
- toggleResolvedStatus() {
- this.isResolving = true;
- this.$apollo
- .mutate({
- mutation: toggleResolveDiscussionMutation,
- variables: { id: this.discussion.id, resolve: !this.discussion.resolved },
- })
- .then(({ data }) => {
- if (data.errors?.length > 0) {
- this.$emit('resolveDiscussionError', data.errors[0]);
- }
- })
- .catch(err => {
- this.$emit('resolveDiscussionError', err);
- })
- .finally(() => {
- this.isResolving = false;
- });
- },
- },
- createNoteMutation,
-};
-</script>
-
-<template>
- <div class="design-discussion-wrapper">
- <div
- class="badge badge-pill gl-display-flex gl-align-items-center gl-justify-content-center"
- :class="{ resolved: discussion.resolved }"
- type="button"
- >
- {{ discussion.index }}
- </div>
- <ul
- class="design-discussion bordered-box gl-relative gl-p-0 gl-list-style-none"
- data-qa-selector="design_discussion_content"
- >
- <design-note
- :note="firstNote"
- :markdown-preview-path="markdownPreviewPath"
- :is-resolving="isResolving"
- :class="{ 'gl-bg-blue-50': isDiscussionHighlighted }"
- @error="$emit('updateNoteError', $event)"
- >
- <template v-if="discussion.resolvable" #resolveDiscussion>
- <button
- v-gl-tooltip
- :class="{ 'is-active': discussion.resolved }"
- :title="resolveCheckboxText"
- :aria-label="resolveCheckboxText"
- type="button"
- class="line-resolve-btn note-action-button gl-mr-3"
- data-testid="resolve-button"
- @click.stop="toggleResolvedStatus"
- >
- <gl-icon v-if="!isResolving" :name="resolveIconName" data-testid="resolve-icon" />
- <gl-loading-icon v-else inline />
- </button>
- </template>
- <template v-if="discussion.resolved" #resolvedStatus>
- <p class="gl-text-gray-500 gl-font-sm gl-m-0 gl-mt-5" data-testid="resolved-message">
- {{ __('Resolved by') }}
- <gl-link
- class="gl-text-gray-500 gl-text-decoration-none gl-font-sm link-inherit-color"
- :href="discussion.resolvedBy.webUrl"
- target="_blank"
- >{{ discussion.resolvedBy.name }}</gl-link
- >
- <time-ago-tooltip :time="discussion.resolvedAt" tooltip-placement="bottom" />
- </p>
- </template>
- </design-note>
- <toggle-replies-widget
- v-if="isRepliesWidgetVisible"
- :collapsed="areRepliesCollapsed"
- :replies="discussionReplies"
- @toggle="areRepliesCollapsed = !areRepliesCollapsed"
- />
- <design-note
- v-for="note in discussionReplies"
- v-show="areRepliesShown"
- :key="note.id"
- :note="note"
- :markdown-preview-path="markdownPreviewPath"
- :is-resolving="isResolving"
- :class="{ 'gl-bg-blue-50': isDiscussionHighlighted }"
- @error="$emit('updateNoteError', $event)"
- />
- <li v-show="isReplyPlaceholderVisible" class="reply-wrapper">
- <reply-placeholder
- v-if="!isFormVisible"
- class="qa-discussion-reply"
- :button-text="__('Reply...')"
- @onClick="showForm"
- />
- <apollo-mutation
- v-else
- #default="{ mutate, loading }"
- :mutation="$options.createNoteMutation"
- :variables="{
- input: mutationPayload,
- }"
- :update="addDiscussionComment"
- @done="onDone"
- @error="onCreateNoteError"
- >
- <design-reply-form
- v-model="discussionComment"
- :is-saving="loading"
- :markdown-preview-path="markdownPreviewPath"
- @submitForm="mutate"
- @cancelForm="hideForm"
- >
- <template v-if="discussion.resolvable" #resolveCheckbox>
- <label data-testid="resolve-checkbox">
- <input v-model="shouldChangeResolvedStatus" type="checkbox" />
- {{ resolveCheckboxText }}
- </label>
- </template>
- </design-reply-form>
- </apollo-mutation>
- </li>
- </ul>
- </div>
-</template>
diff --git a/app/assets/javascripts/design_management_legacy/components/design_notes/design_note.vue b/app/assets/javascripts/design_management_legacy/components/design_notes/design_note.vue
deleted file mode 100644
index b1f3a43a66d..00000000000
--- a/app/assets/javascripts/design_management_legacy/components/design_notes/design_note.vue
+++ /dev/null
@@ -1,156 +0,0 @@
-<script>
-import { ApolloMutation } from 'vue-apollo';
-import { GlTooltipDirective, GlIcon } from '@gitlab/ui';
-import updateNoteMutation from '../../graphql/mutations/update_note.mutation.graphql';
-import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue';
-import TimelineEntryItem from '~/vue_shared/components/notes/timeline_entry_item.vue';
-import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
-import DesignReplyForm from './design_reply_form.vue';
-import { findNoteId } from '../../utils/design_management_utils';
-import { hasErrors } from '../../utils/cache_update';
-
-export default {
- components: {
- UserAvatarLink,
- TimelineEntryItem,
- TimeAgoTooltip,
- DesignReplyForm,
- ApolloMutation,
- GlIcon,
- },
- directives: {
- GlTooltip: GlTooltipDirective,
- },
- props: {
- note: {
- type: Object,
- required: true,
- },
- markdownPreviewPath: {
- type: String,
- required: false,
- default: '',
- },
- },
- data() {
- return {
- noteText: this.note.body,
- isEditing: false,
- };
- },
- computed: {
- author() {
- return this.note.author;
- },
- noteAnchorId() {
- return findNoteId(this.note.id);
- },
- isNoteLinked() {
- return this.$route.hash === `#note_${this.noteAnchorId}`;
- },
- mutationPayload() {
- return {
- id: this.note.id,
- body: this.noteText,
- };
- },
- isEditButtonVisible() {
- return !this.isEditing && this.note.userPermissions.adminNote;
- },
- },
- mounted() {
- if (this.isNoteLinked) {
- this.$refs.anchor.$el.scrollIntoView({ behavior: 'smooth', inline: 'start' });
- }
- },
- methods: {
- hideForm() {
- this.isEditing = false;
- this.noteText = this.note.body;
- },
- onDone({ data }) {
- this.hideForm();
- if (hasErrors(data.updateNote)) {
- this.$emit('error', data.errors[0]);
- }
- },
- },
- updateNoteMutation,
-};
-</script>
-
-<template>
- <timeline-entry-item :id="`note_${noteAnchorId}`" ref="anchor" class="design-note note-form">
- <user-avatar-link
- :link-href="author.webUrl"
- :img-src="author.avatarUrl"
- :img-alt="author.username"
- :img-size="40"
- />
- <div class="d-flex justify-content-between">
- <div>
- <a
- v-once
- :href="author.webUrl"
- class="js-user-link"
- :data-user-id="author.id"
- :data-username="author.username"
- >
- <span class="note-header-author-name bold">{{ author.name }}</span>
- <span v-if="author.status_tooltip_html" v-html="author.status_tooltip_html"></span>
- <span class="note-headline-light">@{{ author.username }}</span>
- </a>
- <span class="note-headline-light note-headline-meta">
- <span class="system-note-message"> <slot></slot> </span>
- <template v-if="note.createdAt">
- <span class="system-note-separator"></span>
- <a class="note-timestamp system-note-separator" :href="`#note_${noteAnchorId}`">
- <time-ago-tooltip :time="note.createdAt" tooltip-placement="bottom" />
- </a>
- </template>
- </span>
- </div>
- <div class="gl-display-flex">
- <slot name="resolveDiscussion"></slot>
- <button
- v-if="isEditButtonVisible"
- v-gl-tooltip
- type="button"
- :title="__('Edit comment')"
- class="note-action-button js-note-edit btn btn-transparent qa-note-edit-button"
- @click="isEditing = true"
- >
- <gl-icon name="pencil" class="link-highlight" />
- </button>
- </div>
- </div>
- <template v-if="!isEditing">
- <div
- class="note-text js-note-text md"
- data-qa-selector="note_content"
- v-html="note.bodyHtml"
- ></div>
- <slot name="resolvedStatus"></slot>
- </template>
- <apollo-mutation
- v-else
- #default="{ mutate, loading }"
- :mutation="$options.updateNoteMutation"
- :variables="{
- input: mutationPayload,
- }"
- @error="$emit('error', $event)"
- @done="onDone"
- >
- <design-reply-form
- v-model="noteText"
- :is-saving="loading"
- :markdown-preview-path="markdownPreviewPath"
- :is-new-comment="false"
- class="mt-5"
- @submitForm="mutate"
- @cancelForm="hideForm"
- />
- </apollo-mutation>
- </timeline-entry-item>
-</template>
diff --git a/app/assets/javascripts/design_management_legacy/components/design_notes/design_reply_form.vue b/app/assets/javascripts/design_management_legacy/components/design_notes/design_reply_form.vue
deleted file mode 100644
index 969034909f2..00000000000
--- a/app/assets/javascripts/design_management_legacy/components/design_notes/design_reply_form.vue
+++ /dev/null
@@ -1,141 +0,0 @@
-<script>
-import { GlDeprecatedButton, GlModal } from '@gitlab/ui';
-import MarkdownField from '~/vue_shared/components/markdown/field.vue';
-import { s__ } from '~/locale';
-
-export default {
- name: 'DesignReplyForm',
- components: {
- MarkdownField,
- GlDeprecatedButton,
- GlModal,
- },
- props: {
- markdownPreviewPath: {
- type: String,
- required: false,
- default: '',
- },
- value: {
- type: String,
- required: true,
- },
- isSaving: {
- type: Boolean,
- required: true,
- },
- isNewComment: {
- type: Boolean,
- required: false,
- default: true,
- },
- },
- data() {
- return {
- formText: this.value,
- };
- },
- computed: {
- hasValue() {
- return this.value.trim().length > 0;
- },
- modalSettings() {
- if (this.isNewComment) {
- return {
- title: s__('DesignManagement|Cancel comment confirmation'),
- okTitle: s__('DesignManagement|Discard comment'),
- cancelTitle: s__('DesignManagement|Keep comment'),
- content: s__('DesignManagement|Are you sure you want to cancel creating this comment?'),
- };
- }
- return {
- title: s__('DesignManagement|Cancel comment update confirmation'),
- okTitle: s__('DesignManagement|Cancel changes'),
- cancelTitle: s__('DesignManagement|Keep changes'),
- content: s__('DesignManagement|Are you sure you want to cancel changes to this comment?'),
- };
- },
- buttonText() {
- return this.isNewComment
- ? s__('DesignManagement|Comment')
- : s__('DesignManagement|Save comment');
- },
- },
- mounted() {
- this.focusInput();
- },
- methods: {
- submitForm() {
- if (this.hasValue) this.$emit('submitForm');
- },
- cancelComment() {
- if (this.hasValue && this.formText !== this.value) {
- this.$refs.cancelCommentModal.show();
- } else {
- this.$emit('cancelForm');
- }
- },
- focusInput() {
- this.$refs.textarea.focus();
- },
- },
-};
-</script>
-
-<template>
- <form class="new-note common-note-form" @submit.prevent>
- <markdown-field
- :markdown-preview-path="markdownPreviewPath"
- :can-attach-file="false"
- :enable-autocomplete="true"
- :textarea-value="value"
- markdown-docs-path="/help/user/markdown"
- class="bordered-box"
- >
- <template #textarea>
- <textarea
- ref="textarea"
- :value="value"
- class="note-textarea js-gfm-input js-autosize markdown-area"
- dir="auto"
- data-supports-quick-actions="false"
- data-qa-selector="note_textarea"
- :aria-label="__('Description')"
- :placeholder="__('Write a comment…')"
- @input="$emit('input', $event.target.value)"
- @keydown.meta.enter="submitForm"
- @keydown.ctrl.enter="submitForm"
- @keyup.esc.stop="cancelComment"
- >
- </textarea>
- </template>
- </markdown-field>
- <slot name="resolveCheckbox"></slot>
- <div class="note-form-actions gl-display-flex gl-justify-content-space-between">
- <gl-deprecated-button
- ref="submitButton"
- :disabled="!hasValue || isSaving"
- variant="success"
- type="submit"
- data-track-event="click_button"
- data-qa-selector="save_comment_button"
- @click="$emit('submitForm')"
- >
- {{ buttonText }}
- </gl-deprecated-button>
- <gl-deprecated-button ref="cancelButton" @click="cancelComment">{{
- __('Cancel')
- }}</gl-deprecated-button>
- </div>
- <gl-modal
- ref="cancelCommentModal"
- ok-variant="danger"
- :title="modalSettings.title"
- :ok-title="modalSettings.okTitle"
- :cancel-title="modalSettings.cancelTitle"
- modal-id="cancel-comment-modal"
- @ok="$emit('cancelForm')"
- >{{ modalSettings.content }}
- </gl-modal>
- </form>
-</template>
diff --git a/app/assets/javascripts/design_management_legacy/components/design_notes/toggle_replies_widget.vue b/app/assets/javascripts/design_management_legacy/components/design_notes/toggle_replies_widget.vue
deleted file mode 100644
index 2e366282de3..00000000000
--- a/app/assets/javascripts/design_management_legacy/components/design_notes/toggle_replies_widget.vue
+++ /dev/null
@@ -1,70 +0,0 @@
-<script>
-import { GlIcon, GlButton, GlLink } from '@gitlab/ui';
-import { __, n__ } from '~/locale';
-import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
-
-export default {
- name: 'ToggleNotesWidget',
- components: {
- GlIcon,
- GlButton,
- GlLink,
- TimeAgoTooltip,
- },
- props: {
- collapsed: {
- type: Boolean,
- required: true,
- },
- replies: {
- type: Array,
- required: true,
- },
- },
- computed: {
- lastReply() {
- return this.replies[this.replies.length - 1];
- },
- iconName() {
- return this.collapsed ? 'chevron-right' : 'chevron-down';
- },
- toggleText() {
- return this.collapsed
- ? `${this.replies.length} ${n__('reply', 'replies', this.replies.length)}`
- : __('Collapse replies');
- },
- },
-};
-</script>
-
-<template>
- <li
- class="toggle-comments gl-bg-gray-50 gl-display-flex gl-align-items-center gl-py-3"
- :class="{ expanded: !collapsed }"
- data-testid="toggle-comments-wrapper"
- >
- <gl-icon :name="iconName" class="gl-ml-3" @click.stop="$emit('toggle')" />
- <gl-button
- variant="link"
- class="toggle-comments-button gl-ml-2 gl-mr-2"
- @click.stop="$emit('toggle')"
- >
- {{ toggleText }}
- </gl-button>
- <template v-if="collapsed">
- <span class="gl-text-gray-500">{{ __('Last reply by') }}</span>
- <gl-link
- :href="lastReply.author.webUrl"
- target="_blank"
- class="link-inherit-color gl-text-body gl-text-decoration-none gl-font-weight-bold gl-ml-2 gl-mr-2"
- >
- {{ lastReply.author.name }}
- </gl-link>
- <time-ago-tooltip
- :time="lastReply.createdAt"
- tooltip-placement="bottom"
- class="gl-text-gray-500"
- />
- </template>
- </li>
-</template>
diff --git a/app/assets/javascripts/design_management_legacy/components/design_overlay.vue b/app/assets/javascripts/design_management_legacy/components/design_overlay.vue
deleted file mode 100644
index 926e7c74802..00000000000
--- a/app/assets/javascripts/design_management_legacy/components/design_overlay.vue
+++ /dev/null
@@ -1,287 +0,0 @@
-<script>
-import activeDiscussionQuery from '../graphql/queries/active_discussion.query.graphql';
-import updateActiveDiscussionMutation from '../graphql/mutations/update_active_discussion.mutation.graphql';
-import DesignNotePin from './design_note_pin.vue';
-import { ACTIVE_DISCUSSION_SOURCE_TYPES } from '../constants';
-
-export default {
- name: 'DesignOverlay',
- components: {
- DesignNotePin,
- },
- props: {
- dimensions: {
- type: Object,
- required: true,
- },
- position: {
- type: Object,
- required: true,
- },
- notes: {
- type: Array,
- required: false,
- default: () => [],
- },
- currentCommentForm: {
- type: Object,
- required: false,
- default: null,
- },
- disableCommenting: {
- type: Boolean,
- required: false,
- default: false,
- },
- resolvedDiscussionsExpanded: {
- type: Boolean,
- required: true,
- },
- },
- apollo: {
- activeDiscussion: {
- query: activeDiscussionQuery,
- },
- },
- data() {
- return {
- movingNoteNewPosition: null,
- movingNoteStartPosition: null,
- activeDiscussion: {},
- };
- },
- computed: {
- overlayStyle() {
- const cursor = this.disableCommenting ? 'unset' : undefined;
-
- return {
- cursor,
- width: `${this.dimensions.width}px`,
- height: `${this.dimensions.height}px`,
- ...this.position,
- };
- },
- isMovingCurrentComment() {
- return Boolean(this.movingNoteStartPosition && !this.movingNoteStartPosition.noteId);
- },
- currentCommentPositionStyle() {
- return this.isMovingCurrentComment && this.movingNoteNewPosition
- ? this.getNotePositionStyle(this.movingNoteNewPosition)
- : this.getNotePositionStyle(this.currentCommentForm);
- },
- },
- methods: {
- setNewNoteCoordinates({ x, y }) {
- this.$emit('openCommentForm', { x, y });
- },
- getNoteRelativePosition(position) {
- const { x, y, width, height } = position;
- const widthRatio = this.dimensions.width / width;
- const heightRatio = this.dimensions.height / height;
- return {
- left: Math.round(x * widthRatio),
- top: Math.round(y * heightRatio),
- };
- },
- getNotePositionStyle(position) {
- const { left, top } = this.getNoteRelativePosition(position);
- return {
- left: `${left}px`,
- top: `${top}px`,
- };
- },
- getMovingNotePositionDelta(e) {
- let deltaX = 0;
- let deltaY = 0;
-
- if (this.movingNoteStartPosition) {
- const { clientX, clientY } = this.movingNoteStartPosition;
- deltaX = e.clientX - clientX;
- deltaY = e.clientY - clientY;
- }
-
- return {
- deltaX,
- deltaY,
- };
- },
- isMovingNote(noteId) {
- const movingNoteId = this.movingNoteStartPosition?.noteId;
- return Boolean(movingNoteId && movingNoteId === noteId);
- },
- canMoveNote(note) {
- const { userPermissions } = note;
- const { adminNote } = userPermissions || {};
-
- return Boolean(adminNote);
- },
- isPositionInOverlay(position) {
- const { top, left } = this.getNoteRelativePosition(position);
- const { height, width } = this.dimensions;
-
- return top >= 0 && top <= height && left >= 0 && left <= width;
- },
- onNewNoteMove(e) {
- if (!this.isMovingCurrentComment) return;
-
- const { deltaX, deltaY } = this.getMovingNotePositionDelta(e);
- const x = this.currentCommentForm.x + deltaX;
- const y = this.currentCommentForm.y + deltaY;
-
- const movingNoteNewPosition = {
- x,
- y,
- width: this.dimensions.width,
- height: this.dimensions.height,
- };
-
- if (!this.isPositionInOverlay(movingNoteNewPosition)) {
- this.onNewNoteMouseup();
- return;
- }
-
- this.movingNoteNewPosition = movingNoteNewPosition;
- },
- onExistingNoteMove(e) {
- const note = this.notes.find(({ id }) => id === this.movingNoteStartPosition.noteId);
- if (!note || !this.canMoveNote(note)) return;
-
- const { position } = note;
- const { width, height } = position;
- const widthRatio = this.dimensions.width / width;
- const heightRatio = this.dimensions.height / height;
-
- const { deltaX, deltaY } = this.getMovingNotePositionDelta(e);
- const x = position.x * widthRatio + deltaX;
- const y = position.y * heightRatio + deltaY;
-
- const movingNoteNewPosition = {
- x,
- y,
- width: this.dimensions.width,
- height: this.dimensions.height,
- };
-
- if (!this.isPositionInOverlay(movingNoteNewPosition)) {
- this.onExistingNoteMouseup();
- return;
- }
-
- this.movingNoteNewPosition = movingNoteNewPosition;
- },
- onNewNoteMouseup() {
- if (!this.movingNoteNewPosition) return;
-
- const { x, y } = this.movingNoteNewPosition;
- this.setNewNoteCoordinates({ x, y });
- },
- onExistingNoteMouseup(note) {
- if (!this.movingNoteStartPosition || !this.movingNoteNewPosition) {
- this.updateActiveDiscussion(note.id);
- this.$emit('closeCommentForm');
- return;
- }
-
- const { x, y } = this.movingNoteNewPosition;
- this.$emit('moveNote', {
- noteId: this.movingNoteStartPosition.noteId,
- discussionId: this.movingNoteStartPosition.discussionId,
- coordinates: { x, y },
- });
- },
- onNoteMousedown({ clientX, clientY }, note) {
- this.movingNoteStartPosition = {
- noteId: note?.id,
- discussionId: note?.discussion.id,
- clientX,
- clientY,
- };
- },
- onOverlayMousemove(e) {
- if (!this.movingNoteStartPosition) return;
-
- if (this.isMovingCurrentComment) {
- this.onNewNoteMove(e);
- } else {
- this.onExistingNoteMove(e);
- }
- },
- onNoteMouseup(note) {
- if (!this.movingNoteStartPosition) return;
-
- if (this.isMovingCurrentComment) {
- this.onNewNoteMouseup();
- } else {
- this.onExistingNoteMouseup(note);
- }
-
- this.movingNoteStartPosition = null;
- this.movingNoteNewPosition = null;
- },
- onAddCommentMouseup({ offsetX, offsetY }) {
- if (this.disableCommenting) return;
- if (this.activeDiscussion.id) {
- this.updateActiveDiscussion();
- }
-
- this.setNewNoteCoordinates({ x: offsetX, y: offsetY });
- },
- updateActiveDiscussion(id) {
- this.$apollo.mutate({
- mutation: updateActiveDiscussionMutation,
- variables: {
- id,
- source: ACTIVE_DISCUSSION_SOURCE_TYPES.pin,
- },
- });
- },
- isNoteInactive(note) {
- return this.activeDiscussion.id && this.activeDiscussion.id !== note.id;
- },
- designPinClass(note) {
- return { inactive: this.isNoteInactive(note), resolved: note.resolved };
- },
- },
-};
-</script>
-
-<template>
- <div
- class="position-absolute image-diff-overlay frame"
- :style="overlayStyle"
- @mousemove="onOverlayMousemove"
- @mouseleave="onNoteMouseup"
- >
- <button
- v-show="!disableCommenting"
- type="button"
- class="btn-transparent position-absolute image-diff-overlay-add-comment w-100 h-100 js-add-image-diff-note-button"
- data-qa-selector="design_image_button"
- @mouseup="onAddCommentMouseup"
- ></button>
- <template v-for="note in notes">
- <design-note-pin
- v-if="resolvedDiscussionsExpanded || !note.resolved"
- :key="note.id"
- :label="note.index"
- :repositioning="isMovingNote(note.id)"
- :position="
- isMovingNote(note.id) && movingNoteNewPosition
- ? getNotePositionStyle(movingNoteNewPosition)
- : getNotePositionStyle(note.position)
- "
- :class="designPinClass(note)"
- @mousedown.stop="onNoteMousedown($event, note)"
- @mouseup.stop="onNoteMouseup(note)"
- />
- </template>
-
- <design-note-pin
- v-if="currentCommentForm"
- :position="currentCommentPositionStyle"
- :repositioning="isMovingCurrentComment"
- @mousedown.stop="onNoteMousedown"
- @mouseup.stop="onNoteMouseup"
- />
- </div>
-</template>
diff --git a/app/assets/javascripts/design_management_legacy/components/design_presentation.vue b/app/assets/javascripts/design_management_legacy/components/design_presentation.vue
deleted file mode 100644
index 84dbb2809d9..00000000000
--- a/app/assets/javascripts/design_management_legacy/components/design_presentation.vue
+++ /dev/null
@@ -1,322 +0,0 @@
-<script>
-import { throttle } from 'lodash';
-import DesignImage from './image.vue';
-import DesignOverlay from './design_overlay.vue';
-
-const CLICK_DRAG_BUFFER_PX = 2;
-
-export default {
- components: {
- DesignImage,
- DesignOverlay,
- },
- props: {
- image: {
- type: String,
- required: false,
- default: '',
- },
- imageName: {
- type: String,
- required: false,
- default: '',
- },
- discussions: {
- type: Array,
- required: true,
- },
- isAnnotating: {
- type: Boolean,
- required: false,
- default: false,
- },
- scale: {
- type: Number,
- required: false,
- default: 1,
- },
- resolvedDiscussionsExpanded: {
- type: Boolean,
- required: true,
- },
- },
- data() {
- return {
- overlayDimensions: null,
- overlayPosition: null,
- currentAnnotationPosition: null,
- zoomFocalPoint: {
- x: 0,
- y: 0,
- width: 0,
- height: 0,
- },
- initialLoad: true,
- lastDragPosition: null,
- isDraggingDesign: false,
- };
- },
- computed: {
- discussionStartingNotes() {
- return this.discussions.map(discussion => ({
- ...discussion.notes[0],
- index: discussion.index,
- }));
- },
- currentCommentForm() {
- return (this.isAnnotating && this.currentAnnotationPosition) || null;
- },
- presentationStyle() {
- return {
- cursor: this.isDraggingDesign ? 'grabbing' : undefined,
- };
- },
- },
- beforeDestroy() {
- const { presentationViewport } = this.$refs;
- if (!presentationViewport) return;
-
- presentationViewport.removeEventListener('scroll', this.scrollThrottled, false);
- },
- mounted() {
- const { presentationViewport } = this.$refs;
- if (!presentationViewport) return;
-
- this.scrollThrottled = throttle(() => {
- this.shiftZoomFocalPoint();
- }, 400);
-
- presentationViewport.addEventListener('scroll', this.scrollThrottled, false);
- },
- methods: {
- syncCurrentAnnotationPosition() {
- if (!this.currentAnnotationPosition) return;
-
- const widthRatio = this.overlayDimensions.width / this.currentAnnotationPosition.width;
- const heightRatio = this.overlayDimensions.height / this.currentAnnotationPosition.height;
- const x = this.currentAnnotationPosition.x * widthRatio;
- const y = this.currentAnnotationPosition.y * heightRatio;
-
- this.currentAnnotationPosition = this.getAnnotationPositon({ x, y });
- },
- setOverlayDimensions(overlayDimensions) {
- this.overlayDimensions = overlayDimensions;
-
- // every time we set overlay dimensions, we need to
- // update the current annotation as well
- this.syncCurrentAnnotationPosition();
- },
- setOverlayPosition() {
- if (!this.overlayDimensions) {
- this.overlayPosition = {};
- }
-
- const { presentationViewport } = this.$refs;
- if (!presentationViewport) return;
-
- // default to center
- this.overlayPosition = {
- left: `calc(50% - ${this.overlayDimensions.width / 2}px)`,
- top: `calc(50% - ${this.overlayDimensions.height / 2}px)`,
- };
-
- // if the overlay overflows, then don't center
- if (this.overlayDimensions.width > presentationViewport.offsetWidth) {
- this.overlayPosition.left = '0';
- }
- if (this.overlayDimensions.height > presentationViewport.offsetHeight) {
- this.overlayPosition.top = '0';
- }
- },
- /**
- * Return a point that represents the center of an
- * overflowing child element w.r.t it's parent
- */
- getViewportCenter() {
- const { presentationViewport } = this.$refs;
- if (!presentationViewport) return {};
-
- // get height of scroll bars (i.e. the max values for scrollTop, scrollLeft)
- const scrollBarWidth = presentationViewport.scrollWidth - presentationViewport.offsetWidth;
- const scrollBarHeight = presentationViewport.scrollHeight - presentationViewport.offsetHeight;
-
- // determine how many child pixels have been scrolled
- const xScrollRatio =
- presentationViewport.scrollLeft > 0 ? presentationViewport.scrollLeft / scrollBarWidth : 0;
- const yScrollRatio =
- presentationViewport.scrollTop > 0 ? presentationViewport.scrollTop / scrollBarHeight : 0;
- const xScrollOffset =
- (presentationViewport.scrollWidth - presentationViewport.offsetWidth - 0) * xScrollRatio;
- const yScrollOffset =
- (presentationViewport.scrollHeight - presentationViewport.offsetHeight - 0) * yScrollRatio;
-
- const viewportCenterX = presentationViewport.offsetWidth / 2;
- const viewportCenterY = presentationViewport.offsetHeight / 2;
- const focalPointX = viewportCenterX + xScrollOffset;
- const focalPointY = viewportCenterY + yScrollOffset;
-
- return {
- x: focalPointX,
- y: focalPointY,
- };
- },
- /**
- * Scroll the viewport such that the focal point is positioned centrally
- */
- scrollToFocalPoint() {
- const { presentationViewport } = this.$refs;
- if (!presentationViewport) return;
-
- const scrollX = this.zoomFocalPoint.x - presentationViewport.offsetWidth / 2;
- const scrollY = this.zoomFocalPoint.y - presentationViewport.offsetHeight / 2;
-
- presentationViewport.scrollTo(scrollX, scrollY);
- },
- scaleZoomFocalPoint() {
- const { x, y, width, height } = this.zoomFocalPoint;
- const widthRatio = this.overlayDimensions.width / width;
- const heightRatio = this.overlayDimensions.height / height;
-
- this.zoomFocalPoint = {
- x: Math.round(x * widthRatio * 100) / 100,
- y: Math.round(y * heightRatio * 100) / 100,
- ...this.overlayDimensions,
- };
- },
- shiftZoomFocalPoint() {
- this.zoomFocalPoint = {
- ...this.getViewportCenter(),
- ...this.overlayDimensions,
- };
- },
- onImageResize(imageDimensions) {
- this.setOverlayDimensions(imageDimensions);
- this.setOverlayPosition();
-
- this.$nextTick(() => {
- if (this.initialLoad) {
- // set focal point on initial load
- this.shiftZoomFocalPoint();
- this.initialLoad = false;
- } else {
- this.scaleZoomFocalPoint();
- this.scrollToFocalPoint();
- }
- });
- },
- getAnnotationPositon(coordinates) {
- const { x, y } = coordinates;
- const { width, height } = this.overlayDimensions;
- return {
- x: Math.round(x),
- y: Math.round(y),
- width: Math.round(width),
- height: Math.round(height),
- };
- },
- openCommentForm(coordinates) {
- this.currentAnnotationPosition = this.getAnnotationPositon(coordinates);
- this.$emit('openCommentForm', this.currentAnnotationPosition);
- },
- closeCommentForm() {
- this.currentAnnotationPosition = null;
- this.$emit('closeCommentForm');
- },
- moveNote({ noteId, discussionId, coordinates }) {
- const position = this.getAnnotationPositon(coordinates);
- this.$emit('moveNote', { noteId, discussionId, position });
- },
- onPresentationMousedown({ clientX, clientY }) {
- if (!this.isDesignOverflowing()) return;
-
- this.lastDragPosition = {
- x: clientX,
- y: clientY,
- };
- },
- getDragDelta(clientX, clientY) {
- return {
- deltaX: this.lastDragPosition.x - clientX,
- deltaY: this.lastDragPosition.y - clientY,
- };
- },
- exceedsDragThreshold(clientX, clientY) {
- const { deltaX, deltaY } = this.getDragDelta(clientX, clientY);
-
- return Math.abs(deltaX) > CLICK_DRAG_BUFFER_PX || Math.abs(deltaY) > CLICK_DRAG_BUFFER_PX;
- },
- shouldDragDesign(clientX, clientY) {
- return (
- this.lastDragPosition &&
- (this.isDraggingDesign || this.exceedsDragThreshold(clientX, clientY))
- );
- },
- onPresentationMousemove({ clientX, clientY }) {
- const { presentationViewport } = this.$refs;
- if (!presentationViewport || !this.shouldDragDesign(clientX, clientY)) return;
-
- this.isDraggingDesign = true;
-
- const { scrollLeft, scrollTop } = presentationViewport;
- const { deltaX, deltaY } = this.getDragDelta(clientX, clientY);
- presentationViewport.scrollTo(scrollLeft + deltaX, scrollTop + deltaY);
-
- this.lastDragPosition = {
- x: clientX,
- y: clientY,
- };
- },
- onPresentationMouseup() {
- this.lastDragPosition = null;
- this.isDraggingDesign = false;
- },
- isDesignOverflowing() {
- const { presentationViewport } = this.$refs;
- if (!presentationViewport) return false;
-
- return (
- presentationViewport.scrollWidth > presentationViewport.offsetWidth ||
- presentationViewport.scrollHeight > presentationViewport.offsetHeight
- );
- },
- },
-};
-</script>
-
-<template>
- <div
- ref="presentationViewport"
- class="h-100 w-100 p-3 overflow-auto position-relative"
- :style="presentationStyle"
- @mousedown="onPresentationMousedown"
- @mousemove="onPresentationMousemove"
- @mouseup="onPresentationMouseup"
- @mouseleave="onPresentationMouseup"
- @touchstart="onPresentationMousedown"
- @touchmove="onPresentationMousemove"
- @touchend="onPresentationMouseup"
- @touchcancel="onPresentationMouseup"
- >
- <div class="h-100 w-100 d-flex align-items-center position-relative">
- <design-image
- v-if="image"
- :image="image"
- :name="imageName"
- :scale="scale"
- @resize="onImageResize"
- />
- <design-overlay
- v-if="overlayDimensions && overlayPosition"
- :dimensions="overlayDimensions"
- :position="overlayPosition"
- :notes="discussionStartingNotes"
- :current-comment-form="currentCommentForm"
- :disable-commenting="isDraggingDesign"
- :resolved-discussions-expanded="resolvedDiscussionsExpanded"
- @openCommentForm="openCommentForm"
- @closeCommentForm="closeCommentForm"
- @moveNote="moveNote"
- />
- </div>
- </div>
-</template>
diff --git a/app/assets/javascripts/design_management_legacy/components/design_scaler.vue b/app/assets/javascripts/design_management_legacy/components/design_scaler.vue
deleted file mode 100644
index 55dee74bef5..00000000000
--- a/app/assets/javascripts/design_management_legacy/components/design_scaler.vue
+++ /dev/null
@@ -1,65 +0,0 @@
-<script>
-import { GlIcon } from '@gitlab/ui';
-
-const SCALE_STEP_SIZE = 0.2;
-const DEFAULT_SCALE = 1;
-const MIN_SCALE = 1;
-const MAX_SCALE = 2;
-
-export default {
- components: {
- GlIcon,
- },
- data() {
- return {
- scale: DEFAULT_SCALE,
- };
- },
- computed: {
- disableReset() {
- return this.scale <= MIN_SCALE;
- },
- disableDecrease() {
- return this.scale === DEFAULT_SCALE;
- },
- disableIncrease() {
- return this.scale >= MAX_SCALE;
- },
- },
- methods: {
- setScale(scale) {
- if (scale < MIN_SCALE) {
- return;
- }
-
- this.scale = Math.round(scale * 100) / 100;
- this.$emit('scale', this.scale);
- },
- incrementScale() {
- this.setScale(this.scale + SCALE_STEP_SIZE);
- },
- decrementScale() {
- this.setScale(this.scale - SCALE_STEP_SIZE);
- },
- resetScale() {
- this.setScale(DEFAULT_SCALE);
- },
- },
-};
-</script>
-
-<template>
- <div class="design-scaler btn-group" role="group">
- <button class="btn" :disabled="disableDecrease" @click="decrementScale">
- <span class="d-flex-center gl-icon s16">
- –
- </span>
- </button>
- <button class="btn" :disabled="disableReset" @click="resetScale">
- <gl-icon name="redo" />
- </button>
- <button class="btn" :disabled="disableIncrease" @click="incrementScale">
- <gl-icon name="plus" />
- </button>
- </div>
-</template>
diff --git a/app/assets/javascripts/design_management_legacy/components/design_sidebar.vue b/app/assets/javascripts/design_management_legacy/components/design_sidebar.vue
deleted file mode 100644
index 622120e2008..00000000000
--- a/app/assets/javascripts/design_management_legacy/components/design_sidebar.vue
+++ /dev/null
@@ -1,178 +0,0 @@
-<script>
-import Cookies from 'js-cookie';
-import { GlCollapse, GlButton, GlPopover } from '@gitlab/ui';
-import { s__ } from '~/locale';
-import { parseBoolean } from '~/lib/utils/common_utils';
-import updateActiveDiscussionMutation from '../graphql/mutations/update_active_discussion.mutation.graphql';
-import { extractDiscussions, extractParticipants } from '../utils/design_management_utils';
-import { ACTIVE_DISCUSSION_SOURCE_TYPES } from '../constants';
-import DesignDiscussion from './design_notes/design_discussion.vue';
-import Participants from '~/sidebar/components/participants/participants.vue';
-
-export default {
- components: {
- DesignDiscussion,
- Participants,
- GlCollapse,
- GlButton,
- GlPopover,
- },
- props: {
- design: {
- type: Object,
- required: true,
- },
- resolvedDiscussionsExpanded: {
- type: Boolean,
- required: true,
- },
- markdownPreviewPath: {
- type: String,
- required: true,
- },
- },
- data() {
- return {
- isResolvedCommentsPopoverHidden: parseBoolean(Cookies.get(this.$options.cookieKey)),
- discussionWithOpenForm: '',
- };
- },
- computed: {
- discussions() {
- return extractDiscussions(this.design.discussions);
- },
- issue() {
- return {
- ...this.design.issue,
- webPath: this.design.issue.webPath.substr(1),
- };
- },
- discussionParticipants() {
- return extractParticipants(this.issue.participants);
- },
- resolvedDiscussions() {
- return this.discussions.filter(discussion => discussion.resolved);
- },
- unresolvedDiscussions() {
- return this.discussions.filter(discussion => !discussion.resolved);
- },
- resolvedCommentsToggleIcon() {
- return this.resolvedDiscussionsExpanded ? 'chevron-down' : 'chevron-right';
- },
- },
- methods: {
- handleSidebarClick() {
- this.isResolvedCommentsPopoverHidden = true;
- Cookies.set(this.$options.cookieKey, 'true', { expires: 365 * 10 });
- this.updateActiveDiscussion();
- },
- updateActiveDiscussion(id) {
- this.$apollo.mutate({
- mutation: updateActiveDiscussionMutation,
- variables: {
- id,
- source: ACTIVE_DISCUSSION_SOURCE_TYPES.discussion,
- },
- });
- },
- closeCommentForm() {
- this.comment = '';
- this.$emit('closeCommentForm');
- },
- updateDiscussionWithOpenForm(id) {
- this.discussionWithOpenForm = id;
- },
- },
- resolveCommentsToggleText: s__('DesignManagement|Resolved Comments'),
- cookieKey: 'hide_design_resolved_comments_popover',
-};
-</script>
-
-<template>
- <div class="image-notes" @click="handleSidebarClick">
- <h2 class="gl-font-weight-bold gl-mt-0">
- {{ issue.title }}
- </h2>
- <a
- class="gl-text-gray-400 gl-text-decoration-none gl-mb-6 gl-display-block"
- :href="issue.webUrl"
- >{{ issue.webPath }}</a
- >
- <participants
- :participants="discussionParticipants"
- :show-participant-label="false"
- class="gl-mb-4"
- />
- <h2
- v-if="unresolvedDiscussions.length === 0"
- class="new-discussion-disclaimer gl-font-base gl-m-0 gl-mb-4"
- data-testid="new-discussion-disclaimer"
- >
- {{ s__("DesignManagement|Click the image where you'd like to start a new discussion") }}
- </h2>
- <design-discussion
- v-for="discussion in unresolvedDiscussions"
- :key="discussion.id"
- :discussion="discussion"
- :design-id="$route.params.id"
- :noteable-id="design.id"
- :markdown-preview-path="markdownPreviewPath"
- :resolved-discussions-expanded="resolvedDiscussionsExpanded"
- :discussion-with-open-form="discussionWithOpenForm"
- data-testid="unresolved-discussion"
- @createNoteError="$emit('onDesignDiscussionError', $event)"
- @updateNoteError="$emit('updateNoteError', $event)"
- @resolveDiscussionError="$emit('resolveDiscussionError', $event)"
- @click.native.stop="updateActiveDiscussion(discussion.notes[0].id)"
- @openForm="updateDiscussionWithOpenForm"
- />
- <template v-if="resolvedDiscussions.length > 0">
- <gl-button
- id="resolved-comments"
- data-testid="resolved-comments"
- :icon="resolvedCommentsToggleIcon"
- variant="link"
- class="link-inherit-color gl-text-body gl-text-decoration-none gl-font-weight-bold gl-mb-4"
- @click="$emit('toggleResolvedComments')"
- >{{ $options.resolveCommentsToggleText }} ({{ resolvedDiscussions.length }})
- </gl-button>
- <gl-popover
- v-if="!isResolvedCommentsPopoverHidden"
- :show="!isResolvedCommentsPopoverHidden"
- target="resolved-comments"
- container="popovercontainer"
- placement="top"
- :title="s__('DesignManagement|Resolved Comments')"
- >
- <p>
- {{
- s__(
- 'DesignManagement|Comments you resolve can be viewed and unresolved by going to the "Resolved Comments" section below',
- )
- }}
- </p>
- <a href="#" rel="noopener noreferrer" target="_blank">{{
- s__('DesignManagement|Learn more about resolving comments')
- }}</a>
- </gl-popover>
- <gl-collapse :visible="resolvedDiscussionsExpanded" class="gl-mt-3">
- <design-discussion
- v-for="discussion in resolvedDiscussions"
- :key="discussion.id"
- :discussion="discussion"
- :design-id="$route.params.id"
- :noteable-id="design.id"
- :markdown-preview-path="markdownPreviewPath"
- :resolved-discussions-expanded="resolvedDiscussionsExpanded"
- :discussion-with-open-form="discussionWithOpenForm"
- data-testid="resolved-discussion"
- @error="$emit('onDesignDiscussionError', $event)"
- @updateNoteError="$emit('updateNoteError', $event)"
- @openForm="updateDiscussionWithOpenForm"
- @click.native.stop="updateActiveDiscussion(discussion.notes[0].id)"
- />
- </gl-collapse>
- </template>
- <slot name="replyForm"></slot>
- </div>
-</template>
diff --git a/app/assets/javascripts/design_management_legacy/components/image.vue b/app/assets/javascripts/design_management_legacy/components/image.vue
deleted file mode 100644
index 91b7b576e0c..00000000000
--- a/app/assets/javascripts/design_management_legacy/components/image.vue
+++ /dev/null
@@ -1,110 +0,0 @@
-<script>
-import { throttle } from 'lodash';
-import { GlIcon } from '@gitlab/ui';
-
-export default {
- components: {
- GlIcon,
- },
- props: {
- image: {
- type: String,
- required: false,
- default: '',
- },
- name: {
- type: String,
- required: false,
- default: '',
- },
- scale: {
- type: Number,
- required: false,
- default: 1,
- },
- },
- data() {
- return {
- baseImageSize: null,
- imageStyle: null,
- imageError: false,
- };
- },
- watch: {
- scale(val) {
- this.zoom(val);
- },
- },
- beforeDestroy() {
- window.removeEventListener('resize', this.resizeThrottled, false);
- },
- mounted() {
- this.onImgLoad();
-
- this.resizeThrottled = throttle(() => {
- // NOTE: if imageStyle is set, then baseImageSize
- // won't change due to resize. We must still emit a
- // `resize` event so that the parent can handle
- // resizes appropriately (e.g. for design_overlay)
- this.setBaseImageSize();
- }, 400);
- window.addEventListener('resize', this.resizeThrottled, false);
- },
- methods: {
- onImgLoad() {
- requestIdleCallback(this.setBaseImageSize, { timeout: 1000 });
- },
- onImgError() {
- this.imageError = true;
- },
- setBaseImageSize() {
- const { contentImg } = this.$refs;
- if (!contentImg || contentImg.offsetHeight === 0 || contentImg.offsetWidth === 0) return;
-
- this.baseImageSize = {
- height: contentImg.offsetHeight,
- width: contentImg.offsetWidth,
- };
- this.onResize({ width: this.baseImageSize.width, height: this.baseImageSize.height });
- },
- onResize({ width, height }) {
- this.$emit('resize', { width, height });
- },
- zoom(amount) {
- if (amount === 1) {
- this.imageStyle = null;
- this.$nextTick(() => {
- this.setBaseImageSize();
- });
- return;
- }
- const width = this.baseImageSize.width * amount;
- const height = this.baseImageSize.height * amount;
-
- this.imageStyle = {
- width: `${width}px`,
- height: `${height}px`,
- };
-
- this.onResize({ width, height });
- },
- },
-};
-</script>
-
-<template>
- <div class="m-auto js-design-image">
- <gl-icon v-if="imageError" class="text-secondary-100" name="media-broken" :size="48" />
- <img
- v-show="!imageError"
- ref="contentImg"
- class="mh-100"
- :src="image"
- :alt="name"
- :style="imageStyle"
- :class="{ 'img-fluid': !imageStyle }"
- @error="onImgError"
- @load="onImgLoad"
- />
- </div>
-</template>
diff --git a/app/assets/javascripts/design_management_legacy/components/list/item.vue b/app/assets/javascripts/design_management_legacy/components/list/item.vue
deleted file mode 100644
index 13c703b8a88..00000000000
--- a/app/assets/javascripts/design_management_legacy/components/list/item.vue
+++ /dev/null
@@ -1,174 +0,0 @@
-<script>
-import { GlLoadingIcon, GlIcon, GlIntersectionObserver } from '@gitlab/ui';
-import Icon from '~/vue_shared/components/icon.vue';
-import Timeago from '~/vue_shared/components/time_ago_tooltip.vue';
-import { n__, __ } from '~/locale';
-import { DESIGN_ROUTE_NAME } from '../../router/constants';
-
-export default {
- components: {
- GlLoadingIcon,
- GlIntersectionObserver,
- GlIcon,
- Icon,
- Timeago,
- },
- props: {
- id: {
- type: [Number, String],
- required: true,
- },
- event: {
- type: String,
- required: true,
- },
- notesCount: {
- type: Number,
- required: true,
- },
- image: {
- type: String,
- required: true,
- },
- filename: {
- type: String,
- required: true,
- },
- updatedAt: {
- type: String,
- required: false,
- default: null,
- },
- isUploading: {
- type: Boolean,
- required: false,
- default: true,
- },
- imageV432x230: {
- type: String,
- required: false,
- default: null,
- },
- },
- data() {
- return {
- imageLoading: true,
- imageError: false,
- wasInView: false,
- };
- },
- computed: {
- icon() {
- const normalizedEvent = this.event.toLowerCase();
- const icons = {
- creation: {
- name: 'file-addition-solid',
- classes: 'text-success-500',
- tooltip: __('Added in this version'),
- },
- modification: {
- name: 'file-modified-solid',
- classes: 'text-primary-500',
- tooltip: __('Modified in this version'),
- },
- deletion: {
- name: 'file-deletion-solid',
- classes: 'text-danger-500',
- tooltip: __('Deleted in this version'),
- },
- };
-
- return icons[normalizedEvent] ? icons[normalizedEvent] : {};
- },
- notesLabel() {
- return n__('%d comment', '%d comments', this.notesCount);
- },
- imageLink() {
- return this.wasInView ? this.imageV432x230 || this.image : '';
- },
- showLoadingSpinner() {
- return this.imageLoading || this.isUploading;
- },
- showImageErrorIcon() {
- return this.wasInView && this.imageError;
- },
- showImage() {
- return !this.showLoadingSpinner && !this.showImageErrorIcon;
- },
- },
- methods: {
- onImageLoad() {
- this.imageLoading = false;
- this.imageError = false;
- },
- onImageError() {
- this.imageLoading = false;
- this.imageError = true;
- },
- onAppear() {
- // do nothing if image has previously
- // been in view
- if (this.wasInView) {
- return;
- }
-
- this.wasInView = true;
- this.imageLoading = true;
- },
- },
- DESIGN_ROUTE_NAME,
-};
-</script>
-
-<template>
- <router-link
- :to="{
- name: $options.DESIGN_ROUTE_NAME,
- params: { id: filename },
- query: $route.query,
- }"
- class="card cursor-pointer text-plain js-design-list-item design-list-item"
- >
- <div class="card-body p-0 d-flex-center overflow-hidden position-relative">
- <div v-if="icon.name" data-testid="designEvent" class="design-event position-absolute">
- <span :title="icon.tooltip" :aria-label="icon.tooltip">
- <icon :name="icon.name" :size="18" :class="icon.classes" />
- </span>
- </div>
- <gl-intersection-observer @appear="onAppear">
- <gl-loading-icon v-if="showLoadingSpinner" size="md" />
- <gl-icon
- v-else-if="showImageErrorIcon"
- name="media-broken"
- class="text-secondary"
- :size="32"
- />
- <img
- v-show="showImage"
- :src="imageLink"
- :alt="filename"
- class="block mx-auto mw-100 mh-100 design-img"
- data-qa-selector="design_image"
- @load="onImageLoad"
- @error="onImageError"
- />
- </gl-intersection-observer>
- </div>
- <div class="card-footer d-flex w-100">
- <div class="d-flex flex-column str-truncated-100">
- <span class="bold str-truncated-100" data-qa-selector="design_file_name">{{
- filename
- }}</span>
- <span v-if="updatedAt" class="str-truncated-100">
- {{ __('Updated') }} <timeago :time="updatedAt" tooltip-placement="bottom" />
- </span>
- </div>
- <div v-if="notesCount" class="ml-auto d-flex align-items-center text-secondary">
- <icon name="comments" class="ml-1" />
- <span :aria-label="notesLabel" class="ml-1">
- {{ notesCount }}
- </span>
- </div>
- </div>
- </router-link>
-</template>
diff --git a/app/assets/javascripts/design_management_legacy/components/toolbar/index.vue b/app/assets/javascripts/design_management_legacy/components/toolbar/index.vue
deleted file mode 100644
index b998dfc47b8..00000000000
--- a/app/assets/javascripts/design_management_legacy/components/toolbar/index.vue
+++ /dev/null
@@ -1,126 +0,0 @@
-<script>
-import { GlDeprecatedButton } from '@gitlab/ui';
-import { __, sprintf } from '~/locale';
-import Icon from '~/vue_shared/components/icon.vue';
-import timeagoMixin from '~/vue_shared/mixins/timeago';
-import Pagination from './pagination.vue';
-import DeleteButton from '../delete_button.vue';
-import permissionsQuery from '../../graphql/queries/design_permissions.query.graphql';
-import appDataQuery from '../../graphql/queries/app_data.query.graphql';
-import { DESIGNS_ROUTE_NAME } from '../../router/constants';
-
-export default {
- components: {
- Icon,
- Pagination,
- DeleteButton,
- GlDeprecatedButton,
- },
- mixins: [timeagoMixin],
- props: {
- id: {
- type: String,
- required: true,
- },
- isDeleting: {
- type: Boolean,
- required: true,
- },
- filename: {
- type: String,
- required: false,
- default: '',
- },
- updatedAt: {
- type: String,
- required: false,
- default: null,
- },
- updatedBy: {
- type: Object,
- required: false,
- default: () => ({}),
- },
- isLatestVersion: {
- type: Boolean,
- required: true,
- },
- image: {
- type: String,
- required: true,
- },
- },
- data() {
- return {
- permissions: {
- createDesign: false,
- },
- projectPath: '',
- issueIid: null,
- };
- },
- apollo: {
- appData: {
- query: appDataQuery,
- manual: true,
- result({ data: { projectPath, issueIid } }) {
- this.projectPath = projectPath;
- this.issueIid = issueIid;
- },
- },
- permissions: {
- query: permissionsQuery,
- variables() {
- return {
- fullPath: this.projectPath,
- iid: this.issueIid,
- };
- },
- update: data => data.project.issue.userPermissions,
- },
- },
- computed: {
- updatedText() {
- return sprintf(__('Updated %{updated_at} by %{updated_by}'), {
- updated_at: this.timeFormatted(this.updatedAt),
- updated_by: this.updatedBy.name,
- });
- },
- canDeleteDesign() {
- return this.permissions.createDesign;
- },
- },
- DESIGNS_ROUTE_NAME,
-};
-</script>
-
-<template>
- <header class="d-flex p-2 bg-white align-items-center js-design-header">
- <router-link
- :to="{
- name: $options.DESIGNS_ROUTE_NAME,
- query: $route.query,
- }"
- :aria-label="s__('DesignManagement|Go back to designs')"
- class="mr-3 text-plain d-flex justify-content-center align-items-center"
- >
- <icon :size="18" name="close" />
- </router-link>
- <div class="overflow-hidden d-flex align-items-center">
- <h2 class="m-0 str-truncated-100 gl-font-base">{{ filename }}</h2>
- <small v-if="updatedAt" class="text-secondary">{{ updatedText }}</small>
- </div>
- <pagination :id="id" class="ml-auto flex-shrink-0" />
- <gl-deprecated-button :href="image" class="mr-2">
- <icon :size="18" name="download" />
- </gl-deprecated-button>
- <delete-button
- v-if="isLatestVersion && canDeleteDesign"
- :is-deleting="isDeleting"
- button-variant="danger"
- @deleteSelectedDesigns="$emit('delete')"
- >
- <icon :size="18" name="remove" />
- </delete-button>
- </header>
-</template>
diff --git a/app/assets/javascripts/design_management_legacy/components/toolbar/pagination.vue b/app/assets/javascripts/design_management_legacy/components/toolbar/pagination.vue
deleted file mode 100644
index bf62a8f66a6..00000000000
--- a/app/assets/javascripts/design_management_legacy/components/toolbar/pagination.vue
+++ /dev/null
@@ -1,83 +0,0 @@
-<script>
-/* global Mousetrap */
-import 'mousetrap';
-import { s__, sprintf } from '~/locale';
-import PaginationButton from './pagination_button.vue';
-import allDesignsMixin from '../../mixins/all_designs';
-import { DESIGN_ROUTE_NAME } from '../../router/constants';
-
-export default {
- components: {
- PaginationButton,
- },
- mixins: [allDesignsMixin],
- props: {
- id: {
- type: String,
- required: true,
- },
- },
- computed: {
- designsCount() {
- return this.designs.length;
- },
- currentIndex() {
- return this.designs.findIndex(design => design.filename === this.id);
- },
- paginationText() {
- return sprintf(s__('DesignManagement|%{current_design} of %{designs_count}'), {
- current_design: this.currentIndex + 1,
- designs_count: this.designsCount,
- });
- },
- previousDesign() {
- if (!this.designsCount) return null;
-
- return this.designs[this.currentIndex - 1];
- },
- nextDesign() {
- if (!this.designsCount) return null;
-
- return this.designs[this.currentIndex + 1];
- },
- },
- mounted() {
- Mousetrap.bind('left', () => this.navigateToDesign(this.previousDesign));
- Mousetrap.bind('right', () => this.navigateToDesign(this.nextDesign));
- },
- beforeDestroy() {
- Mousetrap.unbind(['left', 'right'], this.navigateToDesign);
- },
- methods: {
- navigateToDesign(design) {
- if (design) {
- this.$router.push({
- name: DESIGN_ROUTE_NAME,
- params: { id: design.filename },
- query: this.$route.query,
- });
- }
- },
- },
-};
-</script>
-
-<template>
- <div v-if="designsCount" class="d-flex align-items-center">
- {{ paginationText }}
- <div class="btn-group ml-3 mr-3">
- <pagination-button
- :design="previousDesign"
- :title="s__('DesignManagement|Go to previous design')"
- icon-name="angle-left"
- class="js-previous-design"
- />
- <pagination-button
- :design="nextDesign"
- :title="s__('DesignManagement|Go to next design')"
- icon-name="angle-right"
- class="js-next-design"
- />
- </div>
- </div>
-</template>
diff --git a/app/assets/javascripts/design_management_legacy/components/toolbar/pagination_button.vue b/app/assets/javascripts/design_management_legacy/components/toolbar/pagination_button.vue
deleted file mode 100644
index f00ecefca01..00000000000
--- a/app/assets/javascripts/design_management_legacy/components/toolbar/pagination_button.vue
+++ /dev/null
@@ -1,48 +0,0 @@
-<script>
-import Icon from '~/vue_shared/components/icon.vue';
-import { DESIGN_ROUTE_NAME } from '../../router/constants';
-
-export default {
- components: {
- Icon,
- },
- props: {
- design: {
- type: Object,
- required: false,
- default: null,
- },
- title: {
- type: String,
- required: true,
- },
- iconName: {
- type: String,
- required: true,
- },
- },
- computed: {
- designLink() {
- if (!this.design) return {};
-
- return {
- name: DESIGN_ROUTE_NAME,
- params: { id: this.design.filename },
- query: this.$route.query,
- };
- },
- },
-};
-</script>
-
-<template>
- <router-link
- :to="designLink"
- :disabled="!design"
- :class="{ disabled: !design }"
- :aria-label="title"
- class="btn btn-default"
- >
- <icon :name="iconName" />
- </router-link>
-</template>
diff --git a/app/assets/javascripts/design_management_legacy/components/upload/button.vue b/app/assets/javascripts/design_management_legacy/components/upload/button.vue
deleted file mode 100644
index 68555104a3c..00000000000
--- a/app/assets/javascripts/design_management_legacy/components/upload/button.vue
+++ /dev/null
@@ -1,58 +0,0 @@
-<script>
-import { GlDeprecatedButton, GlLoadingIcon, GlTooltipDirective } from '@gitlab/ui';
-import { VALID_DESIGN_FILE_MIMETYPE } from '../../constants';
-
-export default {
- components: {
- GlDeprecatedButton,
- GlLoadingIcon,
- },
- directives: {
- GlTooltip: GlTooltipDirective,
- },
- props: {
- isSaving: {
- type: Boolean,
- required: true,
- },
- },
- methods: {
- openFileUpload() {
- this.$refs.fileUpload.click();
- },
- onFileUploadChange(e) {
- this.$emit('upload', e.target.files);
- },
- },
- VALID_DESIGN_FILE_MIMETYPE,
-};
-</script>
-
-<template>
- <div>
- <gl-deprecated-button
- v-gl-tooltip.hover
- :title="
- s__(
- 'DesignManagement|Adding a design with the same filename replaces the file in a new version.',
- )
- "
- :disabled="isSaving"
- variant="success"
- @click="openFileUpload"
- >
- {{ s__('DesignManagement|Upload designs') }}
- <gl-loading-icon v-if="isSaving" inline class="ml-1" />
- </gl-deprecated-button>
-
- <input
- ref="fileUpload"
- type="file"
- name="design_file"
- :accept="$options.VALID_DESIGN_FILE_MIMETYPE.mimetype"
- class="hide"
- multiple
- @change="onFileUploadChange"
- />
- </div>
-</template>
diff --git a/app/assets/javascripts/design_management_legacy/components/upload/design_dropzone.vue b/app/assets/javascripts/design_management_legacy/components/upload/design_dropzone.vue
deleted file mode 100644
index e435c84c959..00000000000
--- a/app/assets/javascripts/design_management_legacy/components/upload/design_dropzone.vue
+++ /dev/null
@@ -1,134 +0,0 @@
-<script>
-import { GlIcon, GlLink, GlSprintf } from '@gitlab/ui';
-import { deprecatedCreateFlash as createFlash } from '~/flash';
-import uploadDesignMutation from '../../graphql/mutations/upload_design.mutation.graphql';
-import { UPLOAD_DESIGN_INVALID_FILETYPE_ERROR } from '../../utils/error_messages';
-import { isValidDesignFile } from '../../utils/design_management_utils';
-import { VALID_DATA_TRANSFER_TYPE, VALID_DESIGN_FILE_MIMETYPE } from '../../constants';
-
-export default {
- components: {
- GlIcon,
- GlLink,
- GlSprintf,
- },
- data() {
- return {
- dragCounter: 0,
- isDragDataValid: false,
- };
- },
- computed: {
- dragging() {
- return this.dragCounter !== 0;
- },
- },
- methods: {
- isValidUpload(files) {
- return files.every(isValidDesignFile);
- },
- isValidDragDataType({ dataTransfer }) {
- return Boolean(dataTransfer && dataTransfer.types.some(t => t === VALID_DATA_TRANSFER_TYPE));
- },
- ondrop({ dataTransfer = {} }) {
- this.dragCounter = 0;
- // User already had feedback when dropzone was active, so bail here
- if (!this.isDragDataValid) {
- return;
- }
-
- const { files } = dataTransfer;
- if (!this.isValidUpload(Array.from(files))) {
- createFlash(UPLOAD_DESIGN_INVALID_FILETYPE_ERROR);
- return;
- }
-
- this.$emit('change', files);
- },
- ondragenter(e) {
- this.dragCounter += 1;
- this.isDragDataValid = this.isValidDragDataType(e);
- },
- ondragleave() {
- this.dragCounter -= 1;
- },
- openFileUpload() {
- this.$refs.fileUpload.click();
- },
- onDesignInputChange(e) {
- this.$emit('change', e.target.files);
- },
- },
- uploadDesignMutation,
- VALID_DESIGN_FILE_MIMETYPE,
-};
-</script>
-
-<template>
- <div
- class="w-100 position-relative"
- @dragstart.prevent.stop
- @dragend.prevent.stop
- @dragover.prevent.stop
- @dragenter.prevent.stop="ondragenter"
- @dragleave.prevent.stop="ondragleave"
- @drop.prevent.stop="ondrop"
- >
- <slot>
- <button
- class="card design-dropzone-card design-dropzone-border w-100 h-100 d-flex-center p-3"
- @click="openFileUpload"
- >
- <div class="d-flex-center flex-column text-center">
- <gl-icon name="doc-new" :size="48" class="mb-4" />
- <p>
- <gl-sprintf
- :message="
- __(
- '%{lineOneStart}Drag and drop to upload your designs%{lineOneEnd} or %{linkStart}click to upload%{linkEnd}.',
- )
- "
- >
- <template #lineOne="{ content }"
- ><span class="d-block">{{ content }}</span>
- </template>
-
- <template #link="{ content }">
- <gl-link class="h-100 w-100" @click.stop="openFileUpload">{{ content }}</gl-link>
- </template>
- </gl-sprintf>
- </p>
- </div>
- </button>
-
- <input
- ref="fileUpload"
- type="file"
- name="design_file"
- :accept="$options.VALID_DESIGN_FILE_MIMETYPE.mimetype"
- class="hide"
- multiple
- @change="onDesignInputChange"
- />
- </slot>
- <transition name="design-dropzone-fade">
- <div
- v-show="dragging"
- class="card design-dropzone-border design-dropzone-overlay w-100 h-100 position-absolute d-flex-center p-3 bg-white"
- >
- <div v-show="!isDragDataValid" class="mw-50 text-center">
- <h3>{{ __('Oh no!') }}</h3>
- <span>{{
- __(
- 'You are trying to upload something other than an image. Please upload a .png, .jpg, .jpeg, .gif, .bmp, .tiff or .ico.',
- )
- }}</span>
- </div>
- <div v-show="isDragDataValid" class="mw-50 text-center">
- <h3>{{ __('Incoming!') }}</h3>
- <span>{{ __('Drop your designs to start your upload.') }}</span>
- </div>
- </div>
- </transition>
- </div>
-</template>
diff --git a/app/assets/javascripts/design_management_legacy/components/upload/design_version_dropdown.vue b/app/assets/javascripts/design_management_legacy/components/upload/design_version_dropdown.vue
deleted file mode 100644
index 879d2523848..00000000000
--- a/app/assets/javascripts/design_management_legacy/components/upload/design_version_dropdown.vue
+++ /dev/null
@@ -1,76 +0,0 @@
-<script>
-import { GlDeprecatedDropdown, GlDeprecatedDropdownItem } from '@gitlab/ui';
-import { __, sprintf } from '~/locale';
-import allVersionsMixin from '../../mixins/all_versions';
-import { findVersionId } from '../../utils/design_management_utils';
-
-export default {
- components: {
- GlDeprecatedDropdown,
- GlDeprecatedDropdownItem,
- },
- mixins: [allVersionsMixin],
- computed: {
- queryVersion() {
- return this.$route.query.version;
- },
- currentVersionIdx() {
- if (!this.queryVersion) return 0;
-
- const idx = this.allVersions.findIndex(
- version => this.findVersionId(version.node.id) === this.queryVersion,
- );
-
- // if the currentVersionId isn't a valid version (i.e. not in allVersions)
- // then return the latest version (index 0)
- return idx !== -1 ? idx : 0;
- },
- currentVersionId() {
- if (this.queryVersion) return this.queryVersion;
-
- const currentVersion = this.allVersions[this.currentVersionIdx];
- return this.findVersionId(currentVersion.node.id);
- },
- dropdownText() {
- if (this.isLatestVersion) {
- return __('Showing Latest Version');
- }
- // allVersions is sorted in reverse chronological order (latest first)
- const currentVersionNumber = this.allVersions.length - this.currentVersionIdx;
-
- return sprintf(__('Showing Version #%{versionNumber}'), {
- versionNumber: currentVersionNumber,
- });
- },
- },
- methods: {
- findVersionId,
- },
-};
-</script>
-
-<template>
- <gl-deprecated-dropdown :text="dropdownText" variant="link" class="design-version-dropdown">
- <gl-deprecated-dropdown-item v-for="(version, index) in allVersions" :key="version.node.id">
- <router-link
- class="d-flex js-version-link"
- :to="{ path: $route.path, query: { version: findVersionId(version.node.id) } }"
- >
- <div class="flex-grow-1 ml-2">
- <div>
- <strong
- >{{ __('Version') }} {{ allVersions.length - index }}
- <span v-if="findVersionId(version.node.id) === latestVersionId"
- >({{ __('latest') }})</span
- >
- </strong>
- </div>
- </div>
- <i
- v-if="findVersionId(version.node.id) === currentVersionId"
- class="fa fa-check float-right gl-mr-2"
- ></i>
- </router-link>
- </gl-deprecated-dropdown-item>
- </gl-deprecated-dropdown>
-</template>
diff --git a/app/assets/javascripts/design_management_legacy/constants.js b/app/assets/javascripts/design_management_legacy/constants.js
deleted file mode 100644
index 21ff361a277..00000000000
--- a/app/assets/javascripts/design_management_legacy/constants.js
+++ /dev/null
@@ -1,16 +0,0 @@
-// WARNING: replace this with something
-// more sensical as per https://gitlab.com/gitlab-org/gitlab/issues/118611
-export const VALID_DESIGN_FILE_MIMETYPE = {
- mimetype: 'image/*',
- regex: /image\/.+/,
-};
-
-// https://developer.mozilla.org/en-US/docs/Web/API/DataTransfer/types
-export const VALID_DATA_TRANSFER_TYPE = 'Files';
-
-export const ACTIVE_DISCUSSION_SOURCE_TYPES = {
- pin: 'pin',
- discussion: 'discussion',
-};
-
-export const DESIGN_DETAIL_LAYOUT_CLASSLIST = ['design-detail-layout', 'overflow-hidden', 'm-0'];
diff --git a/app/assets/javascripts/design_management_legacy/graphql.js b/app/assets/javascripts/design_management_legacy/graphql.js
deleted file mode 100644
index fae337aa75b..00000000000
--- a/app/assets/javascripts/design_management_legacy/graphql.js
+++ /dev/null
@@ -1,45 +0,0 @@
-import Vue from 'vue';
-import VueApollo from 'vue-apollo';
-import { uniqueId } from 'lodash';
-import { defaultDataIdFromObject } from 'apollo-cache-inmemory';
-import createDefaultClient from '~/lib/graphql';
-import activeDiscussionQuery from './graphql/queries/active_discussion.query.graphql';
-import typeDefs from './graphql/typedefs.graphql';
-
-Vue.use(VueApollo);
-
-const resolvers = {
- Mutation: {
- updateActiveDiscussion: (_, { id = null, source }, { cache }) => {
- const data = cache.readQuery({ query: activeDiscussionQuery });
- data.activeDiscussion = {
- __typename: 'ActiveDiscussion',
- id,
- source,
- };
- cache.writeQuery({ query: activeDiscussionQuery, data });
- },
- },
-};
-
-const defaultClient = createDefaultClient(
- resolvers,
- // This config is added temporarily to resolve an issue with duplicate design IDs.
- // Should be removed as soon as https://gitlab.com/gitlab-org/gitlab/issues/13495 is resolved
- {
- cacheConfig: {
- dataIdFromObject: object => {
- // eslint-disable-next-line no-underscore-dangle, @gitlab/require-i18n-strings
- if (object.__typename === 'Design') {
- return object.id && object.image ? `${object.id}-${object.image}` : uniqueId();
- }
- return defaultDataIdFromObject(object);
- },
- },
- typeDefs,
- },
-);
-
-export default new VueApollo({
- defaultClient,
-});
diff --git a/app/assets/javascripts/design_management_legacy/graphql/fragments/design.fragment.graphql b/app/assets/javascripts/design_management_legacy/graphql/fragments/design.fragment.graphql
deleted file mode 100644
index 4b1703e41c3..00000000000
--- a/app/assets/javascripts/design_management_legacy/graphql/fragments/design.fragment.graphql
+++ /dev/null
@@ -1,24 +0,0 @@
-#import "./design_note.fragment.graphql"
-#import "./design_list.fragment.graphql"
-#import "./diff_refs.fragment.graphql"
-#import "./discussion_resolved_status.fragment.graphql"
-
-fragment DesignItem on Design {
- ...DesignListItem
- fullPath
- diffRefs {
- ...DesignDiffRefs
- }
- discussions {
- nodes {
- id
- replyId
- ...ResolvedStatus
- notes {
- nodes {
- ...DesignNote
- }
- }
- }
- }
-}
diff --git a/app/assets/javascripts/design_management_legacy/graphql/fragments/design_list.fragment.graphql b/app/assets/javascripts/design_management_legacy/graphql/fragments/design_list.fragment.graphql
deleted file mode 100644
index bc3132f9b42..00000000000
--- a/app/assets/javascripts/design_management_legacy/graphql/fragments/design_list.fragment.graphql
+++ /dev/null
@@ -1,8 +0,0 @@
-fragment DesignListItem on Design {
- id
- event
- filename
- notesCount
- image
- imageV432x230
-}
diff --git a/app/assets/javascripts/design_management_legacy/graphql/fragments/design_note.fragment.graphql b/app/assets/javascripts/design_management_legacy/graphql/fragments/design_note.fragment.graphql
deleted file mode 100644
index 26edd2c0be1..00000000000
--- a/app/assets/javascripts/design_management_legacy/graphql/fragments/design_note.fragment.graphql
+++ /dev/null
@@ -1,29 +0,0 @@
-#import "./diff_refs.fragment.graphql"
-#import "~/graphql_shared/fragments/author.fragment.graphql"
-#import "./note_permissions.fragment.graphql"
-
-fragment DesignNote on Note {
- id
- author {
- ...Author
- }
- body
- bodyHtml
- createdAt
- resolved
- position {
- diffRefs {
- ...DesignDiffRefs
- }
- x
- y
- height
- width
- }
- userPermissions {
- ...DesignNotePermissions
- }
- discussion {
- id
- }
-}
diff --git a/app/assets/javascripts/design_management_legacy/graphql/fragments/diff_refs.fragment.graphql b/app/assets/javascripts/design_management_legacy/graphql/fragments/diff_refs.fragment.graphql
deleted file mode 100644
index 984a55814b0..00000000000
--- a/app/assets/javascripts/design_management_legacy/graphql/fragments/diff_refs.fragment.graphql
+++ /dev/null
@@ -1,5 +0,0 @@
-fragment DesignDiffRefs on DiffRefs {
- baseSha
- startSha
- headSha
-}
diff --git a/app/assets/javascripts/design_management_legacy/graphql/fragments/discussion_resolved_status.fragment.graphql b/app/assets/javascripts/design_management_legacy/graphql/fragments/discussion_resolved_status.fragment.graphql
deleted file mode 100644
index 7483b508721..00000000000
--- a/app/assets/javascripts/design_management_legacy/graphql/fragments/discussion_resolved_status.fragment.graphql
+++ /dev/null
@@ -1,9 +0,0 @@
-fragment ResolvedStatus on Discussion {
- resolvable
- resolved
- resolvedAt
- resolvedBy {
- name
- webUrl
- }
-}
diff --git a/app/assets/javascripts/design_management_legacy/graphql/fragments/note_permissions.fragment.graphql b/app/assets/javascripts/design_management_legacy/graphql/fragments/note_permissions.fragment.graphql
deleted file mode 100644
index c243e39f3d3..00000000000
--- a/app/assets/javascripts/design_management_legacy/graphql/fragments/note_permissions.fragment.graphql
+++ /dev/null
@@ -1,3 +0,0 @@
-fragment DesignNotePermissions on NotePermissions {
- adminNote
-}
diff --git a/app/assets/javascripts/design_management_legacy/graphql/fragments/version.fragment.graphql b/app/assets/javascripts/design_management_legacy/graphql/fragments/version.fragment.graphql
deleted file mode 100644
index 7eb40b12f51..00000000000
--- a/app/assets/javascripts/design_management_legacy/graphql/fragments/version.fragment.graphql
+++ /dev/null
@@ -1,4 +0,0 @@
-fragment VersionListItem on DesignVersion {
- id
- sha
-}
diff --git a/app/assets/javascripts/design_management_legacy/graphql/mutations/create_image_diff_note.mutation.graphql b/app/assets/javascripts/design_management_legacy/graphql/mutations/create_image_diff_note.mutation.graphql
deleted file mode 100644
index c8ade328120..00000000000
--- a/app/assets/javascripts/design_management_legacy/graphql/mutations/create_image_diff_note.mutation.graphql
+++ /dev/null
@@ -1,21 +0,0 @@
-#import "../fragments/design_note.fragment.graphql"
-
-mutation createImageDiffNote($input: CreateImageDiffNoteInput!) {
- createImageDiffNote(input: $input) {
- note {
- ...DesignNote
- discussion {
- id
- replyId
- notes {
- edges {
- node {
- ...DesignNote
- }
- }
- }
- }
- }
- errors
- }
-}
diff --git a/app/assets/javascripts/design_management_legacy/graphql/mutations/create_note.mutation.graphql b/app/assets/javascripts/design_management_legacy/graphql/mutations/create_note.mutation.graphql
deleted file mode 100644
index 184ee6955dc..00000000000
--- a/app/assets/javascripts/design_management_legacy/graphql/mutations/create_note.mutation.graphql
+++ /dev/null
@@ -1,10 +0,0 @@
-#import "../fragments/design_note.fragment.graphql"
-
-mutation createNote($input: CreateNoteInput!) {
- createNote(input: $input) {
- note {
- ...DesignNote
- }
- errors
- }
-}
diff --git a/app/assets/javascripts/design_management_legacy/graphql/mutations/destroy_design.mutation.graphql b/app/assets/javascripts/design_management_legacy/graphql/mutations/destroy_design.mutation.graphql
deleted file mode 100644
index 0b3cf636cdb..00000000000
--- a/app/assets/javascripts/design_management_legacy/graphql/mutations/destroy_design.mutation.graphql
+++ /dev/null
@@ -1,10 +0,0 @@
-#import "../fragments/version.fragment.graphql"
-
-mutation destroyDesign($filenames: [String!]!, $projectPath: ID!, $iid: ID!) {
- designManagementDelete(input: { projectPath: $projectPath, iid: $iid, filenames: $filenames }) {
- version {
- ...VersionListItem
- }
- errors
- }
-}
diff --git a/app/assets/javascripts/design_management_legacy/graphql/mutations/toggle_resolve_discussion.mutation.graphql b/app/assets/javascripts/design_management_legacy/graphql/mutations/toggle_resolve_discussion.mutation.graphql
deleted file mode 100644
index 1157fc05d5f..00000000000
--- a/app/assets/javascripts/design_management_legacy/graphql/mutations/toggle_resolve_discussion.mutation.graphql
+++ /dev/null
@@ -1,17 +0,0 @@
-#import "../fragments/design_note.fragment.graphql"
-#import "../fragments/discussion_resolved_status.fragment.graphql"
-
-mutation toggleResolveDiscussion($id: ID!, $resolve: Boolean!) {
- discussionToggleResolve(input: { id: $id, resolve: $resolve }) {
- discussion {
- id
- ...ResolvedStatus
- notes {
- nodes {
- ...DesignNote
- }
- }
- }
- errors
- }
-}
diff --git a/app/assets/javascripts/design_management_legacy/graphql/mutations/update_active_discussion.mutation.graphql b/app/assets/javascripts/design_management_legacy/graphql/mutations/update_active_discussion.mutation.graphql
deleted file mode 100644
index a24b6737159..00000000000
--- a/app/assets/javascripts/design_management_legacy/graphql/mutations/update_active_discussion.mutation.graphql
+++ /dev/null
@@ -1,3 +0,0 @@
-mutation updateActiveDiscussion($id: String, $source: String) {
- updateActiveDiscussion(id: $id, source: $source) @client
-}
diff --git a/app/assets/javascripts/design_management_legacy/graphql/mutations/update_image_diff_note.mutation.graphql b/app/assets/javascripts/design_management_legacy/graphql/mutations/update_image_diff_note.mutation.graphql
deleted file mode 100644
index 5562ca9d89f..00000000000
--- a/app/assets/javascripts/design_management_legacy/graphql/mutations/update_image_diff_note.mutation.graphql
+++ /dev/null
@@ -1,10 +0,0 @@
-#import "../fragments/design_note.fragment.graphql"
-
-mutation updateImageDiffNote($input: UpdateImageDiffNoteInput!) {
- updateImageDiffNote(input: $input) {
- errors
- note {
- ...DesignNote
- }
- }
-}
diff --git a/app/assets/javascripts/design_management_legacy/graphql/mutations/update_note.mutation.graphql b/app/assets/javascripts/design_management_legacy/graphql/mutations/update_note.mutation.graphql
deleted file mode 100644
index b995e99fb6a..00000000000
--- a/app/assets/javascripts/design_management_legacy/graphql/mutations/update_note.mutation.graphql
+++ /dev/null
@@ -1,10 +0,0 @@
-#import "../fragments/design_note.fragment.graphql"
-
-mutation updateNote($input: UpdateNoteInput!) {
- updateNote(input: $input) {
- note {
- ...DesignNote
- }
- errors
- }
-}
diff --git a/app/assets/javascripts/design_management_legacy/graphql/mutations/upload_design.mutation.graphql b/app/assets/javascripts/design_management_legacy/graphql/mutations/upload_design.mutation.graphql
deleted file mode 100644
index d694e6558a0..00000000000
--- a/app/assets/javascripts/design_management_legacy/graphql/mutations/upload_design.mutation.graphql
+++ /dev/null
@@ -1,21 +0,0 @@
-#import "../fragments/design.fragment.graphql"
-
-mutation uploadDesign($files: [Upload!]!, $projectPath: ID!, $iid: ID!) {
- designManagementUpload(input: { projectPath: $projectPath, iid: $iid, files: $files }) {
- designs {
- ...DesignItem
- versions {
- edges {
- node {
- id
- sha
- }
- }
- }
- }
- skippedDesigns {
- filename
- }
- errors
- }
-}
diff --git a/app/assets/javascripts/design_management_legacy/graphql/queries/active_discussion.query.graphql b/app/assets/javascripts/design_management_legacy/graphql/queries/active_discussion.query.graphql
deleted file mode 100644
index 111023cea68..00000000000
--- a/app/assets/javascripts/design_management_legacy/graphql/queries/active_discussion.query.graphql
+++ /dev/null
@@ -1,6 +0,0 @@
-query activeDiscussion {
- activeDiscussion @client {
- id
- source
- }
-}
diff --git a/app/assets/javascripts/design_management_legacy/graphql/queries/app_data.query.graphql b/app/assets/javascripts/design_management_legacy/graphql/queries/app_data.query.graphql
deleted file mode 100644
index e1269761206..00000000000
--- a/app/assets/javascripts/design_management_legacy/graphql/queries/app_data.query.graphql
+++ /dev/null
@@ -1,4 +0,0 @@
-query projectFullPath {
- projectPath @client
- issueIid @client
-}
diff --git a/app/assets/javascripts/design_management_legacy/graphql/queries/design_permissions.query.graphql b/app/assets/javascripts/design_management_legacy/graphql/queries/design_permissions.query.graphql
deleted file mode 100644
index a87b256dc95..00000000000
--- a/app/assets/javascripts/design_management_legacy/graphql/queries/design_permissions.query.graphql
+++ /dev/null
@@ -1,10 +0,0 @@
-query permissions($fullPath: ID!, $iid: String!) {
- project(fullPath: $fullPath) {
- id
- issue(iid: $iid) {
- userPermissions {
- createDesign
- }
- }
- }
-}
diff --git a/app/assets/javascripts/design_management_legacy/graphql/queries/get_design.query.graphql b/app/assets/javascripts/design_management_legacy/graphql/queries/get_design.query.graphql
deleted file mode 100644
index 07a9af55787..00000000000
--- a/app/assets/javascripts/design_management_legacy/graphql/queries/get_design.query.graphql
+++ /dev/null
@@ -1,31 +0,0 @@
-#import "../fragments/design.fragment.graphql"
-#import "~/graphql_shared/fragments/author.fragment.graphql"
-
-query getDesign($fullPath: ID!, $iid: String!, $atVersion: ID, $filenames: [String!]) {
- project(fullPath: $fullPath) {
- id
- issue(iid: $iid) {
- designCollection {
- designs(atVersion: $atVersion, filenames: $filenames) {
- edges {
- node {
- ...DesignItem
- issue {
- title
- webPath
- webUrl
- participants {
- edges {
- node {
- ...Author
- }
- }
- }
- }
- }
- }
- }
- }
- }
- }
-}
diff --git a/app/assets/javascripts/design_management_legacy/graphql/queries/get_design_list.query.graphql b/app/assets/javascripts/design_management_legacy/graphql/queries/get_design_list.query.graphql
deleted file mode 100644
index 121a50555b3..00000000000
--- a/app/assets/javascripts/design_management_legacy/graphql/queries/get_design_list.query.graphql
+++ /dev/null
@@ -1,26 +0,0 @@
-#import "../fragments/design_list.fragment.graphql"
-#import "../fragments/version.fragment.graphql"
-
-query getDesignList($fullPath: ID!, $iid: String!, $atVersion: ID) {
- project(fullPath: $fullPath) {
- id
- issue(iid: $iid) {
- designCollection {
- designs(atVersion: $atVersion) {
- edges {
- node {
- ...DesignListItem
- }
- }
- }
- versions {
- edges {
- node {
- ...VersionListItem
- }
- }
- }
- }
- }
- }
-}
diff --git a/app/assets/javascripts/design_management_legacy/graphql/typedefs.graphql b/app/assets/javascripts/design_management_legacy/graphql/typedefs.graphql
deleted file mode 100644
index fdbad4a90e0..00000000000
--- a/app/assets/javascripts/design_management_legacy/graphql/typedefs.graphql
+++ /dev/null
@@ -1,12 +0,0 @@
-type ActiveDiscussion {
- id: ID
- source: String
-}
-
-extend type Query {
- activeDiscussion: ActiveDiscussion
-}
-
-extend type Mutation {
- updateActiveDiscussion(id: ID!, source: String!): Boolean
-}
diff --git a/app/assets/javascripts/design_management_legacy/index.js b/app/assets/javascripts/design_management_legacy/index.js
deleted file mode 100644
index 1fc5779515a..00000000000
--- a/app/assets/javascripts/design_management_legacy/index.js
+++ /dev/null
@@ -1,61 +0,0 @@
-// This application is being moved, please do not touch this files
-// Please see https://gitlab.com/gitlab-org/gitlab/-/issues/14744#note_364468096 for details
-
-import $ from 'jquery';
-import Vue from 'vue';
-import createRouter from './router';
-import App from './components/app.vue';
-import apolloProvider from './graphql';
-import getDesignListQuery from './graphql/queries/get_design_list.query.graphql';
-import { DESIGNS_ROUTE_NAME, ROOT_ROUTE_NAME } from './router/constants';
-
-export default () => {
- const el = document.querySelector('.js-design-management');
- const badge = document.querySelector('.js-designs-count');
- const { issueIid, projectPath, issuePath } = el.dataset;
- const router = createRouter(issuePath);
-
- $('.js-issue-tabs').on('shown.bs.tab', ({ target: { id } }) => {
- if (id === 'designs' && router.currentRoute.name === ROOT_ROUTE_NAME) {
- router.push({ name: DESIGNS_ROUTE_NAME });
- } else if (id === 'discussion') {
- router.push({ name: ROOT_ROUTE_NAME });
- }
- });
-
- apolloProvider.clients.defaultClient.cache.writeData({
- data: {
- projectPath,
- issueIid,
- activeDiscussion: {
- __typename: 'ActiveDiscussion',
- id: null,
- source: null,
- },
- },
- });
-
- apolloProvider.clients.defaultClient
- .watchQuery({
- query: getDesignListQuery,
- variables: {
- fullPath: projectPath,
- iid: issueIid,
- atVersion: null,
- },
- })
- .subscribe(({ data }) => {
- if (badge) {
- badge.textContent = data.project.issue.designCollection.designs.edges.length;
- }
- });
-
- return new Vue({
- el,
- router,
- apolloProvider,
- render(createElement) {
- return createElement(App);
- },
- });
-};
diff --git a/app/assets/javascripts/design_management_legacy/mixins/all_designs.js b/app/assets/javascripts/design_management_legacy/mixins/all_designs.js
deleted file mode 100644
index 544429928d2..00000000000
--- a/app/assets/javascripts/design_management_legacy/mixins/all_designs.js
+++ /dev/null
@@ -1,49 +0,0 @@
-import { propertyOf } from 'lodash';
-import { deprecatedCreateFlash as createFlash } from '~/flash';
-import { s__ } from '~/locale';
-import getDesignListQuery from '../graphql/queries/get_design_list.query.graphql';
-import { extractNodes } from '../utils/design_management_utils';
-import allVersionsMixin from './all_versions';
-import { DESIGNS_ROUTE_NAME } from '../router/constants';
-
-export default {
- mixins: [allVersionsMixin],
- apollo: {
- designs: {
- query: getDesignListQuery,
- variables() {
- return {
- fullPath: this.projectPath,
- iid: this.issueIid,
- atVersion: this.designsVersion,
- };
- },
- update: data => {
- const designEdges = propertyOf(data)(['project', 'issue', 'designCollection', 'designs']);
- if (designEdges) {
- return extractNodes(designEdges);
- }
- return [];
- },
- error() {
- this.error = true;
- },
- result() {
- if (this.$route.query.version && !this.hasValidVersion) {
- createFlash(
- s__(
- 'DesignManagement|Requested design version does not exist. Showing latest version instead',
- ),
- );
- this.$router.replace({ name: DESIGNS_ROUTE_NAME, query: { version: undefined } });
- }
- },
- },
- },
- data() {
- return {
- designs: [],
- error: false,
- };
- },
-};
diff --git a/app/assets/javascripts/design_management_legacy/mixins/all_versions.js b/app/assets/javascripts/design_management_legacy/mixins/all_versions.js
deleted file mode 100644
index 3966fe71732..00000000000
--- a/app/assets/javascripts/design_management_legacy/mixins/all_versions.js
+++ /dev/null
@@ -1,62 +0,0 @@
-import getDesignListQuery from '../graphql/queries/get_design_list.query.graphql';
-import appDataQuery from '../graphql/queries/app_data.query.graphql';
-import { findVersionId } from '../utils/design_management_utils';
-
-export default {
- apollo: {
- appData: {
- query: appDataQuery,
- manual: true,
- result({ data: { projectPath, issueIid } }) {
- this.projectPath = projectPath;
- this.issueIid = issueIid;
- },
- },
- allVersions: {
- query: getDesignListQuery,
- variables() {
- return {
- fullPath: this.projectPath,
- iid: this.issueIid,
- atVersion: null,
- };
- },
- update: data => data.project.issue.designCollection.versions.edges,
- },
- },
- computed: {
- hasValidVersion() {
- return (
- this.$route.query.version &&
- this.allVersions &&
- this.allVersions.some(version => version.node.id.endsWith(this.$route.query.version))
- );
- },
- designsVersion() {
- return this.hasValidVersion
- ? `gid://gitlab/DesignManagement::Version/${this.$route.query.version}`
- : null;
- },
- latestVersionId() {
- const latestVersion = this.allVersions[0];
- return latestVersion && findVersionId(latestVersion.node.id);
- },
- isLatestVersion() {
- if (this.allVersions.length > 0) {
- return (
- !this.$route.query.version ||
- !this.latestVersionId ||
- this.$route.query.version === this.latestVersionId
- );
- }
- return true;
- },
- },
- data() {
- return {
- allVersions: [],
- projectPath: '',
- issueIid: null,
- };
- },
-};
diff --git a/app/assets/javascripts/design_management_legacy/pages/design/index.vue b/app/assets/javascripts/design_management_legacy/pages/design/index.vue
deleted file mode 100644
index 2ada9eff8c6..00000000000
--- a/app/assets/javascripts/design_management_legacy/pages/design/index.vue
+++ /dev/null
@@ -1,378 +0,0 @@
-<script>
-import Mousetrap from 'mousetrap';
-import { GlLoadingIcon, GlAlert } from '@gitlab/ui';
-import { ApolloMutation } from 'vue-apollo';
-import { deprecatedCreateFlash as createFlash } from '~/flash';
-import { fetchPolicies } from '~/lib/graphql';
-import allVersionsMixin from '../../mixins/all_versions';
-import Toolbar from '../../components/toolbar/index.vue';
-import DesignDestroyer from '../../components/design_destroyer.vue';
-import DesignScaler from '../../components/design_scaler.vue';
-import DesignPresentation from '../../components/design_presentation.vue';
-import DesignReplyForm from '../../components/design_notes/design_reply_form.vue';
-import DesignSidebar from '../../components/design_sidebar.vue';
-import getDesignQuery from '../../graphql/queries/get_design.query.graphql';
-import appDataQuery from '../../graphql/queries/app_data.query.graphql';
-import createImageDiffNoteMutation from '../../graphql/mutations/create_image_diff_note.mutation.graphql';
-import updateImageDiffNoteMutation from '../../graphql/mutations/update_image_diff_note.mutation.graphql';
-import updateActiveDiscussionMutation from '../../graphql/mutations/update_active_discussion.mutation.graphql';
-import {
- extractDiscussions,
- extractDesign,
- updateImageDiffNoteOptimisticResponse,
-} from '../../utils/design_management_utils';
-import {
- updateStoreAfterAddImageDiffNote,
- updateStoreAfterUpdateImageDiffNote,
-} from '../../utils/cache_update';
-import {
- ADD_DISCUSSION_COMMENT_ERROR,
- ADD_IMAGE_DIFF_NOTE_ERROR,
- UPDATE_IMAGE_DIFF_NOTE_ERROR,
- DESIGN_NOT_FOUND_ERROR,
- DESIGN_VERSION_NOT_EXIST_ERROR,
- UPDATE_NOTE_ERROR,
- designDeletionError,
-} from '../../utils/error_messages';
-import { trackDesignDetailView } from '../../utils/tracking';
-import { DESIGNS_ROUTE_NAME } from '../../router/constants';
-import { ACTIVE_DISCUSSION_SOURCE_TYPES } from '../../constants';
-
-export default {
- components: {
- ApolloMutation,
- DesignReplyForm,
- DesignPresentation,
- DesignScaler,
- DesignDestroyer,
- Toolbar,
- GlLoadingIcon,
- GlAlert,
- DesignSidebar,
- },
- mixins: [allVersionsMixin],
- props: {
- id: {
- type: String,
- required: true,
- },
- },
- data() {
- return {
- design: {},
- comment: '',
- annotationCoordinates: null,
- projectPath: '',
- errorMessage: '',
- issueIid: '',
- scale: 1,
- resolvedDiscussionsExpanded: false,
- };
- },
- apollo: {
- appData: {
- query: appDataQuery,
- manual: true,
- result({ data: { projectPath, issueIid } }) {
- this.projectPath = projectPath;
- this.issueIid = issueIid;
- },
- },
- design: {
- query: getDesignQuery,
- // We want to see cached design version if we have one, and fetch newer version on the background to update discussions
- fetchPolicy: fetchPolicies.CACHE_AND_NETWORK,
- variables() {
- return this.designVariables;
- },
- update: data => extractDesign(data),
- result(res) {
- this.onDesignQueryResult(res);
- },
- error() {
- this.onQueryError(DESIGN_NOT_FOUND_ERROR);
- },
- },
- },
- computed: {
- isFirstLoading() {
- // We only want to show spinner on initial design load (when opened from a deep link to design)
- // If we already have cached a design, loading shouldn't be indicated to user
- return this.$apollo.queries.design.loading && !this.design.filename;
- },
- discussions() {
- if (!this.design.discussions) {
- return [];
- }
- return extractDiscussions(this.design.discussions);
- },
- markdownPreviewPath() {
- return `/${this.projectPath}/preview_markdown?target_type=Issue`;
- },
- isSubmitButtonDisabled() {
- return this.comment.trim().length === 0;
- },
- designVariables() {
- return {
- fullPath: this.projectPath,
- iid: this.issueIid,
- filenames: [this.$route.params.id],
- atVersion: this.designsVersion,
- };
- },
- mutationPayload() {
- const { x, y, width, height } = this.annotationCoordinates;
- return {
- noteableId: this.design.id,
- body: this.comment,
- position: {
- headSha: this.design.diffRefs.headSha,
- baseSha: this.design.diffRefs.baseSha,
- startSha: this.design.diffRefs.startSha,
- x,
- y,
- width,
- height,
- paths: {
- newPath: this.design.fullPath,
- },
- },
- };
- },
- isAnnotating() {
- return Boolean(this.annotationCoordinates);
- },
- resolvedDiscussions() {
- return this.discussions.filter(discussion => discussion.resolved);
- },
- },
- watch: {
- resolvedDiscussions(val) {
- if (!val.length) {
- this.resolvedDiscussionsExpanded = false;
- }
- },
- },
- mounted() {
- Mousetrap.bind('esc', this.closeDesign);
- this.trackEvent();
- // We need to reset the active discussion when opening a new design
- this.updateActiveDiscussion();
- },
- beforeDestroy() {
- Mousetrap.unbind('esc', this.closeDesign);
- },
- methods: {
- addImageDiffNoteToStore(
- store,
- {
- data: { createImageDiffNote },
- },
- ) {
- updateStoreAfterAddImageDiffNote(
- store,
- createImageDiffNote,
- getDesignQuery,
- this.designVariables,
- );
- },
- updateImageDiffNoteInStore(
- store,
- {
- data: { updateImageDiffNote },
- },
- ) {
- return updateStoreAfterUpdateImageDiffNote(
- store,
- updateImageDiffNote,
- getDesignQuery,
- this.designVariables,
- );
- },
- onMoveNote({ noteId, discussionId, position }) {
- const discussion = this.discussions.find(({ id }) => id === discussionId);
- const note = discussion.notes.find(
- ({ discussion: noteDiscussion }) => noteDiscussion.id === discussionId,
- );
-
- const mutationPayload = {
- optimisticResponse: updateImageDiffNoteOptimisticResponse(note, {
- position,
- }),
- variables: {
- input: {
- id: noteId,
- position,
- },
- },
- mutation: updateImageDiffNoteMutation,
- update: this.updateImageDiffNoteInStore,
- };
-
- return this.$apollo.mutate(mutationPayload).catch(e => this.onUpdateImageDiffNoteError(e));
- },
- onDesignQueryResult({ data, loading }) {
- // On the initial load with cache-and-network policy data is undefined while loading is true
- // To prevent throwing an error, we don't perform any logic until loading is false
- if (loading) {
- return;
- }
-
- if (!data || !extractDesign(data)) {
- this.onQueryError(DESIGN_NOT_FOUND_ERROR);
- } else if (this.$route.query.version && !this.hasValidVersion) {
- this.onQueryError(DESIGN_VERSION_NOT_EXIST_ERROR);
- }
- },
- onQueryError(message) {
- // because we redirect user to /designs (the issue page),
- // we want to create these flashes on the issue page
- createFlash(message);
- this.$router.push({ name: this.$options.DESIGNS_ROUTE_NAME });
- },
- onError(message, e) {
- this.errorMessage = message;
- throw e;
- },
- onCreateImageDiffNoteError(e) {
- this.onError(ADD_IMAGE_DIFF_NOTE_ERROR, e);
- },
- onUpdateNoteError(e) {
- this.onError(UPDATE_NOTE_ERROR, e);
- },
- onDesignDiscussionError(e) {
- this.onError(ADD_DISCUSSION_COMMENT_ERROR, e);
- },
- onUpdateImageDiffNoteError(e) {
- this.onError(UPDATE_IMAGE_DIFF_NOTE_ERROR, e);
- },
- onDesignDeleteError(e) {
- this.onError(designDeletionError({ singular: true }), e);
- },
- onResolveDiscussionError(e) {
- this.onError(UPDATE_IMAGE_DIFF_NOTE_ERROR, e);
- },
- openCommentForm(annotationCoordinates) {
- this.annotationCoordinates = annotationCoordinates;
- if (this.$refs.newDiscussionForm) {
- this.$refs.newDiscussionForm.focusInput();
- }
- },
- closeCommentForm() {
- this.comment = '';
- this.annotationCoordinates = null;
- },
- closeDesign() {
- this.$router.push({
- name: this.$options.DESIGNS_ROUTE_NAME,
- query: this.$route.query,
- });
- },
- trackEvent() {
- // TODO: This needs to be made aware of referers, or if it's rendered in a different context than a Issue
- trackDesignDetailView(
- 'issue-design-collection',
- 'issue',
- this.$route.query.version || this.latestVersionId,
- this.isLatestVersion,
- );
- },
- updateActiveDiscussion(id) {
- this.$apollo.mutate({
- mutation: updateActiveDiscussionMutation,
- variables: {
- id,
- source: ACTIVE_DISCUSSION_SOURCE_TYPES.discussion,
- },
- });
- },
- toggleResolvedComments() {
- this.resolvedDiscussionsExpanded = !this.resolvedDiscussionsExpanded;
- },
- },
- createImageDiffNoteMutation,
- DESIGNS_ROUTE_NAME,
-};
-</script>
-
-<template>
- <div
- class="design-detail js-design-detail fixed-top w-100 position-bottom-0 d-flex justify-content-center flex-column flex-lg-row"
- >
- <gl-loading-icon v-if="isFirstLoading" size="xl" class="align-self-center" />
- <template v-else>
- <div class="d-flex overflow-hidden flex-grow-1 flex-column position-relative">
- <design-destroyer
- :filenames="[design.filename]"
- :project-path="projectPath"
- :iid="issueIid"
- @done="$router.push({ name: $options.DESIGNS_ROUTE_NAME })"
- @error="onDesignDeleteError"
- >
- <template #default="{ mutate, loading }">
- <toolbar
- :id="id"
- :is-deleting="loading"
- :is-latest-version="isLatestVersion"
- v-bind="design"
- @delete="mutate"
- />
- </template>
- </design-destroyer>
-
- <div v-if="errorMessage" class="p-3">
- <gl-alert variant="danger" @dismiss="errorMessage = null">
- {{ errorMessage }}
- </gl-alert>
- </div>
- <design-presentation
- :image="design.image"
- :image-name="design.filename"
- :discussions="discussions"
- :is-annotating="isAnnotating"
- :scale="scale"
- :resolved-discussions-expanded="resolvedDiscussionsExpanded"
- @openCommentForm="openCommentForm"
- @closeCommentForm="closeCommentForm"
- @moveNote="onMoveNote"
- />
-
- <div class="design-scaler-wrapper position-absolute mb-4 d-flex-center">
- <design-scaler @scale="scale = $event" />
- </div>
- </div>
- <design-sidebar
- :design="design"
- :resolved-discussions-expanded="resolvedDiscussionsExpanded"
- :markdown-preview-path="markdownPreviewPath"
- @onDesignDiscussionError="onDesignDiscussionError"
- @onCreateImageDiffNoteError="onCreateImageDiffNoteError"
- @updateNoteError="onUpdateNoteError"
- @resolveDiscussionError="onResolveDiscussionError"
- @toggleResolvedComments="toggleResolvedComments"
- >
- <template #replyForm>
- <apollo-mutation
- v-if="isAnnotating"
- #default="{ mutate, loading }"
- :mutation="$options.createImageDiffNoteMutation"
- :variables="{
- input: mutationPayload,
- }"
- :update="addImageDiffNoteToStore"
- @done="closeCommentForm"
- @error="onCreateImageDiffNoteError"
- >
- <design-reply-form
- ref="newDiscussionForm"
- v-model="comment"
- :is-saving="loading"
- :markdown-preview-path="markdownPreviewPath"
- @submitForm="mutate"
- @cancelForm="closeCommentForm"
- /> </apollo-mutation
- ></template>
- </design-sidebar>
- </template>
- </div>
-</template>
diff --git a/app/assets/javascripts/design_management_legacy/pages/index.vue b/app/assets/javascripts/design_management_legacy/pages/index.vue
deleted file mode 100644
index 66008a193ce..00000000000
--- a/app/assets/javascripts/design_management_legacy/pages/index.vue
+++ /dev/null
@@ -1,323 +0,0 @@
-<script>
-import { GlLoadingIcon, GlDeprecatedButton, GlAlert } from '@gitlab/ui';
-import { deprecatedCreateFlash as createFlash } from '~/flash';
-import { s__, sprintf } from '~/locale';
-import UploadButton from '../components/upload/button.vue';
-import DeleteButton from '../components/delete_button.vue';
-import Design from '../components/list/item.vue';
-import DesignDestroyer from '../components/design_destroyer.vue';
-import DesignVersionDropdown from '../components/upload/design_version_dropdown.vue';
-import DesignDropzone from '../components/upload/design_dropzone.vue';
-import uploadDesignMutation from '../graphql/mutations/upload_design.mutation.graphql';
-import permissionsQuery from '../graphql/queries/design_permissions.query.graphql';
-import getDesignListQuery from '../graphql/queries/get_design_list.query.graphql';
-import allDesignsMixin from '../mixins/all_designs';
-import {
- UPLOAD_DESIGN_ERROR,
- EXISTING_DESIGN_DROP_MANY_FILES_MESSAGE,
- EXISTING_DESIGN_DROP_INVALID_FILENAME_MESSAGE,
- designUploadSkippedWarning,
- designDeletionError,
-} from '../utils/error_messages';
-import { updateStoreAfterUploadDesign } from '../utils/cache_update';
-import {
- designUploadOptimisticResponse,
- isValidDesignFile,
-} from '../utils/design_management_utils';
-import { getFilename } from '~/lib/utils/file_upload';
-import { DESIGNS_ROUTE_NAME } from '../router/constants';
-
-const MAXIMUM_FILE_UPLOAD_LIMIT = 10;
-
-export default {
- components: {
- GlLoadingIcon,
- GlAlert,
- GlDeprecatedButton,
- UploadButton,
- Design,
- DesignDestroyer,
- DesignVersionDropdown,
- DeleteButton,
- DesignDropzone,
- },
- mixins: [allDesignsMixin],
- apollo: {
- permissions: {
- query: permissionsQuery,
- variables() {
- return {
- fullPath: this.projectPath,
- iid: this.issueIid,
- };
- },
- update: data => data.project.issue.userPermissions,
- },
- },
- data() {
- return {
- permissions: {
- createDesign: false,
- },
- filesToBeSaved: [],
- selectedDesigns: [],
- };
- },
- computed: {
- isLoading() {
- return this.$apollo.queries.designs.loading || this.$apollo.queries.permissions.loading;
- },
- isSaving() {
- return this.filesToBeSaved.length > 0;
- },
- canCreateDesign() {
- return this.permissions.createDesign;
- },
- showToolbar() {
- return this.canCreateDesign && this.allVersions.length > 0;
- },
- hasDesigns() {
- return this.designs.length > 0;
- },
- hasSelectedDesigns() {
- return this.selectedDesigns.length > 0;
- },
- canDeleteDesigns() {
- return this.isLatestVersion && this.hasSelectedDesigns;
- },
- projectQueryBody() {
- return {
- query: getDesignListQuery,
- variables: { fullPath: this.projectPath, iid: this.issueIid, atVersion: null },
- };
- },
- selectAllButtonText() {
- return this.hasSelectedDesigns
- ? s__('DesignManagement|Deselect all')
- : s__('DesignManagement|Select all');
- },
- },
- mounted() {
- this.toggleOnPasteListener(this.$route.name);
- },
- methods: {
- resetFilesToBeSaved() {
- this.filesToBeSaved = [];
- },
- /**
- * Determine if a design upload is valid, given [files]
- * @param {Array<File>} files
- */
- isValidDesignUpload(files) {
- if (!this.canCreateDesign) return false;
-
- if (files.length > MAXIMUM_FILE_UPLOAD_LIMIT) {
- createFlash(
- sprintf(
- s__(
- 'DesignManagement|The maximum number of designs allowed to be uploaded is %{upload_limit}. Please try again.',
- ),
- {
- upload_limit: MAXIMUM_FILE_UPLOAD_LIMIT,
- },
- ),
- );
-
- return false;
- }
- return true;
- },
- onUploadDesign(files) {
- // convert to Array so that we have Array methods (.map, .some, etc.)
- this.filesToBeSaved = Array.from(files);
- if (!this.isValidDesignUpload(this.filesToBeSaved)) return null;
-
- const mutationPayload = {
- optimisticResponse: designUploadOptimisticResponse(this.filesToBeSaved),
- variables: {
- files: this.filesToBeSaved,
- projectPath: this.projectPath,
- iid: this.issueIid,
- },
- context: {
- hasUpload: true,
- },
- mutation: uploadDesignMutation,
- update: this.afterUploadDesign,
- };
-
- return this.$apollo
- .mutate(mutationPayload)
- .then(res => this.onUploadDesignDone(res))
- .catch(() => this.onUploadDesignError());
- },
- afterUploadDesign(
- store,
- {
- data: { designManagementUpload },
- },
- ) {
- updateStoreAfterUploadDesign(store, designManagementUpload, this.projectQueryBody);
- },
- onUploadDesignDone(res) {
- const skippedFiles = res?.data?.designManagementUpload?.skippedDesigns || [];
- const skippedWarningMessage = designUploadSkippedWarning(this.filesToBeSaved, skippedFiles);
- if (skippedWarningMessage) {
- createFlash(skippedWarningMessage, 'warning');
- }
-
- // if this upload resulted in a new version being created, redirect user to the latest version
- if (!this.isLatestVersion) {
- this.$router.push({ name: DESIGNS_ROUTE_NAME });
- }
- this.resetFilesToBeSaved();
- },
- onUploadDesignError() {
- this.resetFilesToBeSaved();
- createFlash(UPLOAD_DESIGN_ERROR);
- },
- changeSelectedDesigns(filename) {
- if (this.isDesignSelected(filename)) {
- this.selectedDesigns = this.selectedDesigns.filter(design => design !== filename);
- } else {
- this.selectedDesigns.push(filename);
- }
- },
- toggleDesignsSelection() {
- if (this.hasSelectedDesigns) {
- this.selectedDesigns = [];
- } else {
- this.selectedDesigns = this.designs.map(design => design.filename);
- }
- },
- isDesignSelected(filename) {
- return this.selectedDesigns.includes(filename);
- },
- isDesignToBeSaved(filename) {
- return this.filesToBeSaved.some(file => file.name === filename);
- },
- canSelectDesign(filename) {
- return this.isLatestVersion && this.canCreateDesign && !this.isDesignToBeSaved(filename);
- },
- onDesignDelete() {
- this.selectedDesigns = [];
- if (this.$route.query.version) this.$router.push({ name: DESIGNS_ROUTE_NAME });
- },
- onDesignDeleteError() {
- const errorMessage = designDeletionError({ singular: this.selectedDesigns.length === 1 });
- createFlash(errorMessage);
- },
- onExistingDesignDropzoneChange(files, existingDesignFilename) {
- const filesArr = Array.from(files);
-
- if (filesArr.length > 1) {
- createFlash(EXISTING_DESIGN_DROP_MANY_FILES_MESSAGE);
- return;
- }
-
- if (!filesArr.some(({ name }) => existingDesignFilename === name)) {
- createFlash(EXISTING_DESIGN_DROP_INVALID_FILENAME_MESSAGE);
- return;
- }
-
- this.onUploadDesign(files);
- },
- onDesignPaste(event) {
- const { clipboardData } = event;
- const files = Array.from(clipboardData.files);
- if (clipboardData && files.length > 0) {
- if (!files.some(isValidDesignFile)) {
- return;
- }
- event.preventDefault();
- let filename = getFilename(event);
- if (!filename || filename === 'image.png') {
- filename = `design_${Date.now()}.png`;
- }
- const newFile = new File([files[0]], filename);
- this.onUploadDesign([newFile]);
- }
- },
- toggleOnPasteListener(route) {
- if (route === DESIGNS_ROUTE_NAME) {
- document.addEventListener('paste', this.onDesignPaste);
- } else {
- document.removeEventListener('paste', this.onDesignPaste);
- }
- },
- },
- beforeRouteUpdate(to, from, next) {
- this.toggleOnPasteListener(to.name);
- this.selectedDesigns = [];
- next();
- },
- beforeRouteLeave(to, from, next) {
- this.toggleOnPasteListener(to.name);
- next();
- },
-};
-</script>
-
-<template>
- <div>
- <header v-if="showToolbar" class="row-content-block border-top-0 p-2 d-flex">
- <div class="d-flex justify-content-between align-items-center w-100">
- <design-version-dropdown />
- <div :class="['qa-selector-toolbar', { 'd-flex': hasDesigns, 'd-none': !hasDesigns }]">
- <gl-deprecated-button
- v-if="isLatestVersion"
- variant="link"
- class="mr-2 js-select-all"
- @click="toggleDesignsSelection"
- >{{ selectAllButtonText }}</gl-deprecated-button
- >
- <design-destroyer
- #default="{ mutate, loading }"
- :filenames="selectedDesigns"
- :project-path="projectPath"
- :iid="issueIid"
- @done="onDesignDelete"
- @error="onDesignDeleteError"
- >
- <delete-button
- v-if="isLatestVersion"
- :is-deleting="loading"
- button-class="btn-danger btn-inverted mr-2"
- :has-selected-designs="hasSelectedDesigns"
- @deleteSelectedDesigns="mutate()"
- >
- {{ s__('DesignManagement|Delete selected') }}
- <gl-loading-icon v-if="loading" inline class="ml-1" />
- </delete-button>
- </design-destroyer>
- <upload-button v-if="canCreateDesign" :is-saving="isSaving" @upload="onUploadDesign" />
- </div>
- </div>
- </header>
- <div class="mt-4">
- <gl-loading-icon v-if="isLoading" size="md" />
- <gl-alert v-else-if="error" variant="danger" :dismissible="false">
- {{ __('An error occurred while loading designs. Please try again.') }}
- </gl-alert>
- <ol v-else class="list-unstyled row">
- <li class="col-md-6 col-lg-4 mb-3">
- <design-dropzone class="design-list-item" @change="onUploadDesign" />
- </li>
- <li v-for="design in designs" :key="design.id" class="col-md-6 col-lg-4 mb-3">
- <design-dropzone @change="onExistingDesignDropzoneChange($event, design.filename)"
- ><design v-bind="design" :is-uploading="isDesignToBeSaved(design.filename)"
- /></design-dropzone>
-
- <input
- v-if="canSelectDesign(design.filename)"
- :checked="isDesignSelected(design.filename)"
- type="checkbox"
- class="design-checkbox"
- @change="changeSelectedDesigns(design.filename)"
- />
- </li>
- </ol>
- </div>
- <router-view :key="$route.fullPath" />
- </div>
-</template>
diff --git a/app/assets/javascripts/design_management_legacy/router/constants.js b/app/assets/javascripts/design_management_legacy/router/constants.js
deleted file mode 100644
index abeef520e33..00000000000
--- a/app/assets/javascripts/design_management_legacy/router/constants.js
+++ /dev/null
@@ -1,3 +0,0 @@
-export const ROOT_ROUTE_NAME = 'root';
-export const DESIGNS_ROUTE_NAME = 'designs';
-export const DESIGN_ROUTE_NAME = 'design';
diff --git a/app/assets/javascripts/design_management_legacy/router/index.js b/app/assets/javascripts/design_management_legacy/router/index.js
deleted file mode 100644
index 28a81ed0278..00000000000
--- a/app/assets/javascripts/design_management_legacy/router/index.js
+++ /dev/null
@@ -1,35 +0,0 @@
-import $ from 'jquery';
-import Vue from 'vue';
-import VueRouter from 'vue-router';
-import routes from './routes';
-import { DESIGN_ROUTE_NAME } from './constants';
-import { getPageLayoutElement } from '~/design_management_legacy/utils/design_management_utils';
-import { DESIGN_DETAIL_LAYOUT_CLASSLIST } from '../constants';
-
-Vue.use(VueRouter);
-
-export default function createRouter(base) {
- const router = new VueRouter({
- base,
- mode: 'history',
- routes,
- });
- const pageEl = getPageLayoutElement();
-
- router.beforeEach(({ meta: { el }, name }, _, next) => {
- $(`#${el}`).tab('show');
-
- // apply a fullscreen layout style in Design View (a.k.a design detail)
- if (pageEl) {
- if (name === DESIGN_ROUTE_NAME) {
- pageEl.classList.add(...DESIGN_DETAIL_LAYOUT_CLASSLIST);
- } else {
- pageEl.classList.remove(...DESIGN_DETAIL_LAYOUT_CLASSLIST);
- }
- }
-
- next();
- });
-
- return router;
-}
diff --git a/app/assets/javascripts/design_management_legacy/router/routes.js b/app/assets/javascripts/design_management_legacy/router/routes.js
deleted file mode 100644
index 788910e5514..00000000000
--- a/app/assets/javascripts/design_management_legacy/router/routes.js
+++ /dev/null
@@ -1,44 +0,0 @@
-import Home from '../pages/index.vue';
-import DesignDetail from '../pages/design/index.vue';
-import { ROOT_ROUTE_NAME, DESIGNS_ROUTE_NAME, DESIGN_ROUTE_NAME } from './constants';
-
-export default [
- {
- name: ROOT_ROUTE_NAME,
- path: '/',
- component: Home,
- meta: {
- el: 'discussion',
- },
- },
- {
- name: DESIGNS_ROUTE_NAME,
- path: '/designs',
- component: Home,
- meta: {
- el: 'designs',
- },
- children: [
- {
- name: DESIGN_ROUTE_NAME,
- path: ':id',
- component: DesignDetail,
- meta: {
- el: 'designs',
- },
- beforeEnter(
- {
- params: { id },
- },
- from,
- next,
- ) {
- if (typeof id === 'string') {
- next();
- }
- },
- props: ({ params: { id } }) => ({ id }),
- },
- ],
- },
-];
diff --git a/app/assets/javascripts/design_management_legacy/utils/cache_update.js b/app/assets/javascripts/design_management_legacy/utils/cache_update.js
deleted file mode 100644
index 5ba6f84c413..00000000000
--- a/app/assets/javascripts/design_management_legacy/utils/cache_update.js
+++ /dev/null
@@ -1,276 +0,0 @@
-/* eslint-disable @gitlab/require-i18n-strings */
-
-import { deprecatedCreateFlash as createFlash } from '~/flash';
-import { extractCurrentDiscussion, extractDesign } from './design_management_utils';
-import {
- ADD_IMAGE_DIFF_NOTE_ERROR,
- UPDATE_IMAGE_DIFF_NOTE_ERROR,
- ADD_DISCUSSION_COMMENT_ERROR,
- designDeletionError,
-} from './error_messages';
-
-const deleteDesignsFromStore = (store, query, selectedDesigns) => {
- const data = store.readQuery(query);
-
- const changedDesigns = data.project.issue.designCollection.designs.edges.filter(
- ({ node }) => !selectedDesigns.includes(node.filename),
- );
- data.project.issue.designCollection.designs.edges = [...changedDesigns];
-
- store.writeQuery({
- ...query,
- data,
- });
-};
-
-/**
- * Adds a new version of designs to store
- *
- * @param {Object} store
- * @param {Object} query
- * @param {Object} version
- */
-const addNewVersionToStore = (store, query, version) => {
- if (!version) return;
-
- const data = store.readQuery(query);
- const newEdge = { node: version, __typename: 'DesignVersionEdge' };
-
- data.project.issue.designCollection.versions.edges = [
- newEdge,
- ...data.project.issue.designCollection.versions.edges,
- ];
-
- store.writeQuery({
- ...query,
- data,
- });
-};
-
-const addDiscussionCommentToStore = (store, createNote, query, queryVariables, discussionId) => {
- const data = store.readQuery({
- query,
- variables: queryVariables,
- });
-
- const design = extractDesign(data);
- const currentDiscussion = extractCurrentDiscussion(design.discussions, discussionId);
- currentDiscussion.notes.nodes = [...currentDiscussion.notes.nodes, createNote.note];
-
- design.notesCount += 1;
- if (
- !design.issue.participants.edges.some(
- participant => participant.node.username === createNote.note.author.username,
- )
- ) {
- design.issue.participants.edges = [
- ...design.issue.participants.edges,
- {
- __typename: 'UserEdge',
- node: {
- __typename: 'User',
- ...createNote.note.author,
- },
- },
- ];
- }
- store.writeQuery({
- query,
- variables: queryVariables,
- data: {
- ...data,
- design: {
- ...design,
- },
- },
- });
-};
-
-const addImageDiffNoteToStore = (store, createImageDiffNote, query, variables) => {
- const data = store.readQuery({
- query,
- variables,
- });
- const newDiscussion = {
- __typename: 'Discussion',
- id: createImageDiffNote.note.discussion.id,
- replyId: createImageDiffNote.note.discussion.replyId,
- resolvable: true,
- resolved: false,
- resolvedAt: null,
- resolvedBy: null,
- notes: {
- __typename: 'NoteConnection',
- nodes: [createImageDiffNote.note],
- },
- };
- const design = extractDesign(data);
- const notesCount = design.notesCount + 1;
- design.discussions.nodes = [...design.discussions.nodes, newDiscussion];
- if (
- !design.issue.participants.edges.some(
- participant => participant.node.username === createImageDiffNote.note.author.username,
- )
- ) {
- design.issue.participants.edges = [
- ...design.issue.participants.edges,
- {
- __typename: 'UserEdge',
- node: {
- __typename: 'User',
- ...createImageDiffNote.note.author,
- },
- },
- ];
- }
- store.writeQuery({
- query,
- variables,
- data: {
- ...data,
- design: {
- ...design,
- notesCount,
- },
- },
- });
-};
-
-const updateImageDiffNoteInStore = (store, updateImageDiffNote, query, variables) => {
- const data = store.readQuery({
- query,
- variables,
- });
-
- const design = extractDesign(data);
- const discussion = extractCurrentDiscussion(
- design.discussions,
- updateImageDiffNote.note.discussion.id,
- );
-
- discussion.notes = {
- ...discussion.notes,
- nodes: [updateImageDiffNote.note, ...discussion.notes.nodes.slice(1)],
- };
-
- store.writeQuery({
- query,
- variables,
- data: {
- ...data,
- design,
- },
- });
-};
-
-const addNewDesignToStore = (store, designManagementUpload, query) => {
- const data = store.readQuery(query);
-
- const newDesigns = data.project.issue.designCollection.designs.edges.reduce((acc, design) => {
- if (!acc.find(d => d.filename === design.node.filename)) {
- acc.push(design.node);
- }
-
- return acc;
- }, designManagementUpload.designs);
-
- let newVersionNode;
- const findNewVersions = designManagementUpload.designs.find(design => design.versions);
-
- if (findNewVersions) {
- const findNewVersionsEdges = findNewVersions.versions.edges;
-
- if (findNewVersionsEdges && findNewVersionsEdges.length) {
- newVersionNode = [findNewVersionsEdges[0]];
- }
- }
-
- const newVersions = [
- ...(newVersionNode || []),
- ...data.project.issue.designCollection.versions.edges,
- ];
-
- const updatedDesigns = {
- __typename: 'DesignCollection',
- designs: {
- __typename: 'DesignConnection',
- edges: newDesigns.map(design => ({
- __typename: 'DesignEdge',
- node: design,
- })),
- },
- versions: {
- __typename: 'DesignVersionConnection',
- edges: newVersions,
- },
- };
-
- data.project.issue.designCollection = updatedDesigns;
-
- store.writeQuery({
- ...query,
- data,
- });
-};
-
-const onError = (data, message) => {
- createFlash(message);
- throw new Error(data.errors);
-};
-
-export const hasErrors = ({ errors = [] }) => errors?.length;
-
-/**
- * Updates a store after design deletion
- *
- * @param {Object} store
- * @param {Object} data
- * @param {Object} query
- * @param {Array} designs
- */
-export const updateStoreAfterDesignsDelete = (store, data, query, designs) => {
- if (hasErrors(data)) {
- onError(data, designDeletionError({ singular: designs.length === 1 }));
- } else {
- deleteDesignsFromStore(store, query, designs);
- addNewVersionToStore(store, query, data.version);
- }
-};
-
-export const updateStoreAfterAddDiscussionComment = (
- store,
- data,
- query,
- queryVariables,
- discussionId,
-) => {
- if (hasErrors(data)) {
- onError(data, ADD_DISCUSSION_COMMENT_ERROR);
- } else {
- addDiscussionCommentToStore(store, data, query, queryVariables, discussionId);
- }
-};
-
-export const updateStoreAfterAddImageDiffNote = (store, data, query, queryVariables) => {
- if (hasErrors(data)) {
- onError(data, ADD_IMAGE_DIFF_NOTE_ERROR);
- } else {
- addImageDiffNoteToStore(store, data, query, queryVariables);
- }
-};
-
-export const updateStoreAfterUpdateImageDiffNote = (store, data, query, queryVariables) => {
- if (hasErrors(data)) {
- onError(data, UPDATE_IMAGE_DIFF_NOTE_ERROR);
- } else {
- updateImageDiffNoteInStore(store, data, query, queryVariables);
- }
-};
-
-export const updateStoreAfterUploadDesign = (store, data, query) => {
- if (hasErrors(data)) {
- onError(data, data.errors[0]);
- } else {
- addNewDesignToStore(store, data, query);
- }
-};
diff --git a/app/assets/javascripts/design_management_legacy/utils/design_management_utils.js b/app/assets/javascripts/design_management_legacy/utils/design_management_utils.js
deleted file mode 100644
index 22705cf67a1..00000000000
--- a/app/assets/javascripts/design_management_legacy/utils/design_management_utils.js
+++ /dev/null
@@ -1,128 +0,0 @@
-import { uniqueId } from 'lodash';
-import { VALID_DESIGN_FILE_MIMETYPE } from '../constants';
-
-export const isValidDesignFile = ({ type }) =>
- (type.match(VALID_DESIGN_FILE_MIMETYPE.regex) || []).length > 0;
-
-/**
- * Returns formatted array that doesn't contain
- * `edges`->`node` nesting
- *
- * @param {Array} elements
- */
-
-export const extractNodes = elements => elements.edges.map(({ node }) => node);
-
-/**
- * Returns formatted array of discussions that doesn't contain
- * `edges`->`node` nesting for child notes
- *
- * @param {Array} discussions
- */
-
-export const extractDiscussions = discussions =>
- discussions.nodes.map((discussion, index) => ({
- ...discussion,
- index: index + 1,
- notes: discussion.notes.nodes,
- }));
-
-/**
- * Returns a discussion with the given id from discussions array
- *
- * @param {Array} discussions
- */
-
-export const extractCurrentDiscussion = (discussions, id) =>
- discussions.nodes.find(discussion => discussion.id === id);
-
-export const findVersionId = id => (id.match('::Version/(.+$)') || [])[1];
-
-export const findNoteId = id => (id.match('DiffNote/(.+$)') || [])[1];
-
-export const extractDesigns = data => data.project.issue.designCollection.designs.edges;
-
-export const extractDesign = data => (extractDesigns(data) || [])[0]?.node;
-
-/**
- * Generates optimistic response for a design upload mutation
- * @param {Array<File>} files
- */
-export const designUploadOptimisticResponse = files => {
- const designs = files.map(file => ({
- // False positive i18n lint: https://gitlab.com/gitlab-org/frontend/eslint-plugin-i18n/issues/26
- // eslint-disable-next-line @gitlab/require-i18n-strings
- __typename: 'Design',
- id: -uniqueId(),
- image: '',
- imageV432x230: '',
- filename: file.name,
- fullPath: '',
- notesCount: 0,
- event: 'NONE',
- diffRefs: {
- __typename: 'DiffRefs',
- baseSha: '',
- startSha: '',
- headSha: '',
- },
- discussions: {
- __typename: 'DesignDiscussion',
- nodes: [],
- },
- versions: {
- __typename: 'DesignVersionConnection',
- edges: {
- __typename: 'DesignVersionEdge',
- node: {
- __typename: 'DesignVersion',
- id: -uniqueId(),
- sha: -uniqueId(),
- },
- },
- },
- }));
-
- return {
- // False positive i18n lint: https://gitlab.com/gitlab-org/frontend/eslint-plugin-i18n/issues/26
- // eslint-disable-next-line @gitlab/require-i18n-strings
- __typename: 'Mutation',
- designManagementUpload: {
- __typename: 'DesignManagementUploadPayload',
- designs,
- skippedDesigns: [],
- errors: [],
- },
- };
-};
-
-/**
- * Generates optimistic response for a design upload mutation
- * @param {Array<File>} files
- */
-export const updateImageDiffNoteOptimisticResponse = (note, { position }) => ({
- // False positive i18n lint: https://gitlab.com/gitlab-org/frontend/eslint-plugin-i18n/issues/26
- // eslint-disable-next-line @gitlab/require-i18n-strings
- __typename: 'Mutation',
- updateImageDiffNote: {
- __typename: 'UpdateImageDiffNotePayload',
- note: {
- ...note,
- position: {
- ...note.position,
- ...position,
- },
- },
- errors: [],
- },
-});
-
-const normalizeAuthor = author => ({
- ...author,
- web_url: author.webUrl,
- avatar_url: author.avatarUrl,
-});
-
-export const extractParticipants = users => users.edges.map(({ node }) => normalizeAuthor(node));
-
-export const getPageLayoutElement = () => document.querySelector('.layout-page');
diff --git a/app/assets/javascripts/design_management_legacy/utils/error_messages.js b/app/assets/javascripts/design_management_legacy/utils/error_messages.js
deleted file mode 100644
index 7666c726c2f..00000000000
--- a/app/assets/javascripts/design_management_legacy/utils/error_messages.js
+++ /dev/null
@@ -1,95 +0,0 @@
-import { __, s__, n__, sprintf } from '~/locale';
-
-export const ADD_DISCUSSION_COMMENT_ERROR = s__(
- 'DesignManagement|Could not add a new comment. Please try again.',
-);
-
-export const ADD_IMAGE_DIFF_NOTE_ERROR = s__(
- 'DesignManagement|Could not create new discussion. Please try again.',
-);
-
-export const UPDATE_IMAGE_DIFF_NOTE_ERROR = s__(
- 'DesignManagement|Could not update discussion. Please try again.',
-);
-
-export const UPDATE_NOTE_ERROR = s__('DesignManagement|Could not update note. Please try again.');
-
-export const UPLOAD_DESIGN_ERROR = s__(
- 'DesignManagement|Error uploading a new design. Please try again.',
-);
-
-export const UPLOAD_DESIGN_INVALID_FILETYPE_ERROR = __(
- 'Could not upload your designs as one or more files uploaded are not supported.',
-);
-
-export const DESIGN_NOT_FOUND_ERROR = __('Could not find design.');
-
-export const DESIGN_VERSION_NOT_EXIST_ERROR = __('Requested design version does not exist.');
-
-const DESIGN_UPLOAD_SKIPPED_MESSAGE = s__('DesignManagement|Upload skipped.');
-
-const ALL_DESIGNS_SKIPPED_MESSAGE = `${DESIGN_UPLOAD_SKIPPED_MESSAGE} ${s__(
- 'The designs you tried uploading did not change.',
-)}`;
-
-export const EXISTING_DESIGN_DROP_MANY_FILES_MESSAGE = __(
- 'You can only upload one design when dropping onto an existing design.',
-);
-
-export const EXISTING_DESIGN_DROP_INVALID_FILENAME_MESSAGE = __(
- 'You must upload a file with the same file name when dropping onto an existing design.',
-);
-
-const MAX_SKIPPED_FILES_LISTINGS = 5;
-
-const oneDesignSkippedMessage = filename =>
- `${DESIGN_UPLOAD_SKIPPED_MESSAGE} ${sprintf(s__('DesignManagement|%{filename} did not change.'), {
- filename,
- })}`;
-
-/**
- * Return warning message indicating that some (but not all) uploaded
- * files were skipped.
- * @param {Array<{ filename }>} skippedFiles
- */
-const someDesignsSkippedMessage = skippedFiles => {
- const designsSkippedMessage = `${DESIGN_UPLOAD_SKIPPED_MESSAGE} ${s__(
- 'Some of the designs you tried uploading did not change:',
- )}`;
-
- const moreText = sprintf(s__(`DesignManagement|and %{moreCount} more.`), {
- moreCount: skippedFiles.length - MAX_SKIPPED_FILES_LISTINGS,
- });
-
- return `${designsSkippedMessage} ${skippedFiles
- .slice(0, MAX_SKIPPED_FILES_LISTINGS)
- .map(({ filename }) => filename)
- .join(', ')}${skippedFiles.length > MAX_SKIPPED_FILES_LISTINGS ? `, ${moreText}` : '.'}`;
-};
-
-export const designDeletionError = ({ singular = true } = {}) => {
- const design = singular ? __('a design') : __('designs');
- return sprintf(s__('Could not delete %{design}. Please try again.'), {
- design,
- });
-};
-
-/**
- * Return warning message, if applicable, that one, some or all uploaded
- * files were skipped.
- * @param {Array<{ filename }>} uploadedDesigns
- * @param {Array<{ filename }>} skippedFiles
- */
-export const designUploadSkippedWarning = (uploadedDesigns, skippedFiles) => {
- if (skippedFiles.length === 0) {
- return null;
- }
-
- if (skippedFiles.length === uploadedDesigns.length) {
- const { filename } = skippedFiles[0];
-
- return n__(oneDesignSkippedMessage(filename), ALL_DESIGNS_SKIPPED_MESSAGE, skippedFiles.length);
- }
-
- return someDesignsSkippedMessage(skippedFiles);
-};
diff --git a/app/assets/javascripts/design_management_legacy/utils/tracking.js b/app/assets/javascripts/design_management_legacy/utils/tracking.js
deleted file mode 100644
index b3ecc1453a6..00000000000
--- a/app/assets/javascripts/design_management_legacy/utils/tracking.js
+++ /dev/null
@@ -1,27 +0,0 @@
-import Tracking from '~/tracking';
-
-// Tracking Constants
-const DESIGN_TRACKING_CONTEXT_SCHEMA = 'iglu:com.gitlab/design_management_context/jsonschema/1-0-0';
-const DESIGN_TRACKING_PAGE_NAME = 'projects:issues:design';
-const DESIGN_TRACKING_EVENT_NAME = 'view_design';
-
-// eslint-disable-next-line import/prefer-default-export
-export function trackDesignDetailView(
- referer = '',
- owner = '',
- designVersion = 1,
- latestVersion = false,
-) {
- Tracking.event(DESIGN_TRACKING_PAGE_NAME, DESIGN_TRACKING_EVENT_NAME, {
- label: DESIGN_TRACKING_EVENT_NAME,
- context: {
- schema: DESIGN_TRACKING_CONTEXT_SCHEMA,
- data: {
- 'design-version-number': designVersion,
- 'design-is-current-version': latestVersion,
- 'internal-object-referrer': referer,
- 'design-collection-owner': owner,
- },
- },
- });
-}
diff --git a/app/assets/javascripts/diff_notes/components/jump_to_discussion.js b/app/assets/javascripts/diff_notes/components/jump_to_discussion.js
index 0991f5282a8..1de00c9f08b 100644
--- a/app/assets/javascripts/diff_notes/components/jump_to_discussion.js
+++ b/app/assets/javascripts/diff_notes/components/jump_to_discussion.js
@@ -2,6 +2,7 @@
/* global CommentsStore */
import $ from 'jquery';
+import 'vendor/jquery.scrollTo';
import Vue from 'vue';
import { __ } from '~/locale';
diff --git a/app/assets/javascripts/diffs/components/app.vue b/app/assets/javascripts/diffs/components/app.vue
index 5062006424e..dd5addbf1e3 100644
--- a/app/assets/javascripts/diffs/components/app.vue
+++ b/app/assets/javascripts/diffs/components/app.vue
@@ -1,6 +1,6 @@
<script>
import { mapState, mapGetters, mapActions } from 'vuex';
-import { GlLoadingIcon, GlButtonGroup, GlButton, GlAlert } from '@gitlab/ui';
+import { GlLoadingIcon, GlPagination, GlSprintf } from '@gitlab/ui';
import Mousetrap from 'mousetrap';
import { __ } from '~/locale';
import { getParameterByName, parseBoolean } from '~/lib/utils/common_utils';
@@ -13,9 +13,13 @@ import eventHub from '../../notes/event_hub';
import CompareVersions from './compare_versions.vue';
import DiffFile from './diff_file.vue';
import NoChanges from './no_changes.vue';
-import HiddenFilesWarning from './hidden_files_warning.vue';
import CommitWidget from './commit_widget.vue';
import TreeList from './tree_list.vue';
+
+import HiddenFilesWarning from './hidden_files_warning.vue';
+import MergeConflictWarning from './merge_conflict_warning.vue';
+import CollapsedFilesWarning from './collapsed_files_warning.vue';
+
import {
TREE_LIST_WIDTH_STORAGE_KEY,
INITIAL_TREE_WIDTH,
@@ -24,6 +28,9 @@ import {
TREE_HIDE_STATS_WIDTH,
MR_TREE_SHOW_KEY,
CENTERED_LIMITED_CONTAINER_CLASSES,
+ ALERT_OVERFLOW_HIDDEN,
+ ALERT_MERGE_CONFLICT,
+ ALERT_COLLAPSED_FILES,
} from '../constants';
export default {
@@ -33,15 +40,21 @@ export default {
DiffFile,
NoChanges,
HiddenFilesWarning,
+ MergeConflictWarning,
+ CollapsedFilesWarning,
CommitWidget,
TreeList,
GlLoadingIcon,
PanelResizer,
- GlButtonGroup,
- GlButton,
- GlAlert,
+ GlPagination,
+ GlSprintf,
},
mixins: [glFeatureFlagsMixin()],
+ alerts: {
+ ALERT_OVERFLOW_HIDDEN,
+ ALERT_MERGE_CONFLICT,
+ ALERT_COLLAPSED_FILES,
+ },
props: {
endpoint: {
type: String,
@@ -111,6 +124,7 @@ export default {
return {
treeWidth,
diffFilesLength: 0,
+ collapsedWarningDismissed: false,
};
},
computed: {
@@ -139,7 +153,7 @@ export default {
'canMerge',
'hasConflicts',
]),
- ...mapGetters('diffs', ['isParallelView', 'currentDiffIndex']),
+ ...mapGetters('diffs', ['hasCollapsedFile', 'isParallelView', 'currentDiffIndex']),
...mapGetters(['isNotesFetched', 'getNoteableData']),
diffs() {
if (!this.viewDiffsFileByFile) {
@@ -169,6 +183,39 @@ export default {
isDiffHead() {
return parseBoolean(getParameterByName('diff_head'));
},
+ showFileByFileNavigation() {
+ return this.diffFiles.length > 1 && this.viewDiffsFileByFile;
+ },
+ currentFileNumber() {
+ return this.currentDiffIndex + 1;
+ },
+ previousFileNumber() {
+ const { currentDiffIndex } = this;
+
+ return currentDiffIndex >= 1 ? currentDiffIndex : null;
+ },
+ nextFileNumber() {
+ const { currentFileNumber, diffFiles } = this;
+
+ return currentFileNumber < diffFiles.length ? currentFileNumber + 1 : null;
+ },
+ visibleWarning() {
+ let visible = false;
+
+ if (this.renderOverflowWarning) {
+ visible = this.$options.alerts.ALERT_OVERFLOW_HIDDEN;
+ } else if (this.isDiffHead && this.hasConflicts) {
+ visible = this.$options.alerts.ALERT_MERGE_CONFLICT;
+ } else if (
+ this.hasCollapsedFile &&
+ !this.collapsedWarningDismissed &&
+ !this.viewDiffsFileByFile
+ ) {
+ visible = this.$options.alerts.ALERT_COLLAPSED_FILES;
+ }
+
+ return visible;
+ },
},
watch: {
commit(newCommit, oldCommit) {
@@ -186,7 +233,7 @@ export default {
}
},
diffViewType() {
- if (this.needsReload() || this.needsFirstLoad()) {
+ if (!this.glFeatures.unifiedDiffLines && (this.needsReload() || this.needsFirstLoad())) {
this.refetchDiffData();
}
this.adjustView();
@@ -212,7 +259,6 @@ export default {
projectPath: this.projectPath,
dismissEndpoint: this.dismissEndpoint,
showSuggestPopover: this.showSuggestPopover,
- useSingleDiffStyle: this.glFeatures.singleMrDiffView,
viewDiffsFileByFile: this.viewDiffsFileByFile,
});
@@ -262,7 +308,6 @@ export default {
...mapActions('diffs', [
'moveToNeighboringCommit',
'setBaseConfig',
- 'fetchDiffFiles',
'fetchDiffFilesMeta',
'fetchDiffFilesBatch',
'fetchCoverageFiles',
@@ -274,6 +319,9 @@ export default {
'toggleShowTreeList',
'navigateToDiffFileIndex',
]),
+ navigateToDiffFileNumber(number) {
+ this.navigateToDiffFileIndex(number - 1);
+ },
refetchDiffData() {
this.fetchData(false);
},
@@ -286,60 +334,35 @@ export default {
);
},
needsReload() {
- return (
- this.glFeatures.singleMrDiffView &&
- this.diffFiles.length &&
- isSingleViewStyle(this.diffFiles[0])
- );
+ return this.diffFiles.length && isSingleViewStyle(this.diffFiles[0]);
},
needsFirstLoad() {
- return this.glFeatures.singleMrDiffView && !this.diffFiles.length;
+ return !this.diffFiles.length;
},
fetchData(toggleTree = true) {
- if (this.glFeatures.diffsBatchLoad) {
- this.fetchDiffFilesMeta()
- .then(({ real_size }) => {
- this.diffFilesLength = parseInt(real_size, 10);
- if (toggleTree) this.hideTreeListIfJustOneFile();
+ this.fetchDiffFilesMeta()
+ .then(({ real_size }) => {
+ this.diffFilesLength = parseInt(real_size, 10);
+ if (toggleTree) this.hideTreeListIfJustOneFile();
- this.startDiffRendering();
- })
- .catch(() => {
- createFlash(__('Something went wrong on our end. Please try again!'));
- });
-
- this.fetchDiffFilesBatch()
- .then(() => {
- // Guarantee the discussions are assigned after the batch finishes.
- // Just watching the length of the discussions or the diff files
- // isn't enough, because with split diff loading, neither will
- // change when loading the other half of the diff files.
- this.setDiscussions();
- })
- .then(() => this.startDiffRendering())
- .catch(() => {
- createFlash(__('Something went wrong on our end. Please try again!'));
- });
- } else {
- this.fetchDiffFiles()
- .then(({ real_size }) => {
- this.diffFilesLength = parseInt(real_size, 10);
- if (toggleTree) {
- this.hideTreeListIfJustOneFile();
- }
+ this.startDiffRendering();
+ })
+ .catch(() => {
+ createFlash(__('Something went wrong on our end. Please try again!'));
+ });
- requestIdleCallback(
- () => {
- this.setDiscussions();
- this.startRenderDiffsQueue();
- },
- { timeout: 1000 },
- );
- })
- .catch(() => {
- createFlash(__('Something went wrong on our end. Please try again!'));
- });
- }
+ this.fetchDiffFilesBatch()
+ .then(() => {
+ // Guarantee the discussions are assigned after the batch finishes.
+ // Just watching the length of the discussions or the diff files
+ // isn't enough, because with split diff loading, neither will
+ // change when loading the other half of the diff files.
+ this.setDiscussions();
+ })
+ .then(() => this.startDiffRendering())
+ .catch(() => {
+ createFlash(__('Something went wrong on our end. Please try again!'));
+ });
if (this.endpointCoverage) {
this.fetchCoverageFiles();
@@ -406,6 +429,9 @@ export default {
this.toggleShowTreeList(false);
}
},
+ dismissCollapsedWarning() {
+ this.collapsedWarningDismissed = true;
+ },
},
minTreeWidth: MIN_TREE_WIDTH,
maxTreeWidth: MAX_TREE_WIDTH,
@@ -423,59 +449,27 @@ export default {
/>
<hidden-files-warning
- v-if="renderOverflowWarning"
+ v-if="visibleWarning == $options.alerts.ALERT_OVERFLOW_HIDDEN"
:visible="numVisibleFiles"
:total="numTotalFiles"
:plain-diff-path="plainDiffPath"
:email-patch-path="emailPatchPath"
/>
-
- <div
- v-if="isDiffHead && hasConflicts"
- :class="{
- [CENTERED_LIMITED_CONTAINER_CLASSES]: isLimitedContainer,
- }"
- >
- <gl-alert
- :dismissible="false"
- :title="__('There are merge conflicts')"
- variant="warning"
- class="w-100 mb-3"
- >
- <p class="mb-1">
- {{ __('The comparison view may be inaccurate due to merge conflicts.') }}
- </p>
- <p class="mb-0">
- {{
- __(
- 'Resolve these conflicts or ask someone with write access to this repository to merge it locally.',
- )
- }}
- </p>
- <template #actions>
- <gl-button
- v-if="conflictResolutionPath"
- :href="conflictResolutionPath"
- variant="info"
- class="mr-3 gl-alert-action"
- >
- {{ __('Resolve conflicts') }}
- </gl-button>
- <gl-button
- v-if="canMerge"
- class="gl-alert-action"
- data-toggle="modal"
- data-target="#modal_merge_info"
- >
- {{ __('Merge locally') }}
- </gl-button>
- </template>
- </gl-alert>
- </div>
+ <merge-conflict-warning
+ v-if="visibleWarning == $options.alerts.ALERT_MERGE_CONFLICT"
+ :limited="isLimitedContainer"
+ :resolution-path="conflictResolutionPath"
+ :mergeable="canMerge"
+ />
+ <collapsed-files-warning
+ v-if="visibleWarning == $options.alerts.ALERT_COLLAPSED_FILES"
+ :limited="isLimitedContainer"
+ @dismiss="dismissCollapsedWarning"
+ />
<div
:data-can-create-note="getNoteableData.current_user.can_create_note"
- class="files d-flex"
+ class="files d-flex gl-mt-2"
>
<div
v-if="showTreeList"
@@ -509,23 +503,22 @@ export default {
:can-current-user-fork="canCurrentUserFork"
:view-diffs-file-by-file="viewDiffsFileByFile"
/>
- <div v-if="viewDiffsFileByFile" class="d-flex gl-justify-content-center">
- <gl-button-group>
- <gl-button
- :disabled="currentDiffIndex === 0"
- data-testid="singleFilePrevious"
- @click="navigateToDiffFileIndex(currentDiffIndex - 1)"
- >
- {{ __('Prev') }}
- </gl-button>
- <gl-button
- :disabled="currentDiffIndex === diffFiles.length - 1"
- data-testid="singleFileNext"
- @click="navigateToDiffFileIndex(currentDiffIndex + 1)"
- >
- {{ __('Next') }}
- </gl-button>
- </gl-button-group>
+ <div
+ v-if="showFileByFileNavigation"
+ data-testid="file-by-file-navigation"
+ class="gl-display-grid gl-text-center"
+ >
+ <gl-pagination
+ class="gl-mx-auto"
+ :value="currentFileNumber"
+ :prev-page="previousFileNumber"
+ :next-page="nextFileNumber"
+ @input="navigateToDiffFileNumber"
+ />
+ <gl-sprintf :message="__('File %{current} of %{total}')">
+ <template #current>{{ currentFileNumber }}</template>
+ <template #total>{{ diffFiles.length }}</template>
+ </gl-sprintf>
</div>
</template>
<no-changes v-else :changes-empty-state-illustration="changesEmptyStateIllustration" />
diff --git a/app/assets/javascripts/diffs/components/collapsed_files_warning.vue b/app/assets/javascripts/diffs/components/collapsed_files_warning.vue
new file mode 100644
index 00000000000..dded3643115
--- /dev/null
+++ b/app/assets/javascripts/diffs/components/collapsed_files_warning.vue
@@ -0,0 +1,71 @@
+<script>
+import { mapActions } from 'vuex';
+
+import { GlAlert, GlButton } from '@gitlab/ui';
+
+import { CENTERED_LIMITED_CONTAINER_CLASSES } from '../constants';
+
+export default {
+ components: {
+ GlAlert,
+ GlButton,
+ },
+ props: {
+ limited: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ dismissed: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ },
+ data() {
+ return {
+ isDismissed: this.dismissed,
+ };
+ },
+ computed: {
+ containerClasses() {
+ return {
+ [CENTERED_LIMITED_CONTAINER_CLASSES]: this.limited,
+ };
+ },
+ },
+
+ methods: {
+ ...mapActions('diffs', ['expandAllFiles']),
+ dismiss() {
+ this.isDismissed = true;
+ this.$emit('dismiss');
+ },
+ expand() {
+ this.expandAllFiles();
+ this.dismiss();
+ },
+ },
+};
+</script>
+
+<template>
+ <div v-if="!isDismissed" data-testid="root" :class="containerClasses">
+ <gl-alert
+ :dismissible="true"
+ :title="__('Some changes are not shown')"
+ variant="warning"
+ class="gl-mb-5"
+ @dismiss="dismiss"
+ >
+ <p class="gl-mb-0">
+ {{ __('For a faster browsing experience, some files are collapsed by default.') }}
+ </p>
+ <template #actions>
+ <gl-button category="secondary" variant="warning" class="gl-alert-action" @click="expand">
+ {{ __('Expand all files') }}
+ </gl-button>
+ </template>
+ </gl-alert>
+ </div>
+</template>
diff --git a/app/assets/javascripts/diffs/components/commit_item.vue b/app/assets/javascripts/diffs/components/commit_item.vue
index 274a4027e62..23669eecce2 100644
--- a/app/assets/javascripts/diffs/components/commit_item.vue
+++ b/app/assets/javascripts/diffs/components/commit_item.vue
@@ -1,11 +1,11 @@
<script>
+/* eslint-disable vue/no-v-html */
import { mapActions } from 'vuex';
import { GlButtonGroup, GlButton, GlIcon, GlTooltipDirective } from '@gitlab/ui';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue';
-import Icon from '~/vue_shared/components/icon.vue';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
@@ -39,7 +39,6 @@ import { setUrlParams } from '../../lib/utils/url_utility';
export default {
components: {
UserAvatarLink,
- Icon,
ClipboardButton,
TimeAgoTooltip,
CommitPipelineStatus,
@@ -150,14 +149,13 @@ export default {
<span class="commit-row-message d-block d-sm-none">&middot; {{ commit.short_id }}</span>
- <button
+ <gl-button
v-if="commit.description_html && collapsible"
- class="text-expander js-toggle-button"
- type="button"
+ class="js-toggle-button"
+ size="small"
+ icon="ellipsis_h"
:aria-label="__('Toggle commit description')"
- >
- <icon :size="12" name="ellipsis_h" />
- </button>
+ />
<div class="committer">
<a
diff --git a/app/assets/javascripts/diffs/components/compare_dropdown_layout.vue b/app/assets/javascripts/diffs/components/compare_dropdown_layout.vue
index ed4edabd81c..8263e938e69 100644
--- a/app/assets/javascripts/diffs/components/compare_dropdown_layout.vue
+++ b/app/assets/javascripts/diffs/components/compare_dropdown_layout.vue
@@ -1,10 +1,10 @@
<script>
-import Icon from '~/vue_shared/components/icon.vue';
+import { GlIcon } from '@gitlab/ui';
import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue';
export default {
components: {
- Icon,
+ GlIcon,
TimeAgo,
},
props: {
@@ -29,7 +29,7 @@ export default {
aria-expanded="false"
>
<span> {{ selectedVersionName }} </span>
- <icon :size="12" name="angle-down" class="position-absolute" />
+ <gl-icon :size="12" name="angle-down" class="position-absolute" />
</a>
<div class="dropdown-menu dropdown-select dropdown-menu-selectable">
<div class="dropdown-content">
diff --git a/app/assets/javascripts/diffs/components/compare_versions.vue b/app/assets/javascripts/diffs/components/compare_versions.vue
index 35e4527af69..b94874c5644 100644
--- a/app/assets/javascripts/diffs/components/compare_versions.vue
+++ b/app/assets/javascripts/diffs/components/compare_versions.vue
@@ -1,9 +1,8 @@
<script>
import { mapActions, mapGetters, mapState } from 'vuex';
-import { GlTooltipDirective, GlLink, GlDeprecatedButton, GlSprintf } from '@gitlab/ui';
+import { GlTooltipDirective, GlLink, GlButton, GlSprintf } from '@gitlab/ui';
import { __ } from '~/locale';
import { polyfillSticky } from '~/lib/utils/sticky';
-import Icon from '~/vue_shared/components/icon.vue';
import CompareDropdownLayout from './compare_dropdown_layout.vue';
import SettingsDropdown from './settings_dropdown.vue';
import DiffStats from './diff_stats.vue';
@@ -12,9 +11,8 @@ import { CENTERED_LIMITED_CONTAINER_CLASSES } from '../constants';
export default {
components: {
CompareDropdownLayout,
- Icon,
GlLink,
- GlDeprecatedButton,
+ GlButton,
GlSprintf,
SettingsDropdown,
DiffStats,
@@ -84,18 +82,15 @@ export default {
[CENTERED_LIMITED_CONTAINER_CLASSES]: isLimitedContainer,
}"
>
- <button
+ <gl-button
v-gl-tooltip.hover
- type="button"
- class="btn btn-default gl-mr-3 js-toggle-tree-list"
- :class="{
- active: showTreeList,
- }"
+ variant="default"
+ icon="file-tree"
+ class="gl-mr-3 js-toggle-tree-list"
:title="toggleFileBrowserTitle"
+ :selected="showTreeList"
@click="toggleShowTreeList"
- >
- <icon name="file-tree" />
- </button>
+ />
<gl-sprintf
v-if="showDropdowns"
class="d-flex align-items-center compare-versions-container"
@@ -124,16 +119,22 @@ export default {
:added-lines="addedLines"
:removed-lines="removedLines"
/>
- <gl-deprecated-button
+ <gl-button
v-if="commit || startVersion"
:href="latestVersionPath"
+ variant="default"
class="gl-mr-3 js-latest-version"
>
{{ __('Show latest version') }}
- </gl-deprecated-button>
- <gl-deprecated-button v-show="hasCollapsedFile" class="gl-mr-3" @click="expandAllFiles">
+ </gl-button>
+ <gl-button
+ v-show="hasCollapsedFile"
+ variant="default"
+ class="gl-mr-3"
+ @click="expandAllFiles"
+ >
{{ __('Expand all') }}
- </gl-deprecated-button>
+ </gl-button>
<settings-dropdown />
</div>
</div>
diff --git a/app/assets/javascripts/diffs/components/diff_content.vue b/app/assets/javascripts/diffs/components/diff_content.vue
index 087a558efdc..9ecb9a44443 100644
--- a/app/assets/javascripts/diffs/components/diff_content.vue
+++ b/app/assets/javascripts/diffs/components/diff_content.vue
@@ -1,6 +1,7 @@
<script>
import { mapActions, mapGetters, mapState } from 'vuex';
import { GlLoadingIcon } from '@gitlab/ui';
+import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import diffLineNoteFormMixin from '~/notes/mixins/diff_line_note_form';
import draftCommentsMixin from '~/diffs/mixins/draft_comments';
import DiffViewer from '~/vue_shared/components/diff_viewer/diff_viewer.vue';
@@ -32,7 +33,7 @@ export default {
userAvatarLink,
DiffFileDrafts,
},
- mixins: [diffLineNoteFormMixin, draftCommentsMixin],
+ mixins: [diffLineNoteFormMixin, draftCommentsMixin, glFeatureFlagsMixin()],
props: {
diffFile: {
type: Object,
@@ -48,8 +49,12 @@ export default {
...mapState({
projectPath: state => state.diffs.projectPath,
}),
- ...mapGetters('diffs', ['isInlineView', 'isParallelView']),
- ...mapGetters('diffs', ['getCommentFormForDiffFile']),
+ ...mapGetters('diffs', [
+ 'isInlineView',
+ 'isParallelView',
+ 'getCommentFormForDiffFile',
+ 'diffLines',
+ ]),
...mapGetters(['getNoteableData', 'noteableType', 'getUserData']),
diffMode() {
return getDiffMode(this.diffFile);
@@ -114,13 +119,15 @@ export default {
<inline-diff-view
v-if="isInlineView"
:diff-file="diffFile"
- :diff-lines="diffFile.highlighted_diff_lines || []"
+ :diff-lines="diffFile.highlighted_diff_lines"
:help-page-path="helpPagePath"
/>
<parallel-diff-view
v-else-if="isParallelView"
:diff-file="diffFile"
- :diff-lines="diffFile.parallel_diff_lines || []"
+ :diff-lines="
+ glFeatures.unifiedDiffLines ? diffLines(diffFile) : diffFile.parallel_diff_lines || []
+ "
:help-page-path="helpPagePath"
/>
<gl-loading-icon v-if="diffFile.renderingLines" size="md" class="mt-3" />
diff --git a/app/assets/javascripts/diffs/components/diff_discussions.vue b/app/assets/javascripts/diffs/components/diff_discussions.vue
index b6a0724c201..7b55bd2104d 100644
--- a/app/assets/javascripts/diffs/components/diff_discussions.vue
+++ b/app/assets/javascripts/diffs/components/diff_discussions.vue
@@ -1,12 +1,12 @@
<script>
import { mapActions } from 'vuex';
-import Icon from '~/vue_shared/components/icon.vue';
+import { GlIcon } from '@gitlab/ui';
import noteableDiscussion from '../../notes/components/noteable_discussion.vue';
export default {
components: {
noteableDiscussion,
- Icon,
+ GlIcon,
},
props: {
discussions: {
@@ -70,7 +70,7 @@ export default {
class="js-diff-notes-toggle"
@click="toggleDiscussion({ discussionId: discussion.id })"
>
- <icon v-if="discussion.expanded" name="collapse" class="collapse-icon" />
+ <gl-icon v-if="discussion.expanded" name="collapse" class="collapse-icon" />
<template v-else>
{{ index + 1 }}
</template>
diff --git a/app/assets/javascripts/diffs/components/diff_expansion_cell.vue b/app/assets/javascripts/diffs/components/diff_expansion_cell.vue
index e5e63bdcb43..0094b4f8707 100644
--- a/app/assets/javascripts/diffs/components/diff_expansion_cell.vue
+++ b/app/assets/javascripts/diffs/components/diff_expansion_cell.vue
@@ -1,8 +1,9 @@
<script>
import { mapState, mapActions } from 'vuex';
+import { GlIcon } from '@gitlab/ui';
import { deprecatedCreateFlash as createFlash } from '~/flash';
-import { s__ } from '~/locale';
-import Icon from '~/vue_shared/components/icon.vue';
+import { s__, sprintf } from '~/locale';
+import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { UNFOLD_COUNT, INLINE_DIFF_VIEW_TYPE, PARALLEL_DIFF_VIEW_TYPE } from '../constants';
import * as utils from '../store/utils';
import tooltip from '../../vue_shared/directives/tooltip';
@@ -17,17 +18,23 @@ const lineNumberByViewType = (viewType, diffLine) => {
[PARALLEL_DIFF_VIEW_TYPE]: line => (line?.right || line?.left)?.new_line,
};
const numberGetter = numberGetters[viewType];
-
return numberGetter && numberGetter(diffLine);
};
+const i18n = {
+ showMore: sprintf(s__('Diffs|Show %{unfoldCount} lines'), { unfoldCount: UNFOLD_COUNT }),
+ showAll: s__('Diffs|Show all unchanged lines'),
+};
+
export default {
+ i18n,
directives: {
tooltip,
},
components: {
- Icon,
+ GlIcon,
},
+ mixins: [glFeatureFlagsMixin()],
props: {
fileHash: {
type: String,
@@ -59,7 +66,9 @@ export default {
},
computed: {
...mapState({
- diffViewType: state => state.diffs.diffViewType,
+ diffViewType(state) {
+ return this.glFeatures.unifiedDiffLines ? INLINE_DIFF_VIEW_TYPE : state.diffs.diffViewType;
+ },
diffFiles: state => state.diffs.diffFiles,
}),
canExpandUp() {
@@ -226,32 +235,27 @@ export default {
</script>
<template>
- <td :colspan="colspan" class="text-center">
+ <td :colspan="colspan" class="text-center gl-font-regular">
<div class="content js-line-expansion-content">
<a
- v-if="canExpandUp"
- v-tooltip
- class="cursor-pointer js-unfold unfold-icon d-inline-block pt-2 pb-2"
- data-placement="top"
- data-container="body"
- :title="__('Expand up')"
- @click="handleExpandLines(EXPAND_UP)"
+ v-if="canExpandDown"
+ class="gl-mx-2 gl-cursor-pointer js-unfold-down gl-display-inline-block gl-py-4"
+ @click="handleExpandLines(EXPAND_DOWN)"
>
- <icon :size="12" name="expand-up" aria-hidden="true" />
+ <gl-icon :size="12" name="expand-down" aria-hidden="true" />
+ <span>{{ $options.i18n.showMore }}</span>
</a>
- <a class="mx-2 cursor-pointer js-unfold-all" @click="handleExpandLines()">
- <span>{{ s__('Diffs|Show unchanged lines') }}</span>
+ <a class="gl-mx-2 cursor-pointer js-unfold-all" @click="handleExpandLines()">
+ <gl-icon :size="12" name="expand" aria-hidden="true" />
+ <span>{{ $options.i18n.showAll }}</span>
</a>
<a
- v-if="canExpandDown"
- v-tooltip
- class="cursor-pointer js-unfold-down has-tooltip unfold-icon d-inline-block pt-2 pb-2"
- data-placement="top"
- data-container="body"
- :title="__('Expand down')"
- @click="handleExpandLines(EXPAND_DOWN)"
+ v-if="canExpandUp"
+ class="gl-mx-2 gl-cursor-pointer js-unfold gl-display-inline-block gl-py-4"
+ @click="handleExpandLines(EXPAND_UP)"
>
- <icon :size="12" name="expand-down" aria-hidden="true" />
+ <gl-icon :size="12" name="expand-up" aria-hidden="true" />
+ <span>{{ $options.i18n.showMore }}</span>
</a>
</div>
</td>
diff --git a/app/assets/javascripts/diffs/components/diff_file.vue b/app/assets/javascripts/diffs/components/diff_file.vue
index eace673c2d7..9a7ed76bad3 100644
--- a/app/assets/javascripts/diffs/components/diff_file.vue
+++ b/app/assets/javascripts/diffs/components/diff_file.vue
@@ -1,23 +1,32 @@
<script>
import { mapActions, mapGetters, mapState } from 'vuex';
import { escape } from 'lodash';
-import { GlLoadingIcon } from '@gitlab/ui';
+import { GlButton, GlLoadingIcon, GlSafeHtmlDirective as SafeHtml } from '@gitlab/ui';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
-import { __, sprintf } from '~/locale';
+import { sprintf } from '~/locale';
import { deprecatedCreateFlash as createFlash } from '~/flash';
import { hasDiff } from '~/helpers/diffs_helper';
import eventHub from '../../notes/event_hub';
import DiffFileHeader from './diff_file_header.vue';
import DiffContent from './diff_content.vue';
import { diffViewerErrors } from '~/ide/constants';
+import { GENERIC_ERROR, DIFF_FILE } from '../i18n';
export default {
components: {
DiffFileHeader,
DiffContent,
+ GlButton,
GlLoadingIcon,
},
+ directives: {
+ SafeHtml,
+ },
mixins: [glFeatureFlagsMixin()],
+ i18n: {
+ genericError: GENERIC_ERROR,
+ ...DIFF_FILE,
+ },
props: {
file: {
type: Object,
@@ -50,7 +59,7 @@ export default {
...mapGetters('diffs', ['getDiffFileDiscussions']),
viewBlobLink() {
return sprintf(
- __('You can %{linkStart}view the blob%{linkEnd} instead.'),
+ this.$options.i18n.blobView,
{
linkStart: `<a href="${escape(this.file.view_path)}">`,
linkEnd: '</a>',
@@ -72,9 +81,7 @@ export default {
},
forkMessage() {
return sprintf(
- __(
- "You're not allowed to %{tag_start}edit%{tag_end} files in this project directly. Please fork this project, make your changes there, and submit a merge request.",
- ),
+ this.$options.i18n.editInFork,
{
tag_start: '<span class="js-file-fork-suggestion-section-action">',
tag_end: '</span>',
@@ -93,11 +100,7 @@ export default {
},
'file.file_hash': {
handler: function watchFileHash() {
- if (
- this.glFeatures.autoExpandCollapsedDiffs &&
- this.viewDiffsFileByFile &&
- this.file.viewer.collapsed
- ) {
+ if (this.viewDiffsFileByFile && this.file.viewer.collapsed) {
this.isCollapsed = false;
this.handleLoadCollapsedDiff();
} else {
@@ -107,7 +110,7 @@ export default {
immediate: true,
},
'file.viewer.collapsed': function setIsCollapsed(newVal) {
- if (!this.viewDiffsFileByFile && !this.glFeatures.autoExpandCollapsedDiffs) {
+ if (!this.viewDiffsFileByFile) {
this.isCollapsed = newVal;
}
},
@@ -149,7 +152,7 @@ export default {
})
.catch(() => {
this.isLoadingCollapsedDiff = false;
- createFlash(__('Something went wrong on our end. Please try again!'));
+ createFlash(this.$options.i18n.genericError);
});
},
showForkMessage() {
@@ -185,32 +188,38 @@ export default {
/>
<div v-if="forkMessageVisible" class="js-file-fork-suggestion-section file-fork-suggestion">
- <span class="file-fork-suggestion-note" v-html="forkMessage"></span>
+ <span v-safe-html="forkMessage" class="file-fork-suggestion-note"></span>
<a
:href="file.fork_path"
class="js-fork-suggestion-button btn btn-grouped btn-inverted btn-success"
- >{{ __('Fork') }}</a
+ >{{ $options.i18n.fork }}</a
>
<button
class="js-cancel-fork-suggestion-button btn btn-grouped"
type="button"
@click="hideForkMessage"
>
- {{ __('Cancel') }}
+ {{ $options.i18n.cancel }}
</button>
</div>
<gl-loading-icon v-if="showLoadingIcon" class="diff-content loading" />
<template v-else>
<div :id="`diff-content-${file.file_hash}`">
<div v-if="errorMessage" class="diff-viewer">
- <div class="nothing-here-block" v-html="errorMessage"></div>
+ <div v-safe-html="errorMessage" class="nothing-here-block"></div>
</div>
<template v-else>
- <div v-show="isCollapsed" class="nothing-here-block diff-collapsed">
- {{ __('This diff is collapsed.') }}
- <a class="click-to-expand js-click-to-expand" href="#" @click.prevent="handleToggle">{{
- __('Click to expand it.')
- }}</a>
+ <div v-show="isCollapsed" class="gl-p-7 gl-text-center collapsed-file-warning">
+ <p class="gl-mb-8 gl-mt-5">
+ {{ $options.i18n.collapsed }}
+ </p>
+ <gl-button
+ class="gl-alert-action gl-mb-5"
+ data-testid="expandButton"
+ @click="handleToggle"
+ >
+ {{ $options.i18n.expand }}
+ </gl-button>
</div>
<diff-content
v-show="!isCollapsed && !isFileTooLarge"
diff --git a/app/assets/javascripts/diffs/components/diff_file_header.vue b/app/assets/javascripts/diffs/components/diff_file_header.vue
index 5727fbaaf68..fded391cc84 100644
--- a/app/assets/javascripts/diffs/components/diff_file_header.vue
+++ b/app/assets/javascripts/diffs/components/diff_file_header.vue
@@ -1,9 +1,16 @@
<script>
+/* eslint-disable vue/no-v-html */
import { escape } from 'lodash';
import { mapActions, mapGetters } from 'vuex';
-import { GlDeprecatedButton, GlTooltipDirective, GlLoadingIcon } from '@gitlab/ui';
+import {
+ GlDeprecatedButton,
+ GlTooltipDirective,
+ GlSafeHtmlDirective,
+ GlLoadingIcon,
+ GlIcon,
+ GlButton,
+} from '@gitlab/ui';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
-import Icon from '~/vue_shared/components/icon.vue';
import FileIcon from '~/vue_shared/components/file_icon.vue';
import { truncateSha } from '~/lib/utils/text_utility';
import { __, s__, sprintf } from '~/locale';
@@ -18,12 +25,14 @@ export default {
GlDeprecatedButton,
ClipboardButton,
EditButton,
- Icon,
+ GlIcon,
FileIcon,
DiffStats,
+ GlButton,
},
directives: {
GlTooltip: GlTooltipDirective,
+ SafeHtml: GlSafeHtmlDirective,
},
props: {
discussionPath: {
@@ -77,6 +86,21 @@ export default {
return this.discussionPath;
},
+ submoduleDiffCompareLinkText() {
+ if (this.diffFile.submodule_compare) {
+ const truncatedOldSha = escape(truncateSha(this.diffFile.submodule_compare.old_sha));
+ const truncatedNewSha = escape(truncateSha(this.diffFile.submodule_compare.new_sha));
+ return sprintf(
+ s__('Compare %{oldCommitId}...%{newCommitId}'),
+ {
+ oldCommitId: `<span class="commit-sha">${truncatedOldSha}</span>`,
+ newCommitId: `<span class="commit-sha">${truncatedNewSha}</span>`,
+ },
+ false,
+ );
+ }
+ return null;
+ },
filePath() {
if (this.diffFile.submodule) {
return `${this.diffFile.file_path} @ ${truncateSha(this.diffFile.blob.id)}`;
@@ -133,6 +157,7 @@ export default {
'toggleFileDiscussions',
'toggleFileDiscussionWrappers',
'toggleFullDiff',
+ 'toggleActiveFileByHash',
]),
handleToggleFile() {
this.$emit('toggleFile');
@@ -149,6 +174,9 @@ export default {
const selector = this.diffContentIDSelector;
scrollToElement(document.querySelector(selector));
window.location.hash = selector;
+ if (!this.viewDiffsFileByFile) {
+ this.toggleActiveFileByHash(this.diffFile.file_hash);
+ }
}
},
},
@@ -162,7 +190,7 @@ export default {
@click.self="handleToggleFile"
>
<div class="file-header-content">
- <icon
+ <gl-icon
v-if="collapsible"
ref="collapseIcon"
:name="collapseIcon"
@@ -237,7 +265,7 @@ export default {
type="button"
@click="toggleFileDiscussionWrappers(diffFile)"
>
- <icon name="comment" />
+ <gl-icon name="comment" />
</gl-deprecated-button>
</span>
@@ -273,8 +301,8 @@ export default {
@click="toggleFullDiff(diffFile.file_path)"
>
<gl-loading-icon v-if="diffFile.isLoadingFullFile" color="dark" inline />
- <icon v-else-if="diffFile.isShowingFullFile" name="doc-changes" />
- <icon v-else name="doc-expand" />
+ <gl-icon v-else-if="diffFile.isShowingFullFile" name="doc-changes" />
+ <gl-icon v-else name="doc-expand" />
</gl-deprecated-button>
<gl-deprecated-button
ref="viewButton"
@@ -287,7 +315,7 @@ export default {
data-track-property="diff_toggle_view_sha"
:title="viewFileButtonText"
>
- <icon name="doc-text" />
+ <gl-icon name="doc-text" />
</gl-deprecated-button>
<a
@@ -303,9 +331,22 @@ export default {
data-track-property="diff_toggle_external"
class="btn btn-file-option"
>
- <icon name="external-link" />
+ <gl-icon name="external-link" />
</a>
</div>
</div>
+
+ <div
+ v-if="diffFile.submodule_compare"
+ class="file-actions d-none d-sm-flex align-items-center flex-wrap"
+ >
+ <gl-button
+ v-gl-tooltip.hover
+ v-safe-html="submoduleDiffCompareLinkText"
+ class="submodule-compare"
+ :title="s__('Compare submodule commit revisions')"
+ :href="diffFile.submodule_compare.url"
+ />
+ </div>
</div>
</template>
diff --git a/app/assets/javascripts/diffs/components/diff_file_row.vue b/app/assets/javascripts/diffs/components/diff_file_row.vue
index 43b669625f4..2856e6ae8eb 100644
--- a/app/assets/javascripts/diffs/components/diff_file_row.vue
+++ b/app/assets/javascripts/diffs/components/diff_file_row.vue
@@ -3,9 +3,10 @@
* This component is an iterative step towards refactoring and simplifying `vue_shared/components/file_row.vue`
* https://gitlab.com/gitlab-org/gitlab/-/merge_requests/23720
*/
+import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import FileRow from '~/vue_shared/components/file_row.vue';
-import FileRowStats from './file_row_stats.vue';
import ChangedFileIcon from '~/vue_shared/components/changed_file_icon.vue';
+import FileRowStats from './file_row_stats.vue';
export default {
name: 'DiffFileRow',
@@ -14,6 +15,7 @@ export default {
FileRowStats,
ChangedFileIcon,
},
+ mixins: [glFeatureFlagsMixin()],
props: {
file: {
type: Object,
@@ -28,11 +30,28 @@ export default {
required: false,
default: null,
},
+ viewedFiles: {
+ type: Object,
+ required: false,
+ default: () => ({}),
+ },
},
computed: {
showFileRowStats() {
return !this.hideFileStats && this.file.type === 'blob';
},
+ fileClasses() {
+ if (!this.glFeatures.highlightCurrentDiffRow) {
+ return '';
+ }
+
+ return this.file.type === 'blob' && !this.viewedFiles[this.file.fileHash]
+ ? 'gl-font-weight-bold'
+ : '';
+ },
+ isActive() {
+ return this.currentDiffFileId === this.file.fileHash;
+ },
},
};
</script>
@@ -41,8 +60,9 @@ export default {
<file-row
:file="file"
v-bind="$attrs"
- :class="{ 'is-active': currentDiffFileId === file.fileHash }"
+ :class="{ 'is-active': isActive }"
class="diff-file-row"
+ :file-classes="fileClasses"
v-on="$listeners"
>
<file-row-stats v-if="showFileRowStats" :file="file" class="mr-1" />
diff --git a/app/assets/javascripts/diffs/components/diff_gutter_avatars.vue b/app/assets/javascripts/diffs/components/diff_gutter_avatars.vue
index be19d8520b5..439319f487c 100644
--- a/app/assets/javascripts/diffs/components/diff_gutter_avatars.vue
+++ b/app/assets/javascripts/diffs/components/diff_gutter_avatars.vue
@@ -1,14 +1,13 @@
<script>
-import { GlTooltipDirective } from '@gitlab/ui';
+import { GlTooltipDirective, GlIcon } from '@gitlab/ui';
import { n__ } from '~/locale';
-import Icon from '~/vue_shared/components/icon.vue';
import { truncate } from '~/lib/utils/text_utility';
import UserAvatarImage from '~/vue_shared/components/user_avatar/user_avatar_image.vue';
import { COUNT_OF_AVATARS_IN_GUTTER, LENGTH_OF_AVATAR_TOOLTIP } from '../constants';
export default {
components: {
- Icon,
+ GlIcon,
UserAvatarImage,
},
directives: {
@@ -68,7 +67,7 @@ export default {
class="diff-notes-collapse js-diff-comment-avatar js-diff-comment-button"
@click="$emit('toggleLineDiscussions')"
>
- <icon :size="12" name="collapse" />
+ <gl-icon :size="12" name="collapse" />
</button>
<template v-else>
<user-avatar-image
diff --git a/app/assets/javascripts/diffs/components/diff_stats.vue b/app/assets/javascripts/diffs/components/diff_stats.vue
index 439d8097e56..05fbbd39fae 100644
--- a/app/assets/javascripts/diffs/components/diff_stats.vue
+++ b/app/assets/javascripts/diffs/components/diff_stats.vue
@@ -1,10 +1,10 @@
<script>
import { isNumber } from 'lodash';
-import Icon from '~/vue_shared/components/icon.vue';
+import { GlIcon } from '@gitlab/ui';
import { n__ } from '~/locale';
export default {
- components: { Icon },
+ components: { GlIcon },
props: {
addedLines: {
type: Number,
@@ -46,7 +46,7 @@ export default {
}"
>
<div v-if="hasDiffFiles" class="diff-stats-group">
- <icon name="doc-code" class="diff-stats-icon text-secondary" />
+ <gl-icon name="doc-code" class="diff-stats-icon text-secondary" />
<span class="text-secondary bold">{{ diffFilesCountText }} {{ filesText }}</span>
</div>
<div
diff --git a/app/assets/javascripts/diffs/components/edit_button.vue b/app/assets/javascripts/diffs/components/edit_button.vue
index 21fdb19287d..ff1af5569dc 100644
--- a/app/assets/javascripts/diffs/components/edit_button.vue
+++ b/app/assets/javascripts/diffs/components/edit_button.vue
@@ -1,12 +1,11 @@
<script>
-import { GlTooltipDirective, GlDeprecatedButton } from '@gitlab/ui';
+import { GlTooltipDirective, GlDeprecatedButton, GlIcon } from '@gitlab/ui';
import { __ } from '~/locale';
-import Icon from '~/vue_shared/components/icon.vue';
export default {
components: {
GlDeprecatedButton,
- Icon,
+ GlIcon,
},
directives: {
GlTooltip: GlTooltipDirective,
@@ -59,7 +58,7 @@ export default {
class="rounded-0 js-edit-blob"
@click.native="handleEditClick"
>
- <icon name="pencil" />
+ <gl-icon name="pencil" />
</gl-deprecated-button>
</span>
</template>
diff --git a/app/assets/javascripts/diffs/components/image_diff_overlay.vue b/app/assets/javascripts/diffs/components/image_diff_overlay.vue
index be7e6789216..3956c2fab49 100644
--- a/app/assets/javascripts/diffs/components/image_diff_overlay.vue
+++ b/app/assets/javascripts/diffs/components/image_diff_overlay.vue
@@ -2,12 +2,12 @@
import { mapActions, mapGetters } from 'vuex';
import { isArray } from 'lodash';
import imageDiffMixin from 'ee_else_ce/diffs/mixins/image_diff';
-import Icon from '~/vue_shared/components/icon.vue';
+import { GlIcon } from '@gitlab/ui';
export default {
name: 'ImageDiffOverlay',
components: {
- Icon,
+ GlIcon,
},
mixins: [imageDiffMixin],
props: {
@@ -112,7 +112,7 @@ export default {
type="button"
@click="clickedToggle(discussion)"
>
- <icon v-if="showCommentIcon" name="image-comment-dark" />
+ <gl-icon v-if="showCommentIcon" name="image-comment-dark" />
<template v-else>
{{ toggleText(discussion, index) }}
</template>
@@ -127,7 +127,7 @@ export default {
class="btn-transparent comment-indicator"
type="button"
>
- <icon name="image-comment-dark" />
+ <gl-icon name="image-comment-dark" />
</button>
</div>
</template>
diff --git a/app/assets/javascripts/diffs/components/inline_diff_table_row.vue b/app/assets/javascripts/diffs/components/inline_diff_table_row.vue
index 168e8c6c14e..7fab750089e 100644
--- a/app/assets/javascripts/diffs/components/inline_diff_table_row.vue
+++ b/app/assets/javascripts/diffs/components/inline_diff_table_row.vue
@@ -1,7 +1,6 @@
<script>
import { mapActions, mapGetters, mapState } from 'vuex';
-import { GlTooltipDirective } from '@gitlab/ui';
-import DiffTableCell from './diff_table_cell.vue';
+import { GlTooltipDirective, GlIcon, GlSafeHtmlDirective as SafeHtml } from '@gitlab/ui';
import {
MATCH_LINE_TYPE,
NEW_LINE_TYPE,
@@ -10,14 +9,23 @@ import {
CONTEXT_LINE_CLASS_NAME,
LINE_POSITION_LEFT,
LINE_POSITION_RIGHT,
+ LINE_HOVER_CLASS_NAME,
+ OLD_NO_NEW_LINE_TYPE,
+ NEW_NO_NEW_LINE_TYPE,
+ EMPTY_CELL_TYPE,
} from '../constants';
+import { __ } from '~/locale';
+import { getParameterByName, parseBoolean } from '~/lib/utils/common_utils';
+import DiffGutterAvatars from './diff_gutter_avatars.vue';
export default {
components: {
- DiffTableCell,
+ DiffGutterAvatars,
+ GlIcon,
},
directives: {
GlTooltip: GlTooltipDirective,
+ SafeHtml,
},
props: {
fileHash: {
@@ -49,6 +57,7 @@ export default {
};
},
computed: {
+ ...mapGetters(['isLoggedIn']),
...mapGetters('diffs', ['fileLineCoverage']),
...mapState({
isHighlighted(state) {
@@ -78,6 +87,70 @@ export default {
coverageState() {
return this.fileLineCoverage(this.filePath, this.line.new_line);
},
+ isMetaLine() {
+ const { type } = this.line;
+
+ return (
+ type === OLD_NO_NEW_LINE_TYPE || type === NEW_NO_NEW_LINE_TYPE || type === EMPTY_CELL_TYPE
+ );
+ },
+ classNameMapCell() {
+ const { type } = this.line;
+
+ return [
+ type,
+ {
+ hll: this.isHighlighted,
+ [LINE_HOVER_CLASS_NAME]:
+ this.isLoggedIn && this.isHover && !this.isContextLine && !this.isMetaLine,
+ },
+ ];
+ },
+ addCommentTooltip() {
+ const brokenSymlinks = this.line.commentsDisabled;
+ let tooltip = __('Add a comment to this line');
+
+ if (brokenSymlinks) {
+ if (brokenSymlinks.wasSymbolic || brokenSymlinks.isSymbolic) {
+ tooltip = __(
+ 'Commenting on symbolic links that replace or are replaced by files is currently not supported.',
+ );
+ } else if (brokenSymlinks.wasReal || brokenSymlinks.isReal) {
+ tooltip = __(
+ 'Commenting on files that replace or are replaced by symbolic links is currently not supported.',
+ );
+ }
+ }
+
+ return tooltip;
+ },
+ shouldRenderCommentButton() {
+ if (this.isLoggedIn) {
+ const isDiffHead = parseBoolean(getParameterByName('diff_head'));
+ return !isDiffHead || gon.features?.mergeRefHeadComments;
+ }
+
+ return false;
+ },
+ shouldShowCommentButton() {
+ return this.isHover && !this.isContextLine && !this.isMetaLine && !this.hasDiscussions;
+ },
+ hasDiscussions() {
+ return this.line.discussions && this.line.discussions.length > 0;
+ },
+ lineHref() {
+ return `#${this.line.line_code || ''}`;
+ },
+ lineCode() {
+ return (
+ this.line.line_code ||
+ (this.line.left && this.line.left.line_code) ||
+ (this.line.right && this.line.right.line_code)
+ );
+ },
+ shouldShowAvatarsOnGutter() {
+ return this.hasDiscussions;
+ },
},
created() {
this.newLineType = NEW_LINE_TYPE;
@@ -89,12 +162,20 @@ export default {
this.scrollToLineIfNeededInline(this.line);
},
methods: {
- ...mapActions('diffs', ['scrollToLineIfNeededInline']),
+ ...mapActions('diffs', [
+ 'scrollToLineIfNeededInline',
+ 'showCommentForm',
+ 'setHighlightedRow',
+ 'toggleLineDiscussions',
+ ]),
handleMouseMove(e) {
// To show the comment icon on the gutter we need to know if we hover the line.
// Current table structure doesn't allow us to do this with CSS in both of the diff view types
this.isHover = e.type === 'mouseover';
},
+ handleCommentButton() {
+ this.showCommentForm({ lineCode: this.line.line_code, fileHash: this.fileHash });
+ },
},
};
</script>
@@ -108,25 +189,52 @@ export default {
@mouseover="handleMouseMove"
@mouseout="handleMouseMove"
>
- <diff-table-cell
- :file-hash="fileHash"
- :line="line"
- :line-type="oldLineType"
- :is-bottom="isBottom"
- :is-hover="isHover"
- :show-comment-button="true"
- :is-highlighted="isHighlighted"
- class="diff-line-num old_line"
- />
- <diff-table-cell
- :file-hash="fileHash"
- :line="line"
- :line-type="newLineType"
- :is-bottom="isBottom"
- :is-hover="isHover"
- :is-highlighted="isHighlighted"
- class="diff-line-num new_line qa-new-diff-line"
- />
+ <td ref="oldTd" class="diff-line-num old_line" :class="classNameMapCell">
+ <span
+ v-if="shouldRenderCommentButton"
+ ref="addNoteTooltip"
+ v-gl-tooltip
+ class="add-diff-note tooltip-wrapper"
+ :title="addCommentTooltip"
+ >
+ <button
+ v-show="shouldShowCommentButton"
+ ref="addDiffNoteButton"
+ type="button"
+ class="add-diff-note note-button js-add-diff-note-button qa-diff-comment"
+ :disabled="line.commentsDisabled"
+ @click="handleCommentButton"
+ >
+ <gl-icon :size="12" name="comment" />
+ </button>
+ </span>
+ <a
+ v-if="line.old_line"
+ ref="lineNumberRefOld"
+ :data-linenumber="line.old_line"
+ :href="lineHref"
+ @click="setHighlightedRow(lineCode)"
+ >
+ </a>
+ <diff-gutter-avatars
+ v-if="shouldShowAvatarsOnGutter"
+ :discussions="line.discussions"
+ :discussions-expanded="line.discussionsExpanded"
+ @toggleLineDiscussions="
+ toggleLineDiscussions({ lineCode, fileHash, expanded: !line.discussionsExpanded })
+ "
+ />
+ </td>
+ <td ref="newTd" class="diff-line-num new_line qa-new-diff-line" :class="classNameMapCell">
+ <a
+ v-if="line.new_line"
+ ref="lineNumberRefNew"
+ :data-linenumber="line.new_line"
+ :href="lineHref"
+ @click="setHighlightedRow(lineCode)"
+ >
+ </a>
+ </td>
<td
v-gl-tooltip.hover
:title="coverageState.text"
@@ -134,6 +242,7 @@ export default {
class="line-coverage"
></td>
<td
+ v-safe-html="line.rich_text"
:class="[
line.type,
{
@@ -141,7 +250,6 @@ export default {
},
]"
class="line_content with-coverage"
- v-html="line.rich_text"
></td>
</tr>
</template>
diff --git a/app/assets/javascripts/diffs/components/inline_diff_view.vue b/app/assets/javascripts/diffs/components/inline_diff_view.vue
index e82d06ee385..13805910648 100644
--- a/app/assets/javascripts/diffs/components/inline_diff_view.vue
+++ b/app/assets/javascripts/diffs/components/inline_diff_view.vue
@@ -1,5 +1,6 @@
<script>
import { mapGetters, mapState } from 'vuex';
+import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import draftCommentsMixin from '~/diffs/mixins/draft_comments';
import InlineDraftCommentRow from '~/batch_comments/components/inline_draft_comment_row.vue';
import inlineDiffTableRow from './inline_diff_table_row.vue';
@@ -14,7 +15,7 @@ export default {
InlineDraftCommentRow,
inlineDiffExpansionRow,
},
- mixins: [draftCommentsMixin],
+ mixins: [draftCommentsMixin, glFeatureFlagsMixin()],
props: {
diffFile: {
type: Object,
diff --git a/app/assets/javascripts/diffs/components/merge_conflict_warning.vue b/app/assets/javascripts/diffs/components/merge_conflict_warning.vue
new file mode 100644
index 00000000000..e47bea8e589
--- /dev/null
+++ b/app/assets/javascripts/diffs/components/merge_conflict_warning.vue
@@ -0,0 +1,72 @@
+<script>
+import { GlButton, GlAlert } from '@gitlab/ui';
+import { CENTERED_LIMITED_CONTAINER_CLASSES } from '../constants';
+
+export default {
+ components: {
+ GlAlert,
+ GlButton,
+ },
+ props: {
+ limited: {
+ type: Boolean,
+ required: true,
+ },
+ mergeable: {
+ type: Boolean,
+ required: true,
+ },
+ resolutionPath: {
+ type: String,
+ required: true,
+ },
+ },
+ computed: {
+ containerClasses() {
+ return {
+ [CENTERED_LIMITED_CONTAINER_CLASSES]: this.limited,
+ };
+ },
+ },
+};
+</script>
+
+<template>
+ <div :class="containerClasses">
+ <gl-alert
+ :dismissible="false"
+ :title="__('There are merge conflicts')"
+ variant="warning"
+ class="gl-mb-5"
+ >
+ <p class="gl-mb-2">
+ {{ __('The comparison view may be inaccurate due to merge conflicts.') }}
+ </p>
+ <p class="gl-mb-0">
+ {{
+ __(
+ 'Resolve these conflicts or ask someone with write access to this repository to merge it locally.',
+ )
+ }}
+ </p>
+ <template #actions>
+ <gl-button
+ v-if="resolutionPath"
+ :href="resolutionPath"
+ variant="info"
+ class="gl-mr-5 gl-alert-action"
+ >
+ {{ __('Resolve conflicts') }}
+ </gl-button>
+ <gl-button
+ v-if="mergeable"
+ class="gl-alert-action"
+ data-toggle="modal"
+ data-target="#modal_merge_info"
+ >
+ {{ __('Merge locally') }}
+ </gl-button>
+ </template>
+ </gl-alert>
+ </div>
+</template>
diff --git a/app/assets/javascripts/diffs/components/no_changes.vue b/app/assets/javascripts/diffs/components/no_changes.vue
index 93afa978862..a640dcb0a90 100644
--- a/app/assets/javascripts/diffs/components/no_changes.vue
+++ b/app/assets/javascripts/diffs/components/no_changes.vue
@@ -1,12 +1,11 @@
<script>
import { mapGetters } from 'vuex';
-import { escape } from 'lodash';
-import { GlButton } from '@gitlab/ui';
-import { __, sprintf } from '~/locale';
+import { GlButton, GlSprintf } from '@gitlab/ui';
export default {
components: {
GlButton,
+ GlSprintf,
},
props: {
changesEmptyStateIllustration: {
@@ -16,20 +15,6 @@ export default {
},
computed: {
...mapGetters(['getNoteableData']),
- emptyStateText() {
- return sprintf(
- __(
- 'No changes between %{ref_start}%{source_branch}%{ref_end} and %{ref_start}%{target_branch}%{ref_end}',
- ),
- {
- ref_start: '<span class="ref-name">',
- ref_end: '</span>',
- source_branch: escape(this.getNoteableData.source_branch),
- target_branch: escape(this.getNoteableData.target_branch),
- },
- false,
- );
- },
},
};
</script>
@@ -41,7 +26,14 @@ export default {
</div>
<div class="col-12">
<div class="text-content text-center">
- <span v-html="emptyStateText"></span>
+ <gl-sprintf :message="__('No changes between %{sourceBranch} and %{targetBranch}')">
+ <template #sourceBranch>
+ <span class="ref-name">{{ getNoteableData.source_branch }}</span>
+ </template>
+ <template #targetBranch>
+ <span class="ref-name">{{ getNoteableData.target_branch }}</span>
+ </template>
+ </gl-sprintf>
<div class="text-center">
<gl-button :href="getNoteableData.new_blob_path" variant="success" category="primary">{{
__('Create commit')
diff --git a/app/assets/javascripts/diffs/components/parallel_diff_table_row.vue b/app/assets/javascripts/diffs/components/parallel_diff_table_row.vue
index ccb32a2a745..0bf47dc77a6 100644
--- a/app/assets/javascripts/diffs/components/parallel_diff_table_row.vue
+++ b/app/assets/javascripts/diffs/components/parallel_diff_table_row.vue
@@ -1,8 +1,7 @@
<script>
import { mapActions, mapGetters, mapState } from 'vuex';
import $ from 'jquery';
-import { GlTooltipDirective } from '@gitlab/ui';
-import DiffTableCell from './diff_table_cell.vue';
+import { GlTooltipDirective, GlIcon, GlSafeHtmlDirective as SafeHtml } from '@gitlab/ui';
import {
MATCH_LINE_TYPE,
NEW_LINE_TYPE,
@@ -13,14 +12,20 @@ import {
PARALLEL_DIFF_VIEW_TYPE,
NEW_NO_NEW_LINE_TYPE,
EMPTY_CELL_TYPE,
+ LINE_HOVER_CLASS_NAME,
} from '../constants';
+import { __ } from '~/locale';
+import { getParameterByName, parseBoolean } from '~/lib/utils/common_utils';
+import DiffGutterAvatars from './diff_gutter_avatars.vue';
export default {
components: {
- DiffTableCell,
+ GlIcon,
+ DiffGutterAvatars,
},
directives: {
GlTooltip: GlTooltipDirective,
+ SafeHtml,
},
props: {
fileHash: {
@@ -50,10 +55,12 @@ export default {
return {
isLeftHover: false,
isRightHover: false,
+ isCommentButtonRendered: false,
};
},
computed: {
...mapGetters('diffs', ['fileLineCoverage']),
+ ...mapGetters(['isLoggedIn']),
...mapState({
isHighlighted(state) {
if (this.isCommented) return true;
@@ -65,12 +72,15 @@ export default {
return lineCode ? lineCode === state.diffs.highlightedRow : false;
},
}),
- isContextLine() {
+ isContextLineLeft() {
return this.line.left && this.line.left.type === CONTEXT_LINE_TYPE;
},
+ isContextLineRight() {
+ return this.line.right && this.line.right.type === CONTEXT_LINE_TYPE;
+ },
classNameMap() {
return {
- [CONTEXT_LINE_CLASS_NAME]: this.isContextLine,
+ [CONTEXT_LINE_CLASS_NAME]: this.isContextLineLeft,
[PARALLEL_DIFF_VIEW_TYPE]: true,
};
},
@@ -97,6 +107,129 @@ export default {
coverageState() {
return this.fileLineCoverage(this.filePath, this.line.right.new_line);
},
+ classNameMapCellLeft() {
+ const { type } = this.line.left;
+
+ return [
+ type,
+ {
+ hll: this.isHighlighted,
+ [LINE_HOVER_CLASS_NAME]:
+ this.isLoggedIn && this.isLeftHover && !this.isContextLineLeft && !this.isMetaLineLeft,
+ },
+ ];
+ },
+ classNameMapCellRight() {
+ const { type } = this.line.right;
+
+ return [
+ type,
+ {
+ hll: this.isHighlighted,
+ [LINE_HOVER_CLASS_NAME]:
+ this.isLoggedIn &&
+ this.isRightHover &&
+ !this.isContextLineRight &&
+ !this.isMetaLineRight,
+ },
+ ];
+ },
+ addCommentTooltipLeft() {
+ const brokenSymlinks = this.line.left.commentsDisabled;
+ let tooltip = __('Add a comment to this line');
+
+ if (brokenSymlinks) {
+ if (brokenSymlinks.wasSymbolic || brokenSymlinks.isSymbolic) {
+ tooltip = __(
+ 'Commenting on symbolic links that replace or are replaced by files is currently not supported.',
+ );
+ } else if (brokenSymlinks.wasReal || brokenSymlinks.isReal) {
+ tooltip = __(
+ 'Commenting on files that replace or are replaced by symbolic links is currently not supported.',
+ );
+ }
+ }
+
+ return tooltip;
+ },
+ addCommentTooltipRight() {
+ const brokenSymlinks = this.line.right.commentsDisabled;
+ let tooltip = __('Add a comment to this line');
+
+ if (brokenSymlinks) {
+ if (brokenSymlinks.wasSymbolic || brokenSymlinks.isSymbolic) {
+ tooltip = __(
+ 'Commenting on symbolic links that replace or are replaced by files is currently not supported.',
+ );
+ } else if (brokenSymlinks.wasReal || brokenSymlinks.isReal) {
+ tooltip = __(
+ 'Commenting on files that replace or are replaced by symbolic links is currently not supported.',
+ );
+ }
+ }
+
+ return tooltip;
+ },
+ shouldRenderCommentButton() {
+ if (!this.isCommentButtonRendered) {
+ return false;
+ }
+
+ if (this.isLoggedIn) {
+ const isDiffHead = parseBoolean(getParameterByName('diff_head'));
+ return !isDiffHead || gon.features?.mergeRefHeadComments;
+ }
+
+ return false;
+ },
+ shouldShowCommentButtonLeft() {
+ return (
+ this.isLeftHover &&
+ !this.isContextLineLeft &&
+ !this.isMetaLineLeft &&
+ !this.hasDiscussionsLeft
+ );
+ },
+ shouldShowCommentButtonRight() {
+ return (
+ this.isRightHover &&
+ !this.isContextLineRight &&
+ !this.isMetaLineRight &&
+ !this.hasDiscussionsRight
+ );
+ },
+ hasDiscussionsLeft() {
+ return this.line.left?.discussions?.length > 0;
+ },
+ hasDiscussionsRight() {
+ return this.line.right?.discussions?.length > 0;
+ },
+ lineHrefOld() {
+ return `#${this.line.left.line_code || ''}`;
+ },
+ lineHrefNew() {
+ return `#${this.line.right.line_code || ''}`;
+ },
+ lineCode() {
+ return (
+ (this.line.left && this.line.left.line_code) ||
+ (this.line.right && this.line.right.line_code)
+ );
+ },
+ isMetaLineLeft() {
+ const type = this.line.left?.type;
+
+ return (
+ type === OLD_NO_NEW_LINE_TYPE || type === NEW_NO_NEW_LINE_TYPE || type === EMPTY_CELL_TYPE
+ );
+ },
+ isMetaLineRight() {
+ const type = this.line.right?.type;
+
+ return (
+ type === OLD_NO_NEW_LINE_TYPE || type === NEW_NO_NEW_LINE_TYPE || type === EMPTY_CELL_TYPE
+ );
+ },
},
created() {
this.newLineType = NEW_LINE_TYPE;
@@ -105,9 +238,26 @@ export default {
},
mounted() {
this.scrollToLineIfNeededParallel(this.line);
+ this.unwatchShouldShowCommentButton = this.$watch(
+ vm => [vm.shouldShowCommentButtonLeft, vm.shouldShowCommentButtonRight].join(),
+ newVal => {
+ if (newVal) {
+ this.isCommentButtonRendered = true;
+ this.unwatchShouldShowCommentButton();
+ }
+ },
+ );
+ },
+ beforeDestroy() {
+ this.unwatchShouldShowCommentButton();
},
methods: {
- ...mapActions('diffs', ['scrollToLineIfNeededParallel']),
+ ...mapActions('diffs', [
+ 'scrollToLineIfNeededParallel',
+ 'showCommentForm',
+ 'setHighlightedRow',
+ 'toggleLineDiscussions',
+ ]),
handleMouseMove(e) {
const isHover = e.type === 'mouseover';
const hoveringCell = e.target.closest('td');
@@ -133,6 +283,9 @@ export default {
table.addClass(`${lineClass}-selected`);
}
},
+ handleCommentButton(line) {
+ this.showCommentForm({ lineCode: line.line_code, fileHash: this.fileHash });
+ },
},
};
</script>
@@ -145,25 +298,53 @@ export default {
@mouseout="handleMouseMove"
>
<template v-if="line.left && !isMatchLineLeft">
- <diff-table-cell
- :file-hash="fileHash"
- :line="line.left"
- :line-type="oldLineType"
- :is-bottom="isBottom"
- :is-hover="isLeftHover"
- :is-highlighted="isHighlighted"
- :show-comment-button="true"
- :diff-view-type="parallelDiffViewType"
- line-position="left"
- class="diff-line-num old_line"
- />
+ <td ref="oldTd" :class="classNameMapCellLeft" class="diff-line-num old_line">
+ <span
+ v-if="shouldRenderCommentButton"
+ ref="addNoteTooltipLeft"
+ v-gl-tooltip
+ class="add-diff-note tooltip-wrapper"
+ :title="addCommentTooltipLeft"
+ >
+ <button
+ v-show="shouldShowCommentButtonLeft"
+ ref="addDiffNoteButtonLeft"
+ type="button"
+ class="add-diff-note note-button js-add-diff-note-button qa-diff-comment"
+ :disabled="line.left.commentsDisabled"
+ @click="handleCommentButton(line.left)"
+ >
+ <gl-icon :size="12" name="comment" />
+ </button>
+ </span>
+ <a
+ v-if="line.left.old_line"
+ ref="lineNumberRefOld"
+ :data-linenumber="line.left.old_line"
+ :href="lineHrefOld"
+ @click="setHighlightedRow(lineCode)"
+ >
+ </a>
+ <diff-gutter-avatars
+ v-if="hasDiscussionsLeft"
+ :discussions="line.left.discussions"
+ :discussions-expanded="line.left.discussionsExpanded"
+ @toggleLineDiscussions="
+ toggleLineDiscussions({
+ lineCode: line.left.line_code,
+ fileHash,
+ expanded: !line.left.discussionsExpanded,
+ })
+ "
+ />
+ </td>
<td :class="parallelViewLeftLineType" class="line-coverage left-side"></td>
<td
:id="line.left.line_code"
+ v-safe-html="line.left.rich_text"
:class="parallelViewLeftLineType"
class="line_content with-coverage parallel left-side"
@mousedown="handleParallelLineMouseDown"
- v-html="line.left.rich_text"
></td>
</template>
<template v-else>
@@ -172,18 +353,46 @@ export default {
<td class="line_content with-coverage parallel left-side empty-cell"></td>
</template>
<template v-if="line.right && !isMatchLineRight">
- <diff-table-cell
- :file-hash="fileHash"
- :line="line.right"
- :line-type="newLineType"
- :is-bottom="isBottom"
- :is-hover="isRightHover"
- :is-highlighted="isHighlighted"
- :show-comment-button="true"
- :diff-view-type="parallelDiffViewType"
- line-position="right"
- class="diff-line-num new_line"
- />
+ <td ref="newTd" :class="classNameMapCellRight" class="diff-line-num new_line">
+ <span
+ v-if="shouldRenderCommentButton"
+ ref="addNoteTooltipRight"
+ v-gl-tooltip
+ class="add-diff-note tooltip-wrapper"
+ :title="addCommentTooltipRight"
+ >
+ <button
+ v-show="shouldShowCommentButtonRight"
+ ref="addDiffNoteButtonRight"
+ type="button"
+ class="add-diff-note note-button js-add-diff-note-button qa-diff-comment"
+ :disabled="line.right.commentsDisabled"
+ @click="handleCommentButton(line.right)"
+ >
+ <gl-icon :size="12" name="comment" />
+ </button>
+ </span>
+ <a
+ v-if="line.right.new_line"
+ ref="lineNumberRefNew"
+ :data-linenumber="line.right.new_line"
+ :href="lineHrefNew"
+ @click="setHighlightedRow(lineCode)"
+ >
+ </a>
+ <diff-gutter-avatars
+ v-if="hasDiscussionsRight"
+ :discussions="line.right.discussions"
+ :discussions-expanded="line.right.discussionsExpanded"
+ @toggleLineDiscussions="
+ toggleLineDiscussions({
+ lineCode: line.right.line_code,
+ fileHash,
+ expanded: !line.right.discussionsExpanded,
+ })
+ "
+ />
+ </td>
<td
v-gl-tooltip.hover
:title="coverageState.text"
@@ -192,6 +401,7 @@ export default {
></td>
<td
:id="line.right.line_code"
+ v-safe-html="line.right.rich_text"
:class="[
line.right.type,
{
@@ -200,7 +410,6 @@ export default {
]"
class="line_content with-coverage parallel right-side"
@mousedown="handleParallelLineMouseDown"
- v-html="line.right.rich_text"
></td>
</template>
<template v-else>
diff --git a/app/assets/javascripts/diffs/components/settings_dropdown.vue b/app/assets/javascripts/diffs/components/settings_dropdown.vue
index c45de481a17..78647065c8e 100644
--- a/app/assets/javascripts/diffs/components/settings_dropdown.vue
+++ b/app/assets/javascripts/diffs/components/settings_dropdown.vue
@@ -1,17 +1,24 @@
<script>
import { mapActions, mapGetters, mapState } from 'vuex';
-import { GlDeprecatedButton } from '@gitlab/ui';
-import Icon from '~/vue_shared/components/icon.vue';
+import { GlButtonGroup, GlButton, GlDropdown } from '@gitlab/ui';
+import { __ } from '~/locale';
export default {
components: {
- GlDeprecatedButton,
- Icon,
+ GlButtonGroup,
+ GlButton,
+ GlDropdown,
},
computed: {
...mapGetters('diffs', ['isInlineView', 'isParallelView']),
...mapState('diffs', ['renderTreeList', 'showWhitespace']),
},
+ mounted() {
+ this.patchAriaLabel();
+ },
+ updated() {
+ this.patchAriaLabel();
+ },
methods: {
...mapActions('diffs', [
'setInlineDiffViewType',
@@ -19,74 +26,69 @@ export default {
'setRenderTreeList',
'setShowWhitespace',
]),
+ patchAriaLabel() {
+ this.$el
+ .querySelector('.js-show-diff-settings')
+ .setAttribute('aria-label', __('Diff view settings'));
+ },
},
};
</script>
<template>
- <div class="dropdown">
- <button
- type="button"
- class="btn btn-default js-show-diff-settings"
- data-toggle="dropdown"
- data-display="static"
- >
- <icon name="settings" /> <icon name="chevron-down" />
- </button>
- <div class="dropdown-menu dropdown-menu-right p-2 pt-3 pb-3">
- <div>
- <span class="bold d-block mb-1">{{ __('File browser') }}</span>
- <div class="btn-group d-flex">
- <gl-deprecated-button
- :class="{ active: !renderTreeList }"
- class="w-100 js-list-view"
- @click="setRenderTreeList(false)"
- >
- {{ __('List view') }}
- </gl-deprecated-button>
- <gl-deprecated-button
- :class="{ active: renderTreeList }"
- class="w-100 js-tree-view"
- @click="setRenderTreeList(true)"
- >
- {{ __('Tree view') }}
- </gl-deprecated-button>
- </div>
- </div>
- <div class="mt-2">
- <span class="bold d-block mb-1">{{ __('Compare changes') }}</span>
- <div class="btn-group d-flex js-diff-view-buttons">
- <gl-deprecated-button
- id="inline-diff-btn"
- :class="{ active: isInlineView }"
- class="w-100 js-inline-diff-button"
- data-view-type="inline"
- @click="setInlineDiffViewType"
- >
- {{ __('Inline') }}
- </gl-deprecated-button>
- <gl-deprecated-button
- id="parallel-diff-btn"
- :class="{ active: isParallelView }"
- class="w-100 js-parallel-diff-button"
- data-view-type="parallel"
- @click="setParallelDiffViewType"
- >
- {{ __('Side-by-side') }}
- </gl-deprecated-button>
- </div>
- </div>
- <div class="mt-2">
- <label class="mb-0">
- <input
- id="show-whitespace"
- type="checkbox"
- :checked="showWhitespace"
- @change="setShowWhitespace({ showWhitespace: $event.target.checked, pushState: true })"
- />
- {{ __('Show whitespace changes') }}
- </label>
- </div>
+ <gl-dropdown icon="settings" toggle-class="js-show-diff-settings" right>
+ <div class="gl-px-3">
+ <span class="gl-font-weight-bold gl-display-block gl-mb-2">{{ __('File browser') }}</span>
+ <gl-button-group class="gl-display-flex">
+ <gl-button
+ :class="{ selected: !renderTreeList }"
+ class="gl-w-half js-list-view"
+ @click="setRenderTreeList(false)"
+ >
+ {{ __('List view') }}
+ </gl-button>
+ <gl-button
+ :class="{ selected: renderTreeList }"
+ class="gl-w-half js-tree-view"
+ @click="setRenderTreeList(true)"
+ >
+ {{ __('Tree view') }}
+ </gl-button>
+ </gl-button-group>
+ </div>
+ <div class="gl-mt-3 gl-px-3">
+ <span class="gl-font-weight-bold gl-display-block gl-mb-2">{{ __('Compare changes') }}</span>
+ <gl-button-group class="gl-display-flex js-diff-view-buttons">
+ <gl-button
+ id="inline-diff-btn"
+ :class="{ selected: isInlineView }"
+ class="gl-w-half js-inline-diff-button"
+ data-view-type="inline"
+ @click="setInlineDiffViewType"
+ >
+ {{ __('Inline') }}
+ </gl-button>
+ <gl-button
+ id="parallel-diff-btn"
+ :class="{ selected: isParallelView }"
+ class="gl-w-half js-parallel-diff-button"
+ data-view-type="parallel"
+ @click="setParallelDiffViewType"
+ >
+ {{ __('Side-by-side') }}
+ </gl-button>
+ </gl-button-group>
+ </div>
+ <div class="gl-mt-3 gl-px-3">
+ <label class="gl-mb-0">
+ <input
+ id="show-whitespace"
+ type="checkbox"
+ :checked="showWhitespace"
+ @change="setShowWhitespace({ showWhitespace: $event.target.checked, pushState: true })"
+ />
+ {{ __('Show whitespace changes') }}
+ </label>
</div>
- </div>
+ </gl-dropdown>
</template>
diff --git a/app/assets/javascripts/diffs/components/tree_list.vue b/app/assets/javascripts/diffs/components/tree_list.vue
index 38fbd8e61d4..d03d450b12d 100644
--- a/app/assets/javascripts/diffs/components/tree_list.vue
+++ b/app/assets/javascripts/diffs/components/tree_list.vue
@@ -1,8 +1,7 @@
<script>
import { mapActions, mapGetters, mapState } from 'vuex';
-import { GlTooltipDirective } from '@gitlab/ui';
+import { GlTooltipDirective, GlIcon } from '@gitlab/ui';
import { s__, sprintf } from '~/locale';
-import Icon from '~/vue_shared/components/icon.vue';
import FileTree from '~/vue_shared/components/file_tree.vue';
import DiffFileRow from './diff_file_row.vue';
@@ -11,7 +10,7 @@ export default {
GlTooltip: GlTooltipDirective,
},
components: {
- Icon,
+ GlIcon,
FileTree,
},
props: {
@@ -26,7 +25,7 @@ export default {
};
},
computed: {
- ...mapState('diffs', ['tree', 'renderTreeList', 'currentDiffFileId']),
+ ...mapState('diffs', ['tree', 'renderTreeList', 'currentDiffFileId', 'viewedDiffFileIds']),
...mapGetters('diffs', ['allBlobs']),
filteredTreeList() {
const search = this.search.toLowerCase().trim();
@@ -66,7 +65,7 @@ export default {
<div class="tree-list-holder d-flex flex-column">
<div class="gl-mb-3 position-relative tree-list-search d-flex">
<div class="flex-fill d-flex">
- <icon name="search" class="position-absolute tree-list-icon" />
+ <gl-icon name="search" class="position-absolute tree-list-icon" />
<label for="diff-tree-search" class="sr-only">{{ $options.searchPlaceholder }}</label>
<input
id="diff-tree-search"
@@ -83,7 +82,7 @@ export default {
class="position-absolute bg-transparent tree-list-icon tree-list-clear-icon border-0 p-0"
@click="clearSearch"
>
- <icon name="close" />
+ <gl-icon name="close" />
</button>
</div>
</div>
@@ -94,6 +93,7 @@ export default {
:key="file.key"
:file="file"
:level="0"
+ :viewed-files="viewedDiffFileIds"
:hide-file-stats="hideFileStats"
:file-row-component="$options.DiffFileRow"
:current-diff-file-id="currentDiffFileId"
diff --git a/app/assets/javascripts/diffs/constants.js b/app/assets/javascripts/diffs/constants.js
index 447136036ee..dc97d9993da 100644
--- a/app/assets/javascripts/diffs/constants.js
+++ b/app/assets/javascripts/diffs/constants.js
@@ -34,7 +34,6 @@ export const COUNT_OF_AVATARS_IN_GUTTER = 3;
export const LENGTH_OF_AVATAR_TOOLTIP = 17;
export const LINES_TO_BE_RENDERED_DIRECTLY = 100;
-export const MAX_LINES_TO_BE_RENDERED = 2000;
export const DIFF_FILE_SYMLINK_MODE = '120000';
export const DIFF_FILE_DELETED_MODE = '0';
@@ -69,6 +68,11 @@ export const DIFFS_PER_PAGE = 20;
export const DIFF_COMPARE_BASE_VERSION_INDEX = -1;
export const DIFF_COMPARE_HEAD_VERSION_INDEX = -2;
+// Diff View Alerts
+export const ALERT_OVERFLOW_HIDDEN = 'overflow';
+export const ALERT_MERGE_CONFLICT = 'merge-conflict';
+export const ALERT_COLLAPSED_FILES = 'collapsed';
+
// State machine states
export const STATE_IDLING = 'idle';
export const STATE_LOADING = 'loading';
diff --git a/app/assets/javascripts/diffs/diff_file.js b/app/assets/javascripts/diffs/diff_file.js
index 717c4a79ef9..610b71235d9 100644
--- a/app/assets/javascripts/diffs/diff_file.js
+++ b/app/assets/javascripts/diffs/diff_file.js
@@ -18,7 +18,6 @@ function fileSymlinkInformation(file, fileList) {
);
}
-/* eslint-disable-next-line import/prefer-default-export */
export function prepareRawDiffFile({ file, allFiles }) {
Object.assign(file, {
brokenSymlink: fileSymlinkInformation(file, allFiles),
diff --git a/app/assets/javascripts/diffs/i18n.js b/app/assets/javascripts/diffs/i18n.js
new file mode 100644
index 00000000000..8b91543587c
--- /dev/null
+++ b/app/assets/javascripts/diffs/i18n.js
@@ -0,0 +1,14 @@
+import { __ } from '~/locale';
+
+export const GENERIC_ERROR = __('Something went wrong on our end. Please try again!');
+
+export const DIFF_FILE = {
+ blobView: __('You can %{linkStart}view the blob%{linkEnd} instead.'),
+ editInFork: __(
+ "You're not allowed to %{tag_start}edit%{tag_end} files in this project directly. Please fork this project, make your changes there, and submit a merge request.",
+ ),
+ fork: __('Fork'),
+ cancel: __('Cancel'),
+ collapsed: __('This file is collapsed.'),
+ expand: __('Expand file'),
+};
diff --git a/app/assets/javascripts/diffs/store/actions.js b/app/assets/javascripts/diffs/store/actions.js
index d5581474c9b..0f275f1cb3e 100644
--- a/app/assets/javascripts/diffs/store/actions.js
+++ b/app/assets/javascripts/diffs/store/actions.js
@@ -52,7 +52,6 @@ export const setBaseConfig = ({ commit }, options) => {
projectPath,
dismissEndpoint,
showSuggestPopover,
- useSingleDiffStyle,
} = options;
commit(types.SET_BASE_CONFIG, {
endpoint,
@@ -62,61 +61,18 @@ export const setBaseConfig = ({ commit }, options) => {
projectPath,
dismissEndpoint,
showSuggestPopover,
- useSingleDiffStyle,
});
};
-export const fetchDiffFiles = ({ state, commit }) => {
- const worker = new TreeWorker();
- const urlParams = {
- w: state.showWhitespace ? '0' : '1',
- };
- let returnData;
-
- if (state.useSingleDiffStyle) {
- urlParams.view = state.diffViewType;
- }
-
- commit(types.SET_LOADING, true);
-
- worker.addEventListener('message', ({ data }) => {
- commit(types.SET_TREE_DATA, data);
-
- worker.terminate();
- });
-
- return axios
- .get(mergeUrlParams(urlParams, state.endpoint))
- .then(res => {
- commit(types.SET_LOADING, false);
-
- commit(types.SET_MERGE_REQUEST_DIFFS, res.data.merge_request_diffs || []);
- commit(types.SET_DIFF_DATA, res.data);
-
- worker.postMessage(state.diffFiles);
-
- returnData = res.data;
- return Vue.nextTick();
- })
- .then(() => {
- handleLocationHash();
- return returnData;
- })
- .catch(() => worker.terminate());
-};
-
export const fetchDiffFilesBatch = ({ commit, state, dispatch }) => {
const id = window?.location?.hash;
const isNoteLink = id.indexOf('#note') === 0;
const urlParams = {
per_page: DIFFS_PER_PAGE,
w: state.showWhitespace ? '0' : '1',
+ view: window.gon?.features?.unifiedDiffLines ? 'inline' : state.diffViewType,
};
- if (state.useSingleDiffStyle) {
- urlParams.view = state.diffViewType;
- }
-
commit(types.SET_BATCH_LOADING, true);
commit(types.SET_RETRIEVING_BATCHES, true);
@@ -128,7 +84,7 @@ export const fetchDiffFilesBatch = ({ commit, state, dispatch }) => {
commit(types.SET_BATCH_LOADING, false);
if (!isNoteLink && !state.currentDiffFileId) {
- commit(types.UPDATE_CURRENT_DIFF_FILE_ID, diff_files[0].file_hash);
+ commit(types.VIEW_DIFF_FILE, diff_files[0].file_hash);
}
if (isNoteLink) {
@@ -144,7 +100,7 @@ export const fetchDiffFilesBatch = ({ commit, state, dispatch }) => {
!state.diffFiles.some(f => f.file_hash === state.currentDiffFileId) &&
!isNoteLink
) {
- commit(types.UPDATE_CURRENT_DIFF_FILE_ID, state.diffFiles[0].file_hash);
+ commit(types.VIEW_DIFF_FILE, state.diffFiles[0].file_hash);
}
if (gon.features?.codeNavigation) {
@@ -175,11 +131,9 @@ export const fetchDiffFilesBatch = ({ commit, state, dispatch }) => {
export const fetchDiffFilesMeta = ({ commit, state }) => {
const worker = new TreeWorker();
- const urlParams = {};
-
- if (state.useSingleDiffStyle) {
- urlParams.view = state.diffViewType;
- }
+ const urlParams = {
+ view: window.gon?.features?.unifiedDiffLines ? 'inline' : state.diffViewType,
+ };
commit(types.SET_LOADING, true);
@@ -229,7 +183,7 @@ export const fetchCoverageFiles = ({ commit, state }) => {
export const setHighlightedRow = ({ commit }, lineCode) => {
const fileHash = lineCode.split('_')[0];
commit(types.SET_HIGHLIGHTED_ROW, lineCode);
- commit(types.UPDATE_CURRENT_DIFF_FILE_ID, fileHash);
+ commit(types.VIEW_DIFF_FILE, fileHash);
};
// This is adding line discussions to the actual lines in the diff tree
@@ -240,10 +194,7 @@ export const assignDiscussionsToDiff = (
) => {
const id = window?.location?.hash;
const isNoteLink = id.indexOf('#note') === 0;
- const diffPositionByLineCode = getDiffPositionByLineCode(
- state.diffFiles,
- state.useSingleDiffStyle,
- );
+ const diffPositionByLineCode = getDiffPositionByLineCode(state.diffFiles);
const hash = getLocationHash();
discussions
@@ -477,13 +428,17 @@ export const toggleTreeOpen = ({ commit }, path) => {
commit(types.TOGGLE_FOLDER_OPEN, path);
};
+export const toggleActiveFileByHash = ({ commit }, hash) => {
+ commit(types.VIEW_DIFF_FILE, hash);
+};
+
export const scrollToFile = ({ state, commit }, path) => {
if (!state.treeEntries[path]) return;
const { fileHash } = state.treeEntries[path];
document.location.hash = fileHash;
- commit(types.UPDATE_CURRENT_DIFF_FILE_ID, fileHash);
+ commit(types.VIEW_DIFF_FILE, fileHash);
};
export const toggleShowTreeList = ({ commit, state }, saving = true) => {
@@ -751,7 +706,7 @@ export const setCurrentDiffFileIdFromNote = ({ commit, state, rootGetters }, not
const fileHash = rootGetters.getDiscussion(note.discussion_id).diff_file?.file_hash;
if (fileHash && state.diffFiles.some(f => f.file_hash === fileHash)) {
- commit(types.UPDATE_CURRENT_DIFF_FILE_ID, fileHash);
+ commit(types.VIEW_DIFF_FILE, fileHash);
}
};
@@ -759,5 +714,5 @@ export const navigateToDiffFileIndex = ({ commit, state }, index) => {
const fileHash = state.diffFiles[index].file_hash;
document.location.hash = fileHash;
- commit(types.UPDATE_CURRENT_DIFF_FILE_ID, fileHash);
+ commit(types.VIEW_DIFF_FILE, fileHash);
};
diff --git a/app/assets/javascripts/diffs/store/getters.js b/app/assets/javascripts/diffs/store/getters.js
index a24894b8d6b..42df5873a41 100644
--- a/app/assets/javascripts/diffs/store/getters.js
+++ b/app/assets/javascripts/diffs/store/getters.js
@@ -1,4 +1,5 @@
import { __, n__ } from '~/locale';
+import { parallelizeDiffLines } from './utils';
import { PARALLEL_DIFF_VIEW_TYPE, INLINE_DIFF_VIEW_TYPE } from '../constants';
export * from './getters_versions_dropdowns';
@@ -129,3 +130,11 @@ export const fileLineCoverage = state => (file, line) => {
*/
export const currentDiffIndex = state =>
Math.max(0, state.diffFiles.findIndex(diff => diff.file_hash === state.currentDiffFileId));
+
+export const diffLines = state => file => {
+ if (state.diffViewType === INLINE_DIFF_VIEW_TYPE) {
+ return null;
+ }
+
+ return parallelizeDiffLines(file.highlighted_diff_lines || []);
+};
diff --git a/app/assets/javascripts/diffs/store/getters_versions_dropdowns.js b/app/assets/javascripts/diffs/store/getters_versions_dropdowns.js
index 1e8e736c028..135b1c61ef5 100644
--- a/app/assets/javascripts/diffs/store/getters_versions_dropdowns.js
+++ b/app/assets/javascripts/diffs/store/getters_versions_dropdowns.js
@@ -11,17 +11,26 @@ export const diffCompareDropdownTargetVersions = (state, getters) => {
// startVersion only exists if the user has selected a version other
// than "base" so if startVersion is null then base must be selected
- const diffHead = parseBoolean(getParameterByName('diff_head'));
+ const defaultMergeRefForDiffs = window.gon?.features?.defaultMergeRefForDiffs || false;
+ const diffHeadParam = getParameterByName('diff_head');
+ const diffHead = parseBoolean(diffHeadParam) || (!diffHeadParam && defaultMergeRefForDiffs);
const isBaseSelected = !state.startVersion && !diffHead;
const isHeadSelected = !state.startVersion && diffHead;
+ let baseVersion = null;
- const baseVersion = {
- versionName: state.targetBranchName,
- version_index: DIFF_COMPARE_BASE_VERSION_INDEX,
- href: state.mergeRequestDiff.base_version_path,
- isBase: true,
- selected: isBaseSelected,
- };
+ if (
+ !defaultMergeRefForDiffs ||
+ (defaultMergeRefForDiffs && !state.mergeRequestDiff.head_version_path)
+ ) {
+ baseVersion = {
+ versionName: state.targetBranchName,
+ version_index: DIFF_COMPARE_BASE_VERSION_INDEX,
+ href: state.mergeRequestDiff.base_version_path,
+ isBase: true,
+ selected:
+ isBaseSelected || (defaultMergeRefForDiffs && !state.mergeRequestDiff.head_version_path),
+ };
+ }
const headVersion = {
versionName: state.targetBranchName,
@@ -40,7 +49,11 @@ export const diffCompareDropdownTargetVersions = (state, getters) => {
};
};
- return [...state.mergeRequestDiffs.slice(1).map(formatVersion), baseVersion, headVersion];
+ return [
+ ...state.mergeRequestDiffs.slice(1).map(formatVersion),
+ baseVersion,
+ state.mergeRequestDiff.head_version_path && headVersion,
+ ].filter(a => a);
};
export const diffCompareDropdownSourceVersions = (state, getters) => {
diff --git a/app/assets/javascripts/diffs/store/modules/diff_state.js b/app/assets/javascripts/diffs/store/modules/diff_state.js
index d31a600e354..001d9d9f594 100644
--- a/app/assets/javascripts/diffs/store/modules/diff_state.js
+++ b/app/assets/javascripts/diffs/store/modules/diff_state.js
@@ -34,6 +34,7 @@ export default () => ({
showTreeList: true,
currentDiffFileId: '',
projectPath: '',
+ viewedDiffFileIds: {},
commentForms: [],
highlightedRow: null,
renderTreeList: true,
@@ -41,5 +42,4 @@ export default () => ({
fileFinderVisible: false,
dismissEndpoint: '',
showSuggestPopover: true,
- useSingleDiffStyle: false,
});
diff --git a/app/assets/javascripts/diffs/store/mutation_types.js b/app/assets/javascripts/diffs/store/mutation_types.js
index 4b1dbc34902..5dba2e9d10d 100644
--- a/app/assets/javascripts/diffs/store/mutation_types.js
+++ b/app/assets/javascripts/diffs/store/mutation_types.js
@@ -19,7 +19,7 @@ export const SET_LINE_DISCUSSIONS_FOR_FILE = 'SET_LINE_DISCUSSIONS_FOR_FILE';
export const REMOVE_LINE_DISCUSSIONS_FOR_FILE = 'REMOVE_LINE_DISCUSSIONS_FOR_FILE';
export const TOGGLE_FOLDER_OPEN = 'TOGGLE_FOLDER_OPEN';
export const TOGGLE_SHOW_TREE_LIST = 'TOGGLE_SHOW_TREE_LIST';
-export const UPDATE_CURRENT_DIFF_FILE_ID = 'UPDATE_CURRENT_DIFF_FILE_ID';
+export const VIEW_DIFF_FILE = 'VIEW_DIFF_FILE';
export const OPEN_DIFF_FILE_COMMENT_FORM = 'OPEN_DIFF_FILE_COMMENT_FORM';
export const UPDATE_DIFF_FILE_COMMENT_FORM = 'UPDATE_DIFF_FILE_COMMENT_FORM';
diff --git a/app/assets/javascripts/diffs/store/mutations.js b/app/assets/javascripts/diffs/store/mutations.js
index 0d41f1c2178..7925c620c4e 100644
--- a/app/assets/javascripts/diffs/store/mutations.js
+++ b/app/assets/javascripts/diffs/store/mutations.js
@@ -1,4 +1,6 @@
+import Vue from 'vue';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
+import { INLINE_DIFF_VIEW_TYPE } from '../constants';
import {
findDiffFile,
addLineReferences,
@@ -24,7 +26,6 @@ export default {
projectPath,
dismissEndpoint,
showSuggestPopover,
- useSingleDiffStyle,
} = options;
Object.assign(state, {
endpoint,
@@ -34,7 +35,6 @@ export default {
projectPath,
dismissEndpoint,
showSuggestPopover,
- useSingleDiffStyle,
});
},
@@ -57,10 +57,7 @@ export default {
[types.SET_DIFF_DATA](state, data) {
let files = state.diffFiles;
- if (
- !(gon?.features?.diffsBatchLoad && window.location.search.indexOf('diff_id') === -1) &&
- data.diff_files
- ) {
+ if (window.location.search.indexOf('diff_id') !== -1 && data.diff_files) {
files = prepareDiffData(data, files);
}
@@ -154,7 +151,9 @@ export default {
addContextLines({
inlineLines: diffFile.highlighted_diff_lines,
parallelLines: diffFile.parallel_diff_lines,
- diffViewType: state.diffViewType,
+ diffViewType: window.gon?.features?.unifiedDiffLines
+ ? INLINE_DIFF_VIEW_TYPE
+ : state.diffViewType,
contextLines: lines,
bottom,
lineNumbers,
@@ -249,7 +248,7 @@ export default {
});
}
- if (!file.parallel_diff_lines || !file.highlighted_diff_lines) {
+ if (!file.parallel_diff_lines.length || !file.highlighted_diff_lines.length) {
const newDiscussions = (file.discussions || [])
.filter(d => d.id !== discussion.id)
.concat(discussion);
@@ -293,8 +292,9 @@ export default {
[types.TOGGLE_SHOW_TREE_LIST](state) {
state.showTreeList = !state.showTreeList;
},
- [types.UPDATE_CURRENT_DIFF_FILE_ID](state, fileId) {
+ [types.VIEW_DIFF_FILE](state, fileId) {
state.currentDiffFileId = fileId;
+ Vue.set(state.viewedDiffFileIds, fileId, true);
},
[types.OPEN_DIFF_FILE_COMMENT_FORM](state, formData) {
state.commentForms.push({
diff --git a/app/assets/javascripts/diffs/store/utils.js b/app/assets/javascripts/diffs/store/utils.js
index f014cddda32..69330ffae2f 100644
--- a/app/assets/javascripts/diffs/store/utils.js
+++ b/app/assets/javascripts/diffs/store/utils.js
@@ -11,7 +11,6 @@ import {
OLD_LINE_TYPE,
MATCH_LINE_TYPE,
LINES_TO_BE_RENDERED_DIRECTLY,
- MAX_LINES_TO_BE_RENDERED,
TREE_TYPE,
INLINE_DIFF_VIEW_TYPE,
PARALLEL_DIFF_VIEW_TYPE,
@@ -20,6 +19,77 @@ import {
} from '../constants';
import { prepareRawDiffFile } from '../diff_file';
+export const isAdded = line => ['new', 'new-nonewline'].includes(line.type);
+export const isRemoved = line => ['old', 'old-nonewline'].includes(line.type);
+export const isUnchanged = line => !line.type;
+export const isMeta = line => ['match', 'new-nonewline', 'old-nonewline'].includes(line.type);
+
+/**
+ * Pass in the inline diff lines array which gets converted
+ * to the parallel diff lines.
+ * This allows for us to convert inline diff lines to parallel
+ * on the frontend without needing to send any requests
+ * to the API.
+ *
+ * This method has been taken from the already existing backend
+ * implementation at lib/gitlab/diff/parallel_diff.rb
+ *
+ * @param {Object[]} diffLines - inline diff lines
+ *
+ * @returns {Object[]} parallel lines
+ */
+export const parallelizeDiffLines = (diffLines = []) => {
+ let freeRightIndex = null;
+ const lines = [];
+
+ for (let i = 0, diffLinesLength = diffLines.length, index = 0; i < diffLinesLength; i += 1) {
+ const line = diffLines[i];
+
+ if (isRemoved(line)) {
+ lines.push({
+ [LINE_POSITION_LEFT]: line,
+ [LINE_POSITION_RIGHT]: null,
+ });
+
+ if (freeRightIndex === null) {
+ // Once we come upon a new line it can be put on the right of this old line
+ freeRightIndex = index;
+ }
+ index += 1;
+ } else if (isAdded(line)) {
+ if (freeRightIndex !== null) {
+ // If an old line came before this without a line on the right, this
+ // line can be put to the right of it.
+ lines[freeRightIndex].right = line;
+
+ // If there are any other old lines on the left that don't yet have
+ // a new counterpart on the right, update the free_right_index
+ const nextFreeRightIndex = freeRightIndex + 1;
+ freeRightIndex = nextFreeRightIndex < index ? nextFreeRightIndex : null;
+ } else {
+ lines.push({
+ [LINE_POSITION_LEFT]: null,
+ [LINE_POSITION_RIGHT]: line,
+ });
+
+ freeRightIndex = null;
+ index += 1;
+ }
+ } else if (isMeta(line) || isUnchanged(line)) {
+ // line in the right panel is the same as in the left one
+ lines.push({
+ [LINE_POSITION_LEFT]: line,
+ [LINE_POSITION_RIGHT]: line,
+ });
+
+ freeRightIndex = null;
+ index += 1;
+ }
+ }
+
+ return lines;
+};
+
export function findDiffFile(files, match, matchKey = 'file_hash') {
return files.find(file => file[matchKey] === match);
}
@@ -281,7 +351,7 @@ function mergeTwoFiles(target, source) {
function ensureBasicDiffFileLines(file) {
const missingInline = !file.highlighted_diff_lines;
- const missingParallel = !file.parallel_diff_lines;
+ const missingParallel = !file.parallel_diff_lines || window.gon?.features?.unifiedDiffLines;
Object.assign(file, {
highlighted_diff_lines: missingInline ? [] : file.highlighted_diff_lines,
@@ -379,12 +449,10 @@ function getVisibleDiffLines(file) {
}
function finalizeDiffFile(file) {
- const name = (file.viewer && file.viewer.name) || diffViewerModes.text;
const lines = getVisibleDiffLines(file);
Object.assign(file, {
renderIt: lines < LINES_TO_BE_RENDERED_DIRECTLY,
- collapsed: name === diffViewerModes.text && lines > MAX_LINES_TO_BE_RENDERED,
isShowingFullFile: false,
isLoadingFullFile: false,
discussions: [],
@@ -417,11 +485,11 @@ export function prepareDiffData(diff, priorFiles = []) {
return deduplicateFilesList([...priorFiles, ...cleanedFiles]);
}
-export function getDiffPositionByLineCode(diffFiles, useSingleDiffStyle) {
+export function getDiffPositionByLineCode(diffFiles) {
let lines = [];
const hasInlineDiffs = diffFiles.some(file => file.highlighted_diff_lines.length > 0);
- if (!useSingleDiffStyle || hasInlineDiffs) {
+ if (hasInlineDiffs) {
// In either of these cases, we can use `highlighted_diff_lines` because
// that will include all of the parallel diff lines, too
diff --git a/app/assets/javascripts/diffs/utils/uuids.js b/app/assets/javascripts/diffs/utils/uuids.js
index 1a529c07ccc..12448350e62 100644
--- a/app/assets/javascripts/diffs/utils/uuids.js
+++ b/app/assets/javascripts/diffs/utils/uuids.js
@@ -11,9 +11,6 @@
* @typedef {String} UUIDv4
*/
-// https://gitlab.com/gitlab-org/frontend/rfcs/-/issues/20
-/* eslint-disable import/prefer-default-export */
-
import MersenneTwister from 'mersenne-twister';
import stringHash from 'string-hash';
import { isString } from 'lodash';
diff --git a/app/assets/javascripts/due_date_select.js b/app/assets/javascripts/due_date_select.js
index 6ebbeecae1d..5674cc8495d 100644
--- a/app/assets/javascripts/due_date_select.js
+++ b/app/assets/javascripts/due_date_select.js
@@ -6,6 +6,7 @@ import { __ } from '~/locale';
import axios from './lib/utils/axios_utils';
import { timeFor, parsePikadayDate, pikadayToString } from './lib/utils/datetime_utility';
import boardsStore from './boards/stores/boards_store';
+import initDeprecatedJQueryDropdown from '~/deprecated_jquery_dropdown';
class DueDateSelect {
constructor({ $dropdown, $loading } = {}) {
@@ -35,7 +36,7 @@ class DueDateSelect {
}
initGlDropdown() {
- this.$dropdown.glDropdown({
+ initDeprecatedJQueryDropdown(this.$dropdown, {
opened: () => {
const calendar = this.$datePicker.data('pikaday');
calendar.show();
diff --git a/app/assets/javascripts/editor/constants.js b/app/assets/javascripts/editor/constants.js
new file mode 100644
index 00000000000..9ee692e953a
--- /dev/null
+++ b/app/assets/javascripts/editor/constants.js
@@ -0,0 +1,7 @@
+import { __ } from '~/locale';
+
+export const EDITOR_LITE_INSTANCE_ERROR_NO_EL = __(
+ '"el" parameter is required for createInstance()',
+);
+
+export const URI_PREFIX = 'gitlab';
diff --git a/app/assets/javascripts/editor/editor_file_template_ext.js b/app/assets/javascripts/editor/editor_file_template_ext.js
new file mode 100644
index 00000000000..343908b831d
--- /dev/null
+++ b/app/assets/javascripts/editor/editor_file_template_ext.js
@@ -0,0 +1,7 @@
+import { Position } from 'monaco-editor';
+
+export default {
+ navigateFileStart() {
+ this.setPosition(new Position(1, 1));
+ },
+};
diff --git a/app/assets/javascripts/editor/editor_lite.js b/app/assets/javascripts/editor/editor_lite.js
index 0af0c3ecdcf..bbd5461ae4d 100644
--- a/app/assets/javascripts/editor/editor_lite.js
+++ b/app/assets/javascripts/editor/editor_lite.js
@@ -1,18 +1,15 @@
-import { editor as monacoEditor, languages as monacoLanguages, Position, Uri } from 'monaco-editor';
+import { editor as monacoEditor, languages as monacoLanguages, Uri } from 'monaco-editor';
import { DEFAULT_THEME, themes } from '~/ide/lib/themes';
import languages from '~/ide/lib/languages';
import { defaultEditorOptions } from '~/ide/lib/editor_options';
import { registerLanguages } from '~/ide/utils';
import { joinPaths } from '~/lib/utils/url_utility';
import { clearDomElement } from './utils';
+import { EDITOR_LITE_INSTANCE_ERROR_NO_EL, URI_PREFIX } from './constants';
export default class Editor {
constructor(options = {}) {
- this.editorEl = null;
- this.blobContent = '';
- this.blobPath = '';
- this.instance = null;
- this.model = null;
+ this.instances = [];
this.options = {
extraEditorClassName: 'gl-editor-lite',
...defaultEditorOptions,
@@ -31,6 +28,17 @@ export default class Editor {
monacoEditor.setTheme(theme ? themeName : DEFAULT_THEME);
}
+ static updateModelLanguage(path, instance) {
+ if (!instance) return;
+ const model = instance.getModel();
+ const ext = `.${path.split('.').pop()}`;
+ const language = monacoLanguages
+ .getLanguages()
+ .find(lang => lang.extensions.indexOf(ext) !== -1);
+ const id = language ? language.id : 'plaintext';
+ monacoEditor.setModelLanguage(model, id);
+ }
+
/**
* Creates a monaco instance with the given options.
*
@@ -40,74 +48,53 @@ export default class Editor {
* @param {string} options.blobContent The content to initialize the monacoEditor.
* @param {string} options.blobGlobalId This is used to help globally identify monaco instances that are created with the same blobPath.
*/
- createInstance({ el = undefined, blobPath = '', blobContent = '', blobGlobalId = '' } = {}) {
- if (!el) return;
- this.editorEl = el;
- this.blobContent = blobContent;
- this.blobPath = blobPath;
-
- clearDomElement(this.editorEl);
-
- const uriFilePath = joinPaths('gitlab', blobGlobalId, blobPath);
-
- this.model = monacoEditor.createModel(this.blobContent, undefined, Uri.file(uriFilePath));
-
- monacoEditor.onDidCreateEditor(this.renderEditor.bind(this));
-
- this.instance = monacoEditor.create(this.editorEl, this.options);
- this.instance.setModel(this.model);
- }
-
- dispose() {
- if (this.model) {
- this.model.dispose();
- this.model = null;
+ createInstance({
+ el = undefined,
+ blobPath = '',
+ blobContent = '',
+ blobGlobalId = '',
+ ...instanceOptions
+ } = {}) {
+ if (!el) {
+ throw new Error(EDITOR_LITE_INSTANCE_ERROR_NO_EL);
}
- return this.instance && this.instance.dispose();
- }
+ clearDomElement(el);
- renderEditor() {
- delete this.editorEl.dataset.editorLoading;
- }
+ const uriFilePath = joinPaths(URI_PREFIX, blobGlobalId, blobPath);
- onChangeContent(fn) {
- return this.model.onDidChangeContent(fn);
- }
+ const model = monacoEditor.createModel(blobContent, undefined, Uri.file(uriFilePath));
- updateModelLanguage(path) {
- if (path === this.blobPath) return;
- this.blobPath = path;
- const ext = `.${path.split('.').pop()}`;
- const language = monacoLanguages
- .getLanguages()
- .find(lang => lang.extensions.indexOf(ext) !== -1);
- const id = language ? language.id : 'plaintext';
- monacoEditor.setModelLanguage(this.model, id);
- }
+ monacoEditor.onDidCreateEditor(() => {
+ delete el.dataset.editorLoading;
+ });
- getValue() {
- return this.instance.getValue();
- }
-
- setValue(val) {
- this.instance.setValue(val);
- }
+ const instance = monacoEditor.create(el, {
+ ...this.options,
+ ...instanceOptions,
+ });
+ instance.setModel(model);
+ instance.onDidDispose(() => {
+ const index = this.instances.findIndex(inst => inst === instance);
+ this.instances.splice(index, 1);
+ model.dispose();
+ });
+ instance.updateModelLanguage = path => Editor.updateModelLanguage(path, instance);
- focus() {
- this.instance.focus();
+ this.instances.push(instance);
+ return instance;
}
- navigateFileStart() {
- this.instance.setPosition(new Position(1, 1));
- }
-
- updateOptions(options = {}) {
- this.instance.updateOptions(options);
+ dispose() {
+ this.instances.forEach(instance => instance.dispose());
}
- use(exts = []) {
+ use(exts = [], instance = null) {
const extensions = Array.isArray(exts) ? exts : [exts];
- Object.assign(this, ...extensions);
+ if (instance) {
+ Object.assign(instance, ...extensions);
+ } else {
+ this.instances.forEach(inst => Object.assign(inst, ...extensions));
+ }
}
}
diff --git a/app/assets/javascripts/editor/editor_markdown_ext.js b/app/assets/javascripts/editor/editor_markdown_ext.js
index 9d09663e643..c46f5736912 100644
--- a/app/assets/javascripts/editor/editor_markdown_ext.js
+++ b/app/assets/javascripts/editor/editor_markdown_ext.js
@@ -1,7 +1,7 @@
export default {
getSelectedText(selection = this.getSelection()) {
const { startLineNumber, endLineNumber, startColumn, endColumn } = selection;
- const valArray = this.instance.getValue().split('\n');
+ const valArray = this.getValue().split('\n');
let text = '';
if (startLineNumber === endLineNumber) {
text = valArray[startLineNumber - 1].slice(startColumn - 1, endColumn - 1);
@@ -20,20 +20,16 @@ export default {
return text;
},
- getSelection() {
- return this.instance.getSelection();
- },
-
replaceSelectedText(text, select = undefined) {
const forceMoveMarkers = !select;
- this.instance.executeEdits('', [{ range: this.getSelection(), text, forceMoveMarkers }]);
+ this.executeEdits('', [{ range: this.getSelection(), text, forceMoveMarkers }]);
},
moveCursor(dx = 0, dy = 0) {
- const pos = this.instance.getPosition();
+ const pos = this.getPosition();
pos.column += dx;
pos.lineNumber += dy;
- this.instance.setPosition(pos);
+ this.setPosition(pos);
},
/**
@@ -94,6 +90,6 @@ export default {
.setStartPosition(newStartLineNumber, newStartColumn)
.setEndPosition(newEndLineNumber, newEndColumn);
- this.instance.setSelection(newSelection);
+ this.setSelection(newSelection);
},
};
diff --git a/app/assets/javascripts/environments/components/confirm_rollback_modal.vue b/app/assets/javascripts/environments/components/confirm_rollback_modal.vue
index f0723e96ddf..b2a8571820b 100644
--- a/app/assets/javascripts/environments/components/confirm_rollback_modal.vue
+++ b/app/assets/javascripts/environments/components/confirm_rollback_modal.vue
@@ -1,4 +1,5 @@
<script>
+/* eslint-disable vue/no-v-html */
/**
* Render modal to confirm rollback/redeploy.
*/
diff --git a/app/assets/javascripts/environments/components/environment_actions.vue b/app/assets/javascripts/environments/components/environment_actions.vue
index d2978422224..035b276bc3b 100644
--- a/app/assets/javascripts/environments/components/environment_actions.vue
+++ b/app/assets/javascripts/environments/components/environment_actions.vue
@@ -1,8 +1,7 @@
<script>
-import { GlLoadingIcon } from '@gitlab/ui';
+import { GlButton, GlIcon, GlLoadingIcon } from '@gitlab/ui';
import { __, s__, sprintf } from '~/locale';
import { formatTime } from '~/lib/utils/datetime_utility';
-import Icon from '~/vue_shared/components/icon.vue';
import eventHub from '../event_hub';
import tooltip from '../../vue_shared/directives/tooltip';
@@ -11,7 +10,8 @@ export default {
tooltip,
},
components: {
- Icon,
+ GlButton,
+ GlIcon,
GlLoadingIcon,
},
props: {
@@ -69,38 +69,37 @@ export default {
</script>
<template>
<div class="btn-group" role="group">
- <button
+ <gl-button
v-tooltip
:title="title"
:aria-label="title"
:disabled="isLoading"
- type="button"
- class="dropdown btn btn-default dropdown-new js-environment-actions-dropdown"
+ class="dropdown dropdown-new js-environment-actions-dropdown"
data-container="body"
data-toggle="dropdown"
>
<span>
- <icon name="play" />
- <icon name="chevron-down" />
+ <gl-icon name="play" />
+ <gl-icon name="chevron-down" />
<gl-loading-icon v-if="isLoading" />
</span>
- </button>
+ </gl-button>
<ul class="dropdown-menu dropdown-menu-right">
- <li v-for="(action, i) in actions" :key="i">
- <button
+ <li v-for="(action, i) in actions" :key="i" class="gl-display-flex">
+ <gl-button
:class="{ disabled: isActionDisabled(action) }"
:disabled="isActionDisabled(action)"
- type="button"
- class="js-manual-action-link no-btn btn d-flex align-items-center"
+ variant="link"
+ class="js-manual-action-link gl-flex-fill-1"
@click="onClickAction(action)"
>
- <span class="flex-fill">{{ action.name }}</span>
- <span v-if="action.scheduledAt" class="text-secondary">
- <icon name="clock" />
+ <span class="gl-flex-fill-1">{{ action.name }}</span>
+ <span v-if="action.scheduledAt" class="text-secondary float-right">
+ <gl-icon name="clock" />
{{ remainingTime(action) }}
</span>
- </button>
+ </gl-button>
</li>
</ul>
</div>
diff --git a/app/assets/javascripts/environments/components/environment_delete.vue b/app/assets/javascripts/environments/components/environment_delete.vue
index b53c5fa6583..55aaa6d57bd 100644
--- a/app/assets/javascripts/environments/components/environment_delete.vue
+++ b/app/assets/javascripts/environments/components/environment_delete.vue
@@ -5,15 +5,14 @@
*/
import $ from 'jquery';
-import { GlTooltipDirective } from '@gitlab/ui';
-import Icon from '~/vue_shared/components/icon.vue';
+import { GlTooltipDirective, GlIcon } from '@gitlab/ui';
import { s__ } from '~/locale';
import eventHub from '../event_hub';
import LoadingButton from '../../vue_shared/components/loading_button.vue';
export default {
components: {
- Icon,
+ GlIcon,
LoadingButton,
},
directives: {
@@ -65,6 +64,6 @@ export default {
data-target="#delete-environment-modal"
@click="onClick"
>
- <icon name="remove" />
+ <gl-icon name="remove" />
</loading-button>
</template>
diff --git a/app/assets/javascripts/environments/components/environment_item.vue b/app/assets/javascripts/environments/components/environment_item.vue
index fa3d217f148..8850ed19a4b 100644
--- a/app/assets/javascripts/environments/components/environment_item.vue
+++ b/app/assets/javascripts/environments/components/environment_item.vue
@@ -1,13 +1,12 @@
<script>
/* eslint-disable @gitlab/vue-require-i18n-strings */
import { isEmpty } from 'lodash';
-import { GlTooltipDirective } from '@gitlab/ui';
+import { GlTooltipDirective, GlIcon } from '@gitlab/ui';
import { __, sprintf } from '~/locale';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import timeagoMixin from '~/vue_shared/mixins/timeago';
import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue';
import CommitComponent from '~/vue_shared/components/commit.vue';
-import Icon from '~/vue_shared/components/icon.vue';
import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate.vue';
import eventHub from '../event_hub';
import ActionsComponent from './environment_actions.vue';
@@ -30,7 +29,7 @@ export default {
ActionsComponent,
CommitComponent,
ExternalUrlComponent,
- Icon,
+ GlIcon,
MonitoringButtonComponent,
PinComponent,
DeleteComponent,
@@ -535,7 +534,7 @@ export default {
</div>
<span v-if="shouldRenderDeployBoard" class="deploy-board-icon" @click="toggleDeployBoard">
- <icon :name="deployIconName" />
+ <gl-icon :name="deployIconName" />
</span>
<span
@@ -560,9 +559,9 @@ export default {
role="button"
@click="onClickFolder"
>
- <icon :name="folderIconName" class="folder-icon" />
+ <gl-icon :name="folderIconName" class="folder-icon" />
- <icon name="folder" class="folder-icon" />
+ <gl-icon name="folder" class="folder-icon" />
<span> {{ model.folderName }} </span>
diff --git a/app/assets/javascripts/environments/components/environment_monitoring.vue b/app/assets/javascripts/environments/components/environment_monitoring.vue
index bd6feb14319..4dc2c0ec1bd 100644
--- a/app/assets/javascripts/environments/components/environment_monitoring.vue
+++ b/app/assets/javascripts/environments/components/environment_monitoring.vue
@@ -1,15 +1,12 @@
<script>
-import { GlDeprecatedButton, GlTooltipDirective } from '@gitlab/ui';
+import { GlButton, GlTooltipDirective } from '@gitlab/ui';
import { __ } from '~/locale';
/**
* Renders the Monitoring (Metrics) link in environments table.
*/
-import Icon from '~/vue_shared/components/icon.vue';
-
export default {
components: {
- Icon,
- GlDeprecatedButton,
+ GlButton,
},
directives: {
GlTooltip: GlTooltipDirective,
@@ -28,15 +25,14 @@ export default {
};
</script>
<template>
- <gl-deprecated-button
+ <gl-button
v-gl-tooltip
:href="monitoringUrl"
:title="title"
:aria-label="title"
- class="monitoring-url d-none d-sm-none d-md-block"
+ class="monitoring-url gl-display-none gl-display-sm-none gl-display-md-block"
+ icon="chart"
rel="noopener noreferrer nofollow"
variant="default"
- >
- <icon name="chart" />
- </gl-deprecated-button>
+ />
</template>
diff --git a/app/assets/javascripts/environments/components/environment_pin.vue b/app/assets/javascripts/environments/components/environment_pin.vue
index 3a219e98b08..52ac7725bde 100644
--- a/app/assets/javascripts/environments/components/environment_pin.vue
+++ b/app/assets/javascripts/environments/components/environment_pin.vue
@@ -3,15 +3,14 @@
* Renders a prevent auto-stop button.
* Used in environments table.
*/
-import { GlDeprecatedButton, GlTooltipDirective } from '@gitlab/ui';
-import Icon from '~/vue_shared/components/icon.vue';
+import { GlButton, GlTooltipDirective, GlIcon } from '@gitlab/ui';
import { __ } from '~/locale';
import eventHub from '../event_hub';
export default {
components: {
- Icon,
- GlDeprecatedButton,
+ GlIcon,
+ GlButton,
},
directives: {
GlTooltip: GlTooltipDirective,
@@ -31,12 +30,7 @@ export default {
};
</script>
<template>
- <gl-deprecated-button
- v-gl-tooltip
- :title="$options.title"
- :aria-label="$options.title"
- @click="onPinClick"
- >
- <icon name="thumbtack" />
- </gl-deprecated-button>
+ <gl-button v-gl-tooltip :title="$options.title" :aria-label="$options.title" @click="onPinClick">
+ <gl-icon name="thumbtack" />
+ </gl-button>
</template>
diff --git a/app/assets/javascripts/environments/components/environment_rollback.vue b/app/assets/javascripts/environments/components/environment_rollback.vue
index 7a728961b37..32528e6c6ea 100644
--- a/app/assets/javascripts/environments/components/environment_rollback.vue
+++ b/app/assets/javascripts/environments/components/environment_rollback.vue
@@ -5,21 +5,13 @@
*
* Makes a post request when the button is clicked.
*/
-import {
- GlTooltipDirective,
- GlLoadingIcon,
- GlModalDirective,
- GlDeprecatedButton,
-} from '@gitlab/ui';
+import { GlTooltipDirective, GlModalDirective, GlButton } from '@gitlab/ui';
import { s__ } from '~/locale';
-import Icon from '~/vue_shared/components/icon.vue';
import eventHub from '../event_hub';
export default {
components: {
- Icon,
- GlLoadingIcon,
- GlDeprecatedButton,
+ GlButton,
},
directives: {
GlTooltip: GlTooltipDirective,
@@ -73,15 +65,13 @@ export default {
};
</script>
<template>
- <gl-deprecated-button
+ <gl-button
v-gl-tooltip
v-gl-modal.confirm-rollback-modal
- :disabled="isLoading"
+ class="gl-display-none gl-display-md-block text-secondary"
+ :loading="isLoading"
:title="title"
- class="d-none d-md-block text-secondary"
+ :icon="isLastDeployment ? 'repeat' : 'redo'"
@click="onClick"
- >
- <icon v-if="isLastDeployment" name="repeat" /> <icon v-else name="redo" />
- <gl-loading-icon v-if="isLoading" />
- </gl-deprecated-button>
+ />
</template>
diff --git a/app/assets/javascripts/environments/components/environment_terminal_button.vue b/app/assets/javascripts/environments/components/environment_terminal_button.vue
index 37f94f9f5ab..b5a7be90204 100644
--- a/app/assets/javascripts/environments/components/environment_terminal_button.vue
+++ b/app/assets/javascripts/environments/components/environment_terminal_button.vue
@@ -3,13 +3,12 @@
* Renders a terminal button to open a web terminal.
* Used in environments table.
*/
-import { GlTooltipDirective } from '@gitlab/ui';
-import Icon from '~/vue_shared/components/icon.vue';
+import { GlTooltipDirective, GlIcon } from '@gitlab/ui';
import { __ } from '~/locale';
export default {
components: {
- Icon,
+ GlIcon,
},
directives: {
GlTooltip: GlTooltipDirective,
@@ -42,6 +41,6 @@ export default {
:class="{ disabled: disabled }"
class="btn terminal-button d-none d-sm-none d-md-block text-secondary"
>
- <icon name="terminal" />
+ <gl-icon name="terminal" />
</a>
</template>
diff --git a/app/assets/javascripts/environments/components/environments_table.vue b/app/assets/javascripts/environments/components/environments_table.vue
index 1bf705dcda2..c06ab265915 100644
--- a/app/assets/javascripts/environments/components/environments_table.vue
+++ b/app/assets/javascripts/environments/components/environments_table.vue
@@ -14,6 +14,7 @@ export default {
DeployBoard: () => import('ee_component/environments/components/deploy_board_component.vue'),
CanaryDeploymentCallout: () =>
import('ee_component/environments/components/canary_deployment_callout.vue'),
+ EnvironmentAlert: () => import('ee_component/environments/components/environment_alert.vue'),
},
props: {
environments: {
@@ -111,6 +112,9 @@ export default {
shouldShowCanaryCallout(env) {
return env.showCanaryCallout && this.showCanaryDeploymentCallout;
},
+ shouldRenderAlert(env) {
+ return env?.has_opened_alert;
+ },
sortEnvironments(environments) {
/*
* The sorting algorithm should sort in the following priorities:
@@ -185,6 +189,11 @@ export default {
/>
</div>
</div>
+ <environment-alert
+ v-if="shouldRenderAlert(model)"
+ :key="`alert-row-${i}`"
+ :environment="model"
+ />
<template v-if="shouldRenderFolderContent(model)">
<div v-if="model.isLoadingFolderContent" :key="`loading-item-${i}`">
diff --git a/app/assets/javascripts/environments/components/stop_environment_modal.vue b/app/assets/javascripts/environments/components/stop_environment_modal.vue
index 7448fd584c6..88612376b6e 100644
--- a/app/assets/javascripts/environments/components/stop_environment_modal.vue
+++ b/app/assets/javascripts/environments/components/stop_environment_modal.vue
@@ -1,5 +1,5 @@
<script>
-/* eslint-disable @gitlab/vue-require-i18n-strings */
+/* eslint-disable @gitlab/vue-require-i18n-strings, vue/no-v-html */
import { GlTooltipDirective } from '@gitlab/ui';
import DeprecatedModal2 from '~/vue_shared/components/deprecated_modal_2.vue';
import { s__, sprintf } from '~/locale';
diff --git a/app/assets/javascripts/environments/folder/environments_folder_bundle.js b/app/assets/javascripts/environments/folder/environments_folder_bundle.js
index 56896ac4d43..6c547c3713a 100644
--- a/app/assets/javascripts/environments/folder/environments_folder_bundle.js
+++ b/app/assets/javascripts/environments/folder/environments_folder_bundle.js
@@ -1,20 +1,33 @@
import Vue from 'vue';
+import VueApollo from 'vue-apollo';
import canaryCalloutMixin from '../mixins/canary_callout_mixin';
import environmentsFolderApp from './environments_folder_view.vue';
import { parseBoolean } from '../../lib/utils/common_utils';
import Translate from '../../vue_shared/translate';
+import createDefaultClient from '~/lib/graphql';
Vue.use(Translate);
+Vue.use(VueApollo);
-export default () =>
- new Vue({
- el: '#environments-folder-list-view',
+const apolloProvider = new VueApollo({
+ defaultClient: createDefaultClient(),
+});
+
+export default () => {
+ const el = document.getElementById('environments-folder-list-view');
+
+ return new Vue({
+ el,
components: {
environmentsFolderApp,
},
mixins: [canaryCalloutMixin],
+ apolloProvider,
+ provide: {
+ projectPath: el.dataset.projectPath,
+ },
data() {
- const environmentsData = document.querySelector(this.$options.el).dataset;
+ const environmentsData = el.dataset;
return {
endpoint: environmentsData.environmentsDataEndpoint,
@@ -35,3 +48,4 @@ export default () =>
});
},
});
+};
diff --git a/app/assets/javascripts/environments/folder/environments_folder_view.vue b/app/assets/javascripts/environments/folder/environments_folder_view.vue
index e1e356a977f..16d25615779 100644
--- a/app/assets/javascripts/environments/folder/environments_folder_view.vue
+++ b/app/assets/javascripts/environments/folder/environments_folder_view.vue
@@ -23,7 +23,8 @@ export default {
},
cssContainerClass: {
type: String,
- required: true,
+ required: false,
+ default: '',
},
canReadEnvironment: {
type: Boolean,
diff --git a/app/assets/javascripts/environments/index.js b/app/assets/javascripts/environments/index.js
index 4848cb0f13d..8e8af3f32f7 100644
--- a/app/assets/javascripts/environments/index.js
+++ b/app/assets/javascripts/environments/index.js
@@ -1,20 +1,32 @@
import Vue from 'vue';
+import VueApollo from 'vue-apollo';
import canaryCalloutMixin from './mixins/canary_callout_mixin';
import environmentsComponent from './components/environments_app.vue';
import { parseBoolean } from '../lib/utils/common_utils';
import Translate from '../vue_shared/translate';
+import createDefaultClient from '~/lib/graphql';
Vue.use(Translate);
+Vue.use(VueApollo);
-export default () =>
- new Vue({
- el: '#environments-list-view',
+const apolloProvider = new VueApollo({
+ defaultClient: createDefaultClient(),
+});
+
+export default () => {
+ const el = document.getElementById('environments-list-view');
+ return new Vue({
+ el,
components: {
environmentsComponent,
},
mixins: [canaryCalloutMixin],
+ apolloProvider,
+ provide: {
+ projectPath: el.dataset.projectPath,
+ },
data() {
- const environmentsData = document.querySelector(this.$options.el).dataset;
+ const environmentsData = el.dataset;
return {
endpoint: environmentsData.environmentsDataEndpoint,
@@ -39,3 +51,4 @@ export default () =>
});
},
});
+};
diff --git a/app/assets/javascripts/environments/mixins/canary_callout_mixin.js b/app/assets/javascripts/environments/mixins/canary_callout_mixin.js
index 398576a31cb..e9f1a144cb3 100644
--- a/app/assets/javascripts/environments/mixins/canary_callout_mixin.js
+++ b/app/assets/javascripts/environments/mixins/canary_callout_mixin.js
@@ -2,7 +2,7 @@ import { parseBoolean } from '~/lib/utils/common_utils';
export default {
data() {
- const data = document.querySelector(this.$options.el).dataset;
+ const data = this.$options.el.dataset;
return {
canaryDeploymentFeatureId: data.environmentsDataCanaryDeploymentFeatureId,
diff --git a/app/assets/javascripts/environments/stores/helpers.js b/app/assets/javascripts/environments/stores/helpers.js
index 8eba6c00601..eb47ba29412 100644
--- a/app/assets/javascripts/environments/stores/helpers.js
+++ b/app/assets/javascripts/environments/stores/helpers.js
@@ -4,5 +4,4 @@
* @param {Object} environment
* @returns {Object}
*/
-// eslint-disable-next-line import/prefer-default-export
export const setDeployBoard = (oldEnvironmentState, environment) => environment;
diff --git a/app/assets/javascripts/error_tracking/components/error_details.vue b/app/assets/javascripts/error_tracking/components/error_details.vue
index 3d1fdc4f168..5dee3ef3ffe 100644
--- a/app/assets/javascripts/error_tracking/components/error_details.vue
+++ b/app/assets/javascripts/error_tracking/components/error_details.vue
@@ -11,10 +11,10 @@ import {
GlDeprecatedDropdown,
GlDeprecatedDropdownItem,
GlDeprecatedDropdownDivider,
+ GlIcon,
} from '@gitlab/ui';
import { deprecatedCreateFlash as createFlash } from '~/flash';
import { __, sprintf, n__ } from '~/locale';
-import Icon from '~/vue_shared/components/icon.vue';
import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate.vue';
import Stacktrace from './stacktrace.vue';
import TrackEventDirective from '~/vue_shared/directives/track_event';
@@ -38,7 +38,7 @@ export default {
GlLink,
GlLoadingIcon,
TooltipOnTruncate,
- Icon,
+ GlIcon,
Stacktrace,
GlBadge,
GlAlert,
@@ -397,7 +397,7 @@ export default {
data-testid="external-url-link"
>
<span class="text-truncate">{{ error.externalUrl }}</span>
- <icon name="external-link" class="ml-1 flex-shrink-0" />
+ <gl-icon name="external-link" class="ml-1 flex-shrink-0" />
</gl-link>
</li>
<li v-if="error.firstReleaseVersion">
diff --git a/app/assets/javascripts/error_tracking/components/stacktrace_entry.vue b/app/assets/javascripts/error_tracking/components/stacktrace_entry.vue
index c6825d7af45..a4938fe13ed 100644
--- a/app/assets/javascripts/error_tracking/components/stacktrace_entry.vue
+++ b/app/assets/javascripts/error_tracking/components/stacktrace_entry.vue
@@ -1,14 +1,14 @@
<script>
-import { GlTooltip, GlSprintf } from '@gitlab/ui';
+/* eslint-disable vue/no-v-html */
+import { GlTooltip, GlSprintf, GlIcon } from '@gitlab/ui';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import FileIcon from '~/vue_shared/components/file_icon.vue';
-import Icon from '~/vue_shared/components/icon.vue';
export default {
components: {
ClipboardButton,
FileIcon,
- Icon,
+ GlIcon,
GlSprintf,
},
directives: {
@@ -80,7 +80,7 @@ export default {
<div ref="header" class="file-title file-title-flex-parent">
<div class="file-header-content d-flex align-content-center">
<div v-if="hasCode" class="d-inline-block cursor-pointer" @click="toggle()">
- <icon :name="collapseIcon" :size="16" aria-hidden="true" class="gl-mr-2" />
+ <gl-icon :name="collapseIcon" :size="16" aria-hidden="true" class="gl-mr-2" />
</div>
<file-icon :file-name="filePath" :size="18" aria-hidden="true" css-classes="gl-mr-2" />
<strong
diff --git a/app/assets/javascripts/error_tracking/store/details/actions.js b/app/assets/javascripts/error_tracking/store/details/actions.js
index 28806b3915c..df5be5224a7 100644
--- a/app/assets/javascripts/error_tracking/store/details/actions.js
+++ b/app/assets/javascripts/error_tracking/store/details/actions.js
@@ -10,7 +10,6 @@ const stopPolling = poll => {
if (poll) poll.stop();
};
-// eslint-disable-next-line import/prefer-default-export
export function startPollingStacktrace({ commit }, endpoint) {
stackTracePoll = new Poll({
resource: service,
diff --git a/app/assets/javascripts/error_tracking/store/details/getters.js b/app/assets/javascripts/error_tracking/store/details/getters.js
index f2778fbb2c7..a3b31436c81 100644
--- a/app/assets/javascripts/error_tracking/store/details/getters.js
+++ b/app/assets/javascripts/error_tracking/store/details/getters.js
@@ -1,4 +1,3 @@
-// eslint-disable-next-line import/prefer-default-export
export const stacktrace = state =>
state.stacktraceData.stack_trace_entries
? state.stacktraceData.stack_trace_entries.reverse()
diff --git a/app/assets/javascripts/error_tracking_settings/components/error_tracking_form.vue b/app/assets/javascripts/error_tracking_settings/components/error_tracking_form.vue
index 0de67a8bcc7..f1fb1a44758 100644
--- a/app/assets/javascripts/error_tracking_settings/components/error_tracking_form.vue
+++ b/app/assets/javascripts/error_tracking_settings/components/error_tracking_form.vue
@@ -1,11 +1,10 @@
<script>
import { mapActions, mapState } from 'vuex';
-import { GlFormInput } from '@gitlab/ui';
-import Icon from '~/vue_shared/components/icon.vue';
+import { GlFormInput, GlIcon } from '@gitlab/ui';
import LoadingButton from '~/vue_shared/components/loading_button.vue';
export default {
- components: { GlFormInput, Icon, LoadingButton },
+ components: { GlFormInput, GlIcon, LoadingButton },
computed: {
...mapState(['apiHost', 'connectError', 'connectSuccessful', 'isLoadingProjects', 'token']),
tokenInputState() {
@@ -64,7 +63,7 @@ export default {
:loading="isLoadingProjects"
@click="fetchProjects"
/>
- <icon
+ <gl-icon
v-show="connectSuccessful"
class="js-error-tracking-connect-success gl-ml-2 text-success align-middle"
:aria-label="__('Projects Successfully Retrieved')"
diff --git a/app/assets/javascripts/filtered_search/issuable_filtered_search_token_keys.js b/app/assets/javascripts/filtered_search/issuable_filtered_search_token_keys.js
index 9bea7aa7b04..d2ac80fa190 100644
--- a/app/assets/javascripts/filtered_search/issuable_filtered_search_token_keys.js
+++ b/app/assets/javascripts/filtered_search/issuable_filtered_search_token_keys.js
@@ -86,6 +86,11 @@ export const conditions = flattenDeep(
value: __('Any'),
},
{
+ url: 'author_username=support-bot',
+ tokenKey: 'author',
+ value: 'support-bot',
+ },
+ {
url: 'milestone_title=None',
tokenKey: 'milestone',
value: __('None'),
diff --git a/app/assets/javascripts/flash.js b/app/assets/javascripts/flash.js
index 7697d97cb2c..f2dd8d5ace5 100644
--- a/app/assets/javascripts/flash.js
+++ b/app/assets/javascripts/flash.js
@@ -175,4 +175,4 @@ export {
removeFlashClickListener,
FLASH_TYPES,
};
-window.Flash = deprecatedCreateFlash;
+window.Flash = createFlash;
diff --git a/app/assets/javascripts/frequent_items/components/frequent_items_list_item.vue b/app/assets/javascripts/frequent_items/components/frequent_items_list_item.vue
index c0dadedbc51..1203f389931 100644
--- a/app/assets/javascripts/frequent_items/components/frequent_items_list_item.vue
+++ b/app/assets/javascripts/frequent_items/components/frequent_items_list_item.vue
@@ -1,5 +1,5 @@
<script>
-/* eslint-disable vue/require-default-prop */
+/* eslint-disable vue/require-default-prop, vue/no-v-html */
import Identicon from '~/vue_shared/components/identicon.vue';
import highlight from '~/lib/utils/highlight';
import { truncateNamespace } from '~/lib/utils/text_utility';
diff --git a/app/assets/javascripts/frequent_items/components/frequent_items_search_input.vue b/app/assets/javascripts/frequent_items/components/frequent_items_search_input.vue
index 40add09f25d..19cb09f0dcc 100644
--- a/app/assets/javascripts/frequent_items/components/frequent_items_search_input.vue
+++ b/app/assets/javascripts/frequent_items/components/frequent_items_search_input.vue
@@ -1,13 +1,13 @@
<script>
import { debounce } from 'lodash';
import { mapActions } from 'vuex';
-import Icon from '~/vue_shared/components/icon.vue';
+import { GlIcon } from '@gitlab/ui';
import eventHub from '../event_hub';
import frequentItemsMixin from './frequent_items_mixin';
export default {
components: {
- Icon,
+ GlIcon,
},
mixins: [frequentItemsMixin],
data() {
@@ -49,6 +49,6 @@ export default {
type="search"
class="form-control"
/>
- <icon v-if="!searchQuery" name="search" class="search-icon" />
+ <gl-icon v-if="!searchQuery" name="search" class="search-icon" />
</div>
</template>
diff --git a/app/assets/javascripts/frequent_items/index.js b/app/assets/javascripts/frequent_items/index.js
index c074f173776..1998bf4358a 100644
--- a/app/assets/javascripts/frequent_items/index.js
+++ b/app/assets/javascripts/frequent_items/index.js
@@ -28,8 +28,8 @@ export default function initFrequentItemDropdowns() {
return;
}
- $(navEl).on('shown.bs.dropdown', () =>
- import('./components/app.vue').then(({ default: FrequentItems }) => {
+ import('./components/app.vue')
+ .then(({ default: FrequentItems }) => {
// eslint-disable-next-line no-new
new Vue({
el,
@@ -59,9 +59,11 @@ export default function initFrequentItemDropdowns() {
});
},
});
+ })
+ .catch(() => {});
- eventHub.$emit(`${namespace}-dropdownOpen`);
- }),
- );
+ $(navEl).on('shown.bs.dropdown', () => {
+ eventHub.$emit(`${namespace}-dropdownOpen`);
+ });
});
}
diff --git a/app/assets/javascripts/frequent_items/store/getters.js b/app/assets/javascripts/frequent_items/store/getters.js
index 73e66643f06..36cc9020d8d 100644
--- a/app/assets/javascripts/frequent_items/store/getters.js
+++ b/app/assets/javascripts/frequent_items/store/getters.js
@@ -1,2 +1 @@
-// eslint-disable-next-line import/prefer-default-export
export const hasSearchQuery = state => state.searchQuery !== '';
diff --git a/app/assets/javascripts/gfm_auto_complete.js b/app/assets/javascripts/gfm_auto_complete.js
index 36c586ddfd2..409733c73b9 100644
--- a/app/assets/javascripts/gfm_auto_complete.js
+++ b/app/assets/javascripts/gfm_auto_complete.js
@@ -1,5 +1,5 @@
import $ from 'jquery';
-import '@gitlab/at.js';
+import '~/lib/utils/jquery_at_who';
import { escape, template } from 'lodash';
import SidebarMediator from '~/sidebar/sidebar_mediator';
import glRegexp from './lib/utils/regexp';
@@ -52,6 +52,7 @@ export const defaultAutocompleteConfig = {
milestones: true,
labels: true,
snippets: true,
+ vulnerabilities: true,
};
class GfmAutoComplete {
@@ -71,12 +72,15 @@ class GfmAutoComplete {
setupLifecycle() {
this.input.each((i, input) => {
const $input = $(input);
- $input.off('focus.setupAtWho').on('focus.setupAtWho', this.setupAtWho.bind(this, $input));
- $input.on('change.atwho', () => input.dispatchEvent(new Event('input')));
- // This triggers at.js again
- // Needed for quick actions with suffixes (ex: /label ~)
- $input.on('inserted-commands.atwho', $input.trigger.bind($input, 'keyup'));
- $input.on('clear-commands-cache.atwho', () => this.clearCache());
+ if (!$input.hasClass('js-gfm-input-initialized')) {
+ $input.off('focus.setupAtWho').on('focus.setupAtWho', this.setupAtWho.bind(this, $input));
+ $input.on('change.atwho', () => input.dispatchEvent(new Event('input')));
+ // This triggers at.js again
+ // Needed for quick actions with suffixes (ex: /label ~)
+ $input.on('inserted-commands.atwho', $input.trigger.bind($input, 'keyup'));
+ $input.on('clear-commands-cache.atwho', () => this.clearCache());
+ $input.addClass('js-gfm-input-initialized');
+ }
});
}
@@ -644,7 +648,8 @@ class GfmAutoComplete {
// https://github.com/ichord/At.js
const atSymbolsWithBar = Object.keys(controllers)
.join('|')
- .replace(/[$]/, '\\$&');
+ .replace(/[$]/, '\\$&')
+ .replace(/[+]/, '\\+');
const atSymbolsWithoutBar = Object.keys(controllers).join('');
const targetSubtext = subtext.split(GfmAutoComplete.regexSubtext).pop();
const resultantFlag = flag.replace(/[-[\]/{}()*+?.\\^$|]/g, '\\$&');
@@ -675,6 +680,7 @@ GfmAutoComplete.atTypeMap = {
'~': 'labels',
'%': 'milestones',
'/': 'commands',
+ '+': 'vulnerabilities',
$: 'snippets',
};
diff --git a/app/assets/javascripts/gpg_badges.js b/app/assets/javascripts/gpg_badges.js
index 099c46f4b8d..e0f64c8e843 100644
--- a/app/assets/javascripts/gpg_badges.js
+++ b/app/assets/javascripts/gpg_badges.js
@@ -13,7 +13,8 @@ export default class GpgBadges {
const badges = $('.js-loading-gpg-badge');
- badges.html('<i class="fa fa-spinner fa-spin"></i>');
+ badges.html('<span class="gl-spinner gl-spinner-orange gl-spinner-sm"></span>');
+ badges.children().attr('aria-label', __('Loading'));
const displayError = () => createFlash(__('An error occurred while loading commit signatures'));
diff --git a/app/assets/javascripts/grafana_integration/components/grafana_integration.vue b/app/assets/javascripts/grafana_integration/components/grafana_integration.vue
index 59327e36f5f..79494cb173b 100644
--- a/app/assets/javascripts/grafana_integration/components/grafana_integration.vue
+++ b/app/assets/javascripts/grafana_integration/components/grafana_integration.vue
@@ -1,7 +1,6 @@
<script>
-import { GlButton, GlFormGroup, GlFormInput, GlFormCheckbox } from '@gitlab/ui';
+import { GlButton, GlFormGroup, GlFormInput, GlFormCheckbox, GlIcon } from '@gitlab/ui';
import { mapState, mapActions } from 'vuex';
-import Icon from '~/vue_shared/components/icon.vue';
export default {
components: {
@@ -9,7 +8,7 @@ export default {
GlFormCheckbox,
GlFormGroup,
GlFormInput,
- Icon,
+ GlIcon,
},
data() {
return { placeholderUrl: 'https://my-url.grafana.net/' };
@@ -89,7 +88,7 @@ export default {
rel="noopener noreferrer"
>
{{ __('More information') }}
- <icon name="external-link" class="vertical-align-middle" />
+ <gl-icon name="external-link" class="vertical-align-middle" />
</a>
</p>
</gl-form-group>
diff --git a/app/assets/javascripts/graphql_shared/fragments/author.fragment.graphql b/app/assets/javascripts/graphql_shared/fragments/author.fragment.graphql
index 9a2ff1c1648..0855ac2af45 100644
--- a/app/assets/javascripts/graphql_shared/fragments/author.fragment.graphql
+++ b/app/assets/javascripts/graphql_shared/fragments/author.fragment.graphql
@@ -1,4 +1,5 @@
fragment Author on User {
+ id
avatarUrl
name
username
diff --git a/app/assets/javascripts/graphql_shared/fragments/epic.fragment.graphql b/app/assets/javascripts/graphql_shared/fragments/epic.fragment.graphql
new file mode 100644
index 00000000000..286ebbd019e
--- /dev/null
+++ b/app/assets/javascripts/graphql_shared/fragments/epic.fragment.graphql
@@ -0,0 +1,10 @@
+fragment EpicNode on Epic {
+ id
+ iid
+ title
+ state
+ reference
+ webUrl
+ createdAt
+ closedAt
+}
diff --git a/app/assets/javascripts/graphql_shared/fragments/label.fragment.graphql b/app/assets/javascripts/graphql_shared/fragments/label.fragment.graphql
new file mode 100644
index 00000000000..1a2ea0bda1b
--- /dev/null
+++ b/app/assets/javascripts/graphql_shared/fragments/label.fragment.graphql
@@ -0,0 +1,7 @@
+fragment Label on Label {
+ id
+ title
+ description
+ color
+ textColor
+}
diff --git a/app/assets/javascripts/alert_management/graphql/mutations/alert_todo_mark_done.mutation.graphql b/app/assets/javascripts/graphql_shared/mutations/todo_mark_done.mutation.graphql
index 4d59b4d94cd..4d59b4d94cd 100644
--- a/app/assets/javascripts/alert_management/graphql/mutations/alert_todo_mark_done.mutation.graphql
+++ b/app/assets/javascripts/graphql_shared/mutations/todo_mark_done.mutation.graphql
diff --git a/app/assets/javascripts/groups/components/app.vue b/app/assets/javascripts/groups/components/app.vue
index 0b401f4d732..871f5c9a845 100644
--- a/app/assets/javascripts/groups/components/app.vue
+++ b/app/assets/javascripts/groups/components/app.vue
@@ -2,6 +2,7 @@
/* global Flash */
import $ from 'jquery';
+import 'vendor/jquery.scrollTo';
import { GlLoadingIcon } from '@gitlab/ui';
import { s__, sprintf } from '~/locale';
import DeprecatedModal from '~/vue_shared/components/deprecated_modal.vue';
diff --git a/app/assets/javascripts/groups/components/group_item.vue b/app/assets/javascripts/groups/components/group_item.vue
index be90ba12678..44349b33386 100644
--- a/app/assets/javascripts/groups/components/group_item.vue
+++ b/app/assets/javascripts/groups/components/group_item.vue
@@ -1,4 +1,5 @@
<script>
+/* eslint-disable vue/no-v-html */
import { GlLoadingIcon, GlBadge } from '@gitlab/ui';
import { visitUrl } from '../../lib/utils/url_utility';
import tooltip from '../../vue_shared/directives/tooltip';
diff --git a/app/assets/javascripts/groups/components/invite_members_banner.vue b/app/assets/javascripts/groups/components/invite_members_banner.vue
new file mode 100644
index 00000000000..da7adab1d86
--- /dev/null
+++ b/app/assets/javascripts/groups/components/invite_members_banner.vue
@@ -0,0 +1,76 @@
+<script>
+import { GlBanner } from '@gitlab/ui';
+import { s__ } from '~/locale';
+import { parseBoolean, setCookie, getCookie } from '~/lib/utils/common_utils';
+import Tracking from '~/tracking';
+
+const trackingMixin = Tracking.mixin();
+
+export default {
+ components: {
+ GlBanner,
+ },
+ mixins: [trackingMixin],
+ inject: ['svgPath', 'inviteMembersPath', 'isDismissedKey', 'trackLabel'],
+ data() {
+ return {
+ isDismissed: parseBoolean(getCookie(this.isDismissedKey)),
+ tracking: {
+ label: this.trackLabel,
+ },
+ };
+ },
+ created() {
+ this.$nextTick(() => {
+ this.addTrackingAttributesToButton();
+ });
+ },
+ mounted() {
+ this.trackOnShow();
+ },
+ methods: {
+ handleClose() {
+ setCookie(this.isDismissedKey, true);
+ this.isDismissed = true;
+ this.track(this.$options.dismissEvent);
+ },
+ trackOnShow() {
+ if (!this.isDismissed) this.track(this.$options.displayEvent);
+ },
+ addTrackingAttributesToButton() {
+ if (this.$refs.banner === undefined) return;
+
+ const button = this.$refs.banner.$el.querySelector(`[href='${this.inviteMembersPath}']`);
+
+ if (button) {
+ button.setAttribute('data-track-event', this.$options.buttonClickEvent);
+ button.setAttribute('data-track-label', this.trackLabel);
+ }
+ },
+ },
+ i18n: {
+ title: s__('InviteMembersBanner|Collaborate with your team'),
+ body: s__(
+ "InviteMembersBanner|We noticed that you haven't invited anyone to this group. Invite your colleagues so you can discuss issues, collaborate on merge requests, and share your knowledge.",
+ ),
+ button_text: s__('InviteMembersBanner|Invite your colleagues'),
+ },
+ displayEvent: 'invite_members_banner_displayed',
+ buttonClickEvent: 'invite_members_banner_button_clicked',
+ dismissEvent: 'invite_members_banner_dismissed',
+};
+</script>
+
+<template>
+ <gl-banner
+ v-if="!isDismissed"
+ ref="banner"
+ :title="$options.i18n.title"
+ :button-text="$options.i18n.button_text"
+ :svg-path="svgPath"
+ :button-link="inviteMembersPath"
+ @close="handleClose"
+ >
+ <p>{{ $options.i18n.body }}</p>
+ </gl-banner>
+</template>
diff --git a/app/assets/javascripts/groups/components/item_actions.vue b/app/assets/javascripts/groups/components/item_actions.vue
index ac4c12dda24..5487e25066e 100644
--- a/app/assets/javascripts/groups/components/item_actions.vue
+++ b/app/assets/javascripts/groups/components/item_actions.vue
@@ -1,12 +1,12 @@
<script>
+import { GlIcon } from '@gitlab/ui';
import tooltip from '~/vue_shared/directives/tooltip';
-import icon from '~/vue_shared/components/icon.vue';
import eventHub from '../event_hub';
import { COMMON_STR } from '../constants';
export default {
components: {
- icon,
+ GlIcon,
},
directives: {
tooltip,
@@ -56,7 +56,7 @@ export default {
class="leave-group btn btn-xs no-expand gl-text-gray-500 gl-ml-5"
@click.prevent="onLeaveGroup"
>
- <icon name="leave" class="position-top-0" />
+ <gl-icon name="leave" class="position-top-0" />
</a>
<a
v-if="group.canEdit"
@@ -68,7 +68,7 @@ export default {
data-placement="bottom"
class="edit-group btn btn-xs no-expand gl-text-gray-500 gl-ml-5"
>
- <icon name="settings" class="position-top-0 align-middle" />
+ <gl-icon name="settings" class="position-top-0 align-middle" />
</a>
</div>
</template>
diff --git a/app/assets/javascripts/groups/components/item_caret.vue b/app/assets/javascripts/groups/components/item_caret.vue
index cd3e3de4cb4..e23b0fa7413 100644
--- a/app/assets/javascripts/groups/components/item_caret.vue
+++ b/app/assets/javascripts/groups/components/item_caret.vue
@@ -1,9 +1,9 @@
<script>
-import icon from '~/vue_shared/components/icon.vue';
+import { GlIcon } from '@gitlab/ui';
export default {
components: {
- icon,
+ GlIcon,
},
props: {
isGroupOpen: {
@@ -21,5 +21,5 @@ export default {
</script>
<template>
- <span class="folder-caret gl-mr-2"> <icon :size="10" :name="iconClass" /> </span>
+ <span class="folder-caret gl-mr-2"> <gl-icon :size="10" :name="iconClass" /> </span>
</template>
diff --git a/app/assets/javascripts/groups/components/item_stats_value.vue b/app/assets/javascripts/groups/components/item_stats_value.vue
index 27b1c632643..18efd8c6823 100644
--- a/app/assets/javascripts/groups/components/item_stats_value.vue
+++ b/app/assets/javascripts/groups/components/item_stats_value.vue
@@ -1,10 +1,10 @@
<script>
+import { GlIcon } from '@gitlab/ui';
import tooltip from '~/vue_shared/directives/tooltip';
-import icon from '~/vue_shared/components/icon.vue';
export default {
components: {
- icon,
+ GlIcon,
},
directives: {
tooltip,
@@ -57,6 +57,6 @@ export default {
:title="title"
data-container="body"
>
- <icon :name="iconName" /> <span v-if="isValuePresent" class="stat-value"> {{ value }} </span>
+ <gl-icon :name="iconName" /> <span v-if="isValuePresent" class="stat-value"> {{ value }} </span>
</span>
</template>
diff --git a/app/assets/javascripts/groups/components/item_type_icon.vue b/app/assets/javascripts/groups/components/item_type_icon.vue
index ae69fbd7bde..c3787c2df21 100644
--- a/app/assets/javascripts/groups/components/item_type_icon.vue
+++ b/app/assets/javascripts/groups/components/item_type_icon.vue
@@ -1,10 +1,10 @@
<script>
-import icon from '~/vue_shared/components/icon.vue';
+import { GlIcon } from '@gitlab/ui';
import { ITEM_TYPE } from '../constants';
export default {
components: {
- icon,
+ GlIcon,
},
props: {
itemType: {
@@ -29,5 +29,5 @@ export default {
</script>
<template>
- <span class="item-type-icon"> <icon :name="iconClass" /> </span>
+ <span class="item-type-icon"> <gl-icon :name="iconClass" /> </span>
</template>
diff --git a/app/assets/javascripts/groups/init_invite_members_banner.js b/app/assets/javascripts/groups/init_invite_members_banner.js
new file mode 100644
index 00000000000..c7967827917
--- /dev/null
+++ b/app/assets/javascripts/groups/init_invite_members_banner.js
@@ -0,0 +1,23 @@
+import Vue from 'vue';
+import InviteMembersBanner from '~/groups/components/invite_members_banner.vue';
+
+export default function initInviteMembersBanner() {
+ const el = document.querySelector('.js-group-invite-members-banner');
+
+ if (!el) {
+ return false;
+ }
+
+ const { svgPath, inviteMembersPath, isDismissedKey, trackLabel } = el.dataset;
+
+ return new Vue({
+ el,
+ provide: {
+ svgPath,
+ inviteMembersPath,
+ isDismissedKey,
+ trackLabel,
+ },
+ render: createElement => createElement(InviteMembersBanner),
+ });
+}
diff --git a/app/assets/javascripts/groups/members/components/app.vue b/app/assets/javascripts/groups/members/components/app.vue
new file mode 100644
index 00000000000..e94b28f5773
--- /dev/null
+++ b/app/assets/javascripts/groups/members/components/app.vue
@@ -0,0 +1,11 @@
+<script>
+export default {
+ name: 'GroupMembersApp',
+};
+</script>
+
+<template>
+ <span>
+ <!-- Temporary empty template -->
+ </span>
+</template>
diff --git a/app/assets/javascripts/groups/members/index.js b/app/assets/javascripts/groups/members/index.js
new file mode 100644
index 00000000000..4ca1756f10c
--- /dev/null
+++ b/app/assets/javascripts/groups/members/index.js
@@ -0,0 +1,30 @@
+import Vue from 'vue';
+import Vuex from 'vuex';
+import App from './components/app.vue';
+import membersModule from '~/vuex_shared/modules/members';
+import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
+
+export default el => {
+ if (!el) {
+ return () => {};
+ }
+
+ Vue.use(Vuex);
+
+ const { members, groupId } = el.dataset;
+
+ const store = new Vuex.Store({
+ ...membersModule({
+ members: convertObjectPropsToCamelCase(JSON.parse(members), { deep: true }),
+ sourceId: parseInt(groupId, 10),
+ currentUserId: gon.current_user_id || null,
+ }),
+ });
+
+ return new Vue({
+ el,
+ components: { App },
+ store,
+ render: createElement => createElement('app'),
+ });
+};
diff --git a/app/assets/javascripts/groups/transfer_dropdown.js b/app/assets/javascripts/groups/transfer_dropdown.js
index e94b163dfb1..cefd803c631 100644
--- a/app/assets/javascripts/groups/transfer_dropdown.js
+++ b/app/assets/javascripts/groups/transfer_dropdown.js
@@ -1,5 +1,6 @@
import $ from 'jquery';
import { __ } from '~/locale';
+import initDeprecatedJQueryDropdown from '~/deprecated_jquery_dropdown';
export default class TransferDropdown {
constructor() {
@@ -16,7 +17,7 @@ export default class TransferDropdown {
buildDropdown() {
const extraOptions = [{ id: '-1', text: __('No parent group') }, { type: 'divider' }];
- this.groupDropdown.glDropdown({
+ initDeprecatedJQueryDropdown(this.groupDropdown, {
selectable: true,
filterable: true,
toggleLabel: item => item.text,
diff --git a/app/assets/javascripts/groups_select.js b/app/assets/javascripts/groups_select.js
index 4c50bb3a9ac..aac23db8fd6 100644
--- a/app/assets/javascripts/groups_select.js
+++ b/app/assets/javascripts/groups_select.js
@@ -97,7 +97,10 @@ const groupsSelect = () => {
});
};
-export default () =>
- import(/* webpackChunkName: 'select2' */ 'select2/select2')
- .then(groupsSelect)
- .catch(() => {});
+export default () => {
+ if ($('.ajax-groups-select').length) {
+ import(/* webpackChunkName: 'select2' */ 'select2/select2')
+ .then(groupsSelect)
+ .catch(() => {});
+ }
+};
diff --git a/app/assets/javascripts/header.js b/app/assets/javascripts/header.js
index 3f9163e924d..b833cca1db6 100644
--- a/app/assets/javascripts/header.js
+++ b/app/assets/javascripts/header.js
@@ -2,9 +2,6 @@ import $ from 'jquery';
import Vue from 'vue';
import Translate from '~/vue_shared/translate';
import { highCountTrim } from '~/lib/utils/text_utility';
-import SetStatusModalTrigger from './set_status_modal/set_status_modal_trigger.vue';
-import SetStatusModalWrapper from './set_status_modal/set_status_modal_wrapper.vue';
-import { parseBoolean } from '~/lib/utils/common_utils';
import Tracking from '~/tracking';
/**
@@ -17,7 +14,7 @@ import Tracking from '~/tracking';
export default function initTodoToggle() {
$(document).on('todo:toggle', (e, count) => {
const updatedCount = count || e?.detail?.count || 0;
- const $todoPendingCount = $('.todos-count');
+ const $todoPendingCount = $('.js-todos-count');
$todoPendingCount.text(highCountTrim(updatedCount));
$todoPendingCount.toggleClass('hidden', updatedCount === 0);
@@ -26,51 +23,43 @@ export default function initTodoToggle() {
function initStatusTriggers() {
const setStatusModalTriggerEl = document.querySelector('.js-set-status-modal-trigger');
- const setStatusModalWrapperEl = document.querySelector('.js-set-status-modal-wrapper');
- if (setStatusModalTriggerEl || setStatusModalWrapperEl) {
- Vue.use(Translate);
+ if (setStatusModalTriggerEl) {
+ setStatusModalTriggerEl.addEventListener('click', () => {
+ import(
+ /* webpackChunkName: 'statusModalBundle' */ './set_status_modal/set_status_modal_wrapper.vue'
+ )
+ .then(({ default: SetStatusModalWrapper }) => {
+ const setStatusModalWrapperEl = document.querySelector('.js-set-status-modal-wrapper');
+ const statusModalElement = document.createElement('div');
+ setStatusModalWrapperEl.appendChild(statusModalElement);
- // eslint-disable-next-line no-new
- new Vue({
- el: setStatusModalTriggerEl,
- data() {
- const { hasStatus } = this.$options.el.dataset;
+ Vue.use(Translate);
- return {
- hasStatus: parseBoolean(hasStatus),
- };
- },
- render(createElement) {
- return createElement(SetStatusModalTrigger, {
- props: {
- hasStatus: this.hasStatus,
- },
- });
- },
- });
-
- // eslint-disable-next-line no-new
- new Vue({
- el: setStatusModalWrapperEl,
- data() {
- const { currentEmoji, currentMessage } = this.$options.el.dataset;
+ // eslint-disable-next-line no-new
+ new Vue({
+ el: statusModalElement,
+ data() {
+ const { currentEmoji, currentMessage } = setStatusModalWrapperEl.dataset;
- return {
- currentEmoji,
- currentMessage,
- };
- },
- render(createElement) {
- const { currentEmoji, currentMessage } = this;
+ return {
+ currentEmoji,
+ currentMessage,
+ };
+ },
+ render(createElement) {
+ const { currentEmoji, currentMessage } = this;
- return createElement(SetStatusModalWrapper, {
- props: {
- currentEmoji,
- currentMessage,
- },
- });
- },
+ return createElement(SetStatusModalWrapper, {
+ props: {
+ currentEmoji,
+ currentMessage,
+ },
+ });
+ },
+ });
+ })
+ .catch(() => {});
});
}
}
@@ -101,5 +90,5 @@ export function initNavUserDropdownTracking() {
document.addEventListener('DOMContentLoaded', () => {
requestIdleCallback(initStatusTriggers);
- initNavUserDropdownTracking();
+ requestIdleCallback(initNavUserDropdownTracking);
});
diff --git a/app/assets/javascripts/helpers/startup_css_helper.js b/app/assets/javascripts/helpers/startup_css_helper.js
new file mode 100644
index 00000000000..8e25e1421c0
--- /dev/null
+++ b/app/assets/javascripts/helpers/startup_css_helper.js
@@ -0,0 +1,46 @@
+const CSS_LOADED_EVENT = 'CSSLoaded';
+const STARTUP_LINK_LOADED_EVENT = 'CSSStartupLinkLoaded';
+
+const getAllStartupLinks = (() => {
+ let links = null;
+ return () => {
+ if (!links) {
+ links = Array.from(document.querySelectorAll('link[data-startupcss]'));
+ }
+ return links;
+ };
+})();
+const isStartupLinkLoaded = ({ dataset }) => dataset.startupcss === 'loaded';
+const allLinksLoaded = () => getAllStartupLinks().every(isStartupLinkLoaded);
+
+const handleStartupEvents = () => {
+ if (allLinksLoaded()) {
+ document.dispatchEvent(new CustomEvent(CSS_LOADED_EVENT));
+ document.removeEventListener(STARTUP_LINK_LOADED_EVENT, handleStartupEvents);
+ }
+};
+
+/* Wait for.... The methods can be used:
+ - with a callback (preferred),
+ waitFor(action)
+
+ - with then (discouraged),
+ await waitFor().then(action);
+
+ - with await,
+ await waitFor;
+ action();
+-*/
+export const waitForCSSLoaded = (action = () => {}) => {
+ if (!gon.features.startupCss || allLinksLoaded()) {
+ return new Promise(resolve => {
+ action();
+ resolve();
+ });
+ }
+
+ return new Promise(resolve => {
+ document.addEventListener(CSS_LOADED_EVENT, resolve, { once: true });
+ document.addEventListener(STARTUP_LINK_LOADED_EVENT, handleStartupEvents);
+ }).then(action);
+};
diff --git a/app/assets/javascripts/ide/components/activity_bar.vue b/app/assets/javascripts/ide/components/activity_bar.vue
index a65af55fcac..183816921c1 100644
--- a/app/assets/javascripts/ide/components/activity_bar.vue
+++ b/app/assets/javascripts/ide/components/activity_bar.vue
@@ -1,13 +1,13 @@
<script>
import $ from 'jquery';
import { mapActions, mapState } from 'vuex';
-import Icon from '~/vue_shared/components/icon.vue';
+import { GlIcon } from '@gitlab/ui';
import tooltip from '~/vue_shared/directives/tooltip';
import { leftSidebarViews } from '../constants';
export default {
components: {
- Icon,
+ GlIcon,
},
directives: {
tooltip,
@@ -44,11 +44,12 @@ export default {
:aria-label="s__('IDE|Edit')"
data-container="body"
data-placement="right"
+ data-qa-selector="edit_mode_tab"
type="button"
class="ide-sidebar-link js-ide-edit-mode"
@click.prevent="changedActivityView($event, $options.leftSidebarViews.edit.name)"
>
- <icon name="code" />
+ <gl-icon name="code" />
</button>
</li>
<li>
@@ -65,7 +66,7 @@ export default {
class="ide-sidebar-link js-ide-review-mode"
@click.prevent="changedActivityView($event, $options.leftSidebarViews.review.name)"
>
- <icon name="file-modified" />
+ <gl-icon name="file-modified" />
</button>
</li>
<li>
@@ -78,11 +79,12 @@ export default {
:aria-label="s__('IDE|Commit')"
data-container="body"
data-placement="right"
+ data-qa-selector="commit_mode_tab"
type="button"
- class="ide-sidebar-link js-ide-commit-mode qa-commit-mode-tab"
+ class="ide-sidebar-link js-ide-commit-mode"
@click.prevent="changedActivityView($event, $options.leftSidebarViews.commit.name)"
>
- <icon name="commit" />
+ <gl-icon name="commit" />
</button>
</li>
</ul>
diff --git a/app/assets/javascripts/ide/components/branches/item.vue b/app/assets/javascripts/ide/components/branches/item.vue
index 49744d573da..2fe435b92ab 100644
--- a/app/assets/javascripts/ide/components/branches/item.vue
+++ b/app/assets/javascripts/ide/components/branches/item.vue
@@ -1,11 +1,11 @@
<script>
/* eslint-disable @gitlab/vue-require-i18n-strings */
-import Icon from '~/vue_shared/components/icon.vue';
+import { GlIcon } from '@gitlab/ui';
import Timeago from '~/vue_shared/components/time_ago_tooltip.vue';
export default {
components: {
- Icon,
+ GlIcon,
Timeago,
},
props: {
@@ -34,7 +34,7 @@ export default {
<template>
<a :href="branchHref" class="btn-link d-flex align-items-center">
<span class="d-flex gl-mr-3 ide-search-list-current-icon">
- <icon v-if="isActive" :size="18" name="mobile-issue-close" />
+ <gl-icon v-if="isActive" :size="18" name="mobile-issue-close" />
</span>
<span>
<strong> {{ item.name }} </strong>
diff --git a/app/assets/javascripts/ide/components/branches/search_list.vue b/app/assets/javascripts/ide/components/branches/search_list.vue
index dd2d726d525..c317fadb656 100644
--- a/app/assets/javascripts/ide/components/branches/search_list.vue
+++ b/app/assets/javascripts/ide/components/branches/search_list.vue
@@ -1,14 +1,13 @@
<script>
import { mapActions, mapState } from 'vuex';
import { debounce } from 'lodash';
-import { GlLoadingIcon } from '@gitlab/ui';
-import Icon from '~/vue_shared/components/icon.vue';
+import { GlLoadingIcon, GlIcon } from '@gitlab/ui';
import Item from './item.vue';
export default {
components: {
Item,
- Icon,
+ GlIcon,
GlLoadingIcon,
},
data() {
@@ -67,7 +66,7 @@ export default {
class="form-control dropdown-input-field"
@input="searchBranches"
/>
- <icon :size="18" name="search" class="ml-3 input-icon" />
+ <gl-icon :size="18" name="search" class="ml-3 input-icon" />
</label>
<div class="dropdown-content ide-merge-requests-dropdown-content d-flex">
<gl-loading-icon
diff --git a/app/assets/javascripts/ide/components/commit_sidebar/actions.vue b/app/assets/javascripts/ide/components/commit_sidebar/actions.vue
index 407e4c57cd8..de4b0a34002 100644
--- a/app/assets/javascripts/ide/components/commit_sidebar/actions.vue
+++ b/app/assets/javascripts/ide/components/commit_sidebar/actions.vue
@@ -1,4 +1,5 @@
<script>
+/* eslint-disable vue/no-v-html */
import { escape } from 'lodash';
import { mapState, mapGetters, createNamespacedHelpers } from 'vuex';
import { sprintf, s__ } from '~/locale';
diff --git a/app/assets/javascripts/ide/components/commit_sidebar/form.vue b/app/assets/javascripts/ide/components/commit_sidebar/form.vue
index 9342ab87c1a..73c56514fce 100644
--- a/app/assets/javascripts/ide/components/commit_sidebar/form.vue
+++ b/app/assets/javascripts/ide/components/commit_sidebar/form.vue
@@ -1,6 +1,6 @@
<script>
import { mapState, mapActions, mapGetters } from 'vuex';
-import { GlModal } from '@gitlab/ui';
+import { GlModal, GlSafeHtmlDirective } from '@gitlab/ui';
import { n__, __ } from '~/locale';
import LoadingButton from '~/vue_shared/components/loading_button.vue';
import CommitMessageField from './message_field.vue';
@@ -8,6 +8,7 @@ import Actions from './actions.vue';
import SuccessMessage from './success_message.vue';
import { leftSidebarViews, MAX_WINDOW_HEIGHT_COMPACT } from '../../constants';
import consts from '../../stores/modules/commit/constants';
+import { createUnexpectedCommitError } from '../../lib/errors';
export default {
components: {
@@ -17,15 +18,20 @@ export default {
SuccessMessage,
GlModal,
},
+ directives: {
+ SafeHtml: GlSafeHtmlDirective,
+ },
data() {
return {
isCompact: true,
componentHeight: null,
+ // Keep track of "lastCommitError" so we hold onto the value even when "commitError" is cleared.
+ lastCommitError: createUnexpectedCommitError(),
};
},
computed: {
...mapState(['changedFiles', 'stagedFiles', 'currentActivityView', 'lastCommitMsg']),
- ...mapState('commit', ['commitMessage', 'submitCommitLoading']),
+ ...mapState('commit', ['commitMessage', 'submitCommitLoading', 'commitError']),
...mapGetters(['someUncommittedChanges']),
...mapGetters('commit', ['discardDraftButtonDisabled', 'preBuiltCommitMessage']),
overviewText() {
@@ -38,11 +44,28 @@ export default {
currentViewIsCommitView() {
return this.currentActivityView === leftSidebarViews.commit.name;
},
+ commitErrorPrimaryAction() {
+ if (!this.lastCommitError?.canCreateBranch) {
+ return undefined;
+ }
+
+ return {
+ text: __('Create new branch'),
+ };
+ },
},
watch: {
currentActivityView: 'handleCompactState',
someUncommittedChanges: 'handleCompactState',
lastCommitMsg: 'handleCompactState',
+ commitError(val) {
+ if (!val) {
+ return;
+ }
+
+ this.lastCommitError = val;
+ this.$refs.commitErrorModal.show();
+ },
},
methods: {
...mapActions(['updateActivityBarView']),
@@ -53,9 +76,7 @@ export default {
'updateCommitAction',
]),
commit() {
- return this.commitChanges().catch(() => {
- this.$refs.createBranchModal.show();
- });
+ return this.commitChanges();
},
forceCreateNewBranch() {
return this.updateCommitAction(consts.COMMIT_TO_NEW_BRANCH).then(() => this.commit());
@@ -164,17 +185,14 @@ export default {
</button>
</div>
<gl-modal
- ref="createBranchModal"
- modal-id="ide-create-branch-modal"
- :ok-title="__('Create new branch')"
- :title="__('Branch has changed')"
- ok-variant="success"
+ ref="commitErrorModal"
+ modal-id="ide-commit-error-modal"
+ :title="lastCommitError.title"
+ :action-primary="commitErrorPrimaryAction"
+ :action-cancel="{ text: __('Cancel') }"
@ok="forceCreateNewBranch"
>
- {{
- __(`This branch has changed since you started editing.
- Would you like to create a new branch?`)
- }}
+ <div v-safe-html="lastCommitError.messageHTML"></div>
</gl-modal>
</form>
</transition>
diff --git a/app/assets/javascripts/ide/components/commit_sidebar/list.vue b/app/assets/javascripts/ide/components/commit_sidebar/list.vue
index d1422a506e7..609ce287d3f 100644
--- a/app/assets/javascripts/ide/components/commit_sidebar/list.vue
+++ b/app/assets/javascripts/ide/components/commit_sidebar/list.vue
@@ -1,14 +1,13 @@
<script>
import { mapActions } from 'vuex';
-import { GlModal } from '@gitlab/ui';
+import { GlModal, GlIcon } from '@gitlab/ui';
import { __, sprintf } from '~/locale';
-import Icon from '~/vue_shared/components/icon.vue';
import tooltip from '~/vue_shared/directives/tooltip';
import ListItem from './list_item.vue';
export default {
components: {
- Icon,
+ GlIcon,
ListItem,
GlModal,
},
@@ -74,7 +73,7 @@ export default {
<div class="ide-commit-list-container">
<header class="multi-file-commit-panel-header d-flex mb-0">
<div class="d-flex align-items-center flex-fill">
- <icon v-once :name="iconName" :size="18" class="gl-mr-3" />
+ <gl-icon v-once :name="iconName" :size="18" class="gl-mr-3" />
<strong> {{ titleText }} </strong>
<div class="d-flex ml-auto">
<button
@@ -93,7 +92,7 @@ export default {
data-boundary="viewport"
@click="openDiscardModal"
>
- <icon :size="16" name="remove-all" class="ml-auto mr-auto position-top-0" />
+ <gl-icon :size="16" name="remove-all" class="ml-auto mr-auto position-top-0" />
</button>
</div>
</div>
diff --git a/app/assets/javascripts/ide/components/commit_sidebar/list_collapsed.vue b/app/assets/javascripts/ide/components/commit_sidebar/list_collapsed.vue
index 1b257ca11cc..4821b8389ff 100644
--- a/app/assets/javascripts/ide/components/commit_sidebar/list_collapsed.vue
+++ b/app/assets/javascripts/ide/components/commit_sidebar/list_collapsed.vue
@@ -1,11 +1,11 @@
<script>
-import Icon from '~/vue_shared/components/icon.vue';
+import { GlIcon } from '@gitlab/ui';
import tooltip from '~/vue_shared/directives/tooltip';
import { sprintf, n__, __ } from '~/locale';
export default {
components: {
- Icon,
+ GlIcon,
},
directives: {
tooltip,
@@ -77,7 +77,7 @@ export default {
data-placement="left"
class="gl-mb-5"
>
- <icon v-once :name="iconName" :size="18" />
+ <gl-icon v-once :name="iconName" :size="18" />
</div>
<div
v-tooltip
@@ -86,7 +86,7 @@ export default {
data-placement="left"
class="gl-mb-3"
>
- <icon :name="additionIconName" :size="18" :class="addedFilesIconClass" />
+ <gl-icon :name="additionIconName" :size="18" :class="addedFilesIconClass" />
</div>
{{ addedFilesLength }}
<div
@@ -96,7 +96,7 @@ export default {
data-placement="left"
class="gl-mt-3 gl-mb-3"
>
- <icon :name="modifiedIconName" :size="18" :class="modifiedFilesClass" />
+ <gl-icon :name="modifiedIconName" :size="18" :class="modifiedFilesClass" />
</div>
{{ modifiedFilesLength }}
</div>
diff --git a/app/assets/javascripts/ide/components/commit_sidebar/list_item.vue b/app/assets/javascripts/ide/components/commit_sidebar/list_item.vue
index c65169f5d31..a0d6cf3c42d 100644
--- a/app/assets/javascripts/ide/components/commit_sidebar/list_item.vue
+++ b/app/assets/javascripts/ide/components/commit_sidebar/list_item.vue
@@ -1,14 +1,14 @@
<script>
import { mapActions } from 'vuex';
+import { GlIcon } from '@gitlab/ui';
import tooltip from '~/vue_shared/directives/tooltip';
-import Icon from '~/vue_shared/components/icon.vue';
import FileIcon from '~/vue_shared/components/file_icon.vue';
import { viewerTypes } from '../../constants';
import getCommitIconMap from '../../commit_icon';
export default {
components: {
- Icon,
+ GlIcon,
FileIcon,
},
directives: {
@@ -95,7 +95,7 @@ export default {
</span>
<div class="ml-auto d-flex align-items-center">
<div class="d-flex align-items-center ide-commit-list-changed-icon">
- <icon :name="iconName" :size="16" :class="iconClass" />
+ <gl-icon :name="iconName" :size="16" :class="iconClass" />
</div>
</div>
</div>
diff --git a/app/assets/javascripts/ide/components/commit_sidebar/message_field.vue b/app/assets/javascripts/ide/components/commit_sidebar/message_field.vue
index b37c7280a30..2787b10a48b 100644
--- a/app/assets/javascripts/ide/components/commit_sidebar/message_field.vue
+++ b/app/assets/javascripts/ide/components/commit_sidebar/message_field.vue
@@ -1,6 +1,6 @@
<script>
+import { GlIcon } from '@gitlab/ui';
import { __, sprintf } from '../../../locale';
-import Icon from '../../../vue_shared/components/icon.vue';
import popover from '../../../vue_shared/directives/popover';
import { MAX_TITLE_LENGTH, MAX_BODY_LENGTH } from '../../constants';
@@ -9,7 +9,7 @@ export default {
popover,
},
components: {
- Icon,
+ GlIcon,
},
props: {
text: {
@@ -84,7 +84,7 @@ export default {
<li>
{{ __('Commit Message') }}
<span v-popover="$options.popoverOptions" class="form-text text-muted gl-ml-3">
- <icon name="question" />
+ <gl-icon name="question" />
</span>
</li>
</ul>
diff --git a/app/assets/javascripts/ide/components/commit_sidebar/success_message.vue b/app/assets/javascripts/ide/components/commit_sidebar/success_message.vue
index 327b0b8172f..977efb0ca22 100644
--- a/app/assets/javascripts/ide/components/commit_sidebar/success_message.vue
+++ b/app/assets/javascripts/ide/components/commit_sidebar/success_message.vue
@@ -1,4 +1,5 @@
<script>
+/* eslint-disable vue/no-v-html */
import { mapState } from 'vuex';
export default {
diff --git a/app/assets/javascripts/ide/components/error_message.vue b/app/assets/javascripts/ide/components/error_message.vue
index d36adbd798e..08635b43b91 100644
--- a/app/assets/javascripts/ide/components/error_message.vue
+++ b/app/assets/javascripts/ide/components/error_message.vue
@@ -1,4 +1,5 @@
<script>
+/* eslint-disable vue/no-v-html */
import { mapActions } from 'vuex';
import { GlAlert, GlLoadingIcon } from '@gitlab/ui';
diff --git a/app/assets/javascripts/ide/components/file_row_extra.vue b/app/assets/javascripts/ide/components/file_row_extra.vue
index f7cf7a5b251..48ab58e1cb7 100644
--- a/app/assets/javascripts/ide/components/file_row_extra.vue
+++ b/app/assets/javascripts/ide/components/file_row_extra.vue
@@ -1,8 +1,8 @@
<script>
import { mapGetters } from 'vuex';
+import { GlIcon } from '@gitlab/ui';
import { n__ } from '~/locale';
import tooltip from '~/vue_shared/directives/tooltip';
-import Icon from '~/vue_shared/components/icon.vue';
import ChangedFileIcon from '~/vue_shared/components/changed_file_icon.vue';
import NewDropdown from './new_dropdown/index.vue';
import MrFileIcon from './mr_file_icon.vue';
@@ -13,7 +13,7 @@ export default {
tooltip,
},
components: {
- Icon,
+ GlIcon,
NewDropdown,
ChangedFileIcon,
MrFileIcon,
@@ -69,7 +69,7 @@ export default {
<mr-file-icon v-if="file.mrChange" />
<span v-if="showTreeChangesCount" class="ide-tree-changes">
{{ changesCount }}
- <icon
+ <gl-icon
v-tooltip
:title="folderChangesTooltip"
:size="12"
diff --git a/app/assets/javascripts/ide/components/ide.vue b/app/assets/javascripts/ide/components/ide.vue
index 55b3eaf9737..1b03d9eee8b 100644
--- a/app/assets/javascripts/ide/components/ide.vue
+++ b/app/assets/javascripts/ide/components/ide.vue
@@ -47,6 +47,7 @@ export default {
'emptyRepo',
'currentTree',
'editorTheme',
+ 'getUrlForPath',
]),
themeName() {
return window.gon?.user_color_scheme;
@@ -71,7 +72,7 @@ export default {
return returnValue;
},
openFile(file) {
- this.$router.push(`/project${file.url}`);
+ this.$router.push(this.getUrlForPath(file.path));
},
createNewFile() {
this.$refs.newModal.open(modalTypes.blob);
diff --git a/app/assets/javascripts/ide/components/ide_file_row.vue b/app/assets/javascripts/ide/components/ide_file_row.vue
index b777d89f0bb..248677d6a99 100644
--- a/app/assets/javascripts/ide/components/ide_file_row.vue
+++ b/app/assets/javascripts/ide/components/ide_file_row.vue
@@ -3,6 +3,7 @@
* This component is an iterative step towards refactoring and simplifying `vue_shared/components/file_row.vue`
* https://gitlab.com/gitlab-org/gitlab/-/merge_requests/23720
*/
+import { mapGetters } from 'vuex';
import FileRow from '~/vue_shared/components/file_row.vue';
import FileRowExtra from './file_row_extra.vue';
@@ -23,6 +24,9 @@ export default {
dropdownOpen: false,
};
},
+ computed: {
+ ...mapGetters(['getUrlForPath']),
+ },
methods: {
toggleDropdown(val) {
this.dropdownOpen = val;
@@ -32,7 +36,13 @@ export default {
</script>
<template>
- <file-row :file="file" v-bind="$attrs" @mouseleave="toggleDropdown(false)" v-on="$listeners">
+ <file-row
+ :file="file"
+ :file-url="getUrlForPath(file.path)"
+ v-bind="$attrs"
+ @mouseleave="toggleDropdown(false)"
+ v-on="$listeners"
+ >
<file-row-extra :file="file" :dropdown-open="dropdownOpen" @toggle="toggleDropdown($event)" />
</file-row>
</template>
diff --git a/app/assets/javascripts/ide/components/ide_review.vue b/app/assets/javascripts/ide/components/ide_review.vue
index 95348711e1d..e36d0a5a5b1 100644
--- a/app/assets/javascripts/ide/components/ide_review.vue
+++ b/app/assets/javascripts/ide/components/ide_review.vue
@@ -10,7 +10,7 @@ export default {
EditorModeDropdown,
},
computed: {
- ...mapGetters(['currentMergeRequest', 'activeFile']),
+ ...mapGetters(['currentMergeRequest', 'activeFile', 'getUrlForPath']),
...mapState(['viewer', 'currentMergeRequestId']),
showLatestChangesText() {
return !this.currentMergeRequestId || this.viewer === viewerTypes.diff;
@@ -24,7 +24,7 @@ export default {
},
mounted() {
if (this.activeFile && this.activeFile.pending && !this.activeFile.deleted) {
- this.$router.push(`/project${this.activeFile.url}`, () => {
+ this.$router.push(this.getUrlForPath(this.activeFile.path), () => {
this.updateViewer('editor');
});
} else if (this.activeFile && this.activeFile.deleted) {
diff --git a/app/assets/javascripts/ide/components/ide_side_bar.vue b/app/assets/javascripts/ide/components/ide_side_bar.vue
index 1eb89b41495..ed68ca5cae9 100644
--- a/app/assets/javascripts/ide/components/ide_side_bar.vue
+++ b/app/assets/javascripts/ide/components/ide_side_bar.vue
@@ -1,6 +1,6 @@
<script>
import { mapState, mapGetters } from 'vuex';
-import { GlSkeletonLoading } from '@gitlab/ui';
+import { GlDeprecatedSkeletonLoading as GlSkeletonLoading } from '@gitlab/ui';
import IdeTree from './ide_tree.vue';
import ResizablePanel from './resizable_panel.vue';
import ActivityBar from './activity_bar.vue';
diff --git a/app/assets/javascripts/ide/components/ide_status_bar.vue b/app/assets/javascripts/ide/components/ide_status_bar.vue
index ddc126c3d77..146e818d654 100644
--- a/app/assets/javascripts/ide/components/ide_status_bar.vue
+++ b/app/assets/javascripts/ide/components/ide_status_bar.vue
@@ -1,9 +1,9 @@
<script>
/* eslint-disable @gitlab/vue-require-i18n-strings */
import { mapActions, mapState, mapGetters } from 'vuex';
+import { GlIcon } from '@gitlab/ui';
import IdeStatusList from './ide_status_list.vue';
import IdeStatusMr from './ide_status_mr.vue';
-import icon from '~/vue_shared/components/icon.vue';
import tooltip from '~/vue_shared/directives/tooltip';
import timeAgoMixin from '~/vue_shared/mixins/timeago';
import CiIcon from '../../vue_shared/components/ci_icon.vue';
@@ -12,7 +12,7 @@ import { rightSidebarViews } from '../constants';
export default {
components: {
- icon,
+ GlIcon,
userAvatarImage,
CiIcon,
IdeStatusList,
@@ -97,12 +97,13 @@ export default {
{{ latestPipeline.details.status.text }} for
</span>
- <icon name="commit" />
+ <gl-icon name="commit" />
<a
v-tooltip
:title="lastCommit.message"
:href="getCommitPath(lastCommit.short_id)"
class="commit-sha"
+ data-qa-selector="commit_sha_content"
>{{ lastCommit.short_id }}</a
>
by
diff --git a/app/assets/javascripts/ide/components/ide_status_list.vue b/app/assets/javascripts/ide/components/ide_status_list.vue
index 1354fdc3d98..caa122f6ed2 100644
--- a/app/assets/javascripts/ide/components/ide_status_list.vue
+++ b/app/assets/javascripts/ide/components/ide_status_list.vue
@@ -2,7 +2,7 @@
import { mapGetters } from 'vuex';
import { GlLink, GlTooltipDirective } from '@gitlab/ui';
import TerminalSyncStatusSafe from './terminal_sync/terminal_sync_status_safe.vue';
-import { getFileEOL } from '../utils';
+import { isTextFile, getFileEOL } from '~/ide/utils';
export default {
components: {
@@ -17,6 +17,9 @@ export default {
activeFileEOL() {
return getFileEOL(this.activeFile.content);
},
+ activeFileIsText() {
+ return isTextFile(this.activeFile);
+ },
},
};
</script>
@@ -30,7 +33,7 @@ export default {
</gl-link>
</div>
<div>{{ activeFileEOL }}</div>
- <div v-if="!activeFile.binary">{{ activeFile.editorRow }}:{{ activeFile.editorColumn }}</div>
+ <div v-if="activeFileIsText">{{ activeFile.editorRow }}:{{ activeFile.editorColumn }}</div>
<div>{{ activeFile.fileLanguage }}</div>
</template>
<terminal-sync-status-safe />
diff --git a/app/assets/javascripts/ide/components/ide_tree.vue b/app/assets/javascripts/ide/components/ide_tree.vue
index 647f4d4be85..747d5044790 100644
--- a/app/assets/javascripts/ide/components/ide_tree.vue
+++ b/app/assets/javascripts/ide/components/ide_tree.vue
@@ -15,13 +15,13 @@ export default {
},
computed: {
...mapState(['currentBranchId']),
- ...mapGetters(['currentProject', 'currentTree', 'activeFile']),
+ ...mapGetters(['currentProject', 'currentTree', 'activeFile', 'getUrlForPath']),
},
mounted() {
if (!this.activeFile) return;
if (this.activeFile.pending && !this.activeFile.deleted) {
- this.$router.push(`/project${this.activeFile.url}`, () => {
+ this.$router.push(this.getUrlForPath(this.activeFile.path), () => {
this.updateViewer('editor');
});
} else if (this.activeFile.deleted) {
diff --git a/app/assets/javascripts/ide/components/ide_tree_list.vue b/app/assets/javascripts/ide/components/ide_tree_list.vue
index 36e8951bea3..776d8459515 100644
--- a/app/assets/javascripts/ide/components/ide_tree_list.vue
+++ b/app/assets/javascripts/ide/components/ide_tree_list.vue
@@ -1,6 +1,6 @@
<script>
import { mapActions, mapGetters, mapState } from 'vuex';
-import { GlSkeletonLoading } from '@gitlab/ui';
+import { GlDeprecatedSkeletonLoading as GlSkeletonLoading } from '@gitlab/ui';
import FileTree from '~/vue_shared/components/file_tree.vue';
import IdeFileRow from './ide_file_row.vue';
import NavDropdown from './nav_dropdown.vue';
diff --git a/app/assets/javascripts/ide/components/jobs/detail.vue b/app/assets/javascripts/ide/components/jobs/detail.vue
index 975d54c7a4e..11033a5cc88 100644
--- a/app/assets/javascripts/ide/components/jobs/detail.vue
+++ b/app/assets/javascripts/ide/components/jobs/detail.vue
@@ -1,9 +1,10 @@
<script>
+/* eslint-disable vue/no-v-html */
import { mapActions, mapState } from 'vuex';
import { throttle } from 'lodash';
+import { GlIcon } from '@gitlab/ui';
import { __ } from '../../../locale';
import tooltip from '../../../vue_shared/directives/tooltip';
-import Icon from '../../../vue_shared/components/icon.vue';
import ScrollButton from './detail/scroll_button.vue';
import JobDescription from './detail/description.vue';
@@ -17,7 +18,7 @@ export default {
tooltip,
},
components: {
- Icon,
+ GlIcon,
ScrollButton,
JobDescription,
},
@@ -39,10 +40,10 @@ export default {
},
},
mounted() {
- this.getTrace();
+ this.getLogs();
},
methods: {
- ...mapActions('pipelines', ['fetchJobTrace', 'setDetailJob']),
+ ...mapActions('pipelines', ['fetchJobLogs', 'setDetailJob']),
scrollDown() {
if (this.$refs.buildTrace) {
this.$refs.buildTrace.scrollTo(0, this.$refs.buildTrace.scrollHeight);
@@ -65,8 +66,8 @@ export default {
this.scrollPos = '';
}
}),
- getTrace() {
- return this.fetchJobTrace().then(() => this.scrollDown());
+ getLogs() {
+ return this.fetchJobLogs().then(() => this.scrollDown());
},
},
};
@@ -76,7 +77,7 @@ export default {
<div class="ide-pipeline build-page d-flex flex-column flex-fill">
<header class="ide-job-header d-flex align-items-center">
<button class="btn btn-default btn-sm d-flex" @click="setDetailJob(null)">
- <icon name="chevron-left" /> {{ __('View jobs') }}
+ <gl-icon name="chevron-left" /> {{ __('View jobs') }}
</button>
</header>
<div class="top-bar d-flex border-left-0 mr-3">
diff --git a/app/assets/javascripts/ide/components/jobs/detail/description.vue b/app/assets/javascripts/ide/components/jobs/detail/description.vue
index f1ba102fffe..9eaeabad5ef 100644
--- a/app/assets/javascripts/ide/components/jobs/detail/description.vue
+++ b/app/assets/javascripts/ide/components/jobs/detail/description.vue
@@ -1,10 +1,10 @@
<script>
-import Icon from '../../../../vue_shared/components/icon.vue';
+import { GlIcon } from '@gitlab/ui';
import CiIcon from '../../../../vue_shared/components/ci_icon.vue';
export default {
components: {
- Icon,
+ GlIcon,
CiIcon,
},
props: {
@@ -26,8 +26,14 @@ export default {
<ci-icon :status="job.status" :borderless="true" :size="24" class="d-flex" />
<span class="gl-ml-3">
{{ job.name }}
- <a :href="job.path" target="_blank" class="ide-external-link position-relative">
- {{ jobId }} <icon :size="12" name="external-link" />
+ <a
+ v-if="job.path"
+ :href="job.path"
+ target="_blank"
+ class="ide-external-link gl-relative"
+ data-testid="description-detail-link"
+ >
+ {{ jobId }} <gl-icon :size="12" name="external-link" />
</a>
</span>
</div>
diff --git a/app/assets/javascripts/ide/components/jobs/detail/scroll_button.vue b/app/assets/javascripts/ide/components/jobs/detail/scroll_button.vue
index 5674d3ffa80..2c679a3edc7 100644
--- a/app/assets/javascripts/ide/components/jobs/detail/scroll_button.vue
+++ b/app/assets/javascripts/ide/components/jobs/detail/scroll_button.vue
@@ -1,6 +1,6 @@
<script>
+import { GlIcon } from '@gitlab/ui';
import { __ } from '../../../../locale';
-import Icon from '../../../../vue_shared/components/icon.vue';
import tooltip from '../../../../vue_shared/directives/tooltip';
const directions = {
@@ -13,7 +13,7 @@ export default {
tooltip,
},
components: {
- Icon,
+ GlIcon,
},
props: {
direction: {
@@ -58,7 +58,7 @@ export default {
type="button"
@click="clickedScroll"
>
- <icon :name="iconName" />
+ <gl-icon :name="iconName" />
</button>
</div>
</template>
diff --git a/app/assets/javascripts/ide/components/jobs/stage.vue b/app/assets/javascripts/ide/components/jobs/stage.vue
index 75441e8c1c8..0b643947139 100644
--- a/app/assets/javascripts/ide/components/jobs/stage.vue
+++ b/app/assets/javascripts/ide/components/jobs/stage.vue
@@ -1,7 +1,6 @@
<script>
-import { GlLoadingIcon } from '@gitlab/ui';
+import { GlLoadingIcon, GlIcon } from '@gitlab/ui';
import tooltip from '../../../vue_shared/directives/tooltip';
-import Icon from '../../../vue_shared/components/icon.vue';
import CiIcon from '../../../vue_shared/components/ci_icon.vue';
import Item from './item.vue';
@@ -10,7 +9,7 @@ export default {
tooltip,
},
components: {
- Icon,
+ GlIcon,
CiIcon,
Item,
GlLoadingIcon,
@@ -78,7 +77,7 @@ export default {
<div v-if="!stage.isLoading || stage.jobs.length" class="gl-mr-3 gl-ml-2">
<span class="badge badge-pill"> {{ jobsCount }} </span>
</div>
- <icon :name="collapseIcon" class="ide-stage-collapse-icon" />
+ <gl-icon :name="collapseIcon" class="ide-stage-collapse-icon" />
</div>
<div v-show="!stage.isCollapsed" ref="jobList" class="card-body p-0">
<gl-loading-icon v-if="showLoadingIcon" />
diff --git a/app/assets/javascripts/ide/components/merge_requests/item.vue b/app/assets/javascripts/ide/components/merge_requests/item.vue
index 8b7b8d5a91c..7aa9a4f864a 100644
--- a/app/assets/javascripts/ide/components/merge_requests/item.vue
+++ b/app/assets/javascripts/ide/components/merge_requests/item.vue
@@ -1,9 +1,9 @@
<script>
-import Icon from '../../../vue_shared/components/icon.vue';
+import { GlIcon } from '@gitlab/ui';
export default {
components: {
- Icon,
+ GlIcon,
},
props: {
item: {
@@ -41,7 +41,7 @@ export default {
<template>
<a :href="mergeRequestHref" class="btn-link d-flex align-items-center">
<span class="d-flex gl-mr-3 ide-search-list-current-icon">
- <icon v-if="isActive" :size="18" name="mobile-issue-close" />
+ <gl-icon v-if="isActive" :size="18" name="mobile-issue-close" />
</span>
<span>
<strong> {{ item.title }} </strong>
diff --git a/app/assets/javascripts/ide/components/merge_requests/list.vue b/app/assets/javascripts/ide/components/merge_requests/list.vue
index af45d88b84a..4b3c6e61e11 100644
--- a/app/assets/javascripts/ide/components/merge_requests/list.vue
+++ b/app/assets/javascripts/ide/components/merge_requests/list.vue
@@ -1,9 +1,8 @@
<script>
import { mapActions, mapState } from 'vuex';
import { debounce } from 'lodash';
-import { GlLoadingIcon } from '@gitlab/ui';
+import { GlLoadingIcon, GlIcon } from '@gitlab/ui';
import { __ } from '~/locale';
-import Icon from '~/vue_shared/components/icon.vue';
import Item from './item.vue';
import TokenedInput from '../shared/tokened_input.vue';
@@ -16,7 +15,7 @@ export default {
components: {
TokenedInput,
Item,
- Icon,
+ GlIcon,
GlLoadingIcon,
},
data() {
@@ -85,7 +84,7 @@ export default {
@input="searchMergeRequests"
@removeToken="setSearchType(null)"
/>
- <icon :size="18" name="search" class="ml-3 input-icon" />
+ <gl-icon :size="18" name="search" class="ml-3 input-icon" />
</label>
<div class="dropdown-content ide-merge-requests-dropdown-content d-flex">
<gl-loading-icon
@@ -103,7 +102,7 @@ export default {
@click.stop="setSearchType(searchType)"
>
<span class="d-flex gl-mr-3 ide-search-list-current-icon">
- <icon :size="18" name="search" />
+ <gl-icon :size="18" name="search" />
</span>
<span>{{ searchType.label }}</span>
</button>
diff --git a/app/assets/javascripts/ide/components/mr_file_icon.vue b/app/assets/javascripts/ide/components/mr_file_icon.vue
index 4fab57b6f3e..c8629a869e0 100644
--- a/app/assets/javascripts/ide/components/mr_file_icon.vue
+++ b/app/assets/javascripts/ide/components/mr_file_icon.vue
@@ -1,10 +1,10 @@
<script>
-import icon from '~/vue_shared/components/icon.vue';
+import { GlIcon } from '@gitlab/ui';
import tooltip from '~/vue_shared/directives/tooltip';
export default {
components: {
- icon,
+ GlIcon,
},
directives: {
tooltip,
@@ -13,7 +13,7 @@ export default {
</script>
<template>
- <icon
+ <gl-icon
v-tooltip
:title="__('Part of merge request changes')"
:size="12"
diff --git a/app/assets/javascripts/ide/components/nav_dropdown_button.vue b/app/assets/javascripts/ide/components/nav_dropdown_button.vue
index 8dc22620eca..116d3cec03e 100644
--- a/app/assets/javascripts/ide/components/nav_dropdown_button.vue
+++ b/app/assets/javascripts/ide/components/nav_dropdown_button.vue
@@ -1,13 +1,13 @@
<script>
import { mapState } from 'vuex';
+import { GlIcon } from '@gitlab/ui';
import DropdownButton from '~/vue_shared/components/dropdown/dropdown_button.vue';
-import Icon from '~/vue_shared/components/icon.vue';
const EMPTY_LABEL = '-';
export default {
components: {
- Icon,
+ GlIcon,
DropdownButton,
},
props: {
@@ -33,10 +33,10 @@ export default {
<dropdown-button>
<span class="row flex-nowrap">
<span class="col-auto flex-fill text-truncate">
- <icon :size="16" :aria-label="__('Current Branch')" name="branch" /> {{ branchLabel }}
+ <gl-icon :size="16" :aria-label="__('Current Branch')" name="branch" /> {{ branchLabel }}
</span>
<span v-if="showMergeRequests" class="col-5 pl-0 text-truncate">
- <icon :size="16" :aria-label="__('Merge Request')" name="merge-request" />
+ <gl-icon :size="16" :aria-label="__('Merge Request')" name="merge-request" />
{{ mergeRequestLabel }}
</span>
</span>
diff --git a/app/assets/javascripts/ide/components/new_dropdown/button.vue b/app/assets/javascripts/ide/components/new_dropdown/button.vue
index 5bd6642930c..8ae8f97f237 100644
--- a/app/assets/javascripts/ide/components/new_dropdown/button.vue
+++ b/app/assets/javascripts/ide/components/new_dropdown/button.vue
@@ -1,5 +1,5 @@
<script>
-import Icon from '~/vue_shared/components/icon.vue';
+import { GlIcon } from '@gitlab/ui';
import tooltip from '~/vue_shared/directives/tooltip';
export default {
@@ -7,7 +7,7 @@ export default {
tooltip,
},
components: {
- Icon,
+ GlIcon,
},
props: {
label: {
@@ -52,7 +52,7 @@ export default {
class="btn-blank"
@click.stop.prevent="clicked"
>
- <icon :name="icon" :class="iconClasses" />
+ <gl-icon :name="icon" :class="iconClasses" />
<template v-if="showLabel">
{{ label }}
</template>
diff --git a/app/assets/javascripts/ide/components/new_dropdown/index.vue b/app/assets/javascripts/ide/components/new_dropdown/index.vue
index b656e35f150..692878de5e1 100644
--- a/app/assets/javascripts/ide/components/new_dropdown/index.vue
+++ b/app/assets/javascripts/ide/components/new_dropdown/index.vue
@@ -1,6 +1,6 @@
<script>
import { mapActions } from 'vuex';
-import icon from '~/vue_shared/components/icon.vue';
+import { GlIcon } from '@gitlab/ui';
import upload from './upload.vue';
import ItemButton from './button.vue';
import { modalTypes } from '../../constants';
@@ -8,7 +8,7 @@ import NewModal from './modal.vue';
export default {
components: {
- icon,
+ GlIcon,
upload,
ItemButton,
NewModal,
@@ -67,7 +67,7 @@ export default {
data-qa-selector="dropdown_button"
@click.stop="openDropdown()"
>
- <icon name="ellipsis_v" /> <icon name="chevron-down" />
+ <gl-icon name="ellipsis_v" /> <gl-icon name="chevron-down" />
</button>
<ul ref="dropdownMenu" class="dropdown-menu dropdown-menu-right">
<template v-if="type === 'tree'">
diff --git a/app/assets/javascripts/ide/components/new_dropdown/modal.vue b/app/assets/javascripts/ide/components/new_dropdown/modal.vue
index 44986c8c575..528475849de 100644
--- a/app/assets/javascripts/ide/components/new_dropdown/modal.vue
+++ b/app/assets/javascripts/ide/components/new_dropdown/modal.vue
@@ -1,6 +1,6 @@
<script>
import { mapActions, mapState, mapGetters } from 'vuex';
-import { GlModal } from '@gitlab/ui';
+import { GlModal, GlButton } from '@gitlab/ui';
import { deprecatedCreateFlash as flash } from '~/flash';
import { __, sprintf, s__ } from '~/locale';
import { modalTypes } from '../../constants';
@@ -9,6 +9,7 @@ import { trimPathComponents, getPathParent } from '../../utils';
export default {
components: {
GlModal,
+ GlButton,
},
data() {
return {
@@ -156,13 +157,14 @@ export default {
/>
<ul v-if="isCreatingNewFile" class="file-templates gl-mt-3 list-inline qa-template-list">
<li v-for="(template, index) in templateTypes" :key="index" class="list-inline-item">
- <button
- type="button"
- class="btn btn-missing p-1 pr-2 pl-2"
+ <gl-button
+ variant="dashed"
+ category="secondary"
+ class="p-1 pr-2 pl-2"
@click="createFromTemplate(template)"
>
{{ template.name }}
- </button>
+ </gl-button>
</li>
</ul>
</div>
diff --git a/app/assets/javascripts/ide/components/new_dropdown/upload.vue b/app/assets/javascripts/ide/components/new_dropdown/upload.vue
index b2141c13d9f..84ff05c9750 100644
--- a/app/assets/javascripts/ide/components/new_dropdown/upload.vue
+++ b/app/assets/javascripts/ide/components/new_dropdown/upload.vue
@@ -28,14 +28,13 @@ export default {
const { name } = file;
const encodedContent = target.result.split('base64,')[1];
const rawContent = encodedContent ? atob(encodedContent) : '';
- const isText = isTextFile(rawContent, file.type, name);
+ const isText = isTextFile({ content: rawContent, mimeType: file.type, name });
const emitCreateEvent = content =>
this.$emit('create', {
name: `${this.path ? `${this.path}/` : ''}${name}`,
type: 'blob',
content,
- binary: !isText,
rawPath: !isText ? target.result : '',
});
diff --git a/app/assets/javascripts/ide/components/panes/collapsible_sidebar.vue b/app/assets/javascripts/ide/components/panes/collapsible_sidebar.vue
index 4e8e1e3a470..f1b882d8f29 100644
--- a/app/assets/javascripts/ide/components/panes/collapsible_sidebar.vue
+++ b/app/assets/javascripts/ide/components/panes/collapsible_sidebar.vue
@@ -1,7 +1,6 @@
<script>
import { mapActions, mapState } from 'vuex';
import tooltip from '~/vue_shared/directives/tooltip';
-import Icon from '~/vue_shared/components/icon.vue';
import IdeSidebarNav from '../ide_sidebar_nav.vue';
export default {
@@ -10,7 +9,6 @@ export default {
tooltip,
},
components: {
- Icon,
IdeSidebarNav,
},
props: {
diff --git a/app/assets/javascripts/ide/components/pipelines/list.vue b/app/assets/javascripts/ide/components/pipelines/list.vue
index 6038e92f254..91bd64a2c9c 100644
--- a/app/assets/javascripts/ide/components/pipelines/list.vue
+++ b/app/assets/javascripts/ide/components/pipelines/list.vue
@@ -1,9 +1,8 @@
<script>
import { mapActions, mapGetters, mapState } from 'vuex';
import { escape } from 'lodash';
-import { GlLoadingIcon } from '@gitlab/ui';
+import { GlLoadingIcon, GlIcon, GlSafeHtmlDirective as SafeHtml } from '@gitlab/ui';
import { sprintf, __ } from '../../../locale';
-import Icon from '../../../vue_shared/components/icon.vue';
import CiIcon from '../../../vue_shared/components/ci_icon.vue';
import Tabs from '../../../vue_shared/components/tabs/tabs';
import Tab from '../../../vue_shared/components/tabs/tab.vue';
@@ -14,7 +13,7 @@ import IDEServices from '~/ide/services';
export default {
components: {
- Icon,
+ GlIcon,
CiIcon,
Tabs,
Tab,
@@ -22,6 +21,9 @@ export default {
EmptyState,
GlLoadingIcon,
},
+ directives: {
+ SafeHtml,
+ },
computed: {
...mapState(['pipelinesEmptyStateSvgPath', 'links']),
...mapGetters(['currentProject']),
@@ -70,7 +72,7 @@ export default {
target="_blank"
class="ide-external-link position-relative"
>
- #{{ latestPipeline.id }} <icon :size="12" name="external-link" />
+ #{{ latestPipeline.id }} <gl-icon :size="12" name="external-link" />
</a>
</span>
</header>
@@ -84,7 +86,7 @@ export default {
<div v-else-if="latestPipeline.yamlError" class="bs-callout bs-callout-danger">
<p class="gl-mb-0">{{ __('Found errors in your .gitlab-ci.yml:') }}</p>
<p class="gl-mb-0 break-word">{{ latestPipeline.yamlError }}</p>
- <p class="gl-mb-0" v-html="ciLintText"></p>
+ <p v-safe-html="ciLintText" class="gl-mb-0"></p>
</div>
<tabs v-else class="ide-pipeline-list">
<tab :active="!pipelineFailed">
diff --git a/app/assets/javascripts/ide/components/preview/navigator.vue b/app/assets/javascripts/ide/components/preview/navigator.vue
index 0de9dfd8827..60710251fef 100644
--- a/app/assets/javascripts/ide/components/preview/navigator.vue
+++ b/app/assets/javascripts/ide/components/preview/navigator.vue
@@ -1,11 +1,10 @@
<script>
import { listen } from 'codesandbox-api';
-import { GlLoadingIcon } from '@gitlab/ui';
-import Icon from '~/vue_shared/components/icon.vue';
+import { GlLoadingIcon, GlIcon } from '@gitlab/ui';
export default {
components: {
- Icon,
+ GlIcon,
GlLoadingIcon,
},
props: {
@@ -97,7 +96,7 @@ export default {
class="ide-navigator-btn d-flex align-items-center d-transparent border-0 bg-transparent"
@click="back"
>
- <icon :size="24" name="chevron-left" class="m-auto" />
+ <gl-icon :size="24" name="chevron-left" class="m-auto" />
</button>
<button
:aria-label="s__('IDE|Back')"
@@ -109,7 +108,7 @@ export default {
class="ide-navigator-btn d-flex align-items-center d-transparent border-0 bg-transparent"
@click="forward"
>
- <icon :size="24" name="chevron-right" class="m-auto" />
+ <gl-icon :size="24" name="chevron-right" class="m-auto" />
</button>
<button
:aria-label="s__('IDE|Refresh preview')"
@@ -117,7 +116,7 @@ export default {
class="ide-navigator-btn d-flex align-items-center d-transparent border-0 bg-transparent"
@click="refresh"
>
- <icon :size="18" name="retry" class="m-auto" />
+ <gl-icon :size="18" name="retry" class="m-auto" />
</button>
<div class="position-relative w-100 gl-ml-2">
<input
diff --git a/app/assets/javascripts/ide/components/repo_editor.vue b/app/assets/javascripts/ide/components/repo_editor.vue
index d22d430cb4a..f342ce1739c 100644
--- a/app/assets/javascripts/ide/components/repo_editor.vue
+++ b/app/assets/javascripts/ide/components/repo_editor.vue
@@ -14,7 +14,7 @@ import Editor from '../lib/editor';
import FileTemplatesBar from './file_templates/bar.vue';
import { __ } from '~/locale';
import { extractMarkdownImagesFromEntries } from '../stores/utils';
-import { getPathParent, readFileAsDataURL } from '../utils';
+import { getPathParent, readFileAsDataURL, registerSchema, isTextFile } from '../utils';
import { getRulesWithTraversal } from '../lib/editorconfig/parser';
import mapRulesToMonaco from '../lib/editorconfig/rules_mapper';
@@ -48,6 +48,7 @@ export default {
'renderWhitespaceInCode',
'editorTheme',
'entries',
+ 'currentProjectId',
]),
...mapGetters([
'currentMergeRequest',
@@ -55,10 +56,11 @@ export default {
'isEditModeActive',
'isCommitModeActive',
'currentBranch',
+ 'getJsonSchemaForPath',
]),
...mapGetters('fileTemplates', ['showFileTemplatesBar']),
shouldHideEditor() {
- return this.file && this.file.binary;
+ return this.file && !isTextFile(this.file);
},
showContentViewer() {
return (
@@ -196,6 +198,8 @@ export default {
this.editor.clearEditor();
+ this.registerSchemaForFile();
+
Promise.all([this.fetchFileData(), this.fetchEditorconfigRules()])
.then(() => {
this.createEditorInstance();
@@ -329,6 +333,10 @@ export default {
// do nothing if no image is found in the clipboard
return Promise.resolve();
},
+ registerSchemaForFile() {
+ const schema = this.getJsonSchemaForPath(this.file.path);
+ registerSchema(schema);
+ },
},
viewerTypes,
FILE_VIEW_MODE_EDITOR,
@@ -379,7 +387,7 @@ export default {
:path="file.rawPath || file.path"
:file-path="file.path"
:file-size="file.size"
- :project-path="file.projectId"
+ :project-path="currentProjectId"
:commit-sha="currentBranchCommit"
:type="fileType"
/>
@@ -390,7 +398,7 @@ export default {
:new-sha="currentMergeRequest.sha"
:old-path="file.mrChange.old_path"
:old-sha="currentMergeRequest.baseCommitSha"
- :project-path="file.projectId"
+ :project-path="currentProjectId"
/>
</div>
</template>
diff --git a/app/assets/javascripts/ide/components/repo_file_status_icon.vue b/app/assets/javascripts/ide/components/repo_file_status_icon.vue
index 9773e835a5c..1402f7aaf39 100644
--- a/app/assets/javascripts/ide/components/repo_file_status_icon.vue
+++ b/app/assets/javascripts/ide/components/repo_file_status_icon.vue
@@ -1,12 +1,12 @@
<script>
+import { GlIcon } from '@gitlab/ui';
import { __, sprintf } from '~/locale';
-import icon from '~/vue_shared/components/icon.vue';
import tooltip from '~/vue_shared/directives/tooltip';
import '~/lib/utils/datetime_utility';
export default {
components: {
- icon,
+ GlIcon,
},
directives: {
tooltip,
@@ -29,6 +29,6 @@ export default {
<template>
<span v-if="file.file_lock" v-tooltip :title="lockTooltip" data-container="body">
- <icon name="lock" class="file-status-icon" />
+ <gl-icon name="lock" class="file-status-icon" />
</span>
</template>
diff --git a/app/assets/javascripts/ide/components/repo_tab.vue b/app/assets/javascripts/ide/components/repo_tab.vue
index 8370833233a..60a80a31a8b 100644
--- a/app/assets/javascripts/ide/components/repo_tab.vue
+++ b/app/assets/javascripts/ide/components/repo_tab.vue
@@ -1,9 +1,9 @@
<script>
-import { mapActions } from 'vuex';
+import { mapActions, mapGetters } from 'vuex';
+import { GlIcon } from '@gitlab/ui';
import { __, sprintf } from '~/locale';
import FileIcon from '~/vue_shared/components/file_icon.vue';
-import Icon from '~/vue_shared/components/icon.vue';
import ChangedFileIcon from '~/vue_shared/components/changed_file_icon.vue';
import FileStatusIcon from './repo_file_status_icon.vue';
@@ -11,7 +11,7 @@ export default {
components: {
FileStatusIcon,
FileIcon,
- Icon,
+ GlIcon,
ChangedFileIcon,
},
props: {
@@ -26,6 +26,7 @@ export default {
};
},
computed: {
+ ...mapGetters(['getUrlForPath']),
closeLabel() {
if (this.fileHasChanged) {
return sprintf(__(`%{tabname} changed`), { tabname: this.tab.name });
@@ -52,7 +53,7 @@ export default {
if (tab.pending) {
this.openPendingTab({ file: tab, keyPrefix: tab.staged ? 'staged' : 'unstaged' });
} else {
- this.$router.push(`/project${tab.url}`);
+ this.$router.push(this.getUrlForPath(tab.path));
}
},
mouseOverTab() {
@@ -79,7 +80,7 @@ export default {
@mouseover="mouseOverTab"
@mouseout="mouseOutTab"
>
- <div :title="tab.url" class="multi-file-tab">
+ <div :title="getUrlForPath(tab.path)" class="multi-file-tab">
<file-icon :file-name="tab.name" :size="16" />
{{ tab.name }}
<file-status-icon :file="tab" />
@@ -91,7 +92,7 @@ export default {
class="multi-file-tab-close"
@click.stop.prevent="closeFile(tab)"
>
- <icon v-if="!showChangedIcon" :size="12" name="close" />
+ <gl-icon v-if="!showChangedIcon" :size="12" name="close" />
<changed-file-icon v-else :file="tab" />
</button>
</li>
diff --git a/app/assets/javascripts/ide/components/repo_tabs.vue b/app/assets/javascripts/ide/components/repo_tabs.vue
index 47c75be3f7c..c03694e3619 100644
--- a/app/assets/javascripts/ide/components/repo_tabs.vue
+++ b/app/assets/javascripts/ide/components/repo_tabs.vue
@@ -1,5 +1,5 @@
<script>
-import { mapActions } from 'vuex';
+import { mapActions, mapGetters } from 'vuex';
import RepoTab from './repo_tab.vue';
export default {
@@ -20,6 +20,9 @@ export default {
required: true,
},
},
+ computed: {
+ ...mapGetters(['getUrlForPath']),
+ },
methods: {
...mapActions(['updateViewer', 'removePendingTab']),
openFileViewer(viewer) {
@@ -27,7 +30,7 @@ export default {
if (this.activeFile.pending) {
return this.removePendingTab(this.activeFile).then(() => {
- this.$router.push(`/project${this.activeFile.url}`);
+ this.$router.push(this.getUrlForPath(this.activeFile.path));
});
}
diff --git a/app/assets/javascripts/ide/components/shared/tokened_input.vue b/app/assets/javascripts/ide/components/shared/tokened_input.vue
index de3e71dad92..e7a4c5487d1 100644
--- a/app/assets/javascripts/ide/components/shared/tokened_input.vue
+++ b/app/assets/javascripts/ide/components/shared/tokened_input.vue
@@ -1,10 +1,10 @@
<script>
+import { GlIcon } from '@gitlab/ui';
import { __ } from '~/locale';
-import Icon from '~/vue_shared/components/icon.vue';
export default {
components: {
- Icon,
+ GlIcon,
},
props: {
placeholder: {
@@ -81,7 +81,7 @@ export default {
>
<div class="value-container rounded">
<div class="value">{{ token.label }}</div>
- <div class="remove-token inverted"><icon :size="10" name="close" /></div>
+ <div class="remove-token inverted"><gl-icon :size="10" name="close" /></div>
</div>
</button>
</div>
diff --git a/app/assets/javascripts/ide/components/terminal/empty_state.vue b/app/assets/javascripts/ide/components/terminal/empty_state.vue
index 5dd12e62820..3668dd24e81 100644
--- a/app/assets/javascripts/ide/components/terminal/empty_state.vue
+++ b/app/assets/javascripts/ide/components/terminal/empty_state.vue
@@ -1,4 +1,5 @@
<script>
+/* eslint-disable vue/no-v-html */
import { GlLoadingIcon } from '@gitlab/ui';
export default {
diff --git a/app/assets/javascripts/ide/components/terminal_sync/terminal_sync_status.vue b/app/assets/javascripts/ide/components/terminal_sync/terminal_sync_status.vue
index deb13b5615e..c3f722d6052 100644
--- a/app/assets/javascripts/ide/components/terminal_sync/terminal_sync_status.vue
+++ b/app/assets/javascripts/ide/components/terminal_sync/terminal_sync_status.vue
@@ -1,8 +1,7 @@
<script>
import { throttle } from 'lodash';
-import { GlTooltipDirective, GlLoadingIcon } from '@gitlab/ui';
+import { GlTooltipDirective, GlLoadingIcon, GlIcon } from '@gitlab/ui';
import { mapState } from 'vuex';
-import Icon from '~/vue_shared/components/icon.vue';
import {
MSG_TERMINAL_SYNC_CONNECTING,
MSG_TERMINAL_SYNC_UPLOADING,
@@ -11,7 +10,7 @@ import {
export default {
components: {
- Icon,
+ GlIcon,
GlLoadingIcon,
},
directives: {
@@ -70,7 +69,7 @@ export default {
<span>{{ __('Terminal') }}:</span>
<span class="square s16 d-flex-center ml-1" :aria-label="status.text">
<gl-loading-icon v-if="isLoading" inline size="sm" class="d-flex-center" />
- <icon v-else-if="status.icon" :name="status.icon" :size="16" />
+ <gl-icon v-else-if="status.icon" :name="status.icon" :size="16" />
</span>
</div>
</template>
diff --git a/app/assets/javascripts/ide/ide_router.js b/app/assets/javascripts/ide/ide_router.js
index 82cf8d7a10a..396aedbfa10 100644
--- a/app/assets/javascripts/ide/ide_router.js
+++ b/app/assets/javascripts/ide/ide_router.js
@@ -33,7 +33,6 @@ const EmptyRouterComponent = {
},
};
-// eslint-disable-next-line import/prefer-default-export
export const createRouter = store => {
const router = new IdeRouter({
mode: 'history',
diff --git a/app/assets/javascripts/ide/lib/diff/diff.js b/app/assets/javascripts/ide/lib/diff/diff.js
index 3a456b7c4d6..62ec798b372 100644
--- a/app/assets/javascripts/ide/lib/diff/diff.js
+++ b/app/assets/javascripts/ide/lib/diff/diff.js
@@ -1,8 +1,6 @@
import { diffLines } from 'diff';
import { defaultDiffOptions } from '../editor_options';
-// See: https://gitlab.com/gitlab-org/frontend/rfcs/-/issues/20
-// eslint-disable-next-line import/prefer-default-export
export const computeDiff = (originalContent, newContent) => {
// prevent EOL changes from highlighting the entire file
const changes = diffLines(
diff --git a/app/assets/javascripts/ide/lib/editor.js b/app/assets/javascripts/ide/lib/editor.js
index f061fcb1259..2b12230c7cd 100644
--- a/app/assets/javascripts/ide/lib/editor.js
+++ b/app/assets/javascripts/ide/lib/editor.js
@@ -7,10 +7,9 @@ import ModelManager from './common/model_manager';
import { editorOptions, defaultEditorOptions, defaultDiffEditorOptions } from './editor_options';
import { themes } from './themes';
import languages from './languages';
-import schemas from './schemas';
import keymap from './keymap.json';
import { clearDomElement } from '~/editor/utils';
-import { registerLanguages, registerSchemas } from '../utils';
+import { registerLanguages } from '../utils';
function setupThemes() {
themes.forEach(theme => {
@@ -46,10 +45,6 @@ export default class Editor {
setupThemes();
registerLanguages(...languages);
- if (gon.features?.schemaLinting) {
- registerSchemas(...schemas);
- }
-
this.debouncedUpdate = debounce(() => {
this.updateDimensions();
}, 200);
diff --git a/app/assets/javascripts/ide/lib/editorconfig/parser.js b/app/assets/javascripts/ide/lib/editorconfig/parser.js
index a30a8cb868d..1597e4a8bfa 100644
--- a/app/assets/javascripts/ide/lib/editorconfig/parser.js
+++ b/app/assets/javascripts/ide/lib/editorconfig/parser.js
@@ -42,7 +42,6 @@ function getRulesWithConfigs(filePath, configFiles = [], rules = {}) {
return isRoot ? result : getRulesWithConfigs(filePath, nextConfigs, result);
}
-// eslint-disable-next-line import/prefer-default-export
export function getRulesWithTraversal(filePath, getFileContent) {
const editorconfigPaths = [
...getPathParents(filePath).map(x => `${x}/.editorconfig`),
diff --git a/app/assets/javascripts/ide/lib/errors.js b/app/assets/javascripts/ide/lib/errors.js
new file mode 100644
index 00000000000..6ae18bc8180
--- /dev/null
+++ b/app/assets/javascripts/ide/lib/errors.js
@@ -0,0 +1,39 @@
+import { escape } from 'lodash';
+import { __ } from '~/locale';
+
+const CODEOWNERS_REGEX = /Push.*protected branches.*CODEOWNERS/;
+const BRANCH_CHANGED_REGEX = /changed.*since.*start.*edit/;
+
+export const createUnexpectedCommitError = () => ({
+ title: __('Unexpected error'),
+ messageHTML: __('Could not commit. An unexpected error occurred.'),
+ canCreateBranch: false,
+});
+
+export const createCodeownersCommitError = message => ({
+ title: __('CODEOWNERS rule violation'),
+ messageHTML: escape(message),
+ canCreateBranch: true,
+});
+
+export const createBranchChangedCommitError = message => ({
+ title: __('Branch changed'),
+ messageHTML: `${escape(message)}<br/><br/>${__('Would you like to create a new branch?')}`,
+ canCreateBranch: true,
+});
+
+export const parseCommitError = e => {
+ const { message } = e?.response?.data || {};
+
+ if (!message) {
+ return createUnexpectedCommitError();
+ }
+
+ if (CODEOWNERS_REGEX.test(message)) {
+ return createCodeownersCommitError(message);
+ } else if (BRANCH_CHANGED_REGEX.test(message)) {
+ return createBranchChangedCommitError(message);
+ }
+
+ return createUnexpectedCommitError();
+};
diff --git a/app/assets/javascripts/ide/lib/files.js b/app/assets/javascripts/ide/lib/files.js
index 6d85e225fd5..789e09fa8f2 100644
--- a/app/assets/javascripts/ide/lib/files.js
+++ b/app/assets/javascripts/ide/lib/files.js
@@ -1,4 +1,3 @@
-import { viewerInformationForPath } from '~/vue_shared/components/content_viewer/lib/viewer_utils';
import { decorateData, sortTree } from '../stores/utils';
export const splitParent = path => {
@@ -13,15 +12,7 @@ export const splitParent = path => {
/**
* Create file objects from a list of file paths.
*/
-export const decorateFiles = ({
- data,
- projectId,
- branchId,
- tempFile = false,
- content = '',
- binary = false,
- rawPath = '',
-}) => {
+export const decorateFiles = ({ data, tempFile = false, content = '', rawPath = '' }) => {
const treeList = [];
const entries = {};
@@ -41,12 +32,9 @@ export const decorateFiles = ({
parentPath = parentFolder && parentFolder.path;
const tree = decorateData({
- projectId,
- branchId,
id: path,
name,
path,
- url: `/${projectId}/tree/${branchId}/-/${path}/`,
type: 'tree',
tempFile,
changed: tempFile,
@@ -73,21 +61,16 @@ export const decorateFiles = ({
const fileFolder = parent && insertParent(parent);
if (name) {
- const previewMode = viewerInformationForPath(name);
parentPath = fileFolder && fileFolder.path;
file = decorateData({
- projectId,
- branchId,
id: path,
name,
path,
- url: `/${projectId}/blob/${branchId}/-/${path}`,
type: 'blob',
tempFile,
changed: tempFile,
content,
- binary: (previewMode && previewMode.binary) || binary,
rawPath,
parentPath,
});
diff --git a/app/assets/javascripts/ide/lib/schemas/index.js b/app/assets/javascripts/ide/lib/schemas/index.js
deleted file mode 100644
index 38a2f81921b..00000000000
--- a/app/assets/javascripts/ide/lib/schemas/index.js
+++ /dev/null
@@ -1,4 +0,0 @@
-import json from './json';
-import yaml from './yaml';
-
-export default [json, yaml];
diff --git a/app/assets/javascripts/ide/lib/schemas/json/index.js b/app/assets/javascripts/ide/lib/schemas/json/index.js
deleted file mode 100644
index 900d5442bec..00000000000
--- a/app/assets/javascripts/ide/lib/schemas/json/index.js
+++ /dev/null
@@ -1,8 +0,0 @@
-export default {
- language: 'json',
- options: {
- validate: true,
- enableSchemaRequest: true,
- schemas: [],
- },
-};
diff --git a/app/assets/javascripts/ide/lib/schemas/yaml/gitlab_ci.js b/app/assets/javascripts/ide/lib/schemas/yaml/gitlab_ci.js
deleted file mode 100644
index af20744abb3..00000000000
--- a/app/assets/javascripts/ide/lib/schemas/yaml/gitlab_ci.js
+++ /dev/null
@@ -1,4 +0,0 @@
-export default {
- uri: 'https://json.schemastore.org/gitlab-ci',
- fileMatch: ['*.gitlab-ci.yml'],
-};
diff --git a/app/assets/javascripts/ide/lib/schemas/yaml/index.js b/app/assets/javascripts/ide/lib/schemas/yaml/index.js
deleted file mode 100644
index e3fc406df4b..00000000000
--- a/app/assets/javascripts/ide/lib/schemas/yaml/index.js
+++ /dev/null
@@ -1,12 +0,0 @@
-import gitlabCi from './gitlab_ci';
-
-export default {
- language: 'yaml',
- options: {
- validate: true,
- enableSchemaRequest: true,
- hover: true,
- completion: true,
- schemas: [gitlabCi],
- },
-};
diff --git a/app/assets/javascripts/ide/services/gql.js b/app/assets/javascripts/ide/services/gql.js
index 211cc78bd99..89dda187360 100644
--- a/app/assets/javascripts/ide/services/gql.js
+++ b/app/assets/javascripts/ide/services/gql.js
@@ -17,5 +17,4 @@ const getClient = memoize(() =>
),
);
-// eslint-disable-next-line import/prefer-default-export
export const query = (...args) => getClient().query(...args);
diff --git a/app/assets/javascripts/ide/services/index.js b/app/assets/javascripts/ide/services/index.js
index ae4a1ba3db5..70a6a6b423d 100644
--- a/app/assets/javascripts/ide/services/index.js
+++ b/app/assets/javascripts/ide/services/index.js
@@ -33,7 +33,7 @@ export default {
})
.then(({ data }) => data);
},
- getBaseRawFileData(file, sha) {
+ getBaseRawFileData(file, projectId, ref) {
if (file.tempFile || file.baseRaw) return Promise.resolve(file.baseRaw);
// if files are renamed, their base path has changed
@@ -44,10 +44,10 @@ export default {
.get(
joinPaths(
gon.relative_url_root || '/',
- file.projectId,
+ projectId,
'-',
'raw',
- sha,
+ ref,
escapeFileUrl(filePath),
),
{
diff --git a/app/assets/javascripts/ide/stores/actions.js b/app/assets/javascripts/ide/stores/actions.js
index b083dc6325f..b8d59f8bd36 100644
--- a/app/assets/javascripts/ide/stores/actions.js
+++ b/app/assets/javascripts/ide/stores/actions.js
@@ -25,15 +25,7 @@ export const setResizingStatus = ({ commit }, resizing) => {
export const createTempEntry = (
{ state, commit, dispatch, getters },
- {
- name,
- type,
- content = '',
- binary = false,
- rawPath = '',
- openFile = true,
- makeFileActive = true,
- },
+ { name, type, content = '', rawPath = '', openFile = true, makeFileActive = true },
) => {
const fullName = name.slice(-1) !== '/' && type === 'tree' ? `${name}/` : name;
@@ -54,21 +46,14 @@ export const createTempEntry = (
const data = decorateFiles({
data: [fullName],
- projectId: state.currentProjectId,
- branchId: state.currentBranchId,
type,
tempFile: true,
content,
- binary,
rawPath,
});
const { file, parentPath } = data;
- commit(types.CREATE_TMP_ENTRY, {
- data,
- projectId: state.currentProjectId,
- branchId: state.currentBranchId,
- });
+ commit(types.CREATE_TMP_ENTRY, { data });
if (type === 'blob') {
if (openFile) commit(types.TOGGLE_FILE_OPEN, file.path);
@@ -90,7 +75,6 @@ export const addTempImage = ({ dispatch, getters }, { name, rawPath = '' }) =>
name: getters.getAvailableFileName(name),
type: 'blob',
content: rawPath.split('base64,')[1],
- binary: true,
rawPath,
openFile: false,
makeFileActive: false,
@@ -254,7 +238,7 @@ export const renameEntry = ({ dispatch, commit, state, getters }, { path, name,
}
if (newEntry.opened) {
- dispatch('router/push', `/project${newEntry.url}`, { root: true });
+ dispatch('router/push', getters.getUrlForPath(newEntry.path), { root: true });
}
}
diff --git a/app/assets/javascripts/ide/stores/actions/file.js b/app/assets/javascripts/ide/stores/actions/file.js
index c0cb924e749..3515d1fc933 100644
--- a/app/assets/javascripts/ide/stores/actions/file.js
+++ b/app/assets/javascripts/ide/stores/actions/file.js
@@ -6,7 +6,7 @@ import * as types from '../mutation_types';
import { setPageTitleForFile } from '../utils';
import { viewerTypes, stageKeys } from '../../constants';
-export const closeFile = ({ commit, state, dispatch }, file) => {
+export const closeFile = ({ commit, state, dispatch, getters }, file) => {
const { path } = file;
const indexOfClosedFile = state.openFiles.findIndex(f => f.key === file.key);
const fileWasActive = file.active;
@@ -29,10 +29,12 @@ export const closeFile = ({ commit, state, dispatch }, file) => {
keyPrefix: nextFileToOpen.staged ? 'staged' : 'unstaged',
});
} else {
- dispatch('router/push', `/project${nextFileToOpen.url}`, { root: true });
+ dispatch('router/push', getters.getUrlForPath(nextFileToOpen.path), { root: true });
}
} else if (!state.openFiles.length) {
- dispatch('router/push', `/project/${file.projectId}/tree/${file.branchId}/`, { root: true });
+ dispatch('router/push', `/project/${state.currentProjectId}/tree/${state.currentBranchId}/`, {
+ root: true,
+ });
}
eventHub.$emit(`editor.update.model.dispose.${file.key}`);
@@ -121,7 +123,7 @@ export const getRawFileData = ({ state, commit, dispatch, getters }, { path }) =
const baseSha =
(getters.currentMergeRequest && getters.currentMergeRequest.baseCommitSha) || '';
- return service.getBaseRawFileData(file, baseSha).then(baseRaw => {
+ return service.getBaseRawFileData(file, state.currentProjectId, baseSha).then(baseRaw => {
commit(types.SET_FILE_BASE_RAW_DATA, {
file,
baseRaw,
@@ -218,7 +220,7 @@ export const discardFileChanges = ({ dispatch, state, commit, getters }, path) =
if (!isDestructiveDiscard && file.path === getters.activeFile?.path) {
dispatch('updateDelayViewerUpdated', true)
.then(() => {
- dispatch('router/push', `/project${file.url}`, { root: true });
+ dispatch('router/push', getters.getUrlForPath(file.path), { root: true });
})
.catch(e => {
throw e;
@@ -274,7 +276,7 @@ export const openPendingTab = ({ commit, dispatch, getters, state }, { file, key
commit(types.ADD_PENDING_TAB, { file, keyPrefix });
- dispatch('router/push', `/project/${file.projectId}/tree/${state.currentBranchId}/`, {
+ dispatch('router/push', `/project/${state.currentProjectId}/tree/${state.currentBranchId}/`, {
root: true,
});
diff --git a/app/assets/javascripts/ide/stores/actions/tree.js b/app/assets/javascripts/ide/stores/actions/tree.js
index 1ca608f1287..3a7daf30cc4 100644
--- a/app/assets/javascripts/ide/stores/actions/tree.js
+++ b/app/assets/javascripts/ide/stores/actions/tree.js
@@ -61,11 +61,7 @@ export const getFiles = ({ state, commit, dispatch }, payload = {}) =>
service
.getFiles(selectedProject.path_with_namespace, ref)
.then(({ data }) => {
- const { entries, treeList } = decorateFiles({
- data,
- projectId,
- branchId,
- });
+ const { entries, treeList } = decorateFiles({ data });
commit(types.SET_ENTRIES, entries);
diff --git a/app/assets/javascripts/ide/stores/getters.js b/app/assets/javascripts/ide/stores/getters.js
index 53734fa626b..b8304a9b68d 100644
--- a/app/assets/javascripts/ide/stores/getters.js
+++ b/app/assets/javascripts/ide/stores/getters.js
@@ -6,6 +6,7 @@ import {
PERMISSION_CREATE_MR,
PERMISSION_PUSH_CODE,
} from '../constants';
+import Api from '~/api';
export const activeFile = state => state.openFiles.find(file => file.active) || null;
@@ -174,3 +175,21 @@ export const getAvailableFileName = (state, getters) => path => {
return newPath;
};
+
+export const getUrlForPath = state => path =>
+ `/project/${state.currentProjectId}/tree/${state.currentBranchId}/-/${path}/`;
+
+export const getJsonSchemaForPath = (state, getters) => path => {
+ const [namespace, ...project] = state.currentProjectId.split('/');
+ return {
+ uri:
+ // eslint-disable-next-line no-restricted-globals
+ location.origin +
+ Api.buildUrl(Api.projectFileSchemaPath)
+ .replace(':namespace_path', namespace)
+ .replace(':project_path', project.join('/'))
+ .replace(':ref', getters.currentBranch?.commit.id || state.currentBranchId)
+ .replace(':filename', path),
+ fileMatch: [`*${path}`],
+ };
+};
diff --git a/app/assets/javascripts/ide/stores/modules/commit/actions.js b/app/assets/javascripts/ide/stores/modules/commit/actions.js
index 277e6923f17..90a6c644d17 100644
--- a/app/assets/javascripts/ide/stores/modules/commit/actions.js
+++ b/app/assets/javascripts/ide/stores/modules/commit/actions.js
@@ -1,6 +1,5 @@
import { sprintf, __ } from '~/locale';
import { deprecatedCreateFlash as flash } from '~/flash';
-import httpStatusCodes from '~/lib/utils/http_status';
import * as rootTypes from '../../mutation_types';
import { createCommitPayload, createNewMergeRequestUrl } from '../../utils';
import service from '../../../services';
@@ -8,6 +7,7 @@ import * as types from './mutation_types';
import consts from './constants';
import { leftSidebarViews } from '../../../constants';
import eventHub from '../../../eventhub';
+import { parseCommitError } from '../../../lib/errors';
export const updateCommitMessage = ({ commit }, message) => {
commit(types.UPDATE_COMMIT_MESSAGE, message);
@@ -113,6 +113,7 @@ export const commitChanges = ({ commit, state, getters, dispatch, rootState, roo
? Promise.resolve()
: dispatch('stageAllChanges', null, { root: true });
+ commit(types.CLEAR_ERROR);
commit(types.UPDATE_LOADING, true);
return stageFilesPromise
@@ -128,6 +129,12 @@ export const commitChanges = ({ commit, state, getters, dispatch, rootState, roo
return service.commit(rootState.currentProjectId, payload);
})
+ .catch(e => {
+ commit(types.UPDATE_LOADING, false);
+ commit(types.SET_ERROR, parseCommitError(e));
+
+ throw e;
+ })
.then(({ data }) => {
commit(types.UPDATE_LOADING, false);
@@ -214,24 +221,5 @@ export const commitChanges = ({ commit, state, getters, dispatch, rootState, roo
{ root: true },
),
);
- })
- .catch(err => {
- commit(types.UPDATE_LOADING, false);
-
- // don't catch bad request errors, let the view handle them
- if (err.response.status === httpStatusCodes.BAD_REQUEST) throw err;
-
- dispatch(
- 'setErrorMessage',
- {
- text: __('An error occurred while committing your changes.'),
- action: () =>
- dispatch('commitChanges').then(() => dispatch('setErrorMessage', null, { root: true })),
- actionText: __('Please try again'),
- },
- { root: true },
- );
-
- window.dispatchEvent(new Event('resize'));
});
};
diff --git a/app/assets/javascripts/ide/stores/modules/commit/mutation_types.js b/app/assets/javascripts/ide/stores/modules/commit/mutation_types.js
index 7ad8f3570b7..47ec2ffbdde 100644
--- a/app/assets/javascripts/ide/stores/modules/commit/mutation_types.js
+++ b/app/assets/javascripts/ide/stores/modules/commit/mutation_types.js
@@ -3,3 +3,6 @@ export const UPDATE_COMMIT_ACTION = 'UPDATE_COMMIT_ACTION';
export const UPDATE_NEW_BRANCH_NAME = 'UPDATE_NEW_BRANCH_NAME';
export const UPDATE_LOADING = 'UPDATE_LOADING';
export const TOGGLE_SHOULD_CREATE_MR = 'TOGGLE_SHOULD_CREATE_MR';
+
+export const CLEAR_ERROR = 'CLEAR_ERROR';
+export const SET_ERROR = 'SET_ERROR';
diff --git a/app/assets/javascripts/ide/stores/modules/commit/mutations.js b/app/assets/javascripts/ide/stores/modules/commit/mutations.js
index 73b618e250f..2cf6e8e6f36 100644
--- a/app/assets/javascripts/ide/stores/modules/commit/mutations.js
+++ b/app/assets/javascripts/ide/stores/modules/commit/mutations.js
@@ -24,4 +24,10 @@ export default {
shouldCreateMR: shouldCreateMR === undefined ? !state.shouldCreateMR : shouldCreateMR,
});
},
+ [types.CLEAR_ERROR](state) {
+ state.commitError = null;
+ },
+ [types.SET_ERROR](state, error) {
+ state.commitError = error;
+ },
};
diff --git a/app/assets/javascripts/ide/stores/modules/commit/state.js b/app/assets/javascripts/ide/stores/modules/commit/state.js
index f49737485f2..de092a569ad 100644
--- a/app/assets/javascripts/ide/stores/modules/commit/state.js
+++ b/app/assets/javascripts/ide/stores/modules/commit/state.js
@@ -4,4 +4,5 @@ export default () => ({
newBranchName: '',
submitCommitLoading: false,
shouldCreateMR: true,
+ commitError: null,
});
diff --git a/app/assets/javascripts/ide/stores/modules/pane/getters.js b/app/assets/javascripts/ide/stores/modules/pane/getters.js
index 7816172bb6f..ce597329df1 100644
--- a/app/assets/javascripts/ide/stores/modules/pane/getters.js
+++ b/app/assets/javascripts/ide/stores/modules/pane/getters.js
@@ -1,3 +1,2 @@
-// eslint-disable-next-line import/prefer-default-export
export const isAliveView = state => view =>
state.keepAliveViews[view] || (state.isOpen && state.currentView === view);
diff --git a/app/assets/javascripts/ide/stores/modules/pipelines/actions.js b/app/assets/javascripts/ide/stores/modules/pipelines/actions.js
index 86b889546b0..99bd08ee876 100644
--- a/app/assets/javascripts/ide/stores/modules/pipelines/actions.js
+++ b/app/assets/javascripts/ide/stores/modules/pipelines/actions.js
@@ -118,31 +118,31 @@ export const setDetailJob = ({ commit, dispatch }, job) => {
});
};
-export const requestJobTrace = ({ commit }) => commit(types.REQUEST_JOB_TRACE);
-export const receiveJobTraceError = ({ commit, dispatch }) => {
+export const requestJobLogs = ({ commit }) => commit(types.REQUEST_JOB_LOGS);
+export const receiveJobLogsError = ({ commit, dispatch }) => {
dispatch(
'setErrorMessage',
{
- text: __('An error occurred while fetching the job trace.'),
+ text: __('An error occurred while fetching the job logs.'),
action: () =>
- dispatch('fetchJobTrace').then(() => dispatch('setErrorMessage', null, { root: true })),
+ dispatch('fetchJobLogs').then(() => dispatch('setErrorMessage', null, { root: true })),
actionText: __('Please try again'),
actionPayload: null,
},
{ root: true },
);
- commit(types.RECEIVE_JOB_TRACE_ERROR);
+ commit(types.RECEIVE_JOB_LOGS_ERROR);
};
-export const receiveJobTraceSuccess = ({ commit }, data) =>
- commit(types.RECEIVE_JOB_TRACE_SUCCESS, data);
+export const receiveJobLogsSuccess = ({ commit }, data) =>
+ commit(types.RECEIVE_JOB_LOGS_SUCCESS, data);
-export const fetchJobTrace = ({ dispatch, state }) => {
- dispatch('requestJobTrace');
+export const fetchJobLogs = ({ dispatch, state }) => {
+ dispatch('requestJobLogs');
return axios
.get(`${state.detailJob.path}/trace`, { params: { format: 'json' } })
- .then(({ data }) => dispatch('receiveJobTraceSuccess', data))
- .catch(() => dispatch('receiveJobTraceError'));
+ .then(({ data }) => dispatch('receiveJobLogsSuccess', data))
+ .catch(() => dispatch('receiveJobLogsError'));
};
export const resetLatestPipeline = ({ commit }) => {
diff --git a/app/assets/javascripts/ide/stores/modules/pipelines/constants.js b/app/assets/javascripts/ide/stores/modules/pipelines/constants.js
index f5b96327e40..bb4145934ff 100644
--- a/app/assets/javascripts/ide/stores/modules/pipelines/constants.js
+++ b/app/assets/javascripts/ide/stores/modules/pipelines/constants.js
@@ -1,4 +1,3 @@
-// eslint-disable-next-line import/prefer-default-export
export const states = {
failed: 'failed',
};
diff --git a/app/assets/javascripts/ide/stores/modules/pipelines/mutation_types.js b/app/assets/javascripts/ide/stores/modules/pipelines/mutation_types.js
index f4c36b9d96f..fea3055e0fe 100644
--- a/app/assets/javascripts/ide/stores/modules/pipelines/mutation_types.js
+++ b/app/assets/javascripts/ide/stores/modules/pipelines/mutation_types.js
@@ -10,6 +10,6 @@ export const TOGGLE_STAGE_COLLAPSE = 'TOGGLE_STAGE_COLLAPSE';
export const SET_DETAIL_JOB = 'SET_DETAIL_JOB';
-export const REQUEST_JOB_TRACE = 'REQUEST_JOB_TRACE';
-export const RECEIVE_JOB_TRACE_ERROR = 'RECEIVE_JOB_TRACE_ERROR';
-export const RECEIVE_JOB_TRACE_SUCCESS = 'RECEIVE_JOB_TRACE_SUCCESS';
+export const REQUEST_JOB_LOGS = 'REQUEST_JOB_LOGS';
+export const RECEIVE_JOB_LOGS_ERROR = 'RECEIVE_JOB_LOGS_ERROR';
+export const RECEIVE_JOB_LOGS_SUCCESS = 'RECEIVE_JOB_LOGS_SUCCESS';
diff --git a/app/assets/javascripts/ide/stores/modules/pipelines/mutations.js b/app/assets/javascripts/ide/stores/modules/pipelines/mutations.js
index eaaa82cb339..3a3cb4a7cb2 100644
--- a/app/assets/javascripts/ide/stores/modules/pipelines/mutations.js
+++ b/app/assets/javascripts/ide/stores/modules/pipelines/mutations.js
@@ -66,13 +66,13 @@ export default {
[types.SET_DETAIL_JOB](state, job) {
state.detailJob = { ...job };
},
- [types.REQUEST_JOB_TRACE](state) {
+ [types.REQUEST_JOB_LOGS](state) {
state.detailJob.isLoading = true;
},
- [types.RECEIVE_JOB_TRACE_ERROR](state) {
+ [types.RECEIVE_JOB_LOGS_ERROR](state) {
state.detailJob.isLoading = false;
},
- [types.RECEIVE_JOB_TRACE_SUCCESS](state, data) {
+ [types.RECEIVE_JOB_LOGS_SUCCESS](state, data) {
state.detailJob.isLoading = false;
state.detailJob.output = data.html;
},
diff --git a/app/assets/javascripts/ide/stores/modules/pipelines/utils.js b/app/assets/javascripts/ide/stores/modules/pipelines/utils.js
index a6caca2d2dc..95716e0a0c6 100644
--- a/app/assets/javascripts/ide/stores/modules/pipelines/utils.js
+++ b/app/assets/javascripts/ide/stores/modules/pipelines/utils.js
@@ -1,4 +1,3 @@
-// eslint-disable-next-line import/prefer-default-export
export const normalizeJob = job => ({
id: job.id,
name: job.name,
diff --git a/app/assets/javascripts/ide/stores/modules/router/actions.js b/app/assets/javascripts/ide/stores/modules/router/actions.js
index 849067599f2..321ac57817f 100644
--- a/app/assets/javascripts/ide/stores/modules/router/actions.js
+++ b/app/assets/javascripts/ide/stores/modules/router/actions.js
@@ -1,6 +1,5 @@
import * as types from './mutation_types';
-// eslint-disable-next-line import/prefer-default-export
export const push = ({ commit }, fullPath) => {
commit(types.PUSH, fullPath);
};
diff --git a/app/assets/javascripts/ide/stores/modules/router/mutation_types.js b/app/assets/javascripts/ide/stores/modules/router/mutation_types.js
index ae99073cc4c..8f5f949bd5f 100644
--- a/app/assets/javascripts/ide/stores/modules/router/mutation_types.js
+++ b/app/assets/javascripts/ide/stores/modules/router/mutation_types.js
@@ -1,2 +1 @@
-// eslint-disable-next-line import/prefer-default-export
export const PUSH = 'PUSH';
diff --git a/app/assets/javascripts/ide/stores/modules/terminal/getters.js b/app/assets/javascripts/ide/stores/modules/terminal/getters.js
index ef98547ccc4..b29d391845d 100644
--- a/app/assets/javascripts/ide/stores/modules/terminal/getters.js
+++ b/app/assets/javascripts/ide/stores/modules/terminal/getters.js
@@ -1,4 +1,3 @@
-// eslint-disable-next-line import/prefer-default-export
export const allCheck = state => {
const checks = Object.values(state.checks);
diff --git a/app/assets/javascripts/ide/stores/mutations.js b/app/assets/javascripts/ide/stores/mutations.js
index c64839e5019..460d3ced381 100644
--- a/app/assets/javascripts/ide/stores/mutations.js
+++ b/app/assets/javascripts/ide/stores/mutations.js
@@ -7,7 +7,6 @@ import treeMutations from './mutations/tree';
import branchMutations from './mutations/branch';
import {
sortTree,
- replaceFileUrl,
swapInParentTreeWithSorting,
updateFileCollections,
removeFromParentTree,
@@ -49,7 +48,7 @@ export default {
entries,
});
},
- [types.CREATE_TMP_ENTRY](state, { data, projectId, branchId }) {
+ [types.CREATE_TMP_ENTRY](state, { data }) {
Object.keys(data.entries).reduce((acc, key) => {
const entry = data.entries[key];
const foundEntry = state.entries[key];
@@ -72,13 +71,12 @@ export default {
return acc.concat(key);
}, []);
- const foundEntry = state.trees[`${projectId}/${branchId}`].tree.find(
- e => e.path === data.treeList[0].path,
- );
+ const currentTree = state.trees[`${state.currentProjectId}/${state.currentBranchId}`];
+ const foundEntry = currentTree.tree.find(e => e.path === data.treeList[0].path);
if (!foundEntry) {
- Object.assign(state.trees[`${projectId}/${branchId}`], {
- tree: sortTree(state.trees[`${projectId}/${branchId}`].tree.concat(data.treeList)),
+ Object.assign(currentTree, {
+ tree: sortTree(currentTree.tree.concat(data.treeList)),
});
}
},
@@ -139,7 +137,6 @@ export default {
prevId: undefined,
prevPath: undefined,
prevName: undefined,
- prevUrl: undefined,
prevKey: undefined,
prevParentPath: undefined,
});
@@ -195,9 +192,6 @@ export default {
const oldEntry = state.entries[path];
const newPath = parentPath ? `${parentPath}/${name}` : name;
const isRevert = newPath === oldEntry.prevPath;
-
- const newUrl = replaceFileUrl(oldEntry.url, oldEntry.path, newPath);
-
const newKey = oldEntry.key.replace(new RegExp(oldEntry.path, 'g'), newPath);
const baseProps = {
@@ -205,7 +199,6 @@ export default {
name,
id: newPath,
path: newPath,
- url: newUrl,
key: newKey,
parentPath: parentPath || '',
};
@@ -216,7 +209,6 @@ export default {
prevId: undefined,
prevPath: undefined,
prevName: undefined,
- prevUrl: undefined,
prevKey: undefined,
prevParentPath: undefined,
}
@@ -224,7 +216,6 @@ export default {
prevId: oldEntry.prevId || oldEntry.id,
prevPath: oldEntry.prevPath || oldEntry.path,
prevName: oldEntry.prevName || oldEntry.name,
- prevUrl: oldEntry.prevUrl || oldEntry.url,
prevKey: oldEntry.prevKey || oldEntry.key,
prevParentPath: oldEntry.prevParentPath || oldEntry.parentPath,
};
diff --git a/app/assets/javascripts/ide/stores/utils.js b/app/assets/javascripts/ide/stores/utils.js
index f074e6880d0..d9cdc7727ad 100644
--- a/app/assets/javascripts/ide/stores/utils.js
+++ b/app/assets/javascripts/ide/stores/utils.js
@@ -12,10 +12,7 @@ export const dataStructure = () => ({
// it can also contain a prefix `pending-` for files opened in review mode
key: '',
type: '',
- projectId: '',
- branchId: '',
name: '',
- url: '',
path: '',
tempFile: false,
tree: [],
@@ -26,7 +23,6 @@ export const dataStructure = () => ({
staged: false,
lastCommitSha: '',
rawPath: '',
- binary: false,
raw: '',
content: '',
editorRow: 1,
@@ -44,10 +40,7 @@ export const dataStructure = () => ({
export const decorateData = entity => {
const {
id,
- projectId,
- branchId,
type,
- url,
name,
path,
content = '',
@@ -55,7 +48,6 @@ export const decorateData = entity => {
active = false,
opened = false,
changed = false,
- binary = false,
rawPath = '',
file_lock,
parentPath = '',
@@ -63,19 +55,15 @@ export const decorateData = entity => {
return Object.assign(dataStructure(), {
id,
- projectId,
- branchId,
key: `${name}-${type}-${id}`,
type,
name,
- url,
path,
tempFile,
opened,
active,
changed,
content,
- binary,
rawPath,
file_lock,
parentPath,
@@ -189,11 +177,6 @@ export const mergeTrees = (fromTree, toTree) => {
return toTree;
};
-export const replaceFileUrl = (url, oldPath, newPath) => {
- // Add `/-/` so that we don't accidentally replace project path
- return url.replace(`/-/${oldPath}`, `/-/${newPath}`);
-};
-
export const swapInStateArray = (state, arr, key, entryPath) =>
Object.assign(state, {
[arr]: state[arr].map(f => (f.key === key ? state.entries[entryPath] : f)),
diff --git a/app/assets/javascripts/ide/sync_router_and_store.js b/app/assets/javascripts/ide/sync_router_and_store.js
index 1782c32b3b2..b33bcbb94ea 100644
--- a/app/assets/javascripts/ide/sync_router_and_store.js
+++ b/app/assets/javascripts/ide/sync_router_and_store.js
@@ -1,4 +1,3 @@
-/* eslint-disable import/prefer-default-export */
/**
* This method adds listeners to the given router and store and syncs their state with eachother
*
diff --git a/app/assets/javascripts/ide/utils.js b/app/assets/javascripts/ide/utils.js
index 58a6712c232..cde53e1ef00 100644
--- a/app/assets/javascripts/ide/utils.js
+++ b/app/assets/javascripts/ide/utils.js
@@ -1,5 +1,5 @@
import { languages } from 'monaco-editor';
-import { flatten } from 'lodash';
+import { flatten, isString } from 'lodash';
import { SIDE_LEFT, SIDE_RIGHT } from './constants';
const toLowerCase = x => x.toLowerCase();
@@ -42,15 +42,16 @@ const KNOWN_TYPES = [
},
];
-export function isTextFile(content, mimeType, fileName) {
- const knownType = KNOWN_TYPES.find(type => type.isMatch(mimeType, fileName));
+export function isTextFile({ name, content, mimeType = '' }) {
+ const knownType = KNOWN_TYPES.find(type => type.isMatch(mimeType, name));
if (knownType) return knownType.isText;
// does the string contain ascii characters only (ranges from space to tilde, tabs and new lines)
const asciiRegex = /^[ -~\t\n\r]+$/;
+
// for unknown types, determine the type by evaluating the file contents
- return asciiRegex.test(content);
+ return isString(content) && (content === '' || asciiRegex.test(content));
}
export const createPathWithExt = p => {
@@ -75,17 +76,17 @@ export function registerLanguages(def, ...defs) {
languages.setLanguageConfiguration(languageId, def.conf);
}
-export function registerSchemas({ language, options }, ...schemas) {
- schemas.forEach(schema => registerSchemas(schema));
-
- const defaults = {
- json: languages.json.jsonDefaults,
- yaml: languages.yaml.yamlDefaults,
- };
-
- if (defaults[language]) {
- defaults[language].setDiagnosticsOptions(options);
- }
+export function registerSchema(schema) {
+ const defaults = [languages.json.jsonDefaults, languages.yaml.yamlDefaults];
+ defaults.forEach(d =>
+ d.setDiagnosticsOptions({
+ validate: true,
+ enableSchemaRequest: true,
+ hover: true,
+ completion: true,
+ schemas: [schema],
+ }),
+ );
}
export const otherSide = side => (side === SIDE_RIGHT ? SIDE_LEFT : SIDE_RIGHT);
diff --git a/app/assets/javascripts/import_projects/components/import_projects_table.vue b/app/assets/javascripts/import_projects/components/import_projects_table.vue
index 72fdaca7e24..96100e4ac0c 100644
--- a/app/assets/javascripts/import_projects/components/import_projects_table.vue
+++ b/app/assets/javascripts/import_projects/components/import_projects_table.vue
@@ -1,27 +1,17 @@
<script>
-import { throttle } from 'lodash';
import { mapActions, mapState, mapGetters } from 'vuex';
-import { GlButton, GlLoadingIcon } from '@gitlab/ui';
-import { __, sprintf } from '~/locale';
-import PaginationLinks from '~/vue_shared/components/pagination_links.vue';
-import ImportedProjectTableRow from './imported_project_table_row.vue';
+import { GlButton, GlLoadingIcon, GlIntersectionObserver, GlModal } from '@gitlab/ui';
+import { n__, __, sprintf } from '~/locale';
import ProviderRepoTableRow from './provider_repo_table_row.vue';
-import IncompatibleRepoTableRow from './incompatible_repo_table_row.vue';
-import PageQueryParamSync from './page_query_param_sync.vue';
-import { isProjectImportable } from '../utils';
-
-const reposFetchThrottleDelay = 1000;
export default {
name: 'ImportProjectsTable',
components: {
- ImportedProjectTableRow,
ProviderRepoTableRow,
- IncompatibleRepoTableRow,
- PageQueryParamSync,
GlLoadingIcon,
GlButton,
- PaginationLinks,
+ GlModal,
+ GlIntersectionObserver,
},
props: {
providerTitle: {
@@ -41,14 +31,19 @@ export default {
},
computed: {
- ...mapState(['filter', 'repositories', 'namespaces', 'defaultTargetNamespace', 'pageInfo']),
+ ...mapState(['filter', 'repositories', 'namespaces', 'defaultTargetNamespace']),
...mapGetters([
'isLoading',
'isImportingAnyRepo',
'hasImportableRepos',
'hasIncompatibleRepos',
+ 'importAllCount',
]),
+ pagePaginationStateKey() {
+ return `${this.filter}-${this.repositories.length}`;
+ },
+
availableNamespaces() {
const serializedNamespaces = this.namespaces.map(({ fullPath }) => ({
id: fullPath,
@@ -66,8 +61,12 @@ export default {
importAllButtonText() {
return this.hasIncompatibleRepos
- ? __('Import all compatible repositories')
- : __('Import all repositories');
+ ? n__(
+ 'Import %d compatible repository',
+ 'Import %d compatible repositories',
+ this.importAllCount,
+ )
+ : n__('Import %d repository', 'Import %d repositories', this.importAllCount);
},
emptyStateText() {
@@ -83,7 +82,11 @@ export default {
mounted() {
this.fetchNamespaces();
- this.fetchRepos();
+ this.fetchJobs();
+
+ if (!this.paginatable) {
+ this.fetchRepos();
+ }
},
beforeDestroy() {
@@ -94,105 +97,95 @@ export default {
methods: {
...mapActions([
'fetchRepos',
+ 'fetchJobs',
'fetchNamespaces',
'stopJobsPolling',
'clearJobsEtagPoll',
'setFilter',
'importAll',
- 'setPage',
]),
-
- handleFilterInput({ target }) {
- this.setFilter(target.value);
- },
-
- throttledFetchRepos: throttle(function fetch() {
- this.fetchRepos();
- }, reposFetchThrottleDelay),
-
- isProjectImportable,
},
};
</script>
<template>
<div>
- <page-query-param-sync :page="pageInfo.page" @popstate="setPage" />
-
<p class="light text-nowrap mt-2">
- {{ s__('ImportProjects|Select the projects you want to import') }}
+ {{ s__('ImportProjects|Select the repositories you want to import') }}
</p>
<template v-if="hasIncompatibleRepos">
<slot name="incompatible-repos-warning"></slot>
</template>
+ <div class="d-flex justify-content-between align-items-end flex-wrap mb-3">
+ <gl-button
+ variant="success"
+ :loading="isImportingAnyRepo"
+ :disabled="!hasImportableRepos"
+ type="button"
+ @click="$refs.importAllModal.show()"
+ >{{ importAllButtonText }}</gl-button
+ >
+ <gl-modal
+ ref="importAllModal"
+ modal-id="import-all-modal"
+ :title="s__('ImportProjects|Import repositories')"
+ :ok-title="__('Import')"
+ @ok="importAll"
+ >
+ {{
+ n__(
+ 'Are you sure you want to import %d repository?',
+ 'Are you sure you want to import %d repositories?',
+ importAllCount,
+ )
+ }}
+ </gl-modal>
+
+ <slot name="actions"></slot>
+ <form v-if="filterable" class="gl-ml-auto" novalidate @submit.prevent>
+ <input
+ data-qa-selector="githubish_import_filter_field"
+ class="form-control"
+ name="filter"
+ :placeholder="__('Filter your repositories by name')"
+ autofocus
+ size="40"
+ @keyup.enter="setFilter($event.target.value)"
+ />
+ </form>
+ </div>
+ <div v-if="repositories.length" class="table-responsive">
+ <table class="table import-table">
+ <thead>
+ <th class="import-jobs-from-col">{{ fromHeaderText }}</th>
+ <th class="import-jobs-to-col">{{ __('To GitLab') }}</th>
+ <th class="import-jobs-status-col">{{ __('Status') }}</th>
+ <th class="import-jobs-cta-col"></th>
+ </thead>
+ <tbody>
+ <template v-for="repo in repositories">
+ <provider-repo-table-row
+ :key="repo.importSource.providerLink"
+ :repo="repo"
+ :available-namespaces="availableNamespaces"
+ />
+ </template>
+ </tbody>
+ </table>
+ </div>
+ <gl-intersection-observer
+ v-if="paginatable"
+ :key="pagePaginationStateKey"
+ @appear="fetchRepos"
+ />
<gl-loading-icon
v-if="isLoading"
class="js-loading-button-icon import-projects-loading-icon"
size="md"
/>
- <template v-if="!isLoading">
- <div class="d-flex justify-content-between align-items-end flex-wrap mb-3">
- <gl-button
- variant="success"
- :loading="isImportingAnyRepo"
- :disabled="!hasImportableRepos"
- type="button"
- @click="importAll"
- >{{ importAllButtonText }}</gl-button
- >
- <slot name="actions"></slot>
- <form v-if="filterable" class="gl-ml-auto" novalidate @submit.prevent>
- <input
- :value="filter"
- data-qa-selector="githubish_import_filter_field"
- class="form-control"
- name="filter"
- :placeholder="__('Filter your projects by name')"
- autofocus
- size="40"
- @input="handleFilterInput($event)"
- @keyup.enter="throttledFetchRepos"
- />
- </form>
- </div>
- <div v-if="repositories.length" class="table-responsive">
- <table class="table import-table">
- <thead>
- <th class="import-jobs-from-col">{{ fromHeaderText }}</th>
- <th class="import-jobs-to-col">{{ __('To GitLab') }}</th>
- <th class="import-jobs-status-col">{{ __('Status') }}</th>
- <th class="import-jobs-cta-col"></th>
- </thead>
- <tbody>
- <template v-for="repo in repositories">
- <incompatible-repo-table-row
- v-if="repo.importSource.incompatible"
- :key="repo.importSource.id"
- :repo="repo"
- />
- <provider-repo-table-row
- v-else-if="isProjectImportable(repo)"
- :key="repo.importSource.id"
- :repo="repo"
- :available-namespaces="availableNamespaces"
- />
- <imported-project-table-row v-else :key="repo.importSource.id" :project="repo" />
- </template>
- </tbody>
- </table>
- </div>
- <div v-else class="text-center">
- <strong>{{ emptyStateText }}</strong>
- </div>
- <pagination-links
- v-if="paginatable"
- align="center"
- class="gl-mt-3"
- :page-info="pageInfo"
- :prev-page="pageInfo.page - 1"
- :next-page="repositories.length && pageInfo.page + 1"
- :change="setPage"
- />
- </template>
+
+ <div v-if="!isLoading && repositories.length === 0" class="text-center">
+ <strong>{{ emptyStateText }}</strong>
+ </div>
</div>
</template>
diff --git a/app/assets/javascripts/import_projects/components/imported_project_table_row.vue b/app/assets/javascripts/import_projects/components/imported_project_table_row.vue
deleted file mode 100644
index 50e735b4478..00000000000
--- a/app/assets/javascripts/import_projects/components/imported_project_table_row.vue
+++ /dev/null
@@ -1,59 +0,0 @@
-<script>
-import { GlIcon } from '@gitlab/ui';
-import ImportStatus from './import_status.vue';
-import { STATUSES } from '../constants';
-
-export default {
- name: 'ImportedProjectTableRow',
- components: {
- ImportStatus,
- GlIcon,
- },
- props: {
- project: {
- type: Object,
- required: true,
- },
- },
-
- computed: {
- displayFullPath() {
- return this.project.importedProject.fullPath.replace(/^\//, '');
- },
-
- isFinished() {
- return this.project.importStatus === STATUSES.FINISHED;
- },
- },
-};
-</script>
-
-<template>
- <tr class="import-row">
- <td>
- <a
- :href="project.importSource.providerLink"
- rel="noreferrer noopener"
- target="_blank"
- data-testid="providerLink"
- >{{ project.importSource.fullName }}
- <gl-icon v-if="project.importSource.providerLink" name="external-link" />
- </a>
- </td>
- <td data-testid="fullPath">{{ displayFullPath }}</td>
- <td>
- <import-status :status="project.importStatus" />
- </td>
- <td>
- <a
- v-if="isFinished"
- class="btn btn-default"
- data-testid="goToProject"
- :href="project.importedProject.fullPath"
- rel="noreferrer noopener"
- target="_blank"
- >{{ __('Go to project') }}
- </a>
- </td>
- </tr>
-</template>
diff --git a/app/assets/javascripts/import_projects/components/incompatible_repo_table_row.vue b/app/assets/javascripts/import_projects/components/incompatible_repo_table_row.vue
deleted file mode 100644
index 3140585ccd7..00000000000
--- a/app/assets/javascripts/import_projects/components/incompatible_repo_table_row.vue
+++ /dev/null
@@ -1,32 +0,0 @@
-<script>
-import { GlIcon, GlBadge } from '@gitlab/ui';
-
-export default {
- components: {
- GlBadge,
- GlIcon,
- },
- props: {
- repo: {
- type: Object,
- required: true,
- },
- },
-};
-</script>
-
-<template>
- <tr class="import-row">
- <td>
- <a :href="repo.importSource.providerLink" rel="noreferrer noopener" target="_blank"
- >{{ repo.importSource.fullName }}
- <gl-icon v-if="repo.importSource.providerLink" name="external-link" />
- </a>
- </td>
- <td></td>
- <td></td>
- <td>
- <gl-badge variant="danger">{{ __('Incompatible project') }}</gl-badge>
- </td>
- </tr>
-</template>
diff --git a/app/assets/javascripts/import_projects/components/page_query_param_sync.vue b/app/assets/javascripts/import_projects/components/page_query_param_sync.vue
deleted file mode 100644
index 5ba3d70f5d0..00000000000
--- a/app/assets/javascripts/import_projects/components/page_query_param_sync.vue
+++ /dev/null
@@ -1,39 +0,0 @@
-<script>
-import { queryToObject, setUrlParams, updateHistory } from '~/lib/utils/url_utility';
-
-export default {
- props: {
- page: {
- type: Number,
- required: true,
- },
- },
-
- watch: {
- page(newPage) {
- updateHistory({
- url: setUrlParams({
- page: newPage === 1 ? null : newPage,
- }),
- });
- },
- },
-
- created() {
- window.addEventListener('popstate', this.updatePage);
- },
-
- beforeDestroy() {
- window.removeEventListener('popstate', this.updatePage);
- },
-
- methods: {
- updatePage() {
- const page = parseInt(queryToObject(window.location.search).page, 10) || 1;
- this.$emit('popstate', page);
- },
- },
-
- render: () => null,
-};
-</script>
diff --git a/app/assets/javascripts/import_projects/components/provider_repo_table_row.vue b/app/assets/javascripts/import_projects/components/provider_repo_table_row.vue
index d8cffc6a7d5..18971313dfe 100644
--- a/app/assets/javascripts/import_projects/components/provider_repo_table_row.vue
+++ b/app/assets/javascripts/import_projects/components/provider_repo_table_row.vue
@@ -1,9 +1,11 @@
<script>
import { mapState, mapGetters, mapActions } from 'vuex';
-import { GlIcon } from '@gitlab/ui';
+import { GlIcon, GlBadge } from '@gitlab/ui';
import Select2Select from '~/vue_shared/components/select2_select.vue';
import { __ } from '~/locale';
import ImportStatus from './import_status.vue';
+import { STATUSES } from '../constants';
+import { isProjectImportable, isIncompatible, getImportStatus } from '../utils';
export default {
name: 'ProviderRepoTableRow',
@@ -11,6 +13,7 @@ export default {
Select2Select,
ImportStatus,
GlIcon,
+ GlBadge,
},
props: {
repo: {
@@ -27,6 +30,26 @@ export default {
...mapState(['ciCdOnly']),
...mapGetters(['getImportTarget']),
+ displayFullPath() {
+ return this.repo.importedProject.fullPath.replace(/^\//, '');
+ },
+
+ isFinished() {
+ return this.repo.importedProject?.importStatus === STATUSES.FINISHED;
+ },
+
+ isImportNotStarted() {
+ return isProjectImportable(this.repo);
+ },
+
+ isIncompatible() {
+ return isIncompatible(this.repo);
+ },
+
+ importStatus() {
+ return getImportStatus(this.repo);
+ },
+
importTarget() {
return this.getImportTarget(this.repo.importSource.id);
},
@@ -85,9 +108,9 @@ export default {
<gl-icon v-if="repo.importSource.providerLink" name="external-link" />
</a>
</td>
- <td class="d-flex flex-wrap flex-lg-nowrap">
- <template v-if="repo.target">{{ repo.target }}</template>
- <template v-else>
+ <td class="d-flex flex-wrap flex-lg-nowrap" data-testid="fullPath">
+ <template v-if="repo.importSource.target">{{ repo.importSource.target }}</template>
+ <template v-else-if="isImportNotStarted">
<select2-select v-model="targetNamespaceSelect" :options="select2Options" />
<span class="px-2 import-slash-divider d-flex justify-content-center align-items-center"
>/</span
@@ -98,18 +121,31 @@ export default {
class="form-control import-project-name-input qa-project-path-field"
/>
</template>
+ <template v-else-if="repo.importedProject">{{ displayFullPath }}</template>
</td>
<td>
- <import-status :status="repo.importStatus" />
+ <import-status :status="importStatus" />
</td>
- <td>
+ <td data-testid="actions">
+ <a
+ v-if="isFinished"
+ class="btn btn-default"
+ :href="repo.importedProject.fullPath"
+ rel="noreferrer noopener"
+ target="_blank"
+ >{{ __('Go to project') }}
+ </a>
<button
+ v-if="isImportNotStarted"
type="button"
class="qa-import-button btn btn-default"
@click="fetchImport(repo.importSource.id)"
>
{{ importButtonText }}
</button>
+ <gl-badge v-else-if="isIncompatible" variant="danger">{{
+ __('Incompatible project')
+ }}</gl-badge>
</td>
</tr>
</template>
diff --git a/app/assets/javascripts/import_projects/store/actions.js b/app/assets/javascripts/import_projects/store/actions.js
index af410f411d8..7b70d290278 100644
--- a/app/assets/javascripts/import_projects/store/actions.js
+++ b/app/assets/javascripts/import_projects/store/actions.js
@@ -1,11 +1,7 @@
import Visibility from 'visibilityjs';
import * as types from './mutation_types';
import { isProjectImportable } from '../utils';
-import {
- convertObjectPropsToCamelCase,
- normalizeHeaders,
- parseIntPagination,
-} from '~/lib/utils/common_utils';
+import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import Poll from '~/lib/utils/poll';
import { visitUrl, objectToQuery } from '~/lib/utils/url_utility';
import { deprecatedCreateFlash as createFlash } from '~/flash';
@@ -54,12 +50,9 @@ const importAll = ({ state, dispatch }) => {
);
};
-const fetchReposFactory = ({ reposPath = isRequired(), hasPagination }) => ({
- state,
- dispatch,
- commit,
-}) => {
- dispatch('stopJobsPolling');
+const fetchReposFactory = ({ reposPath = isRequired() }) => ({ state, commit }) => {
+ const nextPage = state.pageInfo.page + 1;
+ commit(types.SET_PAGE, nextPage);
commit(types.REQUEST_REPOS);
const { provider, filter } = state;
@@ -68,21 +61,16 @@ const fetchReposFactory = ({ reposPath = isRequired(), hasPagination }) => ({
.get(
pathWithParams({
path: reposPath,
- filter,
- page: hasPagination ? state.pageInfo.page.toString() : '',
+ filter: filter ?? '',
+ page: nextPage === 1 ? '' : nextPage.toString(),
}),
)
- .then(({ data, headers }) => {
- const normalizedHeaders = normalizeHeaders(headers);
-
- if ('X-PAGE' in normalizedHeaders) {
- commit(types.SET_PAGE_INFO, parseIntPagination(normalizedHeaders));
- }
-
+ .then(({ data }) => {
commit(types.RECEIVE_REPOS_SUCCESS, convertObjectPropsToCamelCase(data, { deep: true }));
})
- .then(() => dispatch('fetchJobs'))
.catch(e => {
+ commit(types.SET_PAGE, nextPage - 1);
+
if (hasRedirectInError(e)) {
redirectToUrlInError(e);
} else {
@@ -136,8 +124,6 @@ const fetchImportFactory = (importPath = isRequired()) => ({ state, commit, gett
};
export const fetchJobsFactory = (jobsPath = isRequired()) => ({ state, commit, dispatch }) => {
- const { filter } = state;
-
if (eTagPoll) {
stopJobsPolling();
clearJobsEtagPoll();
@@ -145,7 +131,7 @@ export const fetchJobsFactory = (jobsPath = isRequired()) => ({ state, commit, d
eTagPoll = new Poll({
resource: {
- fetchJobs: () => axios.get(pathWithParams({ path: jobsPath, filter })),
+ fetchJobs: () => axios.get(pathWithParams({ path: jobsPath, filter: state.filter })),
},
method: 'fetchJobs',
successCallback: ({ data }) =>
@@ -157,7 +143,6 @@ export const fetchJobsFactory = (jobsPath = isRequired()) => ({ state, commit, d
createFlash(s__('ImportProjects|Update of imported projects with realtime changes failed'));
}
},
- data: { filter },
});
if (!Visibility.hidden()) {
@@ -196,7 +181,7 @@ const setPage = ({ state, commit, dispatch }, page) => {
return dispatch('fetchRepos');
};
-export default ({ endpoints = isRequired(), hasPagination }) => ({
+export default ({ endpoints = isRequired() }) => ({
clearJobsEtagPoll,
stopJobsPolling,
restartJobsPolling,
@@ -204,7 +189,7 @@ export default ({ endpoints = isRequired(), hasPagination }) => ({
setImportTarget,
importAll,
setPage,
- fetchRepos: fetchReposFactory({ reposPath: endpoints.reposPath, hasPagination }),
+ fetchRepos: fetchReposFactory({ reposPath: endpoints.reposPath }),
fetchImport: fetchImportFactory(endpoints.importPath),
fetchJobs: fetchJobsFactory(endpoints.jobsPath),
fetchNamespaces: fetchNamespacesFactory(endpoints.namespacesPath),
diff --git a/app/assets/javascripts/import_projects/store/getters.js b/app/assets/javascripts/import_projects/store/getters.js
index 7d529c94d7d..b76c52beea2 100644
--- a/app/assets/javascripts/import_projects/store/getters.js
+++ b/app/assets/javascripts/import_projects/store/getters.js
@@ -1,17 +1,20 @@
import { STATUSES } from '../constants';
+import { isProjectImportable, isIncompatible } from '../utils';
export const isLoading = state => state.isLoadingRepos || state.isLoadingNamespaces;
export const isImportingAnyRepo = state =>
state.repositories.some(repo =>
- [STATUSES.SCHEDULING, STATUSES.SCHEDULED, STATUSES.STARTED].includes(repo.importStatus),
+ [STATUSES.SCHEDULING, STATUSES.SCHEDULED, STATUSES.STARTED].includes(
+ repo.importedProject?.importStatus,
+ ),
);
-export const hasIncompatibleRepos = state =>
- state.repositories.some(repo => repo.importSource.incompatible);
+export const hasIncompatibleRepos = state => state.repositories.some(isIncompatible);
-export const hasImportableRepos = state =>
- state.repositories.some(repo => repo.importStatus === STATUSES.NONE);
+export const hasImportableRepos = state => state.repositories.some(isProjectImportable);
+
+export const importAllCount = state => state.repositories.filter(isProjectImportable).length;
export const getImportTarget = state => repoId => {
if (state.customImportTargets[repoId]) {
diff --git a/app/assets/javascripts/import_projects/store/mutations.js b/app/assets/javascripts/import_projects/store/mutations.js
index b3dbef896a6..6999253d4b2 100644
--- a/app/assets/javascripts/import_projects/store/mutations.js
+++ b/app/assets/javascripts/import_projects/store/mutations.js
@@ -2,46 +2,88 @@ import Vue from 'vue';
import * as types from './mutation_types';
import { STATUSES } from '../constants';
+const makeNewImportedProject = importedProject => ({
+ importSource: {
+ id: importedProject.id,
+ fullName: importedProject.importSource,
+ sanitizedName: importedProject.name,
+ providerLink: importedProject.providerLink,
+ },
+ importedProject,
+});
+
+const makeNewIncompatibleProject = project => ({
+ importSource: { ...project, incompatible: true },
+ importedProject: null,
+});
+
+const processLegacyEntries = ({ newRepositories, existingRepositories, factory }) => {
+ const newEntries = [];
+ newRepositories.forEach(project => {
+ const existingProject = existingRepositories.find(p => p.importSource.id === project.id);
+ const importedProjectShape = factory(project);
+
+ if (existingProject) {
+ Object.assign(existingProject, importedProjectShape);
+ } else {
+ newEntries.push(importedProjectShape);
+ }
+ });
+ return newEntries;
+};
+
export default {
[types.SET_FILTER](state, filter) {
state.filter = filter;
+ state.repositories = [];
+ state.pageInfo.page = 0;
},
[types.REQUEST_REPOS](state) {
state.isLoadingRepos = true;
},
- [types.RECEIVE_REPOS_SUCCESS](
- state,
- { importedProjects, providerRepos, incompatibleRepos = [] },
- ) {
- // Normalizing structure to support legacy backend format
- // See https://gitlab.com/gitlab-org/gitlab/-/issues/27370#note_379034091 for details
-
+ [types.RECEIVE_REPOS_SUCCESS](state, repositories) {
state.isLoadingRepos = false;
- state.repositories = [
- ...importedProjects.map(({ importSource, providerLink, importStatus, ...project }) => ({
- importSource: {
- id: `finished-${project.id}`,
- fullName: importSource,
- sanitizedName: project.name,
- providerLink,
- },
- importStatus,
- importedProject: project,
- })),
- ...providerRepos.map(project => ({
- importSource: project,
- importStatus: STATUSES.NONE,
- importedProject: null,
- })),
- ...incompatibleRepos.map(project => ({
- importSource: { ...project, incompatible: true },
- importStatus: STATUSES.NONE,
- importedProject: null,
- })),
- ];
+ if (!Array.isArray(repositories)) {
+ // Legacy code path, will be removed when all importers will be switched to new pagination format
+ // https://gitlab.com/gitlab-org/gitlab/-/issues/27370#note_379034091
+
+ const newImportedProjects = processLegacyEntries({
+ newRepositories: repositories.importedProjects,
+ existingRepositories: state.repositories,
+ factory: makeNewImportedProject,
+ });
+
+ const incompatibleRepos = repositories.incompatibleRepos ?? [];
+ const newIncompatibleProjects = processLegacyEntries({
+ newRepositories: incompatibleRepos,
+ existingRepositories: state.repositories,
+ factory: makeNewIncompatibleProject,
+ });
+
+ state.repositories = [
+ ...newImportedProjects,
+ ...state.repositories,
+ ...repositories.providerRepos.map(project => ({
+ importSource: project,
+ importedProject: null,
+ })),
+ ...newIncompatibleProjects,
+ ];
+
+ if (incompatibleRepos.length === 0 && repositories.providerRepos.length === 0) {
+ state.pageInfo.page -= 1;
+ }
+
+ return;
+ }
+
+ state.repositories = [...state.repositories, ...repositories];
+ if (repositories.length === 0) {
+ state.pageInfo.page -= 1;
+ }
},
[types.RECEIVE_REPOS_ERROR](state) {
@@ -50,31 +92,27 @@ export default {
[types.REQUEST_IMPORT](state, { repoId, importTarget }) {
const existingRepo = state.repositories.find(r => r.importSource.id === repoId);
- existingRepo.importStatus = STATUSES.SCHEDULING;
existingRepo.importedProject = {
+ importStatus: STATUSES.SCHEDULING,
fullPath: `/${importTarget.targetNamespace}/${importTarget.newName}`,
};
},
[types.RECEIVE_IMPORT_SUCCESS](state, { importedProject, repoId }) {
- const { importStatus, ...project } = importedProject;
-
const existingRepo = state.repositories.find(r => r.importSource.id === repoId);
- existingRepo.importStatus = importStatus;
- existingRepo.importedProject = project;
+ existingRepo.importedProject = importedProject;
},
[types.RECEIVE_IMPORT_ERROR](state, repoId) {
const existingRepo = state.repositories.find(r => r.importSource.id === repoId);
- existingRepo.importStatus = STATUSES.NONE;
existingRepo.importedProject = null;
},
[types.RECEIVE_JOBS_SUCCESS](state, updatedProjects) {
updatedProjects.forEach(updatedProject => {
const repo = state.repositories.find(p => p.importedProject?.id === updatedProject.id);
- if (repo) {
- repo.importStatus = updatedProject.importStatus;
+ if (repo?.importedProject) {
+ repo.importedProject.importStatus = updatedProject.importStatus;
}
});
},
@@ -105,10 +143,6 @@ export default {
}
},
- [types.SET_PAGE_INFO](state, pageInfo) {
- state.pageInfo = pageInfo;
- },
-
[types.SET_PAGE](state, page) {
state.pageInfo.page = page;
},
diff --git a/app/assets/javascripts/import_projects/store/state.js b/app/assets/javascripts/import_projects/store/state.js
index 3318181e4af..ecd93561d52 100644
--- a/app/assets/javascripts/import_projects/store/state.js
+++ b/app/assets/javascripts/import_projects/store/state.js
@@ -8,6 +8,6 @@ export default () => ({
ciCdOnly: false,
filter: '',
pageInfo: {
- page: 1,
+ page: 0,
},
});
diff --git a/app/assets/javascripts/import_projects/utils.js b/app/assets/javascripts/import_projects/utils.js
index c2a2d5a607d..695b12cbcba 100644
--- a/app/assets/javascripts/import_projects/utils.js
+++ b/app/assets/javascripts/import_projects/utils.js
@@ -1,7 +1,13 @@
import { STATUSES } from './constants';
-// Will be expanded in future
-// eslint-disable-next-line import/prefer-default-export
+export function isIncompatible(project) {
+ return project.importSource.incompatible;
+}
+
+export function getImportStatus(project) {
+ return project.importedProject?.importStatus ?? STATUSES.NONE;
+}
+
export function isProjectImportable(project) {
- return project.importStatus === STATUSES.NONE && !project.importSource.incompatible;
+ return !isIncompatible(project) && getImportStatus(project) === STATUSES.NONE;
}
diff --git a/app/assets/javascripts/incidents/components/incidents_list.vue b/app/assets/javascripts/incidents/components/incidents_list.vue
index 46852e4ddd9..670c42cbdac 100644
--- a/app/assets/javascripts/incidents/components/incidents_list.vue
+++ b/app/assets/javascripts/incidents/components/incidents_list.vue
@@ -23,6 +23,8 @@ import { s__ } from '~/locale';
import { mergeUrlParams, joinPaths, visitUrl } from '~/lib/utils/url_utility';
import getIncidents from '../graphql/queries/get_incidents.query.graphql';
import getIncidentsCountByStatus from '../graphql/queries/get_count_by_status.query.graphql';
+import SeverityToken from '~/sidebar/components/severity/severity.vue';
+import { INCIDENT_SEVERITY } from '~/sidebar/components/severity/constants';
import { I18N, DEFAULT_PAGE_SIZE, INCIDENT_SEARCH_DELAY, INCIDENT_STATUS_TABS } from '../constants';
const TH_TEST_ID = { 'data-testid': 'incident-management-created-at-sort' };
@@ -45,6 +47,12 @@ export default {
statusTabs: INCIDENT_STATUS_TABS,
fields: [
{
+ key: 'severity',
+ label: s__('IncidentManagement|Severity'),
+ thClass: `gl-pointer-events-none`,
+ tdClass,
+ },
+ {
key: 'title',
label: s__('IncidentManagement|Incident'),
thClass: `gl-pointer-events-none gl-w-half`,
@@ -82,6 +90,7 @@ export default {
PublishedCell: () => import('ee_component/incidents/components/published_cell.vue'),
GlBadge,
GlEmptyState,
+ SeverityToken,
},
directives: {
GlTooltip: GlTooltipDirective,
@@ -208,6 +217,31 @@ export default {
isEmpty() {
return !this.incidents.list?.length;
},
+ showList() {
+ return !this.isEmpty || this.errored || this.loading;
+ },
+ activeClosedTabHasNoIncidents() {
+ const { all, closed } = this.incidentsCount || {};
+ const isClosedTabActive = this.statusFilter === this.$options.statusTabs[1].filters;
+
+ return isClosedTabActive && all && !closed;
+ },
+ emptyStateData() {
+ const {
+ emptyState: { title, emptyClosedTabTitle, description },
+ createIncidentBtnLabel,
+ } = this.$options.i18n;
+
+ if (this.activeClosedTabHasNoIncidents) {
+ return { title: emptyClosedTabTitle };
+ }
+ return {
+ title,
+ description,
+ btnLink: this.newIncidentPath,
+ btnText: createIncidentBtnLabel,
+ };
+ },
},
methods: {
onInputChange: debounce(function debounceSearch(input) {
@@ -255,6 +289,9 @@ export default {
this.sort = `${sortingColumn}_${sortingDirection}`;
},
+ getSeverity(severity) {
+ return INCIDENT_SEVERITY[severity];
+ },
},
};
</script>
@@ -279,7 +316,7 @@ export default {
</gl-tabs>
<gl-button
- v-if="!isEmpty"
+ v-if="!isEmpty || activeClosedTabHasNoIncidents"
class="gl-my-3 gl-mr-5 create-incident-button"
data-testid="createIncidentBtn"
data-qa-selector="create_incident_button"
@@ -307,6 +344,7 @@ export default {
{{ s__('IncidentManagement|Incidents') }}
</h4>
<gl-table
+ v-if="showList"
:items="incidents.list || []"
:fields="availableFields"
:show-empty="true"
@@ -322,6 +360,10 @@ export default {
@row-clicked="navigateToIncidentDetails"
@sort-changed="fetchSortedData"
>
+ <template #cell(severity)="{ item }">
+ <severity-token :severity="getSeverity(item.severity)" />
+ </template>
+
<template #cell(title)="{ item }">
<div :class="{ 'gl-display-flex gl-align-items-center': item.state === 'closed' }">
<div class="gl-max-w-full text-truncate" :title="item.title">{{ item.title }}</div>
@@ -379,21 +421,20 @@ export default {
<gl-loading-icon size="lg" color="dark" class="mt-3" />
</template>
- <template #empty>
- <gl-empty-state
- v-if="!errored"
- :title="$options.i18n.emptyState.title"
- :svg-path="emptyListSvgPath"
- :description="$options.i18n.emptyState.description"
- :primary-button-link="newIncidentPath"
- :primary-button-text="$options.i18n.createIncidentBtnLabel"
- />
- <span v-else>
- {{ $options.i18n.noIncidents }}
- </span>
+ <template v-if="errored" #empty>
+ {{ $options.i18n.noIncidents }}
</template>
</gl-table>
+ <gl-empty-state
+ v-else
+ :title="emptyStateData.title"
+ :svg-path="emptyListSvgPath"
+ :description="emptyStateData.description"
+ :primary-button-link="emptyStateData.btnLink"
+ :primary-button-text="emptyStateData.btnText"
+ />
+
<gl-pagination
v-if="showPaginationControls"
:value="pagination.currentPage"
diff --git a/app/assets/javascripts/incidents/constants.js b/app/assets/javascripts/incidents/constants.js
index 02d8172533d..289b36d9848 100644
--- a/app/assets/javascripts/incidents/constants.js
+++ b/app/assets/javascripts/incidents/constants.js
@@ -9,6 +9,7 @@ export const I18N = {
searchPlaceholder: __('Search results…'),
emptyState: {
title: s__('IncidentManagement|Display your incidents in a dedicated view'),
+ emptyClosedTabTitle: s__('IncidentManagement|There are no closed incidents'),
description: s__(
'IncidentManagement|All alerts promoted to incidents will automatically be displayed within the list. You can also create a new incident using the button below.',
),
@@ -34,4 +35,4 @@ export const INCIDENT_STATUS_TABS = [
];
export const INCIDENT_SEARCH_DELAY = 300;
-export const DEFAULT_PAGE_SIZE = 10;
+export const DEFAULT_PAGE_SIZE = 20;
diff --git a/app/assets/javascripts/incidents/graphql/fragments/incident_fields.fragment.graphql b/app/assets/javascripts/incidents/graphql/fragments/incident_fields.fragment.graphql
new file mode 100644
index 00000000000..eb2dde14464
--- /dev/null
+++ b/app/assets/javascripts/incidents/graphql/fragments/incident_fields.fragment.graphql
@@ -0,0 +1,3 @@
+fragment IncidentFields on Issue {
+ severity
+}
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 0f56e8640bd..dab130835e2 100644
--- a/app/assets/javascripts/incidents/graphql/queries/get_incidents.query.graphql
+++ b/app/assets/javascripts/incidents/graphql/queries/get_incidents.query.graphql
@@ -1,3 +1,5 @@
+#import "ee_else_ce/incidents/graphql/fragments/incident_fields.fragment.graphql"
+
query getIncidents(
$projectPath: ID!
$issueTypes: [IssueType!]
@@ -39,7 +41,7 @@ query getIncidents(
webUrl
}
}
- statusPagePublishedIncident
+ ...IncidentFields
}
pageInfo {
hasNextPage
diff --git a/app/assets/javascripts/incidents_settings/components/alerts_form.vue b/app/assets/javascripts/incidents_settings/components/alerts_form.vue
index 5872ac39c96..17a77f650e0 100644
--- a/app/assets/javascripts/incidents_settings/components/alerts_form.vue
+++ b/app/assets/javascripts/incidents_settings/components/alerts_form.vue
@@ -6,8 +6,8 @@ import {
GlIcon,
GlFormGroup,
GlFormCheckbox,
- GlNewDropdown,
- GlNewDropdownItem,
+ GlDropdown,
+ GlDropdownItem,
} from '@gitlab/ui';
import {
I18N_ALERT_SETTINGS_FORM,
@@ -24,8 +24,8 @@ export default {
GlFormGroup,
GlIcon,
GlFormCheckbox,
- GlNewDropdown,
- GlNewDropdownItem,
+ GlDropdown,
+ GlDropdownItem,
},
inject: ['service', 'alertSettings'],
data() {
@@ -34,6 +34,7 @@ export default {
createIssueEnabled: this.alertSettings.createIssue,
issueTemplate: this.alertSettings.issueTemplateKey,
sendEmailEnabled: this.alertSettings.sendEmail,
+ autoCloseIncident: this.alertSettings.autoCloseIncident,
loading: false,
};
},
@@ -49,6 +50,7 @@ export default {
create_issue: this.createIssueEnabled,
issue_template_key: this.issueTemplate,
send_email: this.sendEmailEnabled,
+ auto_close_incident: this.autoCloseIncident,
};
},
},
@@ -99,13 +101,13 @@ export default {
<gl-icon name="question" :size="12" />
</gl-link>
</label>
- <gl-new-dropdown
+ <gl-dropdown
id="alert-integration-settings-issue-template"
data-qa-selector="incident_templates_dropdown"
:text="issueTemplateHeader"
:block="true"
>
- <gl-new-dropdown-item
+ <gl-dropdown-item
v-for="template in templates"
:key="template.key"
data-qa-selector="incident_templates_item"
@@ -114,8 +116,8 @@ export default {
@click="selectIssueTemplate(template.key)"
>
{{ template.name }}
- </gl-new-dropdown-item>
- </gl-new-dropdown>
+ </gl-dropdown-item>
+ </gl-dropdown>
</gl-form-group>
<gl-form-group class="gl-pl-0 gl-mb-5">
@@ -123,6 +125,11 @@ export default {
<span>{{ $options.i18n.sendEmail.label }}</span>
</gl-form-checkbox>
</gl-form-group>
+ <gl-form-group class="gl-pl-0 gl-mb-5">
+ <gl-form-checkbox v-model="autoCloseIncident">
+ <span>{{ $options.i18n.autoCloseIncidents.label }}</span>
+ </gl-form-checkbox>
+ </gl-form-group>
<div class="gl-display-flex gl-justify-content-end">
<gl-button
ref="submitBtn"
diff --git a/app/assets/javascripts/incidents_settings/constants.js b/app/assets/javascripts/incidents_settings/constants.js
index 77f7ee2c4a3..42f1f645d16 100644
--- a/app/assets/javascripts/incidents_settings/constants.js
+++ b/app/assets/javascripts/incidents_settings/constants.js
@@ -42,6 +42,9 @@ export const I18N_ALERT_SETTINGS_FORM = {
sendEmail: {
label: __('Send a separate email notification to Developers.'),
},
+ autoCloseIncidents: {
+ label: __('Automatically close incident issues when the associated Prometheus alert resolves.'),
+ },
};
export const NO_ISSUE_TEMPLATE_SELECTED = { key: '', name: __('No template selected') };
diff --git a/app/assets/javascripts/incidents_settings/index.js b/app/assets/javascripts/incidents_settings/index.js
index 80e7d07feca..ad875d49768 100644
--- a/app/assets/javascripts/incidents_settings/index.js
+++ b/app/assets/javascripts/incidents_settings/index.js
@@ -20,6 +20,7 @@ export default () => {
pagerdutyActive,
pagerdutyWebhookUrl,
pagerdutyResetKeyPath,
+ autoCloseIncident,
},
} = el;
@@ -33,6 +34,7 @@ export default () => {
createIssue: parseBoolean(createIssue),
issueTemplateKey,
sendEmail: parseBoolean(sendEmail),
+ autoCloseIncident: parseBoolean(autoCloseIncident),
},
pagerDutySettings: {
active: parseBoolean(pagerdutyActive),
diff --git a/app/assets/javascripts/init_changes_dropdown.js b/app/assets/javascripts/init_changes_dropdown.js
index e708e5d0978..c0dc6ce07b1 100644
--- a/app/assets/javascripts/init_changes_dropdown.js
+++ b/app/assets/javascripts/init_changes_dropdown.js
@@ -1,10 +1,11 @@
import $ from 'jquery';
import { stickyMonitor } from './lib/utils/sticky';
+import initDeprecatedJQueryDropdown from '~/deprecated_jquery_dropdown';
export default stickyTop => {
stickyMonitor(document.querySelector('.js-diff-files-changed'), stickyTop);
- $('.js-diff-stats-dropdown').glDropdown({
+ initDeprecatedJQueryDropdown($('.js-diff-stats-dropdown'), {
filterable: true,
remoteFilter: false,
});
diff --git a/app/assets/javascripts/init_issuable_sidebar.js b/app/assets/javascripts/init_issuable_sidebar.js
index e61b37a2d1f..528d5d8072f 100644
--- a/app/assets/javascripts/init_issuable_sidebar.js
+++ b/app/assets/javascripts/init_issuable_sidebar.js
@@ -4,8 +4,8 @@ import MilestoneSelect from './milestone_select';
import LabelsSelect from './labels_select';
import IssuableContext from './issuable_context';
import Sidebar from './right_sidebar';
-
import DueDateSelectors from './due_date_select';
+import { mountSidebarLabels } from '~/sidebar/mount_sidebar';
export default () => {
const sidebarOptions = JSON.parse(document.querySelector('.js-sidebar-options').innerHTML);
@@ -17,4 +17,6 @@ export default () => {
new IssuableContext(sidebarOptions.currentUser);
new DueDateSelectors();
Sidebar.initialize();
+
+ mountSidebarLabels();
};
diff --git a/app/assets/javascripts/integrations/edit/components/active_toggle.vue b/app/assets/javascripts/integrations/edit/components/active_checkbox.vue
index e6a96600539..6698984d02f 100644
--- a/app/assets/javascripts/integrations/edit/components/active_toggle.vue
+++ b/app/assets/javascripts/integrations/edit/components/active_checkbox.vue
@@ -1,36 +1,31 @@
<script>
import { mapGetters } from 'vuex';
-import { GlFormGroup, GlToggle } from '@gitlab/ui';
+import { GlFormGroup, GlFormCheckbox } from '@gitlab/ui';
import eventHub from '../event_hub';
export default {
- name: 'ActiveToggle',
+ name: 'ActiveCheckbox',
components: {
GlFormGroup,
- GlToggle,
- },
- props: {
- initialActivated: {
- type: Boolean,
- required: true,
- },
+ GlFormCheckbox,
},
data() {
return {
- activated: this.initialActivated,
+ activated: false,
};
},
computed: {
- ...mapGetters(['isInheriting']),
+ ...mapGetters(['isInheriting', 'propsSource']),
},
mounted() {
+ this.activated = this.propsSource.initialActivated;
// Initialize view
this.$nextTick(() => {
- this.onToggle(this.activated);
+ this.onChange(this.activated);
});
},
methods: {
- onToggle(e) {
+ onChange(e) {
eventHub.$emit('toggle', e);
},
},
@@ -39,12 +34,15 @@ export default {
<template>
<gl-form-group :label="__('Enable integration')" label-for="service[active]">
- <gl-toggle
+ <input name="service[active]" type="hidden" :value="activated || false" />
+ <gl-form-checkbox
v-model="activated"
name="service[active]"
class="gl-display-block gl-line-height-0"
:disabled="isInheriting"
- @change="onToggle"
- />
+ @change="onChange"
+ >
+ {{ __('Active') }}
+ </gl-form-checkbox>
</gl-form-group>
</template>
diff --git a/app/assets/javascripts/integrations/edit/components/dynamic_field.vue b/app/assets/javascripts/integrations/edit/components/dynamic_field.vue
index 090381b8da4..9dde1ed1055 100644
--- a/app/assets/javascripts/integrations/edit/components/dynamic_field.vue
+++ b/app/assets/javascripts/integrations/edit/components/dynamic_field.vue
@@ -1,4 +1,5 @@
<script>
+/* eslint-disable vue/no-v-html */
import { mapGetters } from 'vuex';
import { capitalize, lowerCase, isEmpty } from 'lodash';
import { GlFormGroup, GlFormCheckbox, GlFormInput, GlFormSelect, GlFormTextarea } from '@gitlab/ui';
diff --git a/app/assets/javascripts/integrations/edit/components/integration_form.vue b/app/assets/javascripts/integrations/edit/components/integration_form.vue
index 5088664c3bd..0460ed6791e 100644
--- a/app/assets/javascripts/integrations/edit/components/integration_form.vue
+++ b/app/assets/javascripts/integrations/edit/components/integration_form.vue
@@ -1,9 +1,11 @@
<script>
import { mapState, mapActions, mapGetters } from 'vuex';
+import { GlButton } from '@gitlab/ui';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
+import eventHub from '../event_hub';
import OverrideDropdown from './override_dropdown.vue';
-import ActiveToggle from './active_toggle.vue';
+import ActiveCheckbox from './active_checkbox.vue';
import JiraTriggerFields from './jira_trigger_fields.vue';
import JiraIssuesFields from './jira_issues_fields.vue';
import TriggerFields from './trigger_fields.vue';
@@ -13,16 +15,20 @@ export default {
name: 'IntegrationForm',
components: {
OverrideDropdown,
- ActiveToggle,
+ ActiveCheckbox,
JiraTriggerFields,
JiraIssuesFields,
TriggerFields,
DynamicField,
+ GlButton,
},
mixins: [glFeatureFlagsMixin()],
computed: {
- ...mapGetters(['currentKey', 'propsSource']),
- ...mapState(['adminState', 'override']),
+ ...mapGetters(['currentKey', 'propsSource', 'isSavingOrTesting']),
+ ...mapState(['defaultState', 'override', 'isSaving', 'isTesting']),
+ isEditable() {
+ return this.propsSource.editable;
+ },
isJira() {
return this.propsSource.type === 'jira';
},
@@ -31,7 +37,15 @@ export default {
},
},
methods: {
- ...mapActions(['setOverride']),
+ ...mapActions(['setOverride', 'setIsSaving', 'setIsTesting']),
+ onSaveClick() {
+ this.setIsSaving(true);
+ eventHub.$emit('saveIntegration');
+ },
+ onTestClick() {
+ this.setIsTesting(true);
+ eventHub.$emit('testIntegration');
+ },
},
};
</script>
@@ -39,16 +53,13 @@ export default {
<template>
<div>
<override-dropdown
- v-if="adminState !== null"
- :inherit-from-id="adminState.id"
+ v-if="defaultState !== null"
+ :inherit-from-id="defaultState.id"
:override="override"
+ :learn-more-path="propsSource.learnMorePath"
@change="setOverride"
/>
- <active-toggle
- v-if="propsSource.showActive"
- :key="`${currentKey}-active-toggle`"
- v-bind="propsSource.activeToggleProps"
- />
+ <active-checkbox v-if="propsSource.showActive" :key="`${currentKey}-active-checkbox`" />
<jira-trigger-fields
v-if="isJira"
:key="`${currentKey}-jira-trigger-fields`"
@@ -70,5 +81,29 @@ export default {
:key="`${currentKey}-jira-issues-fields`"
v-bind="propsSource.jiraIssuesProps"
/>
+ <div v-if="isEditable" class="footer-block row-content-block">
+ <gl-button
+ category="primary"
+ variant="success"
+ type="submit"
+ :loading="isSaving"
+ :disabled="isSavingOrTesting"
+ data-qa-selector="save_changes_button"
+ @click.prevent="onSaveClick"
+ >
+ {{ __('Save changes') }}
+ </gl-button>
+ <gl-button
+ v-if="propsSource.canTest"
+ :loading="isTesting"
+ :disabled="isSavingOrTesting"
+ :href="propsSource.testPath"
+ @click.prevent="onTestClick"
+ >
+ {{ __('Test settings') }}
+ </gl-button>
+
+ <gl-button class="btn-cancel" :href="propsSource.cancelPath">{{ __('Cancel') }}</gl-button>
+ </div>
</div>
</template>
diff --git a/app/assets/javascripts/integrations/edit/components/jira_issues_fields.vue b/app/assets/javascripts/integrations/edit/components/jira_issues_fields.vue
index 5a1f86718b0..1baa2b440b0 100644
--- a/app/assets/javascripts/integrations/edit/components/jira_issues_fields.vue
+++ b/app/assets/javascripts/integrations/edit/components/jira_issues_fields.vue
@@ -37,6 +37,11 @@ export default {
required: false,
default: null,
},
+ gitlabIssuesEnabled: {
+ type: Boolean,
+ required: false,
+ default: true,
+ },
upgradePlanPath: {
type: String,
required: false,
@@ -133,7 +138,7 @@ export default {
:disabled="!enableJiraIssues"
/>
</gl-form-group>
- <p>
+ <p v-if="gitlabIssuesEnabled">
<gl-sprintf
:message="
s__(
diff --git a/app/assets/javascripts/integrations/edit/components/override_dropdown.vue b/app/assets/javascripts/integrations/edit/components/override_dropdown.vue
index accfc26974c..c31dada8d2f 100644
--- a/app/assets/javascripts/integrations/edit/components/override_dropdown.vue
+++ b/app/assets/javascripts/integrations/edit/components/override_dropdown.vue
@@ -1,6 +1,8 @@
<script>
-import { GlNewDropdown, GlNewDropdownItem } from '@gitlab/ui';
+import { mapState } from 'vuex';
+import { GlDropdown, GlDropdownItem, GlLink } from '@gitlab/ui';
import { s__ } from '~/locale';
+import { defaultIntegrationLevel, overrideDropdownDescriptions } from '../constants';
const dropdownOptions = [
{
@@ -17,14 +19,20 @@ export default {
dropdownOptions,
name: 'OverrideDropdown',
components: {
- GlNewDropdown,
- GlNewDropdownItem,
+ GlDropdown,
+ GlDropdownItem,
+ GlLink,
},
props: {
inheritFromId: {
type: Number,
required: true,
},
+ learnMorePath: {
+ type: String,
+ required: false,
+ default: null,
+ },
override: {
type: Boolean,
required: true,
@@ -35,6 +43,16 @@ export default {
selected: dropdownOptions.find(x => x.value === this.override),
};
},
+ computed: {
+ ...mapState(['defaultState']),
+ description() {
+ const level = this.defaultState.integrationLevel;
+
+ return (
+ overrideDropdownDescriptions[level] || overrideDropdownDescriptions[defaultIntegrationLevel]
+ );
+ },
+ },
methods: {
onClick(option) {
this.selected = option;
@@ -48,16 +66,21 @@ export default {
<div
class="gl-display-flex gl-justify-content-space-between gl-align-items-baseline gl-py-4 gl-mt-5 gl-mb-6 gl-border-t-1 gl-border-t-solid gl-border-b-1 gl-border-b-solid gl-border-gray-100"
>
- <span>{{ s__('Integrations|Default settings are inherited from the instance level.') }}</span>
+ <span
+ >{{ description }}
+ <gl-link v-if="learnMorePath" :href="learnMorePath" target="_blank">{{
+ __('Learn more')
+ }}</gl-link>
+ </span>
<input name="service[inherit_from_id]" :value="override ? '' : inheritFromId" type="hidden" />
- <gl-new-dropdown :text="selected.text">
- <gl-new-dropdown-item
+ <gl-dropdown :text="selected.text">
+ <gl-dropdown-item
v-for="option in $options.dropdownOptions"
:key="option.value"
@click="onClick(option)"
>
{{ option.text }}
- </gl-new-dropdown-item>
- </gl-new-dropdown>
+ </gl-dropdown-item>
+ </gl-dropdown>
</div>
</template>
diff --git a/app/assets/javascripts/integrations/edit/constants.js b/app/assets/javascripts/integrations/edit/constants.js
new file mode 100644
index 00000000000..b74ae209eb7
--- /dev/null
+++ b/app/assets/javascripts/integrations/edit/constants.js
@@ -0,0 +1,17 @@
+import { s__ } from '~/locale';
+
+export const integrationLevels = {
+ GROUP: 'group',
+ INSTANCE: 'instance',
+};
+
+export const defaultIntegrationLevel = integrationLevels.INSTANCE;
+
+export const overrideDropdownDescriptions = {
+ [integrationLevels.GROUP]: s__(
+ 'Integrations|Default settings are inherited from the group level.',
+ ),
+ [integrationLevels.INSTANCE]: s__(
+ 'Integrations|Default settings are inherited from the instance level.',
+ ),
+};
diff --git a/app/assets/javascripts/integrations/edit/index.js b/app/assets/javascripts/integrations/edit/index.js
index ea5463832ce..248ee62d43a 100644
--- a/app/assets/javascripts/integrations/edit/index.js
+++ b/app/assets/javascripts/integrations/edit/index.js
@@ -19,27 +19,36 @@ function parseDatasetToProps(data) {
projectKey,
upgradePlanPath,
editProjectPath,
+ learnMorePath,
triggerEvents,
fields,
inheritFromId,
+ integrationLevel,
+ cancelPath,
+ testPath,
...booleanAttributes
} = data;
const {
showActive,
activated,
+ editable,
+ canTest,
commitEvents,
mergeRequestEvents,
enableComments,
showJiraIssuesIntegration,
enableJiraIssues,
+ gitlabIssuesEnabled,
} = parseBooleanInData(booleanAttributes);
return {
- activeToggleProps: {
- initialActivated: activated,
- },
+ initialActivated: activated,
showActive,
type,
+ cancelPath,
+ editable,
+ canTest,
+ testPath,
triggerFieldsProps: {
initialTriggerCommit: commitEvents,
initialTriggerMergeRequest: mergeRequestEvents,
@@ -50,17 +59,20 @@ function parseDatasetToProps(data) {
showJiraIssuesIntegration,
initialEnableJiraIssues: enableJiraIssues,
initialProjectKey: projectKey,
+ gitlabIssuesEnabled,
upgradePlanPath,
editProjectPath,
},
+ learnMorePath,
triggerEvents: JSON.parse(triggerEvents),
fields: JSON.parse(fields),
inheritFromId: parseInt(inheritFromId, 10),
+ integrationLevel,
id: parseInt(id, 10),
};
}
-export default (el, adminEl) => {
+export default (el, defaultEl) => {
if (!el) {
return null;
}
@@ -68,12 +80,12 @@ export default (el, adminEl) => {
const props = parseDatasetToProps(el.dataset);
const initialState = {
- adminState: null,
+ defaultState: null,
customState: props,
};
- if (adminEl) {
- initialState.adminState = Object.freeze(parseDatasetToProps(adminEl.dataset));
+ if (defaultEl) {
+ initialState.defaultState = Object.freeze(parseDatasetToProps(defaultEl.dataset));
}
return new Vue({
diff --git a/app/assets/javascripts/integrations/edit/store/actions.js b/app/assets/javascripts/integrations/edit/store/actions.js
index 3decdaab55d..199c9074ead 100644
--- a/app/assets/javascripts/integrations/edit/store/actions.js
+++ b/app/assets/javascripts/integrations/edit/store/actions.js
@@ -1,4 +1,5 @@
import * as types from './mutation_types';
-// eslint-disable-next-line import/prefer-default-export
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);
diff --git a/app/assets/javascripts/integrations/edit/store/getters.js b/app/assets/javascripts/integrations/edit/store/getters.js
index b68bd668980..4ee5f11855c 100644
--- a/app/assets/javascripts/integrations/edit/store/getters.js
+++ b/app/assets/javascripts/integrations/edit/store/getters.js
@@ -1,6 +1,8 @@
-export const isInheriting = state => (state.adminState === null ? false : !state.override);
+export const isInheriting = state => (state.defaultState === null ? false : !state.override);
+
+export const isSavingOrTesting = state => state.isSaving || state.isTesting;
export const propsSource = (state, getters) =>
- getters.isInheriting ? state.adminState : state.customState;
+ getters.isInheriting ? state.defaultState : state.customState;
export const currentKey = (state, getters) => (getters.isInheriting ? 'admin' : 'custom');
diff --git a/app/assets/javascripts/integrations/edit/store/index.js b/app/assets/javascripts/integrations/edit/store/index.js
index eea5e48780d..a8375f345c6 100644
--- a/app/assets/javascripts/integrations/edit/store/index.js
+++ b/app/assets/javascripts/integrations/edit/store/index.js
@@ -7,7 +7,6 @@ import createState from './state';
Vue.use(Vuex);
-// eslint-disable-next-line import/prefer-default-export
export const createStore = (initialState = {}) =>
new Vuex.Store({
actions,
diff --git a/app/assets/javascripts/integrations/edit/store/mutation_types.js b/app/assets/javascripts/integrations/edit/store/mutation_types.js
index 274afe3fb49..0dae8ea079e 100644
--- a/app/assets/javascripts/integrations/edit/store/mutation_types.js
+++ b/app/assets/javascripts/integrations/edit/store/mutation_types.js
@@ -1,2 +1,3 @@
-// eslint-disable-next-line import/prefer-default-export
export const SET_OVERRIDE = 'SET_OVERRIDE';
+export const SET_IS_SAVING = 'SET_IS_SAVING';
+export const SET_IS_TESTING = 'SET_IS_TESTING';
diff --git a/app/assets/javascripts/integrations/edit/store/mutations.js b/app/assets/javascripts/integrations/edit/store/mutations.js
index 8757d415197..8ac3c476f9e 100644
--- a/app/assets/javascripts/integrations/edit/store/mutations.js
+++ b/app/assets/javascripts/integrations/edit/store/mutations.js
@@ -4,4 +4,10 @@ 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;
+ },
};
diff --git a/app/assets/javascripts/integrations/edit/store/state.js b/app/assets/javascripts/integrations/edit/store/state.js
index 95c1a2be500..a9ecee6c539 100644
--- a/app/assets/javascripts/integrations/edit/store/state.js
+++ b/app/assets/javascripts/integrations/edit/store/state.js
@@ -1,9 +1,11 @@
-export default ({ adminState = null, customState = {} } = {}) => {
- const override = adminState !== null ? adminState.id !== customState.inheritFromId : false;
+export default ({ defaultState = null, customState = {} } = {}) => {
+ const override = defaultState !== null ? defaultState.id !== customState.inheritFromId : false;
return {
override,
- adminState,
+ defaultState,
customState,
+ isSaving: false,
+ isTesting: false,
};
};
diff --git a/app/assets/javascripts/integrations/integration_settings_form.js b/app/assets/javascripts/integrations/integration_settings_form.js
index 1135065b06c..1d0814125e6 100644
--- a/app/assets/javascripts/integrations/integration_settings_form.js
+++ b/app/assets/javascripts/integrations/integration_settings_form.js
@@ -1,7 +1,7 @@
import $ from 'jquery';
import axios from '../lib/utils/axios_utils';
-import { deprecatedCreateFlash as flash } from '../flash';
-import { __ } from '~/locale';
+import { __, s__ } from '~/locale';
+import toast from '~/vue_shared/plugins/global_toast';
import initForm from './edit';
import eventHub from './edit/event_hub';
@@ -10,65 +10,63 @@ export default class IntegrationSettingsForm {
this.$form = $(formSelector);
this.formActive = false;
+ this.vue = null;
+
// Form Metadata
- this.canTestService = this.$form.data('canTest');
this.testEndPoint = this.$form.data('testUrl');
-
- // Form Child Elements
- this.$submitBtn = this.$form.find('button[type="submit"]');
- this.$submitBtnLoader = this.$submitBtn.find('.js-btn-spinner');
- this.$submitBtnLabel = this.$submitBtn.find('.js-btn-label');
}
init() {
// Init Vue component
- initForm(
+ this.vue = initForm(
document.querySelector('.js-vue-integration-settings'),
- document.querySelector('.js-vue-admin-integration-settings'),
+ document.querySelector('.js-vue-default-integration-settings'),
);
eventHub.$on('toggle', active => {
this.formActive = active;
- this.handleServiceToggle();
+ this.toggleServiceState();
+ });
+ eventHub.$on('testIntegration', () => {
+ this.testIntegration();
+ });
+ eventHub.$on('saveIntegration', () => {
+ this.saveIntegration();
});
-
- // Bind Event Listeners
- this.$submitBtn.on('click', e => this.handleSettingsSave(e));
}
- handleSettingsSave(e) {
- // Check if Service is marked active, as if not marked active,
- // We can skip testing it and directly go ahead to allow form to
- // be submitted
- if (!this.formActive) {
- return;
+ saveIntegration() {
+ // Service was marked active so now we check;
+ // 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.
+ if (this.$form.get(0).checkValidity()) {
+ this.$form.submit();
+ } else {
+ eventHub.$emit('validateForm');
+ 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.get(0).checkValidity()) {
- if (this.canTestService) {
- e.preventDefault();
- // eslint-disable-next-line no-jquery/no-serialize
- this.testSettings(this.$form.serialize());
- }
+ // eslint-disable-next-line no-jquery/no-serialize
+ this.testSettings(this.$form.serialize());
} else {
- e.preventDefault();
eventHub.$emit('validateForm');
+ this.vue.$store.dispatch('setIsTesting', false);
}
}
- handleServiceToggle() {
- this.toggleServiceState();
- }
-
/**
* Change Form's validation enforcement based on service status (active/inactive)
*/
toggleServiceState() {
- this.toggleSubmitBtnLabel();
if (this.formActive) {
this.$form.removeAttr('novalidate');
} else if (!this.$form.attr('novalidate')) {
@@ -77,67 +75,23 @@ export default class IntegrationSettingsForm {
}
/**
- * Toggle Submit button label based on Integration status and ability to test service
- */
- toggleSubmitBtnLabel() {
- let btnLabel = __('Save changes');
-
- if (this.formActive && this.canTestService) {
- btnLabel = __('Test settings and save changes');
- }
-
- this.$submitBtnLabel.text(btnLabel);
- }
-
- /**
- * Toggle Submit button state based on provided boolean value of `saveTestActive`
- * When enabled, it does two things, and reverts back when disabled
- *
- * 1. It shows load spinner on submit button
- * 2. Makes submit button disabled
- */
- toggleSubmitBtnState(saveTestActive) {
- if (saveTestActive) {
- this.$submitBtn.disable();
- this.$submitBtnLoader.removeClass('hidden');
- } else {
- this.$submitBtn.enable();
- this.$submitBtnLoader.addClass('hidden');
- }
- }
-
- /**
* Test Integration config
*/
testSettings(formData) {
- this.toggleSubmitBtnState(true);
-
return axios
.put(this.testEndPoint, formData)
.then(({ data }) => {
if (data.error) {
- let flashActions;
-
- if (data.test_failed) {
- flashActions = {
- title: __('Save anyway'),
- clickHandler: e => {
- e.preventDefault();
- this.$form.submit();
- },
- };
- }
-
- flash(`${data.message} ${data.service_response}`, 'alert', document, flashActions);
+ toast(`${data.message} ${data.service_response}`);
} else {
- this.$form.submit();
+ toast(s__('Integrations|Connection successful.'));
}
-
- this.toggleSubmitBtnState(false);
})
.catch(() => {
- flash(__('Something went wrong on our end.'));
- this.toggleSubmitBtnState(false);
+ toast(__('Something went wrong on our end.'));
+ })
+ .finally(() => {
+ this.vue.$store.dispatch('setIsTesting', false);
});
}
}
diff --git a/app/assets/javascripts/issuable_bulk_update_sidebar.js b/app/assets/javascripts/issuable_bulk_update_sidebar.js
index 85c2a370ff3..7d9cefbe66a 100644
--- a/app/assets/javascripts/issuable_bulk_update_sidebar.js
+++ b/app/assets/javascripts/issuable_bulk_update_sidebar.js
@@ -7,7 +7,7 @@ import MilestoneSelect from './milestone_select';
import issueStatusSelect from './issue_status_select';
import subscriptionSelect from './subscription_select';
import LabelsSelect from './labels_select';
-import issueableEventHub from './issuables_list/eventhub';
+import issueableEventHub from './issues_list/eventhub';
const HIDDEN_CLASS = 'hidden';
const DISABLED_CONTENT_CLASS = 'disabled-content';
diff --git a/app/assets/javascripts/issuable_create/components/issuable_create_root.vue b/app/assets/javascripts/issuable_create/components/issuable_create_root.vue
new file mode 100644
index 00000000000..1ef42976032
--- /dev/null
+++ b/app/assets/javascripts/issuable_create/components/issuable_create_root.vue
@@ -0,0 +1,44 @@
+<script>
+import IssuableForm from './issuable_form.vue';
+
+export default {
+ components: {
+ IssuableForm,
+ },
+ props: {
+ descriptionPreviewPath: {
+ type: String,
+ required: true,
+ },
+ descriptionHelpPath: {
+ type: String,
+ required: true,
+ },
+ labelsFetchPath: {
+ type: String,
+ required: true,
+ },
+ labelsManagePath: {
+ type: String,
+ required: true,
+ },
+ },
+};
+</script>
+
+<template>
+ <div class="issuable-create-container">
+ <slot name="title"></slot>
+ <hr />
+ <issuable-form
+ :description-preview-path="descriptionPreviewPath"
+ :description-help-path="descriptionHelpPath"
+ :labels-fetch-path="labelsFetchPath"
+ :labels-manage-path="labelsManagePath"
+ >
+ <template #actions="issuableMeta">
+ <slot name="actions" v-bind="issuableMeta"></slot>
+ </template>
+ </issuable-form>
+ </div>
+</template>
diff --git a/app/assets/javascripts/issuable_create/components/issuable_form.vue b/app/assets/javascripts/issuable_create/components/issuable_form.vue
new file mode 100644
index 00000000000..17e51b3dbac
--- /dev/null
+++ b/app/assets/javascripts/issuable_create/components/issuable_form.vue
@@ -0,0 +1,127 @@
+<script>
+import { GlForm, GlFormInput } from '@gitlab/ui';
+import MarkdownField from '~/vue_shared/components/markdown/field.vue';
+import LabelsSelect from '~/vue_shared/components/sidebar/labels_select_vue/labels_select_root.vue';
+
+import { DropdownVariant } from '~/vue_shared/components/sidebar/labels_select_vue/constants';
+
+export default {
+ LabelSelectVariant: DropdownVariant,
+ components: {
+ GlForm,
+ GlFormInput,
+ MarkdownField,
+ LabelsSelect,
+ },
+ props: {
+ descriptionPreviewPath: {
+ type: String,
+ required: true,
+ },
+ descriptionHelpPath: {
+ type: String,
+ required: true,
+ },
+ labelsFetchPath: {
+ type: String,
+ required: true,
+ },
+ labelsManagePath: {
+ type: String,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ issuableTitle: '',
+ issuableDescription: '',
+ selectedLabels: [],
+ };
+ },
+ methods: {
+ handleUpdateSelectedLabels(labels) {
+ if (labels.length) {
+ this.selectedLabels = labels;
+ }
+ },
+ },
+};
+</script>
+
+<template>
+ <gl-form class="common-note-form gfm-form" @submit.stop.prevent>
+ <div data-testid="issuable-title" class="form-group row">
+ <label for="issuable-title" class="col-form-label col-sm-2">{{ __('Title') }}</label>
+ <div class="col-sm-10">
+ <gl-form-input
+ id="issuable-title"
+ v-model="issuableTitle"
+ :autofocus="true"
+ :placeholder="__('Title')"
+ />
+ </div>
+ </div>
+ <div data-testid="issuable-description" class="form-group row">
+ <label for="issuable-description" class="col-form-label col-sm-2">{{
+ __('Description')
+ }}</label>
+ <div class="col-sm-10">
+ <markdown-field
+ :markdown-preview-path="descriptionPreviewPath"
+ :markdown-docs-path="descriptionHelpPath"
+ :add-spacing-classes="false"
+ :show-suggest-popover="true"
+ >
+ <textarea
+ id="issuable-description"
+ ref="textarea"
+ slot="textarea"
+ v-model="issuableDescription"
+ dir="auto"
+ class="note-textarea qa-issuable-form-description rspec-issuable-form-description js-gfm-input js-autosize markdown-area"
+ :aria-label="__('Description')"
+ :placeholder="__('Write a comment or drag your files here…')"
+ ></textarea>
+ </markdown-field>
+ </div>
+ </div>
+ <div class="row">
+ <div class="col-lg-6">
+ <div data-testid="issuable-labels" class="form-group row">
+ <label for="issuable-labels" class="col-form-label col-md-2 col-lg-4">{{
+ __('Labels')
+ }}</label>
+ <div class="col-md-8 col-sm-10">
+ <div class="issuable-form-select-holder">
+ <labels-select
+ :allow-label-edit="true"
+ :allow-label-create="true"
+ :allow-multiselect="true"
+ :allow-scoped-labels="true"
+ :labels-fetch-path="labelsFetchPath"
+ :labels-manage-path="labelsManagePath"
+ :selected-labels="selectedLabels"
+ :labels-list-title="__('Select label')"
+ :footer-create-label-title="__('Create project label')"
+ :footer-manage-label-title="__('Manage project labels')"
+ :variant="$options.LabelSelectVariant.Embedded"
+ @updateSelectedLabels="handleUpdateSelectedLabels"
+ />
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ <div
+ data-testid="issuable-create-actions"
+ class="footer-block row-content-block gl-display-flex"
+ >
+ <slot
+ name="actions"
+ :issuable-title="issuableTitle"
+ :issuable-description="issuableDescription"
+ :selected-labels="selectedLabels"
+ ></slot>
+ </div>
+ </gl-form>
+</template>
diff --git a/app/assets/javascripts/issuable_form.js b/app/assets/javascripts/issuable_form.js
index 2dcf5e6a0d6..ed34e2f5623 100644
--- a/app/assets/javascripts/issuable_form.js
+++ b/app/assets/javascripts/issuable_form.js
@@ -66,6 +66,7 @@ export default class IssuableForm {
gl.GfmAutoComplete && gl.GfmAutoComplete.dataSources,
).setup();
this.usersSelect = new UsersSelect();
+ this.reviewersSelect = new UsersSelect(undefined, '.js-reviewer-search');
this.zenMode = new ZenMode();
this.titleField = this.form.find('input[name*="[title]"]');
diff --git a/app/assets/javascripts/issuable_list/components/issuable_item.vue b/app/assets/javascripts/issuable_list/components/issuable_item.vue
new file mode 100644
index 00000000000..d8cb1ab07cd
--- /dev/null
+++ b/app/assets/javascripts/issuable_list/components/issuable_item.vue
@@ -0,0 +1,140 @@
+<script>
+import { GlLink, GlLabel, GlTooltipDirective } from '@gitlab/ui';
+
+import { __, sprintf } from '~/locale';
+import { getTimeago } from '~/lib/utils/datetime_utility';
+import { isScopedLabel } from '~/lib/utils/common_utils';
+import timeagoMixin from '~/vue_shared/mixins/timeago';
+
+export default {
+ components: {
+ GlLink,
+ GlLabel,
+ },
+ directives: {
+ GlTooltip: GlTooltipDirective,
+ },
+ mixins: [timeagoMixin],
+ props: {
+ issuableSymbol: {
+ type: String,
+ required: true,
+ },
+ issuable: {
+ type: Object,
+ required: true,
+ },
+ },
+ computed: {
+ author() {
+ return this.issuable.author;
+ },
+ authorId() {
+ const id = parseInt(this.author.id, 10);
+
+ if (Number.isNaN(id)) {
+ return this.author.id.includes('gid')
+ ? this.author.id.split('gid://gitlab/User/').pop()
+ : '';
+ }
+
+ return id;
+ },
+ labels() {
+ return this.issuable.labels?.nodes || this.issuable.labels || [];
+ },
+ createdAt() {
+ return sprintf(__('created %{timeAgo}'), {
+ timeAgo: getTimeago().format(this.issuable.createdAt),
+ });
+ },
+ updatedAt() {
+ return sprintf(__('updated %{timeAgo}'), {
+ timeAgo: getTimeago().format(this.issuable.updatedAt),
+ });
+ },
+ },
+ methods: {
+ scopedLabel(label) {
+ return isScopedLabel(label);
+ },
+ /**
+ * This is needed as an independent method since
+ * when user changes current page, `$refs.authorLink`
+ * will be null until next page results are loaded & rendered.
+ */
+ getAuthorPopoverTarget() {
+ if (this.$refs.authorLink) {
+ return this.$refs.authorLink.$el;
+ }
+ return '';
+ },
+ },
+};
+</script>
+
+<template>
+ <li class="issue">
+ <div class="issue-box">
+ <div class="issuable-info-container">
+ <div class="issuable-main-info">
+ <div data-testid="issuable-title" class="issue-title title">
+ <span class="issue-title-text" dir="auto">
+ <gl-link :href="issuable.webUrl">{{ issuable.title }}</gl-link>
+ </span>
+ </div>
+ <div class="issuable-info">
+ <span data-testid="issuable-reference" class="issuable-reference"
+ >{{ issuableSymbol }}{{ issuable.iid }}</span
+ >
+ <span class="issuable-authored d-none d-sm-inline-block">
+ &middot;
+ <span
+ v-gl-tooltip:tooltipcontainer.bottom
+ data-testid="issuable-created-at"
+ :title="tooltipTitle(issuable.createdAt)"
+ >{{ createdAt }}</span
+ >
+ {{ __('by') }}
+ <gl-link
+ :data-user-id="authorId"
+ :data-username="author.username"
+ :data-name="author.name"
+ :data-avatar-url="author.avatarUrl"
+ :href="author.webUrl"
+ data-testid="issuable-author"
+ class="author-link js-user-link"
+ >
+ <span class="author">{{ author.name }}</span>
+ </gl-link>
+ </span>
+ &nbsp;
+ <gl-label
+ v-for="(label, index) in labels"
+ :key="index"
+ :background-color="label.color"
+ :title="label.title"
+ :description="label.description"
+ :scoped="scopedLabel(label)"
+ :class="{ 'gl-ml-2': index }"
+ size="sm"
+ />
+ </div>
+ </div>
+ <div class="issuable-meta">
+ <div
+ data-testid="issuable-updated-at"
+ class="float-right issuable-updated-at d-none d-sm-inline-block"
+ >
+ <span
+ v-gl-tooltip:tooltipcontainer.bottom
+ :title="tooltipTitle(issuable.updatedAt)"
+ class="issuable-updated-at"
+ >{{ updatedAt }}</span
+ >
+ </div>
+ </div>
+ </div>
+ </div>
+ </li>
+</template>
diff --git a/app/assets/javascripts/issuable_list/components/issuable_list_root.vue b/app/assets/javascripts/issuable_list/components/issuable_list_root.vue
new file mode 100644
index 00000000000..7535203dea1
--- /dev/null
+++ b/app/assets/javascripts/issuable_list/components/issuable_list_root.vue
@@ -0,0 +1,153 @@
+<script>
+import { GlLoadingIcon, GlPagination } from '@gitlab/ui';
+
+import FilteredSearchBar from '~/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue';
+
+import IssuableTabs from './issuable_tabs.vue';
+import IssuableItem from './issuable_item.vue';
+
+export default {
+ components: {
+ GlLoadingIcon,
+ IssuableTabs,
+ FilteredSearchBar,
+ IssuableItem,
+ GlPagination,
+ },
+ props: {
+ namespace: {
+ type: String,
+ required: true,
+ },
+ recentSearchesStorageKey: {
+ type: String,
+ required: true,
+ },
+ searchInputPlaceholder: {
+ type: String,
+ required: true,
+ },
+ searchTokens: {
+ type: Array,
+ required: true,
+ },
+ sortOptions: {
+ type: Array,
+ required: true,
+ },
+ initialFilterValue: {
+ type: Array,
+ required: false,
+ default: () => [],
+ },
+ initialSortBy: {
+ type: String,
+ required: false,
+ default: 'created_desc',
+ },
+ issuables: {
+ type: Array,
+ required: true,
+ },
+ tabs: {
+ type: Array,
+ required: true,
+ },
+ tabCounts: {
+ type: Object,
+ required: true,
+ },
+ currentTab: {
+ type: String,
+ required: true,
+ },
+ issuableSymbol: {
+ type: String,
+ required: false,
+ default: '#',
+ },
+ issuablesLoading: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ showPaginationControls: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ defaultPageSize: {
+ type: Number,
+ required: false,
+ default: 20,
+ },
+ currentPage: {
+ type: Number,
+ required: false,
+ default: 1,
+ },
+ previousPage: {
+ type: Number,
+ required: false,
+ default: 0,
+ },
+ nextPage: {
+ type: Number,
+ required: false,
+ default: 2,
+ },
+ },
+};
+</script>
+
+<template>
+ <div class="issuable-list-container">
+ <issuable-tabs
+ :tabs="tabs"
+ :tab-counts="tabCounts"
+ :current-tab="currentTab"
+ @click="$emit('click-tab', $event)"
+ >
+ <template #nav-actions>
+ <slot name="nav-actions"></slot>
+ </template>
+ </issuable-tabs>
+ <filtered-search-bar
+ :namespace="namespace"
+ :recent-searches-storage-key="recentSearchesStorageKey"
+ :search-input-placeholder="searchInputPlaceholder"
+ :tokens="searchTokens"
+ :sort-options="sortOptions"
+ :initial-filter-value="initialFilterValue"
+ :initial-sort-by="initialSortBy"
+ class="gl-flex-grow-1 row-content-block"
+ @onFilter="$emit('filter', $event)"
+ @onSort="$emit('sort', $event)"
+ />
+ <div class="issuables-holder">
+ <gl-loading-icon v-if="issuablesLoading" size="md" class="gl-mt-5" />
+ <ul
+ v-if="!issuablesLoading && issuables.length"
+ class="content-list issuable-list issues-list"
+ >
+ <issuable-item
+ v-for="issuable in issuables"
+ :key="issuable.id"
+ :issuable-symbol="issuableSymbol"
+ :issuable="issuable"
+ />
+ </ul>
+ <slot v-if="!issuablesLoading && !issuables.length" name="empty-state"></slot>
+ <gl-pagination
+ v-if="showPaginationControls"
+ :per-page="defaultPageSize"
+ :value="currentPage"
+ :prev-page="previousPage"
+ :next-page="nextPage"
+ align="center"
+ class="gl-pagination gl-mt-3"
+ @input="$emit('page-change', $event)"
+ />
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/issuable_list/components/issuable_tabs.vue b/app/assets/javascripts/issuable_list/components/issuable_tabs.vue
new file mode 100644
index 00000000000..df544ce69e7
--- /dev/null
+++ b/app/assets/javascripts/issuable_list/components/issuable_tabs.vue
@@ -0,0 +1,53 @@
+<script>
+import { GlTabs, GlTab, GlBadge } from '@gitlab/ui';
+
+export default {
+ components: {
+ GlTabs,
+ GlTab,
+ GlBadge,
+ },
+ props: {
+ tabs: {
+ type: Array,
+ required: true,
+ },
+ tabCounts: {
+ type: Object,
+ required: true,
+ },
+ currentTab: {
+ type: String,
+ required: true,
+ },
+ },
+ methods: {
+ isTabActive(tabName) {
+ return tabName === this.currentTab;
+ },
+ },
+};
+</script>
+
+<template>
+ <div class="top-area">
+ <gl-tabs class="nav-links mobile-separator issuable-state-filters">
+ <gl-tab
+ v-for="tab in tabs"
+ :key="tab.id"
+ :active="isTabActive(tab.name)"
+ @click="$emit('click', tab.name)"
+ >
+ <template #title>
+ <span :title="tab.titleTooltip">{{ tab.title }}</span>
+ <gl-badge variant="neutral" size="sm" class="gl-px-2 gl-py-1!">{{
+ tabCounts[tab.name]
+ }}</gl-badge>
+ </template>
+ </gl-tab>
+ </gl-tabs>
+ <div class="nav-controls">
+ <slot name="nav-actions"></slot>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/issuable_suggestions/components/app.vue b/app/assets/javascripts/issuable_suggestions/components/app.vue
index 810ca7ac1bd..8a9a880e7ee 100644
--- a/app/assets/javascripts/issuable_suggestions/components/app.vue
+++ b/app/assets/javascripts/issuable_suggestions/components/app.vue
@@ -1,14 +1,13 @@
<script>
-import { GlTooltipDirective } from '@gitlab/ui';
+import { GlTooltipDirective, GlIcon } from '@gitlab/ui';
import { __ } from '~/locale';
-import Icon from '~/vue_shared/components/icon.vue';
import Suggestion from './item.vue';
import query from '../queries/issues.query.graphql';
export default {
components: {
Suggestion,
- Icon,
+ GlIcon,
},
directives: {
GlTooltip: GlTooltipDirective,
@@ -70,7 +69,7 @@ export default {
<div v-show="showSuggestions" class="form-group row issuable-suggestions">
<div v-once class="col-form-label col-sm-2 pt-0">
{{ __('Similar issues') }}
- <icon
+ <gl-icon
v-gl-tooltip.bottom
:title="$options.helpText"
:aria-label="$options.helpText"
diff --git a/app/assets/javascripts/issuable_suggestions/components/item.vue b/app/assets/javascripts/issuable_suggestions/components/item.vue
index dfadb9d2b24..6e265b1bf42 100644
--- a/app/assets/javascripts/issuable_suggestions/components/item.vue
+++ b/app/assets/javascripts/issuable_suggestions/components/item.vue
@@ -1,9 +1,8 @@
<script>
/* eslint-disable @gitlab/vue-require-i18n-strings */
import { uniqueId } from 'lodash';
-import { GlLink, GlTooltip, GlTooltipDirective } from '@gitlab/ui';
+import { GlLink, GlTooltip, GlTooltipDirective, GlIcon } from '@gitlab/ui';
import { __ } from '~/locale';
-import Icon from '~/vue_shared/components/icon.vue';
import UserAvatarImage from '~/vue_shared/components/user_avatar/user_avatar_image.vue';
import TimeagoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
import timeago from '~/vue_shared/mixins/timeago';
@@ -12,7 +11,7 @@ export default {
components: {
GlTooltip,
GlLink,
- Icon,
+ GlIcon,
UserAvatarImage,
TimeagoTooltip,
},
@@ -68,7 +67,7 @@ export default {
<template>
<div class="suggestion-item">
<div class="d-flex align-items-center">
- <icon
+ <gl-icon
v-if="suggestion.confidential"
v-gl-tooltip.bottom
:title="__('Confidential')"
@@ -84,7 +83,7 @@ export default {
</gl-link>
</div>
<div class="text-secondary suggestion-footer">
- <icon
+ <gl-icon
ref="state"
:name="stateIcon"
:class="{
@@ -134,7 +133,7 @@ export default {
:title="tooltipTitle"
class="suggestion-help-hover gl-ml-3 text-tertiary"
>
- <icon :name="icon" /> {{ count }}
+ <gl-icon :name="icon" /> {{ count }}
</span>
</span>
</div>
diff --git a/app/assets/javascripts/issuables_list/service_desk_helper.js b/app/assets/javascripts/issuables_list/service_desk_helper.js
deleted file mode 100644
index 4b4a38c2205..00000000000
--- a/app/assets/javascripts/issuables_list/service_desk_helper.js
+++ /dev/null
@@ -1,55 +0,0 @@
-import { __ } from '~/locale';
-
-/**
- * Returns the attributes used for gl-empty-state in the Service Desk issues list.
- */
-// eslint-disable-next-line import/prefer-default-export
-export function emptyStateHelper(emptyStateMeta) {
- const { isServiceDeskSupported, svgPath, serviceDeskHelpPage } = emptyStateMeta;
-
- if (isServiceDeskSupported) {
- const title = __(
- 'Use Service Desk to connect with your users (e.g. to offer customer support) through email right inside GitLab',
- );
- const commonMessage = __(
- 'Those emails automatically become issues (with the comments becoming the email conversation) listed here.',
- );
- const commonDescription = `
- <span>${commonMessage}</span>
- <a href="${serviceDeskHelpPage}">${__('Read more')}</a>`;
-
- if (emptyStateMeta.canEditProjectSettings && emptyStateMeta.isServiceDeskEnabled) {
- return {
- title,
- svgPath,
- description: `<p>${__('Have your users email')} <code>${
- emptyStateMeta.serviceDeskAddress
- }</code></p> ${commonDescription}`,
- };
- }
-
- if (emptyStateMeta.canEditProjectSettings && !emptyStateMeta.isServiceDeskEnabled) {
- return {
- title,
- svgPath,
- description: commonDescription,
- primaryLink: emptyStateMeta.editProjectPage,
- primaryText: __('Turn on Service Desk'),
- };
- }
-
- return {
- title,
- svgPath,
- description: commonDescription,
- };
- }
-
- return {
- title: __('Service Desk is enabled but not yet active'),
- svgPath,
- description: __('You must set up incoming email before it becomes active.'),
- primaryLink: emptyStateMeta.incomingEmailHelpPage,
- primaryText: __('More information'),
- };
-}
diff --git a/app/assets/javascripts/issue.js b/app/assets/javascripts/issue.js
index f1b37525a6d..0a0cfe918af 100644
--- a/app/assets/javascripts/issue.js
+++ b/app/assets/javascripts/issue.js
@@ -100,6 +100,13 @@ export default class Issue {
initIssueBtnEventListeners() {
const issueFailMessage = __('Unable to update this issue at this time.');
+ $('.report-abuse-link').on('click', e => {
+ // this is needed because of the implementation of
+ // the dropdown toggle and Report Abuse needing to be
+ // linked to another page.
+ e.stopPropagation();
+ });
+
// NOTE: data attribute seems unnecessary but is actually necessary
return $('.js-issuable-buttons[data-action="close-reopen"]').on(
'click',
@@ -173,11 +180,15 @@ export default class Issue {
}
initIssueWarningBtnEventListener() {
- return $(document).on('click', '.js-close-blocked-issue-warning button.btn-secondary', e => {
- e.preventDefault();
- e.stopImmediatePropagation();
- this.toggleWarningAndCloseButton();
- });
+ return $(document).on(
+ 'click',
+ '.js-close-blocked-issue-warning .js-cancel-blocked-issue-warning',
+ e => {
+ e.preventDefault();
+ e.stopImmediatePropagation();
+ this.toggleWarningAndCloseButton();
+ },
+ );
}
initIssueMovedFromServiceDeskDismissHandler() {
diff --git a/app/assets/javascripts/issue_show/components/app.vue b/app/assets/javascripts/issue_show/components/app.vue
index 992d87a969f..22db0f1cfc1 100644
--- a/app/assets/javascripts/issue_show/components/app.vue
+++ b/app/assets/javascripts/issue_show/components/app.vue
@@ -20,7 +20,6 @@ export default {
components: {
GlIcon,
GlIntersectionObserver,
- descriptionComponent,
titleComponent,
editedComponent,
formComponent,
@@ -152,6 +151,18 @@ export default {
required: false,
default: 0,
},
+ descriptionComponent: {
+ type: Object,
+ required: false,
+ default: () => {
+ return descriptionComponent;
+ },
+ },
+ showTitleBorder: {
+ type: Boolean,
+ required: false,
+ default: true,
+ },
},
data() {
const store = new Store({
@@ -209,6 +220,11 @@ export default {
isOpenStatus() {
return this.issuableStatus === IssuableStatus.Open;
},
+ pinnedLinkClasses() {
+ return this.showTitleBorder
+ ? 'gl-border-b-1 gl-border-b-gray-100 gl-border-b-solid gl-mb-6'
+ : '';
+ },
statusIcon() {
return this.isOpenStatus ? 'issue-open-m' : 'mobile-issue-close';
},
@@ -231,7 +247,7 @@ export default {
});
if (!Visibility.hidden()) {
- this.poll.makeRequest();
+ this.poll.makeDelayedRequest(2000);
}
Visibility.change(() => {
@@ -447,10 +463,11 @@ export default {
<pinned-links
:zoom-meeting-url="zoomMeetingUrl"
:published-incident-url="publishedIncidentUrl"
+ :class="pinnedLinkClasses"
/>
- <description-component
- v-if="state.descriptionHtml"
+ <component
+ :is="descriptionComponent"
:can-update="canUpdate"
:description-html="state.descriptionHtml"
:description-text="state.descriptionText"
diff --git a/app/assets/javascripts/issue_show/components/description.vue b/app/assets/javascripts/issue_show/components/description.vue
index abb63f606ae..2a6468c783b 100644
--- a/app/assets/javascripts/issue_show/components/description.vue
+++ b/app/assets/javascripts/issue_show/components/description.vue
@@ -1,5 +1,6 @@
<script>
import $ from 'jquery';
+import { GlSafeHtmlDirective as SafeHtml } from '@gitlab/ui';
import { s__, sprintf } from '~/locale';
import { deprecatedCreateFlash as createFlash } from '~/flash';
import animateMixin from '../mixins/animate';
@@ -7,6 +8,10 @@ import TaskList from '../../task_list';
import recaptchaModalImplementor from '../../vue_shared/mixins/recaptcha_modal_implementor';
export default {
+ directives: {
+ SafeHtml,
+ },
+
mixins: [animateMixin, recaptchaModalImplementor],
props: {
@@ -20,7 +25,8 @@ export default {
},
descriptionText: {
type: String,
- required: true,
+ required: false,
+ default: '',
},
taskStatus: {
type: String,
@@ -47,11 +53,16 @@ export default {
return {
preAnimation: false,
pulseAnimation: false,
+ initialUpdate: true,
};
},
watch: {
- descriptionHtml() {
- this.animateChange();
+ descriptionHtml(newDescription, oldDescription) {
+ if (!this.initialUpdate && newDescription !== oldDescription) {
+ this.animateChange();
+ } else {
+ this.initialUpdate = false;
+ }
this.$nextTick(() => {
this.renderGFM();
@@ -136,12 +147,12 @@ export default {
>
<div
ref="gfm-content"
+ v-safe-html="descriptionHtml"
:class="{
'issue-realtime-pre-pulse': preAnimation,
'issue-realtime-trigger-pulse': pulseAnimation,
}"
class="md"
- v-html="descriptionHtml"
></div>
<textarea
v-if="descriptionText"
diff --git a/app/assets/javascripts/issue_show/components/edit_actions.vue b/app/assets/javascripts/issue_show/components/edit_actions.vue
index 4ee44e50d2f..14ada5adcf6 100644
--- a/app/assets/javascripts/issue_show/components/edit_actions.vue
+++ b/app/assets/javascripts/issue_show/components/edit_actions.vue
@@ -1,5 +1,5 @@
<script>
-/* eslint-disable @gitlab/vue-require-i18n-strings */
+import { GlButton } from '@gitlab/ui';
import { __, sprintf } from '~/locale';
import updateMixin from '../mixins/update';
import eventHub from '../event_hub';
@@ -10,6 +10,9 @@ const issuableTypes = {
};
export default {
+ components: {
+ GlButton,
+ },
mixins: [updateMixin],
props: {
canDestroy: {
@@ -64,28 +67,30 @@ export default {
<template>
<div class="gl-mt-3 gl-mb-3 clearfix">
- <button
- :class="{ disabled: formState.updateLoading || !isSubmitEnabled }"
+ <gl-button
+ :loading="formState.updateLoading"
:disabled="formState.updateLoading || !isSubmitEnabled"
- class="btn btn-success float-left qa-save-button"
+ category="primary"
+ variant="success"
+ class="float-left qa-save-button"
type="submit"
@click.prevent="updateIssuable"
>
- Save changes
- <i v-if="formState.updateLoading" class="fa fa-spinner fa-spin" aria-hidden="true"> </i>
- </button>
- <button class="btn btn-default float-right" type="button" @click="closeForm">
+ {{ __('Save changes') }}
+ </gl-button>
+ <gl-button class="float-right" @click="closeForm">
{{ __('Cancel') }}
- </button>
- <button
+ </gl-button>
+ <gl-button
v-if="shouldShowDeleteButton"
- :class="{ disabled: deleteLoading }"
+ :loading="deleteLoading"
:disabled="deleteLoading"
- class="btn btn-danger float-right gl-mr-3 qa-delete-button"
- type="button"
+ category="primary"
+ variant="danger"
+ class="float-right gl-mr-3 qa-delete-button"
@click="deleteIssuable"
>
- Delete <i v-if="deleteLoading" class="fa fa-spinner fa-spin" aria-hidden="true"> </i>
- </button>
+ {{ __('Delete') }}
+ </gl-button>
</div>
</template>
diff --git a/app/assets/javascripts/issue_show/components/fields/description_template.vue b/app/assets/javascripts/issue_show/components/fields/description_template.vue
index 6d8a9950b6d..e1b308c6f57 100644
--- a/app/assets/javascripts/issue_show/components/fields/description_template.vue
+++ b/app/assets/javascripts/issue_show/components/fields/description_template.vue
@@ -1,9 +1,13 @@
<script>
/* eslint-disable @gitlab/vue-require-i18n-strings */
import $ from 'jquery';
+import { GlIcon } from '@gitlab/ui';
import IssuableTemplateSelectors from '../../../templates/issuable_template_selectors';
export default {
+ components: {
+ GlIcon,
+ },
props: {
formState: {
type: Object,
@@ -61,14 +65,14 @@ export default {
<i aria-hidden="true" class="fa fa-chevron-down"> </i>
</button>
<div class="dropdown-menu dropdown-select">
- <div class="dropdown-title">
- Choose a template
+ <div class="dropdown-title gl-display-flex gl-justify-content-center">
+ <span class="gl-ml-auto">Choose a template</span>
<button
- class="dropdown-title-button dropdown-menu-close"
+ class="dropdown-title-button dropdown-menu-close gl-ml-auto"
:aria-label="__('Close')"
type="button"
>
- <i aria-hidden="true" class="fa fa-times dropdown-menu-close-icon"> </i>
+ <gl-icon name="close" class="dropdown-menu-close-icon" :aria-hidden="true" />
</button>
</div>
<div class="dropdown-input">
@@ -79,12 +83,11 @@ export default {
autocomplete="off"
/>
<i aria-hidden="true" class="fa fa-search dropdown-input-search"> </i>
- <i
- role="button"
+ <gl-icon
+ name="close"
+ class="dropdown-input-clear js-dropdown-input-clear"
:aria-label="__('Clear templates search input')"
- class="fa fa-times dropdown-input-clear js-dropdown-input-clear"
- >
- </i>
+ />
</div>
<div class="dropdown-content"></div>
<div class="dropdown-footer">
diff --git a/app/assets/javascripts/issue_show/components/incidents/graphql/queries/get_alert.graphql b/app/assets/javascripts/issue_show/components/incidents/graphql/queries/get_alert.graphql
new file mode 100644
index 00000000000..00ddc80432d
--- /dev/null
+++ b/app/assets/javascripts/issue_show/components/incidents/graphql/queries/get_alert.graphql
@@ -0,0 +1,20 @@
+query getAlert($iid: String!, $fullPath: ID!) {
+ project(fullPath: $fullPath) {
+ issue(iid: $iid) {
+ alertManagementAlert {
+ iid
+ title
+ detailsUrl
+ severity
+ status
+ startedAt
+ eventCount
+ monitoringTool
+ service
+ description
+ endedAt
+ details
+ }
+ }
+ }
+}
diff --git a/app/assets/javascripts/issue_show/components/incidents/highlight_bar.vue b/app/assets/javascripts/issue_show/components/incidents/highlight_bar.vue
new file mode 100644
index 00000000000..a47fe4c84cf
--- /dev/null
+++ b/app/assets/javascripts/issue_show/components/incidents/highlight_bar.vue
@@ -0,0 +1,42 @@
+<script>
+import { GlLink } from '@gitlab/ui';
+import { formatDate } from '~/lib/utils/datetime_utility';
+
+export default {
+ components: {
+ GlLink,
+ },
+ props: {
+ alert: {
+ type: Object,
+ required: true,
+ },
+ },
+ computed: {
+ startTime() {
+ return formatDate(this.alert.startedAt, 'yyyy-mm-dd Z');
+ },
+ },
+};
+</script>
+
+<template>
+ <div
+ class="gl-border-solid gl-border-1 gl-border-gray-100 gl-p-5 gl-mb-3 gl-rounded-base gl-display-flex gl-justify-content-space-between"
+ >
+ <div class="text-truncate gl-pr-3">
+ <span class="gl-font-weight-bold">{{ s__('HighlightBar|Original alert:') }}</span>
+ <gl-link :href="alert.detailsUrl">{{ alert.title }}</gl-link>
+ </div>
+
+ <div class="gl-pr-3 gl-white-space-nowrap">
+ <span class="gl-font-weight-bold">{{ s__('HighlightBar|Alert start time:') }}</span>
+ {{ startTime }}
+ </div>
+
+ <div class="gl-white-space-nowrap">
+ <span class="gl-font-weight-bold">{{ s__('HighlightBar|Alert events:') }}</span>
+ <span>{{ alert.eventCount }}</span>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/issue_show/components/incidents/incident_tabs.vue b/app/assets/javascripts/issue_show/components/incidents/incident_tabs.vue
new file mode 100644
index 00000000000..4104ddbf06f
--- /dev/null
+++ b/app/assets/javascripts/issue_show/components/incidents/incident_tabs.vue
@@ -0,0 +1,71 @@
+<script>
+import { GlTab, GlTabs } from '@gitlab/ui';
+import DescriptionComponent from '../description.vue';
+import HighlightBar from './highlight_bar.vue';
+import createFlash from '~/flash';
+import { s__ } from '~/locale';
+import AlertDetailsTable from '~/vue_shared/components/alert_details_table.vue';
+
+import getAlert from './graphql/queries/get_alert.graphql';
+
+export default {
+ components: {
+ AlertDetailsTable,
+ DescriptionComponent,
+ GlTab,
+ GlTabs,
+ HighlightBar,
+ },
+ inject: ['fullPath', 'iid'],
+ apollo: {
+ alert: {
+ query: getAlert,
+ variables() {
+ return {
+ fullPath: this.fullPath,
+ iid: this.iid,
+ };
+ },
+ update(data) {
+ return data?.project?.issue?.alertManagementAlert;
+ },
+ error() {
+ createFlash({
+ message: s__('Incident|There was an issue loading alert data. Please try again.'),
+ });
+ },
+ },
+ },
+ data() {
+ return {
+ alert: null,
+ };
+ },
+ computed: {
+ loading() {
+ return this.$apollo.queries.alert.loading;
+ },
+ alertTableFields() {
+ if (this.alert) {
+ const { detailsUrl, __typename, ...restDetails } = this.alert;
+ return restDetails;
+ }
+ return null;
+ },
+ },
+};
+</script>
+
+<template>
+ <div>
+ <gl-tabs content-class="gl-reset-line-height" class="gl-mt-n3" data-testid="incident-tabs">
+ <gl-tab :title="s__('Incident|Summary')">
+ <highlight-bar v-if="alert" :alert="alert" />
+ <description-component v-bind="$attrs" />
+ </gl-tab>
+ <gl-tab v-if="alert" class="alert-management-details" :title="s__('Incident|Alert details')">
+ <alert-details-table :alert="alertTableFields" :loading="loading" />
+ </gl-tab>
+ </gl-tabs>
+ </div>
+</template>
diff --git a/app/assets/javascripts/issue_show/components/locked_warning.vue b/app/assets/javascripts/issue_show/components/locked_warning.vue
index 19c7a11d87b..96f5a7c88e0 100644
--- a/app/assets/javascripts/issue_show/components/locked_warning.vue
+++ b/app/assets/javascripts/issue_show/components/locked_warning.vue
@@ -1,4 +1,5 @@
<script>
+/* eslint-disable vue/no-v-html */
import { __, sprintf } from '~/locale';
export default {
diff --git a/app/assets/javascripts/issue_show/components/pinned_links.vue b/app/assets/javascripts/issue_show/components/pinned_links.vue
index a877aa2ac96..d38189307bd 100644
--- a/app/assets/javascripts/issue_show/components/pinned_links.vue
+++ b/app/assets/javascripts/issue_show/components/pinned_links.vue
@@ -20,20 +20,25 @@ export default {
},
computed: {
pinnedLinks() {
- return [
- {
+ const links = [];
+ if (this.publishedIncidentUrl) {
+ links.push({
id: 'publishedIncidentUrl',
url: this.publishedIncidentUrl,
text: STATUS_PAGE_PUBLISHED,
icon: 'tanuki',
- },
- {
+ });
+ }
+ if (this.zoomMeetingUrl) {
+ links.push({
id: 'zoomMeetingUrl',
url: this.zoomMeetingUrl,
text: JOIN_ZOOM_MEETING,
icon: 'brand-zoom',
- },
- ];
+ });
+ }
+
+ return links;
},
},
methods: {
@@ -45,7 +50,7 @@ export default {
</script>
<template>
- <div class="border-bottom gl-mb-6 gl-display-flex gl-justify-content-start">
+ <div v-if="pinnedLinks && pinnedLinks.length" class="gl-display-flex gl-justify-content-start">
<template v-for="(link, i) in pinnedLinks">
<div v-if="link.url" :key="link.id" :class="{ 'gl-pr-3': needsPaddingClass(i) }">
<gl-button
diff --git a/app/assets/javascripts/issue_show/components/title.vue b/app/assets/javascripts/issue_show/components/title.vue
index 1e1dce5f4fc..b03a91716fe 100644
--- a/app/assets/javascripts/issue_show/components/title.vue
+++ b/app/assets/javascripts/issue_show/components/title.vue
@@ -1,12 +1,15 @@
<script>
+import { GlButton, GlTooltipDirective, GlSafeHtmlDirective as SafeHtml } from '@gitlab/ui';
import animateMixin from '../mixins/animate';
import eventHub from '../event_hub';
-import tooltip from '../../vue_shared/directives/tooltip';
-import { spriteIcon } from '../../lib/utils/common_utils';
export default {
+ components: {
+ GlButton,
+ },
directives: {
- tooltip,
+ GlTooltip: GlTooltipDirective,
+ SafeHtml,
},
mixins: [animateMixin],
props: {
@@ -40,11 +43,6 @@ export default {
titleEl: document.querySelector('title'),
};
},
- computed: {
- pencilIcon() {
- return spriteIcon('pencil', 'link-highlight');
- },
- },
watch: {
titleHtml() {
this.setPageTitle();
@@ -67,25 +65,21 @@ export default {
<template>
<div class="title-container">
<h2
+ v-safe-html="titleHtml"
:class="{
'issue-realtime-pre-pulse': preAnimation,
'issue-realtime-trigger-pulse': pulseAnimation,
}"
class="title qa-title"
dir="auto"
- v-html="titleHtml"
></h2>
- <button
+ <gl-button
v-if="showInlineEditButton && canUpdate"
- v-tooltip
- type="button"
- class="btn btn-default btn-edit btn-svg js-issuable-edit
- qa-edit-button"
+ v-gl-tooltip.bottom
+ icon="pencil"
+ class="btn-edit js-issuable-edit qa-edit-button"
title="Edit title and description"
- data-placement="bottom"
- data-container="body"
@click="edit"
- v-html="pencilIcon"
- ></button>
+ />
</div>
</template>
diff --git a/app/assets/javascripts/issue_show/incident.js b/app/assets/javascripts/issue_show/incident.js
new file mode 100644
index 00000000000..a34e75ee64a
--- /dev/null
+++ b/app/assets/javascripts/issue_show/incident.js
@@ -0,0 +1,36 @@
+import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+import createDefaultClient from '~/lib/graphql';
+import issuableApp from './components/app.vue';
+import incidentTabs from './components/incidents/incident_tabs.vue';
+
+Vue.use(VueApollo);
+
+export default function initIssuableApp(issuableData = {}) {
+ const apolloProvider = new VueApollo({
+ defaultClient: createDefaultClient(),
+ });
+
+ const { projectNamespace, projectPath, iid } = issuableData;
+
+ return new Vue({
+ el: document.getElementById('js-issuable-app'),
+ apolloProvider,
+ components: {
+ issuableApp,
+ },
+ provide: {
+ fullPath: `${projectNamespace}/${projectPath}`,
+ iid,
+ },
+ render(createElement) {
+ return createElement('issuable-app', {
+ props: {
+ ...issuableData,
+ descriptionComponent: incidentTabs,
+ showTitleBorder: false,
+ },
+ });
+ },
+ });
+}
diff --git a/app/assets/javascripts/issue_show/index.js b/app/assets/javascripts/issue_show/issue.js
index e170d338408..f9f61d5aa64 100644
--- a/app/assets/javascripts/issue_show/index.js
+++ b/app/assets/javascripts/issue_show/issue.js
@@ -1,8 +1,7 @@
import Vue from 'vue';
import issuableApp from './components/app.vue';
-import { parseIssuableData } from './utils/parse_data';
-export default function initIssueableApp() {
+export default function initIssuableApp(issuableData) {
return new Vue({
el: document.getElementById('js-issuable-app'),
components: {
@@ -10,7 +9,7 @@ export default function initIssueableApp() {
},
render(createElement) {
return createElement('issuable-app', {
- props: parseIssuableData(),
+ props: issuableData,
});
},
});
diff --git a/app/assets/javascripts/issue_show/stores/index.js b/app/assets/javascripts/issue_show/stores/index.js
index 0cd094243b9..c6f7e892f9b 100644
--- a/app/assets/javascripts/issue_show/stores/index.js
+++ b/app/assets/javascripts/issue_show/stores/index.js
@@ -1,3 +1,4 @@
+import { sanitize } from 'dompurify';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import updateDescription from '../utils/update_description';
@@ -27,8 +28,8 @@ export default class Store {
const details =
descriptionSection != null && descriptionSection.getElementsByTagName('details');
- this.state.descriptionHtml = updateDescription(data.description, details);
- this.state.titleHtml = data.title;
+ this.state.descriptionHtml = updateDescription(sanitize(data.description), details);
+ this.state.titleHtml = sanitize(data.title);
this.state.lock_version = data.lock_version;
}
diff --git a/app/assets/javascripts/issue_show/utils/parse_data.js b/app/assets/javascripts/issue_show/utils/parse_data.js
index 8cd1c1b0e56..a62a5167961 100644
--- a/app/assets/javascripts/issue_show/utils/parse_data.js
+++ b/app/assets/javascripts/issue_show/utils/parse_data.js
@@ -1,10 +1,22 @@
import { sanitize } from 'dompurify';
+// We currently load + parse the data from the issue app and related merge request
+let cachedParsedData;
+
export const parseIssuableData = () => {
try {
+ if (cachedParsedData) return cachedParsedData;
+
const initialDataEl = document.getElementById('js-issuable-app-initial-data');
- return JSON.parse(sanitize(initialDataEl.textContent).replace(/&quot;/g, '"'));
+ const parsedData = JSON.parse(initialDataEl.textContent.replace(/&quot;/g, '"'));
+
+ parsedData.initialTitleHtml = sanitize(parsedData.initialTitleHtml);
+ parsedData.initialDescriptionHtml = sanitize(parsedData.initialDescriptionHtml);
+
+ cachedParsedData = parsedData;
+
+ return parsedData;
} catch (e) {
console.error(e); // eslint-disable-line no-console
diff --git a/app/assets/javascripts/issue_status_select.js b/app/assets/javascripts/issue_status_select.js
index 75edff41a89..02b10730153 100644
--- a/app/assets/javascripts/issue_status_select.js
+++ b/app/assets/javascripts/issue_status_select.js
@@ -1,10 +1,11 @@
import $ from 'jquery';
import { __ } from './locale';
+import initDeprecatedJQueryDropdown from '~/deprecated_jquery_dropdown';
export default function issueStatusSelect() {
$('.js-issue-status').each((i, el) => {
const fieldName = $(el).data('fieldName');
- return $(el).glDropdown({
+ initDeprecatedJQueryDropdown($(el), {
selectable: true,
fieldName,
toggleLabel(selected, element, instance) {
diff --git a/app/assets/javascripts/issuables_list/components/issuable.vue b/app/assets/javascripts/issues_list/components/issuable.vue
index 4fc614f8da4..adfb234fe7a 100644
--- a/app/assets/javascripts/issuables_list/components/issuable.vue
+++ b/app/assets/javascripts/issues_list/components/issuable.vue
@@ -6,7 +6,14 @@
// TODO: need to move this component to graphql - https://gitlab.com/gitlab-org/gitlab/-/issues/221246
import { escape, isNumber } from 'lodash';
-import { GlLink, GlTooltipDirective as GlTooltip, GlSprintf, GlLabel, GlIcon } from '@gitlab/ui';
+import {
+ GlLink,
+ GlTooltipDirective as GlTooltip,
+ GlSprintf,
+ GlLabel,
+ GlIcon,
+ GlSafeHtmlDirective as SafeHtml,
+} from '@gitlab/ui';
import jiraLogo from '@gitlab/svgs/dist/illustrations/logos/jira.svg';
import {
dateInWords,
@@ -41,6 +48,7 @@ export default {
},
directives: {
GlTooltip,
+ SafeHtml,
},
mixins: [glFeatureFlagsMixin()],
props: {
@@ -157,7 +165,7 @@ export default {
value: this.issuable.merge_requests_count,
title: __('Related merge requests'),
dataTestId: 'merge-requests',
- class: 'js-merge-requests icon-merge-request-unmerged',
+ class: 'js-merge-requests',
icon: 'merge-request',
},
{
@@ -298,9 +306,9 @@ export default {
<span class="js-ref-path gl-mr-4 mr-sm-0">
<span
v-if="isJiraIssue"
+ v-safe-html="jiraLogo"
class="svg-container jira-logo-container"
data-testid="jira-logo"
- v-html="jiraLogo"
></span>
{{ referencePath }}
</span>
diff --git a/app/assets/javascripts/issuables_list/components/issuables_list_app.vue b/app/assets/javascripts/issues_list/components/issuables_list_app.vue
index fecb7353efb..0d4f5bce965 100644
--- a/app/assets/javascripts/issuables_list/components/issuables_list_app.vue
+++ b/app/assets/javascripts/issues_list/components/issuables_list_app.vue
@@ -3,7 +3,7 @@ import { toNumber, omit } from 'lodash';
import {
GlEmptyState,
GlPagination,
- GlSkeletonLoading,
+ GlDeprecatedSkeletonLoading as GlSkeletonLoading,
GlSafeHtmlDirective as SafeHtml,
} from '@gitlab/ui';
import { deprecatedCreateFlash as flash } from '~/flash';
diff --git a/app/assets/javascripts/issuables_list/components/issuable_list_root_app.vue b/app/assets/javascripts/issues_list/components/jira_issues_list_root.vue
index cc90d23eda7..61781c576c0 100644
--- a/app/assets/javascripts/issuables_list/components/issuable_list_root_app.vue
+++ b/app/assets/javascripts/issues_list/components/jira_issues_list_root.vue
@@ -11,7 +11,7 @@ import {
} from '~/jira_import/utils/jira_import_utils';
export default {
- name: 'IssuableListRoot',
+ name: 'JiraIssuesList',
components: {
GlAlert,
GlLabel,
diff --git a/app/assets/javascripts/issuables_list/constants.js b/app/assets/javascripts/issues_list/constants.js
index f008ba1bf4a..f008ba1bf4a 100644
--- a/app/assets/javascripts/issuables_list/constants.js
+++ b/app/assets/javascripts/issues_list/constants.js
diff --git a/app/assets/javascripts/issuables_list/eventhub.js b/app/assets/javascripts/issues_list/eventhub.js
index e31806ad199..e31806ad199 100644
--- a/app/assets/javascripts/issuables_list/eventhub.js
+++ b/app/assets/javascripts/issues_list/eventhub.js
diff --git a/app/assets/javascripts/issuables_list/index.js b/app/assets/javascripts/issues_list/index.js
index fa23d6c0eed..1ff41c20d08 100644
--- a/app/assets/javascripts/issuables_list/index.js
+++ b/app/assets/javascripts/issues_list/index.js
@@ -2,10 +2,10 @@ import Vue from 'vue';
import VueApollo from 'vue-apollo';
import createDefaultClient from '~/lib/graphql';
import { parseBoolean, convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
-import IssuableListRootApp from './components/issuable_list_root_app.vue';
+import JiraIssuesListRoot from './components/jira_issues_list_root.vue';
import IssuablesListApp from './components/issuables_list_app.vue';
-function mountIssuableListRootApp() {
+function mountJiraIssuesListApp() {
const el = document.querySelector('.js-projects-issues-root');
if (!el) {
@@ -23,7 +23,7 @@ function mountIssuableListRootApp() {
el,
apolloProvider,
render(createComponent) {
- return createComponent(IssuableListRootApp, {
+ return createComponent(JiraIssuesListRoot, {
props: {
canEdit: parseBoolean(el.dataset.canEdit),
isJiraConfigured: parseBoolean(el.dataset.isJiraConfigured),
@@ -62,6 +62,6 @@ function mountIssuablesListApp() {
}
export default function initIssuablesList() {
- mountIssuableListRootApp();
+ mountJiraIssuesListApp();
mountIssuablesListApp();
}
diff --git a/app/assets/javascripts/issuables_list/queries/get_issues_list_details.query.graphql b/app/assets/javascripts/issues_list/queries/get_issues_list_details.query.graphql
index 8f9b888d19b..8f9b888d19b 100644
--- a/app/assets/javascripts/issuables_list/queries/get_issues_list_details.query.graphql
+++ b/app/assets/javascripts/issues_list/queries/get_issues_list_details.query.graphql
diff --git a/app/assets/javascripts/issues_list/service_desk_helper.js b/app/assets/javascripts/issues_list/service_desk_helper.js
new file mode 100644
index 00000000000..0a34b754377
--- /dev/null
+++ b/app/assets/javascripts/issues_list/service_desk_helper.js
@@ -0,0 +1,111 @@
+import { __ } from '~/locale';
+
+/**
+ * Generates empty state messages for Service Desk issues list.
+ *
+ * @param {emptyStateMeta} emptyStateMeta - Meta data used to generate empty state messages
+ * @returns {Object} Object containing empty state messages generated using the meta data.
+ */
+export function generateMessages(emptyStateMeta) {
+ const {
+ svgPath,
+ serviceDeskHelpPage,
+ serviceDeskAddress,
+ editProjectPage,
+ incomingEmailHelpPage,
+ } = emptyStateMeta;
+
+ const serviceDeskSupportedTitle = __(
+ 'Use Service Desk to connect with your users (e.g. to offer customer support) through email right inside GitLab',
+ );
+
+ const serviceDeskSupportedMessage = __(
+ 'Those emails automatically become issues (with the comments becoming the email conversation) listed here.',
+ );
+
+ const commonDescription = `
+ <span>${serviceDeskSupportedMessage}</span>
+ <a href="${serviceDeskHelpPage}">${__('Read more')}</a>`;
+
+ return {
+ serviceDeskEnabledAndCanEditProjectSettings: {
+ title: serviceDeskSupportedTitle,
+ svgPath,
+ description: `<p>${__('Have your users email')}
+ <code>${serviceDeskAddress}</code>
+ </p>
+ ${commonDescription}`,
+ },
+ serviceDeskEnabledAndCannotEditProjectSettings: {
+ title: serviceDeskSupportedTitle,
+ svgPath,
+ description: commonDescription,
+ },
+ serviceDeskDisabledAndCanEditProjectSettings: {
+ title: serviceDeskSupportedTitle,
+ svgPath,
+ description: commonDescription,
+ primaryLink: editProjectPage,
+ primaryText: __('Turn on Service Desk'),
+ },
+ serviceDeskDisabledAndCannotEditProjectSettings: {
+ title: serviceDeskSupportedTitle,
+ svgPath,
+ description: commonDescription,
+ },
+ serviceDeskIsNotSupported: {
+ title: __('Service Desk is not supported'),
+ svgPath,
+ description: __(
+ 'In order to enable Service Desk for your instance, you must first set up incoming email.',
+ ),
+ primaryLink: incomingEmailHelpPage,
+ primaryText: __('More information'),
+ },
+ serviceDeskIsNotEnabled: {
+ title: __('Service Desk is not enabled'),
+ svgPath,
+ description: __(
+ 'For help setting up the Service Desk for your instance, please contact an administrator.',
+ ),
+ },
+ };
+}
+
+/**
+ * Returns the attributes used for gl-empty-state in the Service Desk issues list.
+ *
+ * @param {Object} emptyStateMeta - Meta data used to generate empty state messages
+ * @returns {Object}
+ */
+export function emptyStateHelper(emptyStateMeta) {
+ const messages = generateMessages(emptyStateMeta);
+
+ const { isServiceDeskSupported, canEditProjectSettings, isServiceDeskEnabled } = emptyStateMeta;
+
+ if (isServiceDeskSupported) {
+ if (isServiceDeskEnabled && canEditProjectSettings) {
+ return messages.serviceDeskEnabledAndCanEditProjectSettings;
+ }
+
+ if (isServiceDeskEnabled && !canEditProjectSettings) {
+ return messages.serviceDeskEnabledAndCannotEditProjectSettings;
+ }
+
+ // !isServiceDeskEnabled && canEditProjectSettings
+ if (canEditProjectSettings) {
+ return messages.serviceDeskDisabledAndCanEditProjectSettings;
+ }
+
+ // !isServiceDeskEnabled && !canEditProjectSettings
+ return messages.serviceDeskDisabledAndCannotEditProjectSettings;
+ }
+
+ // !serviceDeskSupported && canEditProjectSettings
+ if (canEditProjectSettings) {
+ return messages.serviceDeskIsNotSupported;
+ }
+
+ // !serviceDeskSupported && !canEditProjectSettings
+ return messages.serviceDeskIsNotEnabled;
+}
diff --git a/app/assets/javascripts/jira_connect.js b/app/assets/javascripts/jira_connect.js
new file mode 100644
index 00000000000..895cdc4562c
--- /dev/null
+++ b/app/assets/javascripts/jira_connect.js
@@ -0,0 +1,56 @@
+/* eslint-disable func-names, no-var, no-alert */
+/* global $ */
+/* global AP */
+
+/**
+ * This script is not going through Webpack bundling
+ * as it is only included in `app/views/jira_connect/subscriptions/index.html.haml`
+ * which is going to be rendered within iframe on Jira app dashboard
+ * hence any code written here needs to be IE11+ compatible (no fully ES6)
+ */
+
+function onLoaded() {
+ var reqComplete = function() {
+ AP.navigator.reload();
+ };
+
+ var reqFailed = function(res) {
+ alert(res.responseJSON.error);
+ };
+
+ $('#add-subscription-form').on('submit', function(e) {
+ var actionUrl = $(this).attr('action');
+ e.preventDefault();
+
+ AP.context.getToken(function(token) {
+ // eslint-disable-next-line no-jquery/no-ajax
+ $.post(actionUrl, {
+ jwt: token,
+ namespace_path: $('#namespace-input').val(),
+ format: 'json',
+ })
+ .done(reqComplete)
+ .fail(reqFailed);
+ });
+ });
+
+ $('.remove-subscription').on('click', function(e) {
+ var href = $(this).attr('href');
+ e.preventDefault();
+
+ AP.context.getToken(function(token) {
+ // eslint-disable-next-line no-jquery/no-ajax
+ $.ajax({
+ url: href,
+ method: 'DELETE',
+ data: {
+ jwt: token,
+ format: 'json',
+ },
+ })
+ .done(reqComplete)
+ .fail(reqFailed);
+ });
+ });
+}
+document.addEventListener('DOMContentLoaded', onLoaded);
diff --git a/app/assets/javascripts/jira_import/components/jira_import_form.vue b/app/assets/javascripts/jira_import/components/jira_import_form.vue
index 5bf98682f92..4339021d9a0 100644
--- a/app/assets/javascripts/jira_import/components/jira_import_form.vue
+++ b/app/assets/javascripts/jira_import/components/jira_import_form.vue
@@ -2,9 +2,9 @@
import {
GlAlert,
GlButton,
- GlNewDropdown,
- GlNewDropdownItem,
- GlNewDropdownText,
+ GlDropdown,
+ GlDropdownItem,
+ GlDropdownText,
GlFormGroup,
GlFormSelect,
GlIcon,
@@ -34,9 +34,9 @@ export default {
components: {
GlAlert,
GlButton,
- GlNewDropdown,
- GlNewDropdownItem,
- GlNewDropdownText,
+ GlDropdown,
+ GlDropdownItem,
+ GlDropdownText,
GlFormGroup,
GlFormSelect,
GlIcon,
@@ -293,7 +293,7 @@ export default {
<gl-icon name="arrow-right" :aria-label="__('Will be mapped to')" />
</template>
<template #cell(gitlabUsername)="data">
- <gl-new-dropdown
+ <gl-dropdown
:text="data.value || $options.currentUsername"
class="w-100"
:aria-label="
@@ -301,23 +301,23 @@ export default {
"
@hide="resetDropdown"
>
- <gl-search-box-by-type v-model.trim="searchTerm" class="m-2" />
+ <gl-search-box-by-type v-model.trim="searchTerm" class="gl-m-3" />
<gl-loading-icon v-if="isFetching" />
- <gl-new-dropdown-item
+ <gl-dropdown-item
v-for="user in users"
v-else
:key="user.id"
@click="updateMapping(data.item.jiraAccountId, user.id, user.username)"
>
{{ user.username }} ({{ user.name }})
- </gl-new-dropdown-item>
+ </gl-dropdown-item>
- <gl-new-dropdown-text v-show="shouldShowNoMatchesFoundText" class="text-secondary">
+ <gl-dropdown-text v-show="shouldShowNoMatchesFoundText" class="text-secondary">
{{ __('No matches found') }}
- </gl-new-dropdown-text>
- </gl-new-dropdown>
+ </gl-dropdown-text>
+ </gl-dropdown>
</template>
</gl-table>
diff --git a/app/assets/javascripts/jira_import/utils/jira_import_utils.js b/app/assets/javascripts/jira_import/utils/jira_import_utils.js
index a1186b087e1..edd6fad4aac 100644
--- a/app/assets/javascripts/jira_import/utils/jira_import_utils.js
+++ b/app/assets/javascripts/jira_import/utils/jira_import_utils.js
@@ -1,5 +1,5 @@
import { last } from 'lodash';
-import { JIRA_IMPORT_SUCCESS_ALERT_HIDE_MAP_KEY } from '~/issuables_list/constants';
+import { JIRA_IMPORT_SUCCESS_ALERT_HIDE_MAP_KEY } from '~/issues_list/constants';
export const IMPORT_STATE = {
FAILED: 'failed',
diff --git a/app/assets/javascripts/jobs/components/artifacts_block.vue b/app/assets/javascripts/jobs/components/artifacts_block.vue
index 6183779acd4..2850a8e86fd 100644
--- a/app/assets/javascripts/jobs/components/artifacts_block.vue
+++ b/app/assets/javascripts/jobs/components/artifacts_block.vue
@@ -1,11 +1,12 @@
<script>
-import { GlLink } from '@gitlab/ui';
+import { GlIcon, GlLink } from '@gitlab/ui';
import TimeagoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
import timeagoMixin from '~/vue_shared/mixins/timeago';
export default {
components: {
TimeagoTooltip,
+ GlIcon,
GlLink,
},
mixins: [timeagoMixin],
@@ -14,6 +15,10 @@ export default {
type: Object,
required: true,
},
+ helpUrl: {
+ type: String,
+ required: true,
+ },
},
computed: {
isExpired() {
@@ -40,6 +45,14 @@ export default {
<span v-if="isExpired">{{ s__('Job|The artifacts were removed') }}</span>
<span v-if="willExpire">{{ s__('Job|The artifacts will be removed') }}</span>
<timeago-tooltip v-if="artifact.expire_at" :time="artifact.expire_at" />
+ <gl-link
+ :href="helpUrl"
+ target="_blank"
+ rel="noopener noreferrer nofollow"
+ data-testid="artifact-expired-help-link"
+ >
+ <gl-icon name="question" />
+ </gl-link>
</p>
<p v-else-if="isLocked" class="build-detail-row">
<span data-testid="job-locked-message">{{
@@ -71,6 +84,7 @@ export default {
:href="artifact.browse_path"
class="btn btn-sm btn-default"
data-testid="browse-artifacts"
+ data-qa-selector="browse_artifacts_button"
>{{ s__('Job|Browse') }}</gl-link
>
</div>
diff --git a/app/assets/javascripts/jobs/components/job_app.vue b/app/assets/javascripts/jobs/components/job_app.vue
index e760706c97e..00ff3fb939d 100644
--- a/app/assets/javascripts/jobs/components/job_app.vue
+++ b/app/assets/javascripts/jobs/components/job_app.vue
@@ -1,13 +1,13 @@
<script>
+/* eslint-disable vue/no-v-html */
import { throttle, isEmpty } from 'lodash';
import { mapGetters, mapState, mapActions } from 'vuex';
-import { GlLoadingIcon } from '@gitlab/ui';
+import { GlLoadingIcon, GlIcon } from '@gitlab/ui';
import { GlBreakpointInstance as bp } from '@gitlab/ui/dist/utils';
import { isScrolledToBottom } from '~/lib/utils/scroll_utils';
import { polyfillSticky } from '~/lib/utils/sticky';
import CiHeader from '~/vue_shared/components/header_ci_component.vue';
import Callout from '~/vue_shared/components/callout.vue';
-import Icon from '~/vue_shared/components/icon.vue';
import EmptyState from './empty_state.vue';
import EnvironmentsBlock from './environments_block.vue';
import ErasedBlock from './erased_block.vue';
@@ -27,7 +27,7 @@ export default {
EmptyState,
EnvironmentsBlock,
ErasedBlock,
- Icon,
+ GlIcon,
Log,
LogTopBar,
StuckBlock,
@@ -38,6 +38,11 @@ export default {
},
mixins: [delayedJobMixin],
props: {
+ artifactHelpUrl: {
+ type: String,
+ required: false,
+ default: '',
+ },
runnerSettingsUrl: {
type: String,
required: false,
@@ -266,7 +271,7 @@ export default {
:class="{ 'sticky-top border-bottom-0': hasTrace }"
data-testid="archived-job"
>
- <icon name="lock" class="align-text-bottom" />
+ <gl-icon name="lock" class="align-text-bottom" />
{{ __('This job is archived. Only the complete pipeline can be retried.') }}
</div>
<!-- job log -->
@@ -319,6 +324,7 @@ export default {
'right-sidebar-expanded': isSidebarOpen,
'right-sidebar-collapsed': !isSidebarOpen,
}"
+ :artifact-help-url="artifactHelpUrl"
:runner-help-url="runnerHelpUrl"
data-testid="job-sidebar"
/>
diff --git a/app/assets/javascripts/jobs/components/job_container_item.vue b/app/assets/javascripts/jobs/components/job_container_item.vue
index 7bd299bcfa0..79e6623eca8 100644
--- a/app/assets/javascripts/jobs/components/job_container_item.vue
+++ b/app/assets/javascripts/jobs/components/job_container_item.vue
@@ -1,15 +1,14 @@
<script>
-import { GlLink } from '@gitlab/ui';
+import { GlLink, GlIcon } from '@gitlab/ui';
import tooltip from '~/vue_shared/directives/tooltip';
import CiIcon from '~/vue_shared/components/ci_icon.vue';
-import Icon from '~/vue_shared/components/icon.vue';
import delayedJobMixin from '~/jobs/mixins/delayed_job_mixin';
import { sprintf } from '~/locale';
export default {
components: {
CiIcon,
- Icon,
+ GlIcon,
GlLink,
},
directives: {
@@ -56,7 +55,7 @@ export default {
data-boundary="viewport"
class="js-job-link d-flex"
>
- <icon
+ <gl-icon
v-if="isActive"
name="arrow-right"
class="js-arrow-right icon-arrow-right position-absolute d-block"
@@ -66,7 +65,7 @@ export default {
<span class="text-truncate w-100">{{ job.name ? job.name : job.id }}</span>
- <icon v-if="job.retried" name="retry" class="js-retry-icon" />
+ <gl-icon v-if="job.retried" name="retry" class="js-retry-icon" />
</gl-link>
</div>
</template>
diff --git a/app/assets/javascripts/jobs/components/job_log_controllers.vue b/app/assets/javascripts/jobs/components/job_log_controllers.vue
index 4d314eaa106..7e7d9a0549b 100644
--- a/app/assets/javascripts/jobs/components/job_log_controllers.vue
+++ b/app/assets/javascripts/jobs/components/job_log_controllers.vue
@@ -1,16 +1,15 @@
<script>
-import { GlTooltipDirective, GlLink, GlDeprecatedButton } from '@gitlab/ui';
+/* eslint-disable vue/no-v-html */
+import { GlTooltipDirective, GlLink, GlButton } from '@gitlab/ui';
import { polyfillSticky } from '~/lib/utils/sticky';
-import Icon from '~/vue_shared/components/icon.vue';
import { numberToHumanSize } from '~/lib/utils/number_utils';
import { __, sprintf } from '~/locale';
import scrollDown from '../svg/scroll_down.svg';
export default {
components: {
- Icon,
GlLink,
- GlDeprecatedButton,
+ GlButton,
},
directives: {
GlTooltip: GlTooltipDirective,
@@ -87,18 +86,17 @@ export default {
<div class="controllers float-right">
<!-- links -->
- <gl-link
+ <gl-button
v-if="rawPath"
v-gl-tooltip.body
:title="s__('Job|Show complete raw')"
:href="rawPath"
class="controllers-buttons"
data-testid="job-raw-link-controller"
- >
- <icon name="doc-text" />
- </gl-link>
+ icon="doc-text"
+ />
- <gl-link
+ <gl-button
v-if="erasePath"
v-gl-tooltip.body
:title="s__('Job|Erase job log')"
@@ -107,30 +105,28 @@ export default {
class="controllers-buttons"
data-testid="job-log-erase-link"
data-method="post"
- >
- <icon name="remove" />
- </gl-link>
+ icon="remove"
+ />
<!-- eo links -->
<!-- scroll buttons -->
<div v-gl-tooltip :title="s__('Job|Scroll to top')" class="controllers-buttons">
- <gl-deprecated-button
+ <gl-button
:disabled="isScrollTopDisabled"
- type="button"
class="btn-scroll btn-transparent btn-blank"
data-testid="job-controller-scroll-top"
+ icon="scroll_up"
@click="handleScrollToTop"
- >
- <icon name="scroll_up" />
- </gl-deprecated-button>
+ />
</div>
<div v-gl-tooltip :title="s__('Job|Scroll to bottom')" class="controllers-buttons">
- <gl-deprecated-button
+ <gl-button
:disabled="isScrollBottomDisabled"
class="js-scroll-bottom btn-scroll btn-transparent btn-blank"
- :class="{ animate: isScrollingDown }"
data-testid="job-controller-scroll-bottom"
+ icon="scroll_down"
+ :class="{ animate: isScrollingDown }"
@click="handleScrollToBottom"
v-html="$options.scrollDown"
/>
diff --git a/app/assets/javascripts/jobs/components/log/line.vue b/app/assets/javascripts/jobs/components/log/line.vue
index 48f669ae8ed..e68d5b8eda4 100644
--- a/app/assets/javascripts/jobs/components/log/line.vue
+++ b/app/assets/javascripts/jobs/components/log/line.vue
@@ -20,7 +20,7 @@ export default {
return h(
'span',
{
- class: ['ws-pre-wrap', content.style],
+ class: ['gl-white-space-pre-wrap', content.style],
},
content.text,
);
diff --git a/app/assets/javascripts/jobs/components/log/line_header.vue b/app/assets/javascripts/jobs/components/log/line_header.vue
index 85ccd5996b5..4c1c00cb2a7 100644
--- a/app/assets/javascripts/jobs/components/log/line_header.vue
+++ b/app/assets/javascripts/jobs/components/log/line_header.vue
@@ -1,11 +1,11 @@
<script>
-import Icon from '~/vue_shared/components/icon.vue';
+import { GlIcon } from '@gitlab/ui';
import LineNumber from './line_number.vue';
import DurationBadge from './duration_badge.vue';
export default {
components: {
- Icon,
+ GlIcon,
LineNumber,
DurationBadge,
},
@@ -47,12 +47,12 @@ export default {
role="button"
@click="handleOnClick"
>
- <icon :name="iconName" class="arrow position-absolute" />
+ <gl-icon :name="iconName" class="arrow position-absolute" />
<line-number :line-number="line.lineNumber" :path="path" />
<span
v-for="(content, i) in line.content"
:key="i"
- class="line-text w-100 ws-pre-wrap"
+ class="line-text w-100 gl-white-space-pre-wrap"
:class="content.style"
>{{ content.text }}</span
>
diff --git a/app/assets/javascripts/jobs/components/manual_variables_form.vue b/app/assets/javascripts/jobs/components/manual_variables_form.vue
index 9236624a191..bf1930c9a37 100644
--- a/app/assets/javascripts/jobs/components/manual_variables_form.vue
+++ b/app/assets/javascripts/jobs/components/manual_variables_form.vue
@@ -1,15 +1,14 @@
<script>
+/* eslint-disable vue/no-v-html */
import { uniqueId } from 'lodash';
import { mapActions } from 'vuex';
-import { GlDeprecatedButton } from '@gitlab/ui';
+import { GlButton } from '@gitlab/ui';
import { s__, sprintf } from '~/locale';
-import Icon from '~/vue_shared/components/icon.vue';
export default {
name: 'ManualVariablesForm',
components: {
- GlDeprecatedButton,
- Icon,
+ GlButton,
},
props: {
action: {
@@ -137,12 +136,12 @@ export default {
<div class="table-section section-10">
<div class="table-mobile-header" role="rowheader"></div>
<div class="table-mobile-content justify-content-end">
- <gl-deprecated-button
- class="btn-transparent btn-blank w-25"
+ <gl-button
+ category="tertiary"
+ icon="clear"
+ :aria-label="__('Delete variable')"
@click="deleteVariable(variable.id)"
- >
- <icon name="clear" />
- </gl-deprecated-button>
+ />
</div>
</div>
</div>
@@ -176,9 +175,14 @@ export default {
<p class="text-muted" v-html="helpText"></p>
</div>
<div class="d-flex justify-content-center">
- <gl-deprecated-button variant="primary" @click="triggerManualJob(variables)">
+ <gl-button
+ variant="info"
+ category="primary"
+ :aria-label="__('Trigger manual job')"
+ @click="triggerManualJob(variables)"
+ >
{{ action.button_title }}
- </gl-deprecated-button>
+ </gl-button>
</div>
</div>
</template>
diff --git a/app/assets/javascripts/jobs/components/sidebar.vue b/app/assets/javascripts/jobs/components/sidebar.vue
index 517da16dcf8..aa589989e8a 100644
--- a/app/assets/javascripts/jobs/components/sidebar.vue
+++ b/app/assets/javascripts/jobs/components/sidebar.vue
@@ -1,11 +1,11 @@
<script>
import { isEmpty } from 'lodash';
import { mapActions, mapState } from 'vuex';
-import { GlLink, GlDeprecatedButton } from '@gitlab/ui';
+import { GlLink, GlDeprecatedButton, GlIcon } from '@gitlab/ui';
import { __, sprintf } from '~/locale';
import timeagoMixin from '~/vue_shared/mixins/timeago';
+import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate.vue';
import { timeIntervalInWords } from '~/lib/utils/datetime_utility';
-import Icon from '~/vue_shared/components/icon.vue';
import DetailRow from './sidebar_detail_row.vue';
import ArtifactsBlock from './artifacts_block.vue';
import TriggerBlock from './trigger_block.vue';
@@ -19,15 +19,21 @@ export default {
ArtifactsBlock,
CommitBlock,
DetailRow,
- Icon,
+ GlIcon,
TriggerBlock,
StagesDropdown,
JobsContainer,
GlLink,
GlDeprecatedButton,
+ TooltipOnTruncate,
},
mixins: [timeagoMixin],
props: {
+ artifactHelpUrl: {
+ type: String,
+ required: false,
+ default: '',
+ },
runnerHelpUrl: {
type: String,
required: false,
@@ -112,7 +118,11 @@ export default {
<div class="sidebar-container">
<div class="blocks-container">
<div class="block d-flex flex-nowrap align-items-center">
- <h4 class="my-0 mr-2 text-break-word">{{ job.name }}</h4>
+ <tooltip-on-truncate :title="job.name" truncate-target="child"
+ ><h4 class="my-0 mr-2 gl-text-truncate">
+ {{ job.name }}
+ </h4>
+ </tooltip-on-truncate>
<div class="flex-grow-1 flex-shrink-0 text-right">
<gl-link
v-if="job.retry_path"
@@ -157,7 +167,7 @@ export default {
class="js-terminal-link btn btn-primary btn-inverted visible-md-block visible-lg-block float-left"
target="_blank"
>
- {{ __('Debug') }} <icon name="external-link" :size="14" />
+ {{ __('Debug') }} <gl-icon name="external-link" :size="14" />
</gl-link>
</div>
@@ -203,7 +213,7 @@ export default {
</p>
</div>
- <artifacts-block v-if="hasArtifact" :artifact="job.artifact" />
+ <artifacts-block v-if="hasArtifact" :artifact="job.artifact" :help-url="artifactHelpUrl" />
<trigger-block v-if="hasTriggers" :trigger="job.trigger" />
<commit-block
:is-last-block="hasStages"
diff --git a/app/assets/javascripts/jobs/components/sidebar_detail_row.vue b/app/assets/javascripts/jobs/components/sidebar_detail_row.vue
index b826007ec2c..7d541f93bad 100644
--- a/app/assets/javascripts/jobs/components/sidebar_detail_row.vue
+++ b/app/assets/javascripts/jobs/components/sidebar_detail_row.vue
@@ -1,9 +1,10 @@
<script>
-import { GlLink } from '@gitlab/ui';
+import { GlIcon, GlLink } from '@gitlab/ui';
export default {
name: 'SidebarDetailRow',
components: {
+ GlIcon,
GlLink,
},
props: {
@@ -37,7 +38,7 @@ export default {
<span v-if="hasTitle" class="font-weight-bold">{{ title }}:</span> {{ value }}
<span v-if="hasHelpURL" class="help-button float-right">
<gl-link :href="helpUrl" target="_blank" rel="noopener noreferrer nofollow">
- <i class="fa fa-question-circle" aria-hidden="true"></i>
+ <gl-icon name="question-o" />
</gl-link>
</span>
</p>
diff --git a/app/assets/javascripts/jobs/index.js b/app/assets/javascripts/jobs/index.js
index 024a13ce102..6e15360b66c 100644
--- a/app/assets/javascripts/jobs/index.js
+++ b/app/assets/javascripts/jobs/index.js
@@ -18,6 +18,7 @@ export default () => {
},
render(createElement) {
const {
+ artifactHelpUrl,
deploymentHelpUrl,
runnerHelpUrl,
runnerSettingsUrl,
@@ -32,6 +33,7 @@ export default () => {
return createElement('job-app', {
props: {
+ artifactHelpUrl,
deploymentHelpUrl,
runnerHelpUrl,
runnerSettingsUrl,
diff --git a/app/assets/javascripts/labels_select.js b/app/assets/javascripts/labels_select.js
index 1fb8e270e0e..8e172b4827c 100644
--- a/app/assets/javascripts/labels_select.js
+++ b/app/assets/javascripts/labels_select.js
@@ -12,6 +12,7 @@ import { deprecatedCreateFlash as flash } from './flash';
import ModalStore from './boards/stores/modal_store';
import boardsStore from './boards/stores/boards_store';
import { isScopedLabel } from '~/lib/utils/common_utils';
+import initDeprecatedJQueryDropdown from '~/deprecated_jquery_dropdown';
export default class LabelsSelect {
constructor(els, options = {}) {
@@ -173,7 +174,7 @@ export default class LabelsSelect {
})
.catch(() => flash(__('Error saving label update.')));
};
- $dropdown.glDropdown({
+ initDeprecatedJQueryDropdown($dropdown, {
showMenuAbove,
data(term, callback) {
const labelUrl = $dropdown.attr('data-labels');
@@ -203,7 +204,7 @@ export default class LabelsSelect {
callback(data);
if (showMenuAbove) {
- $dropdown.data('glDropdown').positionMenuAbove();
+ $dropdown.data('deprecatedJQueryDropdown').positionMenuAbove();
}
})
.catch(() => flash(__('Error fetching labels.')));
@@ -348,7 +349,7 @@ export default class LabelsSelect {
} else {
if (!$dropdown.hasClass('js-filter-bulk-update')) {
saveLabelData();
- $dropdown.data('glDropdown').clearMenu();
+ $dropdown.data('deprecatedJQueryDropdown').clearMenu();
}
}
}
@@ -455,7 +456,7 @@ export default class LabelsSelect {
if ($dropdown.hasClass('js-issue-board-sidebar')) {
const previousSelection = $dropdown.attr('data-selected');
this.selected = previousSelection ? previousSelection.split(',') : [];
- $dropdown.data('glDropdown').updateLabel();
+ $dropdown.data('deprecatedJQueryDropdown').updateLabel();
}
},
preserveContext: true,
diff --git a/app/assets/javascripts/layout_nav.js b/app/assets/javascripts/layout_nav.js
index 0ecf3301250..d7f5e6f8a5e 100644
--- a/app/assets/javascripts/layout_nav.js
+++ b/app/assets/javascripts/layout_nav.js
@@ -1,7 +1,6 @@
import $ from 'jquery';
import ContextualSidebar from './contextual_sidebar';
import initFlyOutNav from './fly_out_nav';
-import initWhatsNew from '~/whats_new';
function hideEndFade($scrollingTabs) {
$scrollingTabs.each(function scrollTabsLoop() {
@@ -14,6 +13,17 @@ function hideEndFade($scrollingTabs) {
function initDeferred() {
$(document).trigger('init.scrolling-tabs');
+
+ const whatsNewTriggerEl = document.querySelector('.js-whats-new-trigger');
+ if (whatsNewTriggerEl) {
+ whatsNewTriggerEl.addEventListener('click', () => {
+ import(/* webpackChunkName: 'whatsNewApp' */ '~/whats_new')
+ .then(({ default: initWhatsNew }) => {
+ initWhatsNew();
+ })
+ .catch(() => {});
+ });
+ }
}
export default function initLayoutNav() {
@@ -21,7 +31,6 @@ export default function initLayoutNav() {
contextualSidebar.bindEvents();
initFlyOutNav();
- initWhatsNew();
// We need to init it on DomContentLoaded as others could also call it
$(document).on('init.scrolling-tabs', () => {
diff --git a/app/assets/javascripts/lib/graphql.js b/app/assets/javascripts/lib/graphql.js
index 4fed121779e..d2907f401c0 100644
--- a/app/assets/javascripts/lib/graphql.js
+++ b/app/assets/javascripts/lib/graphql.js
@@ -4,6 +4,7 @@ import { createUploadLink } from 'apollo-upload-client';
import { ApolloLink } from 'apollo-link';
import { BatchHttpLink } from 'apollo-link-batch-http';
import csrf from '~/lib/utils/csrf';
+import PerformanceBarService from '~/performance_bar/services/performance_bar_service';
export const fetchPolicies = {
CACHE_FIRST: 'cache-first',
@@ -32,13 +33,35 @@ export default (resolvers = {}, config = {}) => {
credentials: 'same-origin',
};
+ const uploadsLink = ApolloLink.split(
+ operation => operation.getContext().hasUpload || operation.getContext().isSingleRequest,
+ createUploadLink(httpOptions),
+ new BatchHttpLink(httpOptions),
+ );
+
+ const performanceBarLink = new ApolloLink((operation, forward) => {
+ return forward(operation).map(response => {
+ const httpResponse = operation.getContext().response;
+
+ if (PerformanceBarService.interceptor) {
+ PerformanceBarService.interceptor({
+ config: {
+ url: httpResponse.url,
+ },
+ headers: {
+ 'x-request-id': httpResponse.headers.get('x-request-id'),
+ 'x-gitlab-from-cache': httpResponse.headers.get('x-gitlab-from-cache'),
+ },
+ });
+ }
+
+ return response;
+ });
+ });
+
return new ApolloClient({
typeDefs: config.typeDefs,
- link: ApolloLink.split(
- operation => operation.getContext().hasUpload || operation.getContext().isSingleRequest,
- createUploadLink(httpOptions),
- new BatchHttpLink(httpOptions),
- ),
+ link: ApolloLink.from([performanceBarLink, uploadsLink]),
cache: new InMemoryCache({
...config.cacheConfig,
freezeResults: config.assumeImmutableResults,
diff --git a/app/assets/javascripts/lib/utils/axios_startup_calls.js b/app/assets/javascripts/lib/utils/axios_startup_calls.js
index a047cebc8ab..7e2665b910c 100644
--- a/app/assets/javascripts/lib/utils/axios_startup_calls.js
+++ b/app/assets/javascripts/lib/utils/axios_startup_calls.js
@@ -10,6 +10,32 @@ const getFullUrl = req => {
return mergeUrlParams(req.params || {}, url);
};
+const handleStartupCall = async ({ fetchCall }, req) => {
+ const res = await fetchCall;
+ if (!res.ok) {
+ throw new Error(res.statusText);
+ }
+
+ const fetchHeaders = {};
+ res.headers.forEach((val, key) => {
+ fetchHeaders[key] = val;
+ });
+
+ const data = await res.clone().json();
+
+ Object.assign(req, {
+ adapter: () =>
+ Promise.resolve({
+ data,
+ status: res.status,
+ statusText: res.statusText,
+ headers: fetchHeaders,
+ config: req,
+ request: req,
+ }),
+ });
+};
+
const setupAxiosStartupCalls = axios => {
const { startup_calls: startupCalls } = window.gl || {};
@@ -17,35 +43,28 @@ const setupAxiosStartupCalls = axios => {
return;
}
- // TODO: To save performance of future axios calls, we can
- // remove this interceptor once the "startupCalls" have been loaded
- axios.interceptors.request.use(req => {
+ const remainingCalls = new Map(Object.entries(startupCalls));
+
+ const interceptor = axios.interceptors.request.use(async req => {
const fullUrl = getFullUrl(req);
- const existing = startupCalls[fullUrl];
-
- if (existing) {
- // eslint-disable-next-line no-param-reassign
- req.adapter = () =>
- existing.fetchCall.then(res => {
- const fetchHeaders = {};
- res.headers.forEach((val, key) => {
- fetchHeaders[key] = val;
- });
-
- // eslint-disable-next-line promise/no-nesting
- return res
- .clone()
- .json()
- .then(data => ({
- data,
- status: res.status,
- statusText: res.statusText,
- headers: fetchHeaders,
- config: req,
- request: req,
- }));
- });
+ const startupCall = remainingCalls.get(fullUrl);
+
+ if (!startupCall?.fetchCall) {
+ return req;
+ }
+
+ try {
+ await handleStartupCall(startupCall, req);
+ } catch (e) {
+ // eslint-disable-next-line no-console
+ console.warn(`[gitlab] Something went wrong with the startup call for "${fullUrl}"`, e);
+ }
+
+ remainingCalls.delete(fullUrl);
+
+ if (remainingCalls.size === 0) {
+ axios.interceptors.request.eject(interceptor);
}
return req;
diff --git a/app/assets/javascripts/lib/utils/datetime_utility.js b/app/assets/javascripts/lib/utils/datetime_utility.js
index e26b63fbb85..b193a8b2c9a 100644
--- a/app/assets/javascripts/lib/utils/datetime_utility.js
+++ b/app/assets/javascripts/lib/utils/datetime_utility.js
@@ -216,8 +216,9 @@ export const timeFor = (time, expiredLabel) => {
return timeago.format(time, `${timeagoLanguageCode}-remaining`).trim();
};
+export const millisecondsPerDay = 1000 * 60 * 60 * 24;
+
export const getDayDifference = (a, b) => {
- const millisecondsPerDay = 1000 * 60 * 60 * 24;
const date1 = Date.UTC(a.getFullYear(), a.getMonth(), a.getDate());
const date2 = Date.UTC(b.getFullYear(), b.getMonth(), b.getDate());
@@ -642,6 +643,16 @@ export const secondsToMilliseconds = seconds => seconds * 1000;
export const secondsToDays = seconds => Math.round(seconds / 86400);
/**
+ * Returns the date n days after the date provided
+ *
+ * @param {Date} date the initial date
+ * @param {Number} numberOfDays number of days after
+ * @return {Date} the date following the date provided
+ */
+export const nDaysAfter = (date, numberOfDays) =>
+ new Date(newDate(date)).setDate(date.getDate() + numberOfDays);
+
+/**
* Returns the date after the date provided
*
* @param {Date} date the initial date
@@ -702,20 +713,14 @@ export const approximateDuration = (seconds = 0) => {
* @return {Date} the date object from the params
*/
export const dateFromParams = (year, month, day) => {
- const date = new Date();
-
- date.setFullYear(year);
- date.setMonth(month);
- date.setDate(day);
-
- return date;
+ return new Date(year, month, day);
};
/**
* A utility function which computes the difference in seconds
* between 2 dates.
*
- * @param {Date} startDate the start sate
+ * @param {Date} startDate the start date
* @param {Date} endDate the end date
*
* @return {Int} the difference in seconds
@@ -723,3 +728,18 @@ export const dateFromParams = (year, month, day) => {
export const differenceInSeconds = (startDate, endDate) => {
return (endDate.getTime() - startDate.getTime()) / 1000;
};
+
+/**
+ * A utility function which computes the difference in milliseconds
+ * between 2 dates.
+ *
+ * @param {Date|Int} startDate the start date. Can be either a date object or a unix timestamp.
+ * @param {Date|Int} endDate the end date. Can be either a date object or a unix timestamp. Defaults to now.
+ *
+ * @return {Int} the difference in milliseconds
+ */
+export const differenceInMilliseconds = (startDate, endDate = Date.now()) => {
+ const startDateInMS = startDate instanceof Date ? startDate.getTime() : startDate;
+ const endDateInMS = endDate instanceof Date ? endDate.getTime() : endDate;
+ return endDateInMS - startDateInMS;
+};
diff --git a/app/assets/javascripts/lib/utils/forms.js b/app/assets/javascripts/lib/utils/forms.js
index ced44ab9817..1c5f6cefeda 100644
--- a/app/assets/javascripts/lib/utils/forms.js
+++ b/app/assets/javascripts/lib/utils/forms.js
@@ -14,3 +14,40 @@ export const serializeForm = form => {
return serializeFormEntries(entries);
};
+
+/**
+ * Check if the value provided is empty or not
+ *
+ * It is being used to check if a form input
+ * value has been set or not
+ *
+ * @param {String, Number, Array} - Any form value
+ * @returns {Boolean} - returns false if a value is set
+ *
+ * @example
+ * returns true for '', [], null, undefined
+ */
+export const isEmptyValue = value => value == null || value.length === 0;
+
+/**
+ * A form object serializer
+ *
+ * @param {Object} - Form Object
+ * @returns {Object} - Serialized Form Object
+ *
+ * @example
+ * Input
+ * {"project": {"value": "hello", "state": false}, "username": {"value": "john"}}
+ *
+ * Returns
+ * {"project": "hello", "username": "john"}
+ */
+export const serializeFormObject = form =>
+ Object.fromEntries(
+ Object.entries(form).reduce((acc, [name, { value }]) => {
+ if (!isEmptyValue(value)) {
+ acc.push([name, value]);
+ }
+ return acc;
+ }, []),
+ );
diff --git a/app/assets/javascripts/lib/utils/image_utility.js b/app/assets/javascripts/lib/utils/image_utility.js
index 2977ec821cb..af531f9f830 100644
--- a/app/assets/javascripts/lib/utils/image_utility.js
+++ b/app/assets/javascripts/lib/utils/image_utility.js
@@ -1,5 +1,3 @@
-/* eslint-disable import/prefer-default-export */
-
export function isImageLoaded(element) {
return element.complete && element.naturalHeight !== 0;
}
diff --git a/app/assets/javascripts/lib/utils/jquery_at_who.js b/app/assets/javascripts/lib/utils/jquery_at_who.js
new file mode 100644
index 00000000000..88111cb4ae4
--- /dev/null
+++ b/app/assets/javascripts/lib/utils/jquery_at_who.js
@@ -0,0 +1,3 @@
+import 'jquery';
+import 'jquery.caret'; // required by at.js
+import '@gitlab/at.js';
diff --git a/app/assets/javascripts/lib/utils/poll.js b/app/assets/javascripts/lib/utils/poll.js
index 7f0c65868c2..e8583fa951b 100644
--- a/app/assets/javascripts/lib/utils/poll.js
+++ b/app/assets/javascripts/lib/utils/poll.js
@@ -88,7 +88,11 @@ export default class Poll {
}
makeDelayedRequest(delay = 0) {
- this.timeoutID = setTimeout(() => this.makeRequest(), delay);
+ // So we don't make our specs artificially slower
+ this.timeoutID = setTimeout(
+ () => this.makeRequest(),
+ process.env.NODE_ENV !== 'test' ? delay : 1,
+ );
}
makeRequest() {
diff --git a/app/assets/javascripts/lib/utils/set.js b/app/assets/javascripts/lib/utils/set.js
index 3845d648b61..541934c4221 100644
--- a/app/assets/javascripts/lib/utils/set.js
+++ b/app/assets/javascripts/lib/utils/set.js
@@ -4,6 +4,5 @@
* @param {Set} superset The set to be considered as the superset.
* @returns {boolean}
*/
-// eslint-disable-next-line import/prefer-default-export
export const isSubset = (subset, superset) =>
Array.from(subset).every(value => superset.has(value));
diff --git a/app/assets/javascripts/lib/utils/simple_poll.js b/app/assets/javascripts/lib/utils/simple_poll.js
index 576a9ec880c..e4e9fb2e6fa 100644
--- a/app/assets/javascripts/lib/utils/simple_poll.js
+++ b/app/assets/javascripts/lib/utils/simple_poll.js
@@ -1,10 +1,12 @@
+import { differenceInMilliseconds } from '~/lib/utils/datetime_utility';
+
export default (fn, { interval = 2000, timeout = 60000 } = {}) => {
const startTime = Date.now();
return new Promise((resolve, reject) => {
const stop = arg => (arg instanceof Error ? reject(arg) : resolve(arg));
const next = () => {
- if (timeout === 0 || Date.now() - startTime < timeout) {
+ if (timeout === 0 || differenceInMilliseconds(startTime) < timeout) {
setTimeout(fn.bind(null, next, stop), interval);
} else {
reject(new Error('SIMPLE_POLL_TIMEOUT'));
diff --git a/app/assets/javascripts/lib/utils/text_markdown.js b/app/assets/javascripts/lib/utils/text_markdown.js
index 8d23d177410..f4c6e4e3584 100644
--- a/app/assets/javascripts/lib/utils/text_markdown.js
+++ b/app/assets/javascripts/lib/utils/text_markdown.js
@@ -1,6 +1,7 @@
/* eslint-disable func-names, no-param-reassign, operator-assignment, consistent-return */
import $ from 'jquery';
import { insertText } from '~/lib/utils/common_utils';
+import Shortcuts from '~/behaviors/shortcuts/shortcuts';
const LINK_TAG_PATTERN = '[{text}](url)';
@@ -303,23 +304,67 @@ function updateText({ textArea, tag, cursorOffset, blockTag, wrap, select, tagCo
});
}
+/* eslint-disable @gitlab/require-i18n-strings */
+export function keypressNoteText(e) {
+ if (this.selectionStart === this.selectionEnd) {
+ return;
+ }
+ const keys = {
+ '*': '**{text}**', // wraps with bold character
+ _: '_{text}_', // wraps with italic character
+ '`': '`{text}`', // wraps with inline character
+ "'": "'{text}'", // single quotes
+ '"': '"{text}"', // double quotes
+ '[': '[{text}]', // brackets
+ '{': '{{text}}', // braces
+ '(': '({text})', // parentheses
+ '<': '<{text}>', // angle brackets
+ };
+ const tag = keys[e.key];
+
+ if (tag) {
+ e.preventDefault();
+
+ updateText({
+ tag,
+ textArea: this,
+ blockTag: '',
+ wrap: true,
+ select: '',
+ tagContent: '',
+ });
+ }
+}
+/* eslint-enable @gitlab/require-i18n-strings */
+
+export function updateTextForToolbarBtn($toolbarBtn) {
+ return updateText({
+ textArea: $toolbarBtn.closest('.md-area').find('textarea'),
+ tag: $toolbarBtn.data('mdTag'),
+ cursorOffset: $toolbarBtn.data('mdCursorOffset'),
+ blockTag: $toolbarBtn.data('mdBlock'),
+ wrap: !$toolbarBtn.data('mdPrepend'),
+ select: $toolbarBtn.data('mdSelect'),
+ tagContent: $toolbarBtn.data('mdTagContent'),
+ });
+}
+
export function addMarkdownListeners(form) {
- return $('.js-md', form)
+ $('.markdown-area', form)
+ .on('keydown', keypressNoteText)
+ .each(function attachTextareaShortcutHandlers() {
+ Shortcuts.initMarkdownEditorShortcuts($(this), updateTextForToolbarBtn);
+ });
+
+ const $allToolbarBtns = $('.js-md', form)
.off('click')
.on('click', function() {
- const $this = $(this);
- const tag = this.dataset.mdTag;
-
- return updateText({
- textArea: $this.closest('.md-area').find('textarea'),
- tag,
- cursorOffset: $this.data('mdCursorOffset'),
- blockTag: $this.data('mdBlock'),
- wrap: !$this.data('mdPrepend'),
- select: $this.data('mdSelect'),
- tagContent: $this.data('mdTagContent'),
- });
+ const $toolbarBtn = $(this);
+
+ return updateTextForToolbarBtn($toolbarBtn);
});
+
+ return $allToolbarBtns;
}
export function addEditorMarkdownListeners(editor) {
@@ -342,5 +387,11 @@ export function addEditorMarkdownListeners(editor) {
}
export function removeMarkdownListeners(form) {
+ $('.markdown-area', form)
+ .off('keydown', keypressNoteText)
+ .each(function removeTextareaShortcutHandlers() {
+ Shortcuts.removeMarkdownEditorShortcuts($(this));
+ });
+
return $('.js-md', form).off('click');
}
diff --git a/app/assets/javascripts/lib/utils/text_utility.js b/app/assets/javascripts/lib/utils/text_utility.js
index e2953ce330c..8ac6a44cba9 100644
--- a/app/assets/javascripts/lib/utils/text_utility.js
+++ b/app/assets/javascripts/lib/utils/text_utility.js
@@ -275,6 +275,81 @@ export const convertToSentenceCase = string => {
*/
export const convertToTitleCase = string => string.replace(/\b[a-z]/g, s => s.toUpperCase());
+const unicodeConversion = [
+ [/[ÀÁÂÃÅĀĂĄ]/g, 'A'],
+ [/[Æ]/g, 'AE'],
+ [/[ÇĆĈĊČ]/g, 'C'],
+ [/[ÈÉÊËĒĔĖĘĚ]/g, 'E'],
+ [/[ÌÍÎÏĨĪĬĮİ]/g, 'I'],
+ [/[Ððĥħ]/g, 'h'],
+ [/[ÑŃŅŇʼn]/g, 'N'],
+ [/[ÒÓÔÕØŌŎŐ]/g, 'O'],
+ [/[ÙÚÛŨŪŬŮŰŲ]/g, 'U'],
+ [/[ÝŶŸ]/g, 'Y'],
+ [/[Þñþńņň]/g, 'n'],
+ [/[ߌŜŞŠ]/g, 'S'],
+ [/[àáâãåāăąĸ]/g, 'a'],
+ [/[æ]/g, 'ae'],
+ [/[çćĉċč]/g, 'c'],
+ [/[èéêëēĕėęě]/g, 'e'],
+ [/[ìíîïĩīĭį]/g, 'i'],
+ [/[òóôõøōŏő]/g, 'o'],
+ [/[ùúûũūŭůűų]/g, 'u'],
+ [/[ýÿŷ]/g, 'y'],
+ [/[ĎĐ]/g, 'D'],
+ [/[ďđ]/g, 'd'],
+ [/[ĜĞĠĢ]/g, 'G'],
+ [/[ĝğġģŊŋſ]/g, 'g'],
+ [/[ĤĦ]/g, 'H'],
+ [/[ıśŝşš]/g, 's'],
+ [/[IJ]/g, 'IJ'],
+ [/[ij]/g, 'ij'],
+ [/[Ĵ]/g, 'J'],
+ [/[ĵ]/g, 'j'],
+ [/[Ķ]/g, 'K'],
+ [/[ķ]/g, 'k'],
+ [/[ĹĻĽĿŁ]/g, 'L'],
+ [/[ĺļľŀł]/g, 'l'],
+ [/[Œ]/g, 'OE'],
+ [/[œ]/g, 'oe'],
+ [/[ŔŖŘ]/g, 'R'],
+ [/[ŕŗř]/g, 'r'],
+ [/[ŢŤŦ]/g, 'T'],
+ [/[ţťŧ]/g, 't'],
+ [/[Ŵ]/g, 'W'],
+ [/[ŵ]/g, 'w'],
+ [/[ŹŻŽ]/g, 'Z'],
+ [/[źżž]/g, 'z'],
+ [/ö/g, 'oe'],
+ [/ü/g, 'ue'],
+ [/ä/g, 'ae'],
+ // eslint-disable-next-line @gitlab/require-i18n-strings
+ [/Ö/g, 'Oe'],
+ // eslint-disable-next-line @gitlab/require-i18n-strings
+ [/Ü/g, 'Ue'],
+ // eslint-disable-next-line @gitlab/require-i18n-strings
+ [/Ä/g, 'Ae'],
+];
+
+/**
+ * Converts each non-ascii character in a string to
+ * it's ascii equivalent (if defined).
+ *
+ * e.g. "Dĭd söméònê äšk fœŕ Ůnĭċődę?" => "Did someone aesk foer Unicode?"
+ *
+ * @param {String} string
+ * @returns {String}
+ */
+export const convertUnicodeToAscii = string => {
+ let convertedString = string;
+
+ unicodeConversion.forEach(([regex, replacer]) => {
+ convertedString = convertedString.replace(regex, replacer);
+ });
+
+ return convertedString;
+};
+
/**
* Splits camelCase or PascalCase words
* e.g. HelloWorld => Hello World
diff --git a/app/assets/javascripts/lib/utils/type_utility.js b/app/assets/javascripts/lib/utils/type_utility.js
index be86f336bcd..664c0dbbc84 100644
--- a/app/assets/javascripts/lib/utils/type_utility.js
+++ b/app/assets/javascripts/lib/utils/type_utility.js
@@ -1,2 +1 @@
-// eslint-disable-next-line import/prefer-default-export
export const isObject = obj => obj && obj.constructor === Object;
diff --git a/app/assets/javascripts/lib/utils/url_utility.js b/app/assets/javascripts/lib/utils/url_utility.js
index 8077570158a..e9c3fe0a406 100644
--- a/app/assets/javascripts/lib/utils/url_utility.js
+++ b/app/assets/javascripts/lib/utils/url_utility.js
@@ -75,7 +75,7 @@ export function getParameterValues(sParam, url = window.location) {
* @param {Boolean} options.spreadArrays - split array values into separate key/value-pairs
*/
export function mergeUrlParams(params, url, options = {}) {
- const { spreadArrays = false } = options;
+ const { spreadArrays = false, sort = false } = options;
const re = /^([^?#]*)(\?[^#]*)?(.*)/;
let merged = {};
const [, fullpath, query, fragment] = url.match(re);
@@ -108,7 +108,9 @@ export function mergeUrlParams(params, url, options = {}) {
Object.assign(merged, params);
- const newQuery = Object.keys(merged)
+ const mergedKeys = sort ? Object.keys(merged).sort() : Object.keys(merged);
+
+ const newQuery = mergedKeys
.filter(key => merged[key] !== null)
.map(key => {
let value = merged[key];
@@ -334,17 +336,32 @@ export function getWebSocketUrl(path) {
* Convert search query into an object
*
* @param {String} query from "document.location.search"
+ * @param {Object} options
+ * @param {Boolean} options.gatherArrays - gather array values into an Array
* @returns {Object}
*
* ex: "?one=1&two=2" into {one: 1, two: 2}
*/
-export function queryToObject(query) {
+export function queryToObject(query, options = {}) {
+ const { gatherArrays = false } = options;
const removeQuestionMarkFromQuery = String(query).startsWith('?') ? query.slice(1) : query;
return removeQuestionMarkFromQuery.split('&').reduce((accumulator, curr) => {
const [key, value] = curr.split('=');
- if (value !== undefined) {
- accumulator[decodeURIComponent(key)] = decodeURIComponent(value);
+ if (value === undefined) {
+ return accumulator;
}
+ const decodedValue = decodeURIComponent(value);
+
+ if (gatherArrays && key.endsWith('[]')) {
+ const decodedKey = decodeURIComponent(key.slice(0, -2));
+ if (!Array.isArray(accumulator[decodedKey])) {
+ accumulator[decodedKey] = [];
+ }
+ accumulator[decodedKey].push(decodedValue);
+ } else {
+ accumulator[decodeURIComponent(key)] = decodedValue;
+ }
+
return accumulator;
}, {});
}
diff --git a/app/assets/javascripts/lib/utils/webpack.js b/app/assets/javascripts/lib/utils/webpack.js
index 390294afcb7..622c40e0f35 100644
--- a/app/assets/javascripts/lib/utils/webpack.js
+++ b/app/assets/javascripts/lib/utils/webpack.js
@@ -1,7 +1,6 @@
import { joinPaths } from '~/lib/utils/url_utility';
// tell webpack to load assets from origin so that web workers don't break
-// eslint-disable-next-line import/prefer-default-export
export function resetServiceWorkersPublicPath() {
// __webpack_public_path__ is a global variable that can be used to adjust
// the webpack publicPath setting at runtime.
diff --git a/app/assets/javascripts/line_highlighter.js b/app/assets/javascripts/line_highlighter.js
index ed10c7646a8..8621a133776 100644
--- a/app/assets/javascripts/line_highlighter.js
+++ b/app/assets/javascripts/line_highlighter.js
@@ -1,6 +1,7 @@
/* eslint-disable func-names, no-underscore-dangle, no-param-reassign, consistent-return */
import $ from 'jquery';
+import 'vendor/jquery.scrollTo';
// LineHighlighter
//
diff --git a/app/assets/javascripts/logs/stores/getters.js b/app/assets/javascripts/logs/stores/getters.js
index d92969c5389..dc392af8381 100644
--- a/app/assets/javascripts/logs/stores/getters.js
+++ b/app/assets/javascripts/logs/stores/getters.js
@@ -6,8 +6,16 @@ const mapTrace = ({ timestamp = null, pod = '', message = '' }) =>
export const trace = state => state.logs.lines.map(mapTrace).join('\n');
export const showAdvancedFilters = state => {
- const environment = state.environments.options.find(
- ({ name }) => name === state.environments.current,
+ if (state.environments.current) {
+ const environment = state.environments.options.find(
+ ({ name }) => name === state.environments.current,
+ );
+
+ return Boolean(environment?.enable_advanced_logs_querying);
+ }
+ const managedApp = state.managedApps.options.find(
+ ({ name }) => name === state.managedApps.current,
);
- return Boolean(environment?.enable_advanced_logs_querying);
+
+ return Boolean(managedApp?.enable_advanced_logs_querying);
};
diff --git a/app/assets/javascripts/main.js b/app/assets/javascripts/main.js
index 1572e82a66c..9fcf881a1ac 100644
--- a/app/assets/javascripts/main.js
+++ b/app/assets/javascripts/main.js
@@ -20,25 +20,22 @@ import { localTimeAgo } from './lib/utils/datetime_utility';
import { getLocationHash, visitUrl } from './lib/utils/url_utility';
// everything else
-import loadAwardsHandler from './awards_handler';
import { deprecatedCreateFlash as Flash, removeFlashClickListener } from './flash';
-import './gl_dropdown';
import initTodoToggle from './header';
import initImporterStatus from './importer_status';
import initLayoutNav from './layout_nav';
+import initAlertHandler from './alert_handler';
import './feature_highlight/feature_highlight_options';
import LazyLoader from './lazy_loader';
import initLogoAnimation from './logo';
import initFrequentItemDropdowns from './frequent_items';
import initBreadcrumbs from './breadcrumb';
import initUsagePingConsent from './usage_ping_consent';
-import initPerformanceBar from './performance_bar';
-import initSearchAutocomplete from './search_autocomplete';
import GlFieldErrors from './gl_field_errors';
import initUserPopovers from './user_popovers';
import initBroadcastNotifications from './broadcast_notification';
import initPersistentUserCallouts from './persistent_user_callouts';
-import { initUserTracking } from './tracking';
+import { initUserTracking, initDefaultTrackers } from './tracking';
import { __ } from './locale';
import 'ee_else_ce/main_ee';
@@ -112,8 +109,23 @@ function deferredInitialisation() {
initBroadcastNotifications();
initFrequentItemDropdowns();
initPersistentUserCallouts();
-
- if (document.querySelector('.search')) initSearchAutocomplete();
+ initDefaultTrackers();
+
+ const search = document.querySelector('#search');
+ if (search) {
+ search.addEventListener(
+ 'focus',
+ () => {
+ import(/* webpackChunkName: 'globalSearch' */ './search_autocomplete')
+ .then(({ default: initSearchAutocomplete }) => {
+ const searchDropdown = initSearchAutocomplete();
+ searchDropdown.onSearchInputFocus();
+ })
+ .catch(() => {});
+ },
+ { once: true },
+ );
+ }
addSelectOnFocusBehaviour('.js-select-on-focus');
@@ -155,8 +167,6 @@ function deferredInitialisation() {
viewport: '.layout-page',
});
- loadAwardsHandler();
-
// Adding a helper class to activate animations only after all is rendered
setTimeout(() => $body.addClass('page-initialised'), 1000);
}
@@ -166,10 +176,9 @@ document.addEventListener('DOMContentLoaded', () => {
const $document = $(document);
const bootstrapBreakpoint = bp.getBreakpointSize();
- if (document.querySelector('#js-peek')) initPerformanceBar({ container: '#js-peek' });
-
initUserTracking();
initLayoutNav();
+ initAlertHandler();
// Set the default path for all cookies to GitLab's root directory
Cookies.defaults.path = gon.relative_url_root || '/';
diff --git a/app/assets/javascripts/members.js b/app/assets/javascripts/members.js
index f220e9e0192..c3fbb5d6acf 100644
--- a/app/assets/javascripts/members.js
+++ b/app/assets/javascripts/members.js
@@ -1,5 +1,6 @@
import $ from 'jquery';
import { disableButtonIfEmptyField } from '~/lib/utils/common_utils';
+import initDeprecatedJQueryDropdown from '~/deprecated_jquery_dropdown';
export default class Members {
constructor() {
@@ -37,7 +38,7 @@ export default class Members {
$('.js-member-permissions-dropdown').each((i, btn) => {
const $btn = $(btn);
- $btn.glDropdown({
+ initDeprecatedJQueryDropdown($btn, {
selectable: true,
isSelectable: (selected, $el) => this.dropdownIsSelectable(selected, $el),
fieldName: $btn.data('fieldName'),
diff --git a/app/assets/javascripts/merge_request.js b/app/assets/javascripts/merge_request.js
index 8322d36faee..79a4c3700ef 100644
--- a/app/assets/javascripts/merge_request.js
+++ b/app/assets/javascripts/merge_request.js
@@ -66,6 +66,14 @@ MergeRequest.prototype.showAllCommits = function() {
MergeRequest.prototype.initMRBtnListeners = function() {
const _this = this;
+
+ $('.report-abuse-link').on('click', e => {
+ // this is needed because of the implementation of
+ // the dropdown toggle and Report Abuse needing to be
+ // linked to another page.
+ e.stopPropagation();
+ });
+
return $('.btn-close, .btn-reopen').on('click', function(e) {
const $this = $(this);
const shouldSubmit = $this.hasClass('btn-comment');
diff --git a/app/assets/javascripts/merge_request_tabs.js b/app/assets/javascripts/merge_request_tabs.js
index 94b6ba7b1ce..b7cf39db00c 100644
--- a/app/assets/javascripts/merge_request_tabs.js
+++ b/app/assets/javascripts/merge_request_tabs.js
@@ -1,6 +1,7 @@
/* eslint-disable no-new, class-methods-use-this */
import $ from 'jquery';
+import 'vendor/jquery.scrollTo';
import { GlBreakpointInstance as bp } from '@gitlab/ui/dist/utils';
import Cookies from 'js-cookie';
import createEventHub from '~/helpers/event_hub_factory';
diff --git a/app/assets/javascripts/milestone_select.js b/app/assets/javascripts/milestone_select.js
index caa45184bfc..52f6786ca28 100644
--- a/app/assets/javascripts/milestone_select.js
+++ b/app/assets/javascripts/milestone_select.js
@@ -4,10 +4,11 @@
import $ from 'jquery';
import { template, escape } from 'lodash';
-import { __ } from '~/locale';
-import '~/gl_dropdown';
+import { __, sprintf } from '~/locale';
+import initDeprecatedJQueryDropdown from '~/deprecated_jquery_dropdown';
+import Api from '~/api';
import axios from './lib/utils/axios_utils';
-import { timeFor } from './lib/utils/datetime_utility';
+import { timeFor, parsePikadayDate, dateInWords } from './lib/utils/datetime_utility';
import ModalStore from './boards/stores/modal_store';
import boardsStore, {
boardStoreIssueSet,
@@ -34,10 +35,10 @@ export default class MilestoneSelect {
$els.each((i, dropdown) => {
let milestoneLinkNoneTemplate,
milestoneLinkTemplate,
+ milestoneExpiredLinkTemplate,
selectedMilestone,
selectedMilestoneDefault;
const $dropdown = $(dropdown);
- const milestonesUrl = $dropdown.data('milestones');
const issueUpdateURL = $dropdown.data('issueUpdate');
const showNo = $dropdown.data('showNo');
const showAny = $dropdown.data('showAny');
@@ -63,59 +64,109 @@ export default class MilestoneSelect {
milestoneLinkTemplate = template(
'<a href="<%- web_url %>" class="bold has-tooltip" data-container="body" title="<%- remaining %>"><%- title %></a>',
);
+ milestoneExpiredLinkTemplate = template(
+ '<a href="<%- web_url %>" class="bold has-tooltip" data-container="body" title="<%- remaining %>"><%- title %> (Past due)</a>',
+ );
milestoneLinkNoneTemplate = `<span class="no-value">${__('None')}</span>`;
}
- return $dropdown.glDropdown({
+ return initDeprecatedJQueryDropdown($dropdown, {
showMenuAbove,
- data: (term, callback) =>
- axios.get(milestonesUrl).then(({ data }) => {
- const extraOptions = [];
- if (showAny) {
- extraOptions.push({
- id: null,
- name: null,
- title: __('Any milestone'),
- });
- }
- if (showNo) {
- extraOptions.push({
- id: -1,
- name: __('No milestone'),
- title: __('No milestone'),
- });
- }
- if (showUpcoming) {
- extraOptions.push({
- id: -2,
- name: '#upcoming',
- title: __('Upcoming'),
- });
- }
- if (showStarted) {
- extraOptions.push({
- id: -3,
- name: '#started',
- title: __('Started'),
- });
- }
- if (extraOptions.length) {
- extraOptions.push({ type: 'divider' });
- }
+ data: (term, callback) => {
+ let contextId = parseInt($dropdown.get(0).dataset.projectId, 10);
+ let getMilestones = Api.projectMilestones.bind(Api);
+ const reqParams = { state: 'active', include_parent_milestones: true };
- callback(extraOptions.concat(data));
- if (showMenuAbove) {
- $dropdown.data('glDropdown').positionMenuAbove();
- }
- $(`[data-milestone-id="${escape(selectedMilestone)}"] > a`).addClass('is-active');
- }),
- renderRow: milestone => `
- <li data-milestone-id="${escape(milestone.name)}">
+ if (term) {
+ reqParams.search = term.trim();
+ }
+
+ if (!contextId) {
+ contextId = $dropdown.get(0).dataset.groupId;
+ delete reqParams.include_parent_milestones;
+ getMilestones = Api.groupMilestones.bind(Api);
+ }
+
+ // We don't use $.data() as it caches initial value and never updates!
+ return getMilestones(contextId, reqParams)
+ .then(({ data }) =>
+ data
+ .map(m => ({
+ ...m,
+ // Public API includes `title` instead of `name`.
+ name: m.title,
+ }))
+ .sort((mA, mB) => {
+ // Move all expired milestones to the bottom.
+ if (mA.expired) {
+ return 1;
+ }
+ if (mB.expired) {
+ return -1;
+ }
+ return 0;
+ }),
+ )
+ .then(data => {
+ const extraOptions = [];
+ if (showAny) {
+ extraOptions.push({
+ id: null,
+ name: null,
+ title: __('Any milestone'),
+ });
+ }
+ if (showNo) {
+ extraOptions.push({
+ id: -1,
+ name: __('No milestone'),
+ title: __('No milestone'),
+ });
+ }
+ if (showUpcoming) {
+ extraOptions.push({
+ id: -2,
+ name: '#upcoming',
+ title: __('Upcoming'),
+ });
+ }
+ if (showStarted) {
+ extraOptions.push({
+ id: -3,
+ name: '#started',
+ title: __('Started'),
+ });
+ }
+ if (extraOptions.length) {
+ extraOptions.push({ type: 'divider' });
+ }
+
+ callback(extraOptions.concat(data));
+ if (showMenuAbove) {
+ $dropdown.data('deprecatedJQueryDropdown').positionMenuAbove();
+ }
+ $(`[data-milestone-id="${selectedMilestone}"] > a`).addClass('is-active');
+ });
+ },
+ renderRow: milestone => {
+ const milestoneName = milestone.title || milestone.name;
+ let milestoneDisplayName = escape(milestoneName);
+
+ if (milestone.expired) {
+ milestoneDisplayName = sprintf(__('%{milestone} (expired)'), {
+ milestone: milestoneDisplayName,
+ });
+ }
+
+ return `
+ <li data-milestone-id="${escape(milestoneName)}">
<a href='#' class='dropdown-menu-milestone-link'>
- ${escape(milestone.title)}
+ ${milestoneDisplayName}
</a>
</li>
- `,
+ `;
+ },
filterable: true,
+ filterRemote: true,
search: {
fields: ['title'],
},
@@ -149,7 +200,7 @@ export default class MilestoneSelect {
selectedMilestone = $dropdown[0].dataset.selected || selectedMilestoneDefault;
}
$('a.is-active', $el).removeClass('is-active');
- $(`[data-milestone-id="${escape(selectedMilestone)}"] > a`, $el).addClass('is-active');
+ $(`[data-milestone-id="${selectedMilestone}"] > a`, $el).addClass('is-active');
},
vue: $dropdown.hasClass('js-issue-board-sidebar'),
clicked: clickEvent => {
@@ -237,7 +288,16 @@ export default class MilestoneSelect {
if (data.milestone != null) {
data.milestone.remaining = timeFor(data.milestone.due_date);
data.milestone.name = data.milestone.title;
- $value.html(milestoneLinkTemplate(data.milestone));
+ $value.html(
+ data.milestone.expired
+ ? milestoneExpiredLinkTemplate({
+ ...data.milestone,
+ remaining: sprintf(__('%{due_date} (Past due)'), {
+ due_date: dateInWords(parsePikadayDate(data.milestone.due_date)),
+ }),
+ })
+ : milestoneLinkTemplate(data.milestone),
+ );
return $sidebarCollapsedValue
.attr(
'data-original-title',
diff --git a/app/assets/javascripts/milestones/project_milestone_combobox.vue b/app/assets/javascripts/milestones/project_milestone_combobox.vue
index d0179ab5509..5ee917573ce 100644
--- a/app/assets/javascripts/milestones/project_milestone_combobox.vue
+++ b/app/assets/javascripts/milestones/project_milestone_combobox.vue
@@ -1,9 +1,9 @@
<script>
import {
- GlNewDropdown,
- GlNewDropdownDivider,
- GlNewDropdownHeader,
- GlNewDropdownItem,
+ GlDropdown,
+ GlDropdownDivider,
+ GlDropdownSectionHeader,
+ GlDropdownItem,
GlLoadingIcon,
GlSearchBoxByType,
GlIcon,
@@ -13,12 +13,14 @@ import { __, sprintf } from '~/locale';
import Api from '~/api';
import { deprecatedCreateFlash as createFlash } from '~/flash';
+const SEARCH_DEBOUNCE_MS = 250;
+
export default {
components: {
- GlNewDropdown,
- GlNewDropdownDivider,
- GlNewDropdownHeader,
- GlNewDropdownItem,
+ GlDropdown,
+ GlDropdownDivider,
+ GlDropdownSectionHeader,
+ GlDropdownItem,
GlLoadingIcon,
GlSearchBoxByType,
GlIcon,
@@ -89,10 +91,21 @@ export default {
return this.requestCount !== 0;
},
},
+ created() {
+ // This method is defined here instead of in `methods`
+ // because we need to access the .cancel() method
+ // lodash attaches to the function, which is
+ // made inaccessible by Vue. More info:
+ // https://stackoverflow.com/a/52988020/1063392
+ this.debouncedSearchMilestones = debounce(this.searchMilestones, SEARCH_DEBOUNCE_MS);
+ },
mounted() {
this.fetchMilestones();
},
methods: {
+ focusSearchBox() {
+ this.$refs.searchBox.$el.querySelector('input').focus();
+ },
fetchMilestones() {
this.requestCount += 1;
@@ -108,7 +121,7 @@ export default {
this.requestCount -= 1;
});
},
- searchMilestones: debounce(function searchMilestones() {
+ searchMilestones() {
this.requestCount += 1;
const options = {
search: this.searchQuery,
@@ -133,7 +146,14 @@ export default {
.finally(() => {
this.requestCount -= 1;
});
- }, 100),
+ },
+ onSearchBoxInput() {
+ this.debouncedSearchMilestones();
+ },
+ onSearchBoxEnter() {
+ this.debouncedSearchMilestones.cancel();
+ this.searchMilestones();
+ },
toggleMilestoneSelection(clickedMilestone) {
if (!clickedMilestone) return [];
@@ -168,7 +188,7 @@ export default {
</script>
<template>
- <gl-new-dropdown>
+ <gl-dropdown v-bind="$attrs" class="project-milestone-combobox" @shown="focusSearchBox">
<template slot="button-content">
<span ref="buttonText" class="flex-grow-1 ml-1 text-muted">{{
selectedMilestonesLabel
@@ -176,39 +196,41 @@ export default {
<gl-icon name="chevron-down" />
</template>
- <gl-new-dropdown-header>
+ <gl-dropdown-section-header>
<span class="text-center d-block">{{ $options.translations.selectMilestone }}</span>
- </gl-new-dropdown-header>
+ </gl-dropdown-section-header>
- <gl-new-dropdown-divider />
+ <gl-dropdown-divider />
<gl-search-box-by-type
+ ref="searchBox"
v-model.trim="searchQuery"
- class="m-2"
+ class="gl-m-3"
:placeholder="this.$options.translations.searchMilestones"
- @input="searchMilestones"
+ @input="onSearchBoxInput"
+ @keydown.enter.prevent="onSearchBoxEnter"
/>
- <gl-new-dropdown-item @click="onMilestoneClicked(null)">
+ <gl-dropdown-item @click="onMilestoneClicked(null)">
<span :class="{ 'pl-4': true, 'selected-item': selectedMilestones.length === 0 }">
{{ $options.translations.noMilestone }}
</span>
- </gl-new-dropdown-item>
+ </gl-dropdown-item>
- <gl-new-dropdown-divider />
+ <gl-dropdown-divider />
<template v-if="isLoading">
<gl-loading-icon />
- <gl-new-dropdown-divider />
+ <gl-dropdown-divider />
</template>
<template v-else-if="noResults">
<div class="dropdown-item-space">
<span ref="noResults" class="pl-4">{{ $options.translations.noResultsLabel }}</span>
</div>
- <gl-new-dropdown-divider />
+ <gl-dropdown-divider />
</template>
<template v-else-if="dropdownItems.length">
- <gl-new-dropdown-item
+ <gl-dropdown-item
v-for="item in dropdownItems"
:key="item"
role="milestone option"
@@ -217,12 +239,12 @@ export default {
<span :class="{ 'pl-4': true, 'selected-item': isSelectedMilestone(item) }">
{{ item }}
</span>
- </gl-new-dropdown-item>
- <gl-new-dropdown-divider />
+ </gl-dropdown-item>
+ <gl-dropdown-divider />
</template>
- <gl-new-dropdown-item v-for="(item, idx) in extraLinks" :key="idx" :href="item.url">
+ <gl-dropdown-item v-for="(item, idx) in extraLinks" :key="idx" :href="item.url">
<span class="pl-4">{{ item.text }}</span>
- </gl-new-dropdown-item>
- </gl-new-dropdown>
+ </gl-dropdown-item>
+ </gl-dropdown>
</template>
diff --git a/app/assets/javascripts/monitoring/components/alert_widget.vue b/app/assets/javascripts/monitoring/components/alert_widget.vue
index 909ae2980d2..8f9c181258f 100644
--- a/app/assets/javascripts/monitoring/components/alert_widget.vue
+++ b/app/assets/javascripts/monitoring/components/alert_widget.vue
@@ -236,7 +236,7 @@ export default {
>
<gl-badge :variant="isFiring ? 'danger' : 'neutral'" class="d-flex-center text-truncate">
<gl-icon name="warning" :size="16" class="flex-shrink-0" />
- <span class="text-truncate gl-pl-1-deprecated-no-really-do-not-use-me">
+ <span class="text-truncate gl-pl-2">
<gl-sprintf
:message="
hasMultipleAlerts ? multipleAlertsSummary.message : singleAlertSummary.message
diff --git a/app/assets/javascripts/monitoring/components/alert_widget_form.vue b/app/assets/javascripts/monitoring/components/alert_widget_form.vue
index 5fa0da53a04..132df9c9516 100644
--- a/app/assets/javascripts/monitoring/components/alert_widget_form.vue
+++ b/app/assets/javascripts/monitoring/components/alert_widget_form.vue
@@ -7,16 +7,16 @@ import {
GlButtonGroup,
GlFormGroup,
GlFormInput,
- GlNewDropdown as GlDropdown,
- GlNewDropdownItem as GlDropdownItem,
+ GlDropdown,
+ GlDropdownItem,
GlModal,
GlTooltipDirective,
+ GlIcon,
} from '@gitlab/ui';
import { __, s__ } from '~/locale';
import Translate from '~/vue_shared/translate';
import TrackEventDirective from '~/vue_shared/directives/track_event';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
-import Icon from '~/vue_shared/components/icon.vue';
import { alertsValidator, queriesValidator } from '../validators';
import { OPERATORS } from '../constants';
@@ -44,7 +44,7 @@ export default {
GlDropdownItem,
GlModal,
GlLink,
- Icon,
+ GlIcon,
},
directives: {
GlTooltip: GlTooltipDirective,
@@ -242,7 +242,7 @@ export default {
<template #description>
<div class="d-flex align-items-center">
{{ __('Single or combined queries') }}
- <icon
+ <gl-icon
v-gl-tooltip="$options.alertQueryText.descriptionTooltip"
name="question"
class="gl-ml-2"
diff --git a/app/assets/javascripts/monitoring/components/charts/empty_chart.vue b/app/assets/javascripts/monitoring/components/charts/empty_chart.vue
index ad176637538..446ca8e5090 100644
--- a/app/assets/javascripts/monitoring/components/charts/empty_chart.vue
+++ b/app/assets/javascripts/monitoring/components/charts/empty_chart.vue
@@ -1,4 +1,5 @@
<script>
+/* eslint-disable vue/no-v-html */
import chartEmptyStateIllustration from '@gitlab/svgs/dist/illustrations/chart-empty-state.svg';
import { chartHeight } from '../../constants';
diff --git a/app/assets/javascripts/monitoring/components/charts/time_series.vue b/app/assets/javascripts/monitoring/components/charts/time_series.vue
index 054111c203e..6bae3fdcc2e 100644
--- a/app/assets/javascripts/monitoring/components/charts/time_series.vue
+++ b/app/assets/javascripts/monitoring/components/charts/time_series.vue
@@ -1,10 +1,9 @@
<script>
import { isEmpty, omit, throttle } from 'lodash';
-import { GlLink, GlTooltip, GlResizeObserverDirective } from '@gitlab/ui';
+import { GlLink, GlTooltip, GlResizeObserverDirective, GlIcon } from '@gitlab/ui';
import { GlAreaChart, GlLineChart, GlChartSeriesLabel } from '@gitlab/ui/dist/charts';
import { s__ } from '~/locale';
import { getSvgIconPathContent } from '~/lib/utils/icon_utils';
-import Icon from '~/vue_shared/components/icon.vue';
import { panelTypes, chartHeight, lineTypes, lineWidths, legendLayoutTypes } from '../../constants';
import { getYAxisOptions, getTimeAxisOptions, getChartGrid, getTooltipFormatter } from './options';
import { annotationsYAxis, generateAnnotationsSeries } from './annotations';
@@ -27,7 +26,7 @@ export default {
GlTooltip,
GlChartSeriesLabel,
GlLink,
- Icon,
+ GlIcon,
},
directives: {
GlResizeObserverDirective,
@@ -407,7 +406,7 @@ export default {
{{ __('Deployed') }}
</template>
<div slot="tooltipContent" class="d-flex align-items-center">
- <icon name="commit" class="mr-2" />
+ <gl-icon name="commit" class="mr-2" />
<gl-link :href="tooltip.commitUrl">{{ tooltip.sha }}</gl-link>
</div>
</template>
diff --git a/app/assets/javascripts/monitoring/components/dashboard.vue b/app/assets/javascripts/monitoring/components/dashboard.vue
index 24aa7b3f504..cbfacd73b5b 100644
--- a/app/assets/javascripts/monitoring/components/dashboard.vue
+++ b/app/assets/javascripts/monitoring/components/dashboard.vue
@@ -2,7 +2,7 @@
import { mapActions, mapState, mapGetters } from 'vuex';
import VueDraggable from 'vuedraggable';
import Mousetrap from 'mousetrap';
-import { GlButton, GlModalDirective, GlTooltipDirective } from '@gitlab/ui';
+import { GlButton, GlModalDirective, GlTooltipDirective, GlIcon } from '@gitlab/ui';
import DashboardHeader from './dashboard_header.vue';
import DashboardPanel from './dashboard_panel.vue';
import { s__ } from '~/locale';
@@ -10,7 +10,6 @@ import { deprecatedCreateFlash as createFlash } from '~/flash';
import { ESC_KEY } from '~/lib/utils/keys';
import { mergeUrlParams, updateHistory } from '~/lib/utils/url_utility';
import invalidUrl from '~/lib/utils/invalid_url';
-import Icon from '~/vue_shared/components/icon.vue';
import GraphGroup from './graph_group.vue';
import EmptyState from './empty_state.vue';
@@ -33,7 +32,7 @@ export default {
VueDraggable,
DashboardHeader,
DashboardPanel,
- Icon,
+ GlIcon,
GlButton,
GraphGroup,
EmptyState,
@@ -473,7 +472,7 @@ export default {
@click="removePanel(groupData.key, groupData.panels, graphIndex)"
>
<a class="mx-2 p-2 draggable-remove-link" :aria-label="__('Remove')">
- <icon name="close" />
+ <gl-icon name="close" />
</a>
</div>
diff --git a/app/assets/javascripts/monitoring/components/dashboard_actions_menu.vue b/app/assets/javascripts/monitoring/components/dashboard_actions_menu.vue
index 68afa2ace01..070277fe2dc 100644
--- a/app/assets/javascripts/monitoring/components/dashboard_actions_menu.vue
+++ b/app/assets/javascripts/monitoring/components/dashboard_actions_menu.vue
@@ -2,9 +2,9 @@
import { mapState, mapGetters, mapActions } from 'vuex';
import {
GlDeprecatedButton,
- GlNewDropdown,
- GlNewDropdownDivider,
- GlNewDropdownItem,
+ GlDropdown,
+ GlDropdownDivider,
+ GlDropdownItem,
GlModal,
GlIcon,
GlModalDirective,
@@ -23,9 +23,9 @@ import { getAddMetricTrackingOptions } from '../utils';
export default {
components: {
GlDeprecatedButton,
- GlNewDropdown,
- GlNewDropdownDivider,
- GlNewDropdownItem,
+ GlDropdown,
+ GlDropdownDivider,
+ GlDropdownItem,
GlModal,
GlIcon,
DuplicateDashboardModal,
@@ -143,7 +143,7 @@ export default {
as part of https://gitlab.com/gitlab-org/gitlab-ui/-/issues/936
The variant will create a dropdown with an icon, no text and no caret
-->
- <gl-new-dropdown
+ <gl-dropdown
v-gl-tooltip
data-testid="actions-menu"
data-qa-selector="actions_menu_dropdown"
@@ -157,13 +157,13 @@ export default {
</template>
<template v-if="addingMetricsAvailable">
- <gl-new-dropdown-item
+ <gl-dropdown-item
v-gl-modal="$options.modalIds.addMetric"
data-qa-selector="add_metric_button"
data-testid="add-metric-item"
>
{{ $options.i18n.addMetric }}
- </gl-new-dropdown-item>
+ </gl-dropdown-item>
<gl-modal
ref="addMetricModal"
:modal-id="$options.modalIds.addMetric"
@@ -194,20 +194,20 @@ export default {
</gl-modal>
</template>
- <gl-new-dropdown-item
+ <gl-dropdown-item
v-if="isMenuItemEnabled.addPanel"
data-testid="add-panel-item-enabled"
:to="newPanelPageLocation"
>
{{ $options.i18n.addPanel }}
- </gl-new-dropdown-item>
+ </gl-dropdown-item>
<!--
wrapper for tooltip as button can be `disabled`
https://bootstrap-vue.org/docs/components/tooltip#disabled-elements
-->
<div v-else v-gl-tooltip :title="$options.i18n.addPanelInfo">
- <gl-new-dropdown-item
+ <gl-dropdown-item
:alt="$options.i18n.addPanelInfo"
:to="newPanelPageLocation"
data-testid="add-panel-item-disabled"
@@ -215,24 +215,24 @@ export default {
class="gl-cursor-not-allowed"
>
<span class="gl-text-gray-400">{{ $options.i18n.addPanel }}</span>
- </gl-new-dropdown-item>
+ </gl-dropdown-item>
</div>
- <gl-new-dropdown-item
+ <gl-dropdown-item
v-if="isMenuItemEnabled.editDashboard"
:href="selectedDashboard ? selectedDashboard.project_blob_path : null"
data-qa-selector="edit_dashboard_button_enabled"
data-testid="edit-dashboard-item-enabled"
>
{{ $options.i18n.editDashboard }}
- </gl-new-dropdown-item>
+ </gl-dropdown-item>
<!--
wrapper for tooltip as button can be `disabled`
https://bootstrap-vue.org/docs/components/tooltip#disabled-elements
-->
<div v-else v-gl-tooltip :title="$options.i18n.editDashboardInfo">
- <gl-new-dropdown-item
+ <gl-dropdown-item
:alt="$options.i18n.editDashboardInfo"
:href="selectedDashboard ? selectedDashboard.project_blob_path : null"
data-testid="edit-dashboard-item-disabled"
@@ -240,16 +240,16 @@ export default {
class="gl-cursor-not-allowed"
>
<span class="gl-text-gray-400">{{ $options.i18n.editDashboard }}</span>
- </gl-new-dropdown-item>
+ </gl-dropdown-item>
</div>
<template v-if="isMenuItemShown.duplicateDashboard">
- <gl-new-dropdown-item
+ <gl-dropdown-item
v-gl-modal="$options.modalIds.duplicateDashboard"
data-testid="duplicate-dashboard-item"
>
{{ $options.i18n.duplicateDashboard }}
- </gl-new-dropdown-item>
+ </gl-dropdown-item>
<duplicate-dashboard-modal
:default-branch="defaultBranch"
@@ -259,25 +259,25 @@ export default {
/>
</template>
- <gl-new-dropdown-item
+ <gl-dropdown-item
v-if="selectedDashboard"
data-testid="star-dashboard-item"
:disabled="isUpdatingStarredValue"
@click="toggleStarredValue()"
>
{{ selectedDashboard.starred ? $options.i18n.unstarDashboard : $options.i18n.starDashboard }}
- </gl-new-dropdown-item>
+ </gl-dropdown-item>
- <gl-new-dropdown-divider />
+ <gl-dropdown-divider />
- <gl-new-dropdown-item
+ <gl-dropdown-item
v-gl-modal="$options.modalIds.createDashboard"
data-testid="create-dashboard-item"
:disabled="!isMenuItemEnabled.createDashboard"
:class="{ 'monitoring-actions-item-disabled': !isMenuItemEnabled.createDashboard }"
>
{{ $options.i18n.createDashboard }}
- </gl-new-dropdown-item>
+ </gl-dropdown-item>
<template v-if="isMenuItemEnabled.createDashboard">
<create-dashboard-modal
@@ -287,5 +287,5 @@ export default {
:project-path="projectPath"
/>
</template>
- </gl-new-dropdown>
+ </gl-dropdown>
</template>
diff --git a/app/assets/javascripts/monitoring/components/dashboard_header.vue b/app/assets/javascripts/monitoring/components/dashboard_header.vue
index 6a7bf81c643..e468728a954 100644
--- a/app/assets/javascripts/monitoring/components/dashboard_header.vue
+++ b/app/assets/javascripts/monitoring/components/dashboard_header.vue
@@ -3,17 +3,17 @@ import { debounce } from 'lodash';
import { mapActions, mapState, mapGetters } from 'vuex';
import {
GlButton,
- GlNewDropdown,
+ GlDropdown,
GlLoadingIcon,
- GlNewDropdownItem,
- GlNewDropdownHeader,
+ GlDropdownItem,
+ GlDropdownSectionHeader,
GlSearchBoxByType,
GlModalDirective,
GlTooltipDirective,
+ GlIcon,
} from '@gitlab/ui';
import { mergeUrlParams, redirectTo } from '~/lib/utils/url_utility';
import invalidUrl from '~/lib/utils/invalid_url';
-import Icon from '~/vue_shared/components/icon.vue';
import DateTimePicker from '~/vue_shared/components/date_time_picker/date_time_picker.vue';
import DashboardsDropdown from './dashboards_dropdown.vue';
@@ -26,12 +26,12 @@ import { timezones } from '../format_date';
export default {
components: {
- Icon,
+ GlIcon,
GlButton,
- GlNewDropdown,
+ GlDropdown,
GlLoadingIcon,
- GlNewDropdownItem,
- GlNewDropdownHeader,
+ GlDropdownItem,
+ GlDropdownSectionHeader,
GlSearchBoxByType,
@@ -181,7 +181,7 @@ export default {
<span aria-hidden="true" class="gl-pl-3 border-left gl-mb-3 d-none d-sm-block"></span>
<div class="mb-2 pr-2 d-flex d-sm-block">
- <gl-new-dropdown
+ <gl-dropdown
id="monitor-environments-dropdown"
ref="monitorEnvironmentsDropdown"
class="flex-grow-1"
@@ -191,12 +191,12 @@ export default {
:text="environmentDropdownText"
>
<div class="d-flex flex-column overflow-hidden">
- <gl-new-dropdown-header>{{ __('Environment') }}</gl-new-dropdown-header>
- <gl-search-box-by-type class="m-2" @input="debouncedEnvironmentsSearch" />
+ <gl-dropdown-section-header>{{ __('Environment') }}</gl-dropdown-section-header>
+ <gl-search-box-by-type class="gl-m-3" @input="debouncedEnvironmentsSearch" />
<gl-loading-icon v-if="environmentsLoading" :inline="true" />
<div v-else class="flex-fill overflow-auto">
- <gl-new-dropdown-item
+ <gl-dropdown-item
v-for="environment in filteredEnvironments"
:key="environment.id"
:is-check-item="true"
@@ -204,7 +204,7 @@ export default {
:href="getEnvironmentPath(environment.id)"
>
{{ environment.name }}
- </gl-new-dropdown-item>
+ </gl-dropdown-item>
</div>
<div
v-show="shouldShowEnvironmentsDropdownNoMatchedMsg"
@@ -214,7 +214,7 @@ export default {
{{ __('No matching results') }}
</div>
</div>
- </gl-new-dropdown>
+ </gl-dropdown>
</div>
<div class="mb-2 pr-2 d-flex d-sm-block">
@@ -260,7 +260,7 @@ export default {
target="_blank"
rel="noopener noreferrer"
>
- {{ __('View full dashboard') }} <icon name="external-link" />
+ {{ __('View full dashboard') }} <gl-icon name="external-link" />
</gl-button>
</div>
diff --git a/app/assets/javascripts/monitoring/components/dashboard_panel.vue b/app/assets/javascripts/monitoring/components/dashboard_panel.vue
index 278858d3a94..18310f7c71e 100644
--- a/app/assets/javascripts/monitoring/components/dashboard_panel.vue
+++ b/app/assets/javascripts/monitoring/components/dashboard_panel.vue
@@ -6,9 +6,9 @@ import {
GlIcon,
GlLink,
GlLoadingIcon,
- GlNewDropdown as GlDropdown,
- GlNewDropdownItem as GlDropdownItem,
- GlNewDropdownDivider as GlDropdownDivider,
+ GlDropdown,
+ GlDropdownItem,
+ GlDropdownDivider,
GlModal,
GlModalDirective,
GlSprintf,
diff --git a/app/assets/javascripts/monitoring/components/dashboards_dropdown.vue b/app/assets/javascripts/monitoring/components/dashboards_dropdown.vue
index aed27b5ea51..932efeaaf0e 100644
--- a/app/assets/javascripts/monitoring/components/dashboards_dropdown.vue
+++ b/app/assets/javascripts/monitoring/components/dashboards_dropdown.vue
@@ -2,10 +2,10 @@
import { mapState, mapGetters } from 'vuex';
import {
GlIcon,
- GlNewDropdown,
- GlNewDropdownItem,
- GlNewDropdownHeader,
- GlNewDropdownDivider,
+ GlDropdown,
+ GlDropdownItem,
+ GlDropdownSectionHeader,
+ GlDropdownDivider,
GlSearchBoxByType,
GlModalDirective,
} from '@gitlab/ui';
@@ -17,10 +17,10 @@ const events = {
export default {
components: {
GlIcon,
- GlNewDropdown,
- GlNewDropdownItem,
- GlNewDropdownHeader,
- GlNewDropdownDivider,
+ GlDropdown,
+ GlDropdownItem,
+ GlDropdownSectionHeader,
+ GlDropdownDivider,
GlSearchBoxByType,
},
directives: {
@@ -73,21 +73,21 @@ export default {
};
</script>
<template>
- <gl-new-dropdown
+ <gl-dropdown
toggle-class="dropdown-menu-toggle"
menu-class="monitor-dashboard-dropdown-menu"
:text="selectedDashboardText"
>
<div class="d-flex flex-column overflow-hidden">
- <gl-new-dropdown-header>{{ __('Dashboard') }}</gl-new-dropdown-header>
+ <gl-dropdown-section-header>{{ __('Dashboard') }}</gl-dropdown-section-header>
<gl-search-box-by-type
ref="monitorDashboardsDropdownSearch"
v-model="searchTerm"
- class="m-2"
+ class="gl-m-3"
/>
<div class="flex-fill overflow-auto">
- <gl-new-dropdown-item
+ <gl-dropdown-item
v-for="dashboard in starredDashboards"
:key="dashboard.path"
:is-check-item="true"
@@ -95,28 +95,28 @@ export default {
@click="selectDashboard(dashboard)"
>
<div class="gl-display-flex">
- <div class="gl-flex-grow-1 gl-min-w-0">
- <div class="gl-word-break-all">
- {{ dashboardDisplayName(dashboard) }}
- </div>
- </div>
- <gl-icon class="text-muted gl-flex-shrink-0" name="star" />
+ <span class="gl-flex-grow-1 gl-min-w-0 gl-overflow-hidden gl-overflow-wrap-break">
+ {{ dashboardDisplayName(dashboard) }}
+ </span>
+ <gl-icon class="text-muted gl-flex-shrink-0 gl-ml-3 gl-align-self-center" name="star" />
</div>
- </gl-new-dropdown-item>
- <gl-new-dropdown-divider
+ </gl-dropdown-item>
+ <gl-dropdown-divider
v-if="starredDashboards.length && nonStarredDashboards.length"
ref="starredListDivider"
/>
- <gl-new-dropdown-item
+ <gl-dropdown-item
v-for="dashboard in nonStarredDashboards"
:key="dashboard.path"
:is-check-item="true"
:is-checked="dashboard.path === selectedDashboardPath"
@click="selectDashboard(dashboard)"
>
- {{ dashboardDisplayName(dashboard) }}
- </gl-new-dropdown-item>
+ <span class="gl-overflow-hidden gl-overflow-wrap-break">
+ {{ dashboardDisplayName(dashboard) }}
+ </span>
+ </gl-dropdown-item>
</div>
<div
@@ -127,5 +127,5 @@ export default {
{{ __('No matching results') }}
</div>
</div>
- </gl-new-dropdown>
+ </gl-dropdown>
</template>
diff --git a/app/assets/javascripts/monitoring/components/embeds/embed_group.vue b/app/assets/javascripts/monitoring/components/embeds/embed_group.vue
index b60c87fee82..f07483c34b8 100644
--- a/app/assets/javascripts/monitoring/components/embeds/embed_group.vue
+++ b/app/assets/javascripts/monitoring/components/embeds/embed_group.vue
@@ -1,14 +1,14 @@
<script>
import { mapState, mapActions, mapGetters } from 'vuex';
import sum from 'lodash/sum';
-import { GlDeprecatedButton, GlCard, GlIcon } from '@gitlab/ui';
+import { GlButton, GlCard, GlIcon } from '@gitlab/ui';
import { n__ } from '~/locale';
import { monitoringDashboard } from '~/monitoring/stores';
import MetricEmbed from './metric_embed.vue';
export default {
components: {
- GlDeprecatedButton,
+ GlButton,
GlCard,
GlIcon,
MetricEmbed,
@@ -78,15 +78,16 @@ export default {
:body-class="bodyClass"
>
<template #header>
- <gl-deprecated-button
- class="collapsible-card-btn d-flex text-decoration-none"
+ <gl-button
+ class="collapsible-card-btn gl-display-flex gl-text-decoration-none gl-reset-color! gl-hover-text-blue-800! gl-shadow-none!"
:aria-label="buttonLabel"
variant="link"
+ category="tertiary"
@click="toggleCollapsed"
>
<gl-icon class="mr-1" :name="arrowIconName" />
{{ buttonLabel }}
- </gl-deprecated-button>
+ </gl-button>
</template>
<div class="d-flex flex-wrap">
<metric-embed
diff --git a/app/assets/javascripts/monitoring/components/group_empty_state.vue b/app/assets/javascripts/monitoring/components/group_empty_state.vue
index 9cf492dd537..499823fae3f 100644
--- a/app/assets/javascripts/monitoring/components/group_empty_state.vue
+++ b/app/assets/javascripts/monitoring/components/group_empty_state.vue
@@ -1,4 +1,5 @@
<script>
+/* eslint-disable vue/no-v-html */
import { GlEmptyState } from '@gitlab/ui';
import { __, sprintf } from '~/locale';
import { metricStates } from '../constants';
diff --git a/app/assets/javascripts/monitoring/components/refresh_button.vue b/app/assets/javascripts/monitoring/components/refresh_button.vue
index 0e9605450ed..e0d9f92440b 100644
--- a/app/assets/javascripts/monitoring/components/refresh_button.vue
+++ b/app/assets/javascripts/monitoring/components/refresh_button.vue
@@ -4,9 +4,9 @@ import { mapActions } from 'vuex';
import {
GlButtonGroup,
GlButton,
- GlNewDropdown,
- GlNewDropdownItem,
- GlNewDropdownDivider,
+ GlDropdown,
+ GlDropdownItem,
+ GlDropdownDivider,
GlTooltipDirective,
} from '@gitlab/ui';
import { n__, __ } from '~/locale';
@@ -48,9 +48,9 @@ export default {
components: {
GlButtonGroup,
GlButton,
- GlNewDropdown,
- GlNewDropdownItem,
- GlNewDropdownDivider,
+ GlDropdown,
+ GlDropdownItem,
+ GlDropdownDivider,
},
directives: {
GlTooltip: GlTooltipDirective,
@@ -152,27 +152,27 @@ export default {
icon="retry"
@click="refresh"
/>
- <gl-new-dropdown
+ <gl-dropdown
v-if="!disableMetricDashboardRefreshRate"
v-gl-tooltip
:title="s__('Metrics|Set refresh rate')"
:text="dropdownText"
>
- <gl-new-dropdown-item
+ <gl-dropdown-item
:is-check-item="true"
:is-checked="refreshInterval === null"
@click="removeRefreshInterval()"
- >{{ __('Off') }}</gl-new-dropdown-item
+ >{{ __('Off') }}</gl-dropdown-item
>
- <gl-new-dropdown-divider />
- <gl-new-dropdown-item
+ <gl-dropdown-divider />
+ <gl-dropdown-item
v-for="(option, i) in $options.refreshIntervals"
:key="i"
:is-check-item="true"
:is-checked="isChecked(option)"
@click="setRefreshInterval(option)"
- >{{ option.label }}</gl-new-dropdown-item
+ >{{ option.label }}</gl-dropdown-item
>
- </gl-new-dropdown>
+ </gl-dropdown>
</gl-button-group>
</template>
diff --git a/app/assets/javascripts/monitoring/components/variables_section.vue b/app/assets/javascripts/monitoring/components/variables_section.vue
index 25d900b07ad..7c4fb135ec8 100644
--- a/app/assets/javascripts/monitoring/components/variables_section.vue
+++ b/app/assets/javascripts/monitoring/components/variables_section.vue
@@ -37,7 +37,11 @@ export default {
};
</script>
<template>
- <div ref="variablesSection" class="d-sm-flex flex-sm-wrap pt-2 pr-1 pb-0 pl-2 variables-section">
+ <div
+ ref="variablesSection"
+ class="d-sm-flex flex-sm-wrap pt-2 pr-1 pb-0 pl-2 variables-section"
+ data-qa-selector="variables_content"
+ >
<div v-for="variable in variables" :key="variable.name" class="mb-1 pr-2 d-flex d-sm-block">
<component
:is="variableField(variable.type)"
@@ -46,6 +50,7 @@ export default {
:value="variable.value"
:name="variable.name"
:options="variable.options"
+ data-qa-selector="variable_item"
@input="refreshDashboard(variable, $event)"
/>
</div>
diff --git a/app/assets/javascripts/monitoring/csv_export.js b/app/assets/javascripts/monitoring/csv_export.js
index 734e8dc07a7..20cfa23e9b4 100644
--- a/app/assets/javascripts/monitoring/csv_export.js
+++ b/app/assets/javascripts/monitoring/csv_export.js
@@ -125,7 +125,6 @@ const csvData = (metricHeaders, metricValues) => {
* @param {Object} graphData - Panel contents
* @returns {String}
*/
-// eslint-disable-next-line import/prefer-default-export
export const graphDataToCsv = graphData => {
const delimiter = ',';
const br = '\r\n';
diff --git a/app/assets/javascripts/monitoring/stores/embed_group/actions.js b/app/assets/javascripts/monitoring/stores/embed_group/actions.js
index 4a7572bdbd9..ca0d2e5ba35 100644
--- a/app/assets/javascripts/monitoring/stores/embed_group/actions.js
+++ b/app/assets/javascripts/monitoring/stores/embed_group/actions.js
@@ -1,4 +1,3 @@
import * as types from './mutation_types';
-// eslint-disable-next-line import/prefer-default-export
export const addModule = ({ commit }, data) => commit(types.ADD_MODULE, data);
diff --git a/app/assets/javascripts/monitoring/stores/embed_group/getters.js b/app/assets/javascripts/monitoring/stores/embed_group/getters.js
index 096d8d03096..47db787dea5 100644
--- a/app/assets/javascripts/monitoring/stores/embed_group/getters.js
+++ b/app/assets/javascripts/monitoring/stores/embed_group/getters.js
@@ -1,3 +1,2 @@
-// eslint-disable-next-line import/prefer-default-export
export const metricsWithData = (state, getters, rootState, rootGetters) =>
state.modules.map(module => rootGetters[`${module}/metricsWithData`]().length);
diff --git a/app/assets/javascripts/monitoring/stores/embed_group/mutation_types.js b/app/assets/javascripts/monitoring/stores/embed_group/mutation_types.js
index 7fd3f0f8647..288e6db4151 100644
--- a/app/assets/javascripts/monitoring/stores/embed_group/mutation_types.js
+++ b/app/assets/javascripts/monitoring/stores/embed_group/mutation_types.js
@@ -1,2 +1 @@
-// eslint-disable-next-line import/prefer-default-export
export const ADD_MODULE = 'ADD_MODULE';
diff --git a/app/assets/javascripts/mr_popover/components/mr_popover.vue b/app/assets/javascripts/mr_popover/components/mr_popover.vue
index e6bf7a6ec02..bf810978648 100644
--- a/app/assets/javascripts/mr_popover/components/mr_popover.vue
+++ b/app/assets/javascripts/mr_popover/components/mr_popover.vue
@@ -1,6 +1,6 @@
<script>
/* eslint-disable @gitlab/vue-require-i18n-strings */
-import { GlPopover, GlSkeletonLoading } from '@gitlab/ui';
+import { GlPopover, GlDeprecatedSkeletonLoading as GlSkeletonLoading } from '@gitlab/ui';
import CiIcon from '../../vue_shared/components/ci_icon.vue';
import timeagoMixin from '../../vue_shared/mixins/timeago';
import query from '../queries/merge_request.query.graphql';
diff --git a/app/assets/javascripts/namespace_select.js b/app/assets/javascripts/namespace_select.js
index b96a111cf13..8e123c14814 100644
--- a/app/assets/javascripts/namespace_select.js
+++ b/app/assets/javascripts/namespace_select.js
@@ -1,16 +1,16 @@
import $ from 'jquery';
-import '~/gl_dropdown';
import Api from './api';
import { mergeUrlParams } from './lib/utils/url_utility';
import { parseBoolean } from '~/lib/utils/common_utils';
import { __ } from './locale';
+import initDeprecatedJQueryDropdown from '~/deprecated_jquery_dropdown';
export default class NamespaceSelect {
constructor(opts) {
const isFilter = parseBoolean(opts.dropdown.dataset.isFilter);
const fieldName = opts.dropdown.dataset.fieldName || 'namespace_id';
- $(opts.dropdown).glDropdown({
+ initDeprecatedJQueryDropdown($(opts.dropdown), {
filterable: true,
selectable: true,
filterRemote: true,
diff --git a/app/assets/javascripts/notebook/cells/markdown.vue b/app/assets/javascripts/notebook/cells/markdown.vue
index fa1afdcd16f..3bbaa44ec42 100644
--- a/app/assets/javascripts/notebook/cells/markdown.vue
+++ b/app/assets/javascripts/notebook/cells/markdown.vue
@@ -1,4 +1,5 @@
<script>
+/* eslint-disable vue/no-v-html */
import marked from 'marked';
import { sanitize } from 'dompurify';
import katex from 'katex';
diff --git a/app/assets/javascripts/notebook/cells/output/html.vue b/app/assets/javascripts/notebook/cells/output/html.vue
index b36761993ea..856c8f31796 100644
--- a/app/assets/javascripts/notebook/cells/output/html.vue
+++ b/app/assets/javascripts/notebook/cells/output/html.vue
@@ -1,4 +1,5 @@
<script>
+/* eslint-disable vue/no-v-html */
import { sanitize } from 'dompurify';
import Prompt from '../prompt.vue';
diff --git a/app/assets/javascripts/notes.js b/app/assets/javascripts/notes.js
index 3940b4b4724..340fbe4d887 100644
--- a/app/assets/javascripts/notes.js
+++ b/app/assets/javascripts/notes.js
@@ -11,13 +11,12 @@ old_notes_spec.js is the spec for the legacy, jQuery notes application. It has n
*/
import $ from 'jquery';
+import '~/lib/utils/jquery_at_who';
import { escape, uniqueId } from 'lodash';
import Cookies from 'js-cookie';
import Autosize from 'autosize';
-import 'jquery.caret'; // required by at.js
-import '@gitlab/at.js';
import Vue from 'vue';
-import { GlSkeletonLoading } from '@gitlab/ui';
+import { GlDeprecatedSkeletonLoading as GlSkeletonLoading } from '@gitlab/ui';
import AjaxCache from '~/lib/utils/ajax_cache';
import syntaxHighlight from '~/syntax_highlight';
import axios from './lib/utils/axios_utils';
diff --git a/app/assets/javascripts/notes/components/comment_form.vue b/app/assets/javascripts/notes/components/comment_form.vue
index 7cfff98e9f7..54fcf41ca50 100644
--- a/app/assets/javascripts/notes/components/comment_form.vue
+++ b/app/assets/javascripts/notes/components/comment_form.vue
@@ -3,7 +3,7 @@ import $ from 'jquery';
import { mapActions, mapGetters, mapState } from 'vuex';
import { isEmpty } from 'lodash';
import Autosize from 'autosize';
-import { GlAlert, GlIntersperse, GlLink, GlSprintf } from '@gitlab/ui';
+import { GlAlert, GlIntersperse, GlLink, GlSprintf, GlButton } from '@gitlab/ui';
import { __, sprintf } from '~/locale';
import TimelineEntryItem from '~/vue_shared/components/notes/timeline_entry_item.vue';
import { deprecatedCreateFlash as Flash } from '../../flash';
@@ -20,7 +20,6 @@ import eventHub from '../event_hub';
import NoteableWarning from '../../vue_shared/components/notes/noteable_warning.vue';
import markdownField from '../../vue_shared/components/markdown/field.vue';
import userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue';
-import loadingButton from '../../vue_shared/components/loading_button.vue';
import noteSignedOutWidget from './note_signed_out_widget.vue';
import discussionLockedWidget from './discussion_locked_widget.vue';
import issuableStateMixin from '../mixins/issuable_state';
@@ -33,7 +32,7 @@ export default {
discussionLockedWidget,
markdownField,
userAvatarLink,
- loadingButton,
+ GlButton,
TimelineEntryItem,
GlAlert,
GlIntersperse,
@@ -102,6 +101,9 @@ export default {
noteable: this.noteableDisplayName,
});
},
+ buttonVariant() {
+ return this.isOpen ? 'warning' : 'default';
+ },
actionButtonClassNames() {
return {
'btn-reopen': !this.isOpen,
@@ -378,7 +380,7 @@ export default {
dir="auto"
:disabled="isSubmitting"
name="note[note]"
- class="note-textarea js-vue-comment-form js-note-text js-gfm-input js-autosize markdown-area js-vue-textarea qa-comment-input"
+ class="note-textarea js-vue-comment-form js-note-text js-gfm-input js-autosize markdown-area qa-comment-input"
data-supports-quick-actions="true"
:aria-label="__('Description')"
:placeholder="__('Write a comment or drag your files here…')"
@@ -395,7 +397,7 @@ export default {
:secondary-button-text="__('Cancel')"
variant="warning"
:dismissible="false"
- @primaryAction="forceCloseIssue"
+ @primaryAction="toggleBlockedIssueWarning(false) && forceCloseIssue()"
@secondaryAction="toggleBlockedIssueWarning(false) && enableButton()"
>
<p>
@@ -421,27 +423,28 @@ export default {
<div
class="btn-group gl-mr-3 comment-type-dropdown js-comment-type-dropdown droplab-dropdown"
>
- <button
+ <gl-button
:disabled="isSubmitButtonDisabled"
- class="btn btn-success js-comment-button js-comment-submit-button qa-comment-button"
+ class="js-comment-button js-comment-submit-button qa-comment-button"
type="submit"
+ category="primary"
+ variant="success"
:data-track-label="trackingLabel"
data-track-event="click_button"
@click.prevent="handleSave()"
+ >{{ commentButtonTitle }}</gl-button
>
- {{ commentButtonTitle }}
- </button>
- <button
+ <gl-button
:disabled="isSubmitButtonDisabled"
name="button"
- type="button"
- class="btn btn-success note-type-toggle js-note-new-discussion dropdown-toggle qa-note-dropdown"
+ category="primary"
+ variant="success"
+ class="note-type-toggle js-note-new-discussion dropdown-toggle qa-note-dropdown"
data-display="static"
data-toggle="dropdown"
+ icon="chevron-down"
:aria-label="__('Open comment type dropdown')"
- >
- <i aria-hidden="true" class="fa fa-caret-down toggle-icon"></i>
- </button>
+ />
<ul class="note-type-dropdown dropdown-open-top dropdown-menu">
<li :class="{ 'droplab-item-selected': noteType === 'comment' }">
@@ -465,11 +468,7 @@ export default {
</li>
<li class="divider droplab-item-ignore"></li>
<li :class="{ 'droplab-item-selected': noteType === 'discussion' }">
- <button
- type="button"
- class="btn btn-transparent qa-discussion-option"
- @click.prevent="setNoteType('discussion')"
- >
+ <button class="qa-discussion-option" @click.prevent="setNoteType('discussion')">
<i aria-hidden="true" class="fa fa-check icon"></i>
<div class="description">
<strong>{{ __('Start thread') }}</strong>
@@ -480,17 +479,19 @@ export default {
</ul>
</div>
- <loading-button
+ <gl-button
v-if="canToggleIssueState && !isToggleBlockedIssueWarning"
:loading="isToggleStateButtonLoading"
- :container-class="[
+ category="secondary"
+ :variant="buttonVariant"
+ :class="[
actionButtonClassNames,
- 'btn btn-comment btn-comment-and-close js-action-button',
+ 'btn-comment btn-comment-and-close js-action-button',
]"
:disabled="isToggleStateButtonLoading || isSubmitting"
- :label="issueActionButtonTitle"
@click="handleSave(true)"
- />
+ >{{ issueActionButtonTitle }}</gl-button
+ >
</div>
</form>
</div>
diff --git a/app/assets/javascripts/notes/components/diff_discussion_header.vue b/app/assets/javascripts/notes/components/diff_discussion_header.vue
index 50d224a2f08..8e6c01ba63f 100644
--- a/app/assets/javascripts/notes/components/diff_discussion_header.vue
+++ b/app/assets/javascripts/notes/components/diff_discussion_header.vue
@@ -1,4 +1,5 @@
<script>
+/* eslint-disable vue/no-v-html */
import { mapActions } from 'vuex';
import { escape } from 'lodash';
diff --git a/app/assets/javascripts/notes/components/diff_with_note.vue b/app/assets/javascripts/notes/components/diff_with_note.vue
index 8897b54fac7..c01cd8f8037 100644
--- a/app/assets/javascripts/notes/components/diff_with_note.vue
+++ b/app/assets/javascripts/notes/components/diff_with_note.vue
@@ -1,6 +1,7 @@
<script>
+/* eslint-disable vue/no-v-html */
import { mapState, mapActions } from 'vuex';
-import { GlSkeletonLoading } from '@gitlab/ui';
+import { GlDeprecatedSkeletonLoading as GlSkeletonLoading } from '@gitlab/ui';
import DiffFileHeader from '~/diffs/components/diff_file_header.vue';
import DiffViewer from '~/vue_shared/components/diff_viewer/diff_viewer.vue';
import ImageDiffOverlay from '~/diffs/components/image_diff_overlay.vue';
diff --git a/app/assets/javascripts/notes/components/discussion_counter.vue b/app/assets/javascripts/notes/components/discussion_counter.vue
index 4a1a1086329..c6fab271376 100644
--- a/app/assets/javascripts/notes/components/discussion_counter.vue
+++ b/app/assets/javascripts/notes/components/discussion_counter.vue
@@ -1,7 +1,6 @@
<script>
import { mapGetters, mapActions } from 'vuex';
-import { GlTooltipDirective } from '@gitlab/ui';
-import Icon from '~/vue_shared/components/icon.vue';
+import { GlTooltipDirective, GlIcon } from '@gitlab/ui';
import discussionNavigation from '../mixins/discussion_navigation';
export default {
@@ -9,7 +8,7 @@ export default {
GlTooltip: GlTooltipDirective,
},
components: {
- Icon,
+ GlIcon,
},
mixins: [discussionNavigation],
computed: {
@@ -60,7 +59,7 @@ export default {
:class="{ 'line-resolve-btn is-active': allResolved, 'line-resolve-text': !allResolved }"
>
<template v-if="allResolved">
- <icon name="check-circle-filled" />
+ <gl-icon name="check-circle-filled" />
{{ __('All threads resolved') }}
</template>
<template v-else>
@@ -79,7 +78,7 @@ export default {
:title="s__('Resolve all threads in new issue')"
class="new-issue-for-discussion btn btn-default discussion-create-issue-btn"
>
- <icon name="issue-new" />
+ <gl-icon name="issue-new" />
</a>
</div>
<div v-if="isLoggedIn && !allResolved" class="btn-group btn-group-sm" role="group">
@@ -92,7 +91,7 @@ export default {
data-track-property="click_next_unresolved_thread_top"
@click="jumpToNextDiscussion"
>
- <icon name="comment-next" />
+ <gl-icon name="comment-next" />
</button>
</div>
<div class="btn-group btn-group-sm" role="group">
@@ -102,7 +101,7 @@ export default {
class="btn btn-default toggle-all-discussions-btn"
@click="handleExpandDiscussions"
>
- <icon :name="allExpanded ? 'angle-up' : 'angle-down'" />
+ <gl-icon :name="allExpanded ? 'angle-up' : 'angle-down'" />
</button>
</div>
</div>
diff --git a/app/assets/javascripts/notes/components/discussion_filter.vue b/app/assets/javascripts/notes/components/discussion_filter.vue
index 6b1e3298f9a..989ce9ff144 100644
--- a/app/assets/javascripts/notes/components/discussion_filter.vue
+++ b/app/assets/javascripts/notes/components/discussion_filter.vue
@@ -1,8 +1,8 @@
<script>
import $ from 'jquery';
import { mapGetters, mapActions } from 'vuex';
+import { GlIcon } from '@gitlab/ui';
import { getLocationHash, doesHashExistInUrl } from '../../lib/utils/url_utility';
-import Icon from '~/vue_shared/components/icon.vue';
import {
DISCUSSION_FILTERS_DEFAULT_VALUE,
HISTORY_ONLY_FILTER_VALUE,
@@ -14,7 +14,7 @@ import notesEventHub from '../event_hub';
export default {
components: {
- Icon,
+ GlIcon,
},
props: {
filters: {
@@ -120,7 +120,7 @@ export default {
data-toggle="dropdown"
aria-expanded="false"
>
- {{ currentFilter.title }} <icon name="chevron-down" />
+ {{ currentFilter.title }} <gl-icon name="chevron-down" />
</button>
<div
ref="dropdownMenu"
diff --git a/app/assets/javascripts/notes/components/discussion_filter_note.vue b/app/assets/javascripts/notes/components/discussion_filter_note.vue
index 8dc4b43d69a..ae6646cf96c 100644
--- a/app/assets/javascripts/notes/components/discussion_filter_note.vue
+++ b/app/assets/javascripts/notes/components/discussion_filter_note.vue
@@ -1,6 +1,6 @@
<script>
-import { GlButton } from '@gitlab/ui';
-import Icon from '~/vue_shared/components/icon.vue';
+/* eslint-disable vue/no-v-html */
+import { GlButton, GlIcon } from '@gitlab/ui';
import { __, sprintf } from '~/locale';
import notesEventHub from '../event_hub';
@@ -8,7 +8,7 @@ import notesEventHub from '../event_hub';
export default {
components: {
GlButton,
- Icon,
+ GlIcon,
},
computed: {
timelineContent() {
@@ -35,7 +35,7 @@ export default {
<template>
<li class="timeline-entry note note-wrapper discussion-filter-note js-discussion-filter-note">
<div class="timeline-icon d-none d-lg-flex">
- <icon name="comment" />
+ <gl-icon name="comment" />
</div>
<div class="timeline-content">
<div ref="timelineContent" v-html="timelineContent"></div>
diff --git a/app/assets/javascripts/notes/components/discussion_jump_to_next_button.vue b/app/assets/javascripts/notes/components/discussion_jump_to_next_button.vue
index b71ce1b6a0a..f94d0060b41 100644
--- a/app/assets/javascripts/notes/components/discussion_jump_to_next_button.vue
+++ b/app/assets/javascripts/notes/components/discussion_jump_to_next_button.vue
@@ -1,12 +1,11 @@
<script>
-import { GlTooltipDirective } from '@gitlab/ui';
-import icon from '~/vue_shared/components/icon.vue';
+import { GlTooltipDirective, GlIcon } from '@gitlab/ui';
import discussionNavigation from '../mixins/discussion_navigation';
export default {
name: 'JumpToNextDiscussionButton',
components: {
- icon,
+ GlIcon,
},
directives: {
GlTooltip: GlTooltipDirective,
@@ -33,7 +32,7 @@ export default {
data-track-property="click_next_unresolved_thread"
@click="jumpToNextRelativeDiscussion(fromDiscussionId)"
>
- <icon name="comment-next" />
+ <gl-icon name="comment-next" />
</button>
</div>
</template>
diff --git a/app/assets/javascripts/notes/components/discussion_locked_widget.vue b/app/assets/javascripts/notes/components/discussion_locked_widget.vue
index 8636984c6af..2f215e36d5b 100644
--- a/app/assets/javascripts/notes/components/discussion_locked_widget.vue
+++ b/app/assets/javascripts/notes/components/discussion_locked_widget.vue
@@ -1,13 +1,12 @@
<script>
-import { GlLink } from '@gitlab/ui';
-import Icon from '~/vue_shared/components/icon.vue';
+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 {
components: {
- Icon,
+ GlIcon,
GlLink,
},
mixins: [Issuable, issuableStateMixin],
@@ -28,7 +27,7 @@ export default {
<template>
<div class="disabled-comment text-center">
<span class="issuable-note-warning inline">
- <icon :size="16" name="lock" class="icon" />
+ <gl-icon :size="16" name="lock" class="icon" />
<span v-if="isProjectArchived">
{{ projectArchivedWarning }}
<gl-link :href="archivedProjectDocsPath" target="_blank" class="learn-more">
diff --git a/app/assets/javascripts/notes/components/discussion_resolve_button.vue b/app/assets/javascripts/notes/components/discussion_resolve_button.vue
index 77f6f1e51c5..e060a6affd4 100644
--- a/app/assets/javascripts/notes/components/discussion_resolve_button.vue
+++ b/app/assets/javascripts/notes/components/discussion_resolve_button.vue
@@ -1,10 +1,10 @@
<script>
-import { GlLoadingIcon } from '@gitlab/ui';
+import { GlButton } from '@gitlab/ui';
export default {
name: 'ResolveDiscussionButton',
components: {
- GlLoadingIcon,
+ GlButton,
},
props: {
isResolving: {
@@ -21,8 +21,7 @@ export default {
</script>
<template>
- <button ref="button" type="button" class="btn btn-default ml-sm-2" @click="$emit('onClick')">
- <gl-loading-icon v-if="isResolving" ref="isResolvingIcon" inline />
+ <gl-button :loading="isResolving" class="ml-sm-2" @click="$emit('onClick')">
{{ buttonTitle }}
- </button>
+ </gl-button>
</template>
diff --git a/app/assets/javascripts/notes/components/note_actions.vue b/app/assets/javascripts/notes/components/note_actions.vue
index a8ae7fb48f0..a8057276f1a 100644
--- a/app/assets/javascripts/notes/components/note_actions.vue
+++ b/app/assets/javascripts/notes/components/note_actions.vue
@@ -1,18 +1,18 @@
<script>
import { mapGetters } from 'vuex';
-import { GlLoadingIcon, GlTooltipDirective } from '@gitlab/ui';
-import { __ } from '~/locale';
+import { GlLoadingIcon, GlTooltipDirective, GlIcon } from '@gitlab/ui';
+import { __, sprintf } from '~/locale';
import resolvedStatusMixin from '~/batch_comments/mixins/resolved_status';
-import Icon from '~/vue_shared/components/icon.vue';
import ReplyButton from './note_actions/reply_button.vue';
import eventHub from '~/sidebar/event_hub';
import Api from '~/api';
import { deprecatedCreateFlash as flash } from '~/flash';
+import { splitCamelCase } from '../../lib/utils/text_utility';
export default {
name: 'NoteActions',
components: {
- Icon,
+ GlIcon,
ReplyButton,
GlLoadingIcon,
},
@@ -48,6 +48,26 @@ export default {
required: false,
default: null,
},
+ isAuthor: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ isContributor: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ noteableType: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ projectName: {
+ type: String,
+ required: false,
+ default: '',
+ },
showReply: {
type: Boolean,
required: true,
@@ -122,6 +142,9 @@ export default {
targetType() {
return this.getNoteableData.targetType;
},
+ noteableDisplayName() {
+ return splitCamelCase(this.noteableType).toLowerCase();
+ },
assignees() {
return this.getNoteableData.assignees || [];
},
@@ -131,6 +154,22 @@ export default {
canAssign() {
return this.getNoteableData.current_user?.can_update && this.isIssue;
},
+ displayAuthorBadgeText() {
+ return sprintf(__('This user is the author of this %{noteable}.'), {
+ noteable: this.noteableDisplayName,
+ });
+ },
+ displayMemberBadgeText() {
+ return sprintf(__('This user is a %{access} of the %{name} project.'), {
+ access: this.accessLevel.toLowerCase(),
+ name: this.projectName,
+ });
+ },
+ displayContributorBadgeText() {
+ return sprintf(__('This user has previously committed to the %{name} project.'), {
+ name: this.projectName,
+ });
+ },
},
methods: {
onEdit() {
@@ -176,7 +215,24 @@ export default {
<template>
<div class="note-actions">
- <span v-if="accessLevel" class="note-role user-access-role">{{ accessLevel }}</span>
+ <span
+ v-if="isAuthor"
+ class="note-role user-access-role has-tooltip d-none d-md-inline-block"
+ :title="displayAuthorBadgeText"
+ >{{ __('Author') }}</span
+ >
+ <span
+ v-if="accessLevel"
+ class="note-role user-access-role has-tooltip"
+ :title="displayMemberBadgeText"
+ >{{ accessLevel }}</span
+ >
+ <span
+ v-else-if="isContributor"
+ class="note-role user-access-role has-tooltip"
+ :title="displayContributorBadgeText"
+ >{{ __('Contributor') }}</span
+ >
<div v-if="canResolve" class="note-actions-item">
<button
ref="resolveButton"
@@ -189,7 +245,7 @@ export default {
@click="onResolve"
>
<template v-if="!isResolving">
- <icon :name="isResolved ? 'check-circle-filled' : 'check-circle'" />
+ <gl-icon :name="isResolved ? 'check-circle-filled' : 'check-circle'" />
</template>
<gl-loading-icon v-else inline />
</button>
@@ -203,9 +259,9 @@ export default {
title="Add reaction"
data-position="right"
>
- <icon class="link-highlight award-control-icon-neutral" name="slight-smile" />
- <icon class="link-highlight award-control-icon-positive" name="smiley" />
- <icon class="link-highlight award-control-icon-super-positive" name="smiley" />
+ <gl-icon class="link-highlight award-control-icon-neutral" name="slight-smile" />
+ <gl-icon class="link-highlight award-control-icon-positive" name="smiley" />
+ <gl-icon class="link-highlight award-control-icon-super-positive" name="smiley" />
</a>
</div>
<reply-button
@@ -222,7 +278,7 @@ export default {
class="note-action-button js-note-edit btn btn-transparent qa-note-edit-button"
@click="onEdit"
>
- <icon name="pencil" class="link-highlight" />
+ <gl-icon name="pencil" class="link-highlight" />
</button>
</div>
<div v-if="showDeleteAction" class="note-actions-item">
@@ -233,7 +289,7 @@ export default {
class="note-action-button js-note-delete btn btn-transparent"
@click="onDelete"
>
- <icon name="remove" class="link-highlight" />
+ <gl-icon name="remove" class="link-highlight" />
</button>
</div>
<div v-else-if="shouldShowActionsDropdown" class="dropdown more-actions note-actions-item">
@@ -245,7 +301,7 @@ export default {
data-toggle="dropdown"
@click="closeTooltip"
>
- <icon class="icon" name="ellipsis_v" />
+ <gl-icon class="icon" name="ellipsis_v" />
</button>
<ul class="dropdown-menu more-actions-dropdown dropdown-open-left">
<li v-if="canReportAsAbuse">
diff --git a/app/assets/javascripts/notes/components/note_actions/reply_button.vue b/app/assets/javascripts/notes/components/note_actions/reply_button.vue
index 30cb7967c34..f19b7667fb2 100644
--- a/app/assets/javascripts/notes/components/note_actions/reply_button.vue
+++ b/app/assets/javascripts/notes/components/note_actions/reply_button.vue
@@ -1,12 +1,10 @@
<script>
-import { GlTooltipDirective, GlDeprecatedButton } from '@gitlab/ui';
-import Icon from '~/vue_shared/components/icon.vue';
+import { GlTooltipDirective, GlButton } from '@gitlab/ui';
export default {
name: 'ReplyButton',
components: {
- Icon,
- GlDeprecatedButton,
+ GlButton,
},
directives: {
GlTooltip: GlTooltipDirective,
@@ -16,17 +14,17 @@ export default {
<template>
<div class="note-actions-item">
- <gl-deprecated-button
+ <gl-button
ref="button"
v-gl-tooltip
- class="note-action-button"
data-track-event="click_button"
data-track-label="reply_comment_button"
- variant="transparent"
+ category="tertiary"
+ size="small"
+ icon="comment"
:title="__('Reply to comment')"
+ :aria-label="__('Reply to comment')"
@click="$emit('startReplying')"
- >
- <icon name="comment" class="link-highlight" />
- </gl-deprecated-button>
+ />
</div>
</template>
diff --git a/app/assets/javascripts/notes/components/note_body.vue b/app/assets/javascripts/notes/components/note_body.vue
index 42b78929f8a..314fa762768 100644
--- a/app/assets/javascripts/notes/components/note_body.vue
+++ b/app/assets/javascripts/notes/components/note_body.vue
@@ -1,4 +1,5 @@
<script>
+/* eslint-disable vue/no-v-html */
import { mapActions, mapGetters, mapState } from 'vuex';
import $ from 'jquery';
import '~/behaviors/markdown/render_gfm';
diff --git a/app/assets/javascripts/notes/components/note_form.vue b/app/assets/javascripts/notes/components/note_form.vue
index 24227d55ebf..88b4461cf38 100644
--- a/app/assets/javascripts/notes/components/note_form.vue
+++ b/app/assets/javascripts/notes/components/note_form.vue
@@ -1,4 +1,5 @@
<script>
+/* eslint-disable vue/no-v-html */
import { mapGetters, mapActions, mapState } from 'vuex';
import { mergeUrlParams } from '~/lib/utils/url_utility';
import eventHub from '../event_hub';
@@ -336,7 +337,7 @@ export default {
v-model="updatedNoteBody"
:data-supports-quick-actions="!isEditing"
name="note[note]"
- class="note-textarea js-gfm-input js-note-text js-autosize markdown-area js-vue-issue-note-form js-vue-textarea qa-reply-input"
+ class="note-textarea js-gfm-input js-note-text js-autosize markdown-area js-vue-issue-note-form qa-reply-input"
dir="auto"
:aria-label="__('Description')"
:placeholder="__('Write a comment or drag your files here…')"
diff --git a/app/assets/javascripts/notes/components/note_header.vue b/app/assets/javascripts/notes/components/note_header.vue
index 9ded5ab648e..a13a0dbbf30 100644
--- a/app/assets/javascripts/notes/components/note_header.vue
+++ b/app/assets/javascripts/notes/components/note_header.vue
@@ -1,6 +1,7 @@
<script>
+/* eslint-disable vue/no-v-html */
import { mapActions } from 'vuex';
-import { GlIcon, GlTooltipDirective } from '@gitlab/ui';
+import { GlIcon, GlLoadingIcon, GlTooltipDirective } from '@gitlab/ui';
import timeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
export default {
@@ -9,6 +10,7 @@ export default {
GitlabTeamMemberBadge: () =>
import('ee_component/vue_shared/components/user_avatar/badges/gitlab_team_member_badge.vue'),
GlIcon,
+ GlLoadingIcon,
},
directives: {
GlTooltip: GlTooltipDirective,
@@ -194,13 +196,12 @@ export default {
class="gl-ml-1 gl-text-gray-700 align-middle"
/>
<slot name="extra-controls"></slot>
- <i
+ <gl-loading-icon
v-if="showSpinner"
ref="spinner"
- class="fa fa-spinner fa-spin editing-spinner"
- :aria-label="__('Comment is being updated')"
- aria-hidden="true"
- ></i>
+ class="editing-spinner"
+ :label="__('Comment is being updated')"
+ />
</span>
</div>
</template>
diff --git a/app/assets/javascripts/notes/components/note_signed_out_widget.vue b/app/assets/javascripts/notes/components/note_signed_out_widget.vue
index ccfe84ab098..593933016e1 100644
--- a/app/assets/javascripts/notes/components/note_signed_out_widget.vue
+++ b/app/assets/javascripts/notes/components/note_signed_out_widget.vue
@@ -1,8 +1,12 @@
<script>
+import { GlSafeHtmlDirective as SafeHtml } from '@gitlab/ui';
import { mapGetters } from 'vuex';
import { __, sprintf } from '~/locale';
export default {
+ directives: {
+ SafeHtml,
+ },
computed: {
...mapGetters(['getNotesDataByProp']),
registerLink() {
@@ -30,5 +34,5 @@ export default {
</script>
<template>
- <div class="disabled-comment text-center" v-html="signedOutText"></div>
+ <div v-safe-html="signedOutText" class="disabled-comment text-center"></div>
</template>
diff --git a/app/assets/javascripts/notes/components/noteable_discussion.vue b/app/assets/javascripts/notes/components/noteable_discussion.vue
index b4176c6063b..62ee7f30c57 100644
--- a/app/assets/javascripts/notes/components/noteable_discussion.vue
+++ b/app/assets/javascripts/notes/components/noteable_discussion.vue
@@ -1,10 +1,9 @@
<script>
import { mapActions, mapGetters } from 'vuex';
-import { GlTooltipDirective } from '@gitlab/ui';
+import { GlTooltipDirective, GlIcon } from '@gitlab/ui';
import diffLineNoteFormMixin from '~/notes/mixins/diff_line_note_form';
import { s__, __ } from '~/locale';
import { clearDraft, getDiscussionReplyKey } from '~/lib/utils/autosave';
-import icon from '~/vue_shared/components/icon.vue';
import TimelineEntryItem from '~/vue_shared/components/notes/timeline_entry_item.vue';
import DraftNote from '~/batch_comments/components/draft_note.vue';
import { deprecatedCreateFlash as Flash } from '../../flash';
@@ -22,7 +21,7 @@ import DiscussionActions from './discussion_actions.vue';
export default {
name: 'NoteableDiscussion',
components: {
- icon,
+ GlIcon,
userAvatarLink,
diffDiscussionHeader,
noteSignedOutWidget,
diff --git a/app/assets/javascripts/notes/components/noteable_note.vue b/app/assets/javascripts/notes/components/noteable_note.vue
index ce771e67cbb..4f45fcb0062 100644
--- a/app/assets/javascripts/notes/components/noteable_note.vue
+++ b/app/assets/javascripts/notes/components/noteable_note.vue
@@ -2,7 +2,7 @@
import $ from 'jquery';
import { mapGetters, mapActions } from 'vuex';
import { escape } from 'lodash';
-import { GlSprintf } from '@gitlab/ui';
+import { GlSprintf, GlSafeHtmlDirective as SafeHtml } from '@gitlab/ui';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { truncateSha } from '~/lib/utils/text_utility';
import TimelineEntryItem from '~/vue_shared/components/notes/timeline_entry_item.vue';
@@ -34,6 +34,9 @@ export default {
NoteBody,
TimelineEntryItem,
},
+ directives: {
+ SafeHtml,
+ },
mixins: [noteable, resolvable, glFeatureFlagsMixin()],
props: {
note: {
@@ -358,7 +361,7 @@ export default {
</template>
</gl-sprintf>
</div>
- <div v-once class="timeline-icon">
+ <div class="timeline-icon">
<user-avatar-link
:link-href="author.path"
:img-src="author.avatar_url"
@@ -371,14 +374,13 @@ export default {
<div class="timeline-content">
<div class="note-header">
<note-header
- v-once
:author="author"
:created-at="note.created_at"
:note-id="note.id"
:is-confidential="note.confidential"
>
<slot slot="note-header-info" name="note-header-info"></slot>
- <span v-if="commit" v-html="actionText"></span>
+ <span v-if="commit" v-safe-html="actionText"></span>
<span v-else-if="note.created_at" class="d-none d-sm-inline">&middot;</span>
</note-header>
<note-actions
@@ -387,6 +389,10 @@ export default {
:note-id="note.id"
:note-url="note.noteable_note_url"
:access-level="note.human_access"
+ :is-contributor="note.is_contributor"
+ :is-author="note.is_noteable_author"
+ :project-name="note.project_name"
+ :noteable-type="note.noteable_type"
:show-reply="showReplyButton"
:can-edit="note.current_user.can_edit"
:can-award-emoji="note.current_user.can_award_emoji"
diff --git a/app/assets/javascripts/notes/components/toggle_replies_widget.vue b/app/assets/javascripts/notes/components/toggle_replies_widget.vue
index dd132d4f608..bddac60647d 100644
--- a/app/assets/javascripts/notes/components/toggle_replies_widget.vue
+++ b/app/assets/javascripts/notes/components/toggle_replies_widget.vue
@@ -1,12 +1,12 @@
<script>
import { uniqBy } from 'lodash';
-import Icon from '~/vue_shared/components/icon.vue';
+import { GlIcon } from '@gitlab/ui';
import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
export default {
components: {
- Icon,
+ GlIcon,
UserAvatarLink,
TimeAgoTooltip,
},
@@ -44,7 +44,7 @@ export default {
<template>
<li :class="className" class="replies-toggle js-toggle-replies">
<template v-if="collapsed">
- <icon name="chevron-right" @click.native="toggle" />
+ <gl-icon name="chevron-right" @click.native="toggle" />
<div>
<user-avatar-link
v-for="author in uniqueAuthors"
@@ -71,7 +71,7 @@ export default {
class="collapse-replies-btn js-collapse-replies qa-collapse-replies"
@click="toggle"
>
- <icon name="chevron-down" /> {{ s__('Notes|Collapse replies') }}
+ <gl-icon name="chevron-down" /> {{ s__('Notes|Collapse replies') }}
</span>
</li>
</template>
diff --git a/app/assets/javascripts/notes/constants.js b/app/assets/javascripts/notes/constants.js
index c1449237f8a..b81aae7c257 100644
--- a/app/assets/javascripts/notes/constants.js
+++ b/app/assets/javascripts/notes/constants.js
@@ -22,6 +22,8 @@ export const TIME_DIFFERENCE_VALUE = 10;
export const ASC = 'asc';
export const DESC = 'desc';
+export const DISCUSSION_FETCH_TIMEOUT = 750;
+
export const NOTEABLE_TYPE_MAPPING = {
Issue: ISSUE_NOTEABLE_TYPE,
MergeRequest: MERGE_REQUEST_NOTEABLE_TYPE,
diff --git a/app/assets/javascripts/notes/stores/actions.js b/app/assets/javascripts/notes/stores/actions.js
index f6069b509e8..9c63a7e3cd4 100644
--- a/app/assets/javascripts/notes/stores/actions.js
+++ b/app/assets/javascripts/notes/stores/actions.js
@@ -87,6 +87,7 @@ export const fetchDiscussions = ({ commit, dispatch }, { path, filter, persistFi
return axios.get(path, config).then(({ data }) => {
commit(types.SET_INITIAL_DISCUSSIONS, data);
+ commit(types.SET_FETCHING_DISCUSSIONS, false);
dispatch('updateResolvableDiscussionsCounts');
});
@@ -136,6 +137,23 @@ export const updateNote = ({ commit, dispatch }, { endpoint, note }) =>
export const updateOrCreateNotes = ({ commit, state, getters, dispatch }, notes) => {
const { notesById } = getters;
+ const debouncedFetchDiscussions = isFetching => {
+ if (!isFetching) {
+ commit(types.SET_FETCHING_DISCUSSIONS, true);
+ dispatch('fetchDiscussions', { path: state.notesData.discussionsPath });
+ } else {
+ if (isFetching !== true) {
+ clearTimeout(state.currentlyFetchingDiscussions);
+ }
+
+ commit(
+ types.SET_FETCHING_DISCUSSIONS,
+ setTimeout(() => {
+ dispatch('fetchDiscussions', { path: state.notesData.discussionsPath });
+ }, constants.DISCUSSION_FETCH_TIMEOUT),
+ );
+ }
+ };
notes.forEach(note => {
if (notesById[note.id]) {
@@ -146,7 +164,7 @@ export const updateOrCreateNotes = ({ commit, state, getters, dispatch }, notes)
if (discussion) {
commit(types.ADD_NEW_REPLY_TO_DISCUSSION, note);
} else if (note.type === constants.DIFF_NOTE) {
- dispatch('fetchDiscussions', { path: state.notesData.discussionsPath });
+ debouncedFetchDiscussions(state.currentlyFetchingDiscussions);
} else {
commit(types.ADD_NEW_NOTE, note);
}
@@ -457,7 +475,7 @@ export const poll = ({ commit, state, getters, dispatch }) => {
});
if (!Visibility.hidden()) {
- eTagPoll.makeRequest();
+ eTagPoll.makeDelayedRequest(2500);
} else {
dispatch('fetchData');
}
diff --git a/app/assets/javascripts/notes/stores/modules/index.js b/app/assets/javascripts/notes/stores/modules/index.js
index 1649e63c61f..161c9b8b1b5 100644
--- a/app/assets/javascripts/notes/stores/modules/index.js
+++ b/app/assets/javascripts/notes/stores/modules/index.js
@@ -12,6 +12,7 @@ export default () => ({
lastFetchedAt: null,
currentDiscussionId: null,
batchSuggestionsInfo: [],
+ currentlyFetchingDiscussions: false,
/**
* selectedCommentPosition & selectedCommentPosition structures are the same as `position.line_range`:
* {
diff --git a/app/assets/javascripts/notes/stores/mutation_types.js b/app/assets/javascripts/notes/stores/mutation_types.js
index eb3447291bc..23515cdd9e3 100644
--- a/app/assets/javascripts/notes/stores/mutation_types.js
+++ b/app/assets/javascripts/notes/stores/mutation_types.js
@@ -36,6 +36,7 @@ export const SET_CURRENT_DISCUSSION_ID = 'SET_CURRENT_DISCUSSION_ID';
export const SET_DISCUSSIONS_SORT = 'SET_DISCUSSIONS_SORT';
export const SET_SELECTED_COMMENT_POSITION = 'SET_SELECTED_COMMENT_POSITION';
export const SET_SELECTED_COMMENT_POSITION_HOVER = 'SET_SELECTED_COMMENT_POSITION_HOVER';
+export const SET_FETCHING_DISCUSSIONS = 'SET_FETCHING_DISCUSSIONS';
// Issue
export const CLOSE_ISSUE = 'CLOSE_ISSUE';
diff --git a/app/assets/javascripts/notes/stores/mutations.js b/app/assets/javascripts/notes/stores/mutations.js
index aa078f00569..a8bd94cc763 100644
--- a/app/assets/javascripts/notes/stores/mutations.js
+++ b/app/assets/javascripts/notes/stores/mutations.js
@@ -379,4 +379,7 @@ export default {
[types.UPDATE_ASSIGNEES](state, assignees) {
state.noteableData.assignees = assignees;
},
+ [types.SET_FETCHING_DISCUSSIONS](state, value) {
+ state.currentlyFetchingDiscussions = value;
+ },
};
diff --git a/app/assets/javascripts/packages/details/components/additional_metadata.vue b/app/assets/javascripts/packages/details/components/additional_metadata.vue
index a3de6dd46c7..76e0976ac05 100644
--- a/app/assets/javascripts/packages/details/components/additional_metadata.vue
+++ b/app/assets/javascripts/packages/details/components/additional_metadata.vue
@@ -1,7 +1,7 @@
<script>
import { GlLink, GlSprintf } from '@gitlab/ui';
import { s__ } from '~/locale';
-import DetailsRow from '~/registry/shared/components/details_row.vue';
+import DetailsRow from '~/vue_shared/components/registry/details_row.vue';
import { generateConanRecipe } from '../utils';
import { PackageType } from '../../shared/constants';
diff --git a/app/assets/javascripts/packages/details/components/app.vue b/app/assets/javascripts/packages/details/components/app.vue
index dbb5f7be0a0..af3220840a6 100644
--- a/app/assets/javascripts/packages/details/components/app.vue
+++ b/app/assets/javascripts/packages/details/components/app.vue
@@ -25,8 +25,9 @@ import { numberToHumanSize } from '~/lib/utils/number_utils';
import timeagoMixin from '~/vue_shared/mixins/timeago';
import FileIcon from '~/vue_shared/components/file_icon.vue';
import { __, s__ } from '~/locale';
-import { PackageType, TrackingActions } from '../../shared/constants';
+import { PackageType, TrackingActions, SHOW_DELETE_SUCCESS_ALERT } from '../../shared/constants';
import { packageTypeToTrackCategory } from '../../shared/utils';
+import { objectToQueryString } from '~/lib/utils/common_utils';
export default {
name: 'PackagesApp',
@@ -62,17 +63,15 @@ export default {
'packageFiles',
'isLoading',
'canDelete',
- 'destroyPath',
'svgPath',
'npmPath',
'npmHelpPath',
+ 'projectListUrl',
+ 'groupListUrl',
]),
isValidPackage() {
return Boolean(this.packageEntity.name);
},
- canDeletePackage() {
- return this.canDelete && this.destroyPath;
- },
filesTableRows() {
return this.packageFiles.map(x => ({
name: x.file_name,
@@ -100,7 +99,7 @@ export default {
},
},
methods: {
- ...mapActions(['fetchPackageVersions']),
+ ...mapActions(['deletePackage', 'fetchPackageVersions']),
formatSize(size) {
return numberToHumanSize(size);
},
@@ -112,6 +111,16 @@ export default {
this.fetchPackageVersions();
}
},
+ async confirmPackageDeletion() {
+ this.track(TrackingActions.DELETE_PACKAGE);
+ await this.deletePackage();
+ const returnTo =
+ !this.groupListUrl || document.referrer.includes(this.projectName)
+ ? this.projectListUrl
+ : this.groupListUrl; // to avoid security issue url are supplied from backend
+ const modalQuery = objectToQueryString({ [SHOW_DELETE_SUCCESS_ALERT]: true });
+ window.location.replace(`${returnTo}?${modalQuery}`);
+ },
},
i18n: {
deleteModalTitle: s__(`PackageRegistry|Delete Package Version`),
@@ -147,12 +156,10 @@ export default {
/>
<div v-else class="packages-app">
- <div class="detail-page-header d-flex justify-content-between flex-column flex-sm-row">
- <package-title />
-
- <div class="mt-sm-2">
+ <package-title>
+ <template #delete-button>
<gl-button
- v-if="canDeletePackage"
+ v-if="canDelete"
v-gl-modal="'delete-modal'"
class="js-delete-button"
variant="danger"
@@ -161,8 +168,8 @@ export default {
>
{{ __('Delete') }}
</gl-button>
- </div>
- </div>
+ </template>
+ </package-title>
<gl-tabs>
<gl-tab :title="__('Detail')">
@@ -268,22 +275,22 @@ export default {
</template>
</gl-sprintf>
- <div slot="modal-footer" class="w-100">
- <div class="float-right">
- <gl-button @click="cancelDelete()">{{ __('Cancel') }}</gl-button>
- <gl-button
- ref="modal-delete-button"
- data-method="delete"
- :to="destroyPath"
- variant="danger"
- category="primary"
- data-qa-selector="delete_modal_button"
- @click="track($options.trackingActions.DELETE_PACKAGE)"
- >
- {{ __('Delete') }}
- </gl-button>
+ <template #modal-footer>
+ <div class="gl-w-full">
+ <div class="float-right">
+ <gl-button @click="cancelDelete">{{ __('Cancel') }}</gl-button>
+ <gl-button
+ ref="modal-delete-button"
+ variant="danger"
+ category="primary"
+ data-qa-selector="delete_modal_button"
+ @click="confirmPackageDeletion"
+ >
+ {{ __('Delete') }}
+ </gl-button>
+ </div>
</div>
- </div>
+ </template>
</gl-modal>
</div>
</template>
diff --git a/app/assets/javascripts/packages/details/components/code_instruction.vue b/app/assets/javascripts/packages/details/components/code_instruction.vue
deleted file mode 100644
index 0719ddfcd2b..00000000000
--- a/app/assets/javascripts/packages/details/components/code_instruction.vue
+++ /dev/null
@@ -1,63 +0,0 @@
-<script>
-import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
-import Tracking from '~/tracking';
-import { TrackingLabels } from '../constants';
-
-export default {
- name: 'CodeInstruction',
- components: {
- ClipboardButton,
- },
- mixins: [
- Tracking.mixin({
- label: TrackingLabels.CODE_INSTRUCTION,
- }),
- ],
- props: {
- instruction: {
- type: String,
- required: true,
- },
- copyText: {
- type: String,
- required: true,
- },
- multiline: {
- type: Boolean,
- required: false,
- default: false,
- },
- trackingAction: {
- type: String,
- required: false,
- default: '',
- },
- },
- methods: {
- trackCopy() {
- if (this.trackingAction) {
- this.track(this.trackingAction);
- }
- },
- },
-};
-</script>
-
-<template>
- <div v-if="!multiline" class="input-group gl-mb-3">
- <input
- :value="instruction"
- type="text"
- class="form-control monospace js-instruction-input"
- readonly
- @copy="trackCopy"
- />
- <span class="input-group-append js-instruction-button" @click="trackCopy">
- <clipboard-button :text="instruction" :title="copyText" class="input-group-text" />
- </span>
- </div>
-
- <div v-else>
- <pre class="js-instruction-pre" @copy="trackCopy">{{ instruction }}</pre>
- </div>
-</template>
diff --git a/app/assets/javascripts/packages/details/components/composer_installation.vue b/app/assets/javascripts/packages/details/components/composer_installation.vue
index 1934da149ce..60ad468c293 100644
--- a/app/assets/javascripts/packages/details/components/composer_installation.vue
+++ b/app/assets/javascripts/packages/details/components/composer_installation.vue
@@ -2,8 +2,8 @@
import { GlLink, GlSprintf } from '@gitlab/ui';
import { mapGetters, mapState } from 'vuex';
import { s__ } from '~/locale';
-import CodeInstruction from './code_instruction.vue';
-import { TrackingActions } from '../constants';
+import CodeInstruction from '~/vue_shared/components/registry/code_instruction.vue';
+import { TrackingActions, TrackingLabels } from '../constants';
export default {
name: 'ComposerInstallation',
@@ -26,28 +26,30 @@ export default {
),
},
trackingActions: { ...TrackingActions },
+ TrackingLabels,
};
</script>
<template>
<div>
<h3 class="gl-font-lg">{{ __('Installation') }}</h3>
- <h4 class="gl-font-base" data-testid="registry-include-title">
- {{ $options.i18n.registryInclude }}
- </h4>
<code-instruction
+ :label="$options.i18n.registryInclude"
:instruction="composerRegistryInclude"
:copy-text="$options.i18n.copyRegistryInclude"
:tracking-action="$options.trackingActions.COPY_COMPOSER_REGISTRY_INCLUDE_COMMAND"
+ :tracking-label="$options.TrackingLabels.CODE_INSTRUCTION"
+ data-testid="registry-include"
/>
- <h4 class="gl-font-base" data-testid="package-include-title">
- {{ $options.i18n.packageInclude }}
- </h4>
+
<code-instruction
+ :label="$options.i18n.packageInclude"
:instruction="composerPackageInclude"
:copy-text="$options.i18n.copyPackageInclude"
:tracking-action="$options.trackingActions.COPY_COMPOSER_PACKAGE_INCLUDE_COMMAND"
+ :tracking-label="$options.TrackingLabels.CODE_INSTRUCTION"
+ data-testid="package-include"
/>
<span data-testid="help-text">
<gl-sprintf :message="$options.i18n.infoLine">
diff --git a/app/assets/javascripts/packages/details/components/conan_installation.vue b/app/assets/javascripts/packages/details/components/conan_installation.vue
index cff7d73f1e8..a5df87c9c5b 100644
--- a/app/assets/javascripts/packages/details/components/conan_installation.vue
+++ b/app/assets/javascripts/packages/details/components/conan_installation.vue
@@ -2,8 +2,8 @@
import { GlLink, GlSprintf } from '@gitlab/ui';
import { mapGetters, mapState } from 'vuex';
import { s__ } from '~/locale';
-import CodeInstruction from './code_instruction.vue';
-import { TrackingActions } from '../constants';
+import CodeInstruction from '~/vue_shared/components/registry/code_instruction.vue';
+import { TrackingActions, TrackingLabels } from '../constants';
export default {
name: 'ConanInstallation',
@@ -22,30 +22,30 @@ export default {
),
},
trackingActions: { ...TrackingActions },
+ TrackingLabels,
};
</script>
<template>
<div>
<h3 class="gl-font-lg">{{ __('Installation') }}</h3>
- <h4 class="gl-font-base">
- {{ s__('PackageRegistry|Conan Command') }}
- </h4>
<code-instruction
+ :label="s__('PackageRegistry|Conan Command')"
:instruction="conanInstallationCommand"
:copy-text="s__('PackageRegistry|Copy Conan Command')"
:tracking-action="$options.trackingActions.COPY_CONAN_COMMAND"
+ :tracking-label="$options.TrackingLabels.CODE_INSTRUCTION"
/>
<h3 class="gl-font-lg">{{ __('Registry setup') }}</h3>
- <h4 class="gl-font-base">
- {{ s__('PackageRegistry|Add Conan Remote') }}
- </h4>
+
<code-instruction
+ :label="s__('PackageRegistry|Add Conan Remote')"
:instruction="conanSetupCommand"
:copy-text="s__('PackageRegistry|Copy Conan Setup Command')"
:tracking-action="$options.trackingActions.COPY_CONAN_SETUP_COMMAND"
+ :tracking-label="$options.TrackingLabels.CODE_INSTRUCTION"
/>
<gl-sprintf :message="$options.i18n.helpText">
<template #link="{ content }">
diff --git a/app/assets/javascripts/packages/details/components/dependency_row.vue b/app/assets/javascripts/packages/details/components/dependency_row.vue
index 788673d2881..1a2202b23c8 100644
--- a/app/assets/javascripts/packages/details/components/dependency_row.vue
+++ b/app/assets/javascripts/packages/details/components/dependency_row.vue
@@ -26,7 +26,7 @@ export default {
<div
v-if="showVersion"
- class="table-section section-50 gl-display-flex justify-content-md-end"
+ class="table-section section-50 gl-display-flex gl-md-justify-content-end"
data-testid="version-pattern"
>
<span class="gl-text-body">{{ dependency.version_pattern }}</span>
diff --git a/app/assets/javascripts/packages/details/components/maven_installation.vue b/app/assets/javascripts/packages/details/components/maven_installation.vue
index d6641c886a0..c2f6f76967b 100644
--- a/app/assets/javascripts/packages/details/components/maven_installation.vue
+++ b/app/assets/javascripts/packages/details/components/maven_installation.vue
@@ -2,8 +2,8 @@
import { GlLink, GlSprintf } from '@gitlab/ui';
import { mapGetters, mapState } from 'vuex';
import { s__ } from '~/locale';
-import CodeInstruction from './code_instruction.vue';
-import { TrackingActions } from '../constants';
+import CodeInstruction from '~/vue_shared/components/registry/code_instruction.vue';
+import { TrackingActions, TrackingLabels } from '../constants';
export default {
name: 'MavenInstallation',
@@ -28,6 +28,7 @@ export default {
),
},
trackingActions: { ...TrackingActions },
+ TrackingLabels,
};
</script>
@@ -35,9 +36,6 @@ export default {
<div>
<h3 class="gl-font-lg">{{ __('Installation') }}</h3>
- <h4 class="gl-font-base">
- {{ s__('PackageRegistry|Maven XML') }}
- </h4>
<p>
<gl-sprintf :message="$options.i18n.xmlText">
<template #code="{ content }">
@@ -45,20 +43,22 @@ export default {
</template>
</gl-sprintf>
</p>
+
<code-instruction
+ :label="s__('PackageRegistry|Maven XML')"
:instruction="mavenInstallationXml"
:copy-text="s__('PackageRegistry|Copy Maven XML')"
multiline
:tracking-action="$options.trackingActions.COPY_MAVEN_XML"
+ :tracking-label="$options.TrackingLabels.CODE_INSTRUCTION"
/>
- <h4 class="gl-font-base">
- {{ s__('PackageRegistry|Maven Command') }}
- </h4>
<code-instruction
+ :label="s__('PackageRegistry|Maven Command')"
:instruction="mavenInstallationCommand"
:copy-text="s__('PackageRegistry|Copy Maven command')"
:tracking-action="$options.trackingActions.COPY_MAVEN_COMMAND"
+ :tracking-label="$options.TrackingLabels.CODE_INSTRUCTION"
/>
<h3 class="gl-font-lg">{{ __('Registry setup') }}</h3>
@@ -74,6 +74,7 @@ export default {
:copy-text="s__('PackageRegistry|Copy Maven registry XML')"
multiline
:tracking-action="$options.trackingActions.COPY_MAVEN_SETUP"
+ :tracking-label="$options.TrackingLabels.CODE_INSTRUCTION"
/>
<gl-sprintf :message="$options.i18n.helpText">
<template #link="{ content }">
diff --git a/app/assets/javascripts/packages/details/components/npm_installation.vue b/app/assets/javascripts/packages/details/components/npm_installation.vue
index d7ff8428370..37ba279d098 100644
--- a/app/assets/javascripts/packages/details/components/npm_installation.vue
+++ b/app/assets/javascripts/packages/details/components/npm_installation.vue
@@ -2,8 +2,8 @@
import { GlLink, GlSprintf } from '@gitlab/ui';
import { mapGetters, mapState } from 'vuex';
import { s__ } from '~/locale';
-import CodeInstruction from './code_instruction.vue';
-import { NpmManager, TrackingActions } from '../constants';
+import CodeInstruction from '~/vue_shared/components/registry/code_instruction.vue';
+import { NpmManager, TrackingActions, TrackingLabels } from '../constants';
export default {
name: 'NpmInstallation',
@@ -34,41 +34,46 @@ export default {
),
},
trackingActions: { ...TrackingActions },
+ TrackingLabels,
};
</script>
<template>
<div>
<h3 class="gl-font-lg">{{ __('Installation') }}</h3>
- <h4 class="gl-font-base">{{ s__('PackageRegistry|npm command') }}</h4>
<code-instruction
+ :label="s__('PackageRegistry|npm command')"
:instruction="npmCommand"
:copy-text="s__('PackageRegistry|Copy npm command')"
:tracking-action="$options.trackingActions.COPY_NPM_INSTALL_COMMAND"
+ :tracking-label="$options.TrackingLabels.CODE_INSTRUCTION"
/>
- <h4 class="gl-font-base">{{ s__('PackageRegistry|yarn command') }}</h4>
<code-instruction
+ :label="s__('PackageRegistry|yarn command')"
:instruction="yarnCommand"
:copy-text="s__('PackageRegistry|Copy yarn command')"
:tracking-action="$options.trackingActions.COPY_YARN_INSTALL_COMMAND"
+ :tracking-label="$options.TrackingLabels.CODE_INSTRUCTION"
/>
<h3 class="gl-font-lg">{{ __('Registry setup') }}</h3>
- <h4 class="gl-font-base">{{ s__('PackageRegistry|npm command') }}</h4>
<code-instruction
+ :label="s__('PackageRegistry|npm command')"
:instruction="npmSetup"
:copy-text="s__('PackageRegistry|Copy npm setup command')"
:tracking-action="$options.trackingActions.COPY_NPM_SETUP_COMMAND"
+ :tracking-label="$options.TrackingLabels.CODE_INSTRUCTION"
/>
- <h4 class="gl-font-base">{{ s__('PackageRegistry|yarn command') }}</h4>
<code-instruction
+ :label="s__('PackageRegistry|yarn command')"
:instruction="yarnSetupCommand"
:copy-text="s__('PackageRegistry|Copy yarn setup command')"
:tracking-action="$options.trackingActions.COPY_YARN_SETUP_COMMAND"
+ :tracking-label="$options.TrackingLabels.CODE_INSTRUCTION"
/>
<gl-sprintf :message="$options.i18n.helpText">
diff --git a/app/assets/javascripts/packages/details/components/nuget_installation.vue b/app/assets/javascripts/packages/details/components/nuget_installation.vue
index 150b6e3ab0f..36887703716 100644
--- a/app/assets/javascripts/packages/details/components/nuget_installation.vue
+++ b/app/assets/javascripts/packages/details/components/nuget_installation.vue
@@ -2,8 +2,8 @@
import { GlLink, GlSprintf } from '@gitlab/ui';
import { mapGetters, mapState } from 'vuex';
import { s__ } from '~/locale';
-import CodeInstruction from './code_instruction.vue';
-import { TrackingActions } from '../constants';
+import CodeInstruction from '~/vue_shared/components/registry/code_instruction.vue';
+import { TrackingActions, TrackingLabels } from '../constants';
export default {
name: 'NugetInstallation',
@@ -22,29 +22,28 @@ export default {
),
},
trackingActions: { ...TrackingActions },
+ TrackingLabels,
};
</script>
<template>
<div>
<h3 class="gl-font-lg">{{ __('Installation') }}</h3>
- <h4 class="gl-font-base">
- {{ s__('PackageRegistry|NuGet Command') }}
- </h4>
<code-instruction
+ :label="s__('PackageRegistry|NuGet Command')"
:instruction="nugetInstallationCommand"
:copy-text="s__('PackageRegistry|Copy NuGet Command')"
:tracking-action="$options.trackingActions.COPY_NUGET_INSTALL_COMMAND"
+ :tracking-label="$options.TrackingLabels.CODE_INSTRUCTION"
/>
<h3 class="gl-font-lg">{{ __('Registry setup') }}</h3>
- <h4 class="gl-font-base">
- {{ s__('PackageRegistry|Add NuGet Source') }}
- </h4>
<code-instruction
+ :label="s__('PackageRegistry|Add NuGet Source')"
:instruction="nugetSetupCommand"
:copy-text="s__('PackageRegistry|Copy NuGet Setup Command')"
:tracking-action="$options.trackingActions.COPY_NUGET_SETUP_COMMAND"
+ :tracking-label="$options.TrackingLabels.CODE_INSTRUCTION"
/>
<gl-sprintf :message="$options.i18n.helpText">
<template #link="{ content }">
diff --git a/app/assets/javascripts/packages/details/components/package_history.vue b/app/assets/javascripts/packages/details/components/package_history.vue
index 96ce106884d..413ab1d15cb 100644
--- a/app/assets/javascripts/packages/details/components/package_history.vue
+++ b/app/assets/javascripts/packages/details/components/package_history.vue
@@ -2,7 +2,7 @@
import { GlLink, GlSprintf } from '@gitlab/ui';
import { s__ } from '~/locale';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
-import HistoryElement from './history_element.vue';
+import HistoryItem from '~/vue_shared/components/registry/history_item.vue';
export default {
name: 'PackageHistory',
@@ -16,7 +16,7 @@ export default {
components: {
GlLink,
GlSprintf,
- HistoryElement,
+ HistoryItem,
TimeAgoTooltip,
},
props: {
@@ -46,7 +46,7 @@ export default {
<div class="issuable-discussion">
<h3 class="gl-font-lg" data-testid="title">{{ __('History') }}</h3>
<ul class="timeline main-notes-list notes gl-mb-4" data-testid="timeline">
- <history-element icon="clock" data-testid="created-on">
+ <history-item icon="clock" data-testid="created-on">
<gl-sprintf :message="$options.i18n.createdOn">
<template #name>
<strong>{{ packageEntity.name }}</strong>
@@ -58,8 +58,8 @@ export default {
<time-ago-tooltip :time="packageEntity.created_at" />
</template>
</gl-sprintf>
- </history-element>
- <history-element icon="pencil" data-testid="updated-at">
+ </history-item>
+ <history-item icon="pencil" data-testid="updated-at">
<gl-sprintf :message="$options.i18n.updatedAtText">
<template #name>
<strong>{{ packageEntity.name }}</strong>
@@ -71,9 +71,9 @@ export default {
<time-ago-tooltip :time="packageEntity.updated_at" />
</template>
</gl-sprintf>
- </history-element>
+ </history-item>
<template v-if="packagePipeline">
- <history-element icon="commit" data-testid="commit">
+ <history-item icon="commit" data-testid="commit">
<gl-sprintf :message="$options.i18n.commitText">
<template #link>
<gl-link :href="packagePipeline.project.commit_url">{{
@@ -84,8 +84,8 @@ export default {
<strong>{{ packagePipeline.ref }}</strong>
</template>
</gl-sprintf>
- </history-element>
- <history-element icon="pipeline" data-testid="pipeline">
+ </history-item>
+ <history-item icon="pipeline" data-testid="pipeline">
<gl-sprintf :message="$options.i18n.pipelineText">
<template #link>
<gl-link :href="packagePipeline.project.pipeline_url"
@@ -97,9 +97,9 @@ export default {
</template>
<template #author>{{ packagePipeline.user.name }}</template>
</gl-sprintf>
- </history-element>
+ </history-item>
</template>
- <history-element icon="package" data-testid="published">
+ <history-item icon="package" data-testid="published">
<gl-sprintf :message="$options.i18n.publishText">
<template #project>
<strong>{{ projectName }}</strong>
@@ -108,7 +108,7 @@ export default {
<time-ago-tooltip :time="packageEntity.created_at" />
</template>
</gl-sprintf>
- </history-element>
+ </history-item>
</ul>
</div>
</template>
diff --git a/app/assets/javascripts/packages/details/components/package_title.vue b/app/assets/javascripts/packages/details/components/package_title.vue
index d07883e3e7a..69dd494f11a 100644
--- a/app/assets/javascripts/packages/details/components/package_title.vue
+++ b/app/assets/javascripts/packages/details/components/package_title.vue
@@ -1,19 +1,21 @@
<script>
import { mapState, mapGetters } from 'vuex';
-import { GlAvatar, GlIcon, GlLink, GlSprintf, GlTooltipDirective } from '@gitlab/ui';
+import { GlIcon, GlSprintf, GlTooltipDirective } from '@gitlab/ui';
import PackageTags from '../../shared/components/package_tags.vue';
import { numberToHumanSize } from '~/lib/utils/number_utils';
import timeagoMixin from '~/vue_shared/mixins/timeago';
+import TitleArea from '~/vue_shared/components/registry/title_area.vue';
+import MetadataItem from '~/vue_shared/components/registry/metadata_item.vue';
import { __ } from '~/locale';
export default {
name: 'PackageTitle',
components: {
- GlAvatar,
+ TitleArea,
GlIcon,
- GlLink,
GlSprintf,
PackageTags,
+ MetadataItem,
},
directives: {
GlTooltip: GlTooltipDirective,
@@ -36,77 +38,49 @@ export default {
</script>
<template>
- <div class="gl-flex-direction-column">
- <div class="gl-display-flex">
- <gl-avatar
- v-if="packageIcon"
- :src="packageIcon"
- shape="rect"
- class="gl-align-self-center gl-mr-4"
- data-testid="package-icon"
- />
-
- <div class="gl-display-flex gl-flex-direction-column">
- <h1 class="gl-font-size-h1 gl-mt-3 gl-mb-2">
- {{ packageEntity.name }}
- </h1>
+ <title-area :title="packageEntity.name" :avatar="packageIcon" data-qa-selector="package_title">
+ <template #sub-header>
+ <gl-icon name="eye" class="gl-mr-3" />
+ <gl-sprintf :message="$options.i18n.packageInfo">
+ <template #version>
+ {{ packageEntity.version }}
+ </template>
- <div class="gl-display-flex gl-align-items-center gl-text-gray-500">
- <gl-icon name="eye" class="gl-mr-3" />
- <gl-sprintf :message="$options.i18n.packageInfo">
- <template #version>
- {{ packageEntity.version }}
- </template>
+ <template #timeAgo>
+ <span v-gl-tooltip :title="tooltipTitle(packageEntity.created_at)">
+ &nbsp;{{ timeFormatted(packageEntity.created_at) }}
+ </span>
+ </template>
+ </gl-sprintf>
+ </template>
- <template #timeAgo>
- <span v-gl-tooltip :title="tooltipTitle(packageEntity.created_at)">
- &nbsp;{{ timeFormatted(packageEntity.created_at) }}
- </span>
- </template>
- </gl-sprintf>
- </div>
- </div>
- </div>
+ <template v-if="packageTypeDisplay" #metadata_type>
+ <metadata-item data-testid="package-type" icon="package" :text="packageTypeDisplay" />
+ </template>
- <div class="gl-display-flex gl-flex-wrap gl-align-items-center gl-mb-3">
- <div v-if="packageTypeDisplay" class="gl-display-flex gl-align-items-center gl-mr-5">
- <gl-icon name="package" class="gl-text-gray-500 gl-mr-3" />
- <span data-testid="package-type" class="gl-font-weight-bold">{{ packageTypeDisplay }}</span>
- </div>
+ <template #metadata_size>
+ <metadata-item data-testid="package-size" icon="disk" :text="totalSize" />
+ </template>
- <div v-if="hasTagsToDisplay" class="gl-display-flex gl-align-items-center gl-mr-5">
- <package-tags :tag-display-limit="1" :tags="packageEntity.tags" />
- </div>
+ <template v-if="packagePipeline" #metadata_pipeline>
+ <metadata-item
+ data-testid="pipeline-project"
+ icon="review-list"
+ :text="packagePipeline.project.name"
+ :link="packagePipeline.project.web_url"
+ />
+ </template>
- <div v-if="packagePipeline" class="gl-display-flex gl-align-items-center gl-mr-5">
- <gl-icon name="review-list" class="gl-text-gray-500 gl-mr-3" />
- <gl-link
- data-testid="pipeline-project"
- :href="packagePipeline.project.web_url"
- class="gl-font-weight-bold text-truncate"
- >
- {{ packagePipeline.project.name }}
- </gl-link>
- </div>
+ <template v-if="packagePipeline" #metadata_ref>
+ <metadata-item data-testid="package-ref" icon="branch" :text="packagePipeline.ref" />
+ </template>
- <div
- v-if="packagePipeline"
- data-testid="package-ref"
- class="gl-display-flex gl-align-items-center gl-mr-5"
- >
- <gl-icon name="branch" class="gl-text-gray-500 gl-mr-3" />
- <span
- v-gl-tooltip
- class="gl-font-weight-bold text-truncate mw-xs"
- :title="packagePipeline.ref"
- >{{ packagePipeline.ref }}</span
- >
- </div>
+ <template v-if="hasTagsToDisplay" #metadata_tags>
+ <package-tags :tag-display-limit="2" :tags="packageEntity.tags" hide-label />
+ </template>
- <div class="gl-display-flex gl-align-items-center gl-mr-5">
- <gl-icon name="disk" class="gl-text-gray-500 gl-mr-3" />
- <span data-testid="package-size" class="gl-font-weight-bold">{{ totalSize }}</span>
- </div>
- </div>
- </div>
+ <template #right-actions>
+ <slot name="delete-button"></slot>
+ </template>
+ </title-area>
</template>
diff --git a/app/assets/javascripts/packages/details/components/pypi_installation.vue b/app/assets/javascripts/packages/details/components/pypi_installation.vue
index f1c619fd6d3..f87be68be48 100644
--- a/app/assets/javascripts/packages/details/components/pypi_installation.vue
+++ b/app/assets/javascripts/packages/details/components/pypi_installation.vue
@@ -2,8 +2,8 @@
import { GlLink, GlSprintf } from '@gitlab/ui';
import { mapGetters, mapState } from 'vuex';
import { s__ } from '~/locale';
-import CodeInstruction from './code_instruction.vue';
-import { TrackingActions } from '../constants';
+import CodeInstruction from '~/vue_shared/components/registry/code_instruction.vue';
+import { TrackingActions, TrackingLabels } from '../constants';
export default {
name: 'PyPiInstallation',
@@ -25,6 +25,7 @@ export default {
),
},
trackingActions: { ...TrackingActions },
+ TrackingLabels,
};
</script>
@@ -32,15 +33,13 @@ export default {
<div>
<h3 class="gl-font-lg">{{ __('Installation') }}</h3>
- <h4 class="gl-font-base">
- {{ s__('PackageRegistry|Pip Command') }}
- </h4>
-
<code-instruction
+ :label="s__('PackageRegistry|Pip Command')"
:instruction="pypiPipCommand"
:copy-text="s__('PackageRegistry|Copy Pip command')"
data-testid="pip-command"
:tracking-action="$options.trackingActions.COPY_PIP_INSTALL_COMMAND"
+ :tracking-label="$options.TrackingLabels.CODE_INSTRUCTION"
/>
<h3 class="gl-font-lg">{{ __('Registry setup') }}</h3>
@@ -58,6 +57,7 @@ export default {
data-testid="pypi-setup-content"
multiline
:tracking-action="$options.trackingActions.COPY_PYPI_SETUP_COMMAND"
+ :tracking-label="$options.TrackingLabels.CODE_INSTRUCTION"
/>
<gl-sprintf :message="$options.i18n.helpText">
<template #link="{ content }">
diff --git a/app/assets/javascripts/packages/details/store/actions.js b/app/assets/javascripts/packages/details/store/actions.js
index cda80056e19..340f60258a0 100644
--- a/app/assets/javascripts/packages/details/store/actions.js
+++ b/app/assets/javascripts/packages/details/store/actions.js
@@ -1,9 +1,10 @@
import Api from '~/api';
import { deprecatedCreateFlash as createFlash } from '~/flash';
import { FETCH_PACKAGE_VERSIONS_ERROR } from '../constants';
+import { DELETE_PACKAGE_ERROR_MESSAGE } from '~/packages/shared/constants';
import * as types from './mutation_types';
-export default ({ commit, state }) => {
+export const fetchPackageVersions = ({ commit, state }) => {
commit(types.SET_LOADING, true);
const { project_id, id } = state.packageEntity;
@@ -21,3 +22,13 @@ export default ({ commit, state }) => {
commit(types.SET_LOADING, false);
});
};
+
+export const deletePackage = ({
+ state: {
+ packageEntity: { project_id, id },
+ },
+}) => {
+ return Api.deleteProjectPackage(project_id, id).catch(() => {
+ createFlash(DELETE_PACKAGE_ERROR_MESSAGE);
+ });
+};
diff --git a/app/assets/javascripts/packages/details/store/getters.js b/app/assets/javascripts/packages/details/store/getters.js
index d1814d506ad..ede6d39bde7 100644
--- a/app/assets/javascripts/packages/details/store/getters.js
+++ b/app/assets/javascripts/packages/details/store/getters.js
@@ -84,10 +84,10 @@ export const npmSetupCommand = ({ packageEntity, npmPath }) => (type = NpmManage
const scope = packageEntity.name.substring(0, packageEntity.name.indexOf('/'));
if (type === NpmManager.NPM) {
- return `echo ${scope}:registry=${npmPath} >> .npmrc`;
+ return `echo ${scope}:registry=${npmPath}/ >> .npmrc`;
}
- return `echo \\"${scope}:registry\\" \\"${npmPath}\\" >> .yarnrc`;
+ return `echo \\"${scope}:registry\\" \\"${npmPath}/\\" >> .yarnrc`;
};
export const nugetInstallationCommand = ({ packageEntity }) =>
diff --git a/app/assets/javascripts/packages/details/store/index.js b/app/assets/javascripts/packages/details/store/index.js
index 9687eb98544..15e17bcfaac 100644
--- a/app/assets/javascripts/packages/details/store/index.js
+++ b/app/assets/javascripts/packages/details/store/index.js
@@ -1,6 +1,6 @@
import Vue from 'vue';
import Vuex from 'vuex';
-import fetchPackageVersions from './actions';
+import * as actions from './actions';
import * as getters from './getters';
import mutations from './mutations';
@@ -8,9 +8,7 @@ Vue.use(Vuex);
export default (initialState = {}) =>
new Vuex.Store({
- actions: {
- fetchPackageVersions,
- },
+ actions,
getters,
mutations,
state: {
diff --git a/app/assets/javascripts/packages/list/components/packages_list.vue b/app/assets/javascripts/packages/list/components/packages_list.vue
index b26c6087e14..7067f70a923 100644
--- a/app/assets/javascripts/packages/list/components/packages_list.vue
+++ b/app/assets/javascripts/packages/list/components/packages_list.vue
@@ -82,11 +82,11 @@ export default {
</script>
<template>
- <div class="d-flex flex-column">
+ <div class="gl-display-flex gl-flex-direction-column">
<slot v-if="isListEmpty && !isLoading" name="empty-state"></slot>
<div v-else-if="isLoading">
- <packages-list-loader :is-group="isGroupPage" />
+ <packages-list-loader />
</div>
<template v-else>
@@ -106,7 +106,7 @@ export default {
:per-page="perPage"
:total-items="totalItems"
align="center"
- class="w-100 mt-2"
+ class="gl-w-full gl-mt-3"
/>
<gl-modal
diff --git a/app/assets/javascripts/packages/list/components/packages_list_app.vue b/app/assets/javascripts/packages/list/components/packages_list_app.vue
index ef242ea5f75..6304f723f6a 100644
--- a/app/assets/javascripts/packages/list/components/packages_list_app.vue
+++ b/app/assets/javascripts/packages/list/components/packages_list_app.vue
@@ -2,11 +2,14 @@
import { mapActions, mapState } from 'vuex';
import { GlEmptyState, GlTab, GlTabs, GlLink, GlSprintf } from '@gitlab/ui';
import { s__, sprintf } from '~/locale';
+import createFlash from '~/flash';
import PackageFilter from './packages_filter.vue';
import PackageList from './packages_list.vue';
import PackageSort from './packages_sort.vue';
-import { PACKAGE_REGISTRY_TABS } from '../constants';
+import { PACKAGE_REGISTRY_TABS, DELETE_PACKAGE_SUCCESS_MESSAGE } from '../constants';
import PackagesComingSoon from '../coming_soon/packages_coming_soon.vue';
+import { SHOW_DELETE_SUCCESS_ALERT } from '~/packages/shared/constants';
+import { historyReplaceState } from '~/lib/utils/common_utils';
export default {
components: {
@@ -34,6 +37,7 @@ export default {
},
mounted() {
this.requestPackagesList();
+ this.checkDeleteAlert();
},
methods: {
...mapActions(['requestPackagesList', 'requestDeletePackage', 'setSelectedType']),
@@ -64,6 +68,16 @@ export default {
return s__('PackageRegistry|There are no packages yet');
},
+ checkDeleteAlert() {
+ const urlParams = new URLSearchParams(window.location.search);
+ const showAlert = urlParams.get(SHOW_DELETE_SUCCESS_ALERT);
+ if (showAlert) {
+ // to be refactored to use gl-alert
+ createFlash({ message: DELETE_PACKAGE_SUCCESS_MESSAGE, type: 'notice' });
+ const cleanUrl = window.location.href.split('?')[0];
+ historyReplaceState(cleanUrl);
+ }
+ },
},
i18n: {
widenFilters: s__('PackageRegistry|To widen your search, change or remove the filters above.'),
@@ -77,7 +91,9 @@ export default {
<template>
<gl-tabs @input="tabChanged">
<template #tabs-end>
- <div class="d-flex align-self-center ml-md-auto py-1 py-md-0">
+ <div
+ class="gl-display-flex gl-align-self-center gl-py-2 gl-flex-grow-1 gl-justify-content-end"
+ >
<package-filter class="mr-1" @filter="requestPackagesList" />
<package-sort @sort:changed="requestPackagesList" />
</div>
diff --git a/app/assets/javascripts/packages/list/constants.js b/app/assets/javascripts/packages/list/constants.js
index 510d04965cb..0ff8c86362d 100644
--- a/app/assets/javascripts/packages/list/constants.js
+++ b/app/assets/javascripts/packages/list/constants.js
@@ -5,7 +5,6 @@ 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_ERROR_MESSAGE = __('Something went wrong while deleting the package.');
export const DELETE_PACKAGE_SUCCESS_MESSAGE = __('Package deleted successfully');
export const DEFAULT_PAGE = 1;
diff --git a/app/assets/javascripts/packages/list/stores/actions.js b/app/assets/javascripts/packages/list/stores/actions.js
index 0ed24aee2c5..bbc11e3cf13 100644
--- a/app/assets/javascripts/packages/list/stores/actions.js
+++ b/app/assets/javascripts/packages/list/stores/actions.js
@@ -1,10 +1,10 @@
import Api from '~/api';
import axios from '~/lib/utils/axios_utils';
import { deprecatedCreateFlash as createFlash } from '~/flash';
+import { DELETE_PACKAGE_ERROR_MESSAGE } from '~/packages/shared/constants';
import * as types from './mutation_types';
import {
FETCH_PACKAGES_LIST_ERROR_MESSAGE,
- DELETE_PACKAGE_ERROR_MESSAGE,
DELETE_PACKAGE_SUCCESS_MESSAGE,
DEFAULT_PAGE,
DEFAULT_PAGE_SIZE,
diff --git a/app/assets/javascripts/packages/shared/components/package_list_row.vue b/app/assets/javascripts/packages/shared/components/package_list_row.vue
index e000279b794..f93bc51d185 100644
--- a/app/assets/javascripts/packages/shared/components/package_list_row.vue
+++ b/app/assets/javascripts/packages/shared/components/package_list_row.vue
@@ -1,9 +1,10 @@
<script>
-import { GlButton, GlIcon, GlLink, GlSprintf, GlTooltipDirective } from '@gitlab/ui';
+import { GlButton, GlIcon, GlLink, GlSprintf, GlTooltipDirective, GlTruncate } from '@gitlab/ui';
import PackageTags from './package_tags.vue';
import PublishMethod from './publish_method.vue';
import { getPackageTypeLabel } from '../utils';
import timeagoMixin from '~/vue_shared/mixins/timeago';
+import ListItem from '~/vue_shared/components/registry/list_item.vue';
export default {
name: 'PackageListRow',
@@ -12,8 +13,10 @@ export default {
GlIcon,
GlLink,
GlSprintf,
+ GlTruncate,
PackageTags,
PublishMethod,
+ ListItem,
},
directives: {
GlTooltip: GlTooltipDirective,
@@ -59,15 +62,15 @@ export default {
</script>
<template>
- <div class="gl-responsive-table-row" data-qa-selector="packages-row">
- <div class="table-section section-50 d-flex flex-md-column justify-content-between flex-wrap">
- <div class="d-flex align-items-center mr-2">
+ <list-item data-qa-selector="package_row">
+ <template #left-primary>
+ <div class="gl-display-flex gl-align-items-center gl-mr-3 gl-min-w-0">
<gl-link
:href="packageLink"
+ class="gl-text-body gl-min-w-0"
data-qa-selector="package_link"
- class="text-dark font-weight-bold mb-md-1"
>
- {{ packageEntity.name }}
+ <gl-truncate :text="packageEntity.name" />
</gl-link>
<package-tags
@@ -78,41 +81,42 @@ export default {
:tag-display-limit="1"
/>
</div>
-
- <div class="d-flex text-secondary text-truncate mt-md-2">
+ </template>
+ <template #left-secondary>
+ <div class="gl-display-flex">
<span>{{ packageEntity.version }}</span>
- <div v-if="hasPipeline" class="d-none d-md-inline-block ml-1">
+ <div v-if="hasPipeline" class="gl-display-none gl-display-sm-flex gl-ml-2">
<gl-sprintf :message="s__('PackageRegistry|published by %{author}')">
<template #author>{{ packageEntity.pipeline.user.name }}</template>
</gl-sprintf>
</div>
- <div v-if="hasProjectLink" class="d-flex align-items-center">
- <gl-icon name="review-list" class="text-secondary ml-2 mr-1" />
+ <div v-if="hasProjectLink" class="gl-display-flex gl-align-items-center">
+ <gl-icon name="review-list" class="gl-ml-3 gl-mr-2 gl-min-w-0" />
<gl-link
+ class="gl-text-body gl-min-w-0"
data-testid="packages-row-project"
:href="`/${packageEntity.project_path}`"
- class="text-secondary"
- >{{ packageEntity.projectPathName }}</gl-link
>
+ <gl-truncate :text="packageEntity.projectPathName" />
+ </gl-link>
</div>
<div v-if="showPackageType" class="d-flex align-items-center" data-testid="package-type">
- <gl-icon name="package" class="text-secondary ml-2 mr-1" />
+ <gl-icon name="package" class="gl-ml-3 gl-mr-2" />
<span>{{ packageType }}</span>
</div>
</div>
- </div>
+ </template>
- <div
- class="table-section d-flex flex-md-column justify-content-between align-items-md-end"
- :class="disableDelete ? 'section-50' : 'section-40'"
- >
+ <template #right-primary>
<publish-method :package-entity="packageEntity" :is-group="isGroup" />
+ </template>
- <div class="text-secondary order-0 order-md-1 mt-md-2">
+ <template #right-secondary>
+ <span>
<gl-sprintf :message="__('Created %{timestamp}')">
<template #timestamp>
<span v-gl-tooltip :title="tooltipTitle(packageEntity.created_at)">
@@ -120,10 +124,10 @@ export default {
</span>
</template>
</gl-sprintf>
- </div>
- </div>
+ </span>
+ </template>
- <div v-if="!disableDelete" class="table-section section-10 d-flex justify-content-end">
+ <template v-if="!disableDelete" #right-action>
<gl-button
data-testid="action-delete"
icon="remove"
@@ -134,6 +138,6 @@ export default {
:disabled="!packageEntity._links.delete_api_path"
@click="$emit('packageToDelete', packageEntity)"
/>
- </div>
- </div>
+ </template>
+ </list-item>
</template>
diff --git a/app/assets/javascripts/packages/shared/components/package_tags.vue b/app/assets/javascripts/packages/shared/components/package_tags.vue
index 391f53c225b..3d7e233c1ba 100644
--- a/app/assets/javascripts/packages/shared/components/package_tags.vue
+++ b/app/assets/javascripts/packages/shared/components/package_tags.vue
@@ -80,6 +80,7 @@ export default {
data-testid="tagBadge"
:class="tagBadgeClass(index)"
variant="info"
+ size="sm"
>{{ tag.name }}</gl-badge
>
@@ -89,7 +90,8 @@ export default {
data-testid="moreBadge"
variant="muted"
:title="moreTagsTooltip"
- class="gl-display-none d-md-flex gl-ml-2"
+ size="sm"
+ class="gl-display-none gl-display-md-flex gl-ml-2"
><gl-sprintf :message="__('+%{tags} more')">
<template #tags>
{{ moreTagsDisplay }}
@@ -101,7 +103,7 @@ export default {
v-if="moreTagsDisplay && hideLabel"
data-testid="moreBadge"
variant="muted"
- class="d-md-none gl-ml-2"
+ class="gl-display-md-none gl-ml-2"
>{{ tagsDisplay }}</gl-badge
>
</div>
diff --git a/app/assets/javascripts/packages/shared/components/packages_list_loader.vue b/app/assets/javascripts/packages/shared/components/packages_list_loader.vue
index cd9ef74d467..efd9f8db908 100644
--- a/app/assets/javascripts/packages/shared/components/packages_list_loader.vue
+++ b/app/assets/javascripts/packages/shared/components/packages_list_loader.vue
@@ -5,40 +5,13 @@ export default {
components: {
GlSkeletonLoader,
},
- props: {
- isGroup: {
- type: Boolean,
- required: false,
- default: false,
- },
- },
- computed: {
- desktopShapes() {
- return this.isGroup ? this.$options.shapes.groups : this.$options.shapes.projects;
- },
- desktopHeight() {
- return this.isGroup ? 38 : 54;
- },
- mobileHeight() {
- return this.isGroup ? 160 : 170;
- },
- },
- shapes: {
- groups: [
- { type: 'rect', width: '100', height: '10', x: '0', y: '15' },
- { type: 'rect', width: '100', height: '10', x: '195', y: '15' },
- { type: 'rect', width: '60', height: '10', x: '475', y: '15' },
- { type: 'rect', width: '60', height: '10', x: '675', y: '15' },
- { type: 'rect', width: '100', height: '10', x: '900', y: '15' },
- ],
- projects: [
- { type: 'rect', width: '220', height: '10', x: '0', y: '20' },
- { type: 'rect', width: '60', height: '10', x: '305', y: '20' },
- { type: 'rect', width: '60', height: '10', x: '535', y: '20' },
- { type: 'rect', width: '100', height: '10', x: '760', y: '20' },
- { type: 'rect', width: '30', height: '30', x: '970', y: '10', ref: 'button-loader' },
- ],
- },
+ shapes: [
+ { type: 'rect', width: '220', height: '10', x: '0', y: '20' },
+ { type: 'rect', width: '60', height: '10', x: '305', y: '20' },
+ { type: 'rect', width: '60', height: '10', x: '535', y: '20' },
+ { type: 'rect', width: '100', height: '10', x: '760', y: '20' },
+ { type: 'rect', width: '30', height: '30', x: '970', y: '10', ref: 'button-loader' },
+ ],
rowsToRender: {
mobile: 5,
desktop: 20,
@@ -48,34 +21,35 @@ export default {
<template>
<div>
- <div class="d-xs-flex flex-column d-md-none">
+ <div class="gl-flex-direction-column gl-display-sm-none" data-testid="mobile-loader">
<gl-skeleton-loader
v-for="index in $options.rowsToRender.mobile"
:key="index"
:width="500"
- :height="mobileHeight"
+ :height="170"
preserve-aspect-ratio="xMinYMax meet"
>
<rect width="500" height="10" x="0" y="15" rx="4" />
<rect width="500" height="10" x="0" y="45" rx="4" />
<rect width="500" height="10" x="0" y="75" rx="4" />
<rect width="500" height="10" x="0" y="105" rx="4" />
- <rect v-if="isGroup" width="500" height="10" x="0" y="135" rx="4" />
- <rect v-else width="30" height="30" x="470" y="135" rx="4" />
+ <rect width="500" height="10" x="0" y="135" rx="4" />
</gl-skeleton-loader>
</div>
-
- <div class="d-none d-md-flex flex-column">
+ <div
+ class="gl-display-none gl-display-sm-flex gl-flex-direction-column"
+ data-testid="desktop-loader"
+ >
<gl-skeleton-loader
v-for="index in $options.rowsToRender.desktop"
:key="index"
:width="1000"
- :height="desktopHeight"
+ :height="54"
preserve-aspect-ratio="xMinYMax meet"
>
<component
:is="r.type"
- v-for="(r, rIndex) in desktopShapes"
+ v-for="(r, rIndex) in $options.shapes"
:key="rIndex"
rx="4"
v-bind="r"
diff --git a/app/assets/javascripts/packages/shared/components/publish_method.vue b/app/assets/javascripts/packages/shared/components/publish_method.vue
index 1e18562a421..d17e23c4032 100644
--- a/app/assets/javascripts/packages/shared/components/publish_method.vue
+++ b/app/assets/javascripts/packages/shared/components/publish_method.vue
@@ -36,26 +36,28 @@ export default {
</script>
<template>
- <div class="d-flex align-items-center text-secondary order-1 order-md-0 mb-md-1">
+ <div class="gl-display-flex gl-align-items-center">
<template v-if="hasPipeline">
- <gl-icon name="git-merge" class="mr-1" />
- <strong ref="pipeline-ref" class="mr-1 text-dark">{{ packageEntity.pipeline.ref }}</strong>
+ <gl-icon name="git-merge" class="gl-mr-2" />
+ <span data-testid="pipeline-ref" class="gl-mr-2">{{ packageEntity.pipeline.ref }}</span>
- <gl-icon name="commit" class="mr-1" />
- <gl-link ref="pipeline-sha" :href="linkToCommit" class="mr-1">{{ packageShaShort }}</gl-link>
+ <gl-icon name="commit" class="gl-mr-2" />
+ <gl-link data-testid="pipeline-sha" :href="linkToCommit" class="gl-mr-2">{{
+ packageShaShort
+ }}</gl-link>
<clipboard-button
:text="packageEntity.pipeline.sha"
:title="__('Copy commit SHA')"
- css-class="border-0 text-secondary py-0 px-1"
+ css-class="gl-border-0 gl-py-0 gl-px-2"
/>
</template>
<template v-else>
- <gl-icon name="upload" class="mr-1" />
- <strong ref="manual-ref" class="text-dark">{{
- s__('PackageRegistry|Manually Published')
- }}</strong>
+ <gl-icon name="upload" class="gl-mr-2" />
+ <span data-testid="manually-published">
+ {{ s__('PackageRegistry|Manually Published') }}
+ </span>
</template>
</div>
</template>
diff --git a/app/assets/javascripts/packages/shared/constants.js b/app/assets/javascripts/packages/shared/constants.js
index 279c2959fa9..e5131db59bf 100644
--- a/app/assets/javascripts/packages/shared/constants.js
+++ b/app/assets/javascripts/packages/shared/constants.js
@@ -1,3 +1,5 @@
+import { __ } from '~/locale';
+
export const PackageType = {
CONAN: 'conan',
MAVEN: 'maven',
@@ -22,3 +24,6 @@ export const TrackingCategories = {
[PackageType.NPM]: 'NpmPackages',
[PackageType.CONAN]: 'ConanPackages',
};
+
+export const SHOW_DELETE_SUCCESS_ALERT = 'showSuccessDeleteAlert';
+export const DELETE_PACKAGE_ERROR_MESSAGE = __('Something went wrong while deleting the package.');
diff --git a/app/assets/javascripts/pages/admin/application_settings/metrics_and_profiling/index.js b/app/assets/javascripts/pages/admin/application_settings/metrics_and_profiling/index.js
index bbaaeb55c65..a2fca238613 100644
--- a/app/assets/javascripts/pages/admin/application_settings/metrics_and_profiling/index.js
+++ b/app/assets/javascripts/pages/admin/application_settings/metrics_and_profiling/index.js
@@ -1,3 +1,3 @@
-import setup from 'ee_else_ce/admin/application_settings/setup_metrics_and_profiling';
+import setup from '~/admin/application_settings/setup_metrics_and_profiling';
document.addEventListener('DOMContentLoaded', setup);
diff --git a/app/assets/javascripts/pages/admin/application_settings/payload_previewer.js b/app/assets/javascripts/pages/admin/application_settings/payload_previewer.js
index e7b468f039f..f8fc53799a8 100644
--- a/app/assets/javascripts/pages/admin/application_settings/payload_previewer.js
+++ b/app/assets/javascripts/pages/admin/application_settings/payload_previewer.js
@@ -3,9 +3,9 @@ import { __ } from '../../../locale';
import { deprecatedCreateFlash as flash } from '../../../flash';
export default class PayloadPreviewer {
- constructor(trigger, container) {
+ constructor(trigger) {
this.trigger = trigger;
- this.container = container;
+ this.container = document.querySelector(trigger.dataset.payloadSelector);
this.isVisible = false;
this.isInserted = false;
}
diff --git a/app/assets/javascripts/pages/admin/clusters/new/index.js b/app/assets/javascripts/pages/admin/clusters/new/index.js
new file mode 100644
index 00000000000..876bab0b339
--- /dev/null
+++ b/app/assets/javascripts/pages/admin/clusters/new/index.js
@@ -0,0 +1,5 @@
+import initNewCluster from '~/clusters/new_cluster';
+
+document.addEventListener('DOMContentLoaded', () => {
+ initNewCluster();
+});
diff --git a/app/assets/javascripts/pages/admin/cohorts/index.js b/app/assets/javascripts/pages/admin/cohorts/index.js
new file mode 100644
index 00000000000..1cc54df15a1
--- /dev/null
+++ b/app/assets/javascripts/pages/admin/cohorts/index.js
@@ -0,0 +1,22 @@
+import Vue from 'vue';
+import UsagePingDisabled from '~/admin/cohorts/components/usage_ping_disabled.vue';
+
+document.addEventListener('DOMContentLoaded', () => {
+ const emptyStateContainer = document.getElementById('js-cohorts-empty-state');
+
+ if (!emptyStateContainer) return false;
+
+ const { emptyStateSvgPath, enableUsagePingLink, docsLink } = emptyStateContainer.dataset;
+
+ return new Vue({
+ el: emptyStateContainer,
+ provide: {
+ svgPath: emptyStateSvgPath,
+ primaryButtonPath: enableUsagePingLink,
+ docsLink,
+ },
+ render(h) {
+ return h(UsagePingDisabled);
+ },
+ });
+});
diff --git a/app/assets/javascripts/pages/admin/dev_ops_report/index.js b/app/assets/javascripts/pages/admin/dev_ops_report/index.js
new file mode 100644
index 00000000000..643497003ba
--- /dev/null
+++ b/app/assets/javascripts/pages/admin/dev_ops_report/index.js
@@ -0,0 +1,27 @@
+import Vue from 'vue';
+import UserCallout from '~/user_callout';
+import UsagePingDisabled from '~/admin/dev_ops_report/components/usage_ping_disabled.vue';
+
+document.addEventListener('DOMContentLoaded', () => {
+ // eslint-disable-next-line no-new
+ new UserCallout();
+
+ const emptyStateContainer = document.getElementById('js-devops-empty-state');
+
+ if (!emptyStateContainer) return false;
+
+ const { emptyStateSvgPath, enableUsagePingLink, docsLink, isAdmin } = emptyStateContainer.dataset;
+
+ return new Vue({
+ el: emptyStateContainer,
+ provide: {
+ isAdmin: Boolean(isAdmin),
+ svgPath: emptyStateSvgPath,
+ primaryButtonPath: enableUsagePingLink,
+ docsLink,
+ },
+ render(h) {
+ return h(UsagePingDisabled);
+ },
+ });
+});
diff --git a/app/assets/javascripts/pages/admin/projects/index/components/delete_project_modal.vue b/app/assets/javascripts/pages/admin/projects/index/components/delete_project_modal.vue
index 8bb093da771..b92fc8d125d 100644
--- a/app/assets/javascripts/pages/admin/projects/index/components/delete_project_modal.vue
+++ b/app/assets/javascripts/pages/admin/projects/index/components/delete_project_modal.vue
@@ -1,11 +1,14 @@
<script>
+import { GlSafeHtmlDirective as SafeHtml, GlModal } from '@gitlab/ui';
import { escape } from 'lodash';
-import DeprecatedModal from '~/vue_shared/components/deprecated_modal.vue';
-import { s__, sprintf } from '~/locale';
+import { __, s__, sprintf } from '~/locale';
export default {
components: {
- DeprecatedModal,
+ GlModal,
+ },
+ directives: {
+ SafeHtml,
},
props: {
deleteProjectUrl: {
@@ -62,51 +65,57 @@ export default {
false,
);
},
- primaryButtonLabel() {
- return s__('AdminProjects|Delete project');
- },
canSubmit() {
return this.enteredProjectName === this.projectName;
},
+ primaryProps() {
+ return {
+ text: s__('Delete project'),
+ attributes: [{ variant: 'danger' }, { category: 'primary' }, { disabled: !this.canSubmit }],
+ };
+ },
},
methods: {
onCancel() {
this.enteredProjectName = '';
},
onSubmit() {
+ if (!this.canSubmit) {
+ return;
+ }
this.$refs.form.submit();
this.enteredProjectName = '';
},
},
+ cancelProps: {
+ text: __('Cancel'),
+ },
};
</script>
<template>
- <deprecated-modal
- id="delete-project-modal"
+ <gl-modal
+ modal-id="delete-project-modal"
:title="title"
- :text="text"
- :primary-button-label="primaryButtonLabel"
- :submit-disabled="!canSubmit"
- kind="danger"
- @submit="onSubmit"
+ :action-primary="primaryProps"
+ :action-cancel="$options.cancelProps"
+ :ok-disabled="!canSubmit"
+ @primary="onSubmit"
@cancel="onCancel"
>
- <template #body="props">
- <p v-html="props.text"></p>
- <p v-html="confirmationTextLabel"></p>
- <form ref="form" :action="deleteProjectUrl" method="post">
- <input ref="method" type="hidden" name="_method" value="delete" />
- <input :value="csrfToken" type="hidden" name="authenticity_token" />
- <input
- v-model="enteredProjectName"
- name="projectName"
- class="form-control"
- type="text"
- aria-labelledby="input-label"
- autocomplete="off"
- />
- </form>
- </template>
- </deprecated-modal>
+ <p v-safe-html="text"></p>
+ <p v-safe-html="confirmationTextLabel"></p>
+ <form ref="form" :action="deleteProjectUrl" method="post">
+ <input ref="method" type="hidden" name="_method" value="delete" />
+ <input :value="csrfToken" type="hidden" name="authenticity_token" />
+ <input
+ v-model="enteredProjectName"
+ name="projectName"
+ class="form-control"
+ type="text"
+ aria-labelledby="input-label"
+ autocomplete="off"
+ />
+ </form>
+ </gl-modal>
</template>
diff --git a/app/assets/javascripts/pages/admin/projects/index/index.js b/app/assets/javascripts/pages/admin/projects/index/index.js
index 6fa8760545d..ebb1a74e970 100644
--- a/app/assets/javascripts/pages/admin/projects/index/index.js
+++ b/app/assets/javascripts/pages/admin/projects/index/index.js
@@ -1,4 +1,3 @@
-import $ from 'jquery';
import Vue from 'vue';
import Translate from '~/vue_shared/translate';
@@ -17,6 +16,18 @@ document.addEventListener('DOMContentLoaded', () => {
deleteProjectUrl: '',
projectName: '',
},
+ mounted() {
+ const deleteProjectButtons = document.querySelectorAll('.delete-project-button');
+ deleteProjectButtons.forEach(button => {
+ button.addEventListener('click', () => {
+ const buttonProps = button.dataset;
+ deleteModal.deleteProjectUrl = buttonProps.deleteProjectUrl;
+ deleteModal.projectName = buttonProps.projectName;
+
+ this.$root.$emit('bv::show::modal', 'delete-project-modal');
+ });
+ });
+ },
render(createElement) {
return createElement(deleteProjectModal, {
props: {
@@ -27,12 +38,4 @@ document.addEventListener('DOMContentLoaded', () => {
});
},
});
-
- $(document).on('shown.bs.modal', event => {
- if (event.relatedTarget.classList.contains('delete-project-button')) {
- const buttonProps = event.relatedTarget.dataset;
- deleteModal.deleteProjectUrl = buttonProps.deleteProjectUrl;
- deleteModal.projectName = buttonProps.projectName;
- }
- });
});
diff --git a/app/assets/javascripts/pages/admin/services/index/index.js b/app/assets/javascripts/pages/admin/services/index/index.js
new file mode 100644
index 00000000000..b2dfbb5a9fc
--- /dev/null
+++ b/app/assets/javascripts/pages/admin/services/index/index.js
@@ -0,0 +1,6 @@
+import PersistentUserCallout from '~/persistent_user_callout';
+
+document.addEventListener('DOMContentLoaded', () => {
+ const callout = document.querySelector('.js-service-templates-deprecated');
+ PersistentUserCallout.factory(callout);
+});
diff --git a/app/assets/javascripts/pages/admin/users/components/delete_user_modal.vue b/app/assets/javascripts/pages/admin/users/components/delete_user_modal.vue
index e09b8e1bdd5..9c303cc6445 100644
--- a/app/assets/javascripts/pages/admin/users/components/delete_user_modal.vue
+++ b/app/assets/javascripts/pages/admin/users/components/delete_user_modal.vue
@@ -1,6 +1,5 @@
<script>
-import { escape } from 'lodash';
-import { GlModal, GlButton, GlFormInput } from '@gitlab/ui';
+import { GlModal, GlButton, GlFormInput, GlSprintf } from '@gitlab/ui';
import { s__, sprintf } from '~/locale';
export default {
@@ -8,6 +7,7 @@ export default {
GlModal,
GlButton,
GlFormInput,
+ GlSprintf,
},
props: {
title: {
@@ -52,27 +52,6 @@ export default {
modalTitle() {
return sprintf(this.title, { username: this.username });
},
- text() {
- return sprintf(
- this.content,
- {
- username: `<strong>${escape(this.username)}</strong>`,
- strong_start: '<strong>',
- strong_end: '</strong>',
- },
- false,
- );
- },
- confirmationTextLabel() {
- return sprintf(
- s__('AdminUsers|To confirm, type %{username}'),
- {
- username: `<code>${escape(this.username)}</code>`,
- },
- false,
- );
- },
-
secondaryButtonLabel() {
return s__('AdminUsers|Block user');
},
@@ -107,8 +86,25 @@ export default {
<template>
<gl-modal ref="modal" modal-id="delete-user-modal" :title="modalTitle" kind="danger">
<template>
- <p v-html="text"></p>
- <p v-html="confirmationTextLabel"></p>
+ <p>
+ <gl-sprintf :message="content">
+ <template #username>
+ <strong>{{ username }}</strong>
+ </template>
+ <template #strong="props">
+ <strong>{{ props.content }}</strong>
+ </template>
+ </gl-sprintf>
+ </p>
+
+ <p>
+ <gl-sprintf :message="s__('AdminUsers|To confirm, type %{username}')">
+ <template #username>
+ <code>{{ username }}</code>
+ </template>
+ </gl-sprintf>
+ </p>
+
<form ref="form" :action="deleteUserUrl" method="post" @submit.prevent>
<input ref="method" type="hidden" name="_method" value="delete" />
<input :value="csrfToken" type="hidden" name="authenticity_token" />
diff --git a/app/assets/javascripts/pages/admin/users/components/user_operation_confirmation_modal.vue b/app/assets/javascripts/pages/admin/users/components/user_operation_confirmation_modal.vue
index 4c335cfb018..4ca6ce6f1c3 100644
--- a/app/assets/javascripts/pages/admin/users/components/user_operation_confirmation_modal.vue
+++ b/app/assets/javascripts/pages/admin/users/components/user_operation_confirmation_modal.vue
@@ -1,4 +1,5 @@
<script>
+/* eslint-disable vue/no-v-html */
import { GlModal } from '@gitlab/ui';
import { sprintf } from '~/locale';
diff --git a/app/assets/javascripts/pages/constants.js b/app/assets/javascripts/pages/constants.js
index 35c67190b62..a9773807212 100644
--- a/app/assets/javascripts/pages/constants.js
+++ b/app/assets/javascripts/pages/constants.js
@@ -1,5 +1,3 @@
-/* eslint-disable import/prefer-default-export */
-
export const FILTERED_SEARCH = {
MERGE_REQUESTS: 'merge_requests',
ISSUES: 'issues',
diff --git a/app/assets/javascripts/pages/dashboard/projects/index/components/customize_homepage_banner.vue b/app/assets/javascripts/pages/dashboard/projects/index/components/customize_homepage_banner.vue
index 6b907f31a37..9fa441348c7 100644
--- a/app/assets/javascripts/pages/dashboard/projects/index/components/customize_homepage_banner.vue
+++ b/app/assets/javascripts/pages/dashboard/projects/index/components/customize_homepage_banner.vue
@@ -2,11 +2,15 @@
import { GlBanner } from '@gitlab/ui';
import { s__ } from '~/locale';
import axios from '~/lib/utils/axios_utils';
+import Tracking from '~/tracking';
+
+const trackingMixin = Tracking.mixin();
export default {
components: {
GlBanner,
},
+ mixins: [trackingMixin],
inject: {
svgPath: {
default: '',
@@ -20,6 +24,9 @@ export default {
calloutsFeatureId: {
default: '',
},
+ trackLabel: {
+ default: '',
+ },
},
i18n: {
title: s__('CustomizeHomepageBanner|Do you want to customize this page?'),
@@ -31,8 +38,19 @@ export default {
data() {
return {
visible: true,
+ tracking: {
+ label: this.trackLabel,
+ },
};
},
+ created() {
+ this.$nextTick(() => {
+ this.addTrackingAttributesToButton();
+ });
+ },
+ mounted() {
+ this.trackOnShow();
+ },
methods: {
handleClose() {
axios
@@ -45,6 +63,23 @@ export default {
});
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-event', 'click_go_to_preferences');
+ button.setAttribute('data-track-label', this.trackLabel);
+ }
},
},
};
@@ -53,6 +88,7 @@ export default {
<template>
<gl-banner
v-if="visible"
+ ref="banner"
:title="$options.i18n.title"
:button-text="$options.i18n.button_text"
:button-link="preferencesBehaviorPath"
diff --git a/app/assets/javascripts/pages/dashboard/todos/index/todos.js b/app/assets/javascripts/pages/dashboard/todos/index/todos.js
index f76b4b44452..6f8d954d798 100644
--- a/app/assets/javascripts/pages/dashboard/todos/index/todos.js
+++ b/app/assets/javascripts/pages/dashboard/todos/index/todos.js
@@ -1,7 +1,7 @@
/* eslint-disable class-methods-use-this, no-unneeded-ternary */
import $ from 'jquery';
-import '~/gl_dropdown';
+import initDeprecatedJQueryDropdown from '~/deprecated_jquery_dropdown';
import { visitUrl } from '~/lib/utils/url_utility';
import UsersSelect from '~/users_select';
import { isMetaClick } from '~/lib/utils/common_utils';
@@ -50,7 +50,7 @@ export default class Todos {
}
initFilterDropdown($dropdown, fieldName, searchFields) {
- $dropdown.glDropdown({
+ initDeprecatedJQueryDropdown($dropdown, {
fieldName,
selectable: true,
filterable: searchFields ? true : false,
diff --git a/app/assets/javascripts/pages/groups/clusters/new/index.js b/app/assets/javascripts/pages/groups/clusters/new/index.js
new file mode 100644
index 00000000000..876bab0b339
--- /dev/null
+++ b/app/assets/javascripts/pages/groups/clusters/new/index.js
@@ -0,0 +1,5 @@
+import initNewCluster from '~/clusters/new_cluster';
+
+document.addEventListener('DOMContentLoaded', () => {
+ initNewCluster();
+});
diff --git a/app/assets/javascripts/pages/groups/group_members/index.js b/app/assets/javascripts/pages/groups/group_members/index.js
index e146592e134..3fa3a132dfa 100644
--- a/app/assets/javascripts/pages/groups/group_members/index.js
+++ b/app/assets/javascripts/pages/groups/group_members/index.js
@@ -4,6 +4,7 @@ import memberExpirationDate from '~/member_expiration_date';
import UsersSelect from '~/users_select';
import groupsSelect from '~/groups_select';
import RemoveMemberModal from '~/vue_shared/components/remove_member_modal.vue';
+import initGroupMembersApp from '~/groups/members';
function mountRemoveMemberModal() {
const el = document.querySelector('.js-remove-member-modal');
@@ -25,6 +26,11 @@ document.addEventListener('DOMContentLoaded', () => {
memberExpirationDate('.js-access-expiration-date-groups');
mountRemoveMemberModal();
+ initGroupMembersApp(document.querySelector('.js-group-members-list'));
+ initGroupMembersApp(document.querySelector('.js-group-linked-list'));
+ initGroupMembersApp(document.querySelector('.js-group-invited-members-list'));
+ initGroupMembersApp(document.querySelector('.js-group-access-requests-list'));
+
new Members(); // eslint-disable-line no-new
new UsersSelect(); // eslint-disable-line no-new
});
diff --git a/app/assets/javascripts/pages/groups/issues/index.js b/app/assets/javascripts/pages/groups/issues/index.js
index 2496003919a..ae481d16ee9 100644
--- a/app/assets/javascripts/pages/groups/issues/index.js
+++ b/app/assets/javascripts/pages/groups/issues/index.js
@@ -1,5 +1,5 @@
import IssuableFilteredSearchTokenKeys from 'ee_else_ce/filtered_search/issuable_filtered_search_token_keys';
-import initIssuablesList from '~/issuables_list';
+import initIssuablesList from '~/issues_list';
import projectSelect from '~/project_select';
import initFilteredSearch from '~/pages/search/init_filtered_search';
import issuableInitBulkUpdateSidebar from '~/issuable_init_bulk_update_sidebar';
diff --git a/app/assets/javascripts/pages/groups/shared/group_details.js b/app/assets/javascripts/pages/groups/shared/group_details.js
index 37b253d7c48..8546b1f759f 100644
--- a/app/assets/javascripts/pages/groups/shared/group_details.js
+++ b/app/assets/javascripts/pages/groups/shared/group_details.js
@@ -8,6 +8,7 @@ import NotificationsForm from '~/notifications_form';
import ProjectsList from '~/projects_list';
import ShortcutsNavigation from '~/behaviors/shortcuts/shortcuts_navigation';
import GroupTabs from './group_tabs';
+import initInviteMembersBanner from '~/groups/init_invite_members_banner';
export default function initGroupDetails(actionName = 'show') {
const newGroupChildWrapper = document.querySelector('.js-new-project-subgroup');
@@ -27,4 +28,5 @@ export default function initGroupDetails(actionName = 'show') {
if (newGroupChildWrapper) {
new NewGroupChild(newGroupChildWrapper);
}
+ initInviteMembersBanner();
}
diff --git a/app/assets/javascripts/pages/instance_statistics/dev_ops_score/index.js b/app/assets/javascripts/pages/instance_statistics/dev_ops_score/index.js
deleted file mode 100644
index c1056537f90..00000000000
--- a/app/assets/javascripts/pages/instance_statistics/dev_ops_score/index.js
+++ /dev/null
@@ -1,3 +0,0 @@
-import UserCallout from '~/user_callout';
-
-document.addEventListener('DOMContentLoaded', () => new UserCallout());
diff --git a/app/assets/javascripts/pages/milestones/shared/components/delete_milestone_modal.vue b/app/assets/javascripts/pages/milestones/shared/components/delete_milestone_modal.vue
index 5be8e6697a2..983062c79f1 100644
--- a/app/assets/javascripts/pages/milestones/shared/components/delete_milestone_modal.vue
+++ b/app/assets/javascripts/pages/milestones/shared/components/delete_milestone_modal.vue
@@ -1,4 +1,5 @@
<script>
+import { GlSafeHtmlDirective as SafeHtml } from '@gitlab/ui';
import axios from '~/lib/utils/axios_utils';
import { deprecatedCreateFlash as Flash } from '~/flash';
@@ -11,6 +12,9 @@ export default {
components: {
DeprecatedModal,
},
+ directives: {
+ SafeHtml,
+ },
props: {
issueCount: {
type: Number,
@@ -124,7 +128,7 @@ Once deleted, it cannot be undone or recovered.`),
@submit="onSubmit"
>
<template #body="props">
- <p v-html="props.text"></p>
+ <p v-safe-html="props.text"></p>
</template>
</deprecated-modal>
</template>
diff --git a/app/assets/javascripts/pages/projects/blob/show/index.js b/app/assets/javascripts/pages/projects/blob/show/index.js
index cb7198e9789..46e59cd6572 100644
--- a/app/assets/javascripts/pages/projects/blob/show/index.js
+++ b/app/assets/javascripts/pages/projects/blob/show/index.js
@@ -6,6 +6,33 @@ import GpgBadges from '~/gpg_badges';
import '~/sourcegraph/load';
import PipelineTourSuccessModal from '~/blob/pipeline_tour_success_modal.vue';
+const createGitlabCiYmlVisualization = (containerId = '#js-blob-toggle-graph-preview') => {
+ const el = document.querySelector(containerId);
+ const { filename, blobData } = el?.dataset;
+
+ const nameRegexp = /\.gitlab-ci.yml/;
+
+ if (!el || !nameRegexp.test(filename)) {
+ return;
+ }
+
+ // eslint-disable-next-line no-new
+ new Vue({
+ el,
+ components: {
+ GitlabCiYamlVisualization: () =>
+ import('~/pipelines/components/pipeline_graph/gitlab_ci_yaml_visualization.vue'),
+ },
+ render(createElement) {
+ return createElement('gitlabCiYamlVisualization', {
+ props: {
+ blobData,
+ },
+ });
+ },
+ });
+};
+
document.addEventListener('DOMContentLoaded', () => {
new BlobViewer(); // eslint-disable-line no-new
initBlob();
@@ -63,4 +90,8 @@ document.addEventListener('DOMContentLoaded', () => {
});
}
}
+
+ if (gon?.features?.gitlabCiYmlPreview) {
+ createGitlabCiYmlVisualization();
+ }
});
diff --git a/app/assets/javascripts/pages/projects/branches/index/index.js b/app/assets/javascripts/pages/projects/branches/index/index.js
index 37e8c75f299..623d0a10606 100644
--- a/app/assets/javascripts/pages/projects/branches/index/index.js
+++ b/app/assets/javascripts/pages/projects/branches/index/index.js
@@ -1,4 +1,4 @@
-import AjaxLoadingSpinner from '~/ajax_loading_spinner';
+import AjaxLoadingSpinner from '~/branches/ajax_loading_spinner';
import DeleteModal from '~/branches/branches_delete_modal';
import initDiverganceGraph from '~/branches/divergence_graph';
diff --git a/app/assets/javascripts/pages/projects/ci/lints/ci_lint_editor.js b/app/assets/javascripts/pages/projects/ci/lints/ci_lint_editor.js
index 9ab73be80a0..d270bee25c7 100644
--- a/app/assets/javascripts/pages/projects/ci/lints/ci_lint_editor.js
+++ b/app/assets/javascripts/pages/projects/ci/lints/ci_lint_editor.js
@@ -1,19 +1,48 @@
+import createFlash from '~/flash';
+import { BLOB_EDITOR_ERROR } from '~/blob_edit/constants';
+
export default class CILintEditor {
constructor() {
- this.editor = window.ace.edit('ci-editor');
- this.textarea = document.querySelector('#content');
+ const monacoEnabled = window?.gon?.features?.monacoCi;
this.clearYml = document.querySelector('.clear-yml');
-
- this.editor.getSession().setMode('ace/mode/yaml');
- this.editor.on('input', () => {
- const content = this.editor.getSession().getValue();
- this.textarea.value = content;
- });
-
this.clearYml.addEventListener('click', this.clear.bind(this));
+
+ return monacoEnabled ? this.initEditorLite() : this.initAce();
}
clear() {
this.editor.setValue('');
}
+
+ initEditorLite() {
+ import(/* webpackChunkName: 'monaco_editor_lite' */ '~/editor/editor_lite')
+ .then(({ default: EditorLite }) => {
+ const editorEl = document.getElementById('editor');
+ const fileContentEl = document.getElementById('content');
+ const form = document.querySelector('.js-ci-lint-form');
+
+ const rootEditor = new EditorLite();
+
+ this.editor = rootEditor.createInstance({
+ el: editorEl,
+ blobPath: '.gitlab-ci.yml',
+ blobContent: editorEl.innerText,
+ });
+
+ form.addEventListener('submit', () => {
+ fileContentEl.value = this.editor.getValue();
+ });
+ })
+ .catch(() => createFlash({ message: BLOB_EDITOR_ERROR }));
+ }
+
+ initAce() {
+ this.editor = window.ace.edit('ci-editor');
+ this.textarea = document.getElementById('content');
+
+ this.editor.getSession().setMode('ace/mode/yaml');
+ this.editor.on('input', () => {
+ this.textarea.value = this.editor.getSession().getValue();
+ });
+ }
}
diff --git a/app/assets/javascripts/pages/projects/ci/lints/new/index.js b/app/assets/javascripts/pages/projects/ci/lints/new/index.js
index 8e8a843da0b..02bfee9810f 100644
--- a/app/assets/javascripts/pages/projects/ci/lints/new/index.js
+++ b/app/assets/javascripts/pages/projects/ci/lints/new/index.js
@@ -1,3 +1,11 @@
import CILintEditor from '../ci_lint_editor';
+import initCILint from '~/ci_lint/index';
-document.addEventListener('DOMContentLoaded', () => new CILintEditor());
+document.addEventListener('DOMContentLoaded', () => {
+ if (gon?.features?.ciLintVue) {
+ initCILint();
+ } else {
+ // eslint-disable-next-line no-new
+ new CILintEditor();
+ }
+});
diff --git a/app/assets/javascripts/pages/projects/clusters/new/index.js b/app/assets/javascripts/pages/projects/clusters/new/index.js
new file mode 100644
index 00000000000..876bab0b339
--- /dev/null
+++ b/app/assets/javascripts/pages/projects/clusters/new/index.js
@@ -0,0 +1,5 @@
+import initNewCluster from '~/clusters/new_cluster';
+
+document.addEventListener('DOMContentLoaded', () => {
+ initNewCluster();
+});
diff --git a/app/assets/javascripts/pages/projects/commit/show/index.js b/app/assets/javascripts/pages/projects/commit/show/index.js
index a245af72d93..d5fb2a8be3c 100644
--- a/app/assets/javascripts/pages/projects/commit/show/index.js
+++ b/app/assets/javascripts/pages/projects/commit/show/index.js
@@ -14,24 +14,23 @@ import axios from '~/lib/utils/axios_utils';
import syntaxHighlight from '~/syntax_highlight';
import flash from '~/flash';
import { __ } from '~/locale';
+import loadAwardsHandler from '~/awards_handler';
document.addEventListener('DOMContentLoaded', () => {
const hasPerfBar = document.querySelector('.with-performance-bar');
const performanceHeight = hasPerfBar ? 35 : 0;
+ initChangesDropdown(document.querySelector('.navbar-gitlab').offsetHeight + performanceHeight);
+ new ZenMode();
+ new ShortcutsNavigation();
+ new MiniPipelineGraph({
+ container: '.js-commit-pipeline-graph',
+ }).bindEvents();
+ initNotes();
+ // eslint-disable-next-line no-jquery/no-load
+ $('.commit-info.branches').load(document.querySelector('.js-commit-box').dataset.commitPath);
+ fetchCommitMergeRequests();
+
const filesContainer = $('.js-diffs-batch');
- const initAfterPageLoad = () => {
- new Diff();
- new ZenMode();
- new ShortcutsNavigation();
- new MiniPipelineGraph({
- container: '.js-commit-pipeline-graph',
- }).bindEvents();
- initNotes();
- initChangesDropdown(document.querySelector('.navbar-gitlab').offsetHeight + performanceHeight);
- // eslint-disable-next-line no-jquery/no-load
- $('.commit-info.branches').load(document.querySelector('.js-commit-box').dataset.commitPath);
- fetchCommitMergeRequests();
- };
if (filesContainer.length) {
const batchPath = filesContainer.data('diffFilesPath');
@@ -42,12 +41,13 @@ document.addEventListener('DOMContentLoaded', () => {
filesContainer.html($(data.html));
syntaxHighlight(filesContainer);
handleLocationHash();
- initAfterPageLoad();
+ new Diff();
})
.catch(() => {
flash(__('An error occurred while retrieving diff files'));
});
} else {
- initAfterPageLoad();
+ new Diff();
}
+ loadAwardsHandler();
});
diff --git a/app/assets/javascripts/pages/projects/constants.js b/app/assets/javascripts/pages/projects/constants.js
index 9efbf7cd36e..8dc765e5d10 100644
--- a/app/assets/javascripts/pages/projects/constants.js
+++ b/app/assets/javascripts/pages/projects/constants.js
@@ -1,5 +1,3 @@
-/* eslint-disable import/prefer-default-export */
-
export const ISSUABLE_INDEX = {
MERGE_REQUEST: 'merge_request_',
ISSUE: 'issue_',
diff --git a/app/assets/javascripts/pages/projects/environments/show/index.js b/app/assets/javascripts/pages/projects/environments/show/index.js
index 10e3e28f024..5d3a153cbd1 100644
--- a/app/assets/javascripts/pages/projects/environments/show/index.js
+++ b/app/assets/javascripts/pages/projects/environments/show/index.js
@@ -1,3 +1,3 @@
import initShowEnvironment from '~/environments/mount_show';
-document.addEventListener('DOMContentLoaded', () => initShowEnvironment());
+document.addEventListener('DOMContentLoaded', initShowEnvironment);
diff --git a/app/assets/javascripts/pages/projects/forks/new/components/fork_groups_list_item.vue b/app/assets/javascripts/pages/projects/forks/new/components/fork_groups_list_item.vue
index b4816fa2cb3..57838050d55 100644
--- a/app/assets/javascripts/pages/projects/forks/new/components/fork_groups_list_item.vue
+++ b/app/assets/javascripts/pages/projects/forks/new/components/fork_groups_list_item.vue
@@ -7,6 +7,7 @@ import {
GlTooltipDirective,
GlTooltip,
GlBadge,
+ GlSafeHtmlDirective as SafeHtml,
} from '@gitlab/ui';
import { VISIBILITY_TYPE_ICON, GROUP_VISIBILITY_TYPE } from '~/groups/constants';
import { __ } from '~/locale';
@@ -23,6 +24,7 @@ export default {
},
directives: {
GlTooltip: GlTooltipDirective,
+ SafeHtml,
},
props: {
group: {
@@ -119,7 +121,7 @@ export default {
</span>
</div>
<div v-if="group.description" class="description">
- <span v-html="group.markdown_description"> </span>
+ <span v-safe-html="group.markdown_description"> </span>
</div>
</div>
<div class="gl-display-flex gl-flex-shrink-0">
diff --git a/app/assets/javascripts/pages/projects/graphs/charts/index.js b/app/assets/javascripts/pages/projects/graphs/charts/index.js
index 09b440d1413..384216f29eb 100644
--- a/app/assets/javascripts/pages/projects/graphs/charts/index.js
+++ b/app/assets/javascripts/pages/projects/graphs/charts/index.js
@@ -1,154 +1,155 @@
import Vue from 'vue';
import { GlColumnChart } from '@gitlab/ui/dist/charts';
+import { waitForCSSLoaded } from '../../../../helpers/startup_css_helper';
import { __ } from '~/locale';
import CodeCoverage from '../components/code_coverage.vue';
import SeriesDataMixin from './series_data_mixin';
document.addEventListener('DOMContentLoaded', () => {
- const languagesContainer = document.getElementById('js-languages-chart');
- const codeCoverageContainer = document.getElementById('js-code-coverage-chart');
- const monthContainer = document.getElementById('js-month-chart');
- const weekdayContainer = document.getElementById('js-weekday-chart');
- const hourContainer = document.getElementById('js-hour-chart');
+ waitForCSSLoaded(() => {
+ const languagesContainer = document.getElementById('js-languages-chart');
+ const codeCoverageContainer = document.getElementById('js-code-coverage-chart');
+ const monthContainer = document.getElementById('js-month-chart');
+ const weekdayContainer = document.getElementById('js-weekday-chart');
+ const hourContainer = document.getElementById('js-hour-chart');
+ const LANGUAGE_CHART_HEIGHT = 300;
+ const reorderWeekDays = (weekDays, firstDayOfWeek = 0) => {
+ if (firstDayOfWeek === 0) {
+ return weekDays;
+ }
- const LANGUAGE_CHART_HEIGHT = 300;
+ return Object.keys(weekDays).reduce((acc, dayName, idx, arr) => {
+ const reorderedDayName = arr[(idx + firstDayOfWeek) % arr.length];
- const reorderWeekDays = (weekDays, firstDayOfWeek = 0) => {
- if (firstDayOfWeek === 0) {
- return weekDays;
- }
+ return {
+ ...acc,
+ [reorderedDayName]: weekDays[reorderedDayName],
+ };
+ }, {});
+ };
- return Object.keys(weekDays).reduce((acc, dayName, idx, arr) => {
- const reorderedDayName = arr[(idx + firstDayOfWeek) % arr.length];
-
- return {
- ...acc,
- [reorderedDayName]: weekDays[reorderedDayName],
- };
- }, {});
- };
-
- // eslint-disable-next-line no-new
- new Vue({
- el: languagesContainer,
- components: {
- GlColumnChart,
- },
- data() {
- return {
- chartData: JSON.parse(languagesContainer.dataset.chartData),
- };
- },
- computed: {
- seriesData() {
- return { full: this.chartData.map(d => [d.label, d.value]) };
+ // eslint-disable-next-line no-new
+ new Vue({
+ el: languagesContainer,
+ components: {
+ GlColumnChart,
},
- },
- render(h) {
- return h(GlColumnChart, {
- props: {
- data: this.seriesData,
- xAxisTitle: __('Used programming language'),
- yAxisTitle: __('Percentage'),
- xAxisType: 'category',
- },
- attrs: {
- height: LANGUAGE_CHART_HEIGHT,
+ data() {
+ return {
+ chartData: JSON.parse(languagesContainer.dataset.chartData),
+ };
+ },
+ computed: {
+ seriesData() {
+ return { full: this.chartData.map(d => [d.label, d.value]) };
},
- });
- },
- });
+ },
+ render(h) {
+ return h(GlColumnChart, {
+ props: {
+ data: this.seriesData,
+ xAxisTitle: __('Used programming language'),
+ yAxisTitle: __('Percentage'),
+ xAxisType: 'category',
+ },
+ attrs: {
+ height: LANGUAGE_CHART_HEIGHT,
+ },
+ });
+ },
+ });
- // eslint-disable-next-line no-new
- new Vue({
- el: codeCoverageContainer,
- render(h) {
- return h(CodeCoverage, {
- props: {
- graphEndpoint: codeCoverageContainer.dataset?.graphEndpoint,
- },
- });
- },
- });
+ // eslint-disable-next-line no-new
+ new Vue({
+ el: codeCoverageContainer,
+ render(h) {
+ return h(CodeCoverage, {
+ props: {
+ graphEndpoint: codeCoverageContainer.dataset?.graphEndpoint,
+ },
+ });
+ },
+ });
- // eslint-disable-next-line no-new
- new Vue({
- el: monthContainer,
- components: {
- GlColumnChart,
- },
- mixins: [SeriesDataMixin],
- data() {
- return {
- chartData: JSON.parse(monthContainer.dataset.chartData),
- };
- },
- render(h) {
- return h(GlColumnChart, {
- props: {
- data: this.seriesData,
- xAxisTitle: __('Day of month'),
- yAxisTitle: __('No. of commits'),
- xAxisType: 'category',
- },
- });
- },
- });
+ // eslint-disable-next-line no-new
+ new Vue({
+ el: monthContainer,
+ components: {
+ GlColumnChart,
+ },
+ mixins: [SeriesDataMixin],
+ data() {
+ return {
+ chartData: JSON.parse(monthContainer.dataset.chartData),
+ };
+ },
+ render(h) {
+ return h(GlColumnChart, {
+ props: {
+ data: this.seriesData,
+ xAxisTitle: __('Day of month'),
+ yAxisTitle: __('No. of commits'),
+ xAxisType: 'category',
+ },
+ });
+ },
+ });
- // eslint-disable-next-line no-new
- new Vue({
- el: weekdayContainer,
- components: {
- GlColumnChart,
- },
- data() {
- return {
- chartData: JSON.parse(weekdayContainer.dataset.chartData),
- };
- },
- computed: {
- seriesData() {
- const weekDays = reorderWeekDays(this.chartData, gon.first_day_of_week);
- const data = Object.keys(weekDays).reduce((acc, key) => {
- acc.push([key, weekDays[key]]);
- return acc;
- }, []);
- return { full: data };
+ // eslint-disable-next-line no-new
+ new Vue({
+ el: weekdayContainer,
+ components: {
+ GlColumnChart,
},
- },
- render(h) {
- return h(GlColumnChart, {
- props: {
- data: this.seriesData,
- xAxisTitle: __('Weekday'),
- yAxisTitle: __('No. of commits'),
- xAxisType: 'category',
+ data() {
+ return {
+ chartData: JSON.parse(weekdayContainer.dataset.chartData),
+ };
+ },
+ computed: {
+ seriesData() {
+ const weekDays = reorderWeekDays(this.chartData, gon.first_day_of_week);
+ const data = Object.keys(weekDays).reduce((acc, key) => {
+ acc.push([key, weekDays[key]]);
+ return acc;
+ }, []);
+ return { full: data };
},
- });
- },
- });
+ },
+ render(h) {
+ return h(GlColumnChart, {
+ props: {
+ data: this.seriesData,
+ xAxisTitle: __('Weekday'),
+ yAxisTitle: __('No. of commits'),
+ xAxisType: 'category',
+ },
+ });
+ },
+ });
- // eslint-disable-next-line no-new
- new Vue({
- el: hourContainer,
- components: {
- GlColumnChart,
- },
- mixins: [SeriesDataMixin],
- data() {
- return {
- chartData: JSON.parse(hourContainer.dataset.chartData),
- };
- },
- render(h) {
- return h(GlColumnChart, {
- props: {
- data: this.seriesData,
- xAxisTitle: __('Hour (UTC)'),
- yAxisTitle: __('No. of commits'),
- xAxisType: 'category',
- },
- });
- },
+ // eslint-disable-next-line no-new
+ new Vue({
+ el: hourContainer,
+ components: {
+ GlColumnChart,
+ },
+ mixins: [SeriesDataMixin],
+ data() {
+ return {
+ chartData: JSON.parse(hourContainer.dataset.chartData),
+ };
+ },
+ render(h) {
+ return h(GlColumnChart, {
+ props: {
+ data: this.seriesData,
+ xAxisTitle: __('Hour (UTC)'),
+ yAxisTitle: __('No. of commits'),
+ xAxisType: 'category',
+ },
+ });
+ },
+ });
});
});
diff --git a/app/assets/javascripts/pages/projects/integrations/jira/issues/index/index.js b/app/assets/javascripts/pages/projects/integrations/jira/issues/index/index.js
index 260ba69b4bc..534614349bf 100644
--- a/app/assets/javascripts/pages/projects/integrations/jira/issues/index/index.js
+++ b/app/assets/javascripts/pages/projects/integrations/jira/issues/index/index.js
@@ -1,4 +1,4 @@
-import initIssuablesList from '~/issuables_list';
+import initIssuablesList from '~/issues_list';
document.addEventListener('DOMContentLoaded', () => {
initIssuablesList();
diff --git a/app/assets/javascripts/pages/projects/issues/index/index.js b/app/assets/javascripts/pages/projects/issues/index/index.js
index 1711d122080..e1add4a2af3 100644
--- a/app/assets/javascripts/pages/projects/issues/index/index.js
+++ b/app/assets/javascripts/pages/projects/issues/index/index.js
@@ -7,7 +7,7 @@ import UsersSelect from '~/users_select';
import initFilteredSearch from '~/pages/search/init_filtered_search';
import { FILTERED_SEARCH } from '~/pages/constants';
import { ISSUABLE_INDEX } from '~/pages/projects/constants';
-import initIssuablesList from '~/issuables_list';
+import initIssuablesList from '~/issues_list';
import initManualOrdering from '~/manual_ordering';
import { showLearnGitLabIssuesPopover } from '~/onboarding_issues';
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 9304d9b6832..e0c1332796f 100644
--- a/app/assets/javascripts/pages/projects/issues/service_desk/index.js
+++ b/app/assets/javascripts/pages/projects/issues/service_desk/index.js
@@ -1,5 +1,5 @@
import FilteredSearchServiceDesk from './filtered_search';
-import initIssuablesList from '~/issuables_list';
+import initIssuablesList from '~/issues_list';
document.addEventListener('DOMContentLoaded', () => {
const supportBotData = JSON.parse(
diff --git a/app/assets/javascripts/pages/projects/issues/show.js b/app/assets/javascripts/pages/projects/issues/show.js
index 5ac6c17e09d..98ae4e26257 100644
--- a/app/assets/javascripts/pages/projects/issues/show.js
+++ b/app/assets/javascripts/pages/projects/issues/show.js
@@ -1,27 +1,31 @@
+import loadAwardsHandler from '~/awards_handler';
import initIssuableSidebar from '~/init_issuable_sidebar';
import Issue from '~/issue';
import ShortcutsIssuable from '~/behaviors/shortcuts/shortcuts_issuable';
import ZenMode from '~/zen_mode';
import '~/notes/index';
import { store } from '~/notes/stores';
-import initIssueableApp from '~/issue_show';
+import initIssueApp from '~/issue_show/issue';
+import initIncidentApp from '~/issue_show/incident';
import initIssuableHeaderWarning from '~/vue_shared/components/issuable/init_issuable_header_warning';
import initSentryErrorStackTraceApp from '~/sentry_error_stack_trace';
import initRelatedMergeRequestsApp from '~/related_merge_requests';
import initVueIssuableSidebarApp from '~/issuable_sidebar/sidebar_bundle';
+import { parseIssuableData } from '~/issue_show/utils/parse_data';
export default function() {
- initIssueableApp();
+ const { issueType, ...issuableData } = parseIssuableData();
+
+ if (issueType === 'incident') {
+ initIncidentApp(issuableData);
+ } else {
+ initIssueApp(issuableData);
+ }
+
initIssuableHeaderWarning(store);
initSentryErrorStackTraceApp();
initRelatedMergeRequestsApp();
- // This will be removed when we remove the `design_management_moved` feature flag
- // See https://gitlab.com/gitlab-org/gitlab/-/issues/223197
- import(/* webpackChunkName: 'design_management' */ '~/design_management_legacy')
- .then(module => module.default())
- .catch(() => {});
-
import(/* webpackChunkName: 'design_management' */ '~/design_management')
.then(module => module.default())
.catch(() => {});
@@ -34,4 +38,6 @@ export default function() {
} else {
initIssuableSidebar();
}
+
+ loadAwardsHandler();
}
diff --git a/app/assets/javascripts/pages/projects/issues/show/index.js b/app/assets/javascripts/pages/projects/issues/show/index.js
index ce74a6de11f..aef4feef42c 100644
--- a/app/assets/javascripts/pages/projects/issues/show/index.js
+++ b/app/assets/javascripts/pages/projects/issues/show/index.js
@@ -1,4 +1,5 @@
import initSidebarBundle from '~/sidebar/sidebar_bundle';
+import initRelatedIssues from '~/related_issues';
import initShow from '../show';
document.addEventListener('DOMContentLoaded', () => {
@@ -6,4 +7,5 @@ document.addEventListener('DOMContentLoaded', () => {
if (gon.features && !gon.features.vueIssuableSidebar) {
initSidebarBundle();
}
+ initRelatedIssues();
});
diff --git a/app/assets/javascripts/pages/projects/merge_requests/creations/new/target_project_dropdown.js b/app/assets/javascripts/pages/projects/merge_requests/creations/new/target_project_dropdown.js
index b72fe6681df..e9f0e008435 100644
--- a/app/assets/javascripts/pages/projects/merge_requests/creations/new/target_project_dropdown.js
+++ b/app/assets/javascripts/pages/projects/merge_requests/creations/new/target_project_dropdown.js
@@ -1,8 +1,9 @@
import $ from 'jquery';
+import initDeprecatedJQueryDropdown from '~/deprecated_jquery_dropdown';
export default () => {
const $targetProjectDropdown = $('.js-target-project');
- $targetProjectDropdown.glDropdown({
+ initDeprecatedJQueryDropdown($targetProjectDropdown, {
selectable: true,
fieldName: $targetProjectDropdown.data('fieldName'),
filterable: true,
@@ -16,7 +17,7 @@ export default () => {
$('.mr_target_commit').empty();
const $targetBranchDropdown = $('.js-target-branch');
$targetBranchDropdown.data('refsUrl', $el.data('refsUrl'));
- $targetBranchDropdown.data('glDropdown').clearMenu();
+ $targetBranchDropdown.data('deprecatedJQueryDropdown').clearMenu();
},
});
};
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 25abb80cfae..11af50169f5 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
@@ -6,6 +6,7 @@ import howToMerge from '~/how_to_merge';
import initPipelines from '~/commit/pipelines/pipelines_bundle';
import initVueIssuableSidebarApp from '~/issuable_sidebar/sidebar_bundle';
import initSourcegraph from '~/sourcegraph';
+import loadAwardsHandler from '~/awards_handler';
export default function() {
new ZenMode(); // eslint-disable-line no-new
@@ -19,4 +20,5 @@ export default function() {
handleLocationHash();
howToMerge();
initSourcegraph();
+ loadAwardsHandler();
}
diff --git a/app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/pipeline_schedules_callout.vue b/app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/pipeline_schedules_callout.vue
index da96e6f36b4..7a3923dfefd 100644
--- a/app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/pipeline_schedules_callout.vue
+++ b/app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/pipeline_schedules_callout.vue
@@ -1,6 +1,8 @@
<script>
+/* eslint-disable vue/no-v-html */
import Vue from 'vue';
import Cookies from 'js-cookie';
+import { GlIcon } from '@gitlab/ui';
import Translate from '../../../../../vue_shared/translate';
// Full path is needed for Jest to be able to correctly mock this file
import illustrationSvg from '~/pages/projects/pipeline_schedules/shared/icons/intro_illustration.svg';
@@ -12,6 +14,9 @@ const cookieKey = 'pipeline_schedules_callout_dismissed';
export default {
name: 'PipelineSchedulesCallout',
+ components: {
+ GlIcon,
+ },
data() {
return {
docsUrl: document.getElementById('pipeline-schedules-callout').dataset.docsUrl,
@@ -33,7 +38,7 @@ export default {
<div v-if="!calloutDismissed" class="pipeline-schedules-user-callout user-callout">
<div class="bordered-box landing content-block">
<button id="dismiss-callout-btn" class="btn btn-default close" @click="dismissCallout">
- <i aria-hidden="true" class="fa fa-times"> </i>
+ <gl-icon name="close" aria-hidden="true" />
</button>
<div class="svg-container" v-html="illustrationSvg"></div>
<div class="user-callout-copy">
diff --git a/app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/target_branch_dropdown.js b/app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/target_branch_dropdown.js
index 0057700c1b3..4b203891640 100644
--- a/app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/target_branch_dropdown.js
+++ b/app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/target_branch_dropdown.js
@@ -1,4 +1,5 @@
import $ from 'jquery';
+import initDeprecatedJQueryDropdown from '~/deprecated_jquery_dropdown';
export default class TargetBranchDropdown {
constructor() {
@@ -10,7 +11,7 @@ export default class TargetBranchDropdown {
}
initDropdown() {
- this.$dropdown.glDropdown({
+ initDeprecatedJQueryDropdown(this.$dropdown, {
data: this.formatBranchesList(),
filterable: true,
selectable: true,
diff --git a/app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/timezone_dropdown.js b/app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/timezone_dropdown.js
index a20a0526f12..2a58e015ff1 100644
--- a/app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/timezone_dropdown.js
+++ b/app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/timezone_dropdown.js
@@ -1,3 +1,5 @@
+import initDeprecatedJQueryDropdown from '~/deprecated_jquery_dropdown';
+
const defaultTimezone = { name: 'UTC', offset: 0 };
const defaults = {
$inputEl: null,
@@ -42,7 +44,7 @@ export default class TimezoneDropdown {
}
initDropdown() {
- this.$dropdown.glDropdown({
+ initDeprecatedJQueryDropdown(this.$dropdown, {
data: this.timezoneData,
filterable: true,
selectable: true,
diff --git a/app/assets/javascripts/pages/projects/project.js b/app/assets/javascripts/pages/projects/project.js
index bb285635425..2f27814a692 100644
--- a/app/assets/javascripts/pages/projects/project.js
+++ b/app/assets/javascripts/pages/projects/project.js
@@ -8,52 +8,59 @@ import { serializeForm } from '~/lib/utils/forms';
import axios from '~/lib/utils/axios_utils';
import { deprecatedCreateFlash as flash } from '~/flash';
import projectSelect from '../../project_select';
+import initDeprecatedJQueryDropdown from '~/deprecated_jquery_dropdown';
export default class Project {
constructor() {
const $cloneOptions = $('ul.clone-options-dropdown');
- const $projectCloneField = $('#project_clone');
- const $cloneBtnLabel = $('.js-git-clone-holder .js-clone-dropdown-label');
- const mobileCloneField = document.querySelector(
- '.js-mobile-git-clone .js-clone-dropdown-label',
- );
-
- const selectedCloneOption = $cloneBtnLabel.text().trim();
- if (selectedCloneOption.length > 0) {
- $(`a:contains('${selectedCloneOption}')`, $cloneOptions).addClass('is-active');
- }
+ if ($cloneOptions.length) {
+ const $projectCloneField = $('#project_clone');
+ const $cloneBtnLabel = $('.js-git-clone-holder .js-clone-dropdown-label');
+ const mobileCloneField = document.querySelector(
+ '.js-mobile-git-clone .js-clone-dropdown-label',
+ );
+
+ const selectedCloneOption = $cloneBtnLabel.text().trim();
+ if (selectedCloneOption.length > 0) {
+ $(`a:contains('${selectedCloneOption}')`, $cloneOptions).addClass('is-active');
+ }
- $('a', $cloneOptions).on('click', e => {
- e.preventDefault();
- const $this = $(e.currentTarget);
- const url = $this.attr('href');
- const cloneType = $this.data('cloneType');
-
- $('.is-active', $cloneOptions).removeClass('is-active');
- $(`a[data-clone-type="${cloneType}"]`).each(function() {
- const $el = $(this);
- const activeText = $el.find('.dropdown-menu-inner-title').text();
- const $container = $el.closest('.project-clone-holder');
- const $label = $container.find('.js-clone-dropdown-label');
-
- $el.toggleClass('is-active');
- $label.text(activeText);
+ $('a', $cloneOptions).on('click', e => {
+ e.preventDefault();
+ const $this = $(e.currentTarget);
+ const url = $this.attr('href');
+ const cloneType = $this.data('cloneType');
+
+ $('.is-active', $cloneOptions).removeClass('is-active');
+ $(`a[data-clone-type="${cloneType}"]`).each(function() {
+ const $el = $(this);
+ const activeText = $el.find('.dropdown-menu-inner-title').text();
+ const $container = $el.closest('.project-clone-holder');
+ const $label = $container.find('.js-clone-dropdown-label');
+
+ $el.toggleClass('is-active');
+ $label.text(activeText);
+ });
+
+ if (mobileCloneField) {
+ mobileCloneField.dataset.clipboardText = url;
+ } else {
+ $projectCloneField.val(url);
+ }
+ $('.js-git-empty .js-clone').text(url);
});
+ }
- if (mobileCloneField) {
- mobileCloneField.dataset.clipboardText = url;
- } else {
- $projectCloneField.val(url);
- }
- $('.js-git-empty .js-clone').text(url);
- });
// Ref switcher
- Project.initRefSwitcher();
- $('.project-refs-select').on('change', function() {
- return $(this)
- .parents('form')
- .submit();
- });
+ if (document.querySelector('.js-project-refs-dropdown')) {
+ Project.initRefSwitcher();
+ $('.project-refs-select').on('change', function() {
+ return $(this)
+ .parents('form')
+ .submit();
+ });
+ }
+
$('.hide-no-ssh-message').on('click', function(e) {
Cookies.set('hide_no_ssh_message', 'false');
$(this)
@@ -77,6 +84,7 @@ export default class Project {
.remove();
return e.preventDefault();
});
+
Project.projectSelectDropdown();
}
@@ -104,7 +112,7 @@ export default class Project {
const action = $form.attr('action');
const linkTarget = mergeUrlParams(serializeForm($form[0]), action);
- return $dropdown.glDropdown({
+ return initDeprecatedJQueryDropdown($dropdown, {
data(term, callback) {
axios
.get($dropdown.data('refsUrl'), {
diff --git a/app/assets/javascripts/pages/projects/shared/permissions/components/project_setting_row.vue b/app/assets/javascripts/pages/projects/shared/permissions/components/project_setting_row.vue
index 92d23772565..b7546a6bed7 100644
--- a/app/assets/javascripts/pages/projects/shared/permissions/components/project_setting_row.vue
+++ b/app/assets/javascripts/pages/projects/shared/permissions/components/project_setting_row.vue
@@ -1,5 +1,10 @@
<script>
+import { GlIcon } from '@gitlab/ui';
+
export default {
+ components: {
+ GlIcon,
+ },
props: {
label: {
type: String,
@@ -25,7 +30,7 @@ export default {
<label v-if="label" class="label-bold">
{{ label }}
<a v-if="helpPath" :href="helpPath" target="_blank">
- <i aria-hidden="true" data-hidden="true" class="fa fa-question-circle"> </i>
+ <gl-icon name="question-o" />
</a>
</label>
<span v-if="helpText" class="form-text text-muted"> {{ helpText }} </span> <slot></slot>
diff --git a/app/assets/javascripts/pages/search/show/index.js b/app/assets/javascripts/pages/search/show/index.js
index 85aaaa2c9da..92d01343bd5 100644
--- a/app/assets/javascripts/pages/search/show/index.js
+++ b/app/assets/javascripts/pages/search/show/index.js
@@ -1,3 +1,7 @@
import Search from './search';
+import initStateFilter from '~/search/state_filter';
-document.addEventListener('DOMContentLoaded', () => new Search());
+document.addEventListener('DOMContentLoaded', () => {
+ initStateFilter();
+ return new Search();
+});
diff --git a/app/assets/javascripts/pages/search/show/search.js b/app/assets/javascripts/pages/search/show/search.js
index cc2128490ec..6ff74325a5e 100644
--- a/app/assets/javascripts/pages/search/show/search.js
+++ b/app/assets/javascripts/pages/search/show/search.js
@@ -1,5 +1,5 @@
import $ from 'jquery';
-import '~/gl_dropdown';
+import initDeprecatedJQueryDropdown from '~/deprecated_jquery_dropdown';
import { deprecatedCreateFlash as Flash } from '~/flash';
import Api from '~/api';
import { __ } from '~/locale';
@@ -20,7 +20,7 @@ export default class Search {
this.eventListeners();
refreshCounts();
- $groupDropdown.glDropdown({
+ initDeprecatedJQueryDropdown($groupDropdown, {
selectable: true,
filterable: true,
filterRemote: true,
@@ -46,7 +46,7 @@ export default class Search {
clicked: () => Search.submitSearch(),
});
- $projectDropdown.glDropdown({
+ initDeprecatedJQueryDropdown($projectDropdown, {
selectable: true,
filterable: true,
filterRemote: true,
diff --git a/app/assets/javascripts/pages/shared/wikis/components/delete_wiki_modal.vue b/app/assets/javascripts/pages/shared/wikis/components/delete_wiki_modal.vue
index a7b7d597fb7..653aad3d2f5 100644
--- a/app/assets/javascripts/pages/shared/wikis/components/delete_wiki_modal.vue
+++ b/app/assets/javascripts/pages/shared/wikis/components/delete_wiki_modal.vue
@@ -1,11 +1,12 @@
<script>
+import { GlButton, GlModal, GlModalDirective } from '@gitlab/ui';
import { escape } from 'lodash';
-import { GlModal, GlModalDirective } from '@gitlab/ui';
import { s__, sprintf } from '~/locale';
export default {
components: {
GlModal,
+ GlButton,
},
directives: {
'gl-modal': GlModalDirective,
@@ -55,14 +56,14 @@ export default {
<template>
<div class="d-inline-block">
- <button
+ <gl-button
v-gl-modal="modalId"
- type="button"
- class="btn btn-danger"
+ category="primary"
+ variant="danger"
data-qa-selector="delete_button"
>
{{ __('Delete') }}
- </button>
+ </gl-button>
<gl-modal
:title="title"
:action-primary="{
diff --git a/app/assets/javascripts/pdf/page/index.vue b/app/assets/javascripts/pdf/page/index.vue
index 65f84e75e86..843c50cf9bc 100644
--- a/app/assets/javascripts/pdf/page/index.vue
+++ b/app/assets/javascripts/pdf/page/index.vue
@@ -52,19 +52,19 @@ export default {
<style>
.pdf-page {
- margin: 8px auto 0 auto;
+ margin: 8px auto 0;
border-top: 1px #ddd solid;
border-bottom: 1px #ddd solid;
width: 100%;
}
.pdf-page:first-child {
- margin-top: 0px;
- border-top: 0px;
+ margin-top: 0;
+ border-top: 0;
}
.pdf-page:last-child {
- margin-bottom: 0px;
- border-bottom: 0px;
+ margin-bottom: 0;
+ border-bottom: 0;
}
</style>
diff --git a/app/assets/javascripts/performance_bar/components/detailed_metric.vue b/app/assets/javascripts/performance_bar/components/detailed_metric.vue
index ef24dbfb6ce..9f05ee5c7c2 100644
--- a/app/assets/javascripts/performance_bar/components/detailed_metric.vue
+++ b/app/assets/javascripts/performance_bar/components/detailed_metric.vue
@@ -1,14 +1,14 @@
<script>
+import { GlIcon } from '@gitlab/ui';
import RequestWarning from './request_warning.vue';
import DeprecatedModal2 from '~/vue_shared/components/deprecated_modal_2.vue';
-import Icon from '~/vue_shared/components/icon.vue';
export default {
components: {
RequestWarning,
GlModal: DeprecatedModal2,
- Icon,
+ GlIcon,
},
props: {
currentRequest: {
@@ -104,7 +104,7 @@ export default {
type="button"
:aria-label="__('Toggle backtrace')"
>
- <icon :size="12" name="ellipsis_h" />
+ <gl-icon :size="12" name="ellipsis_h" />
</button>
</div>
<pre v-if="item.backtrace" class="backtrace-row js-toggle-content mt-2">{{
diff --git a/app/assets/javascripts/performance_bar/components/performance_bar_app.vue b/app/assets/javascripts/performance_bar/components/performance_bar_app.vue
index cccb5e1be06..165feb1b6aa 100644
--- a/app/assets/javascripts/performance_bar/components/performance_bar_app.vue
+++ b/app/assets/javascripts/performance_bar/components/performance_bar_app.vue
@@ -1,4 +1,5 @@
<script>
+/* eslint-disable vue/no-v-html */
import { glEmojiTag } from '~/emoji';
import AddRequest from './add_request.vue';
diff --git a/app/assets/javascripts/performance_bar/components/request_selector.vue b/app/assets/javascripts/performance_bar/components/request_selector.vue
index c22a648d17f..5a9d3a6d313 100644
--- a/app/assets/javascripts/performance_bar/components/request_selector.vue
+++ b/app/assets/javascripts/performance_bar/components/request_selector.vue
@@ -1,4 +1,5 @@
<script>
+/* eslint-disable vue/no-v-html */
import { GlPopover } from '@gitlab/ui';
import { glEmojiTag } from '~/emoji';
import { n__ } from '~/locale';
diff --git a/app/assets/javascripts/performance_bar/components/request_warning.vue b/app/assets/javascripts/performance_bar/components/request_warning.vue
index 0128d5bd733..b61e1e5b7a9 100644
--- a/app/assets/javascripts/performance_bar/components/request_warning.vue
+++ b/app/assets/javascripts/performance_bar/components/request_warning.vue
@@ -1,4 +1,5 @@
<script>
+/* eslint-disable vue/no-v-html */
import { GlPopover } from '@gitlab/ui';
import { glEmojiTag } from '~/emoji';
diff --git a/app/assets/javascripts/performance_bar/index.js b/app/assets/javascripts/performance_bar/index.js
index a294f3f36a6..f29b5f42d8f 100644
--- a/app/assets/javascripts/performance_bar/index.js
+++ b/app/assets/javascripts/performance_bar/index.js
@@ -5,14 +5,17 @@ import axios from '~/lib/utils/axios_utils';
import PerformanceBarService from './services/performance_bar_service';
import PerformanceBarStore from './stores/performance_bar_store';
-export default ({ container }) =>
- new Vue({
- el: container,
+import initPerformanceBarLog from './performance_bar_log';
+
+const initPerformanceBar = el => {
+ const performanceBarData = el.dataset;
+
+ return new Vue({
+ el,
components: {
PerformanceBarApp: () => import('./components/performance_bar_app.vue'),
},
data() {
- const performanceBarData = document.querySelector(this.$options.el).dataset;
const store = new PerformanceBarStore();
return {
@@ -24,15 +27,12 @@ export default ({ container }) =>
};
},
mounted() {
- this.interceptor = PerformanceBarService.registerInterceptor(
- this.peekUrl,
- this.loadRequestDetails,
- );
+ PerformanceBarService.registerInterceptor(this.peekUrl, this.loadRequestDetails);
this.loadRequestDetails(this.requestId, window.location.href);
},
beforeDestroy() {
- PerformanceBarService.removeInterceptor(this.interceptor);
+ PerformanceBarService.removeInterceptor();
},
methods: {
addRequestManually(urlOrRequestId) {
@@ -121,3 +121,15 @@ export default ({ container }) =>
});
},
});
+};
+
+document.addEventListener('DOMContentLoaded', () => {
+ const jsPeek = document.querySelector('#js-peek');
+ if (jsPeek) {
+ initPerformanceBar(jsPeek);
+ }
+});
+
+initPerformanceBarLog();
+
+export default initPerformanceBar;
diff --git a/app/assets/javascripts/performance_bar/performance_bar_log.js b/app/assets/javascripts/performance_bar/performance_bar_log.js
new file mode 100644
index 00000000000..638c544f2e1
--- /dev/null
+++ b/app/assets/javascripts/performance_bar/performance_bar_log.js
@@ -0,0 +1,28 @@
+/* eslint-disable no-console */
+import { getCLS, getFID, getLCP } from 'web-vitals';
+
+const initVitalsLog = () => {
+ const reportVital = data => {
+ console.log(`${String.fromCodePoint(0x1f4c8)} ${data.name} : `, data);
+ };
+
+ console.log(
+ `${String.fromCodePoint(
+ 0x1f4d1,
+ )} To get the final web vital numbers reported you maybe need to switch away and back to the tab`,
+ );
+ getCLS(reportVital);
+ getFID(reportVital);
+ getLCP(reportVital);
+};
+
+const initPerformanceBarLog = () => {
+ console.log(
+ `%c ${String.fromCodePoint(0x1f98a)} GitLab performance bar`,
+ 'width:100%;background-color: #292961; color: #FFFFFF; font-size:24px; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto; padding: 10px;display:block;padding-right: 100px;',
+ );
+
+ initVitalsLog();
+};
+
+export default initPerformanceBarLog;
diff --git a/app/assets/javascripts/performance_bar/services/performance_bar_service.js b/app/assets/javascripts/performance_bar/services/performance_bar_service.js
index 61b35b4b8f5..3c8303d102e 100644
--- a/app/assets/javascripts/performance_bar/services/performance_bar_service.js
+++ b/app/assets/javascripts/performance_bar/services/performance_bar_service.js
@@ -2,12 +2,14 @@ import axios from '../../lib/utils/axios_utils';
import { parseBoolean } from '~/lib/utils/common_utils';
export default class PerformanceBarService {
+ static interceptor = null;
+
static fetchRequestDetails(peekUrl, requestId) {
return axios.get(peekUrl, { params: { request_id: requestId } });
}
static registerInterceptor(peekUrl, callback) {
- const interceptor = response => {
+ PerformanceBarService.interceptor = response => {
const [fireCallback, requestId, requestUrl] = PerformanceBarService.callbackParams(
response,
peekUrl,
@@ -20,22 +22,20 @@ export default class PerformanceBarService {
return response;
};
- return axios.interceptors.response.use(interceptor);
+ return axios.interceptors.response.use(PerformanceBarService.interceptor);
}
- static removeInterceptor(interceptor) {
- axios.interceptors.response.eject(interceptor);
+ static removeInterceptor() {
+ axios.interceptors.response.eject(PerformanceBarService.interceptor);
+ PerformanceBarService.interceptor = null;
}
static callbackParams(response, peekUrl) {
const requestId = response.headers && response.headers['x-request-id'];
- // Get the request URL from response.config for Axios, and response for
- // Vue Resource.
- const requestUrl = (response.config || response).url;
- const apiRequest = requestUrl && requestUrl.match(/^\/api\//);
+ const requestUrl = response.config?.url;
const cachedResponse =
response.headers && parseBoolean(response.headers['x-gitlab-from-cache']);
- const fireCallback = requestUrl !== peekUrl && requestId && !apiRequest && !cachedResponse;
+ const fireCallback = requestUrl !== peekUrl && Boolean(requestId) && !cachedResponse;
return [fireCallback, requestId, requestUrl];
}
diff --git a/app/assets/javascripts/performance_bar/stores/performance_bar_store.js b/app/assets/javascripts/performance_bar/stores/performance_bar_store.js
index 6f443db47ed..8c88851f039 100644
--- a/app/assets/javascripts/performance_bar/stores/performance_bar_store.js
+++ b/app/assets/javascripts/performance_bar/stores/performance_bar_store.js
@@ -47,7 +47,10 @@ export default class PerformanceBarStore {
}
canTrackRequest(requestUrl) {
- return this.requests.filter(request => request.url === requestUrl).length < 2;
+ return (
+ requestUrl.endsWith('/api/graphql') ||
+ this.requests.filter(request => request.url === requestUrl).length < 2
+ );
}
static truncateUrl(requestUrl) {
diff --git a/app/assets/javascripts/pipeline_new/components/pipeline_new_form.vue b/app/assets/javascripts/pipeline_new/components/pipeline_new_form.vue
index e079603a5d4..be8ce832d20 100644
--- a/app/assets/javascripts/pipeline_new/components/pipeline_new_form.vue
+++ b/app/assets/javascripts/pipeline_new/components/pipeline_new_form.vue
@@ -9,13 +9,13 @@ import {
GlFormInput,
GlFormSelect,
GlLink,
- GlNewDropdown,
- GlNewDropdownItem,
+ GlDropdown,
+ GlDropdownItem,
GlSearchBoxByType,
GlSprintf,
} from '@gitlab/ui';
-import { s__, __ } from '~/locale';
-import Api from '~/api';
+import { s__, __, n__ } from '~/locale';
+import axios from '~/lib/utils/axios_utils';
import { redirectTo } from '~/lib/utils/url_utility';
import { VARIABLE_TYPE, FILE_TYPE } from '../constants';
@@ -29,6 +29,8 @@ export default {
),
formElementClasses: 'gl-mr-3 gl-mb-3 table-section section-15',
errorTitle: __('The form contains the following error:'),
+ warningTitle: __('The form contains the following warning:'),
+ maxWarningsSummary: __('%{total} warnings found: showing first %{warningsDisplayed}'),
components: {
GlAlert,
GlButton,
@@ -37,8 +39,8 @@ export default {
GlFormInput,
GlFormSelect,
GlLink,
- GlNewDropdown,
- GlNewDropdownItem,
+ GlDropdown,
+ GlDropdownItem,
GlSearchBoxByType,
GlSprintf,
},
@@ -74,13 +76,20 @@ export default {
required: false,
default: () => ({}),
},
+ maxWarnings: {
+ type: Number,
+ required: true,
+ },
},
data() {
return {
searchTerm: '',
refValue: this.refParam,
variables: {},
- error: false,
+ error: null,
+ warnings: [],
+ totalWarnings: 0,
+ isWarningDismissed: false,
};
},
computed: {
@@ -91,6 +100,18 @@ export default {
variablesLength() {
return Object.keys(this.variables).length;
},
+ overMaxWarningsLimit() {
+ return this.totalWarnings > this.maxWarnings;
+ },
+ warningsSummary() {
+ return n__('%d warning found:', '%d warnings found:', this.warnings.length);
+ },
+ summaryMessage() {
+ return this.overMaxWarningsLimit ? this.$options.maxWarningsSummary : this.warningsSummary;
+ },
+ shouldShowWarning() {
+ return this.warnings.length > 0 && !this.isWarningDismissed;
+ },
},
created() {
if (this.variableParams) {
@@ -145,13 +166,20 @@ export default {
({ key, value }) => key !== '' && value !== '',
);
- return Api.createPipeline(this.projectId, {
- ref: this.refValue,
- variables: filteredVariables,
- })
- .then(({ data }) => redirectTo(data.web_url))
+ return axios
+ .post(this.pipelinesPath, {
+ ref: this.refValue,
+ variables: filteredVariables,
+ })
+ .then(({ data }) => {
+ redirectTo(`${this.pipelinesPath}/${data.id}`);
+ })
.catch(err => {
- this.error = err.response.data.message.base;
+ const { errors, warnings, total_warnings: totalWarnings } = err.response.data;
+ const [error] = errors;
+ this.error = error;
+ this.warnings = warnings;
+ this.totalWarnings = totalWarnings;
});
},
},
@@ -166,16 +194,45 @@ export default {
:dismissible="false"
variant="danger"
class="gl-mb-4"
+ data-testid="run-pipeline-error-alert"
>{{ error }}</gl-alert
>
+ <gl-alert
+ v-if="shouldShowWarning"
+ :title="$options.warningTitle"
+ variant="warning"
+ class="gl-mb-4"
+ data-testid="run-pipeline-warning-alert"
+ @dismiss="isWarningDismissed = true"
+ >
+ <details>
+ <summary>
+ <gl-sprintf :message="summaryMessage">
+ <template #total>
+ {{ totalWarnings }}
+ </template>
+ <template #warningsDisplayed>
+ {{ maxWarnings }}
+ </template>
+ </gl-sprintf>
+ </summary>
+ <p
+ v-for="(warning, index) in warnings"
+ :key="`warning-${index}`"
+ data-testid="run-pipeline-warning"
+ >
+ {{ warning }}
+ </p>
+ </details>
+ </gl-alert>
<gl-form-group :label="s__('Pipeline|Run for')">
- <gl-new-dropdown :text="refValue" block>
+ <gl-dropdown :text="refValue" block>
<gl-search-box-by-type
v-model.trim="searchTerm"
:placeholder="__('Search branches and tags')"
class="gl-p-2"
/>
- <gl-new-dropdown-item
+ <gl-dropdown-item
v-for="(ref, index) in filteredRefs"
:key="index"
class="gl-font-monospace"
@@ -184,8 +241,8 @@ export default {
@click="setRefSelected(ref)"
>
{{ ref }}
- </gl-new-dropdown-item>
- </gl-new-dropdown>
+ </gl-dropdown-item>
+ </gl-dropdown>
<template #description>
<div>
diff --git a/app/assets/javascripts/pipeline_new/index.js b/app/assets/javascripts/pipeline_new/index.js
index 1c4812c2e0e..f1ea86f8c5f 100644
--- a/app/assets/javascripts/pipeline_new/index.js
+++ b/app/assets/javascripts/pipeline_new/index.js
@@ -11,6 +11,7 @@ export default () => {
fileParam,
refNames,
settingsLink,
+ maxWarnings,
} = el?.dataset;
const variableParams = JSON.parse(varParam);
@@ -29,6 +30,7 @@ export default () => {
fileParams,
refs,
settingsLink,
+ maxWarnings: Number(maxWarnings),
},
});
},
diff --git a/app/assets/javascripts/pipelines/components/graph/action_component.vue b/app/assets/javascripts/pipelines/components/graph/action_component.vue
index 137455bcae1..efa11580c41 100644
--- a/app/assets/javascripts/pipelines/components/graph/action_component.vue
+++ b/app/assets/javascripts/pipelines/components/graph/action_component.vue
@@ -1,10 +1,9 @@
<script>
-import { GlTooltipDirective, GlButton, GlLoadingIcon } from '@gitlab/ui';
+import { GlTooltipDirective, GlButton, GlLoadingIcon, GlIcon } from '@gitlab/ui';
import axios from '~/lib/utils/axios_utils';
import { dasherize } from '~/lib/utils/text_utility';
import { __ } from '~/locale';
import { deprecatedCreateFlash as createFlash } from '~/flash';
-import Icon from '~/vue_shared/components/icon.vue';
/**
* Renders either a cancel, retry or play icon button and handles the post request
@@ -18,7 +17,7 @@ import Icon from '~/vue_shared/components/icon.vue';
*/
export default {
components: {
- Icon,
+ GlIcon,
GlButton,
GlLoadingIcon,
},
@@ -92,6 +91,6 @@ export default {
@click="onClickAction"
>
<gl-loading-icon v-if="isLoading" class="js-action-icon-loading" />
- <icon v-else :name="actionIcon" />
+ <gl-icon v-else :name="actionIcon" class="gl-mr-0!" />
</gl-button>
</template>
diff --git a/app/assets/javascripts/pipelines/components/graph/graph_component.vue b/app/assets/javascripts/pipelines/components/graph/graph_component.vue
index f5bf6a6ed34..924cdeebba1 100644
--- a/app/assets/javascripts/pipelines/components/graph/graph_component.vue
+++ b/app/assets/javascripts/pipelines/components/graph/graph_component.vue
@@ -44,6 +44,10 @@ export default {
return {
downstreamMarginTop: null,
jobName: null,
+ pipelineExpanded: {
+ jobName: '',
+ expanded: false,
+ },
};
},
computed: {
@@ -120,6 +124,19 @@ export default {
setJob(jobName) {
this.jobName = jobName;
},
+ setPipelineExpanded(jobName, expanded) {
+ if (expanded) {
+ this.pipelineExpanded = {
+ jobName,
+ expanded,
+ };
+ } else {
+ this.pipelineExpanded = {
+ expanded,
+ jobName: '',
+ };
+ }
+ },
},
};
</script>
@@ -181,6 +198,7 @@ export default {
:has-triggered-by="hasTriggeredBy"
:action="stage.status.action"
:job-hovered="jobName"
+ :pipeline-expanded="pipelineExpanded"
@refreshPipelineGraph="refreshPipelineGraph"
/>
</ul>
@@ -193,6 +211,7 @@ export default {
graph-position="right"
@linkedPipelineClick="handleClickedDownstream"
@downstreamHovered="setJob"
+ @pipelineExpandToggle="setPipelineExpanded"
/>
<pipeline-graph
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 15c220a554d..11fb2b18e9d 100644
--- a/app/assets/javascripts/pipelines/components/graph/job_group_dropdown.vue
+++ b/app/assets/javascripts/pipelines/components/graph/job_group_dropdown.vue
@@ -69,9 +69,7 @@ export default {
>
<ci-icon :status="group.status" />
- <span
- class="ci-status-text text-truncate mw-70p gl-pl-1-deprecated-no-really-do-not-use-me d-inline-block align-bottom"
- >
+ <span class="ci-status-text text-truncate mw-70p gl-pl-2 d-inline-block align-bottom">
{{ group.name }}
</span>
diff --git a/app/assets/javascripts/pipelines/components/graph/job_item.vue b/app/assets/javascripts/pipelines/components/graph/job_item.vue
index 4d72cc55b34..0fe0b671273 100644
--- a/app/assets/javascripts/pipelines/components/graph/job_item.vue
+++ b/app/assets/javascripts/pipelines/components/graph/job_item.vue
@@ -31,7 +31,7 @@ import delayedJobMixin from '~/jobs/mixins/delayed_job_mixin';
*/
export default {
- hoverClass: 'gl-inset-border-1-blue-500',
+ hoverClass: 'gl-shadow-x0-y0-b3-s1-blue-500',
components: {
ActionComponent,
JobNameComponent,
@@ -61,6 +61,11 @@ export default {
required: false,
default: '',
},
+ pipelineExpanded: {
+ type: Object,
+ required: false,
+ default: () => ({}),
+ },
},
computed: {
boundary() {
@@ -101,8 +106,14 @@ export default {
hasAction() {
return this.job.status && this.job.status.action && this.job.status.action.path;
},
+ relatedDownstreamHovered() {
+ return this.job.name === this.jobHovered;
+ },
+ relatedDownstreamExpanded() {
+ return this.job.name === this.pipelineExpanded.jobName && this.pipelineExpanded.expanded;
+ },
jobClasses() {
- return this.job.name === this.jobHovered
+ return this.relatedDownstreamHovered || this.relatedDownstreamExpanded
? `${this.$options.hoverClass} ${this.cssClassJobName}`
: this.cssClassJobName;
},
@@ -121,8 +132,9 @@ export default {
v-gl-tooltip="{ boundary, placement: 'bottom' }"
:href="status.details_path"
:title="tooltipText"
- :class="cssClassJobName"
+ :class="jobClasses"
class="js-pipeline-graph-job-link qa-job-link menu-item"
+ data-testid="job-with-link"
>
<job-name-component :name="job.name" :status="job.status" />
</gl-link>
diff --git a/app/assets/javascripts/pipelines/components/graph/job_name_component.vue b/app/assets/javascripts/pipelines/components/graph/job_name_component.vue
index 74a261f35d7..30ba243077e 100644
--- a/app/assets/javascripts/pipelines/components/graph/job_name_component.vue
+++ b/app/assets/javascripts/pipelines/components/graph/job_name_component.vue
@@ -27,9 +27,7 @@ export default {
<template>
<span class="ci-job-name-component mw-100">
<ci-icon :status="status" />
- <span
- class="ci-status-text text-truncate mw-70p gl-pl-1-deprecated-no-really-do-not-use-me d-inline-block align-bottom"
- >
+ <span class="ci-status-text text-truncate mw-70p gl-pl-2 d-inline-block align-bottom">
{{ name }}
</span>
</span>
diff --git a/app/assets/javascripts/pipelines/components/graph/linked_pipeline.vue b/app/assets/javascripts/pipelines/components/graph/linked_pipeline.vue
index f0a8f9f7ab7..e359fc787c5 100644
--- a/app/assets/javascripts/pipelines/components/graph/linked_pipeline.vue
+++ b/app/assets/javascripts/pipelines/components/graph/linked_pipeline.vue
@@ -1,5 +1,5 @@
<script>
-import { GlTooltipDirective, GlButton } from '@gitlab/ui';
+import { GlTooltipDirective, GlButton, GlLink, GlLoadingIcon } from '@gitlab/ui';
import CiStatus from '~/vue_shared/components/ci_icon.vue';
import { __, sprintf } from '~/locale';
@@ -10,6 +10,8 @@ export default {
components: {
CiStatus,
GlButton,
+ GlLink,
+ GlLoadingIcon,
},
props: {
pipeline: {
@@ -25,6 +27,11 @@ export default {
required: true,
},
},
+ data() {
+ return {
+ expanded: false,
+ };
+ },
computed: {
tooltipText() {
return `${this.downstreamTitle} #${this.pipeline.id} - ${this.pipelineStatus.label}
@@ -66,11 +73,22 @@ export default {
? sprintf(__('Created by %{job}'), { job: this.pipeline.source_job.name })
: '';
},
+ expandedIcon() {
+ if (this.parentPipeline) {
+ return this.expanded ? 'angle-right' : 'angle-left';
+ }
+ return this.expanded ? 'angle-left' : 'angle-right';
+ },
+ expandButtonPosition() {
+ return this.parentPipeline ? 'gl-left-0 gl-border-r-1!' : 'gl-right-0 gl-border-l-1!';
+ },
},
methods: {
onClickLinkedPipeline() {
this.$root.$emit('bv::hide::tooltip', this.buttonId);
+ this.expanded = !this.expanded;
this.$emit('pipelineClicked', this.$refs.linkedPipeline);
+ this.$emit('pipelineExpandToggle', this.pipeline.source_job.name, this.expanded);
},
hideTooltips() {
this.$root.$emit('bv::hide::tooltip');
@@ -88,27 +106,48 @@ export default {
<template>
<li
ref="linkedPipeline"
+ v-gl-tooltip
class="linked-pipeline build"
+ :title="tooltipText"
:class="{ 'downstream-pipeline': isDownstream }"
data-qa-selector="child_pipeline"
@mouseover="onDownstreamHovered"
@mouseleave="onDownstreamHoverLeave"
>
- <gl-button
- :id="buttonId"
- v-gl-tooltip
- :title="tooltipText"
- class="linked-pipeline-content"
- data-qa-selector="linked_pipeline_button"
- :class="`js-pipeline-expand-${pipeline.id}`"
- :loading="pipeline.isLoading"
- @click="onClickLinkedPipeline"
+ <div
+ class="gl-relative gl-bg-white gl-p-3 gl-border-solid gl-border-gray-100 gl-border-1"
+ :class="{ 'gl-pl-9': parentPipeline }"
>
- <ci-status v-if="!pipeline.isLoading" :status="pipelineStatus" css-classes="gl-top-0" />
- <span class="str-truncated"> {{ downstreamTitle }} &#8226; #{{ pipeline.id }} </span>
+ <div class="gl-display-flex">
+ <ci-status
+ v-if="!pipeline.isLoading"
+ :status="pipelineStatus"
+ css-classes="gl-top-0 gl-pr-2"
+ />
+ <div v-else class="gl-pr-2"><gl-loading-icon inline /></div>
+ <div class="gl-display-flex gl-flex-direction-column gl-w-13">
+ <span class="gl-text-truncate">
+ {{ downstreamTitle }}
+ </span>
+ <div class="gl-text-truncate">
+ <gl-link class="gl-text-blue-500!" :href="pipeline.path" data-testid="pipelineLink"
+ >#{{ pipeline.id }}</gl-link
+ >
+ </div>
+ </div>
+ </div>
<div class="gl-pt-2">
<span class="badge badge-primary" data-testid="downstream-pipeline-label">{{ label }}</span>
</div>
- </gl-button>
+ <gl-button
+ :id="buttonId"
+ class="gl-absolute gl-top-0 gl-bottom-0 gl-shadow-none! gl-rounded-0!"
+ :class="`js-pipeline-expand-${pipeline.id} ${expandButtonPosition}`"
+ :icon="expandedIcon"
+ data-testid="expandPipelineButton"
+ data-qa-selector="expand_pipeline_button"
+ @click="onClickLinkedPipeline"
+ />
+ </div>
</li>
</template>
diff --git a/app/assets/javascripts/pipelines/components/graph/linked_pipelines_column.vue b/app/assets/javascripts/pipelines/components/graph/linked_pipelines_column.vue
index d82885ff8de..3ad28d88345 100644
--- a/app/assets/javascripts/pipelines/components/graph/linked_pipelines_column.vue
+++ b/app/assets/javascripts/pipelines/components/graph/linked_pipelines_column.vue
@@ -44,6 +44,9 @@ export default {
onDownstreamHovered(jobName) {
this.$emit('downstreamHovered', jobName);
},
+ onPipelineExpandToggle(jobName, expanded) {
+ this.$emit('pipelineExpandToggle', jobName, expanded);
+ },
},
};
</script>
@@ -65,6 +68,7 @@ export default {
:project-id="projectId"
@pipelineClicked="onPipelineClick($event, pipeline, index)"
@downstreamHovered="onDownstreamHovered"
+ @pipelineExpandToggle="onPipelineExpandToggle"
/>
</ul>
</div>
diff --git a/app/assets/javascripts/pipelines/components/graph/stage_column_component.vue b/app/assets/javascripts/pipelines/components/graph/stage_column_component.vue
index 9de6ba819c2..1453c349f44 100644
--- a/app/assets/javascripts/pipelines/components/graph/stage_column_component.vue
+++ b/app/assets/javascripts/pipelines/components/graph/stage_column_component.vue
@@ -41,6 +41,11 @@ export default {
required: false,
default: '',
},
+ pipelineExpanded: {
+ type: Object,
+ required: false,
+ default: () => ({}),
+ },
},
computed: {
hasAction() {
@@ -86,6 +91,7 @@ export default {
v-if="group.size === 1"
:job="group.jobs[0]"
:job-hovered="jobHovered"
+ :pipeline-expanded="pipelineExpanded"
css-class-job-name="build-content"
@pipelineActionRequestComplete="pipelineActionRequestComplete"
/>
diff --git a/app/assets/javascripts/pipelines/components/pipeline_graph/gitlab_ci_yaml_visualization.vue b/app/assets/javascripts/pipelines/components/pipeline_graph/gitlab_ci_yaml_visualization.vue
new file mode 100644
index 00000000000..3cc76425e2a
--- /dev/null
+++ b/app/assets/javascripts/pipelines/components/pipeline_graph/gitlab_ci_yaml_visualization.vue
@@ -0,0 +1,76 @@
+<script>
+import { GlTab, GlTabs } from '@gitlab/ui';
+import jsYaml from 'js-yaml';
+import PipelineGraph from './pipeline_graph.vue';
+import { preparePipelineGraphData } from '../../utils';
+
+export default {
+ FILE_CONTENT_SELECTOR: '#blob-content',
+ EMPTY_FILE_SELECTOR: '.nothing-here-block',
+
+ components: {
+ GlTab,
+ GlTabs,
+ PipelineGraph,
+ },
+ props: {
+ blobData: {
+ required: true,
+ type: String,
+ },
+ },
+ data() {
+ return {
+ selectedTabIndex: 0,
+ pipelineData: {},
+ };
+ },
+ computed: {
+ isVisualizationTab() {
+ return this.selectedTabIndex === 1;
+ },
+ },
+ async created() {
+ if (this.blobData) {
+ // The blobData in this case represents the gitlab-ci.yml data
+ const json = await jsYaml.load(this.blobData);
+ this.pipelineData = preparePipelineGraphData(json);
+ }
+ },
+ methods: {
+ // This is used because the blob page still uses haml, and we can't make
+ // our haml hide the unused section so we resort to a standard query here.
+ toggleFileContent({ isFileTab }) {
+ const el = document.querySelector(this.$options.FILE_CONTENT_SELECTOR);
+ const emptySection = document.querySelector(this.$options.EMPTY_FILE_SELECTOR);
+
+ const elementToHide = el || emptySection;
+
+ if (!elementToHide) {
+ return;
+ }
+
+ // Checking for the current style display prevents user
+ // from toggling visiblity on and off when clicking on the tab
+ if (!isFileTab && elementToHide.style.display !== 'none') {
+ elementToHide.style.display = 'none';
+ }
+
+ if (isFileTab && elementToHide.style.display === 'none') {
+ elementToHide.style.display = 'block';
+ }
+ },
+ },
+};
+</script>
+<template>
+ <div>
+ <div>
+ <gl-tabs v-model="selectedTabIndex">
+ <gl-tab :title="__('File')" @click="toggleFileContent({ isFileTab: true })" />
+ <gl-tab :title="__('Visualization')" @click="toggleFileContent({ isFileTab: false })" />
+ </gl-tabs>
+ </div>
+ <pipeline-graph v-if="isVisualizationTab" :pipeline-data="pipelineData" />
+ </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
new file mode 100644
index 00000000000..19d41b166c3
--- /dev/null
+++ b/app/assets/javascripts/pipelines/components/pipeline_graph/job_pill.vue
@@ -0,0 +1,24 @@
+<script>
+import tooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate.vue';
+
+export default {
+ components: {
+ tooltipOnTruncate,
+ },
+ props: {
+ jobName: {
+ type: String,
+ required: true,
+ },
+ },
+};
+</script>
+<template>
+ <tooltip-on-truncate :title="jobName" truncate-target="child" placement="top">
+ <div
+ class="gl-bg-white gl-text-center gl-text-truncate gl-rounded-pill gl-inset-border-1-green-600 gl-mb-3 gl-px-5 gl-py-2 pipeline-job-pill "
+ >
+ {{ jobName }}
+ </div>
+ </tooltip-on-truncate>
+</template>
diff --git a/app/assets/javascripts/pipelines/components/pipeline_graph/pipeline_graph.vue b/app/assets/javascripts/pipelines/components/pipeline_graph/pipeline_graph.vue
new file mode 100644
index 00000000000..6a0d3cce1f3
--- /dev/null
+++ b/app/assets/javascripts/pipelines/components/pipeline_graph/pipeline_graph.vue
@@ -0,0 +1,57 @@
+<script>
+import { isEmpty } from 'lodash';
+import { GlAlert } from '@gitlab/ui';
+import JobPill from './job_pill.vue';
+import StagePill from './stage_pill.vue';
+
+export default {
+ components: {
+ GlAlert,
+ JobPill,
+ StagePill,
+ },
+ props: {
+ pipelineData: {
+ required: true,
+ type: Object,
+ },
+ },
+ computed: {
+ isPipelineDataEmpty() {
+ return isEmpty(this.pipelineData);
+ },
+ emptyClass() {
+ return !this.isPipelineDataEmpty ? 'gl-py-7' : '';
+ },
+ },
+};
+</script>
+<template>
+ <div class="gl-display-flex gl-bg-gray-50 gl-px-4 gl-overflow-auto" :class="emptyClass">
+ <gl-alert v-if="isPipelineDataEmpty" variant="tip" :dismissible="false">
+ {{ __('No content to show') }}
+ </gl-alert>
+ <template v-else>
+ <div
+ v-for="(stage, index) in pipelineData.stages"
+ :key="`${stage.name}-${index}`"
+ class="gl-flex-direction-column"
+ >
+ <div
+ class="gl-display-flex gl-align-items-center gl-bg-white gl-w-full gl-px-8 gl-py-4 gl-mb-5"
+ :class="{
+ 'stage-left-rounded': index === 0,
+ 'stage-right-rounded': index === pipelineData.stages.length - 1,
+ }"
+ >
+ <stage-pill :stage-name="stage.name" :is-empty="stage.groups.length === 0" />
+ </div>
+ <div
+ class="gl-display-flex gl-flex-direction-column gl-align-items-center gl-w-full gl-px-8"
+ >
+ <job-pill v-for="group in stage.groups" :key="group.name" :job-name="group.name" />
+ </div>
+ </div>
+ </template>
+ </div>
+</template>
diff --git a/app/assets/javascripts/pipelines/components/pipeline_graph/stage_pill.vue b/app/assets/javascripts/pipelines/components/pipeline_graph/stage_pill.vue
new file mode 100644
index 00000000000..7b2458db725
--- /dev/null
+++ b/app/assets/javascripts/pipelines/components/pipeline_graph/stage_pill.vue
@@ -0,0 +1,35 @@
+<script>
+import tooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate.vue';
+
+export default {
+ components: {
+ tooltipOnTruncate,
+ },
+ props: {
+ stageName: {
+ type: String,
+ required: true,
+ },
+ isEmpty: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ },
+ computed: {
+ emptyClass() {
+ return this.isEmpty ? 'gl-bg-gray-200' : 'gl-bg-gray-600';
+ },
+ },
+};
+</script>
+<template>
+ <tooltip-on-truncate :title="stageName" truncate-target="child" placement="top">
+ <div
+ class="gl-px-5 gl-py-2 gl-text-white gl-text-center gl-text-truncate gl-rounded-pill pipeline-stage-pill"
+ :class="emptyClass"
+ >
+ {{ stageName }}
+ </div>
+ </tooltip-on-truncate>
+</template>
diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/nav_controls.vue b/app/assets/javascripts/pipelines/components/pipelines_list/nav_controls.vue
index a66bbb7e5ba..d7b6e033bd1 100644
--- a/app/assets/javascripts/pipelines/components/pipelines_list/nav_controls.vue
+++ b/app/assets/javascripts/pipelines/components/pipelines_list/nav_controls.vue
@@ -1,12 +1,10 @@
<script>
-import { GlDeprecatedButton } from '@gitlab/ui';
-import LoadingButton from '~/vue_shared/components/loading_button.vue';
+import { GlButton } from '@gitlab/ui';
export default {
name: 'PipelineNavControls',
components: {
- LoadingButton,
- GlDeprecatedButton,
+ GlButton,
},
props: {
newPipelinePath: {
@@ -42,25 +40,27 @@ export default {
</script>
<template>
<div class="nav-controls">
- <gl-deprecated-button
+ <gl-button
v-if="newPipelinePath"
:href="newPipelinePath"
variant="success"
+ category="primary"
class="js-run-pipeline"
>
{{ s__('Pipelines|Run Pipeline') }}
- </gl-deprecated-button>
+ </gl-button>
- <loading-button
+ <gl-button
v-if="resetCachePath"
:loading="isResetCacheButtonLoading"
- :label="s__('Pipelines|Clear Runner Caches')"
class="js-clear-cache"
@click="onClickResetCache"
- />
+ >
+ {{ s__('Pipelines|Clear Runner Caches') }}
+ </gl-button>
- <gl-deprecated-button v-if="ciLintPath" :href="ciLintPath" class="js-ci-lint">
+ <gl-button v-if="ciLintPath" :href="ciLintPath" class="js-ci-lint">
{{ s__('Pipelines|CI Lint') }}
- </gl-deprecated-button>
+ </gl-button>
</div>
</template>
diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_stop_modal.vue b/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_stop_modal.vue
index f604edd8859..43a54090e18 100644
--- a/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_stop_modal.vue
+++ b/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_stop_modal.vue
@@ -1,4 +1,5 @@
<script>
+/* eslint-disable vue/no-v-html */
import { isEmpty } from 'lodash';
import { GlLink } from '@gitlab/ui';
import DeprecatedModal2 from '~/vue_shared/components/deprecated_modal_2.vue';
diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/pipelines.vue b/app/assets/javascripts/pipelines/components/pipelines_list/pipelines.vue
index 2dfc6485d85..adba86d384b 100644
--- a/app/assets/javascripts/pipelines/components/pipelines_list/pipelines.vue
+++ b/app/assets/javascripts/pipelines/components/pipelines_list/pipelines.vue
@@ -1,5 +1,6 @@
<script>
import { isEqual } from 'lodash';
+import { GlIcon } from '@gitlab/ui';
import { __, s__ } from '~/locale';
import { deprecatedCreateFlash as createFlash } from '~/flash';
import PipelinesService from '../../services/pipelines_service';
@@ -9,7 +10,6 @@ import NavigationTabs from '~/vue_shared/components/navigation_tabs.vue';
import NavigationControls from './nav_controls.vue';
import { getParameterByName } from '~/lib/utils/common_utils';
import CIPaginationMixin from '~/vue_shared/mixins/ci_pagination_api_mixin';
-import Icon from '~/vue_shared/components/icon.vue';
import PipelinesFilteredSearch from './pipelines_filtered_search.vue';
import { validateParams } from '../../utils';
import { ANY_TRIGGER_AUTHOR, RAW_TEXT_WARNING, FILTER_TAG_IDENTIFIER } from '../../constants';
@@ -21,7 +21,7 @@ export default {
NavigationTabs,
NavigationControls,
PipelinesFilteredSearch,
- Icon,
+ GlIcon,
},
mixins: [pipelinesMixin, CIPaginationMixin, glFeatureFlagsMixin()],
props: {
@@ -285,8 +285,8 @@ export default {
v-if="shouldRenderTabs || shouldRenderButtons"
class="top-area scrolling-tabs-container inner-page-scroll-tabs"
>
- <div class="fade-left"><icon name="chevron-lg-left" :size="12" /></div>
- <div class="fade-right"><icon name="chevron-lg-right" :size="12" /></div>
+ <div class="fade-left"><gl-icon name="chevron-lg-left" :size="12" /></div>
+ <div class="fade-right"><gl-icon name="chevron-lg-right" :size="12" /></div>
<navigation-tabs
v-if="shouldRenderTabs"
diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_actions.vue b/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_actions.vue
index 098efe68b83..97595e5d2ce 100644
--- a/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_actions.vue
+++ b/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_actions.vue
@@ -1,10 +1,9 @@
<script>
-import { GlDeprecatedButton, GlTooltipDirective, GlLoadingIcon } from '@gitlab/ui';
+import { GlTooltipDirective, GlButton, GlLoadingIcon, GlIcon } from '@gitlab/ui';
import axios from '~/lib/utils/axios_utils';
import { deprecatedCreateFlash as flash } from '~/flash';
import { s__, __, sprintf } from '~/locale';
import GlCountdown from '~/vue_shared/components/gl_countdown.vue';
-import Icon from '~/vue_shared/components/icon.vue';
import eventHub from '../../event_hub';
export default {
@@ -12,9 +11,9 @@ export default {
GlTooltip: GlTooltipDirective,
},
components: {
- Icon,
+ GlIcon,
GlCountdown,
- GlDeprecatedButton,
+ GlButton,
GlLoadingIcon,
},
props: {
@@ -83,29 +82,32 @@ export default {
type="button"
:disabled="isLoading"
class="dropdown-new btn btn-default js-pipeline-dropdown-manual-actions"
- :title="__('Manual job')"
+ :title="__('Run manual or delayed jobs')"
data-toggle="dropdown"
- :aria-label="__('Manual job')"
+ :aria-label="__('Run manual or delayed jobs')"
>
- <icon name="play" class="icon-play" />
+ <gl-icon name="play" class="icon-play" />
<i class="fa fa-caret-down" aria-hidden="true"></i>
<gl-loading-icon v-if="isLoading" />
</button>
<ul class="dropdown-menu dropdown-menu-right">
<li v-for="action in actions" :key="action.path">
- <gl-deprecated-button
+ <gl-button
+ category="tertiary"
:class="{ disabled: isActionDisabled(action) }"
:disabled="isActionDisabled(action)"
- class="js-pipeline-action-link no-btn btn d-flex align-items-center justify-content-between flex-wrap"
+ class="js-pipeline-action-link"
@click="onClickAction(action)"
>
- {{ action.name }}
- <span v-if="action.scheduled_at">
- <icon name="clock" />
- <gl-countdown :end-date-string="action.scheduled_at" />
- </span>
- </gl-deprecated-button>
+ <div class="d-flex justify-content-between flex-wrap">
+ {{ action.name }}
+ <span v-if="action.scheduled_at">
+ <gl-icon name="clock" />
+ <gl-countdown :end-date-string="action.scheduled_at" />
+ </span>
+ </div>
+ </gl-button>
</li>
</ul>
</div>
diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_artifacts.vue b/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_artifacts.vue
index 59c066b2683..4a3d134685e 100644
--- a/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_artifacts.vue
+++ b/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_artifacts.vue
@@ -1,14 +1,13 @@
<script>
/* eslint-disable @gitlab/vue-require-i18n-strings */
-import { GlLink, GlTooltipDirective } from '@gitlab/ui';
-import Icon from '~/vue_shared/components/icon.vue';
+import { GlLink, GlTooltipDirective, GlIcon } from '@gitlab/ui';
export default {
directives: {
GlTooltip: GlTooltipDirective,
},
components: {
- Icon,
+ GlIcon,
GlLink,
},
props: {
@@ -29,7 +28,7 @@ export default {
data-toggle="dropdown"
:aria-label="__('Artifacts')"
>
- <icon name="download" />
+ <gl-icon name="download" />
<i class="fa fa-caret-down" aria-hidden="true"></i>
</button>
<ul class="dropdown-menu dropdown-menu-right">
diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_table_row.vue b/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_table_row.vue
index f25994a7506..1bdb7d18f04 100644
--- a/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_table_row.vue
+++ b/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_table_row.vue
@@ -1,4 +1,5 @@
<script>
+import { GlButton } from '@gitlab/ui';
import eventHub from '../../event_hub';
import PipelinesActionsComponent from './pipelines_actions.vue';
import PipelinesArtifactsComponent from './pipelines_artifacts.vue';
@@ -8,8 +9,6 @@ import PipelineUrl from './pipeline_url.vue';
import PipelineTriggerer from './pipeline_triggerer.vue';
import PipelinesTimeago from './time_ago.vue';
import CommitComponent from '~/vue_shared/components/commit.vue';
-import LoadingButton from '~/vue_shared/components/loading_button.vue';
-import Icon from '~/vue_shared/components/icon.vue';
import { PIPELINES_TABLE } from '../../constants';
/**
@@ -27,8 +26,7 @@ export default {
PipelineTriggerer,
CiBadge,
PipelinesTimeago,
- LoadingButton,
- Icon,
+ GlButton,
},
props: {
pipeline: {
@@ -274,6 +272,7 @@ export default {
<ci-badge
:status="pipelineStatus"
:show-text="!isChildView"
+ :icon-classes="'gl-vertical-align-middle!'"
data-qa-selector="pipeline_commit_status"
/>
</div>
@@ -337,28 +336,30 @@ export default {
class="d-md-block"
/>
- <loading-button
+ <gl-button
v-if="pipeline.flags.retryable"
:loading="isRetrying"
:disabled="isRetrying"
- container-class="js-pipelines-retry-button btn btn-default btn-retry"
+ class="js-pipelines-retry-button btn-retry"
data-qa-selector="pipeline_retry_button"
+ icon="repeat"
+ variant="default"
+ category="secondary"
@click="handleRetryClick"
- >
- <icon name="repeat" />
- </loading-button>
+ />
- <loading-button
+ <gl-button
v-if="pipeline.flags.cancelable"
:loading="isCancelling"
:disabled="isCancelling"
data-toggle="modal"
data-target="#confirmation-modal"
- container-class="js-pipelines-cancel-button btn btn-remove"
+ icon="close"
+ variant="danger"
+ category="primary"
+ class="js-pipelines-cancel-button"
@click="handleCancelClick"
- >
- <icon name="close" />
- </loading-button>
+ />
</div>
</div>
</div>
diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/stage.vue b/app/assets/javascripts/pipelines/components/pipelines_list/stage.vue
index d992a4b7752..4045f450104 100644
--- a/app/assets/javascripts/pipelines/components/pipelines_list/stage.vue
+++ b/app/assets/javascripts/pipelines/components/pipelines_list/stage.vue
@@ -13,18 +13,17 @@
*/
import $ from 'jquery';
-import { GlLoadingIcon, GlTooltipDirective } from '@gitlab/ui';
+import { GlLoadingIcon, GlTooltipDirective, GlIcon } from '@gitlab/ui';
import { __ } from '~/locale';
import { deprecatedCreateFlash as Flash } from '~/flash';
import axios from '~/lib/utils/axios_utils';
import eventHub from '../../event_hub';
-import Icon from '~/vue_shared/components/icon.vue';
import JobItem from '../graph/job_item.vue';
import { PIPELINES_TABLE } from '../../constants';
export default {
components: {
- Icon,
+ GlIcon,
JobItem,
GlLoadingIcon,
},
@@ -170,7 +169,7 @@ export default {
@click="onClickStage"
>
<span :aria-label="stage.title" aria-hidden="true" class="no-pointer-events">
- <icon :name="borderlessIcon" />
+ <gl-icon :name="borderlessIcon" />
</span>
</button>
diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/time_ago.vue b/app/assets/javascripts/pipelines/components/pipelines_list/time_ago.vue
index 8a01e1fe3f5..7d13ee582c6 100644
--- a/app/assets/javascripts/pipelines/components/pipelines_list/time_ago.vue
+++ b/app/assets/javascripts/pipelines/components/pipelines_list/time_ago.vue
@@ -1,5 +1,5 @@
<script>
-import iconTimerSvg from 'icons/_icon_timer.svg';
+import { GlIcon } from '@gitlab/ui';
import '~/lib/utils/datetime_utility';
import tooltip from '~/vue_shared/directives/tooltip';
import timeagoMixin from '~/vue_shared/mixins/timeago';
@@ -8,6 +8,7 @@ export default {
directives: {
tooltip,
},
+ components: { GlIcon },
mixins: [timeagoMixin],
props: {
finishedTime: {
@@ -19,11 +20,6 @@ export default {
required: true,
},
},
- data() {
- return {
- iconTimerSvg,
- };
- },
computed: {
hasDuration() {
return this.duration > 0;
@@ -59,11 +55,12 @@ export default {
<div class="table-mobile-header" role="rowheader">{{ s__('Pipeline|Duration') }}</div>
<div class="table-mobile-content">
<p v-if="hasDuration" class="duration">
- <span v-html="iconTimerSvg"> </span> {{ durationFormatted }}
+ <gl-icon name="timer" class="gl-vertical-align-baseline!" aria-hidden="true" />
+ {{ durationFormatted }}
</p>
<p v-if="hasFinishedTime" class="finished-at d-none d-sm-none d-md-block">
- <i class="fa fa-calendar" aria-hidden="true"> </i>
+ <gl-icon name="calendar" class="gl-vertical-align-baseline!" aria-hidden="true" />
<time
v-tooltip
diff --git a/app/assets/javascripts/pipelines/components/test_reports/test_reports.vue b/app/assets/javascripts/pipelines/components/test_reports/test_reports.vue
index bc1d22e2976..c3398e90895 100644
--- a/app/assets/javascripts/pipelines/components/test_reports/test_reports.vue
+++ b/app/assets/javascripts/pipelines/components/test_reports/test_reports.vue
@@ -55,13 +55,14 @@ export default {
<template>
<div v-if="isLoading">
- <gl-loading-icon size="lg" class="gl-mt-3 js-loading-spinner" />
+ <gl-loading-icon size="lg" class="gl-mt-3" />
</div>
<div
v-else-if="!isLoading && showTests"
ref="container"
- class="tests-detail position-relative js-tests-detail"
+ class="tests-detail position-relative"
+ data-testid="tests-detail"
>
<transition
name="slide"
@@ -85,7 +86,7 @@ export default {
<div v-else>
<div class="row gl-mt-3">
<div class="col-12">
- <p class="js-no-tests-to-show">{{ s__('TestReports|There are no tests to show.') }}</p>
+ <p data-testid="no-tests-to-show">{{ s__('TestReports|There are no tests to show.') }}</p>
</div>
</div>
</div>
diff --git a/app/assets/javascripts/pipelines/components/test_reports/test_suite_table.vue b/app/assets/javascripts/pipelines/components/test_reports/test_suite_table.vue
index 478073e44d1..aa53c5040e8 100644
--- a/app/assets/javascripts/pipelines/components/test_reports/test_suite_table.vue
+++ b/app/assets/javascripts/pipelines/components/test_reports/test_suite_table.vue
@@ -1,15 +1,12 @@
<script>
import { mapGetters } from 'vuex';
-import { GlTooltipDirective, GlFriendlyWrap } from '@gitlab/ui';
-import Icon from '~/vue_shared/components/icon.vue';
+import { GlTooltipDirective, GlFriendlyWrap, GlIcon } from '@gitlab/ui';
import { __ } from '~/locale';
-import SmartVirtualList from '~/vue_shared/components/smart_virtual_list.vue';
export default {
name: 'TestsSuiteTable',
components: {
- Icon,
- SmartVirtualList,
+ GlIcon,
GlFriendlyWrap,
},
directives: {
@@ -28,8 +25,6 @@ export default {
return this.getSuiteTests.length > 0;
},
},
- maxShownRows: 30,
- typicalRowHeight: 75,
wrapSymbols: ['::', '#', '.', '_', '-', '/', '\\'],
};
</script>
@@ -61,66 +56,60 @@ export default {
</div>
</div>
- <smart-virtual-list
- :length="getSuiteTests.length"
- :remain="$options.maxShownRows"
- :size="$options.typicalRowHeight"
+ <div
+ v-for="(testCase, index) in getSuiteTests"
+ :key="index"
+ class="gl-responsive-table-row rounded align-items-md-start mt-xs-3 js-case-row"
>
- <div
- v-for="(testCase, index) in getSuiteTests"
- :key="index"
- class="gl-responsive-table-row rounded align-items-md-start mt-xs-3 js-case-row"
- >
- <div class="table-section section-20 section-wrap">
- <div role="rowheader" class="table-mobile-header">{{ __('Suite') }}</div>
- <div class="table-mobile-content pr-md-1 gl-overflow-wrap-break">
- <gl-friendly-wrap :symbols="$options.wrapSymbols" :text="testCase.classname" />
- </div>
+ <div class="table-section section-20 section-wrap">
+ <div role="rowheader" class="table-mobile-header">{{ __('Suite') }}</div>
+ <div class="table-mobile-content pr-md-1 gl-overflow-wrap-break">
+ <gl-friendly-wrap :symbols="$options.wrapSymbols" :text="testCase.classname" />
</div>
+ </div>
- <div class="table-section section-20 section-wrap">
- <div role="rowheader" class="table-mobile-header">{{ __('Name') }}</div>
- <div class="table-mobile-content pr-md-1 gl-overflow-wrap-break">
- <gl-friendly-wrap
- data-testid="caseName"
- :symbols="$options.wrapSymbols"
- :text="testCase.name"
- />
- </div>
+ <div class="table-section section-20 section-wrap">
+ <div role="rowheader" class="table-mobile-header">{{ __('Name') }}</div>
+ <div class="table-mobile-content pr-md-1 gl-overflow-wrap-break">
+ <gl-friendly-wrap
+ data-testid="caseName"
+ :symbols="$options.wrapSymbols"
+ :text="testCase.name"
+ />
</div>
+ </div>
- <div class="table-section section-10 section-wrap">
- <div role="rowheader" class="table-mobile-header">{{ __('Status') }}</div>
- <div class="table-mobile-content text-center">
- <div
- class="add-border ci-status-icon d-flex align-items-center justify-content-end justify-content-md-center"
- :class="`ci-status-icon-${testCase.status}`"
- >
- <icon :size="24" :name="testCase.icon" />
- </div>
+ <div class="table-section section-10 section-wrap">
+ <div role="rowheader" class="table-mobile-header">{{ __('Status') }}</div>
+ <div class="table-mobile-content text-center">
+ <div
+ class="add-border ci-status-icon d-flex align-items-center justify-content-end justify-content-md-center"
+ :class="`ci-status-icon-${testCase.status}`"
+ >
+ <gl-icon :size="24" :name="testCase.icon" />
</div>
</div>
+ </div>
- <div class="table-section flex-grow-1">
- <div role="rowheader" class="table-mobile-header">{{ __('Trace'), }}</div>
- <div class="table-mobile-content">
- <pre
- v-if="testCase.system_output"
- class="build-trace build-trace-rounded text-left"
- ><code class="bash p-0">{{testCase.system_output}}</code></pre>
- </div>
+ <div class="table-section flex-grow-1">
+ <div role="rowheader" class="table-mobile-header">{{ __('Trace'), }}</div>
+ <div class="table-mobile-content">
+ <pre
+ v-if="testCase.system_output"
+ class="build-trace build-trace-rounded text-left"
+ ><code class="bash p-0">{{testCase.system_output}}</code></pre>
</div>
+ </div>
- <div class="table-section section-10 section-wrap">
- <div role="rowheader" class="table-mobile-header">
- {{ __('Duration') }}
- </div>
- <div class="table-mobile-content text-right pr-sm-1">
- {{ testCase.formattedTime }}
- </div>
+ <div class="table-section section-10 section-wrap">
+ <div role="rowheader" class="table-mobile-header">
+ {{ __('Duration') }}
+ </div>
+ <div class="table-mobile-content text-right pr-sm-1">
+ {{ testCase.formattedTime }}
</div>
</div>
- </smart-virtual-list>
+ </div>
</div>
<div v-else>
diff --git a/app/assets/javascripts/pipelines/components/test_reports/test_summary.vue b/app/assets/javascripts/pipelines/components/test_reports/test_summary.vue
index 712ac5eb0e5..d33d4e7dfd0 100644
--- a/app/assets/javascripts/pipelines/components/test_reports/test_summary.vue
+++ b/app/assets/javascripts/pipelines/components/test_reports/test_summary.vue
@@ -1,15 +1,13 @@
<script>
-import { GlDeprecatedButton, GlProgressBar } from '@gitlab/ui';
+import { GlButton, GlProgressBar } from '@gitlab/ui';
import { __ } from '~/locale';
-import { formatTime, secondsToMilliseconds } from '~/lib/utils/datetime_utility';
-import Icon from '~/vue_shared/components/icon.vue';
+import { formattedTime } from '../../stores/test_reports/utils';
export default {
name: 'TestSummary',
components: {
- GlDeprecatedButton,
+ GlButton,
GlProgressBar,
- Icon,
},
props: {
report: {
@@ -39,7 +37,7 @@ export default {
return 0;
},
formattedDuration() {
- return formatTime(secondsToMilliseconds(this.report.total_time));
+ return formattedTime(this.report.total_time);
},
progressBarVariant() {
if (this.successPercentage < 33) {
@@ -69,14 +67,13 @@ export default {
<div>
<div class="row">
<div class="col-12 d-flex gl-mt-3 align-items-center">
- <gl-deprecated-button
+ <gl-button
v-if="showBack"
- size="sm"
+ size="small"
class="gl-mr-3 js-back-button"
+ icon="angle-left"
@click="onBackClick"
- >
- <icon name="angle-left" />
- </gl-deprecated-button>
+ />
<h4>{{ heading }}</h4>
</div>
diff --git a/app/assets/javascripts/pipelines/components/test_reports/test_summary_table.vue b/app/assets/javascripts/pipelines/components/test_reports/test_summary_table.vue
index e774fe06fbe..5f9c0be3ccc 100644
--- a/app/assets/javascripts/pipelines/components/test_reports/test_summary_table.vue
+++ b/app/assets/javascripts/pipelines/components/test_reports/test_summary_table.vue
@@ -2,13 +2,11 @@
import { mapGetters } from 'vuex';
import { GlIcon, GlTooltipDirective } from '@gitlab/ui';
import { s__ } from '~/locale';
-import SmartVirtualList from '~/vue_shared/components/smart_virtual_list.vue';
export default {
name: 'TestsSummaryTable',
components: {
GlIcon,
- SmartVirtualList,
},
directives: {
GlTooltip: GlTooltipDirective,
@@ -31,8 +29,6 @@ export default {
this.$emit('row-click', index);
},
},
- maxShownRows: 20,
- typicalRowHeight: 55,
};
</script>
@@ -69,83 +65,77 @@ export default {
</div>
</div>
- <smart-virtual-list
- :length="getTestSuites.length"
- :remain="$options.maxShownRows"
- :size="$options.typicalRowHeight"
+ <div
+ v-for="(testSuite, index) in getTestSuites"
+ :key="index"
+ role="row"
+ class="gl-responsive-table-row test-reports-summary-row rounded js-suite-row"
+ :class="{
+ 'gl-responsive-table-row-clickable cursor-pointer': !testSuite.suite_error,
+ }"
+ @click="tableRowClick(index)"
>
- <div
- v-for="(testSuite, index) in getTestSuites"
- :key="index"
- role="row"
- class="gl-responsive-table-row test-reports-summary-row rounded js-suite-row"
- :class="{
- 'gl-responsive-table-row-clickable cursor-pointer': !testSuite.suite_error,
- }"
- @click="tableRowClick(index)"
- >
- <div class="table-section section-25">
- <div role="rowheader" class="table-mobile-header font-weight-bold">
- {{ __('Suite') }}
- </div>
- <div class="table-mobile-content underline cgray pl-3">
- {{ testSuite.name }}
- <gl-icon
- v-if="testSuite.suite_error"
- ref="suiteErrorIcon"
- v-gl-tooltip
- name="error"
- :title="testSuite.suite_error"
- class="vertical-align-middle"
- />
- </div>
+ <div class="table-section section-25">
+ <div role="rowheader" class="table-mobile-header font-weight-bold">
+ {{ __('Suite') }}
</div>
+ <div class="table-mobile-content underline cgray pl-3">
+ {{ testSuite.name }}
+ <gl-icon
+ v-if="testSuite.suite_error"
+ ref="suiteErrorIcon"
+ v-gl-tooltip
+ name="error"
+ :title="testSuite.suite_error"
+ class="vertical-align-middle"
+ />
+ </div>
+ </div>
- <div class="table-section section-25">
- <div role="rowheader" class="table-mobile-header font-weight-bold">
- {{ __('Duration') }}
- </div>
- <div class="table-mobile-content text-md-left">
- {{ testSuite.formattedTime }}
- </div>
+ <div class="table-section section-25">
+ <div role="rowheader" class="table-mobile-header font-weight-bold">
+ {{ __('Duration') }}
+ </div>
+ <div class="table-mobile-content text-md-left">
+ {{ testSuite.formattedTime }}
</div>
+ </div>
- <div class="table-section section-10 text-center">
- <div role="rowheader" class="table-mobile-header font-weight-bold">
- {{ __('Failed') }}
- </div>
- <div class="table-mobile-content">{{ testSuite.failed_count }}</div>
+ <div class="table-section section-10 text-center">
+ <div role="rowheader" class="table-mobile-header font-weight-bold">
+ {{ __('Failed') }}
</div>
+ <div class="table-mobile-content">{{ testSuite.failed_count }}</div>
+ </div>
- <div class="table-section section-10 text-center">
- <div role="rowheader" class="table-mobile-header font-weight-bold">
- {{ __('Errors') }}
- </div>
- <div class="table-mobile-content">{{ testSuite.error_count }}</div>
+ <div class="table-section section-10 text-center">
+ <div role="rowheader" class="table-mobile-header font-weight-bold">
+ {{ __('Errors') }}
</div>
+ <div class="table-mobile-content">{{ testSuite.error_count }}</div>
+ </div>
- <div class="table-section section-10 text-center">
- <div role="rowheader" class="table-mobile-header font-weight-bold">
- {{ __('Skipped') }}
- </div>
- <div class="table-mobile-content">{{ testSuite.skipped_count }}</div>
+ <div class="table-section section-10 text-center">
+ <div role="rowheader" class="table-mobile-header font-weight-bold">
+ {{ __('Skipped') }}
</div>
+ <div class="table-mobile-content">{{ testSuite.skipped_count }}</div>
+ </div>
- <div class="table-section section-10 text-center">
- <div role="rowheader" class="table-mobile-header font-weight-bold">
- {{ __('Passed') }}
- </div>
- <div class="table-mobile-content">{{ testSuite.success_count }}</div>
+ <div class="table-section section-10 text-center">
+ <div role="rowheader" class="table-mobile-header font-weight-bold">
+ {{ __('Passed') }}
</div>
+ <div class="table-mobile-content">{{ testSuite.success_count }}</div>
+ </div>
- <div class="table-section section-10 text-right pr-md-3">
- <div role="rowheader" class="table-mobile-header font-weight-bold">
- {{ __('Total') }}
- </div>
- <div class="table-mobile-content">{{ testSuite.total_count }}</div>
+ <div class="table-section section-10 text-right pr-md-3">
+ <div role="rowheader" class="table-mobile-header font-weight-bold">
+ {{ __('Total') }}
</div>
+ <div class="table-mobile-content">{{ testSuite.total_count }}</div>
</div>
- </smart-virtual-list>
+ </div>
</div>
<div v-else>
diff --git a/app/assets/javascripts/pipelines/pipeline_details_bundle.js b/app/assets/javascripts/pipelines/pipeline_details_bundle.js
index c57be7c75b0..745f5b886a5 100644
--- a/app/assets/javascripts/pipelines/pipeline_details_bundle.js
+++ b/app/assets/javascripts/pipelines/pipeline_details_bundle.js
@@ -14,10 +14,20 @@ import createTestReportsStore from './stores/test_reports';
Vue.use(Translate);
+const SELECTORS = {
+ PIPELINE_DETAILS: '.js-pipeline-details-vue',
+ PIPELINE_GRAPH: '#js-pipeline-graph-vue',
+ PIPELINE_HEADER: '#js-pipeline-header-vue',
+ PIPELINE_TESTS: '#js-pipeline-tests-detail',
+};
+
const createPipelinesDetailApp = mediator => {
+ if (!document.querySelector(SELECTORS.PIPELINE_GRAPH)) {
+ return;
+ }
// eslint-disable-next-line no-new
new Vue({
- el: '#js-pipeline-graph-vue',
+ el: SELECTORS.PIPELINE_GRAPH,
components: {
pipelineGraph,
},
@@ -47,9 +57,12 @@ const createPipelinesDetailApp = mediator => {
};
const createPipelineHeaderApp = mediator => {
+ if (!document.querySelector(SELECTORS.PIPELINE_HEADER)) {
+ return;
+ }
// eslint-disable-next-line no-new
new Vue({
- el: '#js-pipeline-header-vue',
+ el: SELECTORS.PIPELINE_HEADER,
components: {
pipelineHeader,
},
@@ -93,9 +106,8 @@ const createPipelineHeaderApp = mediator => {
};
const createTestDetails = () => {
- const el = document.querySelector('#js-pipeline-tests-detail');
+ const el = document.querySelector(SELECTORS.PIPELINE_TESTS);
const { summaryEndpoint, suiteEndpoint } = el?.dataset || {};
-
const testReportsStore = createTestReportsStore({
summaryEndpoint,
suiteEndpoint,
@@ -115,7 +127,7 @@ const createTestDetails = () => {
};
export default () => {
- const { dataset } = document.querySelector('.js-pipeline-details-vue');
+ const { dataset } = document.querySelector(SELECTORS.PIPELINE_DETAILS);
const mediator = new PipelinesMediator({ endpoint: dataset.endpoint });
mediator.fetchPipeline();
diff --git a/app/assets/javascripts/pipelines/stores/test_reports/getters.js b/app/assets/javascripts/pipelines/stores/test_reports/getters.js
index 6c670806cc4..c123014756d 100644
--- a/app/assets/javascripts/pipelines/stores/test_reports/getters.js
+++ b/app/assets/javascripts/pipelines/stores/test_reports/getters.js
@@ -1,4 +1,4 @@
-import { addIconStatus, formattedTime, sortTestCases } from './utils';
+import { addIconStatus, formattedTime } from './utils';
export const getTestSuites = state => {
const { test_suites: testSuites = [] } = state.testReports;
@@ -14,5 +14,5 @@ export const getSelectedSuite = state =>
export const getSuiteTests = state => {
const { test_cases: testCases = [] } = getSelectedSuite(state);
- return testCases.sort(sortTestCases).map(addIconStatus);
+ return testCases.map(addIconStatus);
};
diff --git a/app/assets/javascripts/pipelines/stores/test_reports/utils.js b/app/assets/javascripts/pipelines/stores/test_reports/utils.js
index 16fa6935cbe..8f1ac305cda 100644
--- a/app/assets/javascripts/pipelines/stores/test_reports/utils.js
+++ b/app/assets/javascripts/pipelines/stores/test_reports/utils.js
@@ -1,5 +1,4 @@
-import { TestStatus } from '~/pipelines/constants';
-import { formatTime, secondsToMilliseconds } from '~/lib/utils/datetime_utility';
+import { __, sprintf } from '../../../locale';
export function iconForTestStatus(status) {
switch (status) {
@@ -12,25 +11,16 @@ export function iconForTestStatus(status) {
}
}
-export const formattedTime = timeInSeconds => formatTime(secondsToMilliseconds(timeInSeconds));
+export const formattedTime = (seconds = 0) => {
+ if (seconds < 1) {
+ const milliseconds = seconds * 1000;
+ return sprintf(__('%{milliseconds}ms'), { milliseconds: milliseconds.toFixed(2) });
+ }
+ return sprintf(__('%{seconds}s'), { seconds: seconds.toFixed(2) });
+};
export const addIconStatus = testCase => ({
...testCase,
icon: iconForTestStatus(testCase.status),
formattedTime: formattedTime(testCase.execution_time),
});
-
-export const sortTestCases = (a, b) => {
- if (a.status === b.status) {
- return 0;
- }
-
- switch (b.status) {
- case TestStatus.SUCCESS:
- return -1;
- case TestStatus.FAILED:
- return 1;
- default:
- return 0;
- }
-};
diff --git a/app/assets/javascripts/pipelines/utils.js b/app/assets/javascripts/pipelines/utils.js
index 2e08001f6b3..bd53b22784c 100644
--- a/app/assets/javascripts/pipelines/utils.js
+++ b/app/assets/javascripts/pipelines/utils.js
@@ -1,7 +1,49 @@
import { pickBy } from 'lodash';
import { SUPPORTED_FILTER_PARAMETERS } from './constants';
-// eslint-disable-next-line import/prefer-default-export
export const validateParams = params => {
return pickBy(params, (val, key) => SUPPORTED_FILTER_PARAMETERS.includes(key) && val);
};
+
+/**
+ * This function takes a json payload that comes from a yml
+ * file converted to json through `jsyaml` library. Because we
+ * naively convert the entire yaml to json, some keys (like `includes`)
+ * are irrelevant to rendering the graph and must be removed. We also
+ * restructure the data to have the structure from an API response for the
+ * pipeline data.
+ * @param {Object} jsonData
+ * @returns {Array} - Array of stages containing all jobs
+ */
+export const preparePipelineGraphData = jsonData => {
+ const jsonKeys = Object.keys(jsonData);
+ const jobNames = jsonKeys.filter(job => jsonData[job]?.stage);
+
+ // We merge both the stages from the "stages" key in the yaml and the stage associated
+ // with each job to show the user both the stages they explicitly defined, and those
+ // that they added under jobs. We also remove duplicates.
+ const jobStages = jobNames.map(job => jsonData[job].stage);
+ const userDefinedStages = jsonData?.stages ?? [];
+
+ // The order is important here. We always show the stages in order they were
+ // defined in the `stages` key first, and then stages that are under the jobs.
+ const stages = Array.from(new Set([...userDefinedStages, ...jobStages]));
+
+ const arrayOfJobsByStage = stages.map(val => {
+ return jobNames.filter(job => {
+ return jsonData[job].stage === val;
+ });
+ });
+
+ const pipelineData = stages.map((stage, index) => {
+ const stageJobs = arrayOfJobsByStage[index];
+ return {
+ name: stage,
+ groups: stageJobs.map(job => {
+ return { name: job, jobs: [{ ...jsonData[job] }] };
+ }),
+ };
+ });
+
+ return { stages: pipelineData };
+};
diff --git a/app/assets/javascripts/profile/account/components/delete_account_modal.vue b/app/assets/javascripts/profile/account/components/delete_account_modal.vue
index 605859cfb6a..f06dc72d365 100644
--- a/app/assets/javascripts/profile/account/components/delete_account_modal.vue
+++ b/app/assets/javascripts/profile/account/components/delete_account_modal.vue
@@ -1,11 +1,12 @@
<script>
-import DeprecatedModal from '~/vue_shared/components/deprecated_modal.vue';
+/* eslint-disable vue/no-v-html */
+import { GlModal } from '@gitlab/ui';
import { __, s__, sprintf } from '~/locale';
import csrf from '~/lib/utils/csrf';
export default {
components: {
- DeprecatedModal,
+ GlModal,
},
props: {
actionUrl: {
@@ -54,21 +55,38 @@ You are about to permanently delete %{yourAccount}, and all of the issues, merge
Once you confirm %{deleteAccount}, it cannot be undone or recovered.`),
{
yourAccount: `<strong>${s__('Profiles|your account')}</strong>`,
- deleteAccount: `<strong>${s__('Profiles|Delete Account')}</strong>`,
+ deleteAccount: `<strong>${s__('Profiles|Delete account')}</strong>`,
},
false,
);
},
- },
- methods: {
+ primaryProps() {
+ return {
+ text: s__('Delete account'),
+ attributes: [
+ { variant: 'danger', 'data-qa-selector': 'confirm_delete_account_button' },
+ { category: 'primary' },
+ { disabled: !this.canSubmit },
+ ],
+ };
+ },
+ cancelProps() {
+ return {
+ text: s__('Cancel'),
+ };
+ },
canSubmit() {
if (this.confirmWithPassword) {
return this.enteredPassword !== '';
}
-
return this.enteredUsername === this.username;
},
+ },
+ methods: {
onSubmit() {
+ if (!this.canSubmit) {
+ return;
+ }
this.$refs.form.submit();
},
},
@@ -76,42 +94,39 @@ Once you confirm %{deleteAccount}, it cannot be undone or recovered.`),
</script>
<template>
- <deprecated-modal
- id="delete-account-modal"
- :title="s__('Profiles|Delete your account?')"
- :text="text"
- :primary-button-label="s__('Profiles|Delete account')"
- :submit-disabled="!canSubmit()"
- kind="danger"
- @submit="onSubmit"
+ <gl-modal
+ modal-id="delete-account-modal"
+ title="Profiles"
+ :action-primary="primaryProps"
+ :action-cancel="cancelProps"
+ :ok-disabled="!canSubmit"
+ @primary="onSubmit"
>
- <template #body="props">
- <p v-html="props.text"></p>
+ <p v-html="text"></p>
- <form ref="form" :action="actionUrl" method="post">
- <input type="hidden" name="_method" value="delete" />
- <input :value="csrfToken" type="hidden" name="authenticity_token" />
+ <form ref="form" :action="actionUrl" method="post">
+ <input type="hidden" name="_method" value="delete" />
+ <input :value="csrfToken" type="hidden" name="authenticity_token" />
- <p id="input-label" v-html="inputLabel"></p>
+ <p id="input-label" v-html="inputLabel"></p>
- <input
- v-if="confirmWithPassword"
- v-model="enteredPassword"
- name="password"
- class="form-control"
- type="password"
- data-qa-selector="password_confirmation_field"
- aria-labelledby="input-label"
- />
- <input
- v-else
- v-model="enteredUsername"
- name="username"
- class="form-control"
- type="text"
- aria-labelledby="input-label"
- />
- </form>
- </template>
- </deprecated-modal>
+ <input
+ v-if="confirmWithPassword"
+ v-model="enteredPassword"
+ name="password"
+ class="form-control"
+ type="password"
+ data-qa-selector="password_confirmation_field"
+ aria-labelledby="input-label"
+ />
+ <input
+ v-else
+ v-model="enteredUsername"
+ name="username"
+ class="form-control"
+ type="text"
+ aria-labelledby="input-label"
+ />
+ </form>
+ </gl-modal>
</template>
diff --git a/app/assets/javascripts/profile/account/components/update_username.vue b/app/assets/javascripts/profile/account/components/update_username.vue
index 58025381cb2..4aaa2cff2ac 100644
--- a/app/assets/javascripts/profile/account/components/update_username.vue
+++ b/app/assets/javascripts/profile/account/components/update_username.vue
@@ -1,4 +1,5 @@
<script>
+/* eslint-disable vue/no-v-html */
import { escape } from 'lodash';
import axios from '~/lib/utils/axios_utils';
import DeprecatedModal2 from '~/vue_shared/components/deprecated_modal_2.vue';
diff --git a/app/assets/javascripts/profile/account/index.js b/app/assets/javascripts/profile/account/index.js
index f0d9642a2b2..5e89002b3bc 100644
--- a/app/assets/javascripts/profile/account/index.js
+++ b/app/assets/javascripts/profile/account/index.js
@@ -30,6 +30,9 @@ export default () => {
},
mounted() {
deleteAccountButton.classList.remove('disabled');
+ deleteAccountButton.addEventListener('click', () => {
+ this.$root.$emit('bv::show::modal', 'delete-account-modal', '#delete-account-button');
+ });
},
render(createElement) {
return createElement('delete-account-modal', {
diff --git a/app/assets/javascripts/project_select.js b/app/assets/javascripts/project_select.js
index 788553636f9..db2b0856e1b 100644
--- a/app/assets/javascripts/project_select.js
+++ b/app/assets/javascripts/project_select.js
@@ -110,7 +110,10 @@ const projectSelect = () => {
});
};
-export default () =>
- import(/* webpackChunkName: 'select2' */ 'select2/select2')
- .then(projectSelect)
- .catch(() => {});
+export default () => {
+ if ($('.ajax-project-select').length) {
+ import(/* webpackChunkName: 'select2' */ 'select2/select2')
+ .then(projectSelect)
+ .catch(() => {});
+ }
+};
diff --git a/app/assets/javascripts/projects/commits/components/author_select.vue b/app/assets/javascripts/projects/commits/components/author_select.vue
index a8589b50899..2204ec3cbe7 100644
--- a/app/assets/javascripts/projects/commits/components/author_select.vue
+++ b/app/assets/javascripts/projects/commits/components/author_select.vue
@@ -2,11 +2,11 @@
import { debounce } from 'lodash';
import { mapState, mapActions } from 'vuex';
import {
- GlNewDropdown,
- GlNewDropdownHeader,
- GlNewDropdownItem,
+ GlDropdown,
+ GlDropdownSectionHeader,
+ GlDropdownItem,
GlSearchBoxByType,
- GlNewDropdownDivider,
+ GlDropdownDivider,
GlTooltipDirective,
} from '@gitlab/ui';
import { redirectTo } from '~/lib/utils/url_utility';
@@ -18,11 +18,11 @@ const tooltipMessage = __('Searching by both author and message is currently not
export default {
name: 'AuthorSelect',
components: {
- GlNewDropdown,
- GlNewDropdownHeader,
- GlNewDropdownItem,
+ GlDropdown,
+ GlDropdownSectionHeader,
+ GlDropdownItem,
GlSearchBoxByType,
- GlNewDropdownDivider,
+ GlDropdownDivider,
},
directives: {
GlTooltip: GlTooltipDirective,
@@ -107,27 +107,27 @@ export default {
<template>
<div ref="dropdownContainer" v-gl-tooltip :title="tooltipTitle" :disabled="!hasSearchParam">
- <gl-new-dropdown
+ <gl-dropdown
:text="dropdownText"
:disabled="hasSearchParam"
toggle-class="gl-py-3 gl-border-0"
class="w-100 mt-2 mt-sm-0"
>
- <gl-new-dropdown-header>
+ <gl-dropdown-section-header>
{{ __('Search by author') }}
- </gl-new-dropdown-header>
- <gl-new-dropdown-divider />
+ </gl-dropdown-section-header>
+ <gl-dropdown-divider />
<gl-search-box-by-type
v-model.trim="authorInput"
- class="m-2"
+ class="gl-m-3"
:placeholder="__('Search')"
@input="searchAuthors"
/>
- <gl-new-dropdown-item :is-checked="!currentAuthor" @click="selectAuthor(null)">
+ <gl-dropdown-item :is-checked="!currentAuthor" @click="selectAuthor(null)">
{{ __('Any Author') }}
- </gl-new-dropdown-item>
- <gl-new-dropdown-divider />
- <gl-new-dropdown-item
+ </gl-dropdown-item>
+ <gl-dropdown-divider />
+ <gl-dropdown-item
v-for="author in commitsAuthors"
:key="author.id"
:is-checked="author.name === currentAuthor"
@@ -136,7 +136,7 @@ export default {
@click="selectAuthor(author)"
>
{{ author.name }}
- </gl-new-dropdown-item>
- </gl-new-dropdown>
+ </gl-dropdown-item>
+ </gl-dropdown>
</div>
</template>
diff --git a/app/assets/javascripts/projects/components/project_delete_button.vue b/app/assets/javascripts/projects/components/project_delete_button.vue
index 4b27c5e3d30..2f3ff92d7ae 100644
--- a/app/assets/javascripts/projects/components/project_delete_button.vue
+++ b/app/assets/javascripts/projects/components/project_delete_button.vue
@@ -22,10 +22,10 @@ export default {
strings: {
alertTitle: __('You are about to permanently delete this project'),
alertBody: __(
- 'Once a project is permanently deleted it %{strongStart}cannot be recovered%{strongEnd}. Permanently deleting this project will %{strongStart}immediately delete%{strongEnd} its respositories and %{strongStart}all related resources%{strongEnd} including issues, merge requests etc.',
+ 'Once a project is permanently deleted it %{strongStart}cannot be recovered%{strongEnd}. Permanently deleting this project will %{strongStart}immediately delete%{strongEnd} its repositories and %{strongStart}all related resources%{strongEnd} including issues, merge requests etc.',
),
modalBody: __(
- "This action cannot be undone. You will lose the project's respository and all conent: issues, merge requests, etc.",
+ "This action cannot be undone. You will lose the project's repository and all content: issues, merge requests, etc.",
),
},
};
diff --git a/app/assets/javascripts/projects/components/shared/delete_button.vue b/app/assets/javascripts/projects/components/shared/delete_button.vue
index e3f4500d404..051bfcb732a 100644
--- a/app/assets/javascripts/projects/components/shared/delete_button.vue
+++ b/app/assets/javascripts/projects/components/shared/delete_button.vue
@@ -86,7 +86,7 @@ export default {
<slot name="modal-body"></slot>
<p class="gl-mb-1">{{ $options.strings.confirmText }}</p>
<p>
- <code>{{ confirmPhrase }}</code>
+ <code class="gl-white-space-pre-wrap">{{ confirmPhrase }}</code>
</p>
<gl-form-input
id="confirm_name_input"
diff --git a/app/assets/javascripts/projects/experiment_new_project_creation/components/app.vue b/app/assets/javascripts/projects/experiment_new_project_creation/components/app.vue
index ee4a00dbc75..f404e6030f4 100644
--- a/app/assets/javascripts/projects/experiment_new_project_creation/components/app.vue
+++ b/app/assets/javascripts/projects/experiment_new_project_creation/components/app.vue
@@ -1,4 +1,5 @@
<script>
+/* eslint-disable vue/no-v-html */
import { GlBreadcrumb, GlIcon } from '@gitlab/ui';
import WelcomePage from './welcome.vue';
import LegacyContainer from './legacy_container.vue';
diff --git a/app/assets/javascripts/projects/experiment_new_project_creation/components/welcome.vue b/app/assets/javascripts/projects/experiment_new_project_creation/components/welcome.vue
index cd9a72996cf..022328cd8a2 100644
--- a/app/assets/javascripts/projects/experiment_new_project_creation/components/welcome.vue
+++ b/app/assets/javascripts/projects/experiment_new_project_creation/components/welcome.vue
@@ -1,4 +1,5 @@
<script>
+/* eslint-disable vue/no-v-html */
import { GlPopover } from '@gitlab/ui';
import Tracking from '~/tracking';
import LegacyContainer from './legacy_container.vue';
diff --git a/app/assets/javascripts/projects/project_new.js b/app/assets/javascripts/projects/project_new.js
index ec0a83b5736..599aa52831b 100644
--- a/app/assets/javascripts/projects/project_new.js
+++ b/app/assets/javascripts/projects/project_new.js
@@ -1,13 +1,18 @@
import $ from 'jquery';
import DEFAULT_PROJECT_TEMPLATES from 'ee_else_ce/projects/default_project_templates';
import { addSelectOnFocusBehaviour } from '../lib/utils/common_utils';
-import { convertToTitleCase, humanize, slugify } from '../lib/utils/text_utility';
+import {
+ convertToTitleCase,
+ humanize,
+ slugify,
+ convertUnicodeToAscii,
+} from '../lib/utils/text_utility';
let hasUserDefinedProjectPath = false;
let hasUserDefinedProjectName = false;
const onProjectNameChange = ($projectNameInput, $projectPathInput) => {
- const slug = slugify($projectNameInput.val());
+ const slug = slugify(convertUnicodeToAscii($projectNameInput.val()));
$projectPathInput.val(slug);
};
diff --git a/app/assets/javascripts/projects/settings/access_dropdown.js b/app/assets/javascripts/projects/settings/access_dropdown.js
index 4dbf6675357..5d51b7ea57b 100644
--- a/app/assets/javascripts/projects/settings/access_dropdown.js
+++ b/app/assets/javascripts/projects/settings/access_dropdown.js
@@ -4,6 +4,7 @@ import axios from '~/lib/utils/axios_utils';
import { deprecatedCreateFlash as Flash } from '~/flash';
import { n__, s__, __ } from '~/locale';
import { LEVEL_TYPES, LEVEL_ID_PROP, ACCESS_LEVEL_NONE } from './constants';
+import initDeprecatedJQueryDropdown from '~/deprecated_jquery_dropdown';
export default class AccessDropdown {
constructor(options) {
@@ -29,7 +30,7 @@ export default class AccessDropdown {
initDropdown() {
const { onSelect, onHide } = this.options;
- this.$dropdown.glDropdown({
+ initDeprecatedJQueryDropdown(this.$dropdown, {
data: this.getData.bind(this),
selectable: true,
filterable: true,
diff --git a/app/assets/javascripts/projects/settings_service_desk/components/service_desk_root.vue b/app/assets/javascripts/projects/settings_service_desk/components/service_desk_root.vue
index d61569fcd6e..81367f7d6b4 100644
--- a/app/assets/javascripts/projects/settings_service_desk/components/service_desk_root.vue
+++ b/app/assets/javascripts/projects/settings_service_desk/components/service_desk_root.vue
@@ -118,7 +118,10 @@ export default {
this.isTemplateSaving = true;
this.service
.updateTemplate({ selectedTemplate, outgoingName, projectKey }, this.isEnabled)
- .then(() => this.showAlert(__('Template was successfully saved.'), 'success'))
+ .then(({ data }) => {
+ this.incomingEmail = data?.service_desk_address;
+ this.showAlert(__('Changes were successfully made.'), 'success');
+ })
.catch(() =>
this.showAlert(
__('An error occurred while saving the template. Please check if the template exists.'),
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 0b7433d6aaa..6a0810ad3a1 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
@@ -157,14 +157,16 @@ export default {
}}
</span>
</template>
- <gl-button
- variant="success"
- class="gl-mt-5"
- :disabled="isTemplateSaving"
- @click="onSaveTemplate"
- >
- {{ __('Save template') }}
- </gl-button>
+ <div class="gl-display-flex gl-justify-content-end">
+ <gl-button
+ variant="success"
+ class="gl-mt-5"
+ :disabled="isTemplateSaving"
+ @click="onSaveTemplate"
+ >
+ {{ __('Save changes') }}
+ </gl-button>
+ </div>
</div>
</div>
</div>
diff --git a/app/assets/javascripts/prometheus_alerts/components/reset_key.vue b/app/assets/javascripts/prometheus_alerts/components/reset_key.vue
index 05e769f5fc8..6f60141d7ab 100644
--- a/app/assets/javascripts/prometheus_alerts/components/reset_key.vue
+++ b/app/assets/javascripts/prometheus_alerts/components/reset_key.vue
@@ -1,4 +1,5 @@
<script>
+/* eslint-disable vue/no-v-html */
import { GlButton, GlFormGroup, GlFormInput, GlModal, GlModalDirective } from '@gitlab/ui';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import axios from '~/lib/utils/axios_utils';
diff --git a/app/assets/javascripts/protected_tags/protected_tag_access_dropdown.js b/app/assets/javascripts/protected_tags/protected_tag_access_dropdown.js
index def2f091947..202286a5fb4 100644
--- a/app/assets/javascripts/protected_tags/protected_tag_access_dropdown.js
+++ b/app/assets/javascripts/protected_tags/protected_tag_access_dropdown.js
@@ -1,4 +1,5 @@
import { __ } from '~/locale';
+import initDeprecatedJQueryDropdown from '~/deprecated_jquery_dropdown';
export default class ProtectedTagAccessDropdown {
constructor(options) {
@@ -8,7 +9,7 @@ export default class ProtectedTagAccessDropdown {
initDropdown() {
const { onSelect } = this.options;
- this.options.$dropdown.glDropdown({
+ initDeprecatedJQueryDropdown(this.options.$dropdown, {
data: this.options.data,
selectable: true,
inputId: this.options.$dropdown.data('inputId'),
diff --git a/app/assets/javascripts/protected_tags/protected_tag_create.js b/app/assets/javascripts/protected_tags/protected_tag_create.js
index 03a5fe6b353..eb44f0c67fd 100644
--- a/app/assets/javascripts/protected_tags/protected_tag_create.js
+++ b/app/assets/javascripts/protected_tags/protected_tag_create.js
@@ -23,7 +23,7 @@ export default class ProtectedTagCreate {
});
// Select default
- $allowedToCreateDropdown.data('glDropdown').selectRowAtIndex(0);
+ $allowedToCreateDropdown.data('deprecatedJQueryDropdown').selectRowAtIndex(0);
// Protected tag dropdown
this.createItemDropdown = new CreateItemDropdown({
diff --git a/app/assets/javascripts/ref/components/ref_results_section.vue b/app/assets/javascripts/ref/components/ref_results_section.vue
index c8f5c66b0c1..dc74f86fd70 100644
--- a/app/assets/javascripts/ref/components/ref_results_section.vue
+++ b/app/assets/javascripts/ref/components/ref_results_section.vue
@@ -1,12 +1,12 @@
<script>
-import { GlNewDropdownHeader, GlNewDropdownItem, GlBadge, GlIcon } from '@gitlab/ui';
+import { GlDropdownSectionHeader, GlDropdownItem, GlBadge, GlIcon } from '@gitlab/ui';
import { s__ } from '~/locale';
export default {
name: 'RefResultsSection',
components: {
- GlNewDropdownHeader,
- GlNewDropdownItem,
+ GlDropdownSectionHeader,
+ GlDropdownItem,
GlBadge,
GlIcon,
},
@@ -84,12 +84,12 @@ export default {
<template>
<div>
- <gl-new-dropdown-header>
+ <gl-dropdown-section-header>
<div class="gl-display-flex align-items-center" data-testid="section-header">
<span class="gl-mr-2 gl-mb-1">{{ sectionTitle }}</span>
<gl-badge variant="neutral">{{ totalCountText }}</gl-badge>
</div>
- </gl-new-dropdown-header>
+ </gl-dropdown-section-header>
<template v-if="error">
<div class="gl-display-flex align-items-start text-danger gl-ml-4 gl-mr-4 gl-mb-3">
<gl-icon name="error" class="gl-mr-2 gl-mt-2 gl-flex-shrink-0" />
@@ -97,7 +97,7 @@ export default {
</div>
</template>
<template v-else>
- <gl-new-dropdown-item
+ <gl-dropdown-item
v-for="item in items"
:key="item.name"
@click="$emit('selected', item.value || item.name)"
@@ -118,7 +118,7 @@ export default {
s__('DefaultBranchLabel|default')
}}</gl-badge>
</div>
- </gl-new-dropdown-item>
+ </gl-dropdown-item>
</template>
</div>
</template>
diff --git a/app/assets/javascripts/ref/components/ref_selector.vue b/app/assets/javascripts/ref/components/ref_selector.vue
index e388604ed92..85b123530b5 100644
--- a/app/assets/javascripts/ref/components/ref_selector.vue
+++ b/app/assets/javascripts/ref/components/ref_selector.vue
@@ -1,9 +1,9 @@
<script>
import { mapActions, mapGetters, mapState } from 'vuex';
import {
- GlNewDropdown,
- GlNewDropdownDivider,
- GlNewDropdownHeader,
+ GlDropdown,
+ GlDropdownDivider,
+ GlDropdownSectionHeader,
GlSearchBoxByType,
GlSprintf,
GlIcon,
@@ -18,9 +18,9 @@ export default {
name: 'RefSelector',
store: createStore(),
components: {
- GlNewDropdown,
- GlNewDropdownDivider,
- GlNewDropdownHeader,
+ GlDropdown,
+ GlDropdownDivider,
+ GlDropdownSectionHeader,
GlSearchBoxByType,
GlSprintf,
GlIcon,
@@ -87,6 +87,15 @@ export default {
},
},
created() {
+ // This method is defined here instead of in `methods`
+ // because we need to access the .cancel() method
+ // lodash attaches to the function, which is
+ // made inaccessible by Vue. More info:
+ // https://stackoverflow.com/a/52988020/1063392
+ this.debouncedSearch = debounce(function search() {
+ this.search(this.query);
+ }, SEARCH_DEBOUNCE_MS);
+
this.setProjectId(this.projectId);
this.search(this.query);
},
@@ -95,9 +104,13 @@ export default {
focusSearchBox() {
this.$refs.searchBox.$el.querySelector('input').focus();
},
- onSearchBoxInput: debounce(function search() {
+ onSearchBoxEnter() {
+ this.debouncedSearch.cancel();
this.search(this.query);
- }, SEARCH_DEBOUNCE_MS),
+ },
+ onSearchBoxInput() {
+ this.debouncedSearch();
+ },
selectRef(ref) {
this.setSelectedRef(ref);
this.$emit('input', this.selectedRef);
@@ -107,7 +120,7 @@ export default {
</script>
<template>
- <gl-new-dropdown v-bind="$attrs" class="ref-selector" @shown="focusSearchBox">
+ <gl-dropdown v-bind="$attrs" class="ref-selector" @shown="focusSearchBox">
<template slot="button-content">
<span class="gl-flex-grow-1 gl-ml-2 gl-text-gray-400" data-testid="button-content">
<span v-if="selectedRef" class="gl-font-monospace">{{ selectedRef }}</span>
@@ -117,11 +130,11 @@ export default {
</template>
<div class="gl-display-flex gl-flex-direction-column ref-selector-dropdown-content">
- <gl-new-dropdown-header>
+ <gl-dropdown-section-header>
<span class="gl-text-center gl-display-block">{{ i18n.dropdownHeader }}</span>
- </gl-new-dropdown-header>
+ </gl-dropdown-section-header>
- <gl-new-dropdown-divider />
+ <gl-dropdown-divider />
<gl-search-box-by-type
ref="searchBox"
@@ -129,6 +142,7 @@ export default {
class="gl-m-3"
:placeholder="i18n.searchPlaceholder"
@input="onSearchBoxInput"
+ @keydown.enter.prevent="onSearchBoxEnter"
/>
<div class="gl-flex-grow-1 gl-overflow-y-auto">
@@ -161,7 +175,7 @@ export default {
@selected="selectRef($event)"
/>
- <gl-new-dropdown-divider v-if="showTagsSection || showCommitsSection" />
+ <gl-dropdown-divider v-if="showTagsSection || showCommitsSection" />
</template>
<template v-if="showTagsSection">
@@ -176,7 +190,7 @@ export default {
@selected="selectRef($event)"
/>
- <gl-new-dropdown-divider v-if="showCommitsSection" />
+ <gl-dropdown-divider v-if="showCommitsSection" />
</template>
<template v-if="showCommitsSection">
@@ -194,5 +208,5 @@ export default {
</template>
</div>
</div>
- </gl-new-dropdown>
+ </gl-dropdown>
</template>
diff --git a/app/assets/javascripts/ref_select_dropdown.js b/app/assets/javascripts/ref_select_dropdown.js
index 2e0113271df..af8729c1d08 100644
--- a/app/assets/javascripts/ref_select_dropdown.js
+++ b/app/assets/javascripts/ref_select_dropdown.js
@@ -1,11 +1,11 @@
import $ from 'jquery';
-import '~/gl_dropdown';
+import initDeprecatedJQueryDropdown from '~/deprecated_jquery_dropdown';
class RefSelectDropdown {
constructor($dropdownButton, availableRefs) {
const availableRefsValue =
availableRefs || JSON.parse(document.getElementById('availableRefs').innerHTML);
- $dropdownButton.glDropdown({
+ initDeprecatedJQueryDropdown($dropdownButton, {
data: availableRefsValue,
filterable: true,
filterByText: true,
diff --git a/app/assets/javascripts/registry/explorer/components/delete_button.vue b/app/assets/javascripts/registry/explorer/components/delete_button.vue
index dab6a26ea16..ee856a3e546 100644
--- a/app/assets/javascripts/registry/explorer/components/delete_button.vue
+++ b/app/assets/javascripts/registry/explorer/components/delete_button.vue
@@ -47,7 +47,6 @@ export default {
:disabled="disabled"
:title="title"
:aria-label="title"
- category="secondary"
variant="danger"
icon="remove"
@click="$emit('delete')"
diff --git a/app/assets/javascripts/registry/explorer/components/details_page/details_header.vue b/app/assets/javascripts/registry/explorer/components/details_page/details_header.vue
index c254dd05aa4..ff613daf7fa 100644
--- a/app/assets/javascripts/registry/explorer/components/details_page/details_header.vue
+++ b/app/assets/javascripts/registry/explorer/components/details_page/details_header.vue
@@ -1,9 +1,10 @@
<script>
import { GlSprintf } from '@gitlab/ui';
+import TitleArea from '~/vue_shared/components/registry/title_area.vue';
import { DETAILS_PAGE_TITLE } from '../../constants/index';
export default {
- components: { GlSprintf },
+ components: { GlSprintf, TitleArea },
props: {
imageName: {
type: String,
@@ -18,13 +19,13 @@ export default {
</script>
<template>
- <div class="gl-display-flex gl-my-2 gl-align-items-center">
- <h4>
+ <title-area>
+ <template #title>
<gl-sprintf :message="$options.i18n.DETAILS_PAGE_TITLE">
<template #imageName>
{{ imageName }}
</template>
</gl-sprintf>
- </h4>
- </div>
+ </template>
+ </title-area>
</template>
diff --git a/app/assets/javascripts/registry/explorer/components/details_page/tags_list.vue b/app/assets/javascripts/registry/explorer/components/details_page/tags_list.vue
index 8494967ab57..328026d0953 100644
--- a/app/assets/javascripts/registry/explorer/components/details_page/tags_list.vue
+++ b/app/assets/javascripts/registry/explorer/components/details_page/tags_list.vue
@@ -67,7 +67,6 @@ export default {
:key="tag.path"
:tag="tag"
:first="index === 0"
- :last="index === tags.length - 1"
:selected="selectedItems[tag.name]"
:is-desktop="isDesktop"
@select="updateSelectedItems(tag.name)"
diff --git a/app/assets/javascripts/registry/explorer/components/details_page/tags_list_row.vue b/app/assets/javascripts/registry/explorer/components/details_page/tags_list_row.vue
index 8ec5cebbe8e..661213733ac 100644
--- a/app/assets/javascripts/registry/explorer/components/details_page/tags_list_row.vue
+++ b/app/assets/javascripts/registry/explorer/components/details_page/tags_list_row.vue
@@ -5,9 +5,9 @@ import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import { numberToHumanSize } from '~/lib/utils/number_utils';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
import { formatDate } from '~/lib/utils/datetime_utility';
+import ListItem from '~/vue_shared/components/registry/list_item.vue';
import DeleteButton from '../delete_button.vue';
-import ListItem from '../list_item.vue';
-import DetailsRow from '~/registry/shared/components/details_row.vue';
+import DetailsRow from '~/vue_shared/components/registry/details_row.vue';
import {
REMOVE_TAG_BUTTON_TITLE,
DIGEST_LABEL,
diff --git a/app/assets/javascripts/registry/explorer/components/list_page/cli_commands.vue b/app/assets/javascripts/registry/explorer/components/list_page/cli_commands.vue
index 36a46ed58f4..85d87dab042 100644
--- a/app/assets/javascripts/registry/explorer/components/list_page/cli_commands.vue
+++ b/app/assets/javascripts/registry/explorer/components/list_page/cli_commands.vue
@@ -1,8 +1,8 @@
<script>
-import { GlDeprecatedDropdown, GlFormGroup, GlFormInputGroup } from '@gitlab/ui';
+import { GlDeprecatedDropdown } from '@gitlab/ui';
import { mapGetters } from 'vuex';
import Tracking from '~/tracking';
-import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
+import CodeInstruction from '~/vue_shared/components/registry/code_instruction.vue';
import {
QUICK_START,
LOGIN_COMMAND_LABEL,
@@ -13,22 +13,23 @@ import {
COPY_PUSH_TITLE,
} from '../../constants/index';
+const trackingLabel = 'quickstart_dropdown';
+
export default {
components: {
GlDeprecatedDropdown,
- GlFormGroup,
- GlFormInputGroup,
- ClipboardButton,
+ CodeInstruction,
},
- mixins: [Tracking.mixin({ label: 'quickstart_dropdown' })],
+ mixins: [Tracking.mixin({ label: trackingLabel })],
+ trackingLabel,
i18n: {
- dropdownTitle: QUICK_START,
- loginCommandLabel: LOGIN_COMMAND_LABEL,
- copyLoginTitle: COPY_LOGIN_TITLE,
- buildCommandLabel: BUILD_COMMAND_LABEL,
- copyBuildTitle: COPY_BUILD_TITLE,
- pushCommandLabel: PUSH_COMMAND_LABEL,
- copyPushTitle: COPY_PUSH_TITLE,
+ QUICK_START,
+ LOGIN_COMMAND_LABEL,
+ COPY_LOGIN_TITLE,
+ BUILD_COMMAND_LABEL,
+ COPY_BUILD_TITLE,
+ PUSH_COMMAND_LABEL,
+ COPY_PUSH_TITLE,
},
computed: {
...mapGetters(['dockerBuildCommand', 'dockerPushCommand', 'dockerLoginCommand']),
@@ -37,7 +38,7 @@ export default {
</script>
<template>
<gl-deprecated-dropdown
- :text="$options.i18n.dropdownTitle"
+ :text="$options.i18n.QUICK_START"
variant="primary"
size="sm"
right
@@ -45,59 +46,30 @@ export default {
>
<!-- This li is used as a container since gl-dropdown produces a root ul, this mimics the functionality exposed by b-dropdown-form -->
<li role="presentation" class="px-2 py-1 dropdown-menu-large">
- <form>
- <gl-form-group
- label-size="sm"
- label-for="docker-login-btn"
- :label="$options.i18n.loginCommandLabel"
- >
- <gl-form-input-group id="docker-login-btn" :value="dockerLoginCommand" readonly>
- <template #append>
- <clipboard-button
- class="border"
- :text="dockerLoginCommand"
- :title="$options.i18n.copyLoginTitle"
- @click.native="track('click_copy_login')"
- />
- </template>
- </gl-form-input-group>
- </gl-form-group>
+ <code-instruction
+ :label="$options.i18n.LOGIN_COMMAND_LABEL"
+ :instruction="dockerLoginCommand"
+ :copy-text="$options.i18n.COPY_LOGIN_TITLE"
+ tracking-action="click_copy_login"
+ :tracking-label="$options.trackingLabel"
+ />
- <gl-form-group
- label-size="sm"
- label-for="docker-build-btn"
- :label="$options.i18n.buildCommandLabel"
- >
- <gl-form-input-group id="docker-build-btn" :value="dockerBuildCommand" readonly>
- <template #append>
- <clipboard-button
- class="border"
- :text="dockerBuildCommand"
- :title="$options.i18n.copyBuildTitle"
- @click.native="track('click_copy_build')"
- />
- </template>
- </gl-form-input-group>
- </gl-form-group>
+ <code-instruction
+ :label="$options.i18n.BUILD_COMMAND_LABEL"
+ :instruction="dockerBuildCommand"
+ :copy-text="$options.i18n.COPY_BUILD_TITLE"
+ tracking-action="click_copy_build"
+ :tracking-label="$options.trackingLabel"
+ />
- <gl-form-group
- class="mb-0"
- label-size="sm"
- label-for="docker-push-btn"
- :label="$options.i18n.pushCommandLabel"
- >
- <gl-form-input-group id="docker-push-btn" :value="dockerPushCommand" readonly>
- <template #append>
- <clipboard-button
- class="border"
- :text="dockerPushCommand"
- :title="$options.i18n.copyPushTitle"
- @click.native="track('click_copy_push')"
- />
- </template>
- </gl-form-input-group>
- </gl-form-group>
- </form>
+ <code-instruction
+ class="mb-0"
+ :label="$options.i18n.PUSH_COMMAND_LABEL"
+ :instruction="dockerPushCommand"
+ :copy-text="$options.i18n.COPY_PUSH_TITLE"
+ tracking-action="click_copy_push"
+ :tracking-label="$options.trackingLabel"
+ />
</li>
</gl-deprecated-dropdown>
</template>
diff --git a/app/assets/javascripts/registry/explorer/components/list_page/image_list.vue b/app/assets/javascripts/registry/explorer/components/list_page/image_list.vue
index 65cf51fd1d1..d1b9894da0e 100644
--- a/app/assets/javascripts/registry/explorer/components/list_page/image_list.vue
+++ b/app/assets/javascripts/registry/explorer/components/list_page/image_list.vue
@@ -38,7 +38,6 @@ export default {
:key="index"
:item="listItem"
:first="index === 0"
- :last="index === images.length - 1"
@delete="$emit('delete', $event)"
/>
diff --git a/app/assets/javascripts/registry/explorer/components/list_page/image_list_row.vue b/app/assets/javascripts/registry/explorer/components/list_page/image_list_row.vue
index 102311c6062..32bf27f1143 100644
--- a/app/assets/javascripts/registry/explorer/components/list_page/image_list_row.vue
+++ b/app/assets/javascripts/registry/explorer/components/list_page/image_list_row.vue
@@ -2,7 +2,7 @@
import { GlTooltipDirective, GlIcon, GlSprintf } from '@gitlab/ui';
import { n__ } from '~/locale';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
-import ListItem from '../list_item.vue';
+import ListItem from '~/vue_shared/components/registry/list_item.vue';
import DeleteButton from '../delete_button.vue';
import {
diff --git a/app/assets/javascripts/registry/explorer/components/list_page/registry_header.vue b/app/assets/javascripts/registry/explorer/components/list_page/registry_header.vue
index c7b4fd5f4b4..7be68e77def 100644
--- a/app/assets/javascripts/registry/explorer/components/list_page/registry_header.vue
+++ b/app/assets/javascripts/registry/explorer/components/list_page/registry_header.vue
@@ -1,6 +1,8 @@
<script>
-import { GlSprintf, GlLink, GlIcon } from '@gitlab/ui';
-import { n__ } from '~/locale';
+import { GlSprintf, GlLink } from '@gitlab/ui';
+import TitleArea from '~/vue_shared/components/registry/title_area.vue';
+import MetadataItem from '~/vue_shared/components/registry/metadata_item.vue';
+import { n__, sprintf } from '~/locale';
import { approximateDuration, calculateRemainingMilliseconds } from '~/lib/utils/datetime_utility';
import {
@@ -13,9 +15,10 @@ import {
export default {
components: {
- GlIcon,
GlSprintf,
GlLink,
+ TitleArea,
+ MetadataItem,
},
props: {
expirationPolicy: {
@@ -56,11 +59,12 @@ export default {
},
computed: {
imagesCountText() {
- return n__(
+ const pluralisedString = n__(
'ContainerRegistry|%{count} Image repository',
'ContainerRegistry|%{count} Image repositories',
this.imagesCount,
);
+ return sprintf(pluralisedString, { count: this.imagesCount });
},
timeTillRun() {
const difference = calculateRemainingMilliseconds(this.expirationPolicy?.next_run_at);
@@ -71,7 +75,7 @@ export default {
},
expirationPolicyText() {
return this.expirationPolicyEnabled
- ? EXPIRATION_POLICY_WILL_RUN_IN
+ ? sprintf(EXPIRATION_POLICY_WILL_RUN_IN, { time: this.timeTillRun })
: EXPIRATION_POLICY_DISABLED_TEXT;
},
showExpirationPolicyTip() {
@@ -85,37 +89,29 @@ export default {
<template>
<div>
- <div
- class="gl-display-flex gl-justify-content-space-between gl-align-items-center"
- data-testid="header"
- >
- <h4 data-testid="title">{{ $options.i18n.CONTAINER_REGISTRY_TITLE }}</h4>
- <div class="gl-display-none d-sm-block" data-testid="commands-slot">
+ <title-area :title="$options.i18n.CONTAINER_REGISTRY_TITLE">
+ <template #right-actions>
<slot name="commands"></slot>
- </div>
- </div>
- <div
- v-if="imagesCount"
- class="gl-display-flex gl-align-items-center gl-mt-1 gl-mb-3 gl-text-gray-500"
- data-testid="subheader"
- >
- <span class="gl-mr-3" data-testid="images-count">
- <gl-icon class="gl-mr-1" name="container-image" />
- <gl-sprintf :message="imagesCountText">
- <template #count>
- {{ imagesCount }}
- </template>
- </gl-sprintf>
- </span>
- <span v-if="!hideExpirationPolicyData" data-testid="expiration-policy">
- <gl-icon class="gl-mr-1" name="expire" />
- <gl-sprintf :message="expirationPolicyText">
- <template #time>
- {{ timeTillRun }}
- </template>
- </gl-sprintf>
- </span>
- </div>
+ </template>
+ <template #metadata_count>
+ <metadata-item
+ v-if="imagesCount"
+ data-testid="images-count"
+ icon="container-image"
+ :text="imagesCountText"
+ />
+ </template>
+ <template #metadata_exp_policies>
+ <metadata-item
+ v-if="!hideExpirationPolicyData"
+ data-testid="expiration-policy"
+ icon="expire"
+ :text="expirationPolicyText"
+ size="xl"
+ />
+ </template>
+ </title-area>
+
<div data-testid="info-area">
<p>
<span data-testid="default-intro">
diff --git a/app/assets/javascripts/registry/explorer/components/registry_breadcrumb.vue b/app/assets/javascripts/registry/explorer/components/registry_breadcrumb.vue
index d935ca091a1..146d1434b18 100644
--- a/app/assets/javascripts/registry/explorer/components/registry_breadcrumb.vue
+++ b/app/assets/javascripts/registry/explorer/components/registry_breadcrumb.vue
@@ -1,7 +1,9 @@
<script>
import { initial, first, last } from 'lodash';
+import { GlSafeHtmlDirective as SafeHtml } from '@gitlab/ui';
export default {
+ directives: { SafeHtml },
props: {
crumbs: {
type: Array,
@@ -41,14 +43,14 @@ export default {
<li
v-for="(crumb, index) in rootCrumbs"
:key="index"
+ v-safe-html="crumb.innerHTML"
:class="crumb.className"
- v-html="crumb.innerHTML"
></li>
<li v-if="!isRootRoute">
<router-link ref="rootRouteLink" :to="rootRoute.path">
{{ rootRoute.meta.nameGenerator(rootRoute) }}
</router-link>
- <component :is="divider.tagName" :class="divider.classList" v-html="divider.innerHTML" />
+ <component :is="divider.tagName" v-safe-html="divider.innerHTML" :class="divider.classList" />
</li>
<li>
<component :is="lastCrumb.tagName" ref="lastCrumb" :class="lastCrumb.className">
diff --git a/app/assets/javascripts/registry/explorer/stores/index.js b/app/assets/javascripts/registry/explorer/stores/index.js
index 54a8e0e1c1c..18e3351ed13 100644
--- a/app/assets/javascripts/registry/explorer/stores/index.js
+++ b/app/assets/javascripts/registry/explorer/stores/index.js
@@ -7,7 +7,6 @@ import state from './state';
Vue.use(Vuex);
-// eslint-disable-next-line import/prefer-default-export
export const createStore = () =>
new Vuex.Store({
state,
diff --git a/app/assets/javascripts/registry/explorer/utils.js b/app/assets/javascripts/registry/explorer/utils.js
index b1df87c6993..44262a6cbb6 100644
--- a/app/assets/javascripts/registry/explorer/utils.js
+++ b/app/assets/javascripts/registry/explorer/utils.js
@@ -1,2 +1 @@
-// eslint-disable-next-line import/prefer-default-export
export const decodeAndParse = param => JSON.parse(window.atob(param));
diff --git a/app/assets/javascripts/registry/settings/components/settings_form.vue b/app/assets/javascripts/registry/settings/components/settings_form.vue
index f129922c1d2..7a26fb5cbee 100644
--- a/app/assets/javascripts/registry/settings/components/settings_form.vue
+++ b/app/assets/javascripts/registry/settings/components/settings_form.vue
@@ -1,7 +1,7 @@
<script>
import { get } from 'lodash';
import { mapActions, mapState, mapGetters } from 'vuex';
-import { GlCard, GlDeprecatedButton, GlLoadingIcon } from '@gitlab/ui';
+import { GlCard, GlButton, GlLoadingIcon } from '@gitlab/ui';
import Tracking from '~/tracking';
import { mapComputed } from '~/vuex_shared/bindings';
import {
@@ -14,7 +14,7 @@ import { SET_CLEANUP_POLICY_BUTTON, CLEANUP_POLICY_CARD_HEADER } from '../consta
export default {
components: {
GlCard,
- GlDeprecatedButton,
+ GlButton,
GlLoadingIcon,
ExpirationPolicyFields,
},
@@ -104,24 +104,25 @@ export default {
</template>
<template #footer>
<div class="gl-display-flex gl-justify-content-end">
- <gl-deprecated-button
+ <gl-button
ref="cancel-button"
type="reset"
class="gl-mr-3 gl-display-block"
:disabled="isCancelButtonDisabled"
>
{{ __('Cancel') }}
- </gl-deprecated-button>
- <gl-deprecated-button
+ </gl-button>
+ <gl-button
ref="save-button"
type="submit"
:disabled="isSubmitButtonDisabled"
variant="success"
+ category="primary"
class="gl-display-flex gl-justify-content-center gl-align-items-center js-no-auto-disable"
>
{{ $options.i18n.SET_CLEANUP_POLICY_BUTTON }}
<gl-loading-icon v-if="isLoading" class="gl-ml-3" />
- </gl-deprecated-button>
+ </gl-button>
</div>
</template>
</gl-card>
diff --git a/app/assets/javascripts/related_issues/components/add_issuable_form.vue b/app/assets/javascripts/related_issues/components/add_issuable_form.vue
new file mode 100644
index 00000000000..63d61989cba
--- /dev/null
+++ b/app/assets/javascripts/related_issues/components/add_issuable_form.vue
@@ -0,0 +1,207 @@
+<script>
+import { GlFormGroup, GlFormRadioGroup, GlButton } from '@gitlab/ui';
+import { __ } from '~/locale';
+import RelatedIssuableInput from './related_issuable_input.vue';
+import { mergeUrlParams } from '~/lib/utils/url_utility';
+
+import {
+ issuableTypesMap,
+ itemAddFailureTypesMap,
+ linkedIssueTypesMap,
+ addRelatedIssueErrorMap,
+ addRelatedItemErrorMap,
+} from '../constants';
+
+export default {
+ name: 'AddIssuableForm',
+ components: {
+ GlFormGroup,
+ GlFormRadioGroup,
+ RelatedIssuableInput,
+ GlButton,
+ },
+ props: {
+ inputValue: {
+ type: String,
+ required: true,
+ },
+ pendingReferences: {
+ type: Array,
+ required: false,
+ default: () => [],
+ },
+ autoCompleteSources: {
+ type: Object,
+ required: false,
+ default: () => ({}),
+ },
+ showCategorizedIssues: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ isSubmitting: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ pathIdSeparator: {
+ type: String,
+ required: true,
+ },
+ issuableType: {
+ type: String,
+ required: false,
+ default: issuableTypesMap.ISSUE,
+ },
+ hasError: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ itemAddFailureType: {
+ type: String,
+ required: false,
+ default: itemAddFailureTypesMap.NOT_FOUND,
+ },
+ itemAddFailureMessage: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ confidential: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ },
+ data() {
+ return {
+ linkedIssueType: linkedIssueTypesMap.RELATES_TO,
+ linkedIssueTypes: [
+ {
+ text: __('relates to'),
+ value: linkedIssueTypesMap.RELATES_TO,
+ },
+ {
+ text: __('blocks'),
+ value: linkedIssueTypesMap.BLOCKS,
+ },
+ {
+ text: __('is blocked by'),
+ value: linkedIssueTypesMap.IS_BLOCKED_BY,
+ },
+ ],
+ };
+ },
+ computed: {
+ isSubmitButtonDisabled() {
+ return (
+ (this.inputValue.length === 0 && this.pendingReferences.length === 0) || this.isSubmitting
+ );
+ },
+ addRelatedErrorMessage() {
+ if (this.itemAddFailureMessage) {
+ return this.itemAddFailureMessage;
+ } else if (this.itemAddFailureType === itemAddFailureTypesMap.NOT_FOUND) {
+ return addRelatedIssueErrorMap[this.issuableType];
+ }
+ // Only other failure is MAX_NUMBER_OF_CHILD_EPICS at the moment
+ return addRelatedItemErrorMap[this.itemAddFailureType];
+ },
+ transformedAutocompleteSources() {
+ if (!this.confidential) {
+ return this.autoCompleteSources;
+ }
+
+ if (!this.autoCompleteSources?.issues || !this.autoCompleteSources?.epics) {
+ return this.autoCompleteSources;
+ }
+
+ return {
+ ...this.autoCompleteSources,
+ issues: mergeUrlParams({ confidential_only: true }, this.autoCompleteSources.issues),
+ epics: mergeUrlParams({ confidential_only: true }, this.autoCompleteSources.epics),
+ };
+ },
+ },
+ methods: {
+ onPendingIssuableRemoveRequest(params) {
+ this.$emit('pendingIssuableRemoveRequest', params);
+ },
+ onFormSubmit() {
+ this.$emit('addIssuableFormSubmit', {
+ pendingReferences: this.$refs.relatedIssuableInput.$refs.input.value,
+ linkedIssueType: this.linkedIssueType,
+ });
+ },
+ onFormCancel() {
+ this.$emit('addIssuableFormCancel');
+ },
+ onAddIssuableFormInput(params) {
+ this.$emit('addIssuableFormInput', params);
+ },
+ onAddIssuableFormBlur(params) {
+ this.$emit('addIssuableFormBlur', params);
+ },
+ },
+};
+</script>
+
+<template>
+ <form @submit.prevent="onFormSubmit">
+ <template v-if="showCategorizedIssues">
+ <gl-form-group
+ :label="__('The current issue')"
+ label-for="linked-issue-type-radio"
+ label-class="label-bold"
+ class="mb-2"
+ >
+ <gl-form-radio-group
+ id="linked-issue-type-radio"
+ v-model="linkedIssueType"
+ :options="linkedIssueTypes"
+ :checked="linkedIssueType"
+ />
+ </gl-form-group>
+ <p class="bold">
+ {{ __('the following issue(s)') }}
+ </p>
+ </template>
+ <related-issuable-input
+ ref="relatedIssuableInput"
+ input-id="add-related-issues-form-input"
+ :confidential="confidential"
+ :focus-on-mount="true"
+ :references="pendingReferences"
+ :path-id-separator="pathIdSeparator"
+ :input-value="inputValue"
+ :auto-complete-sources="transformedAutocompleteSources"
+ :auto-complete-options="{ issues: true, epics: true }"
+ :issuable-type="issuableType"
+ @pendingIssuableRemoveRequest="onPendingIssuableRemoveRequest"
+ @formCancel="onFormCancel"
+ @addIssuableFormBlur="onAddIssuableFormBlur"
+ @addIssuableFormInput="onAddIssuableFormInput"
+ />
+ <p v-if="hasError" class="gl-field-error">
+ {{ addRelatedErrorMessage }}
+ </p>
+ <div class="add-issuable-form-actions clearfix">
+ <gl-button
+ ref="addButton"
+ category="primary"
+ variant="success"
+ :disabled="isSubmitButtonDisabled"
+ :loading="isSubmitting"
+ type="submit"
+ class="js-add-issuable-form-add-button float-left qa-add-issue-button"
+ >
+ {{ __('Add') }}
+ </gl-button>
+ <gl-button class="float-right" @click="onFormCancel">
+ {{ __('Cancel') }}
+ </gl-button>
+ </div>
+ </form>
+</template>
diff --git a/app/assets/javascripts/related_issues/components/issue_token.vue b/app/assets/javascripts/related_issues/components/issue_token.vue
new file mode 100644
index 00000000000..31d0c7dbbb0
--- /dev/null
+++ b/app/assets/javascripts/related_issues/components/issue_token.vue
@@ -0,0 +1,115 @@
+<script>
+import { GlIcon } from '@gitlab/ui';
+import { __, sprintf } from '~/locale';
+import relatedIssuableMixin from '~/vue_shared/mixins/related_issuable_mixin';
+
+export default {
+ name: 'IssueToken',
+ components: {
+ GlIcon,
+ },
+ mixins: [relatedIssuableMixin],
+ props: {
+ isCondensed: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ },
+ computed: {
+ removeButtonLabel() {
+ const { displayReference } = this;
+ /*
+ * Giving false as third argument to prevent unescaping of ampersand in
+ * epic reference. Eg. &42 will remain &42 instead of &amp;42
+ *
+ * https://docs.gitlab.com/ee/development/i18n/externalization.html#interpolation
+ */
+ return sprintf(__('Remove %{displayReference}'), { displayReference }, false);
+ },
+ stateTitle() {
+ if (this.isCondensed) return '';
+
+ return this.isOpen ? __('Open') : __('Closed');
+ },
+ innerComponentType() {
+ return this.isCondensed ? 'span' : 'div';
+ },
+ issueTitle() {
+ return this.isCondensed ? this.title : '';
+ },
+ },
+};
+</script>
+
+<template>
+ <div
+ :class="{
+ 'issue-token': isCondensed,
+ 'flex-row issuable-info-container': !isCondensed,
+ }"
+ >
+ <component
+ :is="computedLinkElementType"
+ ref="link"
+ v-tooltip
+ :class="{
+ 'issue-token-link': isCondensed,
+ 'issuable-main-info': !isCondensed,
+ }"
+ :href="computedPath"
+ :title="issueTitle"
+ data-placement="top"
+ >
+ <component
+ :is="innerComponentType"
+ v-if="hasTitle"
+ ref="title"
+ :class="{
+ 'issue-token-title issue-token-end': isCondensed,
+ 'issue-title block-truncated': !isCondensed,
+ 'issue-token-title-standalone': !canRemove,
+ }"
+ class="js-issue-token-title"
+ >
+ <span class="issue-token-title-text">{{ title }}</span>
+ </component>
+ <component
+ :is="innerComponentType"
+ ref="reference"
+ :class="{
+ 'issue-token-reference': isCondensed,
+ 'issuable-info': !isCondensed,
+ }"
+ >
+ <gl-icon
+ v-if="hasState"
+ v-tooltip
+ :class="iconClass"
+ :name="iconName"
+ :size="12"
+ :title="stateTitle"
+ :aria-label="state"
+ />
+ {{ displayReference }}
+ </component>
+ </component>
+ <button
+ v-if="canRemove"
+ ref="removeButton"
+ v-tooltip
+ :class="{
+ 'issue-token-remove-button': isCondensed,
+ 'btn btn-default': !isCondensed,
+ }"
+ :title="removeButtonLabel"
+ :aria-label="removeButtonLabel"
+ :disabled="removeDisabled"
+ type="button"
+ class="js-issue-token-remove-button"
+ @click="onRemoveRequest"
+ >
+ <gl-icon name="close" aria-hidden="true" />
+ </button>
+ </div>
+</template>
diff --git a/app/assets/javascripts/related_issues/components/related_issuable_input.vue b/app/assets/javascripts/related_issues/components/related_issuable_input.vue
new file mode 100644
index 00000000000..1931cfb2c00
--- /dev/null
+++ b/app/assets/javascripts/related_issues/components/related_issuable_input.vue
@@ -0,0 +1,231 @@
+<script>
+import $ from 'jquery';
+import GfmAutoComplete from 'ee_else_ce/gfm_auto_complete';
+import issueToken from './issue_token.vue';
+import {
+ autoCompleteTextMap,
+ inputPlaceholderConfidentialTextMap,
+ inputPlaceholderTextMap,
+ issuableTypesMap,
+} from '../constants';
+
+const SPACE_FACTOR = 1;
+
+export default {
+ name: 'RelatedIssuableInput',
+ components: {
+ issueToken,
+ },
+ props: {
+ inputId: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ references: {
+ type: Array,
+ required: false,
+ default: () => [],
+ },
+ pathIdSeparator: {
+ type: String,
+ required: true,
+ },
+ inputValue: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ focusOnMount: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ autoCompleteSources: {
+ type: Object,
+ required: false,
+ default: () => ({}),
+ },
+ autoCompleteOptions: {
+ type: Object,
+ required: false,
+ default: () => ({}),
+ },
+ issuableType: {
+ type: String,
+ required: false,
+ default: issuableTypesMap.ISSUE,
+ },
+ confidential: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ },
+ data() {
+ return {
+ isInputFocused: false,
+ isAutoCompleteOpen: false,
+ areEventsAssigned: false,
+ };
+ },
+ computed: {
+ inputPlaceholder() {
+ const { issuableType, allowAutoComplete, confidential } = this;
+ const inputPlaceholderMapping = confidential
+ ? inputPlaceholderConfidentialTextMap
+ : inputPlaceholderTextMap;
+ const allowAutoCompleteText = autoCompleteTextMap[allowAutoComplete][issuableType];
+ return `${inputPlaceholderMapping[issuableType]}${allowAutoCompleteText}`;
+ },
+ allowAutoComplete() {
+ return Object.keys(this.autoCompleteSources).length > 0;
+ },
+ },
+ mounted() {
+ this.setupAutoComplete();
+ if (this.focusOnMount) {
+ this.$nextTick()
+ .then(() => {
+ this.$refs.input.focus();
+ })
+ .catch(() => {});
+ }
+ },
+ beforeUpdate() {
+ this.setupAutoComplete();
+ },
+ beforeDestroy() {
+ const $input = $(this.$refs.input);
+ $input.off('shown-issues.atwho');
+ $input.off('hidden-issues.atwho');
+ $input.off('inserted-issues.atwho', this.onInput);
+ },
+ methods: {
+ onAutoCompleteToggled(isOpen) {
+ this.isAutoCompleteOpen = isOpen;
+ },
+ onInputWrapperClick() {
+ this.$refs.input.focus();
+ },
+ onInput() {
+ const { value } = this.$refs.input;
+ const caretPos = this.$refs.input.selectionStart;
+ const rawRefs = value.split(/\s/);
+ let touchedReference;
+ let position = 0;
+
+ const untouchedRawRefs = rawRefs
+ .filter(ref => {
+ let isTouched = false;
+
+ if (caretPos >= position && caretPos <= position + ref.length) {
+ touchedReference = ref;
+ isTouched = true;
+ }
+
+ position = position + ref.length + SPACE_FACTOR;
+
+ return !isTouched;
+ })
+ .filter(ref => ref.trim().length > 0);
+
+ this.$emit('addIssuableFormInput', {
+ newValue: value,
+ untouchedRawReferences: untouchedRawRefs,
+ touchedReference,
+ caretPos,
+ });
+ },
+ onBlur(event) {
+ // Early exit if this Blur event is caused by card header
+ const container = this.$root.$el.querySelector('.js-button-container');
+ if (container && container.contains(event.relatedTarget)) {
+ return;
+ }
+
+ this.isInputFocused = false;
+
+ // Avoid tokenizing partial input when clicking an autocomplete item
+ if (!this.isAutoCompleteOpen) {
+ const { value } = this.$refs.input;
+ // Avoid event emission when only pathIdSeparator has been typed
+ if (value !== this.pathIdSeparator) {
+ this.$emit('addIssuableFormBlur', value);
+ }
+ }
+ },
+ onFocus() {
+ this.isInputFocused = true;
+ },
+ setupAutoComplete() {
+ const $input = $(this.$refs.input);
+
+ if (this.allowAutoComplete) {
+ this.gfmAutoComplete = new GfmAutoComplete(this.autoCompleteSources);
+ this.gfmAutoComplete.setup($input, this.autoCompleteOptions);
+ }
+
+ if (!this.areEventsAssigned) {
+ $input.on('shown-issues.atwho', this.onAutoCompleteToggled.bind(this, true));
+ $input.on('hidden-issues.atwho', this.onAutoCompleteToggled.bind(this, true));
+ }
+ this.areEventsAssigned = true;
+ },
+ onIssuableFormWrapperClick() {
+ this.$refs.input.focus();
+ },
+ },
+};
+</script>
+
+<template>
+ <div
+ ref="issuableFormWrapper"
+ :class="{ focus: isInputFocused }"
+ class="add-issuable-form-input-wrapper form-control gl-field-error-outline"
+ role="button"
+ @click="onIssuableFormWrapperClick"
+ >
+ <ul class="add-issuable-form-input-token-list">
+ <!--
+ We need to ensure this key changes any time the pendingReferences array is updated
+ else two consecutive pending ref strings in an array with the same name will collide
+ and cause odd behavior when one is removed.
+ -->
+ <li
+ v-for="(reference, index) in references"
+ :key="`related-issues-token-${reference}`"
+ class="js-add-issuable-form-token-list-item add-issuable-form-token-list-item"
+ >
+ <issue-token
+ :id-key="index"
+ :display-reference="reference.text || reference"
+ :can-remove="true"
+ :is-condensed="true"
+ :path-id-separator="pathIdSeparator"
+ event-namespace="pendingIssuable"
+ @pendingIssuableRemoveRequest="
+ params => {
+ $emit('pendingIssuableRemoveRequest', params);
+ }
+ "
+ />
+ </li>
+ <li class="add-issuable-form-input-list-item">
+ <input
+ :id="inputId"
+ ref="input"
+ :value="inputValue"
+ :placeholder="inputPlaceholder"
+ type="text"
+ class="js-add-issuable-form-input add-issuable-form-input qa-add-issue-input"
+ @input="onInput"
+ @focus="onFocus"
+ @blur="onBlur"
+ @keyup.escape.exact="$emit('addIssuableFormCancel')"
+ />
+ </li>
+ </ul>
+ </div>
+</template>
diff --git a/app/assets/javascripts/related_issues/components/related_issues_block.vue b/app/assets/javascripts/related_issues/components/related_issues_block.vue
new file mode 100644
index 00000000000..f7a79c62716
--- /dev/null
+++ b/app/assets/javascripts/related_issues/components/related_issues_block.vue
@@ -0,0 +1,215 @@
+<script>
+import { GlLink, GlIcon, GlButton } from '@gitlab/ui';
+import AddIssuableForm from './add_issuable_form.vue';
+import RelatedIssuesList from './related_issues_list.vue';
+import {
+ issuableIconMap,
+ issuableQaClassMap,
+ linkedIssueTypesMap,
+ linkedIssueTypesTextMap,
+} from '../constants';
+
+export default {
+ name: 'RelatedIssuesBlock',
+ components: {
+ GlLink,
+ GlButton,
+ GlIcon,
+ AddIssuableForm,
+ RelatedIssuesList,
+ },
+ props: {
+ isFetching: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ isSubmitting: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ relatedIssues: {
+ type: Array,
+ required: false,
+ default: () => [],
+ },
+ canAdmin: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ canReorder: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ isFormVisible: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ pendingReferences: {
+ type: Array,
+ required: false,
+ default: () => [],
+ },
+ inputValue: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ pathIdSeparator: {
+ type: String,
+ required: true,
+ },
+ helpPath: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ autoCompleteSources: {
+ type: Object,
+ required: false,
+ default: () => ({}),
+ },
+ issuableType: {
+ type: String,
+ required: true,
+ },
+ showCategorizedIssues: {
+ type: Boolean,
+ required: false,
+ default: true,
+ },
+ },
+ computed: {
+ hasRelatedIssues() {
+ return this.relatedIssues.length > 0;
+ },
+ categorisedIssues() {
+ if (this.showCategorizedIssues) {
+ return Object.values(linkedIssueTypesMap)
+ .map(linkType => ({
+ linkType,
+ issues: this.relatedIssues.filter(issue => issue.linkType === linkType),
+ }))
+ .filter(obj => obj.issues.length > 0);
+ }
+
+ return [{ issues: this.relatedIssues }];
+ },
+ shouldShowTokenBody() {
+ return this.hasRelatedIssues || this.isFetching;
+ },
+ hasBody() {
+ return this.isFormVisible || this.shouldShowTokenBody;
+ },
+ badgeLabel() {
+ return this.isFetching && this.relatedIssues.length === 0 ? '...' : this.relatedIssues.length;
+ },
+ hasHelpPath() {
+ return this.helpPath.length > 0;
+ },
+ issuableTypeIcon() {
+ return issuableIconMap[this.issuableType];
+ },
+ qaClass() {
+ return issuableQaClassMap[this.issuableType];
+ },
+ },
+ linkedIssueTypesTextMap,
+};
+</script>
+
+<template>
+ <div id="related-issues" class="related-issues-block">
+ <div class="card card-slim gl-overflow-hidden">
+ <div
+ :class="{ 'panel-empty-heading border-bottom-0': !hasBody }"
+ class="card-header gl-display-flex gl-justify-content-space-between"
+ >
+ <h3
+ class="card-title h5 position-relative gl-my-0 gl-display-flex gl-align-items-center gl-h-7"
+ >
+ <gl-link
+ id="user-content-related-issues"
+ class="anchor position-absolute gl-text-decoration-none"
+ href="#related-issues"
+ aria-hidden="true"
+ />
+ <slot name="headerText">{{ __('Linked issues') }}</slot>
+ <gl-link
+ v-if="hasHelpPath"
+ :href="helpPath"
+ target="_blank"
+ class="gl-display-flex gl-align-items-center gl-ml-2 gl-text-gray-500"
+ :aria-label="__('Read more about related issues')"
+ >
+ <gl-icon name="question" :size="12" role="text" />
+ </gl-link>
+
+ <div class="gl-display-inline-flex">
+ <div class="js-related-issues-header-issue-count gl-display-inline-flex gl-mx-5">
+ <span class="gl-display-inline-flex gl-align-items-center">
+ <gl-icon :name="issuableTypeIcon" class="gl-mr-2 gl-text-gray-500" />
+ {{ badgeLabel }}
+ </span>
+ </div>
+ <gl-button
+ v-if="canAdmin"
+ data-qa-selector="related_issues_plus_button"
+ icon="plus"
+ :aria-label="__('Add a related issue')"
+ :class="qaClass"
+ class="js-issue-count-badge-add-button"
+ @click="$emit('toggleAddRelatedIssuesForm', $event)"
+ />
+ </div>
+ </h3>
+ <slot name="headerActions"></slot>
+ </div>
+ <div
+ class="linked-issues-card-body bg-gray-light"
+ :class="{
+ 'gl-p-5': isFormVisible || shouldShowTokenBody,
+ }"
+ >
+ <div
+ v-if="isFormVisible"
+ class="js-add-related-issues-form-area card-body bordered-box bg-white"
+ >
+ <add-issuable-form
+ :show-categorized-issues="showCategorizedIssues"
+ :is-submitting="isSubmitting"
+ :issuable-type="issuableType"
+ :input-value="inputValue"
+ :pending-references="pendingReferences"
+ :auto-complete-sources="autoCompleteSources"
+ :path-id-separator="pathIdSeparator"
+ @pendingIssuableRemoveRequest="$emit('pendingIssuableRemoveRequest', $event)"
+ @addIssuableFormInput="$emit('addIssuableFormInput', $event)"
+ @addIssuableFormBlur="$emit('addIssuableFormBlur', $event)"
+ @addIssuableFormSubmit="$emit('addIssuableFormSubmit', $event)"
+ @addIssuableFormCancel="$emit('addIssuableFormCancel', $event)"
+ />
+ </div>
+ <template v-if="shouldShowTokenBody">
+ <related-issues-list
+ v-for="category in categorisedIssues"
+ :key="category.linkType"
+ :heading="$options.linkedIssueTypesTextMap[category.linkType]"
+ :can-admin="canAdmin"
+ :can-reorder="canReorder"
+ :is-fetching="isFetching"
+ :issuable-type="issuableType"
+ :path-id-separator="pathIdSeparator"
+ :related-issues="category.issues"
+ @relatedIssueRemoveRequest="$emit('relatedIssueRemoveRequest', $event)"
+ @saveReorder="$emit('saveReorder', $event)"
+ />
+ </template>
+ </div>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/related_issues/components/related_issues_list.vue b/app/assets/javascripts/related_issues/components/related_issues_list.vue
new file mode 100644
index 00000000000..a75fe4397bb
--- /dev/null
+++ b/app/assets/javascripts/related_issues/components/related_issues_list.vue
@@ -0,0 +1,146 @@
+<script>
+import { GlLoadingIcon } from '@gitlab/ui';
+import Sortable from 'sortablejs';
+import sortableConfig from 'ee_else_ce/sortable/sortable_config';
+import RelatedIssuableItem from '~/vue_shared/components/issue/related_issuable_item.vue';
+import tooltip from '~/vue_shared/directives/tooltip';
+
+export default {
+ name: 'RelatedIssuesList',
+ directives: {
+ tooltip,
+ },
+ components: {
+ GlLoadingIcon,
+ RelatedIssuableItem,
+ },
+ props: {
+ canAdmin: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ canReorder: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ heading: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ isFetching: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ issuableType: {
+ type: String,
+ required: true,
+ },
+ pathIdSeparator: {
+ type: String,
+ required: true,
+ },
+ relatedIssues: {
+ type: Array,
+ required: false,
+ default: () => [],
+ },
+ },
+ mounted() {
+ if (this.canReorder) {
+ this.sortable = Sortable.create(this.$refs.list, {
+ ...sortableConfig,
+ onStart: this.addDraggingCursor,
+ onEnd: this.reordered,
+ });
+ }
+ },
+ methods: {
+ getBeforeAfterId(itemEl) {
+ const prevItemEl = itemEl.previousElementSibling;
+ const nextItemEl = itemEl.nextElementSibling;
+
+ return {
+ beforeId: prevItemEl && parseInt(prevItemEl.dataset.orderingId, 0),
+ afterId: nextItemEl && parseInt(nextItemEl.dataset.orderingId, 0),
+ };
+ },
+ reordered(event) {
+ this.removeDraggingCursor();
+ const { beforeId, afterId } = this.getBeforeAfterId(event.item);
+ const { oldIndex, newIndex } = event;
+
+ this.$emit('saveReorder', {
+ issueId: parseInt(event.item.dataset.key, 10),
+ oldIndex,
+ newIndex,
+ afterId,
+ beforeId,
+ });
+ },
+ addDraggingCursor() {
+ document.body.classList.add('is-dragging');
+ },
+ removeDraggingCursor() {
+ document.body.classList.remove('is-dragging');
+ },
+ issuableOrderingId({ epicIssueId, id }) {
+ return this.issuableType === 'issue' ? epicIssueId : id;
+ },
+ },
+};
+</script>
+
+<template>
+ <div>
+ <h4 v-if="heading" class="gl-font-base mt-0">{{ heading }}</h4>
+ <div
+ class="related-issues-token-body bordered-box bg-white"
+ :class="{ 'sortable-container': canReorder }"
+ >
+ <div v-if="isFetching" class="related-issues-loading-icon qa-related-issues-loading-icon">
+ <gl-loading-icon ref="loadingIcon" label="Fetching linked issues" class="gl-mt-2" />
+ </div>
+ <ul ref="list" :class="{ 'content-list': !canReorder }" class="related-items-list">
+ <li
+ v-for="issue in relatedIssues"
+ :key="issue.id"
+ :class="{
+ 'user-can-drag': canReorder,
+ 'sortable-row': canReorder,
+ 'card card-slim': canReorder,
+ }"
+ :data-key="issue.id"
+ :data-ordering-id="issuableOrderingId(issue)"
+ class="js-related-issues-token-list-item list-item pt-0 pb-0"
+ >
+ <related-issuable-item
+ :id-key="issue.id"
+ :display-reference="issue.reference"
+ :confidential="issue.confidential"
+ :title="issue.title"
+ :path="issue.path"
+ :state="issue.state"
+ :milestone="issue.milestone"
+ :assignees="issue.assignees"
+ :created-at="issue.createdAt"
+ :closed-at="issue.closedAt"
+ :weight="issue.weight"
+ :due-date="issue.dueDate"
+ :can-remove="canAdmin"
+ :can-reorder="canReorder"
+ :path-id-separator="pathIdSeparator"
+ :is-locked="issue.lockIssueRemoval"
+ :locked-message="issue.lockedMessage"
+ event-namespace="relatedIssue"
+ class="qa-related-issuable-item"
+ @relatedIssueRemoveRequest="$emit('relatedIssueRemoveRequest', $event)"
+ />
+ </li>
+ </ul>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/related_issues/components/related_issues_root.vue b/app/assets/javascripts/related_issues/components/related_issues_root.vue
new file mode 100644
index 00000000000..6f68b25b6fb
--- /dev/null
+++ b/app/assets/javascripts/related_issues/components/related_issues_root.vue
@@ -0,0 +1,247 @@
+<script>
+/*
+`rawReferences` are separated by spaces.
+Given `abc 123 zxc`, `rawReferences = ['abc', '123', 'zxc']`
+
+Consider you are typing `abc 123 zxc` in the input and your caret position is
+at position 4 right before the `123` `rawReference`. Then you type `#` and
+it becomes a valid reference, `#123`, but we don't want to jump it straight into
+`pendingReferences` because you could still want to type. Say you typed `999`
+and now we have `#999123`. Only when you move your caret away from that `rawReference`
+do we actually put it in the `pendingReferences`.
+
+Your caret can stop touching a `rawReference` can happen in a variety of ways:
+
+ - As you type, we only tokenize after you type a space or move with the arrow keys
+ - On blur, we consider your caret not touching anything
+
+---
+
+ - When you click the "Add related issues"(in the `AddIssuableForm`),
+ we submit the `pendingReferences` to the server and they come back as actual `relatedIssues`
+ - When you click the "Cancel"(in the `AddIssuableForm`), we clear out `pendingReferences`
+ and hide the `AddIssuableForm` area.
+
+*/
+import { deprecatedCreateFlash as Flash } from '~/flash';
+import { __ } from '~/locale';
+import RelatedIssuesBlock from './related_issues_block.vue';
+import RelatedIssuesStore from '../stores/related_issues_store';
+import RelatedIssuesService from '../services/related_issues_service';
+import {
+ relatedIssuesRemoveErrorMap,
+ pathIndeterminateErrorMap,
+ addRelatedIssueErrorMap,
+ issuableTypesMap,
+ PathIdSeparator,
+} from '../constants';
+
+export default {
+ name: 'RelatedIssuesRoot',
+ components: {
+ relatedIssuesBlock: RelatedIssuesBlock,
+ },
+ props: {
+ endpoint: {
+ type: String,
+ required: true,
+ },
+ canAdmin: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ canReorder: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ helpPath: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ issuableType: {
+ type: String,
+ required: false,
+ default: issuableTypesMap.ISSUE,
+ },
+ allowAutoComplete: {
+ type: Boolean,
+ required: false,
+ default: true,
+ },
+ pathIdSeparator: {
+ type: String,
+ required: false,
+ default: PathIdSeparator.Issue,
+ },
+ cssClass: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ showCategorizedIssues: {
+ type: Boolean,
+ required: false,
+ default: true,
+ },
+ },
+ data() {
+ this.store = new RelatedIssuesStore();
+
+ return {
+ state: this.store.state,
+ isFetching: false,
+ isSubmitting: false,
+ isFormVisible: false,
+ inputValue: '',
+ };
+ },
+ computed: {
+ autoCompleteSources() {
+ if (!this.allowAutoComplete) return {};
+ return gl.GfmAutoComplete && gl.GfmAutoComplete.dataSources;
+ },
+ },
+ created() {
+ this.service = new RelatedIssuesService(this.endpoint);
+ this.fetchRelatedIssues();
+ },
+ methods: {
+ findRelatedIssueById(id) {
+ return this.state.relatedIssues.find(issue => issue.id === id);
+ },
+ onRelatedIssueRemoveRequest(idToRemove) {
+ const issueToRemove = this.findRelatedIssueById(idToRemove);
+
+ if (issueToRemove) {
+ RelatedIssuesService.remove(issueToRemove.relationPath)
+ .then(({ data }) => {
+ this.store.setRelatedIssues(data.issuables);
+ })
+ .catch(res => {
+ if (res && res.status !== 404) {
+ Flash(relatedIssuesRemoveErrorMap[this.issuableType]);
+ }
+ });
+ } else {
+ Flash(pathIndeterminateErrorMap[this.issuableType]);
+ }
+ },
+ onToggleAddRelatedIssuesForm() {
+ this.isFormVisible = !this.isFormVisible;
+ },
+ onPendingIssueRemoveRequest(indexToRemove) {
+ this.store.removePendingRelatedIssue(indexToRemove);
+ },
+ onPendingFormSubmit(event) {
+ this.processAllReferences(event.pendingReferences);
+
+ if (this.state.pendingReferences.length > 0) {
+ this.isSubmitting = true;
+ this.service
+ .addRelatedIssues(this.state.pendingReferences, event.linkedIssueType)
+ .then(({ data }) => {
+ // We could potentially lose some pending issues in the interim here
+ this.store.setPendingReferences([]);
+ this.store.setRelatedIssues(data.issuables);
+
+ // Close the form on submission
+ this.isFormVisible = false;
+ })
+ .catch(({ response }) => {
+ let errorMessage = addRelatedIssueErrorMap[this.issuableType];
+ if (response && response.data && response.data.message) {
+ errorMessage = response.data.message;
+ }
+ Flash(errorMessage);
+ })
+ .finally(() => {
+ this.isSubmitting = false;
+ });
+ }
+ },
+ onPendingFormCancel() {
+ this.isFormVisible = false;
+ this.store.setPendingReferences([]);
+ this.inputValue = '';
+ },
+ fetchRelatedIssues() {
+ this.isFetching = true;
+ this.service
+ .fetchRelatedIssues()
+ .then(({ data }) => {
+ this.store.setRelatedIssues(data);
+ })
+ .catch(() => {
+ this.store.setRelatedIssues([]);
+ Flash(__('An error occurred while fetching issues.'));
+ })
+ .finally(() => {
+ this.isFetching = false;
+ });
+ },
+ saveIssueOrder({ issueId, beforeId, afterId, oldIndex, newIndex }) {
+ const issueToReorder = this.findRelatedIssueById(issueId);
+
+ if (issueToReorder) {
+ RelatedIssuesService.saveOrder({
+ endpoint: issueToReorder.relationPath,
+ move_before_id: beforeId,
+ move_after_id: afterId,
+ })
+ .then(({ data }) => {
+ if (!data.message) {
+ this.store.updateIssueOrder(oldIndex, newIndex);
+ }
+ })
+ .catch(() => {
+ Flash(__('An error occurred while reordering issues.'));
+ });
+ }
+ },
+ onInput({ untouchedRawReferences, touchedReference }) {
+ this.store.addPendingReferences(untouchedRawReferences);
+
+ this.inputValue = `${touchedReference}`;
+ },
+ onBlur(newValue) {
+ this.processAllReferences(newValue);
+ },
+ processAllReferences(value = '') {
+ const rawReferences = value.split(/\s+/).filter(reference => reference.trim().length > 0);
+
+ this.store.addPendingReferences(rawReferences);
+ this.inputValue = '';
+ },
+ },
+};
+</script>
+
+<template>
+ <related-issues-block
+ :class="cssClass"
+ :help-path="helpPath"
+ :is-fetching="isFetching"
+ :is-submitting="isSubmitting"
+ :related-issues="state.relatedIssues"
+ :can-admin="canAdmin"
+ :can-reorder="canReorder"
+ :pending-references="state.pendingReferences"
+ :is-form-visible="isFormVisible"
+ :input-value="inputValue"
+ :auto-complete-sources="autoCompleteSources"
+ :issuable-type="issuableType"
+ :path-id-separator="pathIdSeparator"
+ :show-categorized-issues="showCategorizedIssues"
+ @saveReorder="saveIssueOrder"
+ @toggleAddRelatedIssuesForm="onToggleAddRelatedIssuesForm"
+ @addIssuableFormInput="onInput"
+ @addIssuableFormBlur="onBlur"
+ @addIssuableFormSubmit="onPendingFormSubmit"
+ @addIssuableFormCancel="onPendingFormCancel"
+ @pendingIssuableRemoveRequest="onPendingIssueRemoveRequest"
+ @relatedIssueRemoveRequest="onRelatedIssueRemoveRequest"
+ />
+</template>
diff --git a/app/assets/javascripts/related_issues/constants.js b/app/assets/javascripts/related_issues/constants.js
new file mode 100644
index 00000000000..89eae069a24
--- /dev/null
+++ b/app/assets/javascripts/related_issues/constants.js
@@ -0,0 +1,106 @@
+import { __, sprintf } from '~/locale';
+
+export const issuableTypesMap = {
+ ISSUE: 'issue',
+ EPIC: 'epic',
+ MERGE_REQUEST: 'merge_request',
+};
+
+export const linkedIssueTypesMap = {
+ BLOCKS: 'blocks',
+ IS_BLOCKED_BY: 'is_blocked_by',
+ RELATES_TO: 'relates_to',
+};
+
+export const linkedIssueTypesTextMap = {
+ [linkedIssueTypesMap.RELATES_TO]: __('Relates to'),
+ [linkedIssueTypesMap.BLOCKS]: __('Blocks'),
+ [linkedIssueTypesMap.IS_BLOCKED_BY]: __('Is blocked by'),
+};
+
+export const autoCompleteTextMap = {
+ true: {
+ [issuableTypesMap.ISSUE]: sprintf(
+ __(' or %{emphasisStart}#issue id%{emphasisEnd}'),
+ { emphasisStart: '<', emphasisEnd: '>' },
+ false,
+ ),
+ [issuableTypesMap.EPIC]: sprintf(
+ __(' or %{emphasisStart}&epic id%{emphasisEnd}'),
+ { emphasisStart: '<', emphasisEnd: '>' },
+ false,
+ ),
+ [issuableTypesMap.MERGE_REQUEST]: sprintf(
+ __(' or %{emphasisStart}!merge request id%{emphasisEnd}'),
+ { emphasisStart: '<', emphasisEnd: '>' },
+ false,
+ ),
+ },
+ false: {
+ [issuableTypesMap.ISSUE]: '',
+ [issuableTypesMap.EPIC]: '',
+ [issuableTypesMap.MERGE_REQUEST]: __(' or references (e.g. path/to/project!merge_request_id)'),
+ },
+};
+
+export const inputPlaceholderTextMap = {
+ [issuableTypesMap.ISSUE]: __('Paste issue link'),
+ [issuableTypesMap.EPIC]: __('Paste epic link'),
+ [issuableTypesMap.MERGE_REQUEST]: __('Enter merge request URLs'),
+};
+
+export const inputPlaceholderConfidentialTextMap = {
+ [issuableTypesMap.ISSUE]: __('Paste confidential issue link'),
+ [issuableTypesMap.EPIC]: __('Paste confidential epic link'),
+ [issuableTypesMap.MERGE_REQUEST]: __('Enter merge request URLs'),
+};
+
+export const relatedIssuesRemoveErrorMap = {
+ [issuableTypesMap.ISSUE]: __('An error occurred while removing issues.'),
+ [issuableTypesMap.EPIC]: __('An error occurred while removing epics.'),
+};
+
+export const pathIndeterminateErrorMap = {
+ [issuableTypesMap.ISSUE]: __('We could not determine the path to remove the issue'),
+ [issuableTypesMap.EPIC]: __('We could not determine the path to remove the epic'),
+};
+
+export const itemAddFailureTypesMap = {
+ NOT_FOUND: 'not_found',
+ MAX_NUMBER_OF_CHILD_EPICS: 'conflict',
+};
+
+export const addRelatedIssueErrorMap = {
+ [issuableTypesMap.ISSUE]: __('Issue cannot be found.'),
+ [issuableTypesMap.EPIC]: __('Epic cannot be found.'),
+};
+
+export const addRelatedItemErrorMap = {
+ [itemAddFailureTypesMap.MAX_NUMBER_OF_CHILD_EPICS]: __(
+ 'This epic already has the maximum number of child epics.',
+ ),
+};
+
+/**
+ * These are used to map issuableType to the correct icon.
+ * Since these are never used for any display purposes, don't wrap
+ * them inside i18n functions.
+ */
+export const issuableIconMap = {
+ [issuableTypesMap.ISSUE]: 'issues',
+ [issuableTypesMap.EPIC]: 'epic',
+};
+
+/**
+ * These are used to map issuableType to the correct QA class.
+ * Since these are never used for any display purposes, don't wrap
+ * them inside i18n functions.
+ */
+export const issuableQaClassMap = {
+ [issuableTypesMap.EPIC]: 'qa-add-epics-button',
+};
+
+export const PathIdSeparator = {
+ Epic: '&',
+ Issue: '#',
+};
diff --git a/app/assets/javascripts/related_issues/index.js b/app/assets/javascripts/related_issues/index.js
new file mode 100644
index 00000000000..2e8626890cb
--- /dev/null
+++ b/app/assets/javascripts/related_issues/index.js
@@ -0,0 +1,27 @@
+import Vue from 'vue';
+import { parseBoolean } from '~/lib/utils/common_utils';
+import RelatedIssuesRoot from './components/related_issues_root.vue';
+
+export default function initRelatedIssues() {
+ const relatedIssuesRootElement = document.querySelector('.js-related-issues-root');
+ if (relatedIssuesRootElement) {
+ // eslint-disable-next-line no-new
+ new Vue({
+ el: relatedIssuesRootElement,
+ components: {
+ relatedIssuesRoot: RelatedIssuesRoot,
+ },
+ render: createElement =>
+ createElement('related-issues-root', {
+ props: {
+ endpoint: relatedIssuesRootElement.dataset.endpoint,
+ canAdmin: parseBoolean(relatedIssuesRootElement.dataset.canAddRelatedIssues),
+ helpPath: relatedIssuesRootElement.dataset.helpPath,
+ showCategorizedIssues: parseBoolean(
+ relatedIssuesRootElement.dataset.showCategorizedIssues,
+ ),
+ },
+ }),
+ });
+ }
+}
diff --git a/app/assets/javascripts/related_issues/services/related_issues_service.js b/app/assets/javascripts/related_issues/services/related_issues_service.js
new file mode 100644
index 00000000000..3c19f63157e
--- /dev/null
+++ b/app/assets/javascripts/related_issues/services/related_issues_service.js
@@ -0,0 +1,34 @@
+import axios from '~/lib/utils/axios_utils';
+import { linkedIssueTypesMap } from '../constants';
+
+class RelatedIssuesService {
+ constructor(endpoint) {
+ this.endpoint = endpoint;
+ }
+
+ fetchRelatedIssues() {
+ return axios.get(this.endpoint);
+ }
+
+ addRelatedIssues(newIssueReferences, linkType = linkedIssueTypesMap.RELATES_TO) {
+ return axios.post(this.endpoint, {
+ issuable_references: newIssueReferences,
+ link_type: linkType,
+ });
+ }
+
+ static saveOrder({ endpoint, move_before_id, move_after_id }) {
+ return axios.put(endpoint, {
+ epic: {
+ move_before_id,
+ move_after_id,
+ },
+ });
+ }
+
+ static remove(endpoint) {
+ return axios.delete(endpoint);
+ }
+}
+
+export default RelatedIssuesService;
diff --git a/app/assets/javascripts/related_issues/stores/related_issues_store.js b/app/assets/javascripts/related_issues/stores/related_issues_store.js
new file mode 100644
index 00000000000..14d71628cad
--- /dev/null
+++ b/app/assets/javascripts/related_issues/stores/related_issues_store.js
@@ -0,0 +1,50 @@
+import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
+
+class RelatedIssuesStore {
+ constructor() {
+ this.state = {
+ // Stores issue objects of the known related issues
+ relatedIssues: [],
+ // Stores references of the "staging area" related issues that are planned to be added
+ pendingReferences: [],
+ };
+ }
+
+ setRelatedIssues(issues = []) {
+ this.state.relatedIssues = convertObjectPropsToCamelCase(issues, { deep: true });
+ }
+
+ addRelatedIssues(issues = []) {
+ this.setRelatedIssues(this.state.relatedIssues.concat(issues));
+ }
+
+ removeRelatedIssue(issue) {
+ this.state.relatedIssues = this.state.relatedIssues.filter(x => x.id !== issue.id);
+ }
+
+ updateIssueOrder(oldIndex, newIndex) {
+ if (this.state.relatedIssues.length > 0) {
+ const updatedIssue = this.state.relatedIssues.splice(oldIndex, 1)[0];
+ this.state.relatedIssues.splice(newIndex, 0, updatedIssue);
+ }
+ }
+
+ setPendingReferences(issues) {
+ // Remove duplicates but retain order.
+ // If you don't do this, Vue will be confused by duplicates and refuse to delete them all.
+ this.state.pendingReferences = issues.filter((ref, idx) => issues.indexOf(ref) === idx);
+ }
+
+ addPendingReferences(references = []) {
+ const issues = this.state.pendingReferences.concat(references);
+ this.setPendingReferences(issues);
+ }
+
+ removePendingRelatedIssue(indexToRemove) {
+ this.state.pendingReferences = this.state.pendingReferences.filter(
+ (reference, index) => index !== indexToRemove,
+ );
+ }
+}
+
+export default RelatedIssuesStore;
diff --git a/app/assets/javascripts/related_merge_requests/components/related_merge_requests.vue b/app/assets/javascripts/related_merge_requests/components/related_merge_requests.vue
index 15e9b8559d4..7db76ed576c 100644
--- a/app/assets/javascripts/related_merge_requests/components/related_merge_requests.vue
+++ b/app/assets/javascripts/related_merge_requests/components/related_merge_requests.vue
@@ -1,15 +1,14 @@
<script>
import { mapState, mapActions } from 'vuex';
-import { GlLink, GlLoadingIcon } from '@gitlab/ui';
+import { GlLink, GlLoadingIcon, GlIcon } from '@gitlab/ui';
import { sprintf, n__, s__ } from '~/locale';
-import Icon from '~/vue_shared/components/icon.vue';
import RelatedIssuableItem from '~/vue_shared/components/issue/related_issuable_item.vue';
import { parseIssuableData } from '../../issue_show/utils/parse_data';
export default {
name: 'RelatedMergeRequests',
components: {
- Icon,
+ GlIcon,
GlLink,
GlLoadingIcon,
RelatedIssuableItem,
@@ -85,7 +84,7 @@ export default {
<div class="mr-count-badge gl-display-inline-flex">
<div class="mr-count-badge-count">
<svg class="s16 mr-1 text-secondary">
- <icon name="merge-request" class="mr-1 text-secondary" />
+ <gl-icon name="merge-request" class="mr-1 text-secondary" />
</svg>
<span class="js-items-count">{{ totalCount }}</span>
</div>
diff --git a/app/assets/javascripts/releases/components/app_edit_new.vue b/app/assets/javascripts/releases/components/app_edit_new.vue
index 7b7c80a6269..e1edf3d689d 100644
--- a/app/assets/javascripts/releases/components/app_edit_new.vue
+++ b/app/assets/javascripts/releases/components/app_edit_new.vue
@@ -1,4 +1,5 @@
<script>
+/* eslint-disable vue/no-v-html */
import { mapState, mapActions, mapGetters } from 'vuex';
import { GlButton, GlFormInput, GlFormGroup } from '@gitlab/ui';
import { __, sprintf } from '~/locale';
@@ -139,7 +140,7 @@ export default {
class="form-control"
/>
</gl-form-group>
- <gl-form-group class="w-50" @keydown.enter.prevent.capture>
+ <gl-form-group class="w-50" data-testid="milestones-field">
<label>{{ __('Milestones') }}</label>
<div class="d-flex flex-column col-md-6 col-sm-10 pl-0">
<milestone-combobox
diff --git a/app/assets/javascripts/releases/components/app_index.vue b/app/assets/javascripts/releases/components/app_index.vue
index 67085ecca2b..b8cf6ce478f 100644
--- a/app/assets/javascripts/releases/components/app_index.vue
+++ b/app/assets/javascripts/releases/components/app_index.vue
@@ -1,6 +1,11 @@
<script>
import { mapState, mapActions } from 'vuex';
-import { GlSkeletonLoading, GlEmptyState, GlLink, GlButton } from '@gitlab/ui';
+import {
+ GlDeprecatedSkeletonLoading as GlSkeletonLoading,
+ GlEmptyState,
+ GlLink,
+ GlButton,
+} from '@gitlab/ui';
import {
getParameterByName,
historyPushState,
@@ -20,27 +25,16 @@ export default {
GlLink,
GlButton,
},
- props: {
- projectId: {
- type: String,
- required: true,
- },
- documentationPath: {
- type: String,
- required: true,
- },
- illustrationPath: {
- type: String,
- required: true,
- },
- newReleasePath: {
- type: String,
- required: false,
- default: '',
- },
- },
computed: {
- ...mapState('list', ['isLoading', 'releases', 'hasError', 'pageInfo']),
+ ...mapState('list', [
+ 'documentationPath',
+ 'illustrationPath',
+ 'newReleasePath',
+ 'isLoading',
+ 'releases',
+ 'hasError',
+ 'pageInfo',
+ ]),
shouldRenderEmptyState() {
return !this.releases.length && !this.hasError && !this.isLoading;
},
@@ -56,14 +50,13 @@ export default {
created() {
this.fetchReleases({
page: getParameterByName('page'),
- projectId: this.projectId,
});
},
methods: {
...mapActions('list', ['fetchReleases']),
onChangePage(page) {
historyPushState(buildUrlWithCurrentLocation(`?page=${page}`));
- this.fetchReleases({ page, projectId: this.projectId });
+ this.fetchReleases({ page });
},
},
};
diff --git a/app/assets/javascripts/releases/components/app_show.vue b/app/assets/javascripts/releases/components/app_show.vue
index 0e65d722952..8b89f0cf3fc 100644
--- a/app/assets/javascripts/releases/components/app_show.vue
+++ b/app/assets/javascripts/releases/components/app_show.vue
@@ -1,6 +1,6 @@
<script>
import { mapState, mapActions } from 'vuex';
-import { GlSkeletonLoading } from '@gitlab/ui';
+import { GlDeprecatedSkeletonLoading as GlSkeletonLoading } from '@gitlab/ui';
import ReleaseBlock from './release_block.vue';
export default {
diff --git a/app/assets/javascripts/releases/components/evidence_block.vue b/app/assets/javascripts/releases/components/evidence_block.vue
index 6468e2ded62..3724162f6d5 100644
--- a/app/assets/javascripts/releases/components/evidence_block.vue
+++ b/app/assets/javascripts/releases/components/evidence_block.vue
@@ -80,9 +80,7 @@ export default {
<span class="js-short monospace">{{ shortSha(index) }}</span>
</template>
<template #expanded>
- <span class="js-expanded monospace gl-pl-1-deprecated-no-really-do-not-use-me">{{
- sha(index)
- }}</span>
+ <span class="js-expanded monospace gl-pl-2">{{ sha(index) }}</span>
</template>
</expand-button>
<clipboard-button
diff --git a/app/assets/javascripts/releases/components/release_block.vue b/app/assets/javascripts/releases/components/release_block.vue
index e0061d88ccb..2629df08be7 100644
--- a/app/assets/javascripts/releases/components/release_block.vue
+++ b/app/assets/javascripts/releases/components/release_block.vue
@@ -1,4 +1,5 @@
<script>
+/* eslint-disable vue/no-v-html */
import { isEmpty } from 'lodash';
import $ from 'jquery';
import { slugify } from '~/lib/utils/text_utility';
diff --git a/app/assets/javascripts/releases/components/release_block_assets.vue b/app/assets/javascripts/releases/components/release_block_assets.vue
index 9583f5737df..8824cbefd7e 100644
--- a/app/assets/javascripts/releases/components/release_block_assets.vue
+++ b/app/assets/javascripts/releases/components/release_block_assets.vue
@@ -1,7 +1,6 @@
<script>
import { GlTooltipDirective, GlLink, GlButton, GlCollapse, GlIcon, GlBadge } from '@gitlab/ui';
import { difference, get } from 'lodash';
-import Icon from '~/vue_shared/components/icon.vue';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { ASSET_LINK_TYPE } from '../constants';
import { __, s__, sprintf } from '~/locale';
@@ -13,7 +12,6 @@ export default {
GlButton,
GlCollapse,
GlIcon,
- Icon,
GlBadge,
},
directives: {
@@ -157,7 +155,7 @@ export default {
<ul v-if="assets.links.length" class="pl-0 mb-0 gl-mt-3 list-unstyled js-assets-list">
<li v-for="link in assets.links" :key="link.name" class="gl-mb-3">
<gl-link v-gl-tooltip.bottom :title="__('Download asset')" :href="link.directAssetUrl">
- <icon name="package" class="align-middle gl-mr-2 align-text-bottom" />
+ <gl-icon name="package" class="align-middle gl-mr-2 align-text-bottom" />
{{ link.name }}
<span v-if="link.external" data-testid="external-link-indicator">{{
__('(external source)')
@@ -174,9 +172,9 @@ export default {
aria-haspopup="true"
aria-expanded="false"
>
- <icon name="doc-code" class="align-top gl-mr-2" />
+ <gl-icon name="doc-code" class="align-top gl-mr-2" />
{{ __('Source code') }}
- <icon name="chevron-down" />
+ <gl-icon name="chevron-down" />
</button>
<div class="js-sources-dropdown dropdown-menu">
diff --git a/app/assets/javascripts/releases/components/release_block_footer.vue b/app/assets/javascripts/releases/components/release_block_footer.vue
index 26154272d39..3beec466c54 100644
--- a/app/assets/javascripts/releases/components/release_block_footer.vue
+++ b/app/assets/javascripts/releases/components/release_block_footer.vue
@@ -1,6 +1,5 @@
<script>
-import { GlTooltipDirective, GlLink } from '@gitlab/ui';
-import Icon from '~/vue_shared/components/icon.vue';
+import { GlTooltipDirective, GlLink, GlIcon } from '@gitlab/ui';
import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue';
import timeagoMixin from '~/vue_shared/mixins/timeago';
import { __, sprintf } from '~/locale';
@@ -8,7 +7,7 @@ import { __, sprintf } from '~/locale';
export default {
name: 'ReleaseBlockFooter',
components: {
- Icon,
+ GlIcon,
GlLink,
UserAvatarLink,
},
@@ -68,7 +67,7 @@ export default {
<template>
<div>
<div v-if="commit" class="float-left mr-3 d-flex align-items-center js-commit-info">
- <icon ref="commitIcon" name="commit" class="mr-1" />
+ <gl-icon ref="commitIcon" name="commit" class="mr-1" />
<div v-gl-tooltip.bottom :title="commit.title">
<gl-link v-if="commitPath" :href="commitPath">
{{ commit.shortId }}
@@ -78,7 +77,7 @@ export default {
</div>
<div v-if="tagName" class="float-left mr-3 d-flex align-items-center js-tag-info">
- <icon name="tag" class="mr-1" />
+ <gl-icon name="tag" class="mr-1" />
<div v-gl-tooltip.bottom :title="__('Tag')">
<gl-link v-if="tagPath" :href="tagPath">
{{ tagName }}
diff --git a/app/assets/javascripts/releases/components/release_block_header.vue b/app/assets/javascripts/releases/components/release_block_header.vue
index 310fba0fe76..95292a26bce 100644
--- a/app/assets/javascripts/releases/components/release_block_header.vue
+++ b/app/assets/javascripts/releases/components/release_block_header.vue
@@ -1,6 +1,5 @@
<script>
-import { GlTooltipDirective, GlLink, GlBadge, GlButton } from '@gitlab/ui';
-import Icon from '~/vue_shared/components/icon.vue';
+import { GlTooltipDirective, GlLink, GlBadge, GlButton, GlIcon } from '@gitlab/ui';
import { BACK_URL_PARAM } from '~/releases/constants';
import { setUrlParams } from '~/lib/utils/url_utility';
@@ -9,7 +8,7 @@ export default {
components: {
GlLink,
GlBadge,
- Icon,
+ GlIcon,
GlButton,
},
directives: {
@@ -42,7 +41,7 @@ export default {
<template>
<div class="card-header d-flex align-items-center bg-white pr-0">
- <h2 class="card-title my-2 mr-auto gl-font-size-20-deprecated-no-really-do-not-use-me">
+ <h2 class="card-title my-2 mr-auto">
<gl-link v-if="selfLink" :href="selfLink" class="font-size-inherit">
{{ release.name }}
</gl-link>
@@ -60,7 +59,7 @@ export default {
:title="__('Edit this release')"
:href="editLink"
>
- <icon name="pencil" />
+ <gl-icon name="pencil" />
</gl-button>
</div>
</template>
diff --git a/app/assets/javascripts/releases/components/release_block_metadata.vue b/app/assets/javascripts/releases/components/release_block_metadata.vue
index 861c2e11798..2247b4c0064 100644
--- a/app/assets/javascripts/releases/components/release_block_metadata.vue
+++ b/app/assets/javascripts/releases/components/release_block_metadata.vue
@@ -1,7 +1,6 @@
<script>
-import { GlTooltipDirective, GlLink } from '@gitlab/ui';
+import { GlTooltipDirective, GlLink, GlIcon } from '@gitlab/ui';
import { __, sprintf } from '~/locale';
-import Icon from '~/vue_shared/components/icon.vue';
import timeagoMixin from '~/vue_shared/mixins/timeago';
import ReleaseBlockAuthor from './release_block_author.vue';
import ReleaseBlockMilestones from './release_block_milestones.vue';
@@ -9,7 +8,7 @@ import ReleaseBlockMilestones from './release_block_milestones.vue';
export default {
name: 'ReleaseBlockMetadata',
components: {
- Icon,
+ GlIcon,
GlLink,
ReleaseBlockAuthor,
ReleaseBlockMilestones,
@@ -58,7 +57,7 @@ export default {
<template>
<div class="card-subtitle d-flex flex-wrap text-secondary">
<div class="gl-mr-3">
- <icon name="commit" class="align-middle" />
+ <gl-icon name="commit" class="align-middle" />
<gl-link v-if="commitUrl" v-gl-tooltip.bottom :title="commit.title" :href="commitUrl">
{{ commit.shortId }}
</gl-link>
@@ -66,7 +65,7 @@ export default {
</div>
<div class="gl-mr-3">
- <icon name="tag" class="align-middle" />
+ <gl-icon name="tag" class="align-middle" />
<gl-link v-if="tagUrl" v-gl-tooltip.bottom :title="__('Tag')" :href="tagUrl">
{{ release.tagName }}
</gl-link>
diff --git a/app/assets/javascripts/releases/components/release_block_milestones.vue b/app/assets/javascripts/releases/components/release_block_milestones.vue
index 9abd3345b22..1da683764b3 100644
--- a/app/assets/javascripts/releases/components/release_block_milestones.vue
+++ b/app/assets/javascripts/releases/components/release_block_milestones.vue
@@ -1,13 +1,12 @@
<script>
-import { GlTooltipDirective, GlLink } from '@gitlab/ui';
+import { GlTooltipDirective, GlLink, GlIcon } from '@gitlab/ui';
import { n__ } from '~/locale';
-import Icon from '~/vue_shared/components/icon.vue';
export default {
name: 'ReleaseBlockMilestones',
components: {
GlLink,
- Icon,
+ GlIcon,
},
directives: {
GlTooltip: GlTooltipDirective,
@@ -29,7 +28,7 @@ export default {
<template>
<div>
<div class="js-milestone-list-label">
- <icon name="flag" class="align-middle" />
+ <gl-icon name="flag" class="align-middle" />
<span class="js-label-text">{{ labelText }}</span>
</div>
diff --git a/app/assets/javascripts/releases/components/releases_pagination.vue b/app/assets/javascripts/releases/components/releases_pagination.vue
new file mode 100644
index 00000000000..062c72b445b
--- /dev/null
+++ b/app/assets/javascripts/releases/components/releases_pagination.vue
@@ -0,0 +1,20 @@
+<script>
+import { mapGetters } from 'vuex';
+import ReleasesPaginationGraphql from './releases_pagination_graphql.vue';
+import ReleasesPaginationRest from './releases_pagination_rest.vue';
+
+export default {
+ name: 'ReleasesPagination',
+ components: { ReleasesPaginationGraphql, ReleasesPaginationRest },
+ computed: {
+ ...mapGetters(['useGraphQLEndpoint']),
+ },
+};
+</script>
+
+<template>
+ <div class="gl-display-flex gl-justify-content-center">
+ <releases-pagination-graphql v-if="useGraphQLEndpoint" />
+ <releases-pagination-rest v-else />
+ </div>
+</template>
diff --git a/app/assets/javascripts/releases/components/releases_pagination_graphql.vue b/app/assets/javascripts/releases/components/releases_pagination_graphql.vue
new file mode 100644
index 00000000000..a4fe407a5bd
--- /dev/null
+++ b/app/assets/javascripts/releases/components/releases_pagination_graphql.vue
@@ -0,0 +1,35 @@
+<script>
+import { mapActions, mapState } from 'vuex';
+import { GlKeysetPagination } from '@gitlab/ui';
+import { historyPushState, buildUrlWithCurrentLocation } from '~/lib/utils/common_utils';
+
+export default {
+ name: 'ReleasesPaginationGraphql',
+ components: { GlKeysetPagination },
+ computed: {
+ ...mapState('list', ['graphQlPageInfo']),
+ showPagination() {
+ return this.graphQlPageInfo.hasPreviousPage || this.graphQlPageInfo.hasNextPage;
+ },
+ },
+ methods: {
+ ...mapActions('list', ['fetchReleasesGraphQl']),
+ onPrev(before) {
+ historyPushState(buildUrlWithCurrentLocation(`?before=${before}`));
+ this.fetchReleasesGraphQl({ before });
+ },
+ onNext(after) {
+ historyPushState(buildUrlWithCurrentLocation(`?after=${after}`));
+ this.fetchReleasesGraphQl({ after });
+ },
+ },
+};
+</script>
+<template>
+ <gl-keyset-pagination
+ v-if="showPagination"
+ v-bind="graphQlPageInfo"
+ @prev="onPrev($event)"
+ @next="onNext($event)"
+ />
+</template>
diff --git a/app/assets/javascripts/releases/components/releases_pagination_rest.vue b/app/assets/javascripts/releases/components/releases_pagination_rest.vue
new file mode 100644
index 00000000000..992cc4cd469
--- /dev/null
+++ b/app/assets/javascripts/releases/components/releases_pagination_rest.vue
@@ -0,0 +1,24 @@
+<script>
+import { mapActions, mapState } from 'vuex';
+import TablePagination from '~/vue_shared/components/pagination/table_pagination.vue';
+import { historyPushState, buildUrlWithCurrentLocation } from '~/lib/utils/common_utils';
+
+export default {
+ name: 'ReleasesPaginationRest',
+ components: { TablePagination },
+ computed: {
+ ...mapState('list', ['pageInfo']),
+ },
+ methods: {
+ ...mapActions('list', ['fetchReleasesRest']),
+ onChangePage(page) {
+ historyPushState(buildUrlWithCurrentLocation(`?page=${page}`));
+ this.fetchReleasesRest({ page });
+ },
+ },
+};
+</script>
+
+<template>
+ <table-pagination :change="onChangePage" :page-info="pageInfo" />
+</template>
diff --git a/app/assets/javascripts/releases/mount_edit.js b/app/assets/javascripts/releases/mount_edit.js
index c7385b3c57f..623b18591a0 100644
--- a/app/assets/javascripts/releases/mount_edit.js
+++ b/app/assets/javascripts/releases/mount_edit.js
@@ -1,8 +1,11 @@
import Vue from 'vue';
+import Vuex from 'vuex';
import ReleaseEditNewApp from './components/app_edit_new.vue';
import createStore from './stores';
import createDetailModule from './stores/modules/detail';
+Vue.use(Vuex);
+
export default () => {
const el = document.getElementById('js-edit-release-page');
diff --git a/app/assets/javascripts/releases/mount_index.js b/app/assets/javascripts/releases/mount_index.js
index 5f0bf3b6459..cd4fa5c5df5 100644
--- a/app/assets/javascripts/releases/mount_index.js
+++ b/app/assets/javascripts/releases/mount_index.js
@@ -1,7 +1,10 @@
import Vue from 'vue';
+import Vuex from 'vuex';
import ReleaseListApp from './components/app_index.vue';
import createStore from './stores';
-import listModule from './stores/modules/list';
+import createListModule from './stores/modules/list';
+
+Vue.use(Vuex);
export default () => {
const el = document.getElementById('js-releases-page');
@@ -10,12 +13,14 @@ export default () => {
el,
store: createStore({
modules: {
- list: listModule,
+ list: createListModule(el.dataset),
+ },
+ featureFlags: {
+ graphqlReleaseData: Boolean(gon.features?.graphqlReleaseData),
+ graphqlReleasesPage: Boolean(gon.features?.graphqlReleasesPage),
+ graphqlMilestoneStats: Boolean(gon.features?.graphqlMilestoneStats),
},
}),
- render: h =>
- h(ReleaseListApp, {
- props: el.dataset,
- }),
+ render: h => h(ReleaseListApp),
});
};
diff --git a/app/assets/javascripts/releases/mount_new.js b/app/assets/javascripts/releases/mount_new.js
index 68003f6a346..10725e47740 100644
--- a/app/assets/javascripts/releases/mount_new.js
+++ b/app/assets/javascripts/releases/mount_new.js
@@ -1,8 +1,11 @@
import Vue from 'vue';
+import Vuex from 'vuex';
import ReleaseEditNewApp from './components/app_edit_new.vue';
import createStore from './stores';
import createDetailModule from './stores/modules/detail';
+Vue.use(Vuex);
+
export default () => {
const el = document.getElementById('js-new-release-page');
diff --git a/app/assets/javascripts/releases/mount_show.js b/app/assets/javascripts/releases/mount_show.js
index 7ddc8e786c1..eef015ee0a6 100644
--- a/app/assets/javascripts/releases/mount_show.js
+++ b/app/assets/javascripts/releases/mount_show.js
@@ -1,8 +1,11 @@
import Vue from 'vue';
+import Vuex from 'vuex';
import ReleaseShowApp from './components/app_show.vue';
import createStore from './stores';
import createDetailModule from './stores/modules/detail';
+Vue.use(Vuex);
+
export default () => {
const el = document.getElementById('js-show-release-page');
diff --git a/app/assets/javascripts/releases/queries/all_releases.query.graphql b/app/assets/javascripts/releases/queries/all_releases.query.graphql
new file mode 100644
index 00000000000..7a99f32fdfa
--- /dev/null
+++ b/app/assets/javascripts/releases/queries/all_releases.query.graphql
@@ -0,0 +1,69 @@
+query allReleases($fullPath: ID!) {
+ project(fullPath: $fullPath) {
+ releases(first: 20) {
+ count
+ nodes {
+ name
+ tagName
+ tagPath
+ descriptionHtml
+ releasedAt
+ upcomingRelease
+ assets {
+ count
+ sources {
+ nodes {
+ format
+ url
+ }
+ }
+ links {
+ nodes {
+ id
+ name
+ url
+ directAssetUrl
+ linkType
+ external
+ }
+ }
+ }
+ evidences {
+ nodes {
+ filepath
+ collectedAt
+ sha
+ }
+ }
+ links {
+ editUrl
+ issuesUrl
+ mergeRequestsUrl
+ selfUrl
+ }
+ commit {
+ sha
+ webUrl
+ title
+ }
+ author {
+ webUrl
+ avatarUrl
+ username
+ }
+ milestones {
+ nodes {
+ id
+ title
+ description
+ webPath
+ stats {
+ totalIssuesCount
+ closedIssuesCount
+ }
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/app/assets/javascripts/releases/stores/index.js b/app/assets/javascripts/releases/stores/index.js
index 7f211145ccf..b2e93d789d7 100644
--- a/app/assets/javascripts/releases/stores/index.js
+++ b/app/assets/javascripts/releases/stores/index.js
@@ -1,8 +1,5 @@
-import Vue from 'vue';
import Vuex from 'vuex';
-Vue.use(Vuex);
-
export default ({ modules, featureFlags }) =>
new Vuex.Store({
modules,
diff --git a/app/assets/javascripts/releases/stores/modules/list/actions.js b/app/assets/javascripts/releases/stores/modules/list/actions.js
index 90fba319e9f..945b093b983 100644
--- a/app/assets/javascripts/releases/stores/modules/list/actions.js
+++ b/app/assets/javascripts/releases/stores/modules/list/actions.js
@@ -7,6 +7,8 @@ import {
parseIntPagination,
convertObjectPropsToCamelCase,
} from '~/lib/utils/common_utils';
+import allReleasesQuery from '~/releases/queries/all_releases.query.graphql';
+import { gqClient, convertGraphQLResponse } from '../../../util';
/**
* Commits a mutation to update the state while the main endpoint is being requested.
@@ -21,13 +23,31 @@ export const requestReleases = ({ commit }) => commit(types.REQUEST_RELEASES);
*
* @param {String} projectId
*/
-export const fetchReleases = ({ dispatch }, { page = '1', projectId }) => {
+export const fetchReleases = ({ dispatch, rootState, state }, { page = '1' }) => {
dispatch('requestReleases');
- api
- .releases(projectId, { page })
- .then(response => dispatch('receiveReleasesSuccess', response))
- .catch(() => dispatch('receiveReleasesError'));
+ if (
+ rootState.featureFlags.graphqlReleaseData &&
+ rootState.featureFlags.graphqlReleasesPage &&
+ rootState.featureFlags.graphqlMilestoneStats
+ ) {
+ gqClient
+ .query({
+ query: allReleasesQuery,
+ variables: {
+ fullPath: state.projectPath,
+ },
+ })
+ .then(response => {
+ dispatch('receiveReleasesSuccess', convertGraphQLResponse(response));
+ })
+ .catch(() => dispatch('receiveReleasesError'));
+ } else {
+ api
+ .releases(state.projectId, { page })
+ .then(response => dispatch('receiveReleasesSuccess', response))
+ .catch(() => dispatch('receiveReleasesError'));
+ }
};
export const receiveReleasesSuccess = ({ commit }, { data, headers }) => {
diff --git a/app/assets/javascripts/releases/stores/modules/list/index.js b/app/assets/javascripts/releases/stores/modules/list/index.js
index e4633b15a0c..0f97fa83ced 100644
--- a/app/assets/javascripts/releases/stores/modules/list/index.js
+++ b/app/assets/javascripts/releases/stores/modules/list/index.js
@@ -1,10 +1,10 @@
-import state from './state';
+import createState from './state';
import * as actions from './actions';
import mutations from './mutations';
-export default {
+export default initialState => ({
namespaced: true,
actions,
mutations,
- state,
-};
+ state: createState(initialState),
+});
diff --git a/app/assets/javascripts/releases/stores/modules/list/state.js b/app/assets/javascripts/releases/stores/modules/list/state.js
index c251f56c9c5..9fe313745fc 100644
--- a/app/assets/javascripts/releases/stores/modules/list/state.js
+++ b/app/assets/javascripts/releases/stores/modules/list/state.js
@@ -1,4 +1,16 @@
-export default () => ({
+export default ({
+ projectId,
+ projectPath,
+ documentationPath,
+ illustrationPath,
+ newReleasePath = '',
+}) => ({
+ projectId,
+ projectPath,
+ documentationPath,
+ illustrationPath,
+ newReleasePath,
+
isLoading: false,
hasError: false,
releases: [],
diff --git a/app/assets/javascripts/releases/util.js b/app/assets/javascripts/releases/util.js
index 842a423b142..d7fac7a9b65 100644
--- a/app/assets/javascripts/releases/util.js
+++ b/app/assets/javascripts/releases/util.js
@@ -1,3 +1,6 @@
+import { pick } from 'lodash';
+import createGqClient, { fetchPolicies } from '~/lib/graphql';
+import { truncateSha } from '~/lib/utils/text_utility';
import {
convertObjectPropsToCamelCase,
convertObjectPropsToSnakeCase,
@@ -39,3 +42,89 @@ export const apiJsonToRelease = json => {
return release;
};
+
+export const gqClient = createGqClient({}, { fetchPolicy: fetchPolicies.NO_CACHE });
+
+const convertScalarProperties = graphQLRelease =>
+ pick(graphQLRelease, [
+ 'name',
+ 'tagName',
+ 'tagPath',
+ 'descriptionHtml',
+ 'releasedAt',
+ 'upcomingRelease',
+ ]);
+
+const convertAssets = graphQLRelease => ({
+ assets: {
+ count: graphQLRelease.assets.count,
+ sources: [...graphQLRelease.assets.sources.nodes],
+ links: graphQLRelease.assets.links.nodes.map(l => ({
+ ...l,
+ linkType: l.linkType?.toLowerCase(),
+ })),
+ },
+});
+
+const convertEvidences = graphQLRelease => ({
+ evidences: graphQLRelease.evidences.nodes.map(e => e),
+});
+
+const convertLinks = graphQLRelease => ({
+ _links: {
+ ...graphQLRelease.links,
+ self: graphQLRelease.links?.selfUrl,
+ },
+});
+
+const convertCommit = graphQLRelease => {
+ if (!graphQLRelease.commit) {
+ return {};
+ }
+
+ return {
+ commit: {
+ shortId: truncateSha(graphQLRelease.commit.sha),
+ title: graphQLRelease.commit.title,
+ },
+ commitPath: graphQLRelease.commit.webUrl,
+ };
+};
+
+const convertAuthor = graphQLRelease => ({ author: graphQLRelease.author });
+
+const convertMilestones = graphQLRelease => ({
+ milestones: graphQLRelease.milestones.nodes.map(m => ({
+ ...m,
+ webUrl: m.webPath,
+ webPath: undefined,
+ issueStats: {
+ total: m.stats.totalIssuesCount,
+ closed: m.stats.closedIssuesCount,
+ },
+ stats: undefined,
+ })),
+});
+
+/**
+ * Converts the response from the GraphQL endpoint into the
+ * same shape as is returned from the Releases REST API.
+ *
+ * This allows the release components to use the response
+ * from either endpoint interchangeably.
+ *
+ * @param response The response received from the GraphQL endpoint
+ */
+export const convertGraphQLResponse = response => {
+ const releases = response.data.project.releases.nodes.map(r => ({
+ ...convertScalarProperties(r),
+ ...convertAssets(r),
+ ...convertEvidences(r),
+ ...convertLinks(r),
+ ...convertCommit(r),
+ ...convertAuthor(r),
+ ...convertMilestones(r),
+ }));
+
+ return { data: releases };
+};
diff --git a/app/assets/javascripts/reports/accessibility_report/store/index.js b/app/assets/javascripts/reports/accessibility_report/store/index.js
index 047964260ad..9fa2c589324 100644
--- a/app/assets/javascripts/reports/accessibility_report/store/index.js
+++ b/app/assets/javascripts/reports/accessibility_report/store/index.js
@@ -7,10 +7,11 @@ import state from './state';
Vue.use(Vuex);
-export default initialState =>
- new Vuex.Store({
- actions,
- getters,
- mutations,
- state: state(initialState),
- });
+export const getStoreConfig = initialState => ({
+ actions,
+ getters,
+ mutations,
+ state: state(initialState),
+});
+
+export default initialState => new Vuex.Store(getStoreConfig(initialState));
diff --git a/app/assets/javascripts/reports/codequality_report/store/index.js b/app/assets/javascripts/reports/codequality_report/store/index.js
index 047964260ad..9fa2c589324 100644
--- a/app/assets/javascripts/reports/codequality_report/store/index.js
+++ b/app/assets/javascripts/reports/codequality_report/store/index.js
@@ -7,10 +7,11 @@ import state from './state';
Vue.use(Vuex);
-export default initialState =>
- new Vuex.Store({
- actions,
- getters,
- mutations,
- state: state(initialState),
- });
+export const getStoreConfig = initialState => ({
+ actions,
+ getters,
+ mutations,
+ state: state(initialState),
+});
+
+export default initialState => new Vuex.Store(getStoreConfig(initialState));
diff --git a/app/assets/javascripts/reports/components/issue_status_icon.vue b/app/assets/javascripts/reports/components/issue_status_icon.vue
index d79e3ddd798..bd41b8d23f1 100644
--- a/app/assets/javascripts/reports/components/issue_status_icon.vue
+++ b/app/assets/javascripts/reports/components/issue_status_icon.vue
@@ -1,11 +1,11 @@
<script>
-import Icon from '~/vue_shared/components/icon.vue';
+import { GlIcon } from '@gitlab/ui';
import { STATUS_FAILED, STATUS_NEUTRAL, STATUS_SUCCESS } from '../constants';
export default {
name: 'IssueStatusIcon',
components: {
- Icon,
+ GlIcon,
},
props: {
status: {
@@ -49,6 +49,6 @@ export default {
}"
class="report-block-list-icon"
>
- <icon :name="iconName" :size="statusIconSize" :data-qa-selector="`status_${status}_icon`" />
+ <gl-icon :name="iconName" :size="statusIconSize" :data-qa-selector="`status_${status}_icon`" />
</div>
</template>
diff --git a/app/assets/javascripts/reports/store/getters.js b/app/assets/javascripts/reports/store/getters.js
index 6345be69f6f..d49e5760b3f 100644
--- a/app/assets/javascripts/reports/store/getters.js
+++ b/app/assets/javascripts/reports/store/getters.js
@@ -1,6 +1,5 @@
import { LOADING, ERROR, SUCCESS, STATUS_FAILED } from '../constants';
-// eslint-disable-next-line import/prefer-default-export
export const summaryStatus = state => {
if (state.isLoading) {
return LOADING;
diff --git a/app/assets/javascripts/reports/store/index.js b/app/assets/javascripts/reports/store/index.js
index 467c692b438..a2edfa94a48 100644
--- a/app/assets/javascripts/reports/store/index.js
+++ b/app/assets/javascripts/reports/store/index.js
@@ -7,10 +7,11 @@ import state from './state';
Vue.use(Vuex);
-export default () =>
- new Vuex.Store({
- actions,
- mutations,
- getters,
- state: state(),
- });
+export const getStoreConfig = () => ({
+ actions,
+ mutations,
+ getters,
+ state: state(),
+});
+
+export default () => new Vuex.Store(getStoreConfig());
diff --git a/app/assets/javascripts/repository/components/breadcrumbs.vue b/app/assets/javascripts/repository/components/breadcrumbs.vue
index 368fa029d07..74437f286b4 100644
--- a/app/assets/javascripts/repository/components/breadcrumbs.vue
+++ b/app/assets/javascripts/repository/components/breadcrumbs.vue
@@ -4,10 +4,10 @@ import {
GlDeprecatedDropdownDivider,
GlDeprecatedDropdownHeader,
GlDeprecatedDropdownItem,
+ GlIcon,
} from '@gitlab/ui';
import { joinPaths, escapeFileUrl } from '~/lib/utils/url_utility';
import { __ } from '../../locale';
-import Icon from '../../vue_shared/components/icon.vue';
import getRefMixin from '../mixins/get_ref';
import projectShortPathQuery from '../queries/project_short_path.query.graphql';
import projectPathQuery from '../queries/project_path.query.graphql';
@@ -24,7 +24,7 @@ export default {
GlDeprecatedDropdownDivider,
GlDeprecatedDropdownHeader,
GlDeprecatedDropdownItem,
- Icon,
+ GlIcon,
},
apollo: {
projectShortPath: {
@@ -249,8 +249,8 @@ export default {
<gl-deprecated-dropdown toggle-class="add-to-tree qa-add-to-tree ml-1">
<template #button-content>
<span class="sr-only">{{ __('Add to tree') }}</span>
- <icon name="plus" :size="16" class="float-left" />
- <icon name="chevron-down" :size="16" class="float-left" />
+ <gl-icon name="plus" :size="16" class="float-left" />
+ <gl-icon name="chevron-down" :size="16" class="float-left" />
</template>
<template v-for="(item, i) in dropdownItems">
<component :is="getComponent(item.type)" :key="i" v-bind="item.attrs">
diff --git a/app/assets/javascripts/repository/components/last_commit.vue b/app/assets/javascripts/repository/components/last_commit.vue
index 3337ce6c6df..59831890a4e 100644
--- a/app/assets/javascripts/repository/components/last_commit.vue
+++ b/app/assets/javascripts/repository/components/last_commit.vue
@@ -1,8 +1,8 @@
<script>
-import { GlTooltipDirective, GlLink, GlDeprecatedButton, GlLoadingIcon } from '@gitlab/ui';
+/* eslint-disable vue/no-v-html */
+import { GlTooltipDirective, GlLink, GlDeprecatedButton, GlLoadingIcon, GlIcon } from '@gitlab/ui';
import defaultAvatarUrl from 'images/no_avatar.png';
import { sprintf, s__ } from '~/locale';
-import Icon from '../../vue_shared/components/icon.vue';
import UserAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue';
import TimeagoTooltip from '../../vue_shared/components/time_ago_tooltip.vue';
import CiIcon from '../../vue_shared/components/ci_icon.vue';
@@ -13,7 +13,7 @@ import pathLastCommitQuery from '../queries/path_last_commit.query.graphql';
export default {
components: {
- Icon,
+ GlIcon,
UserAvatarLink,
TimeagoTooltip,
ClipboardButton,
@@ -130,7 +130,7 @@ export default {
class="text-expander"
@click="toggleShowDescription"
>
- <icon name="ellipsis_h" :size="10" />
+ <gl-icon name="ellipsis_h" :size="10" />
</gl-deprecated-button>
<div class="committer">
<gl-link
diff --git a/app/assets/javascripts/repository/components/preview/index.vue b/app/assets/javascripts/repository/components/preview/index.vue
index 013092ffefd..eca53f73a7f 100644
--- a/app/assets/javascripts/repository/components/preview/index.vue
+++ b/app/assets/javascripts/repository/components/preview/index.vue
@@ -1,4 +1,5 @@
<script>
+/* eslint-disable vue/no-v-html */
import $ from 'jquery';
import '~/behaviors/markdown/render_gfm';
import { GlLink, GlLoadingIcon } from '@gitlab/ui';
diff --git a/app/assets/javascripts/repository/components/table/index.vue b/app/assets/javascripts/repository/components/table/index.vue
index d0cc617d755..c6652c57c1f 100644
--- a/app/assets/javascripts/repository/components/table/index.vue
+++ b/app/assets/javascripts/repository/components/table/index.vue
@@ -1,5 +1,5 @@
<script>
-import { GlSkeletonLoading } from '@gitlab/ui';
+import { GlDeprecatedSkeletonLoading as GlSkeletonLoading, GlButton } from '@gitlab/ui';
import { sprintf, __ } from '../../../locale';
import getRefMixin from '../../mixins/get_ref';
import projectPathQuery from '../../queries/project_path.query.graphql';
@@ -13,6 +13,7 @@ export default {
TableHeader,
TableRow,
ParentRow,
+ GlButton,
},
mixins: [getRefMixin],
apollo: {
@@ -39,6 +40,10 @@ export default {
required: false,
default: '',
},
+ hasMore: {
+ type: Boolean,
+ required: true,
+ },
},
data() {
return {
@@ -65,6 +70,11 @@ export default {
return !this.isLoading && ['', '/'].indexOf(this.path) === -1;
},
},
+ methods: {
+ showMore() {
+ this.$emit('showMore');
+ },
+ },
};
</script>
@@ -110,6 +120,20 @@ export default {
<td><gl-skeleton-loading :lines="1" class="ml-auto h-auto w-50" /></td>
</tr>
</template>
+ <template v-if="hasMore">
+ <tr>
+ <td align="center" colspan="3" class="gl-p-0!">
+ <gl-button
+ variant="link"
+ class="gl-display-flex gl-w-full gl-py-4!"
+ :loading="isLoading"
+ @click="showMore"
+ >
+ {{ s__('ProjectFileTree|Show more') }}
+ </gl-button>
+ </td>
+ </tr>
+ </template>
</tbody>
</table>
</div>
diff --git a/app/assets/javascripts/repository/components/table/row.vue b/app/assets/javascripts/repository/components/table/row.vue
index d2fef6693e2..d749a8c0dee 100644
--- a/app/assets/javascripts/repository/components/table/row.vue
+++ b/app/assets/javascripts/repository/components/table/row.vue
@@ -1,9 +1,10 @@
<script>
+/* eslint-disable vue/no-v-html */
import { escapeRegExp } from 'lodash';
import {
GlBadge,
GlLink,
- GlSkeletonLoading,
+ GlDeprecatedSkeletonLoading as GlSkeletonLoading,
GlTooltipDirective,
GlLoadingIcon,
GlIcon,
diff --git a/app/assets/javascripts/repository/components/tree_content.vue b/app/assets/javascripts/repository/components/tree_content.vue
index fe3065a2145..365b6cbb550 100644
--- a/app/assets/javascripts/repository/components/tree_content.vue
+++ b/app/assets/javascripts/repository/components/tree_content.vue
@@ -1,5 +1,4 @@
<script>
-import { GlButton } from '@gitlab/ui';
import { deprecatedCreateFlash as createFlash } from '~/flash';
import { __ } from '../../locale';
import FileTable from './table/index.vue';
@@ -17,7 +16,6 @@ export default {
components: {
FileTable,
FilePreview,
- GlButton,
},
mixins: [getRefMixin],
apollo: {
@@ -127,7 +125,7 @@ export default {
.concat(data.trees.pageInfo, data.submodules.pageInfo, data.blobs.pageInfo)
.find(({ hasNextPage }) => hasNextPage);
},
- showMore() {
+ handleShowMore() {
this.clickedShowMore = true;
this.fetchFiles();
},
@@ -142,20 +140,9 @@ export default {
:entries="entries"
:is-loading="isLoadingFiles"
:loading-path="loadingPath"
+ :has-more="hasShowMore"
+ @showMore="handleShowMore"
/>
- <div
- v-if="hasShowMore"
- class="gl-border-1 gl-border-gray-100 gl-rounded-base gl-border-t-none gl-border-b-solid gl-border-l-solid gl-border-r-solid gl-rounded-top-right-none gl-rounded-top-left-none gl-mt-n1"
- >
- <gl-button
- variant="link"
- class="gl-display-flex gl-w-full gl-py-4!"
- :loading="isLoadingFiles"
- @click="showMore"
- >
- {{ s__('ProjectFileTree|Show more') }}
- </gl-button>
- </div>
<file-preview v-if="readme" :blob="readme" />
</div>
</template>
diff --git a/app/assets/javascripts/repository/components/web_ide_link.vue b/app/assets/javascripts/repository/components/web_ide_link.vue
deleted file mode 100644
index 6549d5a3878..00000000000
--- a/app/assets/javascripts/repository/components/web_ide_link.vue
+++ /dev/null
@@ -1,47 +0,0 @@
-<script>
-import TreeActionLink from './tree_action_link.vue';
-import { __ } from '~/locale';
-import { webIDEUrl } from '~/lib/utils/url_utility';
-
-export default {
- components: {
- TreeActionLink,
- },
- props: {
- projectPath: {
- type: String,
- required: true,
- },
- refSha: {
- type: String,
- required: true,
- },
- canPushCode: {
- type: Boolean,
- required: false,
- default: true,
- },
- forkPath: {
- type: String,
- required: false,
- default: '',
- },
- },
- computed: {
- showLinkToFork() {
- return !this.canPushCode && this.forkPath;
- },
- text() {
- return this.showLinkToFork ? __('Edit fork in Web IDE') : __('Web IDE');
- },
- path() {
- const path = this.showLinkToFork ? this.forkPath : this.projectPath;
- return webIDEUrl(`/${path}/edit/${this.refSha}/-/${this.$route.params.path || ''}`);
- },
- },
-};
-</script>
-
-<template>
- <tree-action-link :path="path" :text="text" data-qa-selector="web_ide_button" />
-</template>
diff --git a/app/assets/javascripts/repository/graphql.js b/app/assets/javascripts/repository/graphql.js
index 450a1571165..8dd18027945 100644
--- a/app/assets/javascripts/repository/graphql.js
+++ b/app/assets/javascripts/repository/graphql.js
@@ -58,6 +58,7 @@ const defaultClient = createDefaultClient(
/* eslint-enable @gitlab/require-i18n-strings */
},
},
+ assumeImmutableResults: true,
},
);
diff --git a/app/assets/javascripts/repository/index.js b/app/assets/javascripts/repository/index.js
index 187bbfed125..7f72524b6fe 100644
--- a/app/assets/javascripts/repository/index.js
+++ b/app/assets/javascripts/repository/index.js
@@ -1,30 +1,22 @@
import Vue from 'vue';
-import { escapeFileUrl } from '../lib/utils/url_utility';
+import { escapeFileUrl, joinPaths, webIDEUrl } from '../lib/utils/url_utility';
import createRouter from './router';
import App from './components/app.vue';
import Breadcrumbs from './components/breadcrumbs.vue';
import LastCommit from './components/last_commit.vue';
import TreeActionLink from './components/tree_action_link.vue';
-import WebIdeLink from './components/web_ide_link.vue';
+import WebIdeLink from '~/vue_shared/components/web_ide_link.vue';
import DirectoryDownloadLinks from './components/directory_download_links.vue';
import apolloProvider from './graphql';
import { setTitle } from './utils/title';
import { updateFormAction } from './utils/dom';
-import { parseBoolean } from '../lib/utils/common_utils';
+import { convertObjectPropsToCamelCase, parseBoolean } from '../lib/utils/common_utils';
import { __ } from '../locale';
export default function setupVueRepositoryList() {
const el = document.getElementById('js-tree-list');
const { dataset } = el;
- const {
- canPushCode,
- projectPath,
- projectShortPath,
- forkPath,
- ref,
- escapedRef,
- fullName,
- } = dataset;
+ const { projectPath, projectShortPath, ref, escapedRef, fullName } = dataset;
const router = createRouter(projectPath, escapedRef);
apolloProvider.clients.defaultClient.cache.writeData({
@@ -121,6 +113,10 @@ export default function setupVueRepositoryList() {
const webIdeLinkEl = document.getElementById('js-tree-web-ide-link');
if (webIdeLinkEl) {
+ const { ideBasePath, ...options } = convertObjectPropsToCamelCase(
+ JSON.parse(webIdeLinkEl.dataset.options),
+ );
+
// eslint-disable-next-line no-new
new Vue({
el: webIdeLinkEl,
@@ -128,10 +124,10 @@ export default function setupVueRepositoryList() {
render(h) {
return h(WebIdeLink, {
props: {
- projectPath,
- refSha: ref,
- forkPath,
- canPushCode: parseBoolean(canPushCode),
+ webIdeUrl: webIDEUrl(
+ joinPaths('/', ideBasePath, 'edit', ref, '-', this.$route.params.path || '', '/'),
+ ),
+ ...options,
},
});
},
diff --git a/app/assets/javascripts/repository/log_tree.js b/app/assets/javascripts/repository/log_tree.js
index 704dd88aabe..361e0b62bb7 100644
--- a/app/assets/javascripts/repository/log_tree.js
+++ b/app/assets/javascripts/repository/log_tree.js
@@ -1,3 +1,4 @@
+import produce from 'immer';
import { normalizeData } from 'ee_else_ce/repository/utils/commit';
import axios from '~/lib/utils/axios_utils';
import commitsQuery from './queries/commits.query.graphql';
@@ -34,16 +35,18 @@ export function fetchLogsTree(client, path, offset, resolver = null) {
params: { format: 'json', offset },
},
)
- .then(({ data, headers }) => {
+ .then(({ data: newData, headers }) => {
const headerLogsOffset = headers['more-logs-offset'];
- const { commits } = client.readQuery({ query: commitsQuery });
- const newCommitData = [...commits, ...normalizeData(data, path)];
+ const sourceData = client.readQuery({ query: commitsQuery });
+ const data = produce(sourceData, draftState => {
+ draftState.commits.push(...normalizeData(newData, path));
+ });
client.writeQuery({
query: commitsQuery,
- data: { commits: newCommitData },
+ data,
});
- resolvers.forEach(r => resolveCommit(newCommitData, path, r));
+ resolvers.forEach(r => resolveCommit(data.commits, path, r));
fetchpromise = null;
diff --git a/app/assets/javascripts/repository/router.js b/app/assets/javascripts/repository/router.js
index c5646c32850..38a596e229e 100644
--- a/app/assets/javascripts/repository/router.js
+++ b/app/assets/javascripts/repository/router.js
@@ -1,5 +1,6 @@
import Vue from 'vue';
import VueRouter from 'vue-router';
+import { escapeRegExp } from 'lodash';
import { joinPaths } from '../lib/utils/url_utility';
import IndexPage from './pages/index.vue';
import TreePage from './pages/tree.vue';
@@ -27,7 +28,7 @@ export default function createRouter(base, baseRef) {
{
name: 'treePath',
// Support without decoding as well just in case the ref doesn't need to be decoded
- path: `(/-)?/tree/${baseRef}/:path*`,
+ path: `(/-)?/tree/${escapeRegExp(baseRef)}/:path*`,
...treePathRoute,
},
{
diff --git a/app/assets/javascripts/repository/utils/commit.js b/app/assets/javascripts/repository/utils/commit.js
index 90ac01c5874..0704ac1627f 100644
--- a/app/assets/javascripts/repository/utils/commit.js
+++ b/app/assets/javascripts/repository/utils/commit.js
@@ -1,4 +1,3 @@
-// eslint-disable-next-line import/prefer-default-export
export function normalizeData(data, path, extra = () => {}) {
return data.map(d => ({
sha: d.commit.id,
diff --git a/app/assets/javascripts/repository/utils/icon.js b/app/assets/javascripts/repository/utils/icon.js
index 661ebb6edfc..47b045c7eaf 100644
--- a/app/assets/javascripts/repository/utils/icon.js
+++ b/app/assets/javascripts/repository/utils/icon.js
@@ -88,7 +88,6 @@ const fileTypeIcons = [
},
];
-// eslint-disable-next-line import/prefer-default-export
export const getIconName = (type, path) => {
if (entryTypeIcons[type]) return entryTypeIcons[type];
diff --git a/app/assets/javascripts/repository/utils/readme.js b/app/assets/javascripts/repository/utils/readme.js
index 5b62271b02e..50692779b1a 100644
--- a/app/assets/javascripts/repository/utils/readme.js
+++ b/app/assets/javascripts/repository/utils/readme.js
@@ -28,5 +28,4 @@ const isPlainReadme = file => {
return re.test(file.name);
};
-// eslint-disable-next-line import/prefer-default-export
export const readmeFile = blobs => blobs.find(isRichReadme) || blobs.find(isPlainReadme);
diff --git a/app/assets/javascripts/search/state_filter/components/state_filter.vue b/app/assets/javascripts/search/state_filter/components/state_filter.vue
new file mode 100644
index 00000000000..f08adaf8c83
--- /dev/null
+++ b/app/assets/javascripts/search/state_filter/components/state_filter.vue
@@ -0,0 +1,94 @@
+<script>
+import { GlDropdown, GlDropdownItem, GlDropdownDivider } from '@gitlab/ui';
+import {
+ FILTER_STATES,
+ SCOPES,
+ FILTER_STATES_BY_SCOPE,
+ FILTER_HEADER,
+ FILTER_TEXT,
+} from '../constants';
+import { setUrlParams, visitUrl } from '~/lib/utils/url_utility';
+
+const FILTERS_ARRAY = Object.values(FILTER_STATES);
+
+export default {
+ name: 'StateFilter',
+ components: {
+ GlDropdown,
+ GlDropdownItem,
+ GlDropdownDivider,
+ },
+ props: {
+ scope: {
+ type: String,
+ required: true,
+ },
+ state: {
+ type: String,
+ required: false,
+ default: FILTER_STATES.ANY.value,
+ validator: v => FILTERS_ARRAY.some(({ value }) => value === v),
+ },
+ },
+ computed: {
+ selectedFilterText() {
+ const filter = FILTERS_ARRAY.find(({ value }) => value === this.selectedFilter);
+ if (!filter || filter === FILTER_STATES.ANY) {
+ return FILTER_TEXT;
+ }
+
+ return filter.label;
+ },
+ showDropdown() {
+ return Object.values(SCOPES).includes(this.scope);
+ },
+ selectedFilter: {
+ get() {
+ if (FILTERS_ARRAY.some(({ value }) => value === this.state)) {
+ return this.state;
+ }
+
+ return FILTER_STATES.ANY.value;
+ },
+ set(state) {
+ visitUrl(setUrlParams({ state }));
+ },
+ },
+ },
+ methods: {
+ dropDownItemClass(filter) {
+ return {
+ 'gl-border-b-solid gl-border-b-gray-100 gl-border-b-1 gl-pb-2! gl-mb-2':
+ filter === FILTER_STATES.ANY,
+ };
+ },
+ isFilterSelected(filter) {
+ return filter === this.selectedFilter;
+ },
+ handleFilterChange(state) {
+ this.selectedFilter = state;
+ },
+ },
+ filterStates: FILTER_STATES,
+ filterHeader: FILTER_HEADER,
+ filtersByScope: FILTER_STATES_BY_SCOPE,
+};
+</script>
+
+<template>
+ <gl-dropdown v-if="showDropdown" :text="selectedFilterText" class="col-sm-3 gl-pt-4 gl-pl-0">
+ <header class="gl-text-center gl-font-weight-bold gl-font-lg">
+ {{ $options.filterHeader }}
+ </header>
+ <gl-dropdown-divider />
+ <gl-dropdown-item
+ v-for="filter in $options.filtersByScope[scope]"
+ :key="filter.value"
+ :is-check-item="true"
+ :is-checked="isFilterSelected(filter.value)"
+ :class="dropDownItemClass(filter)"
+ @click="handleFilterChange(filter.value)"
+ >{{ filter.label }}</gl-dropdown-item
+ >
+ </gl-dropdown>
+</template>
diff --git a/app/assets/javascripts/search/state_filter/constants.js b/app/assets/javascripts/search/state_filter/constants.js
new file mode 100644
index 00000000000..2f11cab9044
--- /dev/null
+++ b/app/assets/javascripts/search/state_filter/constants.js
@@ -0,0 +1,39 @@
+import { __ } from '~/locale';
+
+export const FILTER_HEADER = __('Status');
+
+export const FILTER_TEXT = __('Any Status');
+
+export const FILTER_STATES = {
+ ANY: {
+ label: __('Any'),
+ value: 'all',
+ },
+ OPEN: {
+ label: __('Open'),
+ value: 'opened',
+ },
+ CLOSED: {
+ label: __('Closed'),
+ value: 'closed',
+ },
+ MERGED: {
+ label: __('Merged'),
+ value: 'merged',
+ },
+};
+
+export const SCOPES = {
+ ISSUES: 'issues',
+ MERGE_REQUESTS: 'merge_requests',
+};
+
+export const FILTER_STATES_BY_SCOPE = {
+ [SCOPES.ISSUES]: [FILTER_STATES.ANY, FILTER_STATES.OPEN, FILTER_STATES.CLOSED],
+ [SCOPES.MERGE_REQUESTS]: [
+ FILTER_STATES.ANY,
+ FILTER_STATES.OPEN,
+ FILTER_STATES.MERGED,
+ FILTER_STATES.CLOSED,
+ ],
+};
diff --git a/app/assets/javascripts/search/state_filter/index.js b/app/assets/javascripts/search/state_filter/index.js
new file mode 100644
index 00000000000..13708574cfb
--- /dev/null
+++ b/app/assets/javascripts/search/state_filter/index.js
@@ -0,0 +1,34 @@
+import Vue from 'vue';
+import Translate from '~/vue_shared/translate';
+import StateFilter from './components/state_filter.vue';
+
+Vue.use(Translate);
+
+export default () => {
+ const el = document.getElementById('js-search-filter-by-state');
+
+ if (!el) return false;
+
+ return new Vue({
+ el,
+ components: {
+ StateFilter,
+ },
+ data() {
+ const { dataset } = this.$options.el;
+ return {
+ scope: dataset.scope,
+ state: dataset.state,
+ };
+ },
+
+ render(createElement) {
+ return createElement('state-filter', {
+ props: {
+ scope: this.scope,
+ state: this.state,
+ },
+ });
+ },
+ });
+};
diff --git a/app/assets/javascripts/search_autocomplete.js b/app/assets/javascripts/search_autocomplete.js
index 990c8faf253..7073b9ca12d 100644
--- a/app/assets/javascripts/search_autocomplete.js
+++ b/app/assets/javascripts/search_autocomplete.js
@@ -13,6 +13,7 @@ import {
spriteIcon,
} from './lib/utils/common_utils';
import Tracking from '~/tracking';
+import initDeprecatedJQueryDropdown from '~/deprecated_jquery_dropdown';
/**
* Search input in top navigation bar.
@@ -119,7 +120,7 @@ export class SearchAutocomplete {
}
createAutocomplete() {
- return this.searchInput.glDropdown({
+ return initDeprecatedJQueryDropdown(this.searchInput, {
filterInputBlur: false,
filterable: true,
filterRemote: true,
@@ -134,6 +135,7 @@ export class SearchAutocomplete {
data: this.getData.bind(this),
selectable: true,
clicked: this.onClick.bind(this),
+ trackSuggestionClickedLabel: 'search_autocomplete_suggestion',
});
}
@@ -145,10 +147,10 @@ export class SearchAutocomplete {
if (!term) {
const contents = this.getCategoryContents();
if (contents) {
- const glDropdownInstance = this.searchInput.data('glDropdown');
+ const deprecatedJQueryDropdownInstance = this.searchInput.data('deprecatedJQueryDropdown');
- if (glDropdownInstance) {
- glDropdownInstance.filter.options.callback(contents);
+ if (deprecatedJQueryDropdownInstance) {
+ deprecatedJQueryDropdownInstance.filter.options.callback(contents);
}
this.enableAutocomplete();
}
@@ -463,7 +465,7 @@ export class SearchAutocomplete {
}
highlightFirstRow() {
- this.searchInput.data('glDropdown').highlightRowAtIndex(null, 0);
+ this.searchInput.data('deprecatedJQueryDropdown').highlightRowAtIndex(null, 0);
}
getAvatar(item) {
diff --git a/app/assets/javascripts/self_monitor/components/self_monitor_form.vue b/app/assets/javascripts/self_monitor/components/self_monitor_form.vue
index adc7d3c5ebd..1ccf5e9e032 100644
--- a/app/assets/javascripts/self_monitor/components/self_monitor_form.vue
+++ b/app/assets/javascripts/self_monitor/components/self_monitor_form.vue
@@ -1,4 +1,5 @@
<script>
+/* eslint-disable vue/no-v-html */
import Vue from 'vue';
import { GlFormGroup, GlDeprecatedButton, GlModal, GlToast, GlToggle } from '@gitlab/ui';
import { mapState, mapActions } from 'vuex';
diff --git a/app/assets/javascripts/serverless/components/functions.vue b/app/assets/javascripts/serverless/components/functions.vue
index c44a14f1785..e15549f5864 100644
--- a/app/assets/javascripts/serverless/components/functions.vue
+++ b/app/assets/javascripts/serverless/components/functions.vue
@@ -1,4 +1,5 @@
<script>
+/* eslint-disable vue/no-v-html */
import { mapState, mapActions, mapGetters } from 'vuex';
import { GlLink, GlLoadingIcon } from '@gitlab/ui';
import { sprintf, s__ } from '~/locale';
diff --git a/app/assets/javascripts/set_status_modal/event_hub.js b/app/assets/javascripts/set_status_modal/event_hub.js
deleted file mode 100644
index e31806ad199..00000000000
--- a/app/assets/javascripts/set_status_modal/event_hub.js
+++ /dev/null
@@ -1,3 +0,0 @@
-import createEventHub from '~/helpers/event_hub_factory';
-
-export default createEventHub();
diff --git a/app/assets/javascripts/set_status_modal/set_status_modal_trigger.vue b/app/assets/javascripts/set_status_modal/set_status_modal_trigger.vue
deleted file mode 100644
index 0e8b6d93f42..00000000000
--- a/app/assets/javascripts/set_status_modal/set_status_modal_trigger.vue
+++ /dev/null
@@ -1,27 +0,0 @@
-<script>
-import { s__ } from '~/locale';
-import eventHub from './event_hub';
-
-export default {
- props: {
- hasStatus: {
- type: Boolean,
- required: true,
- },
- },
- computed: {
- buttonText() {
- return this.hasStatus ? s__('SetStatusModal|Edit status') : s__('SetStatusModal|Set status');
- },
- },
- methods: {
- openModal() {
- eventHub.$emit('openModal');
- },
- },
-};
-</script>
-
-<template>
- <button type="button" class="btn menu-item" @click="openModal">{{ buttonText }}</button>
-</template>
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 cb047530c17..09e893ff285 100644
--- a/app/assets/javascripts/set_status_modal/set_status_modal_wrapper.vue
+++ b/app/assets/javascripts/set_status_modal/set_status_modal_wrapper.vue
@@ -1,12 +1,11 @@
<script>
+/* eslint-disable vue/no-v-html */
import $ from 'jquery';
import GfmAutoComplete from 'ee_else_ce/gfm_auto_complete';
-import { GlModal, GlTooltipDirective } from '@gitlab/ui';
+import { GlModal, GlTooltipDirective, GlIcon } from '@gitlab/ui';
import { deprecatedCreateFlash as createFlash } from '~/flash';
-import Icon from '~/vue_shared/components/icon.vue';
import { __, s__ } from '~/locale';
import Api from '~/api';
-import eventHub from './event_hub';
import EmojiMenuInModal from './emoji_menu_in_modal';
import * as Emoji from '~/emoji';
@@ -14,7 +13,7 @@ const emojiMenuClass = 'js-modal-status-emoji-menu';
export default {
components: {
- Icon,
+ GlIcon,
GlModal,
},
directives: {
@@ -48,15 +47,12 @@ export default {
},
},
mounted() {
- eventHub.$on('openModal', this.openModal);
+ this.$root.$emit('bv::show::modal', this.modalId);
},
beforeDestroy() {
this.emojiMenu.destroy();
},
methods: {
- openModal() {
- this.$root.$emit('bv::show::modal', this.modalId);
- },
closeModal() {
this.$root.$emit('bv::hide::modal', this.modalId);
},
@@ -196,9 +192,9 @@ export default {
v-show="noEmoji"
class="js-no-emoji-placeholder no-emoji-placeholder position-relative"
>
- <icon name="slight-smile" class="award-control-icon-neutral" />
- <icon name="smiley" class="award-control-icon-positive" />
- <icon name="smile" class="award-control-icon-super-positive" />
+ <gl-icon name="slight-smile" class="award-control-icon-neutral" />
+ <gl-icon name="smiley" class="award-control-icon-positive" />
+ <gl-icon name="smile" class="award-control-icon-super-positive" />
</span>
</button>
</span>
@@ -223,7 +219,7 @@ export default {
class="js-clear-user-status-button clear-user-status btn"
@click="clearStatusInputs()"
>
- <icon name="close" />
+ <gl-icon name="close" />
</button>
</span>
</div>
diff --git a/app/assets/javascripts/shared/milestones/form.js b/app/assets/javascripts/shared/milestones/form.js
index 0ff84dc4667..9ee02f923d5 100644
--- a/app/assets/javascripts/shared/milestones/form.js
+++ b/app/assets/javascripts/shared/milestones/form.js
@@ -16,5 +16,6 @@ export default (initGFM = true) => {
milestones: initGFM,
labels: initGFM,
snippets: initGFM,
+ vulnerabilities: initGFM,
});
};
diff --git a/app/assets/javascripts/sidebar/components/assignees/assignee_avatar_link.vue b/app/assets/javascripts/sidebar/components/assignees/assignee_avatar_link.vue
index 9a60172db2e..878b331fb3c 100644
--- a/app/assets/javascripts/sidebar/components/assignees/assignee_avatar_link.vue
+++ b/app/assets/javascripts/sidebar/components/assignees/assignee_avatar_link.vue
@@ -59,7 +59,7 @@ export default {
};
},
assigneeUrl() {
- return this.user.web_url;
+ return this.user.web_url || this.user.webUrl;
},
},
};
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 7375855f899..eabd4d88d52 100644
--- a/app/assets/javascripts/sidebar/components/assignees/collapsed_assignee_list.vue
+++ b/app/assets/javascripts/sidebar/components/assignees/collapsed_assignee_list.vue
@@ -1,5 +1,5 @@
<script>
-import { GlTooltipDirective } from '@gitlab/ui';
+import { GlIcon, GlTooltipDirective } from '@gitlab/ui';
import { __, sprintf } from '~/locale';
import CollapsedAssignee from './collapsed_assignee.vue';
@@ -12,6 +12,7 @@ export default {
},
components: {
CollapsedAssignee,
+ GlIcon,
},
props: {
users: {
@@ -102,7 +103,7 @@ export default {
:title="tooltipTitle"
class="sidebar-collapsed-icon sidebar-collapsed-user"
>
- <i v-if="hasNoUsers" :aria-label="__('None')" class="fa fa-user"> </i>
+ <gl-icon v-if="hasNoUsers" name="user" :aria-label="__('None')" />
<collapsed-assignee
v-for="user in collapsedUsers"
:key="user.id"
diff --git a/app/assets/javascripts/sidebar/components/assignees/issuable_assignees.vue b/app/assets/javascripts/sidebar/components/assignees/issuable_assignees.vue
new file mode 100644
index 00000000000..4697d85472b
--- /dev/null
+++ b/app/assets/javascripts/sidebar/components/assignees/issuable_assignees.vue
@@ -0,0 +1,37 @@
+<script>
+import { n__ } from '~/locale';
+import UncollapsedAssigneeList from '~/sidebar/components/assignees/uncollapsed_assignee_list.vue';
+
+export default {
+ components: {
+ UncollapsedAssigneeList,
+ },
+ inject: ['rootPath'],
+ props: {
+ users: {
+ type: Array,
+ required: true,
+ },
+ },
+ computed: {
+ assigneesText() {
+ return n__('Assignee', '%d Assignees', this.users.length);
+ },
+ emptyUsers() {
+ return this.users.length === 0;
+ },
+ },
+};
+</script>
+
+<template>
+ <div class="gl-display-flex gl-flex-direction-column">
+ <label data-testid="assigneeLabel">{{ assigneesText }}</label>
+ <div v-if="emptyUsers" data-testid="none">
+ <span>
+ {{ __('None') }}
+ </span>
+ </div>
+ <uncollapsed-assignee-list v-else :users="users" :root-path="rootPath" />
+ </div>
+</template>
diff --git a/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.vue b/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.vue
index 14c14d0bad1..2f714ac3847 100644
--- a/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.vue
+++ b/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.vue
@@ -62,7 +62,7 @@ export default {
this.addAssignee = this.store.addAssignee.bind(this.store);
this.removeAllAssignees = this.store.removeAllAssignees.bind(this.store);
- // Get events from glDropdown
+ // Get events from deprecatedJQueryDropdown
eventHub.$on('sidebar.removeAssignee', this.removeAssignee);
eventHub.$on('sidebar.addAssignee', this.addAssignee);
eventHub.$on('sidebar.removeAllAssignees', this.removeAllAssignees);
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 fed9e5886c0..95934c0ef2a 100644
--- a/app/assets/javascripts/sidebar/components/assignees/uncollapsed_assignee_list.vue
+++ b/app/assets/javascripts/sidebar/components/assignees/uncollapsed_assignee_list.vue
@@ -73,9 +73,9 @@ export default {
:root-path="rootPath"
:issuable-type="issuableType"
>
- <div class="ml-2">
- <span class="author"> {{ user.name }} </span>
- <span class="username"> {{ username }} </span>
+ <div class="ml-2 gl-line-height-normal">
+ <div>{{ user.name }}</div>
+ <div>{{ username }}</div>
</div>
</assignee-avatar-link>
<div v-else>
diff --git a/app/assets/javascripts/sidebar/components/confidential/confidential_issue_sidebar.vue b/app/assets/javascripts/sidebar/components/confidential/confidential_issue_sidebar.vue
index c6f7d5e44ad..2530cb77acd 100644
--- a/app/assets/javascripts/sidebar/components/confidential/confidential_issue_sidebar.vue
+++ b/app/assets/javascripts/sidebar/components/confidential/confidential_issue_sidebar.vue
@@ -1,18 +1,17 @@
<script>
import { mapState } from 'vuex';
+import { GlIcon, GlTooltipDirective } from '@gitlab/ui';
import { __, sprintf } from '~/locale';
-import tooltip from '~/vue_shared/directives/tooltip';
-import Icon from '~/vue_shared/components/icon.vue';
import eventHub from '~/sidebar/event_hub';
import EditForm from './edit_form.vue';
export default {
components: {
EditForm,
- Icon,
+ GlIcon,
},
directives: {
- tooltip,
+ GlTooltip: GlTooltipDirective,
},
props: {
fullPath: {
@@ -73,15 +72,12 @@ export default {
<div class="block issuable-sidebar-item confidentiality">
<div
ref="collapseIcon"
- v-tooltip
+ v-gl-tooltip.viewport.left
:title="tooltipLabel"
class="sidebar-collapsed-icon"
- data-container="body"
- data-placement="left"
- data-boundary="viewport"
@click="toggleForm"
>
- <icon :name="confidentialityIcon" aria-hidden="true" />
+ <gl-icon :name="confidentialityIcon" aria-hidden="true" />
</div>
<div class="title hide-collapsed">
{{ __('Confidentiality') }}
@@ -105,11 +101,11 @@ export default {
:issuable-type="issuableType"
/>
<div v-if="!confidential" class="no-value sidebar-item-value" data-testid="not-confidential">
- <icon :size="16" name="eye" aria-hidden="true" class="sidebar-item-icon inline" />
+ <gl-icon :size="16" name="eye" aria-hidden="true" class="sidebar-item-icon inline" />
{{ __('Not confidential') }}
</div>
<div v-else class="value sidebar-item-value hide-collapsed">
- <icon
+ <gl-icon
:size="16"
name="eye-slash"
aria-hidden="true"
diff --git a/app/assets/javascripts/sidebar/components/labels/sidebar_labels.vue b/app/assets/javascripts/sidebar/components/labels/sidebar_labels.vue
new file mode 100644
index 00000000000..d7be8927c29
--- /dev/null
+++ b/app/assets/javascripts/sidebar/components/labels/sidebar_labels.vue
@@ -0,0 +1,97 @@
+<script>
+import $ from 'jquery';
+import { difference, union } from 'lodash';
+import { mapState, mapActions } from 'vuex';
+import flash from '~/flash';
+import axios from '~/lib/utils/axios_utils';
+import { __ } from '~/locale';
+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';
+
+export default {
+ components: {
+ LabelsSelect,
+ },
+ variant: DropdownVariant.Sidebar,
+ inject: [
+ 'allowLabelCreate',
+ 'allowLabelEdit',
+ 'allowScopedLabels',
+ 'iid',
+ 'initiallySelectedLabels',
+ 'issuableType',
+ 'labelsFetchPath',
+ 'labelsManagePath',
+ 'labelsUpdatePath',
+ 'projectIssuesPath',
+ 'projectPath',
+ ],
+ data: () => ({
+ labelsSelectInProgress: false,
+ }),
+ computed: {
+ ...mapState(['selectedLabels']),
+ },
+ mounted() {
+ this.setInitialState({
+ selectedLabels: this.initiallySelectedLabels,
+ });
+ },
+ methods: {
+ ...mapActions(['setInitialState', 'replaceSelectedLabels']),
+ handleDropdownClose() {
+ $(this.$el).trigger('hidden.gl.dropdown');
+ },
+ handleUpdateSelectedLabels(labels) {
+ 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);
+
+ const issuableLabels = difference(
+ union(currentLabelIds, userAddedLabelIds),
+ userRemovedLabelIds,
+ );
+
+ this.labelsSelectInProgress = true;
+
+ axios({
+ data: {
+ [this.issuableType]: {
+ label_ids: issuableLabels,
+ },
+ },
+ method: 'put',
+ url: this.labelsUpdatePath,
+ })
+ .then(({ data }) => this.replaceSelectedLabels(data.labels))
+ .catch(() => flash(__('An error occurred while updating labels.')))
+ .finally(() => {
+ this.labelsSelectInProgress = false;
+ });
+ },
+ },
+};
+</script>
+
+<template>
+ <labels-select
+ class="block labels js-labels-block"
+ :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="labelsSelectInProgress"
+ :selected-labels="selectedLabels"
+ :variant="$options.sidebar"
+ @onDropdownClose="handleDropdownClose"
+ @updateSelectedLabels="handleUpdateSelectedLabels"
+ >
+ {{ __('None') }}
+ </labels-select>
+</template>
diff --git a/app/assets/javascripts/sidebar/components/lock/issuable_lock_form.vue b/app/assets/javascripts/sidebar/components/lock/issuable_lock_form.vue
index 1b4968fabf6..53ee7f46ad9 100644
--- a/app/assets/javascripts/sidebar/components/lock/issuable_lock_form.vue
+++ b/app/assets/javascripts/sidebar/components/lock/issuable_lock_form.vue
@@ -1,8 +1,8 @@
<script>
import { mapGetters } from 'vuex';
+import { GlIcon } from '@gitlab/ui';
import { __ } from '~/locale';
import tooltip from '~/vue_shared/directives/tooltip';
-import Icon from '~/vue_shared/components/icon.vue';
import eventHub from '~/sidebar/event_hub';
import editForm from './edit_form.vue';
@@ -22,7 +22,7 @@ export default {
},
components: {
editForm,
- Icon,
+ GlIcon,
},
directives: {
@@ -88,7 +88,7 @@ export default {
data-boundary="viewport"
@click="toggleForm"
>
- <icon :name="lockStatus.icon" class="sidebar-item-icon is-active" />
+ <gl-icon :name="lockStatus.icon" class="sidebar-item-icon is-active" />
</div>
<div class="title hide-collapsed">
@@ -116,7 +116,7 @@ export default {
/>
<div data-testid="lock-status" class="sidebar-item-value" :class="lockStatus.class">
- <icon
+ <gl-icon
:size="16"
:name="lockStatus.icon"
class="sidebar-item-icon"
diff --git a/app/assets/javascripts/sidebar/components/participants/participants.vue b/app/assets/javascripts/sidebar/components/participants/participants.vue
index d2904f4157c..e7dbc47aea1 100644
--- a/app/assets/javascripts/sidebar/components/participants/participants.vue
+++ b/app/assets/javascripts/sidebar/components/participants/participants.vue
@@ -1,5 +1,5 @@
<script>
-import { GlLoadingIcon } from '@gitlab/ui';
+import { GlIcon, GlLoadingIcon } from '@gitlab/ui';
import { __, n__, sprintf } from '~/locale';
import tooltip from '~/vue_shared/directives/tooltip';
import userAvatarImage from '~/vue_shared/components/user_avatar/user_avatar_image.vue';
@@ -10,6 +10,7 @@ export default {
},
components: {
userAvatarImage,
+ GlIcon,
GlLoadingIcon,
},
props: {
@@ -94,7 +95,7 @@ export default {
data-boundary="viewport"
@click="onClickCollapsedIcon"
>
- <i class="fa fa-users" aria-hidden="true"> </i>
+ <gl-icon name="users" />
<gl-loading-icon v-if="loading" />
<span v-else data-testid="collapsed-count"> {{ participantCount }} </span>
</div>
diff --git a/app/assets/javascripts/sidebar/components/severity/constants.js b/app/assets/javascripts/sidebar/components/severity/constants.js
new file mode 100644
index 00000000000..4f58ff38121
--- /dev/null
+++ b/app/assets/javascripts/sidebar/components/severity/constants.js
@@ -0,0 +1,41 @@
+import { __, s__ } from '~/locale';
+
+export const INCIDENT_SEVERITY = {
+ CRITICAL: {
+ value: 'CRITICAL',
+ icon: 'critical',
+ label: s__('IncidentManagement|Critical - S1'),
+ },
+ HIGH: {
+ value: 'HIGH',
+ icon: 'high',
+ label: s__('IncidentManagement|High - S2'),
+ },
+ MEDIUM: {
+ value: 'MEDIUM',
+ icon: 'medium',
+ label: s__('IncidentManagement|Medium - S3'),
+ },
+ LOW: {
+ value: 'LOW',
+ icon: 'low',
+ label: s__('IncidentManagement|Low - S4'),
+ },
+ UNKNOWN: {
+ value: 'UNKNOWN',
+ icon: 'unknown',
+ label: s__('IncidentManagement|Unknown'),
+ },
+};
+
+export const ISSUABLE_TYPES = {
+ INCIDENT: 'incident',
+};
+
+export const I18N = {
+ UPDATE_SEVERITY_ERROR: s__('SeverityWidget|There was an error while updating severity.'),
+ TRY_AGAIN: __('Please try again'),
+ EDIT: __('Edit'),
+ SEVERITY: s__('SeverityWidget|Severity'),
+ SEVERITY_VALUE: s__('SeverityWidget|Severity: %{severity}'),
+};
diff --git a/app/assets/javascripts/sidebar/components/severity/graphql/mutations/update_issuable_severity.mutation.graphql b/app/assets/javascripts/sidebar/components/severity/graphql/mutations/update_issuable_severity.mutation.graphql
new file mode 100644
index 00000000000..750e757971f
--- /dev/null
+++ b/app/assets/javascripts/sidebar/components/severity/graphql/mutations/update_issuable_severity.mutation.graphql
@@ -0,0 +1,9 @@
+mutation updateIssuableSeverity($projectPath: ID!, $severity: IssuableSeverity!, $iid: String!) {
+ issueSetSeverity(input: { iid: $iid, severity: $severity, projectPath: $projectPath }) {
+ errors
+ issue {
+ iid
+ severity
+ }
+ }
+}
diff --git a/app/assets/javascripts/sidebar/components/severity/severity.vue b/app/assets/javascripts/sidebar/components/severity/severity.vue
new file mode 100644
index 00000000000..7e7d62256c9
--- /dev/null
+++ b/app/assets/javascripts/sidebar/components/severity/severity.vue
@@ -0,0 +1,42 @@
+<script>
+import { GlIcon } from '@gitlab/ui';
+
+export default {
+ components: {
+ GlIcon,
+ },
+ props: {
+ severity: {
+ type: Object,
+ required: true,
+ validator(severity) {
+ const { value, label, icon } = severity;
+ return value && label && icon;
+ },
+ },
+ iconSize: {
+ type: Number,
+ required: false,
+ default: 12,
+ },
+ iconOnly: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ },
+};
+</script>
+
+<template>
+ <div
+ class="incident-severity gl-display-inline-flex gl-align-items-center gl-justify-content-between"
+ >
+ <gl-icon
+ :size="iconSize"
+ :name="`severity-${severity.icon}`"
+ :class="[`icon-${severity.icon}`, { 'gl-mr-3': !iconOnly }]"
+ />
+ <span v-if="!iconOnly">{{ severity.label }}</span>
+ </div>
+</template>
diff --git a/app/assets/javascripts/sidebar/components/severity/sidebar_severity.vue b/app/assets/javascripts/sidebar/components/severity/sidebar_severity.vue
new file mode 100644
index 00000000000..8f3610b912a
--- /dev/null
+++ b/app/assets/javascripts/sidebar/components/severity/sidebar_severity.vue
@@ -0,0 +1,187 @@
+<script>
+import {
+ GlDropdown,
+ GlDropdownItem,
+ GlLoadingIcon,
+ GlTooltip,
+ GlSprintf,
+ GlLink,
+} from '@gitlab/ui';
+import { INCIDENT_SEVERITY, ISSUABLE_TYPES, I18N } from './constants';
+import updateIssuableSeverity from './graphql/mutations/update_issuable_severity.mutation.graphql';
+import SeverityToken from './severity.vue';
+import createFlash from '~/flash';
+
+export default {
+ i18n: I18N,
+ components: {
+ GlLoadingIcon,
+ GlTooltip,
+ GlSprintf,
+ GlDropdown,
+ GlDropdownItem,
+ GlLink,
+ SeverityToken,
+ },
+ props: {
+ projectPath: {
+ type: String,
+ required: true,
+ },
+ iid: {
+ type: String,
+ required: true,
+ },
+ initialSeverity: {
+ type: String,
+ required: false,
+ default: INCIDENT_SEVERITY.UNKNOWN.value,
+ },
+ issuableType: {
+ type: String,
+ required: false,
+ default: ISSUABLE_TYPES.INCIDENT,
+ validator: value => {
+ // currently severity is supported only for incidents, but this list might be extended
+ return [ISSUABLE_TYPES.INCIDENT].includes(value);
+ },
+ },
+ },
+ data() {
+ return {
+ isDropdownShowing: false,
+ isUpdating: false,
+ severity: this.initialSeverity,
+ };
+ },
+ computed: {
+ severitiesList() {
+ switch (this.issuableType) {
+ case ISSUABLE_TYPES.INCIDENT:
+ return Object.values(INCIDENT_SEVERITY);
+ default:
+ return [];
+ }
+ },
+ dropdownClass() {
+ return this.isDropdownShowing ? 'show' : 'gl-display-none';
+ },
+ selectedItem() {
+ return this.severitiesList.find(severity => severity.value === this.severity);
+ },
+ },
+ mounted() {
+ document.addEventListener('click', this.handleOffClick);
+ },
+ beforeDestroy() {
+ document.removeEventListener('click', this.handleOffClick);
+ },
+ methods: {
+ handleOffClick(event) {
+ if (!this.isDropdownShowing) {
+ return;
+ }
+
+ if (!this.$refs.sidebarSeverity.contains(event.target)) {
+ this.hideDropdown();
+ }
+ },
+ hideDropdown() {
+ this.isDropdownShowing = false;
+ const event = new Event('hidden.gl.dropdown');
+ this.$el.dispatchEvent(event);
+ },
+ toggleFormDropdown() {
+ this.isDropdownShowing = !this.isDropdownShowing;
+ },
+ updateSeverity(value) {
+ this.hideDropdown();
+ this.isUpdating = true;
+ this.$apollo
+ .mutate({
+ mutation: updateIssuableSeverity,
+ variables: {
+ iid: this.iid,
+ severity: value,
+ projectPath: this.projectPath,
+ },
+ })
+ .then(resp => {
+ const {
+ data: {
+ issueSetSeverity: {
+ errors = [],
+ issue: { severity },
+ },
+ },
+ } = resp;
+
+ if (errors[0]) {
+ throw errors[0];
+ }
+ this.severity = severity;
+ })
+ .catch(() =>
+ createFlash({
+ message: `${this.$options.i18n.UPDATE_SEVERITY_ERROR} ${this.$options.i18n.TRY_AGAIN}`,
+ }),
+ )
+ .finally(() => {
+ this.isUpdating = false;
+ });
+ },
+ },
+};
+</script>
+
+<template>
+ <div ref="sidebarSeverity" class="block">
+ <div ref="severity" class="sidebar-collapsed-icon" @click="toggleFormDropdown">
+ <severity-token :severity="selectedItem" :icon-size="14" :icon-only="true" />
+ <gl-tooltip :target="() => $refs.severity" boundary="viewport" placement="left">
+ <gl-sprintf :message="$options.i18n.SEVERITY_VALUE">
+ <template #severity>
+ {{ selectedItem.label }}
+ </template>
+ </gl-sprintf>
+ </gl-tooltip>
+ </div>
+
+ <div class="hide-collapsed">
+ <p class="title gl-display-flex gl-justify-content-space-between">
+ {{ $options.i18n.SEVERITY }}
+ <gl-link
+ data-testid="editButton"
+ href="#"
+ @click="toggleFormDropdown"
+ @keydown.esc="hideDropdown"
+ >
+ {{ $options.i18n.EDIT }}
+ </gl-link>
+ </p>
+
+ <gl-dropdown
+ :class="dropdownClass"
+ block
+ :text="selectedItem.label"
+ toggle-class="dropdown-menu-toggle gl-mb-2"
+ @keydown.esc.native="hideDropdown"
+ >
+ <gl-dropdown-item
+ v-for="option in severitiesList"
+ :key="option.value"
+ data-testid="severityDropdownItem"
+ :is-check-item="true"
+ :is-checked="option.value === severity"
+ @click="updateSeverity(option.value)"
+ >
+ <severity-token :severity="option" />
+ </gl-dropdown-item>
+ </gl-dropdown>
+
+ <gl-loading-icon v-if="isUpdating" :inline="true" />
+
+ <severity-token v-else-if="!isDropdownShowing" :severity="selectedItem" />
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/sidebar/components/subscriptions/subscriptions.vue b/app/assets/javascripts/sidebar/components/subscriptions/subscriptions.vue
index 3b92ead8859..0457aad8795 100644
--- a/app/assets/javascripts/sidebar/components/subscriptions/subscriptions.vue
+++ b/app/assets/javascripts/sidebar/components/subscriptions/subscriptions.vue
@@ -1,7 +1,7 @@
<script>
+import { GlIcon } from '@gitlab/ui';
import { __ } from '~/locale';
import Tracking from '~/tracking';
-import icon from '~/vue_shared/components/icon.vue';
import toggleButton from '~/vue_shared/components/toggle_button.vue';
import tooltip from '~/vue_shared/directives/tooltip';
import eventHub from '../../event_hub';
@@ -16,7 +16,7 @@ export default {
tooltip,
},
components: {
- icon,
+ GlIcon,
toggleButton,
},
mixins: [Tracking.mixin({ label: 'right_sidebar' })],
@@ -118,7 +118,7 @@ export default {
data-boundary="viewport"
@click="onClickCollapsedIcon"
>
- <icon
+ <gl-icon
:name="notificationIcon"
:size="16"
aria-hidden="true"
diff --git a/app/assets/javascripts/sidebar/components/time_tracking/collapsed_state.vue b/app/assets/javascripts/sidebar/components/time_tracking/collapsed_state.vue
index 65ecd5be05d..bc2319c0f36 100644
--- a/app/assets/javascripts/sidebar/components/time_tracking/collapsed_state.vue
+++ b/app/assets/javascripts/sidebar/components/time_tracking/collapsed_state.vue
@@ -1,12 +1,12 @@
<script>
+import { GlIcon } from '@gitlab/ui';
import { __, sprintf } from '~/locale';
-import icon from '~/vue_shared/components/icon.vue';
import tooltip from '~/vue_shared/directives/tooltip';
export default {
name: 'TimeTrackingCollapsedState',
components: {
- icon,
+ GlIcon,
},
directives: {
tooltip,
@@ -105,7 +105,7 @@ export default {
data-placement="left"
data-boundary="viewport"
>
- <icon name="timer" />
+ <gl-icon name="timer" />
<div class="time-tracking-collapsed-summary">
<div :class="divClass">
<span :class="spanClass"> {{ text }} </span>
diff --git a/app/assets/javascripts/sidebar/components/time_tracking/help_state.vue b/app/assets/javascripts/sidebar/components/time_tracking/help_state.vue
index 67abde0c22a..b45746e789d 100644
--- a/app/assets/javascripts/sidebar/components/time_tracking/help_state.vue
+++ b/app/assets/javascripts/sidebar/components/time_tracking/help_state.vue
@@ -1,4 +1,5 @@
<script>
+/* eslint-disable vue/no-v-html */
import { sprintf, s__ } from '../../../locale';
import { joinPaths } from '~/lib/utils/url_utility';
diff --git a/app/assets/javascripts/sidebar/components/time_tracking/spent_only_pane.vue b/app/assets/javascripts/sidebar/components/time_tracking/spent_only_pane.vue
index c2f30310e2e..b2b3b289c5c 100644
--- a/app/assets/javascripts/sidebar/components/time_tracking/spent_only_pane.vue
+++ b/app/assets/javascripts/sidebar/components/time_tracking/spent_only_pane.vue
@@ -1,4 +1,5 @@
<script>
+/* eslint-disable vue/no-v-html */
import { sprintf, s__ } from '~/locale';
export default {
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 67a8f11b760..a2fb0ebcbc6 100644
--- a/app/assets/javascripts/sidebar/components/time_tracking/time_tracker.vue
+++ b/app/assets/javascripts/sidebar/components/time_tracking/time_tracker.vue
@@ -106,7 +106,7 @@ export default {
<div class="title hide-collapsed">
{{ __('Time tracking') }}
<div v-if="!showHelpState" class="help-button float-right" @click="toggleHelpState(true)">
- <i class="fa fa-question-circle" aria-hidden="true"> </i>
+ <gl-icon name="question-o" />
</div>
<div
v-if="showHelpState"
diff --git a/app/assets/javascripts/sidebar/components/todo_toggle/todo.vue b/app/assets/javascripts/sidebar/components/todo_toggle/todo.vue
index 5281c03ab3f..51719df313f 100644
--- a/app/assets/javascripts/sidebar/components/todo_toggle/todo.vue
+++ b/app/assets/javascripts/sidebar/components/todo_toggle/todo.vue
@@ -1,10 +1,8 @@
<script>
-import { GlLoadingIcon } from '@gitlab/ui';
+import { GlLoadingIcon, GlIcon } from '@gitlab/ui';
import { __ } from '~/locale';
import tooltip from '~/vue_shared/directives/tooltip';
-import Icon from '~/vue_shared/components/icon.vue';
-
const MARK_TEXT = __('Mark as done');
const TODO_TEXT = __('Add a To-Do');
@@ -13,7 +11,7 @@ export default {
tooltip,
},
components: {
- Icon,
+ GlIcon,
GlLoadingIcon,
},
props: {
@@ -85,7 +83,7 @@ export default {
data-boundary="viewport"
@click="handleButtonClick"
>
- <icon
+ <gl-icon
v-show="collapsedButtonIconVisible"
:class="collapsedButtonIconClasses"
:name="collapsedButtonIcon"
diff --git a/app/assets/javascripts/sidebar/event_hub.js b/app/assets/javascripts/sidebar/event_hub.js
index f35506fd5de..dd4bd9a5ab7 100644
--- a/app/assets/javascripts/sidebar/event_hub.js
+++ b/app/assets/javascripts/sidebar/event_hub.js
@@ -1,6 +1,6 @@
-import Vue from 'vue';
+import createEventHub from '~/helpers/event_hub_factory';
-const eventHub = new Vue();
+const eventHub = createEventHub();
// TODO: remove eventHub hack after code splitting refactor
window.emitSidebarEvent = (...args) => eventHub.$emit(...args);
diff --git a/app/assets/javascripts/sidebar/lib/sidebar_move_issue.js b/app/assets/javascripts/sidebar/lib/sidebar_move_issue.js
index 0fb9cf22653..edeb1bba020 100644
--- a/app/assets/javascripts/sidebar/lib/sidebar_move_issue.js
+++ b/app/assets/javascripts/sidebar/lib/sidebar_move_issue.js
@@ -1,7 +1,7 @@
import $ from 'jquery';
-import '~/gl_dropdown';
import { escape } from 'lodash';
import { __ } from '~/locale';
+import initDeprecatedJQueryDropdown from '~/deprecated_jquery_dropdown';
function isValidProjectId(id) {
return id > 0;
@@ -27,7 +27,7 @@ class SidebarMoveIssue {
}
initDropdown() {
- this.$dropdownToggle.glDropdown({
+ initDeprecatedJQueryDropdown(this.$dropdownToggle, {
search: {
fields: ['name_with_namespace'],
},
diff --git a/app/assets/javascripts/sidebar/mount_sidebar.js b/app/assets/javascripts/sidebar/mount_sidebar.js
index 015219200db..be559b16420 100644
--- a/app/assets/javascripts/sidebar/mount_sidebar.js
+++ b/app/assets/javascripts/sidebar/mount_sidebar.js
@@ -1,21 +1,26 @@
import $ from 'jquery';
import Vue from 'vue';
import VueApollo from 'vue-apollo';
+import Vuex from 'vuex';
import SidebarTimeTracking from './components/time_tracking/sidebar_time_tracking.vue';
import SidebarAssignees from './components/assignees/sidebar_assignees.vue';
+import SidebarLabels from './components/labels/sidebar_labels.vue';
import ConfidentialIssueSidebar from './components/confidential/confidential_issue_sidebar.vue';
import SidebarMoveIssue from './lib/sidebar_move_issue';
import IssuableLockForm from './components/lock/issuable_lock_form.vue';
import sidebarParticipants from './components/participants/sidebar_participants.vue';
import sidebarSubscriptions from './components/subscriptions/sidebar_subscriptions.vue';
+import SidebarSeverity from './components/severity/sidebar_severity.vue';
import Translate from '../vue_shared/translate';
import createDefaultClient from '~/lib/graphql';
import { store } from '~/notes/stores';
-import { isInIssuePage } from '~/lib/utils/common_utils';
+import { isInIssuePage, parseBoolean } from '~/lib/utils/common_utils';
import mergeRequestStore from '~/mr_notes/stores';
+import labelsSelectModule from '~/vue_shared/components/sidebar/labels_select_vue/store';
Vue.use(Translate);
Vue.use(VueApollo);
+Vue.use(Vuex);
function getSidebarOptions() {
return JSON.parse(document.querySelector('.js-sidebar-options').innerHTML);
@@ -51,6 +56,29 @@ function mountAssigneesComponent(mediator) {
});
}
+export function mountSidebarLabels() {
+ const el = document.querySelector('.js-sidebar-labels');
+
+ if (!el) {
+ return false;
+ }
+
+ const labelsStore = new Vuex.Store(labelsSelectModule());
+
+ return new Vue({
+ el,
+ provide: {
+ ...el.dataset,
+ allowLabelCreate: parseBoolean(el.dataset.allowLabelCreate),
+ allowLabelEdit: parseBoolean(el.dataset.canEdit),
+ allowScopedLabels: parseBoolean(el.dataset.allowScopedLabels),
+ initiallySelectedLabels: JSON.parse(el.dataset.selectedLabels),
+ },
+ store: labelsStore,
+ render: createElement => createElement(SidebarLabels),
+ });
+}
+
function mountConfidentialComponent(mediator) {
const el = document.getElementById('js-confidential-entry-point');
@@ -159,6 +187,35 @@ function mountTimeTrackingComponent() {
});
}
+function mountSeverityComponent() {
+ const severityContainerEl = document.querySelector('#js-severity');
+
+ if (!severityContainerEl) {
+ return false;
+ }
+ const apolloProvider = new VueApollo({
+ defaultClient: createDefaultClient(),
+ });
+
+ const { fullPath, iid, severity } = getSidebarOptions();
+
+ return new Vue({
+ el: severityContainerEl,
+ apolloProvider,
+ components: {
+ SidebarSeverity,
+ },
+ render: createElement =>
+ createElement('sidebar-severity', {
+ props: {
+ projectPath: fullPath,
+ iid: String(iid),
+ initialSeverity: severity.toUpperCase(),
+ },
+ }),
+ });
+}
+
export function mountSidebar(mediator) {
mountAssigneesComponent(mediator);
mountConfidentialComponent(mediator);
@@ -173,6 +230,8 @@ export function mountSidebar(mediator) {
).init();
mountTimeTrackingComponent();
+
+ mountSeverityComponent();
}
export { getSidebarOptions };
diff --git a/app/assets/javascripts/sidebar/queries/sidebarDetailsForHealthStatusFeatureFlag.query.graphql b/app/assets/javascripts/sidebar/queries/sidebarDetailsForHealthStatusFeatureFlag.query.graphql
deleted file mode 100644
index 2aff7da4605..00000000000
--- a/app/assets/javascripts/sidebar/queries/sidebarDetailsForHealthStatusFeatureFlag.query.graphql
+++ /dev/null
@@ -1,7 +0,0 @@
-query($fullPath: ID!, $iid: String!) {
- project(fullPath: $fullPath) {
- issue(iid: $iid) {
- iid
- }
- }
-}
diff --git a/app/assets/javascripts/sidebar/services/sidebar_service.js b/app/assets/javascripts/sidebar/services/sidebar_service.js
index 8714bea1729..a61af631661 100644
--- a/app/assets/javascripts/sidebar/services/sidebar_service.js
+++ b/app/assets/javascripts/sidebar/services/sidebar_service.js
@@ -1,5 +1,4 @@
import sidebarDetailsQuery from 'ee_else_ce/sidebar/queries/sidebarDetails.query.graphql';
-import sidebarDetailsForHealthStatusFeatureFlagQuery from 'ee_else_ce/sidebar/queries/sidebarDetailsForHealthStatusFeatureFlag.query.graphql';
import axios from '~/lib/utils/axios_utils';
import createGqClient, { fetchPolicies } from '~/lib/graphql';
@@ -27,14 +26,10 @@ export default class SidebarService {
}
get() {
- const hasHealthStatusFeatureFlag = gon.features && gon.features.saveIssuableHealthStatus;
-
return Promise.all([
axios.get(this.endpoint),
gqClient.query({
- query: hasHealthStatusFeatureFlag
- ? sidebarDetailsForHealthStatusFeatureFlagQuery
- : sidebarDetailsQuery,
+ query: sidebarDetailsQuery,
variables: {
fullPath: this.fullPath,
iid: this.iid.toString(),
diff --git a/app/assets/javascripts/snippet/snippet_edit.js b/app/assets/javascripts/snippet/snippet_edit.js
index b0d373b1a4b..3dc74922a77 100644
--- a/app/assets/javascripts/snippet/snippet_edit.js
+++ b/app/assets/javascripts/snippet/snippet_edit.js
@@ -14,6 +14,7 @@ document.addEventListener('DOMContentLoaded', () => {
milestones: false,
labels: false,
snippets: false,
+ vulnerabilities: false,
};
const projectSnippetOptions = {};
diff --git a/app/assets/javascripts/snippet/snippet_show.js b/app/assets/javascripts/snippet/snippet_show.js
index 9a463b4762b..bbddfc579c5 100644
--- a/app/assets/javascripts/snippet/snippet_show.js
+++ b/app/assets/javascripts/snippet/snippet_show.js
@@ -4,6 +4,7 @@ import ZenMode from '~/zen_mode';
import initNotes from '~/init_notes';
import snippetEmbed from '~/snippet/snippet_embed';
import { SnippetShowInit } from '~/snippets';
+import loadAwardsHandler from '~/awards_handler';
document.addEventListener('DOMContentLoaded', () => {
if (!gon.features.snippetsVue) {
@@ -16,4 +17,5 @@ document.addEventListener('DOMContentLoaded', () => {
SnippetShowInit();
initNotes();
}
+ loadAwardsHandler();
});
diff --git a/app/assets/javascripts/snippets/components/edit.vue b/app/assets/javascripts/snippets/components/edit.vue
index 0978fcc7f93..1a539aa0876 100644
--- a/app/assets/javascripts/snippets/components/edit.vue
+++ b/app/assets/javascripts/snippets/components/edit.vue
@@ -4,21 +4,23 @@ import { GlButton, GlLoadingIcon } from '@gitlab/ui';
import { deprecatedCreateFlash as Flash } from '~/flash';
import { __, sprintf } from '~/locale';
import TitleField from '~/vue_shared/components/form/title.vue';
-import { redirectTo } from '~/lib/utils/url_utility';
+import { redirectTo, joinPaths } from '~/lib/utils/url_utility';
import FormFooterActions from '~/vue_shared/components/form/form_footer_actions.vue';
+import { SNIPPET_MARK_EDIT_APP_START } from '~/performance_constants';
import UpdateSnippetMutation from '../mutations/updateSnippet.mutation.graphql';
import CreateSnippetMutation from '../mutations/createSnippet.mutation.graphql';
import { getSnippetMixin } from '../mixins/snippets';
import {
- SNIPPET_VISIBILITY_PRIVATE,
SNIPPET_CREATE_MUTATION_ERROR,
SNIPPET_UPDATE_MUTATION_ERROR,
+ SNIPPET_VISIBILITY_PRIVATE,
} from '../constants';
+import defaultVisibilityQuery from '../queries/snippet_visibility.query.graphql';
+
import SnippetBlobActionsEdit from './snippet_blob_actions_edit.vue';
import SnippetVisibilityEdit from './snippet_visibility_edit.vue';
import SnippetDescriptionEdit from './snippet_description_edit.vue';
-import { SNIPPET_MARK_EDIT_APP_START } from '~/performance_constants';
export default {
components: {
@@ -31,6 +33,15 @@ export default {
GlLoadingIcon,
},
mixins: [getSnippetMixin],
+ apollo: {
+ defaultVisibility: {
+ query: defaultVisibilityQuery,
+ manual: true,
+ result({ data: { selectedLevel } }) {
+ this.selectedLevelDefault = selectedLevel;
+ },
+ },
+ },
props: {
markdownPreviewPath: {
type: String,
@@ -56,6 +67,7 @@ export default {
isUpdating: false,
newSnippet: false,
actions: [],
+ selectedLevelDefault: SNIPPET_VISIBILITY_PRIVATE,
};
},
computed: {
@@ -88,7 +100,7 @@ export default {
},
cancelButtonHref() {
if (this.newSnippet) {
- return this.projectPath ? `/${this.projectPath}/-/snippets` : `/-/snippets`;
+ return joinPaths('/', gon.relative_url_root, this.projectPath, '-/snippets');
}
return this.snippet.webUrl;
},
@@ -98,6 +110,13 @@ export default {
descriptionFieldId() {
return `${this.isProjectSnippet ? 'project' : 'personal'}_snippet_description`;
},
+ newSnippetSchema() {
+ return {
+ title: '',
+ description: '',
+ visibilityLevel: this.selectedLevelDefault,
+ };
+ },
},
beforeCreate() {
performance.mark(SNIPPET_MARK_EDIT_APP_START);
@@ -126,7 +145,7 @@ export default {
},
onNewSnippetFetched() {
this.newSnippet = true;
- this.snippet = this.$options.newSnippetSchema;
+ this.snippet = this.newSnippetSchema;
},
onExistingSnippetFetched() {
this.newSnippet = false;
@@ -184,11 +203,6 @@ export default {
this.actions = actions;
},
},
- newSnippetSchema: {
- title: '',
- description: '',
- visibilityLevel: SNIPPET_VISIBILITY_PRIVATE,
- },
};
</script>
<template>
@@ -202,7 +216,7 @@ export default {
v-if="isLoading"
:label="__('Loading snippet')"
size="lg"
- class="loading-animation prepend-top-20 append-bottom-20"
+ class="loading-animation prepend-top-20 gl-mb-6"
/>
<template v-else>
<title-field
diff --git a/app/assets/javascripts/snippets/components/embed_dropdown.vue b/app/assets/javascripts/snippets/components/embed_dropdown.vue
new file mode 100644
index 00000000000..589754a8b19
--- /dev/null
+++ b/app/assets/javascripts/snippets/components/embed_dropdown.vue
@@ -0,0 +1,78 @@
+<script>
+import { escape as esc } from 'lodash';
+import {
+ GlButton,
+ GlDropdown,
+ GlDropdownSectionHeader,
+ GlDropdownText,
+ GlFormInputGroup,
+ GlTooltipDirective,
+} from '@gitlab/ui';
+import { __ } from '~/locale';
+
+const MSG_EMBED = __('Embed');
+const MSG_SHARE = __('Share');
+const MSG_COPY = __('Copy');
+
+export default {
+ components: {
+ GlButton,
+ GlDropdown,
+ GlDropdownSectionHeader,
+ GlDropdownText,
+ GlFormInputGroup,
+ },
+ directives: {
+ GlTooltip: GlTooltipDirective,
+ },
+ props: {
+ url: {
+ type: String,
+ required: true,
+ },
+ },
+ computed: {
+ sections() {
+ return [
+ // eslint-disable-next-line no-useless-escape
+ { name: MSG_EMBED, value: `<script src="${esc(this.url)}.js"><\/script>` },
+ { name: MSG_SHARE, value: this.url },
+ ];
+ },
+ },
+ MSG_EMBED,
+ MSG_COPY,
+};
+</script>
+<template>
+ <gl-dropdown
+ right
+ :text="$options.MSG_EMBED"
+ menu-class="gl-px-1! gl-pb-5! gl-dropdown-menu-wide"
+ >
+ <template v-for="{ name, value } in sections">
+ <gl-dropdown-section-header :key="`header_${name}`" data-testid="header">{{
+ name
+ }}</gl-dropdown-section-header>
+ <gl-dropdown-text
+ :key="`input_${name}`"
+ tag="div"
+ class="gl-dropdown-text-py-0 gl-dropdown-text-block"
+ data-testid="input"
+ >
+ <gl-form-input-group :value="value" readonly select-on-click>
+ <template #append>
+ <gl-button
+ v-gl-tooltip.hover
+ :title="$options.MSG_COPY"
+ :data-clipboard-text="value"
+ icon="copy-to-clipboard"
+ data-qa-selector="copy_button"
+ :data-qa-action="name"
+ />
+ </template>
+ </gl-form-input-group>
+ </gl-dropdown-text>
+ </template>
+ </gl-dropdown>
+</template>
diff --git a/app/assets/javascripts/snippets/components/show.vue b/app/assets/javascripts/snippets/components/show.vue
index ca41fd0a2b1..43be2cb7ed8 100644
--- a/app/assets/javascripts/snippets/components/show.vue
+++ b/app/assets/javascripts/snippets/components/show.vue
@@ -1,6 +1,6 @@
<script>
import { GlLoadingIcon } from '@gitlab/ui';
-import BlobEmbeddable from '~/blob/components/blob_embeddable.vue';
+import EmbedDropdown from './embed_dropdown.vue';
import SnippetHeader from './snippet_header.vue';
import SnippetTitle from './snippet_title.vue';
import SnippetBlob from './snippet_blob_view.vue';
@@ -13,7 +13,7 @@ import { SNIPPET_MARK_VIEW_APP_START } from '~/performance_constants';
export default {
components: {
- BlobEmbeddable,
+ EmbedDropdown,
SnippetHeader,
SnippetTitle,
GlLoadingIcon,
@@ -40,13 +40,17 @@ export default {
v-if="isLoading"
:label="__('Loading snippet')"
size="lg"
- class="loading-animation prepend-top-20 append-bottom-20"
+ class="loading-animation prepend-top-20 gl-mb-6"
/>
<template v-else>
<snippet-header :snippet="snippet" />
<snippet-title :snippet="snippet" />
<div class="gl-display-flex gl-justify-content-end gl-mb-5">
- <blob-embeddable v-if="embeddable" class="gl-flex-fill-1" :url="snippet.webUrl" />
+ <embed-dropdown
+ v-if="embeddable"
+ :url="snippet.webUrl"
+ data-qa-selector="snippet_embed_dropdown"
+ />
<clone-dropdown-button
v-if="canBeCloned"
class="gl-ml-3"
diff --git a/app/assets/javascripts/snippets/components/snippet_blob_edit.vue b/app/assets/javascripts/snippets/components/snippet_blob_edit.vue
index ff03432f942..f3f894ed649 100644
--- a/app/assets/javascripts/snippets/components/snippet_blob_edit.vue
+++ b/app/assets/javascripts/snippets/components/snippet_blob_edit.vue
@@ -53,7 +53,10 @@ export default {
const url = joinPaths(baseUrl, this.blob.rawPath);
axios
- .get(url)
+ .get(url, {
+ // This prevents axios from automatically JSON.parse response
+ transformResponse: [f => f],
+ })
.then(res => {
this.notifyAboutUpdates({ content: res.data });
})
@@ -80,7 +83,7 @@ export default {
v-if="!blob.isLoaded"
:label="__('Loading snippet')"
size="lg"
- class="loading-animation prepend-top-20 append-bottom-20"
+ class="loading-animation prepend-top-20 gl-mb-6"
/>
<blob-content-edit
v-else
diff --git a/app/assets/javascripts/snippets/components/snippet_description_view.vue b/app/assets/javascripts/snippets/components/snippet_description_view.vue
index a5107f09fc7..e462f20535b 100644
--- a/app/assets/javascripts/snippets/components/snippet_description_view.vue
+++ b/app/assets/javascripts/snippets/components/snippet_description_view.vue
@@ -1,4 +1,5 @@
<script>
+/* eslint-disable vue/no-v-html */
import MarkdownFieldView from '~/vue_shared/components/markdown/field_view.vue';
export default {
diff --git a/app/assets/javascripts/snippets/components/snippet_header.vue b/app/assets/javascripts/snippets/components/snippet_header.vue
index ed087dcfaf9..0ca69f3161a 100644
--- a/app/assets/javascripts/snippets/components/snippet_header.vue
+++ b/app/assets/javascripts/snippets/components/snippet_header.vue
@@ -17,6 +17,7 @@ import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
import DeleteSnippetMutation from '../mutations/deleteSnippet.mutation.graphql';
import CanCreatePersonalSnippet from '../queries/userPermissions.query.graphql';
import CanCreateProjectSnippet from '../queries/projectPermissions.query.graphql';
+import { joinPaths } from '~/lib/utils/url_utility';
export default {
components: {
@@ -96,8 +97,8 @@ export default {
condition: this.canCreateSnippet,
text: __('New snippet'),
href: this.snippet.project
- ? `${this.snippet.project.webUrl}/-/snippets/new`
- : '/-/snippets/new',
+ ? joinPaths(this.snippet.project.webUrl, '-/snippets/new')
+ : joinPaths('/', gon.relative_url_root, '/-/snippets/new'),
variant: 'success',
category: 'secondary',
cssClass: 'ml-2',
@@ -137,7 +138,7 @@ export default {
redirectToSnippets() {
window.location.pathname = this.snippet.project
? `${this.snippet.project.fullPath}/-/snippets`
- : 'dashboard/snippets';
+ : `${gon.relative_url_root}dashboard/snippets`;
},
closeDeleteModal() {
this.$refs.deleteModal.hide();
diff --git a/app/assets/javascripts/snippets/components/snippet_visibility_edit.vue b/app/assets/javascripts/snippets/components/snippet_visibility_edit.vue
index 299bb8fcfad..25ad7c214b2 100644
--- a/app/assets/javascripts/snippets/components/snippet_visibility_edit.vue
+++ b/app/assets/javascripts/snippets/components/snippet_visibility_edit.vue
@@ -1,11 +1,8 @@
<script>
import { GlIcon, GlFormGroup, GlFormRadio, GlFormRadioGroup, GlLink } from '@gitlab/ui';
-import {
- SNIPPET_VISIBILITY,
- SNIPPET_VISIBILITY_PRIVATE,
- SNIPPET_VISIBILITY_INTERNAL,
- SNIPPET_VISIBILITY_PUBLIC,
-} from '~/snippets/constants';
+import defaultVisibilityQuery from '../queries/snippet_visibility.query.graphql';
+import { defaultSnippetVisibilityLevels } from '../utils/blob';
+import { SNIPPET_LEVELS_RESTRICTED, SNIPPET_LEVELS_DISABLED } from '~/snippets/constants';
export default {
components: {
@@ -15,6 +12,16 @@ export default {
GlFormRadioGroup,
GlLink,
},
+ apollo: {
+ defaultVisibility: {
+ query: defaultVisibilityQuery,
+ manual: true,
+ result({ data: { visibilityLevels, multipleLevelsRestricted } }) {
+ this.visibilityLevels = defaultSnippetVisibilityLevels(visibilityLevels);
+ this.multipleLevelsRestricted = multipleLevelsRestricted;
+ },
+ },
+ },
props: {
helpLink: {
type: String,
@@ -28,19 +35,17 @@ export default {
},
value: {
type: String,
- required: false,
- default: SNIPPET_VISIBILITY_PRIVATE,
+ required: true,
},
},
- computed: {
- visibilityOptions() {
- return [
- SNIPPET_VISIBILITY_PRIVATE,
- SNIPPET_VISIBILITY_INTERNAL,
- SNIPPET_VISIBILITY_PUBLIC,
- ].map(key => ({ value: key, ...SNIPPET_VISIBILITY[key] }));
- },
+ data() {
+ return {
+ visibilityLevels: [],
+ multipleLevelsRestricted: false,
+ };
},
+ SNIPPET_LEVELS_DISABLED,
+ SNIPPET_LEVELS_RESTRICTED,
};
</script>
<template>
@@ -51,10 +56,10 @@ export default {
><gl-icon :size="12" name="question"
/></gl-link>
</label>
- <gl-form-group id="visibility-level-setting">
- <gl-form-radio-group v-bind="$attrs" :checked="value" stacked v-on="$listeners">
+ <gl-form-group id="visibility-level-setting" class="gl-mb-0">
+ <gl-form-radio-group :checked="value" stacked v-bind="$attrs" v-on="$listeners">
<gl-form-radio
- v-for="option in visibilityOptions"
+ v-for="option in visibilityLevels"
:key="option.value"
:value="option.value"
class="mb-3"
@@ -71,5 +76,12 @@ export default {
</gl-form-radio>
</gl-form-radio-group>
</gl-form-group>
+
+ <div class="text-muted" data-testid="restricted-levels-info">
+ <template v-if="!visibilityLevels.length">{{ $options.SNIPPET_LEVELS_DISABLED }}</template>
+ <template v-else-if="multipleLevelsRestricted">{{
+ $options.SNIPPET_LEVELS_RESTRICTED
+ }}</template>
+ </div>
</div>
</template>
diff --git a/app/assets/javascripts/snippets/constants.js b/app/assets/javascripts/snippets/constants.js
index 12b83525bf7..e75922df15f 100644
--- a/app/assets/javascripts/snippets/constants.js
+++ b/app/assets/javascripts/snippets/constants.js
@@ -33,3 +33,15 @@ export const SNIPPET_BLOB_ACTION_MOVE = 'move';
export const SNIPPET_BLOB_ACTION_DELETE = 'delete';
export const SNIPPET_MAX_BLOBS = 10;
+
+export const SNIPPET_LEVELS_MAP = {
+ 0: SNIPPET_VISIBILITY_PRIVATE,
+ 10: SNIPPET_VISIBILITY_INTERNAL,
+ 20: SNIPPET_VISIBILITY_PUBLIC,
+};
+export const SNIPPET_LEVELS_RESTRICTED = __(
+ 'Other visibility settings have been disabled by the administrator.',
+);
+export const SNIPPET_LEVELS_DISABLED = __(
+ 'Visibility settings have been disabled by the administrator.',
+);
diff --git a/app/assets/javascripts/snippets/index.js b/app/assets/javascripts/snippets/index.js
index bb5e7d6e3f0..c70ad9b95f8 100644
--- a/app/assets/javascripts/snippets/index.js
+++ b/app/assets/javascripts/snippets/index.js
@@ -5,6 +5,7 @@ import createDefaultClient from '~/lib/graphql';
import SnippetsShow from './components/show.vue';
import SnippetsEdit from './components/edit.vue';
+import { SNIPPET_LEVELS_MAP, SNIPPET_VISIBILITY_PRIVATE } from '~/snippets/constants';
Vue.use(VueApollo);
Vue.use(Translate);
@@ -18,13 +19,28 @@ function appFactory(el, Component) {
defaultClient: createDefaultClient(),
});
+ const {
+ visibilityLevels = '[]',
+ selectedLevel,
+ multipleLevelsRestricted,
+ ...restDataset
+ } = el.dataset;
+
+ apolloProvider.clients.defaultClient.cache.writeData({
+ data: {
+ visibilityLevels: JSON.parse(visibilityLevels),
+ selectedLevel: SNIPPET_LEVELS_MAP[selectedLevel] ?? SNIPPET_VISIBILITY_PRIVATE,
+ multipleLevelsRestricted: 'multipleLevelsRestricted' in el.dataset,
+ },
+ });
+
return new Vue({
el,
apolloProvider,
render(createElement) {
return createElement(Component, {
props: {
- ...el.dataset,
+ ...restDataset,
},
});
},
diff --git a/app/assets/javascripts/snippets/mixins/snippets.js b/app/assets/javascripts/snippets/mixins/snippets.js
index 3f5d64a768f..15daaa8d84a 100644
--- a/app/assets/javascripts/snippets/mixins/snippets.js
+++ b/app/assets/javascripts/snippets/mixins/snippets.js
@@ -2,7 +2,6 @@ import GetSnippetQuery from '../queries/snippet.query.graphql';
const blobsDefault = [];
-// eslint-disable-next-line import/prefer-default-export
export const getSnippetMixin = {
apollo: {
snippet: {
diff --git a/app/assets/javascripts/snippets/queries/snippet_visibility.query.graphql b/app/assets/javascripts/snippets/queries/snippet_visibility.query.graphql
new file mode 100644
index 00000000000..5bd6c131bab
--- /dev/null
+++ b/app/assets/javascripts/snippets/queries/snippet_visibility.query.graphql
@@ -0,0 +1,5 @@
+query defaultSnippetVisibility {
+ visibilityLevels @client
+ selectedLevel @client
+ multipleLevelsRestricted @client
+}
diff --git a/app/assets/javascripts/snippets/utils/blob.js b/app/assets/javascripts/snippets/utils/blob.js
index fd5ff9a3d2e..21f52671801 100644
--- a/app/assets/javascripts/snippets/utils/blob.js
+++ b/app/assets/javascripts/snippets/utils/blob.js
@@ -4,6 +4,8 @@ import {
SNIPPET_BLOB_ACTION_UPDATE,
SNIPPET_BLOB_ACTION_MOVE,
SNIPPET_BLOB_ACTION_DELETE,
+ SNIPPET_LEVELS_MAP,
+ SNIPPET_VISIBILITY,
} from '../constants';
const createLocalId = () => uniqueId('blob_local_');
@@ -64,3 +66,16 @@ export const diffAll = (blobs, origBlobs) => {
return [...deletedEntries, ...newEntries];
};
+
+export const defaultSnippetVisibilityLevels = arr => {
+ if (Array.isArray(arr)) {
+ return arr.map(l => {
+ const translatedLevel = SNIPPET_LEVELS_MAP[l];
+ return {
+ value: translatedLevel,
+ ...SNIPPET_VISIBILITY[translatedLevel],
+ };
+ });
+ }
+ return [];
+};
diff --git a/app/assets/javascripts/static_site_editor/components/edit_area.vue b/app/assets/javascripts/static_site_editor/components/edit_area.vue
index 53fbb2a330d..e602f26acdf 100644
--- a/app/assets/javascripts/static_site_editor/components/edit_area.vue
+++ b/app/assets/javascripts/static_site_editor/components/edit_area.vue
@@ -2,6 +2,7 @@
import RichContentEditor from '~/vue_shared/components/rich_content_editor/rich_content_editor.vue';
import PublishToolbar from './publish_toolbar.vue';
import EditHeader from './edit_header.vue';
+import EditDrawer from './edit_drawer.vue';
import UnsavedChangesConfirmDialog from './unsaved_changes_confirm_dialog.vue';
import parseSourceFile from '~/static_site_editor/services/parse_source_file';
import { EDITOR_TYPES } from '~/vue_shared/components/rich_content_editor/constants';
@@ -15,6 +16,7 @@ export default {
RichContentEditor,
PublishToolbar,
EditHeader,
+ EditDrawer,
UnsavedChangesConfirmDialog,
},
props: {
@@ -48,6 +50,8 @@ export default {
parsedSource: parseSourceFile(this.preProcess(true, this.content)),
editorMode: EDITOR_TYPES.wysiwyg,
isModified: false,
+ hasMatter: false,
+ isDrawerOpen: false,
};
},
imageRepository: imageRepository(),
@@ -55,10 +59,19 @@ export default {
editableContent() {
return this.parsedSource.content(this.isWysiwygMode);
},
+ editableMatter() {
+ return this.isDrawerOpen ? this.parsedSource.matter() : {};
+ },
+ hasSettings() {
+ return this.hasMatter && this.isWysiwygMode;
+ },
isWysiwygMode() {
return this.editorMode === EDITOR_TYPES.wysiwyg;
},
},
+ created() {
+ this.refreshEditHelpers();
+ },
methods: {
preProcess(isWrap, value) {
const formattedContent = formatter(value);
@@ -67,9 +80,21 @@ export default {
: templater.unwrap(formattedContent);
return templatedContent;
},
- onInputChange(newVal) {
- this.parsedSource.sync(newVal, this.isWysiwygMode);
+ refreshEditHelpers() {
this.isModified = this.parsedSource.isModified();
+ this.hasMatter = this.parsedSource.hasMatter();
+ },
+ onDrawerOpen() {
+ this.isDrawerOpen = true;
+ this.refreshEditHelpers();
+ },
+ onDrawerClose() {
+ this.isDrawerOpen = false;
+ this.refreshEditHelpers();
+ },
+ onInputChange(newVal) {
+ this.parsedSource.syncContent(newVal, this.isWysiwygMode);
+ this.refreshEditHelpers();
},
onModeChange(mode) {
this.editorMode = mode;
@@ -77,6 +102,9 @@ export default {
const preProcessedContent = this.preProcess(this.isWysiwygMode, this.editableContent);
this.$refs.editor.resetInitialValue(preProcessedContent);
},
+ onUpdateSettings(settings) {
+ this.parsedSource.syncMatter(settings);
+ },
onUploadImage({ file, imageUrl }) {
this.$options.imageRepository.add(file, imageUrl);
},
@@ -93,12 +121,19 @@ export default {
<template>
<div class="d-flex flex-grow-1 flex-column h-100">
<edit-header class="py-2" :title="title" />
+ <edit-drawer
+ v-if="hasMatter"
+ :is-open="isDrawerOpen"
+ :settings="editableMatter"
+ @close="onDrawerClose"
+ @updateSettings="onUpdateSettings"
+ />
<rich-content-editor
ref="editor"
:content="editableContent"
:initial-edit-type="editorMode"
:image-root="imageRoot"
- class="mb-9 h-100"
+ class="mb-9 pb-6 h-100"
@modeChange="onModeChange"
@input="onInputChange"
@uploadImage="onUploadImage"
@@ -106,9 +141,11 @@ export default {
<unsaved-changes-confirm-dialog :modified="isModified" />
<publish-toolbar
class="gl-fixed gl-left-0 gl-bottom-0 gl-w-full"
+ :has-settings="hasSettings"
:return-url="returnUrl"
:saveable="isModified"
:saving-changes="savingChanges"
+ @editSettings="onDrawerOpen"
@submit="onSubmit"
/>
</div>
diff --git a/app/assets/javascripts/static_site_editor/components/edit_drawer.vue b/app/assets/javascripts/static_site_editor/components/edit_drawer.vue
new file mode 100644
index 00000000000..0484d38dde0
--- /dev/null
+++ b/app/assets/javascripts/static_site_editor/components/edit_drawer.vue
@@ -0,0 +1,32 @@
+<script>
+import { GlDrawer } from '@gitlab/ui';
+import FrontMatterControls from './front_matter_controls.vue';
+
+export default {
+ components: {
+ GlDrawer,
+ FrontMatterControls,
+ },
+ props: {
+ isOpen: {
+ type: Boolean,
+ required: true,
+ },
+ settings: {
+ type: Object,
+ required: true,
+ },
+ },
+};
+</script>
+<template>
+ <gl-drawer class="gl-pt-8" :open="isOpen" @close="$emit('close')">
+ <template #header>{{ __('Page settings') }}</template>
+ <template>
+ <front-matter-controls
+ :settings="settings"
+ @updateSettings="$emit('updateSettings', $event)"
+ />
+ </template>
+ </gl-drawer>
+</template>
diff --git a/app/assets/javascripts/static_site_editor/components/front_matter_controls.vue b/app/assets/javascripts/static_site_editor/components/front_matter_controls.vue
new file mode 100644
index 00000000000..dad3907c3ff
--- /dev/null
+++ b/app/assets/javascripts/static_site_editor/components/front_matter_controls.vue
@@ -0,0 +1,57 @@
+<script>
+import { GlForm, GlFormInput, GlFormGroup } from '@gitlab/ui';
+import { humanize } from '~/lib/utils/text_utility';
+
+export default {
+ components: {
+ GlForm,
+ GlFormInput,
+ GlFormGroup,
+ },
+ props: {
+ settings: {
+ type: Object,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ editableSettings: { ...this.settings },
+ };
+ },
+ methods: {
+ getId(type, key) {
+ return `sse-front-matter-${type}-${key}`;
+ },
+ getIsSupported(val) {
+ return ['string', 'number'].includes(typeof val);
+ },
+ getLabel(str) {
+ return humanize(str);
+ },
+ onUpdate() {
+ this.$emit('updateSettings', { ...this.editableSettings });
+ },
+ },
+};
+</script>
+<template>
+ <gl-form>
+ <template v-for="(value, key) of editableSettings">
+ <gl-form-group
+ v-if="getIsSupported(value)"
+ :id="getId('form-group', key)"
+ :key="key"
+ :label="getLabel(key)"
+ :label-for="getId('control', key)"
+ >
+ <gl-form-input
+ :id="getId('control', key)"
+ v-model.lazy="editableSettings[key]"
+ type="text"
+ @input="onUpdate"
+ />
+ </gl-form-group>
+ </template>
+ </gl-form>
+</template>
diff --git a/app/assets/javascripts/static_site_editor/components/publish_toolbar.vue b/app/assets/javascripts/static_site_editor/components/publish_toolbar.vue
index 6cd2a4dd700..2d62964cb3b 100644
--- a/app/assets/javascripts/static_site_editor/components/publish_toolbar.vue
+++ b/app/assets/javascripts/static_site_editor/components/publish_toolbar.vue
@@ -6,6 +6,11 @@ export default {
GlButton,
},
props: {
+ hasSettings: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
returnUrl: {
type: String,
required: false,
@@ -31,12 +36,21 @@ export default {
s__('StaticSiteEditor|Return to site')
}}</gl-button>
<gl-button
+ v-if="hasSettings"
+ ref="settings"
+ :disabled="savingChanges"
+ @click="$emit('editSettings')"
+ >
+ {{ __('Settings') }}
+ </gl-button>
+ <gl-button
+ ref="submit"
variant="success"
:disabled="!saveable"
:loading="savingChanges"
@click="$emit('submit')"
>
- <span>{{ __('Submit Changes') }}</span>
+ {{ __('Submit changes') }}
</gl-button>
</div>
</div>
diff --git a/app/assets/javascripts/static_site_editor/services/formatter.js b/app/assets/javascripts/static_site_editor/services/formatter.js
index 92d5e8a5df8..9a5dcd307eb 100644
--- a/app/assets/javascripts/static_site_editor/services/formatter.js
+++ b/app/assets/javascripts/static_site_editor/services/formatter.js
@@ -1,3 +1,45 @@
+import { repeat } from 'lodash';
+
+const topLevelOrderedRegexp = /^\d{1,3}/;
+const nestedLineRegexp = /^\s+/;
+
+/**
+ * DISCLAIMER: This is a temporary fix that corrects the indentation
+ * spaces of list items. This workaround originates in the usage of
+ * the Static Site Editor to edit the Handbook. The Handbook uses a
+ * Markdown parser called Kramdown interprets lines indented
+ * with two spaces as content within a list. For example:
+ *
+ * 1. ordered list
+ * - nested unordered list
+ *
+ * The Static Site Editor uses a different Markdown parser based on the
+ * CommonMark specification (official Markdown spec) called ToastMark.
+ * When the SSE encounters a nested list with only two spaces, it flattens
+ * the list:
+ *
+ * 1. ordered list
+ * - nested unordered list
+ *
+ * This function attempts to correct this problem before the content is loaded
+ * by Toast UI.
+ */
+const correctNestedContentIndenation = source => {
+ const lines = source.split('\n');
+ let topLevelOrderedListDetected = false;
+
+ return lines
+ .reduce((result, line) => {
+ if (topLevelOrderedListDetected && nestedLineRegexp.test(line)) {
+ return [...result, line.replace(nestedLineRegexp, repeat(' ', 4))];
+ }
+
+ topLevelOrderedListDetected = topLevelOrderedRegexp.test(line);
+ return [...result, line];
+ }, [])
+ .join('\n');
+};
+
const removeOrphanedBrTags = source => {
/* Until the underlying Squire editor of Toast UI Editor resolves duplicate `<br>` tags, this
`replace` solution will clear out orphaned `<br>` tags that it generates. Additionally,
@@ -8,7 +50,7 @@ const removeOrphanedBrTags = source => {
};
const format = source => {
- return removeOrphanedBrTags(source);
+ return correctNestedContentIndenation(removeOrphanedBrTags(source));
};
export default format;
diff --git a/app/assets/javascripts/static_site_editor/services/image_service.js b/app/assets/javascripts/static_site_editor/services/image_service.js
index edc69d0579a..25ab1084572 100644
--- a/app/assets/javascripts/static_site_editor/services/image_service.js
+++ b/app/assets/javascripts/static_site_editor/services/image_service.js
@@ -1,4 +1,3 @@
-// eslint-disable-next-line import/prefer-default-export
export const getBinary = file => {
return new Promise((resolve, reject) => {
const reader = new FileReader();
diff --git a/app/assets/javascripts/static_site_editor/services/parse_source_file.js b/app/assets/javascripts/static_site_editor/services/parse_source_file.js
index 126dfe81b90..640186ee1d0 100644
--- a/app/assets/javascripts/static_site_editor/services/parse_source_file.js
+++ b/app/assets/javascripts/static_site_editor/services/parse_source_file.js
@@ -1,64 +1,40 @@
-const parseSourceFile = raw => {
- const frontMatterRegex = /(^---$[\s\S]*?^---$)/m;
- const preGroupedRegex = /([\s\S]*?)(^---$[\s\S]*?^---$)(\s*)([\s\S]*)/m; // preFrontMatter, frontMatter, spacing, and content
- let initial;
- let editable;
-
- const hasFrontMatter = source => frontMatterRegex.test(source);
+import grayMatter from 'gray-matter';
- const buildPayload = (source, header, spacing, body) => {
- return { raw: source, header, spacing, body };
- };
+const parseSourceFile = raw => {
+ const remake = source => grayMatter(source, {});
- const parse = source => {
- if (hasFrontMatter(source)) {
- const match = source.match(preGroupedRegex);
- const [, preFrontMatter, frontMatter, spacing, content] = match;
- const header = preFrontMatter + frontMatter;
+ let editable = remake(raw);
- return buildPayload(source, header, spacing, content);
+ const syncContent = (newVal, isBody) => {
+ if (isBody) {
+ editable.content = newVal;
+ } else {
+ editable = remake(newVal);
}
-
- return buildPayload(source, '', '', source);
};
- const syncEditable = () => {
- /*
- We re-parse as markdown editing could have added non-body changes (preFrontMatter, frontMatter, or spacing).
- Re-parsing additionally gets us the desired body that was extracted from the potentially mutated editable.raw
- */
- editable = parse(editable.raw);
- };
+ const trimmedEditable = () => grayMatter.stringify(editable).trim();
- const syncBodyToRaw = () => {
- editable.raw = `${editable.header}${editable.spacing}${editable.body}`;
- };
-
- const sync = (newVal, isBodyToRaw) => {
- const editableKey = isBodyToRaw ? 'body' : 'raw';
- editable[editableKey] = newVal;
+ const content = (isBody = false) => (isBody ? editable.content.trim() : trimmedEditable()); // gray-matter internally adds an eof newline so we trim to bypass, open issue: https://github.com/jonschlinkert/gray-matter/issues/96
- if (isBodyToRaw) {
- syncBodyToRaw();
- }
-
- syncEditable();
- };
+ const matter = () => editable.data;
- const content = (isBody = false) => {
- const editableKey = isBody ? 'body' : 'raw';
- return editable[editableKey];
+ const syncMatter = settings => {
+ const source = grayMatter.stringify(editable.content, settings);
+ syncContent(source);
};
- const isModified = () => initial.raw !== editable.raw;
+ const isModified = () => trimmedEditable() !== raw;
- initial = parse(raw);
- editable = parse(raw);
+ const hasMatter = () => editable.matter.length > 0;
return {
+ matter,
+ syncMatter,
content,
+ syncContent,
isModified,
- sync,
+ hasMatter,
};
};
diff --git a/app/assets/javascripts/subscription_select.js b/app/assets/javascripts/subscription_select.js
index 7206bbd7109..354ee00a977 100644
--- a/app/assets/javascripts/subscription_select.js
+++ b/app/assets/javascripts/subscription_select.js
@@ -1,11 +1,12 @@
import $ from 'jquery';
import { __ } from './locale';
+import initDeprecatedJQueryDropdown from '~/deprecated_jquery_dropdown';
export default function subscriptionSelect() {
$('.js-subscription-event').each((i, element) => {
const fieldName = $(element).data('fieldName');
- return $(element).glDropdown({
+ return initDeprecatedJQueryDropdown($(element), {
selectable: true,
fieldName,
toggleLabel(selected, el, instance) {
diff --git a/app/assets/javascripts/templates/issuable_template_selector.js b/app/assets/javascripts/templates/issuable_template_selector.js
index 10ad4170930..22bbd083a5d 100644
--- a/app/assets/javascripts/templates/issuable_template_selector.js
+++ b/app/assets/javascripts/templates/issuable_template_selector.js
@@ -33,7 +33,7 @@ export default class IssuableTemplateSelector extends TemplateSelector {
this.templateWarningEl.find('.js-close-btn').on('click', () => {
// Explicitly check against 0 value
if (this.previousSelectedIndex !== undefined) {
- this.dropdown.data('glDropdown').selectRowAtIndex(this.previousSelectedIndex);
+ this.dropdown.data('deprecatedJQueryDropdown').selectRowAtIndex(this.previousSelectedIndex);
} else {
this.reset();
}
@@ -61,7 +61,7 @@ export default class IssuableTemplateSelector extends TemplateSelector {
}
setSelectedIndex() {
- this.previousSelectedIndex = this.dropdown.data('glDropdown').selectedIndex;
+ this.previousSelectedIndex = this.dropdown.data('deprecatedJQueryDropdown').selectedIndex;
}
onDropdownClicked(query) {
diff --git a/app/assets/javascripts/tooltips/components/tooltips.vue b/app/assets/javascripts/tooltips/components/tooltips.vue
new file mode 100644
index 00000000000..8307f878def
--- /dev/null
+++ b/app/assets/javascripts/tooltips/components/tooltips.vue
@@ -0,0 +1,116 @@
+<script>
+import { GlTooltip, GlSafeHtmlDirective as SafeHtml } from '@gitlab/ui';
+import { uniqueId } from 'lodash';
+
+const getTooltipTitle = element => {
+ return element.getAttribute('title') || element.dataset.title;
+};
+
+const newTooltip = (element, config = {}) => {
+ const { placement, container, boundary, html, triggers } = element.dataset;
+ const title = getTooltipTitle(element);
+
+ return {
+ id: uniqueId('gl-tooltip'),
+ target: element,
+ title,
+ html,
+ placement,
+ container,
+ boundary,
+ triggers,
+ disabled: !title,
+ ...config,
+ };
+};
+
+export default {
+ components: {
+ GlTooltip,
+ },
+ directives: {
+ SafeHtml,
+ },
+ data() {
+ return {
+ tooltips: [],
+ };
+ },
+ created() {
+ this.observer = new MutationObserver(mutations => {
+ mutations.forEach(mutation => {
+ mutation.removedNodes.forEach(this.dispose);
+ });
+ });
+ },
+ beforeDestroy() {
+ this.observer.disconnect();
+ },
+ methods: {
+ addTooltips(elements, config) {
+ const newTooltips = elements
+ .filter(element => !this.tooltipExists(element))
+ .map(element => newTooltip(element, config));
+
+ newTooltips.forEach(tooltip => this.observe(tooltip));
+
+ this.tooltips.push(...newTooltips);
+ },
+ observe(tooltip) {
+ this.observer.observe(tooltip.target.parentElement, {
+ childList: true,
+ });
+ },
+ dispose(target) {
+ if (!target) {
+ this.tooltips = [];
+ } else {
+ const index = this.tooltips.indexOf(this.findTooltipByTarget(target));
+
+ if (index > -1) {
+ this.tooltips.splice(index, 1);
+ }
+ }
+ },
+ fixTitle(target) {
+ const tooltip = this.findTooltipByTarget(target);
+
+ if (tooltip) {
+ tooltip.title = target.getAttribute('title');
+ }
+ },
+ triggerEvent(target, event) {
+ const tooltip = this.findTooltipByTarget(target);
+
+ if (tooltip) {
+ this.$refs[tooltip.id][0].$emit(event);
+ }
+ },
+ tooltipExists(element) {
+ return Boolean(this.findTooltipByTarget(element));
+ },
+ findTooltipByTarget(element) {
+ return this.tooltips.find(tooltip => tooltip.target === element);
+ },
+ },
+};
+</script>
+<template>
+ <div>
+ <gl-tooltip
+ v-for="(tooltip, index) in tooltips"
+ :id="tooltip.id"
+ :ref="tooltip.id"
+ :key="index"
+ :target="tooltip.target"
+ :triggers="tooltip.triggers"
+ :placement="tooltip.placement"
+ :container="tooltip.container"
+ :boundary="tooltip.boundary"
+ :disabled="tooltip.disabled"
+ >
+ <span v-if="tooltip.html" v-safe-html="tooltip.title"></span>
+ <span v-else>{{ tooltip.title }}</span>
+ </gl-tooltip>
+ </div>
+</template>
diff --git a/app/assets/javascripts/tooltips/index.js b/app/assets/javascripts/tooltips/index.js
new file mode 100644
index 00000000000..cfbd88d6c40
--- /dev/null
+++ b/app/assets/javascripts/tooltips/index.js
@@ -0,0 +1,120 @@
+import Vue from 'vue';
+import jQuery from 'jquery';
+import { toArray, isFunction } from 'lodash';
+import Tooltips from './components/tooltips.vue';
+
+let app;
+
+const EVENTS_MAP = {
+ hover: 'mouseenter',
+ click: 'click',
+ focus: 'focus',
+};
+
+const DEFAULT_TRIGGER = 'hover focus';
+const APP_ELEMENT_ID = 'gl-tooltips-app';
+
+const tooltipsApp = () => {
+ if (!app) {
+ const container = document.createElement('div');
+
+ container.setAttribute('id', APP_ELEMENT_ID);
+ document.body.appendChild(container);
+
+ app = new Vue({
+ render(h) {
+ return h(Tooltips, {
+ props: {
+ elements: this.elements,
+ },
+ ref: 'tooltips',
+ });
+ },
+ }).$mount(container);
+ }
+
+ return app.$refs.tooltips;
+};
+
+const isTooltip = (node, selector) => node.matches && node.matches(selector);
+
+const addTooltips = (elements, config) => {
+ tooltipsApp().addTooltips(toArray(elements), config);
+};
+
+const handleTooltipEvent = (rootTarget, e, selector, config = {}) => {
+ for (let { target } = e; target && target !== rootTarget; target = target.parentNode) {
+ if (isTooltip(target, selector)) {
+ addTooltips([target], {
+ show: true,
+ ...config,
+ });
+ break;
+ }
+ }
+};
+
+const applyToElements = (elements, handler) => toArray(elements).forEach(handler);
+
+const invokeBootstrapApi = (elements, method) => {
+ if (isFunction(elements.tooltip)) {
+ jQuery(elements).tooltip(method);
+ }
+};
+
+const isGlTooltipsEnabled = () => Boolean(window.gon.glTooltipsEnabled);
+
+const tooltipApiInvoker = ({ glHandler, bsHandler }) => (elements, ...params) => {
+ if (isGlTooltipsEnabled()) {
+ applyToElements(elements, glHandler);
+ } else {
+ bsHandler(elements, ...params);
+ }
+};
+
+export const initTooltips = (config = {}) => {
+ if (isGlTooltipsEnabled()) {
+ const triggers = config?.triggers || DEFAULT_TRIGGER;
+ const events = triggers.split(' ').map(trigger => EVENTS_MAP[trigger]);
+
+ events.forEach(event => {
+ document.addEventListener(
+ event,
+ e => handleTooltipEvent(document, e, config.selector, config),
+ true,
+ );
+ });
+
+ return tooltipsApp();
+ }
+
+ return invokeBootstrapApi(document.body, config);
+};
+export const dispose = tooltipApiInvoker({
+ glHandler: element => tooltipsApp().dispose(element),
+ bsHandler: elements => invokeBootstrapApi(elements, 'dispose'),
+});
+export const fixTitle = tooltipApiInvoker({
+ glHandler: element => tooltipsApp().fixTitle(element),
+ bsHandler: elements => invokeBootstrapApi(elements, '_fixTitle'),
+});
+export const enable = tooltipApiInvoker({
+ glHandler: element => tooltipsApp().triggerEvent(element, 'enable'),
+ bsHandler: elements => invokeBootstrapApi(elements, 'enable'),
+});
+export const disable = tooltipApiInvoker({
+ glHandler: element => tooltipsApp().triggerEvent(element, 'disable'),
+ bsHandler: elements => invokeBootstrapApi(elements, 'disable'),
+});
+export const hide = tooltipApiInvoker({
+ glHandler: element => tooltipsApp().triggerEvent(element, 'close'),
+ bsHandler: elements => invokeBootstrapApi(elements, 'hide'),
+});
+export const show = tooltipApiInvoker({
+ glHandler: element => tooltipsApp().triggerEvent(element, 'open'),
+ bsHandler: elements => invokeBootstrapApi(elements, 'show'),
+});
+export const destroy = () => {
+ tooltipsApp().$destroy();
+ app = null;
+};
diff --git a/app/assets/javascripts/tracking.js b/app/assets/javascripts/tracking.js
index 10510595570..37ebe6b6c4d 100644
--- a/app/assets/javascripts/tracking.js
+++ b/app/assets/javascripts/tracking.js
@@ -126,14 +126,18 @@ export function initUserTracking() {
const opts = { ...DEFAULT_SNOWPLOW_OPTIONS, ...window.snowplowOptions };
window.snowplow('newTracker', opts.namespace, opts.hostname, opts);
+ document.dispatchEvent(new Event('SnowplowInitialized'));
+}
+
+export function initDefaultTrackers() {
+ if (!Tracking.enabled()) return;
+
window.snowplow('enableActivityTracking', 30, 30);
window.snowplow('trackPageView'); // must be after enableActivityTracking
- if (opts.formTracking) window.snowplow('enableFormTracking');
- if (opts.linkClickTracking) window.snowplow('enableLinkClickTracking');
+ if (window.snowplowOptions.formTracking) window.snowplow('enableFormTracking');
+ if (window.snowplowOptions.linkClickTracking) window.snowplow('enableLinkClickTracking');
Tracking.bindDocument();
Tracking.trackLoadEvents();
-
- document.dispatchEvent(new Event('SnowplowInitialized'));
}
diff --git a/app/assets/javascripts/ui_development_kit.js b/app/assets/javascripts/ui_development_kit.js
index be18ac5da24..4f32e143de8 100644
--- a/app/assets/javascripts/ui_development_kit.js
+++ b/app/assets/javascripts/ui_development_kit.js
@@ -1,8 +1,9 @@
import $ from 'jquery';
import Api from './api';
+import initDeprecatedJQueryDropdown from '~/deprecated_jquery_dropdown';
export default () => {
- $('#js-project-dropdown').glDropdown({
+ initDeprecatedJQueryDropdown($('#js-project-dropdown'), {
data: (term, callback) => {
Api.projects(
term,
diff --git a/app/assets/javascripts/users_select/index.js b/app/assets/javascripts/users_select/index.js
index e45b0de9083..5f4260f26ff 100644
--- a/app/assets/javascripts/users_select/index.js
+++ b/app/assets/javascripts/users_select/index.js
@@ -11,8 +11,9 @@ import {
import axios from '../lib/utils/axios_utils';
import { s__, __, sprintf } from '../locale';
import ModalStore from '../boards/stores/modal_store';
-import { parseBoolean } from '../lib/utils/common_utils';
+import { parseBoolean, spriteIcon } from '../lib/utils/common_utils';
import { getAjaxUsersSelectOptions, getAjaxUsersSelectParams } from './utils';
+import initDeprecatedJQueryDropdown from '~/deprecated_jquery_dropdown';
// TODO: remove eventHub hack after code splitting refactor
window.emitSidebarEvent = window.emitSidebarEvent || $.noop;
@@ -54,6 +55,7 @@ function UsersSelect(currentUser, els, options = {}) {
const defaultLabel = $dropdown.data('defaultLabel');
const issueURL = $dropdown.data('issueUpdate');
const $selectbox = $dropdown.closest('.selectbox');
+ const $assignToMeLink = $selectbox.next('.assign-to-me-link');
let $block = $selectbox.closest('.block');
const abilityName = $dropdown.data('abilityName');
let $value = $block.find('.value');
@@ -160,7 +162,7 @@ function UsersSelect(currentUser, els, options = {}) {
});
};
- $('.assign-to-me-link').on('click', e => {
+ $assignToMeLink.on('click', e => {
e.preventDefault();
$(e.currentTarget).hide();
@@ -224,7 +226,9 @@ function UsersSelect(currentUser, els, options = {}) {
});
};
collapsedAssigneeTemplate = template(
- '<% if( avatar ) { %> <a class="author-link" href="/<%- username %>"> <img width="24" class="avatar avatar-inline s24" alt="" src="<%- avatar %>"> </a> <% } else { %> <i class="fa fa-user"></i> <% } %>',
+ `<% if( avatar ) { %> <a class="author-link" href="/<%- username %>"> <img width="24" class="avatar avatar-inline s24" alt="" src="<%- avatar %>"> </a> <% } else { %> ${spriteIcon(
+ 'user',
+ )} <% } %>`,
);
assigneeTemplate = template(
`<% if (username) { %> <a class="author-link bold" href="/<%- username %>"> <% if( avatar ) { %> <img width="32" class="avatar avatar-inline s32" alt="" src="<%- avatar %>"> <% } %> <span class="author"><%- name %></span> <span class="username"> @<%- username %> </span> </a> <% } else { %> <span class="no-value assign-yourself">
@@ -233,14 +237,14 @@ function UsersSelect(currentUser, els, options = {}) {
closingTag: '</a>',
})}</span> <% } %>`,
);
- return $dropdown.glDropdown({
+ return initDeprecatedJQueryDropdown($dropdown, {
showMenuAbove,
data(term, callback) {
return userSelect.users(term, options, users => {
// GitLabDropdownFilter returns this.instance
// GitLabDropdownRemote returns this.options.instance
- const glDropdown = this.instance || this.options.instance;
- glDropdown.options.processData(term, users, callback);
+ const deprecatedJQueryDropdown = this.instance || this.options.instance;
+ deprecatedJQueryDropdown.options.processData(term, users, callback);
});
},
processData(term, data, callback) {
@@ -349,7 +353,7 @@ function UsersSelect(currentUser, els, options = {}) {
callback(users);
if (showMenuAbove) {
- $dropdown.data('glDropdown').positionMenuAbove();
+ $dropdown.data('deprecatedJQueryDropdown').positionMenuAbove();
}
},
filterable: true,
@@ -359,13 +363,13 @@ function UsersSelect(currentUser, els, options = {}) {
},
selectable: true,
fieldName: $dropdown.data('fieldName'),
- toggleLabel(selected, el, glDropdown) {
- const inputValue = glDropdown.filterInput.val();
+ toggleLabel(selected, el, deprecatedJQueryDropdown) {
+ const inputValue = deprecatedJQueryDropdown.filterInput.val();
if (this.multiSelect && inputValue === '') {
// Remove non-users from the fullData array
- const users = glDropdown.filteredFullData();
- const callback = glDropdown.parseData.bind(glDropdown);
+ const users = deprecatedJQueryDropdown.filteredFullData();
+ const callback = deprecatedJQueryDropdown.parseData.bind(deprecatedJQueryDropdown);
// Update the data model
this.processData(inputValue, users, callback);
@@ -448,9 +452,9 @@ function UsersSelect(currentUser, els, options = {}) {
}
if (getSelected().find(u => u === gon.current_user_id)) {
- $('.assign-to-me-link').hide();
+ $assignToMeLink.hide();
} else {
- $('.assign-to-me-link').show();
+ $assignToMeLink.show();
}
}
@@ -557,92 +561,99 @@ function UsersSelect(currentUser, els, options = {}) {
},
});
});
- import(/* webpackChunkName: 'select2' */ 'select2/select2')
- .then(() => {
- $('.ajax-users-select').each((i, select) => {
- const options = getAjaxUsersSelectOptions($(select), AJAX_USERS_SELECT_OPTIONS_MAP);
- options.skipLdap = $(select).hasClass('skip_ldap');
- const showNullUser = $(select).data('nullUser');
- const showAnyUser = $(select).data('anyUser');
- const showEmailUser = $(select).data('emailUser');
- const firstUser = $(select).data('firstUser');
- return $(select).select2({
- placeholder: __('Search for a user'),
- multiple: $(select).hasClass('multiselect'),
- minimumInputLength: 0,
- query(query) {
- return userSelect.users(query.term, options, users => {
- let name;
- const data = {
- results: users,
- };
- if (query.term.length === 0) {
- if (firstUser) {
- // Move current user to the front of the list
- const ref = data.results;
-
- for (let index = 0, len = ref.length; index < len; index += 1) {
- const obj = ref[index];
- if (obj.username === firstUser) {
- data.results.splice(index, 1);
- data.results.unshift(obj);
- break;
+
+ if ($('.ajax-users-select').length) {
+ import(/* webpackChunkName: 'select2' */ 'select2/select2')
+ .then(() => {
+ $('.ajax-users-select').each((i, select) => {
+ const options = getAjaxUsersSelectOptions($(select), AJAX_USERS_SELECT_OPTIONS_MAP);
+ options.skipLdap = $(select).hasClass('skip_ldap');
+ const showNullUser = $(select).data('nullUser');
+ const showAnyUser = $(select).data('anyUser');
+ const showEmailUser = $(select).data('emailUser');
+ const firstUser = $(select).data('firstUser');
+ return $(select).select2({
+ placeholder: __('Search for a user'),
+ multiple: $(select).hasClass('multiselect'),
+ minimumInputLength: 0,
+ query(query) {
+ return userSelect.users(query.term, options, users => {
+ let name;
+ const data = {
+ results: users,
+ };
+ if (query.term.length === 0) {
+ if (firstUser) {
+ // Move current user to the front of the list
+ const ref = data.results;
+
+ for (let index = 0, len = ref.length; index < len; index += 1) {
+ const obj = ref[index];
+ if (obj.username === firstUser) {
+ data.results.splice(index, 1);
+ data.results.unshift(obj);
+ break;
+ }
}
}
- }
- if (showNullUser) {
- const nullUser = {
- name: s__('UsersSelect|Unassigned'),
- id: 0,
- };
- data.results.unshift(nullUser);
- }
- if (showAnyUser) {
- name = showAnyUser;
- if (name === true) {
- name = s__('UsersSelect|Any User');
+ if (showNullUser) {
+ const nullUser = {
+ name: s__('UsersSelect|Unassigned'),
+ id: 0,
+ };
+ data.results.unshift(nullUser);
+ }
+ if (showAnyUser) {
+ name = showAnyUser;
+ if (name === true) {
+ name = s__('UsersSelect|Any User');
+ }
+ const anyUser = {
+ name,
+ id: null,
+ };
+ data.results.unshift(anyUser);
}
- const anyUser = {
- name,
- id: null,
+ }
+ if (
+ showEmailUser &&
+ data.results.length === 0 &&
+ query.term.match(/^[^@]+@[^@]+$/)
+ ) {
+ const trimmed = query.term.trim();
+ const emailUser = {
+ name: sprintf(__('Invite "%{trimmed}" by email'), { trimmed }),
+ username: trimmed,
+ id: trimmed,
+ invite: true,
};
- data.results.unshift(anyUser);
+ data.results.unshift(emailUser);
}
- }
- if (showEmailUser && data.results.length === 0 && query.term.match(/^[^@]+@[^@]+$/)) {
- const trimmed = query.term.trim();
- const emailUser = {
- name: sprintf(__('Invite "%{trimmed}" by email'), { trimmed }),
- username: trimmed,
- id: trimmed,
- invite: true,
- };
- data.results.unshift(emailUser);
- }
- return query.callback(data);
- });
- },
- initSelection() {
- const args = 1 <= arguments.length ? [].slice.call(arguments, 0) : [];
- return userSelect.initSelection.apply(userSelect, args);
- },
- formatResult() {
- const args = 1 <= arguments.length ? [].slice.call(arguments, 0) : [];
- return userSelect.formatResult.apply(userSelect, args);
- },
- formatSelection() {
- const args = 1 <= arguments.length ? [].slice.call(arguments, 0) : [];
- return userSelect.formatSelection.apply(userSelect, args);
- },
- dropdownCssClass: 'ajax-users-dropdown',
- // we do not want to escape markup since we are displaying html in results
- escapeMarkup(m) {
- return m;
- },
+ return query.callback(data);
+ });
+ },
+ initSelection() {
+ const args = 1 <= arguments.length ? [].slice.call(arguments, 0) : [];
+ return userSelect.initSelection.apply(userSelect, args);
+ },
+ formatResult() {
+ const args = 1 <= arguments.length ? [].slice.call(arguments, 0) : [];
+ return userSelect.formatResult.apply(userSelect, args);
+ },
+ formatSelection() {
+ const args = 1 <= arguments.length ? [].slice.call(arguments, 0) : [];
+ return userSelect.formatSelection.apply(userSelect, args);
+ },
+ dropdownCssClass: 'ajax-users-dropdown',
+ // we do not want to escape markup since we are displaying html in results
+ escapeMarkup(m) {
+ return m;
+ },
+ });
});
- });
- })
- .catch(() => {});
+ })
+ .catch(() => {});
+ }
}
UsersSelect.prototype.initSelection = function(element, callback) {
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/approvals/approvals_summary_optional.vue b/app/assets/javascripts/vue_merge_request_widget/components/approvals/approvals_summary_optional.vue
index 24cd9d6428d..55fa24fb51a 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/approvals/approvals_summary_optional.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/approvals/approvals_summary_optional.vue
@@ -1,11 +1,10 @@
<script>
-import { GlTooltipDirective, GlLink } from '@gitlab/ui';
-import Icon from '~/vue_shared/components/icon.vue';
+import { GlTooltipDirective, GlLink, GlIcon } from '@gitlab/ui';
export default {
components: {
GlLink,
- Icon,
+ GlIcon,
},
directives: {
GlTooltip: GlTooltipDirective,
@@ -35,7 +34,7 @@ export default {
target="_blank"
class="d-flex-center pl-1"
>
- <icon name="question" />
+ <gl-icon name="question" />
</gl-link>
</div>
</template>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/deployment/constants.js b/app/assets/javascripts/vue_merge_request_widget/components/deployment/constants.js
index a7ab11290eb..66de4f8b682 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/deployment/constants.js
+++ b/app/assets/javascripts/vue_merge_request_widget/components/deployment/constants.js
@@ -11,3 +11,9 @@ export const CANCELED = 'canceled';
export const STOPPING = 'stopping';
export const DEPLOYING = 'deploying';
export const REDEPLOYING = 'redeploying';
+
+export const ACT_BUTTON_ICONS = {
+ play: 'play',
+ repeat: 'repeat',
+ stop: 'stop',
+};
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/deployment/deployment_action_button.vue b/app/assets/javascripts/vue_merge_request_widget/components/deployment/deployment_action_button.vue
index 7d74d5531b4..cc3efae565a 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/deployment/deployment_action_button.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/deployment/deployment_action_button.vue
@@ -1,12 +1,12 @@
<script>
-import { GlTooltipDirective, GlDeprecatedButton } from '@gitlab/ui';
+import { GlTooltipDirective, GlButton } from '@gitlab/ui';
import { __ } from '~/locale';
import { RUNNING } from './constants';
export default {
name: 'DeploymentActionButton',
components: {
- GlDeprecatedButton,
+ GlButton,
},
directives: {
GlTooltip: GlTooltipDirective,
@@ -35,6 +35,10 @@ export default {
required: false,
default: '',
},
+ icon: {
+ type: String,
+ required: true,
+ },
},
computed: {
isActionInProgress() {
@@ -58,18 +62,19 @@ export default {
</script>
<template>
- <span v-gl-tooltip :title="actionInProgressTooltip" class="d-inline-block" tabindex="0">
- <gl-deprecated-button
+ <span v-gl-tooltip :title="actionInProgressTooltip" class="gl-display-inline-block" tabindex="0">
+ <gl-button
v-gl-tooltip
+ category="primary"
+ size="small"
:title="buttonTitle"
:loading="isLoading"
:disabled="isActionInProgress"
- :class="`btn btn-default btn-sm inline gl-ml-2 ${containerClasses}`"
+ :class="`inline gl-ml-2 ${containerClasses}`"
+ :icon="icon"
@click="$emit('click')"
>
- <span class="d-inline-flex align-items-baseline">
- <slot> </slot>
- </span>
- </gl-deprecated-button>
+ <slot> </slot>
+ </gl-button>
</span>
</template>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/deployment/deployment_actions.vue b/app/assets/javascripts/vue_merge_request_widget/components/deployment/deployment_actions.vue
index af0b4087d46..208df03b6a4 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/deployment/deployment_actions.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/deployment/deployment_actions.vue
@@ -1,5 +1,4 @@
<script>
-import { GlIcon } from '@gitlab/ui';
import { __, s__ } from '~/locale';
import { deprecatedCreateFlash as createFlash } from '~/flash';
import { visitUrl } from '~/lib/utils/url_utility';
@@ -7,14 +6,22 @@ import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import MRWidgetService from '../../services/mr_widget_service';
import DeploymentActionButton from './deployment_action_button.vue';
import DeploymentViewButton from './deployment_view_button.vue';
-import { MANUAL_DEPLOY, FAILED, SUCCESS, STOPPING, DEPLOYING, REDEPLOYING } from './constants';
+import {
+ MANUAL_DEPLOY,
+ FAILED,
+ SUCCESS,
+ STOPPING,
+ DEPLOYING,
+ REDEPLOYING,
+ ACT_BUTTON_ICONS,
+} from './constants';
export default {
name: 'DeploymentActions',
+ btnIcons: ACT_BUTTON_ICONS,
components: {
DeploymentActionButton,
DeploymentViewButton,
- GlIcon,
},
mixins: [glFeatureFlagsMixin()],
props: {
@@ -151,10 +158,10 @@ export default {
:action-in-progress="actionInProgress"
:actions-configuration="$options.actionsConfiguration[constants.DEPLOYING]"
:computed-deployment-status="computedDeploymentStatus"
+ :icon="$options.btnIcons.play"
container-classes="js-manual-deploy-action"
@click="deployManually"
>
- <gl-icon name="play" />
<span>{{ $options.actionsConfiguration[constants.DEPLOYING].buttonText }}</span>
</deployment-action-button>
<deployment-action-button
@@ -162,10 +169,10 @@ export default {
:action-in-progress="actionInProgress"
:actions-configuration="$options.actionsConfiguration[constants.REDEPLOYING]"
:computed-deployment-status="computedDeploymentStatus"
+ :icon="$options.btnIcons.repeat"
container-classes="js-manual-redeploy-action"
@click="redeploy"
>
- <gl-icon name="repeat" />
<span>{{ $options.actionsConfiguration[constants.REDEPLOYING].buttonText }}</span>
</deployment-action-button>
<deployment-view-button
@@ -181,10 +188,9 @@ export default {
:computed-deployment-status="computedDeploymentStatus"
:actions-configuration="$options.actionsConfiguration[constants.STOPPING]"
:button-title="$options.actionsConfiguration[constants.STOPPING].buttonText"
+ :icon="$options.btnIcons.stop"
container-classes="js-stop-env"
@click="stopEnvironment"
- >
- <gl-icon name="stop" />
- </deployment-action-button>
+ />
</div>
</template>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/deployment/deployment_view_button.vue b/app/assets/javascripts/vue_merge_request_widget/components/deployment/deployment_view_button.vue
index b12250d1d1c..157d6d60290 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/deployment/deployment_view_button.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/deployment/deployment_view_button.vue
@@ -1,17 +1,23 @@
<script>
-import { GlLink } from '@gitlab/ui';
-import FilteredSearchDropdown from '~/vue_shared/components/filtered_search_dropdown.vue';
+import { GlButtonGroup, GlDropdown, GlDropdownItem, GlLink, GlSearchBoxByType } from '@gitlab/ui';
+import autofocusonshow from '~/vue_shared/directives/autofocusonshow';
import ReviewAppLink from '../review_app_link.vue';
export default {
name: 'DeploymentViewButton',
components: {
- FilteredSearchDropdown,
+ GlButtonGroup,
+ GlDropdown,
+ GlDropdownItem,
GlLink,
+ GlSearchBoxByType,
ReviewAppLink,
VisualReviewAppLink: () =>
import('ee_component/vue_merge_request_widget/components/visual_review_app_link.vue'),
},
+ directives: {
+ autofocusonshow,
+ },
props: {
appButtonText: {
type: Object,
@@ -37,6 +43,9 @@ export default {
}),
},
},
+ data() {
+ return { searchTerm: '' };
+ },
computed: {
deploymentExternalUrl() {
if (this.deployment.changes && this.deployment.changes.length === 1) {
@@ -47,44 +56,52 @@ export default {
shouldRenderDropdown() {
return this.deployment.changes && this.deployment.changes.length > 1;
},
+ filteredChanges() {
+ return this.deployment?.changes?.filter(change => change.path.includes(this.searchTerm));
+ },
},
};
</script>
-
<template>
<span>
- <filtered-search-dropdown
- v-if="shouldRenderDropdown"
- class="js-mr-wigdet-deployment-dropdown inline"
- :items="deployment.changes"
- :main-action-link="deploymentExternalUrl"
- filter-key="path"
- >
- <template #mainAction="{ className }">
- <review-app-link
- :display="appButtonText"
- :link="deploymentExternalUrl"
- :css-class="`deploy-link js-deploy-url inline ${className}`"
+ <gl-button-group v-if="shouldRenderDropdown" size="small">
+ <review-app-link
+ :display="appButtonText"
+ :link="deploymentExternalUrl"
+ size="small"
+ css-class="deploy-link js-deploy-url inline"
+ />
+ <gl-dropdown size="small" class="js-mr-wigdet-deployment-dropdown">
+ <gl-search-box-by-type
+ v-model.trim="searchTerm"
+ v-autofocusonshow
+ autofocus
+ class="gl-m-3"
/>
- </template>
-
- <template #result="{ result }">
- <gl-link
- :href="result.external_url"
- target="_blank"
- rel="noopener noreferrer nofollow"
- class="js-deploy-url-menu-item menu-item"
+ <gl-dropdown-item
+ v-for="change in filteredChanges"
+ :key="change.path"
+ class="js-filtered-dropdown-result"
>
- <strong class="str-truncated-100 gl-mb-0 d-block">{{ result.path }}</strong>
-
- <p class="text-secondary str-truncated-100 gl-mb-0 d-block">{{ result.external_url }}</p>
- </gl-link>
- </template>
- </filtered-search-dropdown>
+ <gl-link
+ :href="change.external_url"
+ target="_blank"
+ rel="noopener noreferrer nofollow"
+ class="js-deploy-url-menu-item menu-item"
+ >
+ <strong class="str-truncated-100 gl-mb-0 gl-display-block">{{ change.path }}</strong>
+ <p class="text-secondary str-truncated-100 gl-mb-0 d-block">
+ {{ change.external_url }}
+ </p>
+ </gl-link>
+ </gl-dropdown-item>
+ </gl-dropdown>
+ </gl-button-group>
<review-app-link
v-else
:display="appButtonText"
:link="deploymentExternalUrl"
+ size="small"
css-class="js-deploy-url deploy-link btn btn-default btn-sm inline"
/>
<visual-review-app-link
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/deployment/memory_usage.vue b/app/assets/javascripts/vue_merge_request_widget/components/deployment/memory_usage.vue
index fe41a15979e..9b2cd41092e 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/deployment/memory_usage.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/deployment/memory_usage.vue
@@ -1,5 +1,6 @@
<script>
-import { sprintf, s__ } from '~/locale';
+import { GlLoadingIcon, GlSprintf, GlLink } from '@gitlab/ui';
+import { s__ } from '~/locale';
import statusCodes from '~/lib/utils/http_status';
import { bytesToMiB } from '~/lib/utils/number_utils';
import { backOff } from '~/lib/utils/common_utils';
@@ -10,6 +11,9 @@ export default {
name: 'MemoryUsage',
components: {
MemoryGraph,
+ GlLoadingIcon,
+ GlSprintf,
+ GlLink,
},
props: {
metricsUrl: {
@@ -47,45 +51,22 @@ export default {
return !this.loadingMetrics && !this.hasMetrics && !this.loadFailed;
},
memoryChangeMessage() {
- const messageProps = {
- memoryFrom: this.memoryFrom,
- memoryTo: this.memoryTo,
- metricsLinkStart: `<a href="${this.metricsMonitoringUrl}">`,
- metricsLinkEnd: '</a>',
- emphasisStart: '<b>',
- emphasisEnd: '</b>',
- };
const memoryTo = Number(this.memoryTo);
const memoryFrom = Number(this.memoryFrom);
- let memoryUsageMsg = '';
if (memoryTo > memoryFrom) {
- memoryUsageMsg = sprintf(
- s__(
- 'mrWidget|%{metricsLinkStart} Memory %{metricsLinkEnd} usage %{emphasisStart} increased %{emphasisEnd} from %{memoryFrom}MB to %{memoryTo}MB',
- ),
- messageProps,
- false,
+ return s__(
+ 'mrWidget|%{metricsLinkStart} Memory %{metricsLinkEnd} usage %{emphasisStart} increased %{emphasisEnd} from %{memoryFrom}MB to %{memoryTo}MB',
);
} else if (memoryTo < memoryFrom) {
- memoryUsageMsg = sprintf(
- s__(
- 'mrWidget|%{metricsLinkStart} Memory %{metricsLinkEnd} usage %{emphasisStart} decreased %{emphasisEnd} from %{memoryFrom}MB to %{memoryTo}MB',
- ),
- messageProps,
- false,
- );
- } else {
- memoryUsageMsg = sprintf(
- s__(
- 'mrWidget|%{metricsLinkStart} Memory %{metricsLinkEnd} usage is %{emphasisStart} unchanged %{emphasisEnd} at %{memoryFrom}MB',
- ),
- messageProps,
- false,
+ return s__(
+ 'mrWidget|%{metricsLinkStart} Memory %{metricsLinkEnd} usage %{emphasisStart} decreased %{emphasisEnd} from %{memoryFrom}MB to %{memoryTo}MB',
);
}
- return memoryUsageMsg;
+ return s__(
+ 'mrWidget|%{metricsLinkStart} Memory %{metricsLinkEnd} usage is %{emphasisStart} unchanged %{emphasisEnd} at %{memoryFrom}MB',
+ );
},
},
mounted() {
@@ -155,14 +136,23 @@ export default {
<template>
<div class="mr-info-list clearfix mr-memory-usage js-mr-memory-usage">
<p v-if="shouldShowLoading" class="usage-info js-usage-info usage-info-loading">
- <i class="fa fa-spinner fa-spin usage-info-load-spinner" aria-hidden="true"> </i
- >{{ s__('mrWidget|Loading deployment statistics') }}
+ <gl-loading-icon class="usage-info-load-spinner" />{{
+ s__('mrWidget|Loading deployment statistics')
+ }}
+ </p>
+ <p v-if="shouldShowMemoryGraph" class="usage-info js-usage-info">
+ <gl-sprintf :message="memoryChangeMessage">
+ <template #metricsLink="{ content }">
+ <gl-link :href="metricsMonitoringUrl">{{ content }}</gl-link>
+ </template>
+ <template #emphasis="{content}">
+ <strong>{{ content }}</strong>
+ </template>
+ <template #memoryFrom>{{ memoryFrom }}</template>
+ <template #memoryTo>{{ memoryTo }}</template>
+ </gl-sprintf>
</p>
- <p
- v-if="shouldShowMemoryGraph"
- class="usage-info js-usage-info"
- v-html="memoryChangeMessage"
- ></p>
+
<p v-if="shouldShowLoadFailure" class="usage-info js-usage-info usage-info-failed">
{{ s__('mrWidget|Failed to load deployment statistics') }}
</p>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_collapsible_extension.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_collapsible_extension.vue
index 24174c29d51..b6b5b56e5aa 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/mr_collapsible_extension.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_collapsible_extension.vue
@@ -1,13 +1,12 @@
<script>
-import { GlButton, GlLoadingIcon } from '@gitlab/ui';
+import { GlButton, GlLoadingIcon, GlIcon } from '@gitlab/ui';
import { __ } from '~/locale';
-import Icon from '~/vue_shared/components/icon.vue';
export default {
components: {
GlButton,
GlLoadingIcon,
- Icon,
+ GlIcon,
},
props: {
title: {
@@ -66,7 +65,7 @@ export default {
@click="toggleCollapsed"
>
<gl-loading-icon v-if="isLoading" />
- <icon v-else :name="arrowIconName" class="js-icon" />
+ <gl-icon v-else :name="arrowIconName" class="js-icon" />
</button>
<gl-button
variant="link"
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_alert_message.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_alert_message.vue
index 19a222462b3..a2636ce52ad 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_alert_message.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_alert_message.vue
@@ -1,13 +1,12 @@
<script>
-import { GlLink } from '@gitlab/ui';
-import Icon from '~/vue_shared/components/icon.vue';
+import { GlLink, GlIcon } from '@gitlab/ui';
import { WARNING, DANGER, WARNING_MESSAGE_CLASS, DANGER_MESSAGE_CLASS } from '../constants';
export default {
name: 'MrWidgetAlertMessage',
components: {
GlLink,
- Icon,
+ GlIcon,
},
props: {
type: {
@@ -40,7 +39,7 @@ export default {
<div class="m-3 ml-7" :class="messageClass">
<slot></slot>
<gl-link v-if="helpPath" :href="helpPath" target="_blank">
- <icon :size="16" name="question-o" class="align-middle" />
+ <gl-icon :size="16" name="question-o" class="align-middle" />
</gl-link>
</div>
</template>
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 897f706290d..814d4e8341e 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
@@ -1,24 +1,33 @@
<script>
+/* eslint-disable vue/no-v-html */
import Mousetrap from 'mousetrap';
import { escape } from 'lodash';
+import {
+ GlButton,
+ GlDropdown,
+ GlDropdownSectionHeader,
+ GlDropdownItem,
+ GlTooltipDirective,
+} from '@gitlab/ui';
import { n__, s__, sprintf } from '~/locale';
import { mergeUrlParams, webIDEUrl } from '~/lib/utils/url_utility';
-import Icon from '~/vue_shared/components/icon.vue';
import clipboardButton from '~/vue_shared/components/clipboard_button.vue';
-import tooltip from '~/vue_shared/directives/tooltip';
import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate.vue';
import MrWidgetIcon from './mr_widget_icon.vue';
export default {
name: 'MRWidgetHeader',
components: {
- Icon,
clipboardButton,
TooltipOnTruncate,
MrWidgetIcon,
+ GlButton,
+ GlDropdown,
+ GlDropdownSectionHeader,
+ GlDropdownItem,
},
directives: {
- tooltip,
+ GlTooltip: GlTooltipDirective,
},
props: {
mr: {
@@ -124,62 +133,59 @@ export default {
<div class="branch-actions d-flex">
<template v-if="mr.isOpen">
- <a
+ <span
v-if="!mr.sourceBranchRemoved"
- v-tooltip
- :href="webIdePath"
+ v-gl-tooltip
:title="ideButtonTitle"
- :class="{ disabled: !mr.canPushToSourceBranch }"
- class="btn btn-default js-web-ide d-none d-md-inline-block gl-mr-3"
- data-placement="bottom"
- tabindex="0"
- role="button"
- data-qa-selector="open_in_web_ide_button"
+ class="gl-display-none d-md-inline-block gl-mr-3"
+ :tabindex="!mr.canPushToSourceBranch ? 0 : null"
>
- {{ s__('mrWidget|Open in Web IDE') }}
- </a>
- <button
+ <gl-button
+ :href="webIdePath"
+ :disabled="!mr.canPushToSourceBranch"
+ class="js-web-ide"
+ tabindex="0"
+ role="button"
+ data-qa-selector="open_in_web_ide_button"
+ >
+ {{ s__('mrWidget|Open in Web IDE') }}
+ </gl-button>
+ </span>
+ <gl-button
:disabled="mr.sourceBranchRemoved"
data-target="#modal_merge_info"
data-toggle="modal"
- class="btn btn-default js-check-out-branch gl-mr-3"
- type="button"
+ class="js-check-out-branch gl-mr-3"
>
{{ s__('mrWidget|Check out branch') }}
- </button>
+ </gl-button>
</template>
- <span class="dropdown">
- <button
- type="button"
- class="btn dropdown-toggle qa-dropdown-toggle"
- data-toggle="dropdown"
- :aria-label="__('Download as')"
- aria-haspopup="true"
- aria-expanded="false"
+ <gl-dropdown
+ v-gl-tooltip
+ :title="__('Download as')"
+ :aria-label="__('Download as')"
+ icon="download"
+ right
+ data-qa-selector="download_dropdown"
+ >
+ <gl-dropdown-section-header>{{ s__('Download as') }}</gl-dropdown-section-header>
+ <gl-dropdown-item
+ :href="mr.emailPatchesPath"
+ class="js-download-email-patches"
+ download
+ data-qa-selector="download_email_patches"
>
- <icon name="download" /> <i class="fa fa-caret-down" aria-hidden="true"> </i>
- </button>
- <ul class="dropdown-menu dropdown-menu-right">
- <li>
- <a
- :href="mr.emailPatchesPath"
- class="js-download-email-patches qa-download-email-patches"
- download
- >
- {{ s__('mrWidget|Email patches') }}
- </a>
- </li>
- <li>
- <a
- :href="mr.plainDiffPath"
- class="js-download-plain-diff qa-download-plain-diff"
- download
- >
- {{ s__('mrWidget|Plain diff') }}
- </a>
- </li>
- </ul>
- </span>
+ {{ s__('mrWidget|Email patches') }}
+ </gl-dropdown-item>
+ <gl-dropdown-item
+ :href="mr.plainDiffPath"
+ class="js-download-plain-diff"
+ download
+ data-qa-selector="download_plain_diff"
+ >
+ {{ s__('mrWidget|Plain diff') }}
+ </gl-dropdown-item>
+ </gl-dropdown>
</div>
</div>
</div>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_icon.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_icon.vue
index e1659d9a167..472df8e3110 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_icon.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_icon.vue
@@ -1,8 +1,8 @@
<script>
-import Icon from '~/vue_shared/components/icon.vue';
+import { GlIcon } from '@gitlab/ui';
export default {
- components: { Icon },
+ components: { GlIcon },
props: {
name: {
type: String,
@@ -14,6 +14,6 @@ export default {
<template>
<div class="circle-icon-container gl-mr-3 align-self-start align-self-lg-center">
- <icon :name="name" :size="24" />
+ <gl-icon :name="name" :size="24" />
</div>
</template>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue
index 7326bd0804d..5066a88b52b 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
@@ -1,8 +1,15 @@
<script>
-/* eslint-disable vue/require-default-prop */
-import { GlIcon, GlLink, GlLoadingIcon, GlSprintf, GlTooltipDirective } from '@gitlab/ui';
+/* eslint-disable vue/require-default-prop, vue/no-v-html */
+import {
+ GlIcon,
+ GlLink,
+ GlLoadingIcon,
+ GlSprintf,
+ GlTooltip,
+ GlTooltipDirective,
+} from '@gitlab/ui';
import mrWidgetPipelineMixin from 'ee_else_ce/vue_merge_request_widget/mixins/mr_widget_pipeline';
-import { s__ } from '~/locale';
+import { s__, n__ } from '~/locale';
import PipelineStage from '~/pipelines/components/pipelines_list/stage.vue';
import CiIcon from '~/vue_shared/components/ci_icon.vue';
import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate.vue';
@@ -15,6 +22,7 @@ export default {
GlLoadingIcon,
GlIcon,
GlSprintf,
+ GlTooltip,
PipelineStage,
TooltipOnTruncate,
LinkedPipelinesMiniList: () =>
@@ -33,6 +41,11 @@ export default {
type: String,
required: false,
},
+ buildsWithCoverage: {
+ type: Array,
+ required: false,
+ default: () => [],
+ },
// This prop needs to be camelCase, html attributes are case insensive
// https://vuejs.org/v2/guide/components.html#camelCase-vs-kebab-case
hasCi: {
@@ -100,6 +113,16 @@ export default {
}
return '';
},
+ pipelineCoverageJobNumberText() {
+ return n__('from %d job', 'from %d jobs', this.buildsWithCoverage.length);
+ },
+ pipelineCoverageTooltipDescription() {
+ return n__(
+ 'Coverage value for this pipeline was calculated by the coverage value of %d job.',
+ 'Coverage value for this pipeline was calculated by averaging the resulting coverage values of %d jobs.',
+ this.buildsWithCoverage.length,
+ );
+ },
},
errorText: s__(
'Pipeline|Could not retrieve the pipeline status. For troubleshooting steps, read the %{linkStart}documentation%{linkEnd}.',
@@ -139,7 +162,7 @@ export default {
>
<gl-icon
name="question"
- :small="12"
+ :size="12"
tabindex="0"
role="text"
:aria-label="__('Link to go to GitLab pipeline documentation')"
@@ -189,14 +212,30 @@ export default {
</div>
<div v-if="pipeline.coverage" class="coverage" data-testid="pipeline-coverage">
{{ s__('Pipeline|Coverage') }} {{ pipeline.coverage }}%
-
<span
v-if="pipelineCoverageDelta"
:class="coverageDeltaClass"
data-testid="pipeline-coverage-delta"
+ >({{ pipelineCoverageDelta }}%)</span
>
- ({{ pipelineCoverageDelta }}%)
+
+ {{ pipelineCoverageJobNumberText }}
+ <span ref="pipelineCoverageQuestion">
+ <gl-icon name="question" :size="12" />
</span>
+ <gl-tooltip
+ :target="() => $refs.pipelineCoverageQuestion"
+ data-testid="pipeline-coverage-tooltip"
+ >
+ {{ pipelineCoverageTooltipDescription }}
+ <div
+ v-for="(build, index) in buildsWithCoverage"
+ :key="`${build.name}-${index}`"
+ class="gl-mt-3 gl-text-left gl-px-4"
+ >
+ {{ build.name }} ({{ build.coverage }}%)
+ </div>
+ </gl-tooltip>
</div>
</div>
</div>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline_container.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline_container.vue
index 5c307b5ff0c..55efd7e7d3b 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline_container.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline_container.vue
@@ -77,6 +77,7 @@ export default {
<mr-widget-pipeline
:pipeline="pipeline"
:pipeline-coverage-delta="mr.pipelineCoverageDelta"
+ :builds-with-coverage="mr.buildsWithCoverage"
:ci-status="mr.ciStatus"
:has-ci="mr.hasCI"
:pipeline-must-succeed="mr.onlyAllowMergeIfPipelineSucceeds"
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 1b3b589c32f..56a50b55f9d 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,4 +1,5 @@
<script>
+/* eslint-disable vue/no-v-html */
import { s__ } from '~/locale';
export default {
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_suggest_pipeline.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_suggest_pipeline.vue
index 936fdc9aff5..a9d148505e1 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_suggest_pipeline.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_suggest_pipeline.vue
@@ -108,7 +108,9 @@ export default {
</div>
</template>
<div class="row">
- <div class="col-md-5 order-md-last col-12 gl-mt-5 mt-md-n1 pt-md-1 svg-content svg-225">
+ <div
+ class="col-md-5 order-md-last col-12 gl-mt-5 gl-mt-md-n2! gl-pt-md-2 svg-content svg-225"
+ >
<img data-testid="pipeline-image" :src="pipelineSvgPath" />
</div>
<div class="col-md-7 order-md-first col-12">
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/review_app_link.vue b/app/assets/javascripts/vue_merge_request_widget/components/review_app_link.vue
index c38c41f13b6..ebd2b5cd22d 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/review_app_link.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/review_app_link.vue
@@ -1,10 +1,10 @@
<script>
-import { GlTooltipDirective } from '@gitlab/ui';
-import Icon from '~/vue_shared/components/icon.vue';
+import { GlButton, GlTooltipDirective, GlIcon } from '@gitlab/ui';
export default {
components: {
- Icon,
+ GlButton,
+ GlIcon,
},
directives: {
GlTooltip: GlTooltipDirective,
@@ -22,20 +22,26 @@ export default {
type: String,
required: true,
},
+ size: {
+ type: String,
+ required: false,
+ default: 'medium',
+ },
},
};
</script>
<template>
- <a
+ <gl-button
v-gl-tooltip
:title="display.tooltip"
:href="link"
+ :size="size"
target="_blank"
rel="noopener noreferrer nofollow"
:class="cssClass"
data-track-event="open_review_app"
data-track-label="review_app"
>
- {{ display.text }} <icon class="fgray" name="external-link" />
- </a>
+ {{ display.text }} <gl-icon class="fgray" name="external-link" />
+ </gl-button>
</template>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/source_branch_removal_status.vue b/app/assets/javascripts/vue_merge_request_widget/components/source_branch_removal_status.vue
index dab0540f44e..859f2c57598 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/source_branch_removal_status.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/source_branch_removal_status.vue
@@ -1,5 +1,5 @@
<script>
-import { GlSprintf } from '@gitlab/ui';
+import { GlIcon, GlSprintf } from '@gitlab/ui';
import tooltip from '../../vue_shared/directives/tooltip';
import { __ } from '../../locale';
@@ -9,6 +9,7 @@ export default {
tooltipTitle: __('A user with write access to the source branch selected this option'),
},
components: {
+ GlIcon,
GlSprintf,
},
directives: {
@@ -26,12 +27,11 @@ export default {
</template>
</gl-sprintf>
</span>
- <i
+ <gl-icon
v-tooltip
:title="$options.i18n.tooltipTitle"
:aria-label="$options.i18n.tooltipTitle"
- class="fa fa-question-circle"
- >
- </i>
+ name="question-o"
+ />
</p>
</template>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/commits_header.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/commits_header.vue
index d52e6d38ac6..bdcea9871ea 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/commits_header.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/commits_header.vue
@@ -1,13 +1,12 @@
<script>
-import { GlDeprecatedButton } from '@gitlab/ui';
+/* eslint-disable vue/no-v-html */
+import { GlButton } from '@gitlab/ui';
import { escape } from 'lodash';
import { __, n__, sprintf, s__ } from '~/locale';
-import Icon from '~/vue_shared/components/icon.vue';
export default {
components: {
- Icon,
- GlDeprecatedButton,
+ GlButton,
},
props: {
isSquashEnabled: {
@@ -80,20 +79,19 @@ export default {
class="js-mr-widget-commits-count mr-widget-extension clickable d-flex align-items-center px-3 py-2"
@click="toggle()"
>
- <gl-deprecated-button
+ <gl-button
:aria-label="ariaLabel"
- variant="blank"
- class="commit-edit-toggle square s24 gl-mr-3"
+ category="tertiary"
+ class="commit-edit-toggle gl-mr-3"
+ :icon="collapseIcon"
@click.stop="toggle()"
- >
- <icon :name="collapseIcon" :size="16" />
- </gl-deprecated-button>
+ />
<span v-if="expanded">{{ __('Collapse') }}</span>
<span v-else>
<span class="vertical-align-middle" v-html="message"></span>
- <gl-deprecated-button variant="link" class="modify-message-button">
+ <gl-button variant="link" class="modify-message-button">
{{ modifyLinkMessage }}
- </gl-deprecated-button>
+ </gl-button>
</span>
</div>
<div v-show="expanded"><slot></slot></div>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_failed_to_merge.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_failed_to_merge.vue
index 9df0c045fe4..a5ec095b8ec 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_failed_to_merge.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_failed_to_merge.vue
@@ -1,4 +1,5 @@
<script>
+import { GlButton } from '@gitlab/ui';
import { n__ } from '~/locale';
import { stripHtml } from '~/lib/utils/text_utility';
import statusIcon from '../mr_widget_status_icon.vue';
@@ -8,6 +9,7 @@ export default {
name: 'MRWidgetFailedToMerge',
components: {
+ GlButton,
statusIcon,
},
@@ -84,14 +86,14 @@ export default {
<span v-else> {{ s__('mrWidget|Merge failed.') }} </span>
<span :class="{ 'has-custom-error': mr.mergeError }"> {{ timerText }} </span>
</span>
- <button
- class="btn btn-default btn-sm js-refresh-button"
+ <gl-button
+ size="small"
+ data-testid="merge-request-failed-refresh-button"
data-qa-selector="merge_request_error_content"
- type="button"
@click="refresh"
>
{{ s__('mrWidget|Refresh now') }}
- </button>
+ </gl-button>
</div>
</template>
</div>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.vue
index 166700dbcbf..58839251edc 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.vue
@@ -1,6 +1,6 @@
<script>
/* eslint-disable @gitlab/vue-require-i18n-strings */
-import { GlLoadingIcon } from '@gitlab/ui';
+import { GlLoadingIcon, GlButton } from '@gitlab/ui';
import { deprecatedCreateFlash as Flash } from '~/flash';
import tooltip from '~/vue_shared/directives/tooltip';
import { s__, __ } from '~/locale';
@@ -19,6 +19,7 @@ export default {
statusIcon,
ClipboardButton,
GlLoadingIcon,
+ GlButton,
},
props: {
mr: {
@@ -112,48 +113,52 @@ export default {
:date-title="mr.metrics.mergedAt"
:date-readable="mr.metrics.readableMergedAt"
/>
- <a
+ <gl-button
v-if="mr.canRevertInCurrentMR"
v-tooltip
:title="revertTitle"
- class="btn btn-close btn-sm"
+ size="small"
+ category="secondary"
+ variant="warning"
href="#modal-revert-commit"
data-toggle="modal"
data-container="body"
>
{{ revertLabel }}
- </a>
- <a
+ </gl-button>
+ <gl-button
v-else-if="mr.revertInForkPath"
v-tooltip
:href="mr.revertInForkPath"
:title="revertTitle"
- class="btn btn-close btn-sm"
+ size="small"
+ category="secondary"
+ variant="warning"
data-method="post"
>
{{ revertLabel }}
- </a>
- <a
+ </gl-button>
+ <gl-button
v-if="mr.canCherryPickInCurrentMR"
v-tooltip
:title="cherryPickTitle"
- class="btn btn-default btn-sm"
+ size="small"
href="#modal-cherry-pick-commit"
data-toggle="modal"
data-container="body"
>
{{ cherryPickLabel }}
- </a>
- <a
+ </gl-button>
+ <gl-button
v-else-if="mr.cherryPickInForkPath"
v-tooltip
:href="mr.cherryPickInForkPath"
:title="cherryPickTitle"
- class="btn btn-default btn-sm"
+ size="small"
data-method="post"
>
{{ cherryPickLabel }}
- </a>
+ </gl-button>
</div>
<section class="mr-info-list" data-qa-selector="merged_status_content">
<p>
@@ -181,14 +186,14 @@ export default {
</p>
<p v-if="shouldShowRemoveSourceBranch" class="space-children">
<span>{{ s__('mrWidget|You can delete the source branch now') }}</span>
- <button
+ <gl-button
:disabled="isMakingRequest"
- type="button"
- class="btn btn-sm btn-default js-remove-branch-button"
+ size="small"
+ class="js-remove-branch-button"
@click="removeSourceBranch"
>
{{ s__('mrWidget|Delete source branch') }}
- </button>
+ </gl-button>
</p>
<p v-if="shouldShowSourceBranchRemoving">
<gl-loading-icon :inline="true" />
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_missing_branch.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_missing_branch.vue
index 8f38ca69453..83783528cc1 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_missing_branch.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_missing_branch.vue
@@ -1,4 +1,5 @@
<script>
+import { GlIcon } from '@gitlab/ui';
import { sprintf, s__ } from '~/locale';
import tooltip from '~/vue_shared/directives/tooltip';
import statusIcon from '../mr_widget_status_icon.vue';
@@ -9,6 +10,7 @@ export default {
tooltip,
},
components: {
+ GlIcon,
statusIcon,
},
props: {
@@ -50,7 +52,7 @@ export default {
<span class="bold js-branch-text">
<span class="capitalize"> {{ missingBranchName }} </span>
{{ s__('mrWidget|branch does not exist.') }} {{ missingBranchNameMessage }}
- <i v-tooltip :title="message" :aria-label="message" class="fa fa-question-circle"> </i>
+ <gl-icon v-tooltip :title="message" :aria-label="message" name="question-o" />
</span>
</div>
</div>
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 794c994bffe..ec0934c5b4b 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_rebase.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_rebase.vue
@@ -1,4 +1,5 @@
<script>
+/* eslint-disable vue/no-v-html */
import { GlLoadingIcon } from '@gitlab/ui';
import { escape } from 'lodash';
import simplePoll from '../../../lib/utils/simple_poll';
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/nothing_to_merge.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/nothing_to_merge.vue
index 4d7d49398eb..14a29483d3c 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/nothing_to_merge.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/nothing_to_merge.vue
@@ -1,4 +1,5 @@
<script>
+/* eslint-disable vue/no-v-html */
import emptyStateSVG from 'icons/_mr_widget_empty_state.svg';
export default {
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 930a2b68d8e..240bab58297 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
@@ -1,6 +1,7 @@
<script>
+/* eslint-disable vue/no-v-html */
import { isEmpty } from 'lodash';
-import { GlIcon, GlDeprecatedButton, GlSprintf, GlLink } from '@gitlab/ui';
+import { GlIcon, GlButton, GlSprintf, GlLink } from '@gitlab/ui';
import successSvg from 'icons/_icon_status_success.svg';
import warningSvg from 'icons/_icon_status_warning.svg';
import readyToMergeMixin from 'ee_else_ce/vue_merge_request_widget/mixins/ready_to_merge';
@@ -37,7 +38,7 @@ export default {
GlIcon,
GlSprintf,
GlLink,
- GlDeprecatedButton,
+ GlButton,
MergeTrainHelperText: () =>
import('ee_component/vue_merge_request_widget/components/merge_train_helper_text.vue'),
MergeImmediatelyConfirmationDialog: () =>
@@ -297,16 +298,16 @@ export default {
<div class="media-body">
<div class="mr-widget-body-controls media space-children">
<span class="btn-group">
- <gl-deprecated-button
- size="sm"
+ <gl-button
+ size="medium"
+ category="primary"
class="qa-merge-button accept-merge-request"
:variant="mergeButtonVariant"
:disabled="isMergeButtonDisabled"
:loading="isMakingRequest"
@click="handleMergeButtonClick(isAutoMergeAvailable)"
+ >{{ mergeButtonText }}</gl-button
>
- {{ mergeButtonText }}
- </gl-deprecated-button>
<button
v-if="shouldShowMergeImmediatelyDropdown"
:disabled="isMergeButtonDisabled"
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/squash_before_merge.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/squash_before_merge.vue
index 3cf7dc3c4d1..6608381f348 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/squash_before_merge.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/squash_before_merge.vue
@@ -1,11 +1,11 @@
<script>
-import Icon from '~/vue_shared/components/icon.vue';
+import { GlIcon } from '@gitlab/ui';
import tooltip from '~/vue_shared/directives/tooltip';
import { __ } from '~/locale';
export default {
components: {
- Icon,
+ GlIcon,
},
directives: {
tooltip,
@@ -62,7 +62,7 @@ export default {
rel="noopener noreferrer nofollow"
data-container="body"
>
- <icon name="question" />
+ <gl-icon name="question" />
</a>
</div>
</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 44668170fe4..61cc950f058 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
@@ -55,13 +55,15 @@ export default {
},
methods: {
removeWipMutation() {
+ const { mergeRequestQueryVariables } = this;
+
this.isMakingRequest = true;
this.$apollo
.mutate({
mutation: removeWipMutation,
variables: {
- ...this.mergeRequestQueryVariables,
+ ...mergeRequestQueryVariables,
wip: false,
},
update(
@@ -83,14 +85,14 @@ export default {
const data = store.readQuery({
query: getStateQuery,
- variables: this.mergeRequestQueryVariables,
+ variables: mergeRequestQueryVariables,
});
data.project.mergeRequest.workInProgress = workInProgress;
data.project.mergeRequest.title = title;
store.writeQuery({
query: getStateQuery,
data,
- variables: this.mergeRequestQueryVariables,
+ variables: mergeRequestQueryVariables,
});
},
optimisticResponse: {
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/terraform/mr_widget_terraform_container.vue b/app/assets/javascripts/vue_merge_request_widget/components/terraform/mr_widget_terraform_container.vue
index c7d9453a5c9..4de41dd5887 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/terraform/mr_widget_terraform_container.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/terraform/mr_widget_terraform_container.vue
@@ -1,5 +1,5 @@
<script>
-import { GlSkeletonLoading, GlSprintf } from '@gitlab/ui';
+import { GlDeprecatedSkeletonLoading as GlSkeletonLoading, GlSprintf } from '@gitlab/ui';
import { n__ } from '~/locale';
import axios from '~/lib/utils/axios_utils';
import MrWidgetExpanableSection from '../mr_widget_expandable_section.vue';
diff --git a/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue b/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue
index 36a883869f1..43ce748b41d 100644
--- a/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue
@@ -7,6 +7,7 @@ import stateMaps from 'ee_else_ce/vue_merge_request_widget/stores/state_maps';
import { sprintf, s__, __ } from '~/locale';
import Project from '~/pages/projects/project';
import SmartInterval from '~/smart_interval';
+import { secondsToMilliseconds } from '~/lib/utils/datetime_utility';
import { deprecatedCreateFlash as createFlash } from '../flash';
import mergeRequestQueryVariablesMixin from './mixins/merge_request_query_variables';
import Loading from './components/loading.vue';
@@ -96,12 +97,11 @@ export default {
variables() {
return this.mergeRequestQueryVariables;
},
- result({
- data: {
- project: { mergeRequest },
- },
- }) {
- this.mr.setGraphqlData(mergeRequest);
+ result({ data: { project } }) {
+ if (project) {
+ this.mr.setGraphqlData(project);
+ this.loading = false;
+ }
},
},
},
@@ -120,9 +120,17 @@ export default {
mr: store,
state: store && store.state,
service: store && this.createService(store),
+ loading: true,
};
},
computed: {
+ isLoaded() {
+ if (window.gon?.features?.mergeRequestWidgetGraphql) {
+ return !this.loading;
+ }
+
+ return this.mr;
+ },
shouldRenderApprovals() {
return this.mr.state !== 'nothingToMerge';
},
@@ -195,7 +203,10 @@ export default {
},
mounted() {
MRWidgetService.fetchInitialData()
- .then(({ data }) => this.initWidget(data))
+ .then(({ data, headers }) => {
+ this.startingPollInterval = Number(headers['POLL-INTERVAL']);
+ this.initWidget(data);
+ })
.catch(() =>
createFlash(__('Unable to load the merge request widget. Try reloading the page.')),
);
@@ -285,9 +296,10 @@ export default {
initPolling() {
this.pollingInterval = new SmartInterval({
callback: this.checkStatus,
- startingInterval: 10 * 1000,
- maxInterval: 240 * 1000,
- hiddenInterval: window.gon?.features?.widgetVisibilityPolling && 360 * 1000,
+ startingInterval: this.startingPollInterval,
+ maxInterval: this.startingPollInterval + secondsToMilliseconds(4 * 60),
+ hiddenInterval:
+ window.gon?.features?.widgetVisibilityPolling && secondsToMilliseconds(6 * 60),
incrementByFactorOf: 2,
});
},
@@ -409,7 +421,7 @@ export default {
};
</script>
<template>
- <div v-if="mr" class="mr-state-widget gl-mt-3">
+ <div v-if="isLoaded" class="mr-state-widget gl-mt-3">
<mr-widget-header :mr="mr" />
<mr-widget-suggest-pipeline
v-if="shouldSuggestPipelines"
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 488397e7735..44fc1cc7f23 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,7 +1,27 @@
query getState($projectPath: ID!, $iid: String!) {
project(fullPath: $projectPath) {
+ archived
+ onlyAllowMergeIfPipelineSucceeds
+
mergeRequest(iid: $iid) {
- title
+ autoMergeEnabled
+ commitCount
+ conflicts
+ diffHeadSha
+ mergeError
+ mergeStatus
+ mergeableDiscussionsState
+ pipelines(first: 1) {
+ nodes {
+ status
+ }
+ }
+ shouldBeRebased
+ sourceBranchExists
+ targetBranchExists
+ userPermissions {
+ canMerge
+ }
workInProgress
}
}
diff --git a/app/assets/javascripts/vue_merge_request_widget/services/mr_widget_service.js b/app/assets/javascripts/vue_merge_request_widget/services/mr_widget_service.js
index ee9e3cc6d08..2ad15f231bb 100644
--- a/app/assets/javascripts/vue_merge_request_widget/services/mr_widget_service.js
+++ b/app/assets/javascripts/vue_merge_request_widget/services/mr_widget_service.js
@@ -1,3 +1,4 @@
+import { normalizeHeaders } from '~/lib/utils/common_utils';
import axios from '../../lib/utils/axios_utils';
export default class MRWidgetService {
@@ -82,6 +83,11 @@ export default class MRWidgetService {
return Promise.all([
axios.get(window.gl.mrWidgetData.merge_request_cached_widget_path),
axios.get(window.gl.mrWidgetData.merge_request_widget_path),
- ]).then(axios.spread((res, cachedRes) => ({ data: Object.assign(res.data, cachedRes.data) })));
+ ]).then(
+ axios.spread((res, cachedRes) => ({
+ data: Object.assign(res.data, cachedRes.data),
+ headers: normalizeHeaders(res.headers),
+ })),
+ );
}
}
diff --git a/app/assets/javascripts/vue_merge_request_widget/stores/artifacts_list/getters.js b/app/assets/javascripts/vue_merge_request_widget/stores/artifacts_list/getters.js
index 5215d210e1c..15d67ea18ea 100644
--- a/app/assets/javascripts/vue_merge_request_widget/stores/artifacts_list/getters.js
+++ b/app/assets/javascripts/vue_merge_request_widget/stores/artifacts_list/getters.js
@@ -1,6 +1,5 @@
import { s__, n__ } from '~/locale';
-// eslint-disable-next-line import/prefer-default-export
export const title = state => {
if (state.isLoading) {
return s__('BuildArtifacts|Loading artifacts');
diff --git a/app/assets/javascripts/vue_merge_request_widget/stores/get_state_key.js b/app/assets/javascripts/vue_merge_request_widget/stores/get_state_key.js
index 3bd512c89bf..9d3f4eb01ed 100644
--- a/app/assets/javascripts/vue_merge_request_widget/stores/get_state_key.js
+++ b/app/assets/javascripts/vue_merge_request_widget/stores/get_state_key.js
@@ -17,7 +17,7 @@ export default function deviseState() {
return stateKey.pipelineFailed;
} else if (this.workInProgress) {
return stateKey.workInProgress;
- } else if (this.hasMergeableDiscussionsState) {
+ } else if (this.hasMergeableDiscussionsState && !this.autoMergeEnabled) {
return stateKey.unresolvedDiscussions;
} else if (this.isPipelineBlocked) {
return stateKey.pipelineBlocked;
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 8c98ba1b023..846b1c453a1 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
@@ -43,15 +43,14 @@ export default class MergeRequestStore {
this.conflictsDocsPath = data.conflicts_docs_path;
this.mergeRequestPipelinesHelpPath = data.merge_request_pipelines_docs_path;
this.mergeTrainWhenPipelineSucceedsDocsPath = data.merge_train_when_pipeline_succeeds_docs_path;
- this.mergeStatus = data.merge_status;
this.commitMessage = data.default_merge_commit_message;
- this.shortMergeCommitSha = data.short_merge_commit_sha;
- this.mergeCommitSha = data.merge_commit_sha;
+ this.shortMergeCommitSha = data.short_merged_commit_sha;
+ this.mergeCommitSha = data.merged_commit_sha;
this.commitMessageWithDescription = data.default_merge_commit_message_with_description;
- this.commitsCount = data.commits_count;
this.divergedCommitsCount = data.diverged_commits_count;
this.pipeline = data.pipeline || {};
this.pipelineCoverageDelta = data.pipeline_coverage_delta;
+ this.buildsWithCoverage = data.builds_with_coverage;
this.mergePipeline = data.merge_pipeline || {};
this.deployments = this.deployments || data.deployments || [];
this.postMergeDeployments = this.postMergeDeployments || [];
@@ -60,9 +59,6 @@ export default class MergeRequestStore {
this.rebaseInProgress = data.rebase_in_progress;
this.mergeRequestDiffsPath = data.diffs_path;
this.approvalsWidgetType = data.approvals_widget_type;
- this.projectArchived = data.project_archived;
- this.branchMissing = data.branch_missing;
- this.hasConflicts = data.has_conflicts;
if (data.issues_links) {
const links = data.issues_links;
@@ -80,25 +76,18 @@ export default class MergeRequestStore {
this.setToAutoMergeBy = MergeRequestStore.formatUserObject(data.merge_user || {});
this.mergeUserId = data.merge_user_id;
this.currentUserId = gon.current_user_id;
- this.mergeError = data.merge_error;
this.sourceBranchRemoved = !data.source_branch_exists;
this.shouldRemoveSourceBranch = data.remove_source_branch || false;
- this.onlyAllowMergeIfPipelineSucceeds = data.only_allow_merge_if_pipeline_succeeds || false;
- this.autoMergeEnabled = Boolean(data.auto_merge_enabled);
this.autoMergeStrategy = data.auto_merge_strategy;
this.availableAutoMergeStrategies = data.available_auto_merge_strategies;
this.preferredAutoMergeStrategy = MergeRequestStore.getPreferredAutoMergeStrategy(
this.availableAutoMergeStrategies,
);
this.ffOnlyEnabled = data.ff_only_enabled;
- this.shouldBeRebased = Boolean(data.should_be_rebased);
this.isRemovingSourceBranch = this.isRemovingSourceBranch || false;
this.mergeRequestState = data.state;
this.isOpen = this.mergeRequestState === 'opened';
- this.hasMergeableDiscussionsState = data.mergeable_discussions_state === false;
- this.isSHAMismatch = this.sha !== data.diff_head_sha;
this.latestSHA = data.diff_head_sha;
- this.canBeMerged = data.can_be_merged || false;
this.isMergeAllowed = data.mergeable || false;
this.mergeOngoing = data.merge_ongoing;
this.allowCollaboration = data.allow_collaboration;
@@ -108,13 +97,13 @@ export default class MergeRequestStore {
// CI related
this.hasCI = data.has_ci;
this.ciStatus = data.ci_status;
- this.isPipelineFailed = this.ciStatus === 'failed' || this.ciStatus === 'canceled';
this.isPipelinePassing =
this.ciStatus === 'success' || this.ciStatus === 'success-with-warnings';
this.isPipelineSkipped = this.ciStatus === 'skipped';
this.pipelineDetailedStatus = pipelineStatus;
this.isPipelineActive = data.pipeline ? data.pipeline.active : false;
- this.isPipelineBlocked = pipelineStatus ? pipelineStatus.group === 'manual' : false;
+ this.isPipelineBlocked =
+ data.only_allow_merge_if_pipeline_succeeds && pipelineStatus?.group === 'manual';
this.ciStatusFaviconPath = pipelineStatus ? pipelineStatus.favicon : null;
this.terraformReportsPath = data.terraform_reports_path;
this.testResultsPath = data.test_reports_path;
@@ -133,11 +122,24 @@ export default class MergeRequestStore {
this.removeWIPPath = data.remove_wip_path;
this.createIssueToResolveDiscussionsPath = data.create_issue_to_resolve_discussions_path;
this.mergePath = data.merge_path;
- this.canMerge = Boolean(data.merge_path);
- this.mergeCommitPath = data.merge_commit_path;
+ this.mergeCommitPath = data.merged_commit_path;
this.canPushToSourceBranch = data.can_push_to_source_branch;
- if (data.work_in_progress !== undefined) {
+ if (!window.gon?.features?.mergeRequestWidgetGraphql) {
+ this.autoMergeEnabled = Boolean(data.auto_merge_enabled);
+ this.canBeMerged = data.can_be_merged || false;
+ this.canMerge = Boolean(data.merge_path);
+ this.commitsCount = data.commits_count;
+ this.branchMissing = data.branch_missing;
+ this.hasConflicts = data.has_conflicts;
+ this.hasMergeableDiscussionsState = data.mergeable_discussions_state === false;
+ this.isPipelineFailed = this.ciStatus === 'failed' || this.ciStatus === 'canceled';
+ this.mergeError = data.merge_error;
+ this.mergeStatus = data.merge_status;
+ this.onlyAllowMergeIfPipelineSucceeds = data.only_allow_merge_if_pipeline_succeeds || false;
+ this.projectArchived = data.project_archived;
+ this.isSHAMismatch = this.sha !== data.diff_head_sha;
+ this.shouldBeRebased = Boolean(data.should_be_rebased);
this.workInProgress = data.work_in_progress;
}
@@ -154,8 +156,27 @@ export default class MergeRequestStore {
this.setState();
}
- setGraphqlData(data) {
- this.workInProgress = data.workInProgress;
+ setGraphqlData(project) {
+ const { mergeRequest } = project;
+ const pipeline = mergeRequest.pipelines?.nodes?.[0];
+
+ this.projectArchived = project.archived;
+ this.onlyAllowMergeIfPipelineSucceeds = project.onlyAllowMergeIfPipelineSucceeds;
+
+ this.autoMergeEnabled = mergeRequest.autoMergeEnabled;
+ this.canBeMerged = mergeRequest.mergeStatus === 'can_be_merged';
+ this.canMerge = mergeRequest.userPermissions.canMerge;
+ this.ciStatus = pipeline?.status.toLowerCase();
+ this.commitsCount = mergeRequest.commitCount;
+ this.branchMissing = !mergeRequest.sourceBranchExists || !mergeRequest.targetBranchExists;
+ this.hasConflicts = mergeRequest.conflicts;
+ this.hasMergeableDiscussionsState = mergeRequest.mergeableDiscussionsState === false;
+ this.mergeError = mergeRequest.mergeError;
+ this.mergeStatus = mergeRequest.mergeStatus;
+ this.isPipelineFailed = this.ciStatus === 'failed' || this.ciStatus === 'canceled';
+ this.isSHAMismatch = this.sha !== mergeRequest.diffHeadSha;
+ this.shouldBeRebased = mergeRequest.shouldBeRebased;
+ this.workInProgress = mergeRequest.workInProgress;
this.setState();
}
diff --git a/app/assets/javascripts/vue_shared/components/actions_button.vue b/app/assets/javascripts/vue_shared/components/actions_button.vue
new file mode 100644
index 00000000000..f333ab49ead
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/actions_button.vue
@@ -0,0 +1,90 @@
+<script>
+import {
+ GlDropdown,
+ GlDropdownItem,
+ GlDropdownDivider,
+ GlLink,
+ GlTooltipDirective,
+} from '@gitlab/ui';
+
+export default {
+ components: {
+ GlDropdown,
+ GlDropdownItem,
+ GlDropdownDivider,
+ GlLink,
+ },
+ directives: {
+ GlTooltip: GlTooltipDirective,
+ },
+ props: {
+ actions: {
+ type: Array,
+ required: true,
+ },
+ selectedKey: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ },
+ computed: {
+ hasMultipleActions() {
+ return this.actions.length > 1;
+ },
+ selectedAction() {
+ return this.actions.find(x => x.key === this.selectedKey) || this.actions[0];
+ },
+ },
+ methods: {
+ handleItemClick(action) {
+ this.$emit('select', action.key);
+ },
+ handleClick(action, evt) {
+ return action.handle?.(evt);
+ },
+ },
+};
+</script>
+
+<template>
+ <gl-dropdown
+ v-if="hasMultipleActions"
+ v-gl-tooltip="selectedAction.tooltip"
+ class="gl-button-deprecated-adapter"
+ :text="selectedAction.text"
+ :split-href="selectedAction.href"
+ split
+ @click="handleClick(selectedAction, $event)"
+ >
+ <template slot="button-content">
+ <span class="gl-new-dropdown-button-text" v-bind="selectedAction.attrs">
+ {{ selectedAction.text }}
+ </span>
+ </template>
+ <template v-for="(action, index) in actions">
+ <gl-dropdown-item
+ :key="action.key"
+ class="gl-dropdown-item-deprecated-adapter"
+ :is-check-item="true"
+ :is-checked="action.key === selectedAction.key"
+ :secondary-text="action.secondaryText"
+ :data-testid="`action_${action.key}`"
+ @click="handleItemClick(action)"
+ >
+ {{ action.text }}
+ </gl-dropdown-item>
+ <gl-dropdown-divider v-if="index != actions.length - 1" :key="action.key + '_divider'" />
+ </template>
+ </gl-dropdown>
+ <gl-link
+ v-else-if="selectedAction"
+ v-gl-tooltip="selectedAction.tooltip"
+ v-bind="selectedAction.attrs"
+ class="btn"
+ :href="selectedAction.href"
+ @click="handleClick(selectedAction, $event)"
+ >
+ {{ selectedAction.text }}
+ </gl-link>
+</template>
diff --git a/app/assets/javascripts/vue_shared/components/alert_details_table.vue b/app/assets/javascripts/vue_shared/components/alert_details_table.vue
new file mode 100644
index 00000000000..c94e784c01e
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/alert_details_table.vue
@@ -0,0 +1,70 @@
+<script>
+import { GlLoadingIcon, GlTable } from '@gitlab/ui';
+import { s__ } from '~/locale';
+import {
+ capitalizeFirstCharacter,
+ convertToSentenceCase,
+ splitCamelCase,
+} from '~/lib/utils/text_utility';
+
+const thClass = 'gl-bg-transparent! gl-border-1! gl-border-b-solid! gl-border-gray-200!';
+const tdClass = 'gl-border-gray-100! gl-p-5!';
+
+export default {
+ components: {
+ GlLoadingIcon,
+ GlTable,
+ },
+ props: {
+ alert: {
+ type: Object,
+ required: false,
+ default: null,
+ },
+ loading: {
+ type: Boolean,
+ required: true,
+ },
+ },
+ fields: [
+ {
+ key: 'fieldName',
+ label: s__('AlertManagement|Key'),
+ thClass,
+ tdClass,
+ formatter: string => capitalizeFirstCharacter(convertToSentenceCase(splitCamelCase(string))),
+ },
+ {
+ key: 'value',
+ thClass: `${thClass} w-60p`,
+ tdClass,
+ label: s__('AlertManagement|Value'),
+ },
+ ],
+ computed: {
+ items() {
+ if (!this.alert) {
+ return [];
+ }
+ return Object.entries(this.alert).map(([fieldName, value]) => ({
+ fieldName,
+ value,
+ }));
+ },
+ },
+};
+</script>
+<template>
+ <gl-table
+ class="alert-management-details-table"
+ :busy="loading"
+ :empty-text="s__('AlertManagement|No alert data to display.')"
+ :items="items"
+ :fields="$options.fields"
+ show-empty
+ >
+ <template #table-busy>
+ <gl-loading-icon size="lg" color="dark" class="gl-mt-5" />
+ </template>
+ </gl-table>
+</template>
diff --git a/app/assets/javascripts/vue_shared/components/awards_list.vue b/app/assets/javascripts/vue_shared/components/awards_list.vue
index c0a42e08dee..e1f54b62223 100644
--- a/app/assets/javascripts/vue_shared/components/awards_list.vue
+++ b/app/assets/javascripts/vue_shared/components/awards_list.vue
@@ -1,4 +1,5 @@
<script>
+/* eslint-disable vue/no-v-html */
import { groupBy } from 'lodash';
import { GlIcon } from '@gitlab/ui';
import tooltip from '~/vue_shared/directives/tooltip';
diff --git a/app/assets/javascripts/vue_shared/components/blob_viewers/rich_viewer.vue b/app/assets/javascripts/vue_shared/components/blob_viewers/rich_viewer.vue
index 52ce05f0d99..d0f5570db6b 100644
--- a/app/assets/javascripts/vue_shared/components/blob_viewers/rich_viewer.vue
+++ b/app/assets/javascripts/vue_shared/components/blob_viewers/rich_viewer.vue
@@ -1,4 +1,5 @@
<script>
+import { GlSafeHtmlDirective as SafeHtml } from '@gitlab/ui';
import MarkdownFieldView from '~/vue_shared/components/markdown/field_view.vue';
import ViewerMixin from './mixins';
import { handleBlobRichViewer } from '~/blob/viewer';
@@ -7,6 +8,9 @@ export default {
components: {
MarkdownFieldView,
},
+ directives: {
+ SafeHtml,
+ },
mixins: [ViewerMixin],
mounted() {
handleBlobRichViewer(this.$refs.content, this.type);
@@ -14,5 +18,5 @@ export default {
};
</script>
<template>
- <markdown-field-view ref="content" v-html="content" />
+ <markdown-field-view ref="content" v-safe-html="content" />
</template>
diff --git a/app/assets/javascripts/vue_shared/components/blob_viewers/simple_viewer.vue b/app/assets/javascripts/vue_shared/components/blob_viewers/simple_viewer.vue
index 55a6267f9ff..bbe72a2b122 100644
--- a/app/assets/javascripts/vue_shared/components/blob_viewers/simple_viewer.vue
+++ b/app/assets/javascripts/vue_shared/components/blob_viewers/simple_viewer.vue
@@ -1,4 +1,5 @@
<script>
+/* eslint-disable vue/no-v-html */
import { GlIcon } from '@gitlab/ui';
import ViewerMixin from './mixins';
import { HIGHLIGHT_CLASS_NAME } from './constants';
diff --git a/app/assets/javascripts/vue_shared/components/changed_file_icon.vue b/app/assets/javascripts/vue_shared/components/changed_file_icon.vue
index 7431b7e9ed4..f28e49df56e 100644
--- a/app/assets/javascripts/vue_shared/components/changed_file_icon.vue
+++ b/app/assets/javascripts/vue_shared/components/changed_file_icon.vue
@@ -1,12 +1,11 @@
<script>
-import { GlTooltipDirective } from '@gitlab/ui';
-import Icon from '~/vue_shared/components/icon.vue';
+import { GlTooltipDirective, GlIcon } from '@gitlab/ui';
import getCommitIconMap from '~/ide/commit_icon';
import { __ } from '~/locale';
export default {
components: {
- Icon,
+ GlIcon,
},
directives: {
GlTooltip: GlTooltipDirective,
@@ -81,7 +80,7 @@ export default {
:class="{ 'ml-auto': isCentered }"
class="file-changed-icon d-inline-block"
>
- <icon v-if="showIcon" :name="changedIcon" :size="size" :class="changedIconClass" />
+ <gl-icon v-if="showIcon" :name="changedIcon" :size="size" :class="changedIconClass" />
</span>
</template>
diff --git a/app/assets/javascripts/vue_shared/components/ci_badge_link.vue b/app/assets/javascripts/vue_shared/components/ci_badge_link.vue
index 0e0bb8735b4..d7af3b3298e 100644
--- a/app/assets/javascripts/vue_shared/components/ci_badge_link.vue
+++ b/app/assets/javascripts/vue_shared/components/ci_badge_link.vue
@@ -39,6 +39,11 @@ export default {
required: false,
default: true,
},
+ iconClasses: {
+ type: String,
+ required: false,
+ default: '',
+ },
},
computed: {
cssClass() {
@@ -55,7 +60,7 @@ export default {
:class="cssClass"
:title="!showText ? status.text : ''"
>
- <ci-icon :status="status" />
+ <ci-icon :status="status" :css-classes="iconClasses" />
<template v-if="showText">
{{ status.text }}
diff --git a/app/assets/javascripts/vue_shared/components/ci_icon.vue b/app/assets/javascripts/vue_shared/components/ci_icon.vue
index 890dbe86c0d..ff665d9cc58 100644
--- a/app/assets/javascripts/vue_shared/components/ci_icon.vue
+++ b/app/assets/javascripts/vue_shared/components/ci_icon.vue
@@ -1,5 +1,5 @@
<script>
-import Icon from './icon.vue';
+import { GlIcon } from '@gitlab/ui';
/**
* Renders CI icon based on API response shared between all places where it is used.
@@ -28,7 +28,7 @@ const validSizes = [8, 12, 16, 18, 24, 32, 48, 72];
export default {
components: {
- Icon,
+ GlIcon,
},
props: {
status: {
@@ -66,5 +66,5 @@ export default {
};
</script>
<template>
- <span :class="cssClass"> <icon :name="icon" :size="size" :class="cssClasses" /> </span>
+ <span :class="cssClass"> <gl-icon :name="icon" :size="size" :class="cssClasses" /> </span>
</template>
diff --git a/app/assets/javascripts/vue_shared/components/clone_dropdown.vue b/app/assets/javascripts/vue_shared/components/clone_dropdown.vue
index 6f5ea8dcbee..5c6bd5892ae 100644
--- a/app/assets/javascripts/vue_shared/components/clone_dropdown.vue
+++ b/app/assets/javascripts/vue_shared/components/clone_dropdown.vue
@@ -1,7 +1,7 @@
<script>
import {
- GlNewDropdown,
- GlNewDropdownHeader,
+ GlDropdown,
+ GlDropdownSectionHeader,
GlFormInputGroup,
GlButton,
GlTooltipDirective,
@@ -11,8 +11,8 @@ import { getHTTPProtocol } from '~/lib/utils/url_utility';
export default {
components: {
- GlNewDropdown,
- GlNewDropdownHeader,
+ GlDropdown,
+ GlDropdownSectionHeader,
GlFormInputGroup,
GlButton,
},
@@ -45,10 +45,10 @@ export default {
};
</script>
<template>
- <gl-new-dropdown right :text="$options.labels.defaultLabel" category="primary" variant="info">
+ <gl-dropdown right :text="$options.labels.defaultLabel" category="primary" variant="info">
<div class="pb-2 mx-1">
<template v-if="sshLink">
- <gl-new-dropdown-header>{{ $options.labels.ssh }}</gl-new-dropdown-header>
+ <gl-dropdown-section-header>{{ $options.labels.ssh }}</gl-dropdown-section-header>
<div class="mx-3">
<gl-form-input-group :value="sshLink" readonly select-on-click>
@@ -67,7 +67,7 @@ export default {
</template>
<template v-if="httpLink">
- <gl-new-dropdown-header>{{ httpLabel }}</gl-new-dropdown-header>
+ <gl-dropdown-section-header>{{ httpLabel }}</gl-dropdown-section-header>
<div class="mx-3">
<gl-form-input-group :value="httpLink" readonly select-on-click>
@@ -85,5 +85,5 @@ export default {
</div>
</template>
</div>
- </gl-new-dropdown>
+ </gl-dropdown>
</template>
diff --git a/app/assets/javascripts/vue_shared/components/commit.vue b/app/assets/javascripts/vue_shared/components/commit.vue
index 23bea6c28b4..c1c8fb3a6e2 100644
--- a/app/assets/javascripts/vue_shared/components/commit.vue
+++ b/app/assets/javascripts/vue_shared/components/commit.vue
@@ -1,10 +1,9 @@
<script>
import { isString, isEmpty } from 'lodash';
-import { GlTooltipDirective, GlLink } from '@gitlab/ui';
+import { GlTooltipDirective, GlLink, GlIcon } from '@gitlab/ui';
import { __, sprintf } from '~/locale';
import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate.vue';
import UserAvatarLink from './user_avatar/user_avatar_link.vue';
-import Icon from './icon.vue';
export default {
directives: {
@@ -12,14 +11,14 @@ export default {
},
components: {
UserAvatarLink,
- Icon,
+ GlIcon,
GlLink,
TooltipOnTruncate,
},
props: {
/**
* Indicates the existence of a tag.
- * Used to render the correct icon, if true will render `fa-tag` icon,
+ * Used to render the correct GlIcon, if true will render `tag` GlIcon,
* if false will render a svg sprite fork icon
*/
tag: {
@@ -141,9 +140,9 @@ export default {
<div class="branch-commit cgray">
<template v-if="shouldShowRefInfo">
<div class="icon-container">
- <icon v-if="tag" name="tag" />
- <icon v-else-if="mergeRequestRef" name="git-merge" />
- <icon v-else name="branch" />
+ <gl-icon v-if="tag" name="tag" />
+ <gl-icon v-else-if="mergeRequestRef" name="git-merge" />
+ <gl-icon v-else name="branch" />
</div>
<gl-link
@@ -163,7 +162,7 @@ export default {
>{{ commitRef.name }}</gl-link
>
</template>
- <icon name="commit" class="commit-icon js-commit-icon" />
+ <gl-icon name="commit" class="commit-icon js-commit-icon" />
<gl-link :href="commitUrl" class="commit-sha mr-0">{{ shortSha }}</gl-link>
diff --git a/app/assets/javascripts/vue_shared/components/content_viewer/viewers/download_viewer.vue b/app/assets/javascripts/vue_shared/components/content_viewer/viewers/download_viewer.vue
index 3bf629d4acb..f9d3d76e7f5 100644
--- a/app/assets/javascripts/vue_shared/components/content_viewer/viewers/download_viewer.vue
+++ b/app/assets/javascripts/vue_shared/components/content_viewer/viewers/download_viewer.vue
@@ -1,12 +1,11 @@
<script>
-import { GlLink } from '@gitlab/ui';
-import Icon from '../../icon.vue';
+import { GlLink, GlIcon } from '@gitlab/ui';
import { numberToHumanSize } from '../../../../lib/utils/number_utils';
export default {
components: {
GlLink,
- Icon,
+ GlIcon,
},
props: {
path: {
@@ -52,7 +51,7 @@ export default {
:download="fileName"
target="_blank"
>
- <icon :size="16" name="download" class="float-left gl-mr-3" />
+ <gl-icon :size="16" name="download" class="float-left gl-mr-3" />
{{ __('Download') }}
</gl-link>
</div>
diff --git a/app/assets/javascripts/vue_shared/components/content_viewer/viewers/markdown_viewer.vue b/app/assets/javascripts/vue_shared/components/content_viewer/viewers/markdown_viewer.vue
index f9b678e33cd..6bb05e59f6b 100644
--- a/app/assets/javascripts/vue_shared/components/content_viewer/viewers/markdown_viewer.vue
+++ b/app/assets/javascripts/vue_shared/components/content_viewer/viewers/markdown_viewer.vue
@@ -1,8 +1,9 @@
<script>
+/* eslint-disable vue/no-v-html */
import $ from 'jquery';
import '~/behaviors/markdown/render_gfm';
-import { GlSkeletonLoading } from '@gitlab/ui';
+import { GlDeprecatedSkeletonLoading as GlSkeletonLoading } from '@gitlab/ui';
import { forEach, escape } from 'lodash';
import axios from '~/lib/utils/axios_utils';
import { __ } from '~/locale';
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 3b6b0a91e97..a7e6438a935 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
@@ -1,11 +1,5 @@
<script>
-import {
- GlIcon,
- GlButton,
- GlDeprecatedDropdown,
- GlDeprecatedDropdownItem,
- GlFormGroup,
-} from '@gitlab/ui';
+import { GlIcon, GlButton, GlDropdown, GlDropdownItem, GlFormGroup } from '@gitlab/ui';
import { __, sprintf } from '~/locale';
import { convertToFixedRange, isEqualTimeRanges, findTimeRange } from '~/lib/utils/datetime_range';
@@ -29,8 +23,8 @@ export default {
components: {
GlIcon,
GlButton,
- GlDeprecatedDropdown,
- GlDeprecatedDropdownItem,
+ GlDropdown,
+ GlDropdownItem,
GlFormGroup,
TooltipOnTruncate,
DateTimePickerInput,
@@ -212,7 +206,7 @@ export default {
placement="top"
class="d-inline-block"
>
- <gl-deprecated-dropdown
+ <gl-dropdown
ref="dropdown"
:text="timeWindowText"
v-bind="$attrs"
@@ -228,15 +222,15 @@ export default {
<gl-icon class="gl-dropdown-caret" name="chevron-down" aria-hidden="true" />
</template>
- <div class="d-flex justify-content-between gl-p-2-deprecated-no-really-do-not-use-me">
+ <div class="d-flex justify-content-between gl-p-2">
<gl-form-group
v-if="customEnabled"
:label="customLabel"
label-for="custom-from-time"
- label-class="gl-pb-1-deprecated-no-really-do-not-use-me"
- class="custom-time-range-form-group col-md-7 gl-pl-1-deprecated-no-really-do-not-use-me gl-pr-0 m-0"
+ label-class="gl-pb-2"
+ class="custom-time-range-form-group col-md-7 gl-pl-2 gl-pr-0 m-0"
>
- <div class="gl-pt-2-deprecated-no-really-do-not-use-me">
+ <div class="gl-pt-3">
<date-time-picker-input
id="custom-time-from"
v-model="startInput"
@@ -264,15 +258,12 @@ export default {
</gl-button>
</gl-form-group>
</gl-form-group>
- <gl-form-group
- label-for="group-id-dropdown"
- class="col-md-5 gl-pl-1-deprecated-no-really-do-not-use-me gl-pr-1-deprecated-no-really-do-not-use-me m-0"
- >
+ <gl-form-group label-for="group-id-dropdown" class="col-md-5 gl-px-2 m-0">
<template #label>
- <span class="gl-pl-5-deprecated-no-really-do-not-use-me">{{ __('Quick range') }}</span>
+ <span class="gl-pl-7">{{ __('Quick range') }}</span>
</template>
- <gl-deprecated-dropdown-item
+ <gl-dropdown-item
v-for="(option, index) in options"
:key="index"
data-qa-selector="quick_range_item"
@@ -286,9 +277,9 @@ export default {
:class="{ invisible: !isOptionActive(option) }"
/>
{{ option.label }}
- </gl-deprecated-dropdown-item>
+ </gl-dropdown-item>
</gl-form-group>
</div>
- </gl-deprecated-dropdown>
+ </gl-dropdown>
</tooltip-on-truncate>
</template>
diff --git a/app/assets/javascripts/vue_shared/components/dismissible_alert.vue b/app/assets/javascripts/vue_shared/components/dismissible_alert.vue
index 986fa14349e..8494f99fd7d 100644
--- a/app/assets/javascripts/vue_shared/components/dismissible_alert.vue
+++ b/app/assets/javascripts/vue_shared/components/dismissible_alert.vue
@@ -1,4 +1,5 @@
<script>
+/* eslint-disable vue/no-v-html */
import { GlAlert } from '@gitlab/ui';
export default {
diff --git a/app/assets/javascripts/vue_shared/components/dropdown/dropdown_search_input.vue b/app/assets/javascripts/vue_shared/components/dropdown/dropdown_search_input.vue
index 610bce9a705..7157337f8f3 100644
--- a/app/assets/javascripts/vue_shared/components/dropdown/dropdown_search_input.vue
+++ b/app/assets/javascripts/vue_shared/components/dropdown/dropdown_search_input.vue
@@ -41,12 +41,5 @@ export default {
autocomplete="off"
/>
<i class="fa fa-search dropdown-input-search" aria-hidden="true" data-hidden="true"> </i>
- <i
- class="fa fa-times dropdown-input-clear js-dropdown-input-clear"
- aria-hidden="true"
- data-hidden="true"
- role="button"
- >
- </i>
</div>
</template>
diff --git a/app/assets/javascripts/vue_shared/components/droplab_dropdown_button.vue b/app/assets/javascripts/vue_shared/components/droplab_dropdown_button.vue
index e1f336f5250..4d85726065b 100644
--- a/app/assets/javascripts/vue_shared/components/droplab_dropdown_button.vue
+++ b/app/assets/javascripts/vue_shared/components/droplab_dropdown_button.vue
@@ -1,10 +1,9 @@
<script>
-import { GlDeprecatedButton } from '@gitlab/ui';
-import Icon from './icon.vue';
+import { GlDeprecatedButton, GlIcon } from '@gitlab/ui';
export default {
components: {
- Icon,
+ GlIcon,
GlDeprecatedButton,
},
props: {
@@ -73,7 +72,7 @@ export default {
data-display="static"
data-toggle="dropdown"
>
- <icon name="chevron-down" :aria-label="__('toggle dropdown')" />
+ <gl-icon name="chevron-down" :aria-label="__('toggle dropdown')" />
</button>
<ul :class="dropdownClass" class="dropdown-menu dropdown-open-top">
<template v-for="(action, index) in actions">
diff --git a/app/assets/javascripts/vue_shared/components/file_finder/index.vue b/app/assets/javascripts/vue_shared/components/file_finder/index.vue
index d6f591ccca1..012aca8105a 100644
--- a/app/assets/javascripts/vue_shared/components/file_finder/index.vue
+++ b/app/assets/javascripts/vue_shared/components/file_finder/index.vue
@@ -2,6 +2,7 @@
import fuzzaldrinPlus from 'fuzzaldrin-plus';
import Mousetrap from 'mousetrap';
import VirtualList from 'vue-virtual-scroll-list';
+import { GlIcon } from '@gitlab/ui';
import Item from './item.vue';
import { UP_KEY_CODE, DOWN_KEY_CODE, ENTER_KEY_CODE, ESC_KEY_CODE } from '~/lib/utils/keycodes';
@@ -9,10 +10,11 @@ export const MAX_FILE_FINDER_RESULTS = 40;
export const FILE_FINDER_ROW_HEIGHT = 55;
export const FILE_FINDER_EMPTY_ROW_HEIGHT = 33;
-const originalStopCallback = Mousetrap.stopCallback;
+const originalStopCallback = Mousetrap.prototype.stopCallback;
export default {
components: {
+ GlIcon,
Item,
VirtualList,
},
@@ -126,7 +128,7 @@ export default {
this.focusedIndex = 0;
}
- Mousetrap.bind(['t', 'command+p', 'ctrl+p'], e => {
+ Mousetrap.bind(['t', 'mod+p'], e => {
if (e.preventDefault) {
e.preventDefault();
}
@@ -134,7 +136,18 @@ export default {
this.toggle(!this.visible);
});
- Mousetrap.stopCallback = (e, el, combo) => this.mousetrapStopCallback(e, el, combo);
+ Mousetrap.prototype.stopCallback = function customStopCallback(e, el, combo) {
+ if (
+ (combo === 't' && el.classList.contains('dropdown-input-field')) ||
+ el.classList.contains('inputarea')
+ ) {
+ return true;
+ } else if (combo === 'mod+p') {
+ return false;
+ }
+
+ return originalStopCallback.call(this, e, el, combo);
+ };
},
methods: {
toggle(visible) {
@@ -199,18 +212,6 @@ export default {
this.cancelMouseOver = false;
this.onMouseOver(index);
},
- mousetrapStopCallback(e, el, combo) {
- if (
- (combo === 't' && el.classList.contains('dropdown-input-field')) ||
- el.classList.contains('inputarea')
- ) {
- return true;
- } else if (combo === 'command+p' || combo === 'ctrl+p') {
- return false;
- }
-
- return originalStopCallback(e, el, combo);
- },
},
};
</script>
@@ -236,12 +237,13 @@ export default {
aria-hidden="true"
class="fa fa-search dropdown-input-search"
></i>
- <i
- :aria-label="__('Clear search input')"
+ <gl-icon
+ name="close"
+ class="dropdown-input-clear"
role="button"
- class="fa fa-times dropdown-input-clear"
+ :aria-label="__('Clear search input')"
@click="clearSearchInput"
- ></i>
+ />
</div>
<div>
<virtual-list ref="virtualScrollList" :size="listHeight" :remain="listShowCount" wtag="ul">
diff --git a/app/assets/javascripts/vue_shared/components/file_finder/item.vue b/app/assets/javascripts/vue_shared/components/file_finder/item.vue
index 79c62cd9938..4c496ba3f9b 100644
--- a/app/assets/javascripts/vue_shared/components/file_finder/item.vue
+++ b/app/assets/javascripts/vue_shared/components/file_finder/item.vue
@@ -1,6 +1,6 @@
<script>
import fuzzaldrinPlus from 'fuzzaldrin-plus';
-import Icon from '~/vue_shared/components/icon.vue';
+import { GlIcon } from '@gitlab/ui';
import FileIcon from '../file_icon.vue';
import ChangedFileIcon from '../changed_file_icon.vue';
@@ -8,7 +8,7 @@ const MAX_PATH_LENGTH = 60;
export default {
components: {
- Icon,
+ GlIcon,
ChangedFileIcon,
FileIcon,
},
@@ -103,10 +103,10 @@ export default {
<span v-if="file.changed || file.tempFile" v-once class="diff-changed-stats">
<span v-if="showDiffStats">
<span class="cgreen bold">
- <icon name="file-addition" class="align-text-top" /> {{ file.addedLines }}
+ <gl-icon name="file-addition" class="align-text-top" /> {{ file.addedLines }}
</span>
<span class="cred bold ml-1">
- <icon name="file-deletion" class="align-text-top" /> {{ file.removedLines }}
+ <gl-icon name="file-deletion" class="align-text-top" /> {{ file.removedLines }}
</span>
</span>
<changed-file-icon v-else :file="file" />
diff --git a/app/assets/javascripts/vue_shared/components/file_row.vue b/app/assets/javascripts/vue_shared/components/file_row.vue
index 0952e37e46e..c1c4f437dee 100644
--- a/app/assets/javascripts/vue_shared/components/file_row.vue
+++ b/app/assets/javascripts/vue_shared/components/file_row.vue
@@ -14,10 +14,20 @@ export default {
type: Object,
required: true,
},
+ fileUrl: {
+ type: String,
+ required: false,
+ default: '',
+ },
level: {
type: Number,
required: true,
},
+ fileClasses: {
+ type: String,
+ required: false,
+ default: '',
+ },
},
computed: {
isTree() {
@@ -43,6 +53,9 @@ export default {
// don't output a title if we don't have the expanded path
return this.file?.tree?.length ? this.file.tree[0].parentPath : false;
},
+ fileRouterUrl() {
+ return this.fileUrl || `/project${this.file.url}`;
+ },
},
watch: {
'file.active': function fileActiveWatch(active) {
@@ -69,7 +82,7 @@ export default {
this.toggleTreeOpen(this.file.path);
}
- if (this.$router) this.$router.push(`/project${this.file.url}`);
+ if (this.$router && !this.hasUrlAtCurrentRoute()) this.$router.push(this.fileRouterUrl);
if (this.isBlob) this.clickedFile(this.file.path);
},
@@ -99,7 +112,7 @@ export default {
hasUrlAtCurrentRoute() {
if (!this.$router || !this.$router.currentRoute) return true;
- return this.$router.currentRoute.path === `/project${escapeFileUrl(this.file.url)}`;
+ return this.$router.currentRoute.path === escapeFileUrl(this.fileRouterUrl);
},
},
};
@@ -123,6 +136,7 @@ export default {
:style="levelIndentation"
class="file-row-name str-truncated"
data-qa-selector="file_name_content"
+ :class="fileClasses"
>
<file-icon
class="file-row-icon"
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 7b3d1d0afd6..3d8afd162cb 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
@@ -1,8 +1,11 @@
+/* eslint-disable @gitlab/require-i18n-strings */
import { __ } from '~/locale';
-export const ANY_AUTHOR = 'Any';
+const DEFAULT_LABEL_NO_LABEL = { value: 'No label', text: __('No label') };
+export const DEFAULT_LABEL_NONE = { value: 'None', text: __('None') };
+export const DEFAULT_LABEL_ANY = { value: 'Any', text: __('Any') };
-export const NO_LABEL = 'No label';
+export const DEFAULT_LABELS = [DEFAULT_LABEL_NO_LABEL];
export const DEBOUNCE_DELAY = 200;
@@ -11,13 +14,11 @@ export const SortDirection = {
ascending: 'ascending',
};
-export const defaultMilestones = [
- // eslint-disable-next-line @gitlab/require-i18n-strings
- { value: 'None', text: __('None') },
- // eslint-disable-next-line @gitlab/require-i18n-strings
- { value: 'Any', text: __('Any') },
- // eslint-disable-next-line @gitlab/require-i18n-strings
+export const DEFAULT_MILESTONES = [
+ DEFAULT_LABEL_NONE,
+ DEFAULT_LABEL_ANY,
{ value: 'Upcoming', text: __('Upcoming') },
- // eslint-disable-next-line @gitlab/require-i18n-strings
{ value: 'Started', text: __('Started') },
];
+
+/* eslint-enable @gitlab/require-i18n-strings */
diff --git a/app/assets/javascripts/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue b/app/assets/javascripts/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue
index ee293d37b66..25478ad6f4f 100644
--- a/app/assets/javascripts/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue
+++ b/app/assets/javascripts/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue
@@ -3,8 +3,8 @@ import {
GlFilteredSearch,
GlButtonGroup,
GlButton,
- GlNewDropdown as GlDropdown,
- GlNewDropdownItem as GlDropdownItem,
+ GlDropdown,
+ GlDropdownItem,
GlTooltipDirective,
} from '@gitlab/ui';
@@ -15,7 +15,7 @@ import { deprecatedCreateFlash as createFlash } from '~/flash';
import RecentSearchesStore from '~/filtered_search/stores/recent_searches_store';
import RecentSearchesService from '~/filtered_search/services/recent_searches_service';
-import { stripQuotes } from './filtered_search_utils';
+import { stripQuotes, uniqueTokens } from './filtered_search_utils';
import { SortDirection } from './constants';
export default {
@@ -120,10 +120,31 @@ export default {
? __('Sort direction: Ascending')
: __('Sort direction: Descending');
},
+ /**
+ * This prop fixes a behaviour affecting GlFilteredSearch
+ * where selecting duplicate token values leads to history
+ * dropdown also showing that selection.
+ */
filteredRecentSearches() {
- return this.recentSearchesStorageKey
- ? this.recentSearches.filter(item => typeof item !== 'string')
- : undefined;
+ if (this.recentSearchesStorageKey) {
+ const knownItems = [];
+ return this.recentSearches.reduce((historyItems, item) => {
+ // Only include non-string history items (discard items from legacy search)
+ if (typeof item !== 'string') {
+ const sanitizedItem = uniqueTokens(item);
+ const itemString = JSON.stringify(sanitizedItem);
+ // Only include items which aren't already part of history
+ if (!knownItems.includes(itemString)) {
+ historyItems.push(sanitizedItem);
+ // We're storing string for comparision as doing direct object compare
+ // won't work due to object reference not being the same.
+ knownItems.push(itemString);
+ }
+ }
+ return historyItems;
+ }, []);
+ }
+ return undefined;
},
},
watch: {
@@ -245,12 +266,14 @@ export default {
this.recentSearchesService.save(resultantSearches);
this.recentSearches = [];
},
- handleFilterSubmit(filters) {
+ handleFilterSubmit() {
+ const filterTokens = uniqueTokens(this.filterValue);
+ this.filterValue = filterTokens;
if (this.recentSearchesStorageKey) {
this.recentSearchesPromise
.then(() => {
- if (filters.length) {
- const resultantSearches = this.recentSearchesStore.addRecentSearch(filters);
+ if (filterTokens.length) {
+ const resultantSearches = this.recentSearchesStore.addRecentSearch(filterTokens);
this.recentSearchesService.save(resultantSearches);
this.recentSearches = resultantSearches;
}
@@ -260,7 +283,7 @@ export default {
});
}
this.blurSearchInput();
- this.$emit('onFilter', this.removeQuotesEnclosure(filters));
+ this.$emit('onFilter', this.removeQuotesEnclosure(filterTokens));
},
},
};
diff --git a/app/assets/javascripts/vue_shared/components/filtered_search_bar/filtered_search_utils.js b/app/assets/javascripts/vue_shared/components/filtered_search_bar/filtered_search_utils.js
index 85f7f746b49..e7d7b7d9f1b 100644
--- a/app/assets/javascripts/vue_shared/components/filtered_search_bar/filtered_search_utils.js
+++ b/app/assets/javascripts/vue_shared/components/filtered_search_bar/filtered_search_utils.js
@@ -1,4 +1,164 @@
-// eslint-disable-next-line import/prefer-default-export
-export const stripQuotes = value => {
- return value.includes(' ') ? value.slice(1, -1) : value;
+import { isEmpty } from 'lodash';
+import { queryToObject } from '~/lib/utils/url_utility';
+
+/**
+ * Strips enclosing quotations from a string if it has one.
+ *
+ * @param {String} value String to strip quotes from
+ *
+ * @returns {String} String without any enclosure
+ */
+export const stripQuotes = value => value.replace(/^('|")(.*)('|")$/, '$2');
+
+/**
+ * This method removes duplicate tokens from tokens array.
+ *
+ * @param {Array} tokens Array of tokens as defined by `GlFilteredSearch`
+ *
+ * @returns {Array} Unique array of tokens
+ */
+export const uniqueTokens = tokens => {
+ const knownTokens = [];
+ return tokens.reduce((uniques, token) => {
+ if (typeof token === 'object' && token.type !== 'filtered-search-term') {
+ const tokenString = `${token.type}${token.value.operator}${token.value.data}`;
+ if (!knownTokens.includes(tokenString)) {
+ uniques.push(token);
+ knownTokens.push(tokenString);
+ }
+ } else {
+ uniques.push(token);
+ }
+ return uniques;
+ }, []);
};
+
+/**
+ * Creates a token from a type and a filter. Example returned object
+ * { type: 'myType', value: { data: 'myData', operator: '= '} }
+ * @param {String} type the name of the filter
+ * @param {Object}
+ * @param {Object.value} filter value to be returned as token data
+ * @param {Object.operator} filter operator to be retuned as token operator
+ * @return {Object}
+ * @return {Object.type} token type
+ * @return {Object.value} token value
+ */
+function createToken(type, filter) {
+ return { type, value: { data: filter.value, operator: filter.operator } };
+}
+
+/**
+ * This function takes a filter object and translates it into a token array
+ * @param {Object} filters
+ * @param {Object.myFilterName} a single filter value or an array of filters
+ * @return {Array} tokens an array of tokens created from filter values
+ */
+export function prepareTokens(filters = {}) {
+ return Object.keys(filters).reduce((memo, key) => {
+ const value = filters[key];
+ if (!value) {
+ return memo;
+ }
+ if (Array.isArray(value)) {
+ return [...memo, ...value.map(filterValue => createToken(key, filterValue))];
+ }
+
+ return [...memo, createToken(key, value)];
+ }, []);
+}
+
+export function processFilters(filters) {
+ return filters.reduce((acc, token) => {
+ const { type, value } = token;
+ const { operator } = value;
+ const tokenValue = value.data;
+
+ if (!acc[type]) {
+ acc[type] = [];
+ }
+
+ acc[type].push({ value: tokenValue, operator });
+ return acc;
+ }, {});
+}
+
+/**
+ * This function takes a filter object and maps it into a query object. Example filter:
+ * { myFilterName: { value: 'foo', operator: '=' } }
+ * gets translated into:
+ * { myFilterName: 'foo', 'not[myFilterName]': null }
+ * @param {Object} filters
+ * @param {Object.myFilterName} a single filter value or an array of filters
+ * @return {Object} query object with both filter name and not-name with values
+ */
+export function filterToQueryObject(filters = {}) {
+ return Object.keys(filters).reduce((memo, key) => {
+ const filter = filters[key];
+
+ let selected;
+ let unselected;
+ if (Array.isArray(filter)) {
+ selected = filter.filter(item => item.operator === '=').map(item => item.value);
+ unselected = filter.filter(item => item.operator === '!=').map(item => item.value);
+ } else {
+ selected = filter?.operator === '=' ? filter.value : null;
+ unselected = filter?.operator === '!=' ? filter.value : null;
+ }
+
+ if (isEmpty(selected)) {
+ selected = null;
+ }
+ if (isEmpty(unselected)) {
+ unselected = null;
+ }
+
+ return { ...memo, [key]: selected, [`not[${key}]`]: unselected };
+ }, {});
+}
+
+/**
+ * Extracts filter name from url name, e.g. `not[my_filter]` => `my_filter`
+ * and returns the operator with it depending on the filter name
+ * @param {String} filterName from url
+ * @return {Object}
+ * @return {Object.filterName} extracted filtern ame
+ * @return {Object.operator} `=` or `!=`
+ */
+function extractNameAndOperator(filterName) {
+ // eslint-disable-next-line @gitlab/require-i18n-strings
+ if (filterName.startsWith('not[') && filterName.endsWith(']')) {
+ return { filterName: filterName.slice(4, -1), operator: '!=' };
+ }
+
+ return { filterName, operator: '=' };
+}
+
+/**
+ * This function takes a URL query string and maps it into a filter object. Example query string:
+ * '?myFilterName=foo'
+ * gets translated into:
+ * { myFilterName: { value: 'foo', operator: '=' } }
+ * @param {String} query URL quert string, e.g. from `window.location.search`
+ * @return {Object} filter object with filter names and their values
+ */
+export function urlQueryToFilter(query = '') {
+ const filters = queryToObject(query, { gatherArrays: true });
+ return Object.keys(filters).reduce((memo, key) => {
+ const value = filters[key];
+ if (!value) {
+ return memo;
+ }
+ const { filterName, operator } = extractNameAndOperator(key);
+ let previousValues = [];
+ if (Array.isArray(memo[filterName])) {
+ previousValues = memo[filterName];
+ }
+ if (Array.isArray(value)) {
+ const newAdditions = value.filter(Boolean).map(item => ({ value: item, operator }));
+ return { ...memo, [filterName]: [...previousValues, ...newAdditions] };
+ }
+
+ return { ...memo, [filterName]: { value, operator } };
+ }, {});
+}
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 969e914ef0c..ee0e00b0f5d 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
@@ -3,7 +3,7 @@ import {
GlFilteredSearchToken,
GlAvatar,
GlFilteredSearchSuggestion,
- GlDeprecatedDropdownDivider,
+ GlDropdownDivider,
GlLoadingIcon,
} from '@gitlab/ui';
import { debounce } from 'lodash';
@@ -11,15 +11,14 @@ import { debounce } from 'lodash';
import { deprecatedCreateFlash as createFlash } from '~/flash';
import { __ } from '~/locale';
-import { ANY_AUTHOR, DEBOUNCE_DELAY } from '../constants';
+import { DEFAULT_LABEL_ANY, DEBOUNCE_DELAY } from '../constants';
export default {
- anyAuthor: ANY_AUTHOR,
components: {
GlFilteredSearchToken,
GlAvatar,
GlFilteredSearchSuggestion,
- GlDeprecatedDropdownDivider,
+ GlDropdownDivider,
GlLoadingIcon,
},
props: {
@@ -35,6 +34,7 @@ export default {
data() {
return {
authors: this.config.initialAuthors || [],
+ defaultAuthors: this.config.defaultAuthors || [DEFAULT_LABEL_ANY],
loading: true,
};
},
@@ -99,10 +99,14 @@ export default {
<span>{{ activeAuthor ? activeAuthor.name : inputValue }}</span>
</template>
<template #suggestions>
- <gl-filtered-search-suggestion :value="$options.anyAuthor">
- {{ __('Any') }}
+ <gl-filtered-search-suggestion
+ v-for="author in defaultAuthors"
+ :key="author.value"
+ :value="author.value"
+ >
+ {{ author.text }}
</gl-filtered-search-suggestion>
- <gl-deprecated-dropdown-divider />
+ <gl-dropdown-divider v-if="defaultAuthors.length" />
<gl-loading-icon v-if="loading" />
<template v-else>
<gl-filtered-search-suggestion
diff --git a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/branch_token.vue b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/branch_token.vue
new file mode 100644
index 00000000000..c18bdfc5c20
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/branch_token.vue
@@ -0,0 +1,115 @@
+<script>
+import {
+ GlToken,
+ GlFilteredSearchToken,
+ GlFilteredSearchSuggestion,
+ GlDropdownDivider,
+ GlLoadingIcon,
+} from '@gitlab/ui';
+import { debounce } from 'lodash';
+
+import createFlash from '~/flash';
+import { __ } from '~/locale';
+
+import { DEBOUNCE_DELAY } from '../constants';
+
+export default {
+ components: {
+ GlToken,
+ GlFilteredSearchToken,
+ GlFilteredSearchSuggestion,
+ GlDropdownDivider,
+ GlLoadingIcon,
+ },
+ props: {
+ config: {
+ type: Object,
+ required: true,
+ },
+ value: {
+ type: Object,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ branches: this.config.initialBranches || [],
+ defaultBranches: this.config.defaultBranches || [],
+ loading: true,
+ };
+ },
+ computed: {
+ currentValue() {
+ return this.value.data.toLowerCase();
+ },
+ activeBranch() {
+ return this.branches.find(branch => branch.name.toLowerCase() === this.currentValue);
+ },
+ },
+ watch: {
+ active: {
+ immediate: true,
+ handler(newValue) {
+ if (!newValue && !this.branches.length) {
+ this.fetchBranchBySearchTerm(this.value.data);
+ }
+ },
+ },
+ },
+ methods: {
+ fetchBranchBySearchTerm(searchTerm) {
+ this.loading = true;
+ this.config
+ .fetchBranches(searchTerm)
+ .then(({ data }) => {
+ this.branches = data;
+ })
+ .catch(() => createFlash({ message: __('There was a problem fetching branches.') }))
+ .finally(() => {
+ this.loading = false;
+ });
+ },
+ searchBranches: debounce(function debouncedSearch({ data }) {
+ this.fetchBranchBySearchTerm(data);
+ }, DEBOUNCE_DELAY),
+ },
+};
+</script>
+
+<template>
+ <gl-filtered-search-token
+ :config="config"
+ v-bind="{ ...$props, ...$attrs }"
+ v-on="$listeners"
+ @input="searchBranches"
+ >
+ <template #view-token="{ inputValue }">
+ <gl-token variant="search-value">{{
+ activeBranch ? activeBranch.name : inputValue
+ }}</gl-token>
+ </template>
+ <template #suggestions>
+ <gl-filtered-search-suggestion
+ v-for="branch in defaultBranches"
+ :key="branch.value"
+ :value="branch.value"
+ >
+ {{ branch.text }}
+ </gl-filtered-search-suggestion>
+ <gl-dropdown-divider v-if="defaultBranches.length" />
+ <gl-loading-icon v-if="loading" />
+ <template v-else>
+ <gl-filtered-search-suggestion
+ v-for="branch in branches"
+ :key="branch.id"
+ :value="branch.name"
+ >
+ <div class="gl-display-flex">
+ <span class="gl-display-inline-block gl-mr-3 gl-p-3"></span>
+ <div>{{ branch.name }}</div>
+ </div>
+ </gl-filtered-search-suggestion>
+ </template>
+ </template>
+ </gl-filtered-search-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 726a1c49993..7a9c5c277eb 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
@@ -3,7 +3,7 @@ import {
GlToken,
GlFilteredSearchToken,
GlFilteredSearchSuggestion,
- GlNewDropdownDivider as GlDropdownDivider,
+ GlDropdownDivider,
GlLoadingIcon,
} from '@gitlab/ui';
import { debounce } from 'lodash';
@@ -14,10 +14,9 @@ import { __ } from '~/locale';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import { stripQuotes } from '../filtered_search_utils';
-import { NO_LABEL, DEBOUNCE_DELAY } from '../constants';
+import { DEFAULT_LABELS, DEBOUNCE_DELAY } from '../constants';
export default {
- noLabel: NO_LABEL,
components: {
GlToken,
GlFilteredSearchToken,
@@ -38,6 +37,7 @@ export default {
data() {
return {
labels: this.config.initialLabels || [],
+ defaultLabels: this.config.defaultLabels || DEFAULT_LABELS,
loading: true,
};
},
@@ -105,10 +105,14 @@ export default {
>
</template>
<template #suggestions>
- <gl-filtered-search-suggestion :value="$options.noLabel">{{
- __('No label')
- }}</gl-filtered-search-suggestion>
- <gl-dropdown-divider />
+ <gl-filtered-search-suggestion
+ v-for="label in defaultLabels"
+ :key="label.value"
+ :value="label.value"
+ >
+ {{ label.text }}
+ </gl-filtered-search-suggestion>
+ <gl-dropdown-divider v-if="defaultLabels.length" />
<gl-loading-icon v-if="loading" />
<template v-else>
<gl-filtered-search-suggestion v-for="label in labels" :key="label.id" :value="label.title">
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 cf1ac4e718b..89952623d0d 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 {
GlFilteredSearchToken,
GlFilteredSearchSuggestion,
- GlNewDropdownDivider as GlDropdownDivider,
+ GlDropdownDivider,
GlLoadingIcon,
} from '@gitlab/ui';
import { debounce } from 'lodash';
@@ -11,10 +11,9 @@ import createFlash from '~/flash';
import { __ } from '~/locale';
import { stripQuotes } from '../filtered_search_utils';
-import { defaultMilestones, DEBOUNCE_DELAY } from '../constants';
+import { DEFAULT_MILESTONES, DEBOUNCE_DELAY } from '../constants';
export default {
- defaultMilestones,
components: {
GlFilteredSearchToken,
GlFilteredSearchSuggestion,
@@ -34,6 +33,7 @@ export default {
data() {
return {
milestones: this.config.initialMilestones || [],
+ defaultMilestones: this.config.defaultMilestones || DEFAULT_MILESTONES,
loading: true,
};
},
@@ -89,12 +89,13 @@ export default {
</template>
<template #suggestions>
<gl-filtered-search-suggestion
- v-for="milestone in $options.defaultMilestones"
+ v-for="milestone in defaultMilestones"
:key="milestone.value"
:value="milestone.value"
- >{{ milestone.text }}</gl-filtered-search-suggestion
>
- <gl-dropdown-divider />
+ {{ milestone.text }}
+ </gl-filtered-search-suggestion>
+ <gl-dropdown-divider v-if="defaultMilestones.length" />
<gl-loading-icon v-if="loading" />
<template v-else>
<gl-filtered-search-suggestion
diff --git a/app/assets/javascripts/vue_shared/components/filtered_search_dropdown.vue b/app/assets/javascripts/vue_shared/components/filtered_search_dropdown.vue
deleted file mode 100644
index 58afcebb7b3..00000000000
--- a/app/assets/javascripts/vue_shared/components/filtered_search_dropdown.vue
+++ /dev/null
@@ -1,154 +0,0 @@
-<script>
-import $ from 'jquery';
-import { GlDeprecatedButton } from '@gitlab/ui';
-import { __ } from '~/locale';
-import Icon from '~/vue_shared/components/icon.vue';
-/**
- * Renders a split dropdown with
- * an input that allows to search through the given
- * array of options.
- *
- * When there are no results and `showCreateMode` is true
- * it renders a create button with the value typed.
- */
-export default {
- name: 'FilteredSearchDropdown',
- components: {
- Icon,
- GlDeprecatedButton,
- },
- props: {
- title: {
- type: String,
- required: false,
- default: '',
- },
- buttonType: {
- required: false,
- validator: value =>
- ['primary', 'default', 'secondary', 'success', 'info', 'warning', 'danger'].indexOf(
- value,
- ) !== -1,
- default: 'default',
- },
- size: {
- required: false,
- type: String,
- default: 'sm',
- },
- items: {
- type: Array,
- required: true,
- },
- visibleItems: {
- type: Number,
- required: false,
- default: 5,
- },
- filterKey: {
- type: String,
- required: true,
- },
- showCreateMode: {
- type: Boolean,
- required: false,
- default: false,
- },
- createButtonText: {
- type: String,
- required: false,
- default: __('Create'),
- },
- },
- data() {
- return {
- filter: '',
- };
- },
- computed: {
- className() {
- return `btn btn-${this.buttonType} btn-${this.size}`;
- },
- filteredResults() {
- if (this.filter !== '') {
- return this.items.filter(
- item =>
- item[this.filterKey] &&
- item[this.filterKey].toLowerCase().includes(this.filter.toLowerCase()),
- );
- }
-
- return this.items.slice(0, this.visibleItems);
- },
- computedCreateButtonText() {
- return `${this.createButtonText} ${this.filter}`;
- },
- shouldRenderCreateButton() {
- return this.showCreateMode && this.filteredResults.length === 0 && this.filter !== '';
- },
- },
- mounted() {
- /**
- * Resets the filter every time the user closes the dropdown
- */
- $(this.$el)
- .on('shown.bs.dropdown', () => {
- this.$nextTick(() => this.$refs.searchInput.focus());
- })
- .on('hidden.bs.dropdown', () => {
- this.filter = '';
- });
- },
-};
-</script>
-<template>
- <div class="dropdown">
- <div class="btn-group">
- <slot name="mainAction" :class-name="className">
- <button type="button" :class="className">{{ title }}</button>
- </slot>
-
- <button
- type="button"
- :class="className"
- class="dropdown-toggle dropdown-toggle-split"
- data-toggle="dropdown"
- aria-haspopup="true"
- aria-expanded="false"
- :aria-label="__('Expand dropdown')"
- >
- <icon name="angle-down" :size="12" />
- </button>
- <div class="dropdown-menu dropdown-menu-right">
- <div class="dropdown-input">
- <input
- ref="searchInput"
- v-model="filter"
- type="search"
- :placeholder="__('Filter')"
- class="js-filtered-dropdown-input dropdown-input-field"
- />
- <icon class="dropdown-input-search" name="search" />
- </div>
-
- <div class="dropdown-content">
- <ul>
- <li v-for="(result, i) in filteredResults" :key="i" class="js-filtered-dropdown-result">
- <slot name="result" :result="result">{{ result[filterKey] }}</slot>
- </li>
- </ul>
- </div>
-
- <div v-if="shouldRenderCreateButton" class="dropdown-footer">
- <slot name="footer" :filter="filter">
- <gl-deprecated-button
- class="js-dropdown-create-button btn-transparent"
- @click="$emit('createItem', filter)"
- >{{ computedCreateButtonText }}</gl-deprecated-button
- >
- </slot>
- </div>
- </div>
- </div>
- </div>
-</template>
diff --git a/app/assets/javascripts/vue_shared/components/gl_mentions.vue b/app/assets/javascripts/vue_shared/components/gl_mentions.vue
index 00bc46257ed..da4b0aedef5 100644
--- a/app/assets/javascripts/vue_shared/components/gl_mentions.vue
+++ b/app/assets/javascripts/vue_shared/components/gl_mentions.vue
@@ -9,6 +9,7 @@ const AutoComplete = {
Issues: 'issues',
Labels: 'labels',
Members: 'members',
+ MergeRequests: 'mergeRequests',
};
function doesCurrentLineStartWith(searchString, fullText, selectionStart) {
@@ -99,6 +100,14 @@ const autoCompleteMap = {
${icon}`;
},
},
+ [AutoComplete.MergeRequests]: {
+ filterValues() {
+ return this[AutoComplete.MergeRequests];
+ },
+ menuItemTemplate({ original }) {
+ return `<small>${original.reference || original.iid}</small> ${escape(original.title)}`;
+ },
+ },
};
export default {
@@ -139,6 +148,13 @@ export default {
: `~${original.title}`,
values: this.getValues(AutoComplete.Labels),
},
+ {
+ trigger: '!',
+ lookup: value => value.iid + value.title,
+ menuItemTemplate: autoCompleteMap[AutoComplete.MergeRequests].menuItemTemplate,
+ selectTemplate: ({ original }) => original.reference || `!${original.iid}`,
+ values: this.getValues(AutoComplete.MergeRequests),
+ },
],
});
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 2625fcc9d09..6ff6f10f786 100644
--- a/app/assets/javascripts/vue_shared/components/header_ci_component.vue
+++ b/app/assets/javascripts/vue_shared/components/header_ci_component.vue
@@ -1,4 +1,5 @@
<script>
+/* eslint-disable vue/no-v-html */
import { GlTooltipDirective, GlLink, GlDeprecatedButton } from '@gitlab/ui';
import { __, sprintf } from '~/locale';
import CiIconBadge from './ci_badge_link.vue';
diff --git a/app/assets/javascripts/vue_shared/components/help_popover.vue b/app/assets/javascripts/vue_shared/components/help_popover.vue
index a57fa09f753..7154360611f 100644
--- a/app/assets/javascripts/vue_shared/components/help_popover.vue
+++ b/app/assets/javascripts/vue_shared/components/help_popover.vue
@@ -1,6 +1,6 @@
<script>
import $ from 'jquery';
-import Icon from '~/vue_shared/components/icon.vue';
+import { GlIcon } from '@gitlab/ui';
import { inserted } from '~/feature_highlight/feature_highlight_helper';
import { mouseenter, debouncedMouseleave, togglePopover } from '~/shared/popover';
@@ -11,7 +11,7 @@ import { mouseenter, debouncedMouseleave, togglePopover } from '~/shared/popover
export default {
name: 'HelpPopover',
components: {
- Icon,
+ GlIcon,
},
props: {
options: {
@@ -44,6 +44,6 @@ export default {
</script>
<template>
<button type="button" class="btn btn-blank btn-transparent btn-help" tabindex="0">
- <icon name="question" />
+ <gl-icon name="question" />
</button>
</template>
diff --git a/app/assets/javascripts/vue_shared/components/icon.vue b/app/assets/javascripts/vue_shared/components/icon.vue
deleted file mode 100644
index 68eeadf0f25..00000000000
--- a/app/assets/javascripts/vue_shared/components/icon.vue
+++ /dev/null
@@ -1,72 +0,0 @@
-<script>
-import iconsPath from '@gitlab/svgs/dist/icons.svg';
-
-// only allow classes in images.scss e.g. s12
-const validSizes = [8, 10, 12, 14, 16, 18, 24, 32, 48, 72];
-let iconValidator = () => true;
-
-/*
- During development/tests we want to validate that we are just using icons that are actually defined
-*/
-if (process.env.NODE_ENV !== 'production') {
- // eslint-disable-next-line global-require
- const data = require('@gitlab/svgs/dist/icons.json');
- const { icons } = data;
- iconValidator = value => {
- if (icons.includes(value)) {
- return true;
- }
- // eslint-disable-next-line no-console
- console.warn(`Icon '${value}' is not a known icon of @gitlab/gitlab-svg`);
- return false;
- };
-}
-
-/** This is a re-usable vue component for rendering a svg sprite icon
- * @example
- * <icon
- * name="retry"
- * :size="32"
- * class="top"
- * />
- */
-export default {
- props: {
- name: {
- type: String,
- required: true,
- validator: iconValidator,
- },
-
- size: {
- type: Number,
- required: false,
- default: 16,
- validator: value => validSizes.includes(value),
- },
- },
-
- computed: {
- spriteHref() {
- return `${iconsPath}#${this.name}`;
- },
- iconTestClass() {
- return `ic-${this.name}`;
- },
- iconSizeClass() {
- return this.size ? `s${this.size}` : '';
- },
- },
-};
-</script>
-
-<template>
- <svg
- :key="spriteHref"
- :class="[iconSizeClass, iconTestClass]"
- aria-hidden="true"
- v-on="$listeners"
- >
- <use v-bind="{ 'xlink:href': spriteHref }" />
- </svg>
-</template>
diff --git a/app/assets/javascripts/vue_shared/components/issue/issue_milestone.vue b/app/assets/javascripts/vue_shared/components/issue/issue_milestone.vue
index cfbc5b0df3c..c745ea61f8b 100644
--- a/app/assets/javascripts/vue_shared/components/issue/issue_milestone.vue
+++ b/app/assets/javascripts/vue_shared/components/issue/issue_milestone.vue
@@ -1,13 +1,12 @@
<script>
-import { GlTooltip } from '@gitlab/ui';
+import { GlTooltip, GlIcon } from '@gitlab/ui';
import { __, sprintf } from '~/locale';
import timeagoMixin from '~/vue_shared/mixins/timeago';
import { timeFor, parsePikadayDate, dateInWords } from '~/lib/utils/datetime_utility';
-import Icon from '~/vue_shared/components/icon.vue';
export default {
components: {
- Icon,
+ GlIcon,
GlTooltip,
},
mixins: [timeagoMixin],
@@ -73,7 +72,7 @@ export default {
</script>
<template>
<div ref="milestoneDetails" class="issue-milestone-details">
- <icon :size="16" class="inline icon" name="clock" />
+ <gl-icon :size="16" class="gl-mr-2" name="clock" />
<span class="milestone-title d-inline-block">{{ milestone.title }}</span>
<gl-tooltip :target="() => $refs.milestoneDetails" placement="bottom" class="js-item-milestone">
<span class="bold">{{ __('Milestone') }}</span> <br />
diff --git a/app/assets/javascripts/vue_shared/components/issue/related_issuable_item.vue b/app/assets/javascripts/vue_shared/components/issue/related_issuable_item.vue
index 1662e7923b7..2ff4033a07e 100644
--- a/app/assets/javascripts/vue_shared/components/issue/related_issuable_item.vue
+++ b/app/assets/javascripts/vue_shared/components/issue/related_issuable_item.vue
@@ -1,6 +1,7 @@
<script>
+/* eslint-disable vue/no-v-html */
import '~/commons/bootstrap';
-import { GlIcon, GlTooltip, GlTooltipDirective } from '@gitlab/ui';
+import { GlIcon, GlTooltip, GlTooltipDirective, GlButton } from '@gitlab/ui';
import { sprintf } from '~/locale';
import IssueMilestone from './issue_milestone.vue';
import IssueAssignees from './issue_assignees.vue';
@@ -18,6 +19,7 @@ export default {
GlTooltip,
IssueWeight: () => import('ee_component/boards/components/issue_card_weight.vue'),
IssueDueDate,
+ GlButton,
},
directives: {
GlTooltip: GlTooltipDirective,
@@ -29,6 +31,16 @@ export default {
required: false,
default: false,
},
+ isLocked: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ lockedMessage: {
+ type: String,
+ required: false,
+ default: '',
+ },
},
computed: {
stateTitle() {
@@ -156,19 +168,27 @@ export default {
</div>
</div>
- <button
- v-if="canRemove"
+ <span
+ v-if="isLocked"
+ ref="lockIcon"
+ v-gl-tooltip
+ class="gl-px-3 gl-display-inline-block gl-cursor-not-allowed"
+ :title="lockedMessage"
+ >
+ <gl-icon name="lock" />
+ </span>
+ <gl-button
+ v-else-if="canRemove"
ref="removeButton"
v-gl-tooltip
+ icon="close"
+ category="tertiary"
:disabled="removeDisabled"
- type="button"
- class="btn btn-default btn-svg btn-item-remove js-issue-item-remove-button"
+ class="js-issue-item-remove-button gl-ml-3"
data-qa-selector="remove_related_issue_button"
:title="__('Remove')"
:aria-label="__('Remove')"
@click="onRemoveRequest"
- >
- <icon :size="16" class="btn-item-remove-icon" name="close" />
- </button>
+ />
</div>
</template>
diff --git a/app/assets/javascripts/vue_shared/components/lib/utils/diff_utils.js b/app/assets/javascripts/vue_shared/components/lib/utils/diff_utils.js
index 188ab1769a4..221c4f5b8a8 100644
--- a/app/assets/javascripts/vue_shared/components/lib/utils/diff_utils.js
+++ b/app/assets/javascripts/vue_shared/components/lib/utils/diff_utils.js
@@ -1,5 +1,3 @@
-/* eslint-disable import/prefer-default-export */
-
function trimFirstCharOfLineContent(text) {
if (!text) {
return text;
diff --git a/app/assets/javascripts/vue_shared/components/markdown/field.vue b/app/assets/javascripts/vue_shared/components/markdown/field.vue
index 6df0119c3db..a48c279d0e3 100644
--- a/app/assets/javascripts/vue_shared/components/markdown/field.vue
+++ b/app/assets/javascripts/vue_shared/components/markdown/field.vue
@@ -1,14 +1,15 @@
<script>
+/* eslint-disable vue/no-v-html */
import $ from 'jquery';
import '~/behaviors/markdown/render_gfm';
import { unescape } from 'lodash';
+import { GlIcon } from '@gitlab/ui';
import { __, sprintf } from '~/locale';
import { stripHtml } from '~/lib/utils/text_utility';
import { deprecatedCreateFlash as Flash } from '~/flash';
import GLForm from '~/gl_form';
import MarkdownHeader from './header.vue';
import MarkdownToolbar from './toolbar.vue';
-import Icon from '../icon.vue';
import GlMentions from '~/vue_shared/components/gl_mentions.vue';
import Suggestions from '~/vue_shared/components/markdown/suggestions.vue';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
@@ -19,7 +20,7 @@ export default {
GlMentions,
MarkdownHeader,
MarkdownToolbar,
- Icon,
+ GlIcon,
Suggestions,
},
mixins: [glFeatureFlagsMixin()],
@@ -168,11 +169,12 @@ export default {
emojis: this.enableAutocomplete,
members: this.enableAutocomplete && !this.glFeatures.tributeAutocomplete,
issues: this.enableAutocomplete && !this.glFeatures.tributeAutocomplete,
- mergeRequests: this.enableAutocomplete,
+ mergeRequests: this.enableAutocomplete && !this.glFeatures.tributeAutocomplete,
epics: this.enableAutocomplete,
milestones: this.enableAutocomplete,
labels: this.enableAutocomplete && !this.glFeatures.tributeAutocomplete,
snippets: this.enableAutocomplete,
+ vulnerabilities: this.enableAutocomplete,
});
},
beforeDestroy() {
@@ -254,7 +256,7 @@ export default {
href="#"
:aria-label="__('Leave zen mode')"
>
- <icon :size="16" name="screen-normal" />
+ <gl-icon :size="16" name="minimize" />
</a>
<markdown-toolbar
:markdown-docs-path="markdownDocsPath"
diff --git a/app/assets/javascripts/vue_shared/components/markdown/header.vue b/app/assets/javascripts/vue_shared/components/markdown/header.vue
index 7e6edcfbd25..d0a0560846a 100644
--- a/app/assets/javascripts/vue_shared/components/markdown/header.vue
+++ b/app/assets/javascripts/vue_shared/components/markdown/header.vue
@@ -1,15 +1,15 @@
<script>
import $ from 'jquery';
-import { GlPopover, GlButton, GlTooltipDirective } from '@gitlab/ui';
+import { GlPopover, GlButton, GlTooltipDirective, GlIcon } from '@gitlab/ui';
+import { s__ } from '~/locale';
import { getSelectedFragment } from '~/lib/utils/common_utils';
import { CopyAsGFM } from '../../../behaviors/markdown/copy_as_gfm';
import ToolbarButton from './toolbar_button.vue';
-import Icon from '../icon.vue';
export default {
components: {
ToolbarButton,
- Icon,
+ GlIcon,
GlPopover,
GlButton,
},
@@ -55,6 +55,15 @@ export default {
mdSuggestion() {
return ['```suggestion:-0+0', `{text}`, '```'].join('\n');
},
+ isMac() {
+ // Accessing properties using ?. to allow tests to use
+ // this component without setting up window.gl.client.
+ // In production, window.gl.client should always be present.
+ return Boolean(window.gl?.client?.isMac);
+ },
+ modifierKey() {
+ return this.isMac ? '⌘' : s__('KeyboardKey|Ctrl+');
+ },
},
mounted() {
$(document).on('markdown-preview:show.vue', this.previewMarkdownTab);
@@ -129,8 +138,22 @@ export default {
</li>
<li :class="{ active: !previewMarkdown }" class="md-header-toolbar">
<div class="d-inline-block">
- <toolbar-button tag="**" :button-title="__('Add bold text')" icon="bold" />
- <toolbar-button tag="*" :button-title="__('Add italic text')" icon="italic" />
+ <toolbar-button
+ tag="**"
+ :button-title="
+ sprintf(s__('MarkdownEditor|Add bold text (%{modifierKey}B)'), { modifierKey })
+ "
+ shortcuts="mod+b"
+ icon="bold"
+ />
+ <toolbar-button
+ tag="_"
+ :button-title="
+ sprintf(s__('MarkdownEditor|Add italic text (%{modifierKey}I)'), { modifierKey })
+ "
+ shortcuts="mod+i"
+ icon="italic"
+ />
<toolbar-button
:prepend="true"
:tag="tag"
@@ -181,7 +204,10 @@ export default {
<toolbar-button
tag="[{text}](url)"
tag-select="url"
- :button-title="__('Add a link')"
+ :button-title="
+ sprintf(s__('MarkdownEditor|Add a link (%{modifierKey}K)'), { modifierKey })
+ "
+ shortcuts="mod+k"
icon="link"
/>
</div>
@@ -221,7 +247,7 @@ export default {
:title="__('Go full screen')"
type="button"
>
- <icon name="screen-full" />
+ <gl-icon name="maximize" />
</button>
</div>
</li>
diff --git a/app/assets/javascripts/vue_shared/components/markdown/suggestion_diff_header.vue b/app/assets/javascripts/vue_shared/components/markdown/suggestion_diff_header.vue
index 4de80e9b4c2..1fc54d2f52e 100644
--- a/app/assets/javascripts/vue_shared/components/markdown/suggestion_diff_header.vue
+++ b/app/assets/javascripts/vue_shared/components/markdown/suggestion_diff_header.vue
@@ -1,11 +1,10 @@
<script>
-import { GlDeprecatedButton, GlLoadingIcon, GlTooltipDirective } from '@gitlab/ui';
-import Icon from '~/vue_shared/components/icon.vue';
+import { GlButton, GlLoadingIcon, GlTooltipDirective, GlIcon } from '@gitlab/ui';
import { __ } from '~/locale';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
export default {
- components: { Icon, GlDeprecatedButton, GlLoadingIcon },
+ components: { GlIcon, GlButton, GlLoadingIcon },
directives: { 'gl-tooltip': GlTooltipDirective },
mixins: [glFeatureFlagsMixin()],
props: {
@@ -97,7 +96,7 @@ export default {
<div class="qa-suggestion-diff-header js-suggestion-diff-header font-weight-bold">
{{ __('Suggested change') }}
<a v-if="helpPagePath" :href="helpPagePath" :aria-label="__('Help')" class="js-help-btn">
- <icon name="question-o" css-classes="link-highlight" />
+ <gl-icon name="question-o" css-classes="link-highlight" />
</a>
</div>
<div v-if="isApplied" class="badge badge-success">{{ __('Applied') }}</div>
@@ -106,14 +105,14 @@ export default {
<span>{{ applyingSuggestionsMessage }}</span>
</div>
<div v-else-if="canApply && canBeBatched && isBatched" class="d-flex align-items-center">
- <gl-deprecated-button
+ <gl-button
class="btn-inverted js-remove-from-batch-btn btn-grouped"
:disabled="isApplying"
@click="removeSuggestionFromBatch"
>
{{ __('Remove from batch') }}
- </gl-deprecated-button>
- <gl-deprecated-button
+ </gl-button>
+ <gl-button
v-gl-tooltip.viewport="__('This also resolves all related threads')"
class="btn-inverted js-apply-batch-btn btn-grouped"
:disabled="isApplying"
@@ -124,26 +123,26 @@ export default {
<span class="badge badge-pill badge-pill-success">
{{ batchSuggestionsCount }}
</span>
- </gl-deprecated-button>
+ </gl-button>
</div>
<div v-else class="d-flex align-items-center">
- <gl-deprecated-button
+ <gl-button
v-if="canBeBatched && !isDisableButton"
class="btn-inverted js-add-to-batch-btn btn-grouped"
:disabled="isDisableButton"
@click="addSuggestionToBatch"
>
{{ __('Add suggestion to batch') }}
- </gl-deprecated-button>
+ </gl-button>
<span v-gl-tooltip.viewport="tooltipMessage" tabindex="0">
- <gl-deprecated-button
+ <gl-button
class="btn-inverted js-apply-btn btn-grouped"
:disabled="isDisableButton"
variant="success"
@click="applySuggestion"
>
{{ __('Apply suggestion') }}
- </gl-deprecated-button>
+ </gl-button>
</span>
</div>
</div>
diff --git a/app/assets/javascripts/vue_shared/components/markdown/suggestion_diff_row.vue b/app/assets/javascripts/vue_shared/components/markdown/suggestion_diff_row.vue
index 112bd03b49b..9059f0d2a8b 100644
--- a/app/assets/javascripts/vue_shared/components/markdown/suggestion_diff_row.vue
+++ b/app/assets/javascripts/vue_shared/components/markdown/suggestion_diff_row.vue
@@ -1,4 +1,5 @@
<script>
+/* eslint-disable vue/no-v-html */
export default {
name: 'SuggestionDiffRow',
props: {
@@ -26,9 +27,14 @@ export default {
<td class="diff-line-num new_line border-top-0 border-bottom-0" :class="lineType">
{{ line.new_line }}
</td>
- <td class="line_content" :class="[{ 'd-table-cell': displayAsCell }, lineType]">
- <span v-if="line.rich_text" v-html="line.rich_text"></span>
- <span v-else-if="line.text">{{ line.text }}</span>
+ <td
+ class="line_content"
+ :class="[{ 'd-table-cell': displayAsCell }, lineType]"
+ data-testid="suggestion-diff-content"
+ >
+ <span v-if="line.rich_text" class="line" v-html="line.rich_text"></span>
+ <span v-else-if="line.text" class="line">{{ line.text }}</span>
+ <span v-else class="line"></span>
</td>
</tr>
</template>
diff --git a/app/assets/javascripts/vue_shared/components/markdown/suggestions.vue b/app/assets/javascripts/vue_shared/components/markdown/suggestions.vue
index 1216484b35f..083f581af05 100644
--- a/app/assets/javascripts/vue_shared/components/markdown/suggestions.vue
+++ b/app/assets/javascripts/vue_shared/components/markdown/suggestions.vue
@@ -1,10 +1,14 @@
<script>
import Vue from 'vue';
+import { GlSafeHtmlDirective as SafeHtml } from '@gitlab/ui';
import { __ } from '~/locale';
import SuggestionDiff from './suggestion_diff.vue';
import { deprecatedCreateFlash as Flash } from '~/flash';
export default {
+ directives: {
+ SafeHtml,
+ },
props: {
lineType: {
type: String,
@@ -115,6 +119,6 @@ export default {
<template>
<div>
<div class="flash-container js-suggestions-flash"></div>
- <div v-show="isRendered" ref="container" class="md" v-html="noteHtml"></div>
+ <div v-show="isRendered" ref="container" v-safe-html="noteHtml" class="md"></div>
</div>
</template>
diff --git a/app/assets/javascripts/vue_shared/components/markdown/toolbar_button.vue b/app/assets/javascripts/vue_shared/components/markdown/toolbar_button.vue
index f37dd9e171c..6c35741e7e5 100644
--- a/app/assets/javascripts/vue_shared/components/markdown/toolbar_button.vue
+++ b/app/assets/javascripts/vue_shared/components/markdown/toolbar_button.vue
@@ -1,10 +1,9 @@
<script>
-import { GlTooltipDirective } from '@gitlab/ui';
-import Icon from '../icon.vue';
+import { GlTooltipDirective, GlIcon } from '@gitlab/ui';
export default {
components: {
- Icon,
+ GlIcon,
},
directives: {
GlTooltip: GlTooltipDirective,
@@ -47,6 +46,26 @@ export default {
required: false,
default: 0,
},
+
+ /**
+ * A string (or an array of strings) of
+ * [mousetrap](https://craig.is/killing/mice) keyboard shortcuts
+ * that should be attached to this button. For example:
+ * "command+k"
+ * ...or...
+ * ["command+k", "ctrl+k"]
+ */
+ shortcuts: {
+ type: [String, Array],
+ required: false,
+ default: () => [],
+ },
+ },
+ computed: {
+ shortcutsString() {
+ const shortcutArray = Array.isArray(this.shortcuts) ? this.shortcuts : [this.shortcuts];
+ return JSON.stringify(shortcutArray);
+ },
},
};
</script>
@@ -60,6 +79,7 @@ export default {
:data-md-block="tagBlock"
:data-md-tag-content="tagContent"
:data-md-prepend="prepend"
+ :data-md-shortcuts="shortcutsString"
:title="buttonTitle"
:aria-label="buttonTitle"
type="button"
@@ -67,6 +87,6 @@ export default {
data-container="body"
@click="() => $emit('click')"
>
- <icon :name="icon" />
+ <gl-icon :name="icon" />
</button>
</template>
diff --git a/app/assets/javascripts/vue_shared/components/modal_copy_button.vue b/app/assets/javascripts/vue_shared/components/modal_copy_button.vue
index 69ba5cb97e2..35ba7c665d5 100644
--- a/app/assets/javascripts/vue_shared/components/modal_copy_button.vue
+++ b/app/assets/javascripts/vue_shared/components/modal_copy_button.vue
@@ -1,14 +1,13 @@
<script>
import $ from 'jquery';
-import { GlDeprecatedButton, GlTooltipDirective } from '@gitlab/ui';
+import { GlDeprecatedButton, GlTooltipDirective, GlIcon } from '@gitlab/ui';
import Clipboard from 'clipboard';
import { __ } from '~/locale';
-import Icon from '~/vue_shared/components/icon.vue';
export default {
components: {
GlDeprecatedButton,
- Icon,
+ GlIcon,
},
directives: {
@@ -121,7 +120,7 @@ export default {
:title="title"
>
<slot>
- <icon name="copy-to-clipboard" />
+ <gl-icon name="copy-to-clipboard" />
</slot>
</gl-deprecated-button>
</template>
diff --git a/app/assets/javascripts/vue_shared/components/notes/noteable_warning.vue b/app/assets/javascripts/vue_shared/components/notes/noteable_warning.vue
index f986b105f20..c12012d8419 100644
--- a/app/assets/javascripts/vue_shared/components/notes/noteable_warning.vue
+++ b/app/assets/javascripts/vue_shared/components/notes/noteable_warning.vue
@@ -1,8 +1,8 @@
<script>
-import { GlLink } from '@gitlab/ui';
+/* eslint-disable vue/no-v-html */
+import { GlLink, GlIcon } from '@gitlab/ui';
import { escape } from 'lodash';
import { __, sprintf } from '~/locale';
-import icon from '../icon.vue';
function buildDocsLinkStart(path) {
return `<a href="${escape(path)}" target="_blank" rel="noopener noreferrer">`;
@@ -16,7 +16,7 @@ const NoteableTypeText = {
export default {
components: {
- icon,
+ GlIcon,
GlLink,
},
props: {
@@ -89,7 +89,7 @@ export default {
</script>
<template>
<div class="issuable-note-warning">
- <icon v-if="!isLockedAndConfidential" :name="warningIcon" :size="16" class="icon inline" />
+ <gl-icon v-if="!isLockedAndConfidential" :name="warningIcon" :size="16" class="icon inline" />
<span v-if="isLockedAndConfidential" ref="lockedAndConfidential">
<span v-html="confidentialAndLockedDiscussionText"></span>
diff --git a/app/assets/javascripts/vue_shared/components/notes/skeleton_note.vue b/app/assets/javascripts/vue_shared/components/notes/skeleton_note.vue
index e75ac8c54bc..53dbae39608 100644
--- a/app/assets/javascripts/vue_shared/components/notes/skeleton_note.vue
+++ b/app/assets/javascripts/vue_shared/components/notes/skeleton_note.vue
@@ -1,5 +1,5 @@
<script>
-import { GlSkeletonLoading } from '@gitlab/ui';
+import { GlDeprecatedSkeletonLoading as GlSkeletonLoading } from '@gitlab/ui';
import TimelineEntryItem from '~/vue_shared/components/notes/timeline_entry_item.vue';
export default {
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 fe57d4f29ca..f30676e8ef3 100644
--- a/app/assets/javascripts/vue_shared/components/notes/system_note.vue
+++ b/app/assets/javascripts/vue_shared/components/notes/system_note.vue
@@ -1,4 +1,6 @@
<script>
+/* eslint-disable vue/no-v-html */
+
/**
* Common component to render a system note, icon and user information.
*
@@ -18,10 +20,15 @@
*/
import $ from 'jquery';
import { mapGetters, mapActions, mapState } from 'vuex';
-import { GlDeprecatedButton, GlSkeletonLoading, GlTooltipDirective } from '@gitlab/ui';
+import {
+ GlButton,
+ GlDeprecatedSkeletonLoading as GlSkeletonLoading,
+ GlTooltipDirective,
+ GlIcon,
+ GlSafeHtmlDirective as SafeHtml,
+} from '@gitlab/ui';
import descriptionVersionHistoryMixin from 'ee_else_ce/notes/mixins/description_version_history';
import noteHeader from '~/notes/components/note_header.vue';
-import Icon from '~/vue_shared/components/icon.vue';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import TimelineEntryItem from './timeline_entry_item.vue';
import { spriteIcon } from '../../../lib/utils/common_utils';
@@ -32,14 +39,15 @@ const MAX_VISIBLE_COMMIT_LIST_COUNT = 3;
export default {
name: 'SystemNote',
components: {
- Icon,
+ GlIcon,
noteHeader,
TimelineEntryItem,
- GlDeprecatedButton,
+ GlButton,
GlSkeletonLoading,
},
directives: {
GlTooltip: GlTooltipDirective,
+ SafeHtml,
},
mixins: [descriptionVersionHistoryMixin, glFeatureFlagsMixin()],
props: {
@@ -104,25 +112,28 @@ export default {
<div class="timeline-content">
<div class="note-header">
<note-header :author="note.author" :created-at="note.created_at" :note-id="note.id">
- <span v-html="actionTextHtml"></span>
+ <span v-safe-html="actionTextHtml"></span>
<template v-if="canSeeDescriptionVersion" slot="extra-controls">
&middot;
- <button type="button" class="btn-blank btn-link" @click="toggleDescriptionVersion">
- {{ __('Compare with previous version') }}
- <icon :name="descriptionVersionToggleIcon" :size="12" class="append-left-5" />
- </button>
+ <gl-button
+ variant="link"
+ :icon="descriptionVersionToggleIcon"
+ data-testid="compare-btn"
+ @click="toggleDescriptionVersion"
+ >{{ __('Compare with previous version') }}</gl-button
+ >
</template>
</note-header>
</div>
<div class="note-body">
<div
+ v-safe-html="note.note_html"
:class="{ 'system-note-commit-list': hasMoreCommits, 'hide-shade': expanded }"
class="note-text md"
- v-html="note.note_html"
></div>
<div v-if="hasMoreCommits" class="flex-list">
<div class="system-note-commit-list-toggler flex-row" @click="expanded = !expanded">
- <icon :name="toggleIcon" :size="8" class="gl-mr-2" />
+ <gl-icon :name="toggleIcon" :size="8" class="gl-mr-2" />
<span>{{ __('Toggle commit list') }}</span>
</div>
</div>
@@ -130,17 +141,18 @@ export default {
<pre v-if="isLoadingDescriptionVersion" class="loading-state">
<gl-skeleton-loading />
</pre>
- <pre v-else class="wrapper mt-2" v-html="descriptionVersion"></pre>
- <gl-deprecated-button
+ <pre v-else v-safe-html="descriptionVersion" class="wrapper mt-2"></pre>
+ <gl-button
v-if="displayDeleteButton"
- ref="deleteDescriptionVersionButton"
v-gl-tooltip
:title="__('Remove description history')"
- class="btn-transparent delete-description-history"
+ variant="default"
+ category="tertiary"
+ icon="remove"
+ class="delete-description-history"
+ data-testid="delete-description-version-button"
@click="deleteDescriptionVersion"
- >
- <icon name="remove" />
- </gl-deprecated-button>
+ />
</div>
</div>
</div>
diff --git a/app/assets/javascripts/vue_shared/components/project_selector/project_list_item.vue b/app/assets/javascripts/vue_shared/components/project_selector/project_list_item.vue
index e053a9ddaa6..154671fe9fa 100644
--- a/app/assets/javascripts/vue_shared/components/project_selector/project_list_item.vue
+++ b/app/assets/javascripts/vue_shared/components/project_selector/project_list_item.vue
@@ -1,14 +1,14 @@
<script>
-import { GlDeprecatedButton } from '@gitlab/ui';
+/* eslint-disable vue/no-v-html */
+import { GlButton, GlIcon } from '@gitlab/ui';
import { isString } from 'lodash';
-import Icon from '~/vue_shared/components/icon.vue';
import ProjectAvatar from '~/vue_shared/components/project_avatar/default.vue';
import highlight from '~/lib/utils/highlight';
import { truncateNamespace } from '~/lib/utils/text_utility';
export default {
name: 'ProjectListItem',
- components: { Icon, ProjectAvatar, GlDeprecatedButton },
+ components: { GlIcon, ProjectAvatar, GlButton },
props: {
project: {
type: Object,
@@ -40,17 +40,16 @@ export default {
};
</script>
<template>
- <gl-deprecated-button
- class="d-flex align-items-center btn pt-1 pb-1 border-0 project-list-item"
+ <gl-button
+ category="tertiary"
+ class="gl-display-flex gl-align-items-center gl-justify-content-start! gl-mb-2 gl-w-full"
@click="onClick"
>
- <icon
- class="gl-ml-3 gl-mr-3 flex-shrink-0 position-top-0 js-selected-icon"
- :class="{ 'js-selected visible': selected, 'js-unselected invisible': !selected }"
- name="mobile-issue-close"
- />
- <project-avatar class="flex-shrink-0 js-project-avatar" :project="project" :size="32" />
- <div class="d-flex flex-wrap project-namespace-name-container">
+ <div
+ class="gl-display-flex gl-align-items-center gl-flex-wrap project-namespace-name-container"
+ >
+ <gl-icon v-if="selected" class="js-selected-icon" name="mobile-issue-close" />
+ <project-avatar class="gl-flex-shrink-0 js-project-avatar" :project="project" :size="32" />
<div
v-if="truncatedNamespace"
:title="projectNameWithNamespace"
@@ -65,5 +64,5 @@ export default {
v-html="highlightedProjectName"
></div>
</div>
- </gl-deprecated-button>
+ </gl-button>
</template>
diff --git a/app/assets/javascripts/vue_shared/components/project_selector/project_selector.vue b/app/assets/javascripts/vue_shared/components/project_selector/project_selector.vue
index 0b91588a006..4e2029cd74f 100644
--- a/app/assets/javascripts/vue_shared/components/project_selector/project_selector.vue
+++ b/app/assets/javascripts/vue_shared/components/project_selector/project_selector.vue
@@ -100,7 +100,7 @@ export default {
@bottomReached="bottomReached"
>
<template v-if="!showLoadingIndicator" #items>
- <div class="d-flex flex-column">
+ <div class="gl-display-flex gl-flex-direction-column gl-p-3">
<project-list-item
v-for="project in projectSearchResults"
:key="project.id"
diff --git a/app/assets/javascripts/vue_shared/components/recaptcha_modal.vue b/app/assets/javascripts/vue_shared/components/recaptcha_modal.vue
index 25701df33f3..fc1f3675a3d 100644
--- a/app/assets/javascripts/vue_shared/components/recaptcha_modal.vue
+++ b/app/assets/javascripts/vue_shared/components/recaptcha_modal.vue
@@ -1,4 +1,5 @@
<script>
+/* eslint-disable vue/no-v-html */
import DeprecatedModal from './deprecated_modal.vue';
import { eventHub } from './recaptcha_eventhub';
diff --git a/app/assets/javascripts/vue_shared/components/registry/code_instruction.vue b/app/assets/javascripts/vue_shared/components/registry/code_instruction.vue
new file mode 100644
index 00000000000..08ee23d25bf
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/registry/code_instruction.vue
@@ -0,0 +1,82 @@
+<script>
+import { uniqueId } from 'lodash';
+import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
+import Tracking from '~/tracking';
+
+export default {
+ name: 'CodeInstruction',
+ components: {
+ ClipboardButton,
+ },
+ mixins: [Tracking.mixin()],
+ props: {
+ label: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ instruction: {
+ type: String,
+ required: true,
+ },
+ copyText: {
+ type: String,
+ required: true,
+ },
+ multiline: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ trackingAction: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ trackingLabel: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ },
+ created() {
+ this.uniqueId = uniqueId();
+ },
+ methods: {
+ trackCopy() {
+ if (this.trackingAction) {
+ this.track(this.trackingAction, { label: this.trackingLabel });
+ }
+ },
+ generateFormId(name) {
+ return `${name}_${this.uniqueId}`;
+ },
+ },
+};
+</script>
+
+<template>
+ <div v-if="!multiline" class="gl-mb-3">
+ <label v-if="label" :for="generateFormId('instruction-input')">{{ label }}</label>
+ <div class="input-group gl-mb-3">
+ <input
+ :id="generateFormId('instruction-input')"
+ :value="instruction"
+ type="text"
+ class="form-control gl-font-monospace"
+ data-testid="instruction-input"
+ readonly
+ @copy="trackCopy"
+ />
+ <span class="input-group-append" data-testid="instruction-button" @click="trackCopy">
+ <clipboard-button :text="instruction" :title="copyText" class="input-group-text" />
+ </span>
+ </div>
+ </div>
+
+ <div v-else>
+ <pre class="gl-font-monospace" data-testid="multiline-instruction" @copy="trackCopy">{{
+ instruction
+ }}</pre>
+ </div>
+</template>
diff --git a/app/assets/javascripts/registry/shared/components/details_row.vue b/app/assets/javascripts/vue_shared/components/registry/details_row.vue
index 2e245fadead..2e245fadead 100644
--- a/app/assets/javascripts/registry/shared/components/details_row.vue
+++ b/app/assets/javascripts/vue_shared/components/registry/details_row.vue
diff --git a/app/assets/javascripts/packages/details/components/history_element.vue b/app/assets/javascripts/vue_shared/components/registry/history_item.vue
index 8a51c1528cf..a60b630b207 100644
--- a/app/assets/javascripts/packages/details/components/history_element.vue
+++ b/app/assets/javascripts/vue_shared/components/registry/history_item.vue
@@ -3,12 +3,11 @@ import { GlIcon } from '@gitlab/ui';
import TimelineEntryItem from '~/vue_shared/components/notes/timeline_entry_item.vue';
export default {
- name: 'HistoryElement',
+ name: 'HistoryItem',
components: {
GlIcon,
TimelineEntryItem,
},
-
props: {
icon: {
type: String,
@@ -29,7 +28,9 @@ export default {
<slot></slot>
</span>
</div>
- <div class="note-body"></div>
+ <div class="note-body">
+ <slot name="body"></slot>
+ </div>
</div>
</timeline-entry-item>
</template>
diff --git a/app/assets/javascripts/registry/explorer/components/list_item.vue b/app/assets/javascripts/vue_shared/components/registry/list_item.vue
index c57645cc3a1..50a19dc2156 100644
--- a/app/assets/javascripts/registry/explorer/components/list_item.vue
+++ b/app/assets/javascripts/vue_shared/components/registry/list_item.vue
@@ -10,11 +10,6 @@ export default {
default: false,
required: false,
},
- last: {
- type: Boolean,
- default: false,
- required: false,
- },
disabled: {
type: Boolean,
default: false,
@@ -35,12 +30,10 @@ export default {
computed: {
optionalClasses() {
return {
- 'gl-border-t-1': !this.first,
- 'gl-border-t-2': this.first,
- 'gl-border-b-1': !this.last,
- 'gl-border-b-2': this.last,
+ 'gl-border-t-transparent': !this.first && !this.selected,
+ 'gl-border-t-gray-100': this.first && !this.selected,
'disabled-content': this.disabled,
- 'gl-border-gray-100': !this.selected,
+ 'gl-border-b-gray-100': !this.selected,
'gl-bg-blue-50 gl-border-blue-200': this.selected,
};
},
@@ -58,21 +51,26 @@ export default {
<template>
<div
- class="gl-display-flex gl-flex-direction-column gl-border-b-solid gl-border-t-solid"
+ 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-4 gl-px-2">
+ <div class="gl-display-flex gl-align-items-center gl-py-5">
<div
v-if="$slots['left-action']"
class="gl-w-7 gl-display-none gl-display-sm-flex gl-justify-content-start gl-pl-2"
>
<slot name="left-action"></slot>
</div>
- <div class="gl-display-flex gl-flex-direction-column gl-flex-fill-1">
+ <div
+ class="gl-display-flex gl-xs-flex-direction-column gl-justify-content-space-between gl-align-items-stretch gl-flex-fill-1"
+ >
<div
- class="gl-display-flex gl-align-items-center gl-justify-content-space-between gl-text-body gl-font-weight-bold"
+ class="gl-display-flex gl-flex-direction-column gl-justify-content-space-between gl-xs-mb-3 gl-min-w-0 gl-flex-grow-1"
>
- <div class="gl-display-flex gl-align-items-center">
+ <div
+ v-if="$slots['left-primary']"
+ class="gl-display-flex gl-align-items-center gl-text-body gl-font-weight-bold gl-min-h-6 gl-min-w-0"
+ >
<slot name="left-primary"></slot>
<gl-button
v-if="detailsSlots.length > 0"
@@ -83,24 +81,33 @@ export default {
@click="toggleDetails"
/>
</div>
- <div>
- <slot name="right-primary"></slot>
+ <div
+ v-if="$slots['left-secondary']"
+ class="gl-display-flex gl-align-items-center gl-text-gray-500 gl-mt-1 gl-min-h-6 gl-min-w-0 gl-flex-fill-1"
+ >
+ <slot name="left-secondary"></slot>
</div>
</div>
<div
- class="gl-display-flex gl-align-items-center gl-justify-content-space-between gl-font-sm gl-text-gray-300"
+ class="gl-display-flex gl-flex-direction-column gl-sm-align-items-flex-end gl-justify-content-space-between gl-text-gray-500 gl-flex-shrink-0"
>
- <div>
- <slot name="left-secondary"></slot>
+ <div
+ v-if="$slots['right-primary']"
+ class="gl-display-flex gl-align-items-center gl-sm-text-body gl-sm-font-weight-bold gl-min-h-6"
+ >
+ <slot name="right-primary"></slot>
</div>
- <div>
+ <div
+ v-if="$slots['right-secondary']"
+ class="gl-display-flex gl-align-items-center gl-mt-1 gl-min-h-6"
+ >
<slot name="right-secondary"></slot>
</div>
</div>
</div>
<div
v-if="$slots['right-action']"
- class="gl-w-9 gl-display-none gl-display-sm-flex gl-justify-content-end gl-pr-2"
+ class="gl-w-9 gl-display-none gl-display-sm-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
new file mode 100644
index 00000000000..8ef623b68eb
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/registry/metadata_item.vue
@@ -0,0 +1,63 @@
+<script>
+import { GlIcon, GlLink } from '@gitlab/ui';
+import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate.vue';
+
+export default {
+ name: 'MetadataItem',
+ components: {
+ GlIcon,
+ GlLink,
+ TooltipOnTruncate,
+ },
+ props: {
+ icon: {
+ type: String,
+ required: false,
+ default: null,
+ },
+ text: {
+ type: String,
+ required: true,
+ },
+ link: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ size: {
+ type: String,
+ required: false,
+ default: 's',
+ validator(value) {
+ return !value || ['xs', 's', 'm', 'l', 'xl'].includes(value);
+ },
+ },
+ },
+ computed: {
+ sizeClass() {
+ return `mw-${this.size}`;
+ },
+ },
+};
+</script>
+
+<template>
+ <div class="gl-display-inline-flex gl-align-items-center">
+ <gl-icon v-if="icon" :name="icon" class="gl-text-gray-500 gl-mr-3" />
+ <tooltip-on-truncate v-if="link" :title="text" class="gl-text-truncate" :class="sizeClass">
+ <gl-link :href="link" class="gl-font-weight-bold">
+ {{ text }}
+ </gl-link>
+ </tooltip-on-truncate>
+ <div
+ v-else
+ data-testid="metadata-item-text"
+ class="gl-font-weight-bold gl-display-inline-flex"
+ :class="sizeClass"
+ >
+ <tooltip-on-truncate :title="text" class="gl-text-truncate">
+ {{ text }}
+ </tooltip-on-truncate>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/vue_shared/components/registry/title_area.vue b/app/assets/javascripts/vue_shared/components/registry/title_area.vue
new file mode 100644
index 00000000000..cc33b8f85cd
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/registry/title_area.vue
@@ -0,0 +1,66 @@
+<script>
+import { GlAvatar } from '@gitlab/ui';
+
+export default {
+ name: 'TitleArea',
+ components: {
+ GlAvatar,
+ },
+ props: {
+ avatar: {
+ type: String,
+ default: null,
+ required: false,
+ },
+ title: {
+ type: String,
+ default: null,
+ required: false,
+ },
+ },
+ data() {
+ return {
+ metadataSlots: [],
+ };
+ },
+ mounted() {
+ this.metadataSlots = Object.keys(this.$slots).filter(k => k.startsWith('metadata_'));
+ },
+};
+</script>
+
+<template>
+ <div class="gl-display-flex gl-justify-content-space-between gl-py-3">
+ <div class="gl-flex-direction-column">
+ <div class="gl-display-flex">
+ <gl-avatar v-if="avatar" :src="avatar" shape="rect" class="gl-align-self-center gl-mr-4" />
+
+ <div class="gl-display-flex gl-flex-direction-column">
+ <h1 class="gl-font-size-h1 gl-mt-3 gl-mb-2" data-testid="title">
+ <slot name="title">{{ title }}</slot>
+ </h1>
+
+ <div
+ v-if="$slots['sub-header']"
+ class="gl-display-flex gl-align-items-center gl-text-gray-500 gl-mt-1"
+ >
+ <slot name="sub-header"></slot>
+ </div>
+ </div>
+ </div>
+
+ <div class="gl-display-flex gl-flex-wrap gl-align-items-center gl-mt-3">
+ <div
+ v-for="(row, metadataIndex) in metadataSlots"
+ :key="metadataIndex"
+ class="gl-display-flex gl-align-items-center gl-mr-5"
+ >
+ <slot :name="row"></slot>
+ </div>
+ </div>
+ </div>
+ <div v-if="$slots['right-actions']" class="gl-mt-3">
+ <slot name="right-actions"></slot>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/vue_shared/components/resizable_chart/constants.js b/app/assets/javascripts/vue_shared/components/resizable_chart/constants.js
index edc5ffb7b77..68d86777995 100644
--- a/app/assets/javascripts/vue_shared/components/resizable_chart/constants.js
+++ b/app/assets/javascripts/vue_shared/components/resizable_chart/constants.js
@@ -1,6 +1,6 @@
export const DEFAULT_RX = 0.4;
-export const DEFAULT_BAR_WIDTH = 6;
-export const DEFAULT_LABEL_WIDTH = 4;
-export const DEFAULT_LABEL_HEIGHT = 5;
+export const DEFAULT_BAR_WIDTH = 4;
+export const DEFAULT_LABEL_WIDTH = 3;
+export const DEFAULT_LABEL_HEIGHT = 3;
export const BAR_HEIGHTS = [5, 7, 9, 14, 21, 35, 50, 80];
export const GRID_YS = [30, 60, 90];
diff --git a/app/assets/javascripts/vue_shared/components/resizable_chart/skeleton_loader.vue b/app/assets/javascripts/vue_shared/components/resizable_chart/skeleton_loader.vue
index 306fa61780f..a9f35a73db0 100644
--- a/app/assets/javascripts/vue_shared/components/resizable_chart/skeleton_loader.vue
+++ b/app/assets/javascripts/vue_shared/components/resizable_chart/skeleton_loader.vue
@@ -61,35 +61,37 @@ export default {
};
</script>
<template>
- <gl-skeleton-loader :unique-key="uniqueKey">
- <rect
- v-for="(y, index) in $options.GRID_YS"
- :key="`grid-${index}`"
- data-testid="skeleton-chart-grid"
- x="0"
- :y="`${y}%`"
- width="100%"
- height="1px"
- />
- <rect
- v-for="(height, index) in $options.BAR_HEIGHTS"
- :key="`bar-${index}`"
- data-testid="skeleton-chart-bar"
- :x="`${getBarXPosition(index)}%`"
- :y="`${90 - height}%`"
- :width="`${barWidth}%`"
- :height="`${height}%`"
- :rx="`${rx}%`"
- />
- <rect
- v-for="(height, index) in $options.BAR_HEIGHTS"
- :key="`label-${index}`"
- data-testid="skeleton-chart-label"
- :x="`${labelCentering + getBarXPosition(index)}%`"
- :y="`${100 - labelHeight}%`"
- :width="`${labelWidth}%`"
- :height="`${labelHeight}%`"
- :rx="`${rx}%`"
- />
- </gl-skeleton-loader>
+ <div class="gl-px-8">
+ <gl-skeleton-loader :unique-key="uniqueKey" class="gl-p-8">
+ <rect
+ v-for="(y, index) in $options.GRID_YS"
+ :key="`grid-${index}`"
+ data-testid="skeleton-chart-grid"
+ x="0"
+ :y="`${y}%`"
+ width="100%"
+ height="1px"
+ />
+ <rect
+ v-for="(height, index) in $options.BAR_HEIGHTS"
+ :key="`bar-${index}`"
+ data-testid="skeleton-chart-bar"
+ :x="`${getBarXPosition(index)}%`"
+ :y="`${90 - height}%`"
+ :width="`${barWidth}%`"
+ :height="`${height}%`"
+ :rx="`${rx}%`"
+ />
+ <rect
+ v-for="(height, index) in $options.BAR_HEIGHTS"
+ :key="`label-${index}`"
+ data-testid="skeleton-chart-label"
+ :x="`${labelCentering + getBarXPosition(index)}%`"
+ :y="`${100 - labelHeight}%`"
+ :width="`${labelWidth}%`"
+ :height="`${labelHeight}%`"
+ :rx="`${rx}%`"
+ />
+ </gl-skeleton-loader>
+ </div>
</template>
diff --git a/app/assets/javascripts/vue_shared/components/rich_content_editor/services/build_custom_renderer.js b/app/assets/javascripts/vue_shared/components/rich_content_editor/services/build_custom_renderer.js
index a9c5d442f62..108c60c3edb 100644
--- a/app/assets/javascripts/vue_shared/components/rich_content_editor/services/build_custom_renderer.js
+++ b/app/assets/javascripts/vue_shared/components/rich_content_editor/services/build_custom_renderer.js
@@ -1,17 +1,19 @@
import { union, mapValues } from 'lodash';
import renderBlockHtml from './renderers/render_html_block';
-import renderKramdownList from './renderers/render_kramdown_list';
-import renderKramdownText from './renderers/render_kramdown_text';
+import renderHeading from './renderers/render_heading';
import renderIdentifierInstanceText from './renderers/render_identifier_instance_text';
import renderIdentifierParagraph from './renderers/render_identifier_paragraph';
import renderFontAwesomeHtmlInline from './renderers/render_font_awesome_html_inline';
import renderSoftbreak from './renderers/render_softbreak';
+import renderAttributeDefinition from './renderers/render_attribute_definition';
+import renderListItem from './renderers/render_list_item';
const htmlInlineRenderers = [renderFontAwesomeHtmlInline];
const htmlBlockRenderers = [renderBlockHtml];
-const listRenderers = [renderKramdownList];
-const paragraphRenderers = [renderIdentifierParagraph];
-const textRenderers = [renderKramdownText, renderIdentifierInstanceText];
+const headingRenderers = [renderHeading];
+const paragraphRenderers = [renderIdentifierParagraph, renderBlockHtml];
+const textRenderers = [renderIdentifierInstanceText, renderAttributeDefinition];
+const listItemRenderers = [renderListItem];
const softbreakRenderers = [renderSoftbreak];
const executeRenderer = (renderers, node, context) => {
@@ -25,7 +27,8 @@ const buildCustomHTMLRenderer = customRenderers => {
...customRenderers,
htmlBlock: union(htmlBlockRenderers, customRenderers?.htmlBlock),
htmlInline: union(htmlInlineRenderers, customRenderers?.htmlInline),
- list: union(listRenderers, customRenderers?.list),
+ heading: union(headingRenderers, customRenderers?.heading),
+ item: union(listItemRenderers, customRenderers?.listItem),
paragraph: union(paragraphRenderers, customRenderers?.paragraph),
text: union(textRenderers, customRenderers?.text),
softbreak: union(softbreakRenderers, customRenderers?.softbreak),
diff --git a/app/assets/javascripts/vue_shared/components/rich_content_editor/services/build_html_to_markdown_renderer.js b/app/assets/javascripts/vue_shared/components/rich_content_editor/services/build_html_to_markdown_renderer.js
index 868ede9426e..2bce691e793 100644
--- a/app/assets/javascripts/vue_shared/components/rich_content_editor/services/build_html_to_markdown_renderer.js
+++ b/app/assets/javascripts/vue_shared/components/rich_content_editor/services/build_html_to_markdown_renderer.js
@@ -28,6 +28,8 @@ const buildHTMLToMarkdownRender = (baseRenderer, formattingPreferences = {}) =>
const orderedListItemNode = 'OL LI';
const emphasisNode = 'EM, I';
const strongNode = 'STRONG, B';
+ const headingNode = 'H1, H2, H3, H4, H5, H6';
+ const preCodeNode = 'PRE CODE';
return {
TEXT_NODE(node) {
@@ -63,8 +65,10 @@ const buildHTMLToMarkdownRender = (baseRenderer, formattingPreferences = {}) =>
},
[unorderedListItemNode](node, subContent) {
const baseResult = baseRenderer.convert(node, subContent);
+ const formatted = baseResult.replace(/^(\s*)([*|-])/, `$1${unorderedListBulletChar}`);
+ const { attributeDefinition } = node.dataset;
- return baseResult.replace(/^(\s*)([*|-])/, `$1${unorderedListBulletChar}`);
+ return attributeDefinition ? `${formatted.trimRight()}\n${attributeDefinition}\n` : formatted;
},
[orderedListItemNode](node, subContent) {
const baseResult = baseRenderer.convert(node, subContent);
@@ -82,6 +86,19 @@ const buildHTMLToMarkdownRender = (baseRenderer, formattingPreferences = {}) =>
return result.replace(/^[*_]{2}/, strongSyntax).replace(/[*_]{2}$/, strongSyntax);
},
+ [headingNode](node, subContent) {
+ const result = baseRenderer.convert(node, subContent);
+ const { attributeDefinition } = node.dataset;
+
+ return attributeDefinition ? `${result.trimRight()}\n${attributeDefinition}\n\n` : result;
+ },
+ [preCodeNode](node, subContent) {
+ const isReferenceDefinition = Boolean(node.dataset.sseReferenceDefinition);
+
+ return isReferenceDefinition
+ ? `\n\n${node.innerText}\n\n`
+ : baseRenderer.convert(node, subContent);
+ },
};
};
diff --git a/app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/render_attribute_definition.js b/app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/render_attribute_definition.js
new file mode 100644
index 00000000000..bd419447a48
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/render_attribute_definition.js
@@ -0,0 +1,7 @@
+import { isAttributeDefinition } from './render_utils';
+
+const canRender = ({ literal }) => isAttributeDefinition(literal);
+
+const render = () => ({ type: 'html', content: '<!-- sse-attribute-definition -->' });
+
+export default { canRender, render };
diff --git a/app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/render_heading.js b/app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/render_heading.js
new file mode 100644
index 00000000000..71026fd0d65
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/render_heading.js
@@ -0,0 +1,6 @@
+import {
+ renderWithAttributeDefinitions as render,
+ willAlwaysRender as canRender,
+} from './render_utils';
+
+export default { render, canRender };
diff --git a/app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/render_identifier_paragraph.js b/app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/render_identifier_paragraph.js
index 4ec45ecd3a7..3f9c6291d1b 100644
--- a/app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/render_identifier_paragraph.js
+++ b/app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/render_identifier_paragraph.js
@@ -1,5 +1,3 @@
-import { renderUneditableBranch as render } from './render_utils';
-
const identifierRegex = /(^\[.+\]: .+)/;
const isIdentifier = text => {
@@ -10,4 +8,33 @@ const canRender = (node, context) => {
return isIdentifier(context.getChildrenText(node));
};
+const getReferenceDefinitions = (node, definitions = '') => {
+ if (!node) {
+ return definitions;
+ }
+
+ const definition = node.type === 'text' ? node.literal : '\n';
+
+ return getReferenceDefinitions(node.next, `${definitions}${definition}`);
+};
+
+const render = (node, { skipChildren }) => {
+ const content = getReferenceDefinitions(node.firstChild);
+
+ skipChildren();
+
+ return [
+ {
+ type: 'openTag',
+ tagName: 'pre',
+ classNames: ['code-block', 'language-markdown'],
+ attributes: { 'data-sse-reference-definition': true },
+ },
+ { type: 'openTag', tagName: 'code' },
+ { type: 'text', content },
+ { type: 'closeTag', tagName: 'code' },
+ { type: 'closeTag', tagName: 'pre' },
+ ];
+};
+
export default { canRender, render };
diff --git a/app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/render_kramdown_list.js b/app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/render_kramdown_list.js
deleted file mode 100644
index 949ca0e5c2a..00000000000
--- a/app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/render_kramdown_list.js
+++ /dev/null
@@ -1,24 +0,0 @@
-import { renderUneditableBranch as render } from './render_utils';
-
-const isKramdownTOC = ({ type, literal }) => type === 'text' && literal === 'TOC';
-
-const canRender = node => {
- let targetNode = node;
- while (targetNode !== null) {
- const { firstChild } = targetNode;
- const isLeaf = firstChild === null;
- if (isLeaf) {
- if (isKramdownTOC(targetNode)) {
- return true;
- }
-
- break;
- }
-
- targetNode = targetNode.firstChild;
- }
-
- return false;
-};
-
-export default { canRender, render };
diff --git a/app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/render_kramdown_text.js b/app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/render_kramdown_text.js
deleted file mode 100644
index 0551894918c..00000000000
--- a/app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/render_kramdown_text.js
+++ /dev/null
@@ -1,9 +0,0 @@
-import { renderUneditableLeaf as render } from './render_utils';
-
-const kramdownRegex = /(^{:.+}$)/;
-
-const canRender = ({ literal }) => {
- return kramdownRegex.test(literal);
-};
-
-export default { canRender, render };
diff --git a/app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/render_list_item.js b/app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/render_list_item.js
new file mode 100644
index 00000000000..71026fd0d65
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/render_list_item.js
@@ -0,0 +1,6 @@
+import {
+ renderWithAttributeDefinitions as render,
+ willAlwaysRender as canRender,
+} from './render_utils';
+
+export default { render, canRender };
diff --git a/app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/render_utils.js b/app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/render_utils.js
index cec6491557b..4cba2c70486 100644
--- a/app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/render_utils.js
+++ b/app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/render_utils.js
@@ -8,3 +8,31 @@ export const renderUneditableLeaf = (_, { origin }) => buildUneditableBlockToken
export const renderUneditableBranch = (_, { entering, origin }) =>
entering ? buildUneditableOpenTokens(origin()) : buildUneditableCloseToken();
+
+const attributeDefinitionRegexp = /(^{:.+}$)/;
+
+export const isAttributeDefinition = text => attributeDefinitionRegexp.test(text);
+
+const findAttributeDefinition = node => {
+ const literal =
+ node?.next?.firstChild?.literal || node?.firstChild?.firstChild?.next?.next?.literal; // for headings // for list items;
+
+ return isAttributeDefinition(literal) ? literal : null;
+};
+
+export const renderWithAttributeDefinitions = (node, { origin }) => {
+ const attributes = findAttributeDefinition(node);
+ const token = origin();
+
+ if (token.type === 'openTag' && attributes) {
+ Object.assign(token, {
+ attributes: {
+ 'data-attribute-definition': attributes,
+ },
+ });
+ }
+
+ return token;
+};
+
+export const willAlwaysRender = () => true;
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/collapsed_calendar_icon.vue b/app/assets/javascripts/vue_shared/components/sidebar/collapsed_calendar_icon.vue
index cc24fedceed..0ed5a050fe4 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/collapsed_calendar_icon.vue
+++ b/app/assets/javascripts/vue_shared/components/sidebar/collapsed_calendar_icon.vue
@@ -1,4 +1,5 @@
<script>
+import { GlIcon } from '@gitlab/ui';
import tooltip from '~/vue_shared/directives/tooltip';
export default {
@@ -6,6 +7,9 @@ export default {
directives: {
tooltip,
},
+ components: {
+ GlIcon,
+ },
props: {
containerClass: {
type: String,
@@ -47,7 +51,7 @@ export default {
data-boundary="viewport"
@click="click"
>
- <i v-if="showIcon" class="fa fa-calendar" aria-hidden="true"> </i>
+ <gl-icon v-if="showIcon" name="calendar" />
<slot>
<span> {{ text }} </span>
</slot>
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select/base.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/base.vue
index 5eef439aa90..1ef3d5627ae 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select/base.vue
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/base.vue
@@ -14,7 +14,10 @@ import DropdownSearchInput from './dropdown_search_input.vue';
import DropdownFooter from './dropdown_footer.vue';
import DropdownCreateLabel from './dropdown_create_label.vue';
+import { DropdownVariant } from '../labels_select_vue/constants';
+
export default {
+ DropdownVariant,
components: {
DropdownTitle,
DropdownValue,
@@ -80,6 +83,11 @@ export default {
required: false,
default: false,
},
+ variant: {
+ type: String,
+ required: false,
+ default: DropdownVariant.Sidebar,
+ },
},
computed: {
hiddenInputName() {
@@ -123,7 +131,7 @@ export default {
<template>
<div class="block labels js-labels-block">
<dropdown-value-collapsed
- v-if="showCreate"
+ v-if="showCreate && variant === $options.DropdownVariant.Sidebar"
:labels="context.labels"
@onValueClick="handleCollapsedValueClick"
/>
@@ -150,18 +158,21 @@ export default {
:labels-path="labelsPath"
:namespace="namespace"
:labels="context.labels"
- :show-extra-options="!showCreate"
+ :show-extra-options="!showCreate || variant !== $options.DropdownVariant.Sidebar"
:enable-scoped-labels="enableScopedLabels"
/>
<div
- class="dropdown-menu dropdown-select dropdown-menu-paging
-dropdown-menu-labels dropdown-menu-selectable"
+ class="dropdown-menu dropdown-select dropdown-menu-paging dropdown-menu-labels dropdown-menu-selectable"
>
<div class="dropdown-page-one">
- <dropdown-header v-if="showCreate" />
+ <dropdown-header v-if="showCreate && variant === $options.DropdownVariant.Sidebar" />
<dropdown-search-input />
<div class="dropdown-content" data-qa-selector="labels_dropdown_content"></div>
- <div class="dropdown-loading"><gl-loading-icon /></div>
+ <div class="dropdown-loading">
+ <gl-loading-icon
+ class="gl-display-flex gl-justify-content-center gl-align-items-center gl-h-full"
+ />
+ </div>
<dropdown-footer
v-if="showCreate"
:labels-web-url="labelsWebUrl"
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_create_label.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_create_label.vue
index 74c5e063c3d..434aabc3df9 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_create_label.vue
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_create_label.vue
@@ -1,7 +1,14 @@
<script>
+import { GlButton, GlTooltipDirective } from '@gitlab/ui';
import { __ } from '~/locale';
export default {
+ components: {
+ GlButton,
+ },
+ directives: {
+ GlTooltip: GlTooltipDirective,
+ },
props: {
headerTitle: {
type: String,
@@ -10,29 +17,35 @@ export default {
},
},
created() {
- this.suggestedColors = gon.suggested_label_colors;
+ const rawLabelsColors = gon.suggested_label_colors;
+ this.suggestedColors = Object.keys(rawLabelsColors).map(colorCode => ({
+ colorCode,
+ title: rawLabelsColors[colorCode],
+ }));
},
};
</script>
<template>
<div class="dropdown-page-two dropdown-new-label">
- <div class="dropdown-title">
- <button
+ <div
+ class="dropdown-title gl-display-flex gl-justify-content-space-between gl-align-items-center"
+ >
+ <gl-button
:aria-label="__('Go back')"
- type="button"
- class="dropdown-title-button dropdown-menu-back"
- >
- <i aria-hidden="true" class="fa fa-arrow-left" data-hidden="true"> </i>
- </button>
+ category="tertiary"
+ class="dropdown-menu-back"
+ icon="arrow-left"
+ size="small"
+ />
{{ headerTitle }}
- <button
+ <gl-button
:aria-label="__('Close')"
- type="button"
- class="dropdown-title-button dropdown-menu-close"
- >
- <i aria-hidden="true" class="fa fa-times dropdown-menu-close-icon" data-hidden="true"> </i>
- </button>
+ category="tertiary"
+ class="dropdown-menu-close"
+ icon="close"
+ size="small"
+ />
</div>
<div class="dropdown-content">
<div class="dropdown-labels-error js-label-error"></div>
@@ -46,10 +59,12 @@ export default {
<a
v-for="(color, index) in suggestedColors"
:key="index"
- :data-color="color"
+ v-gl-tooltip
+ :data-color="color.colorCode"
:style="{
- backgroundColor: color,
+ backgroundColor: color.colorCode,
}"
+ :title="color.title"
href="#"
>
&nbsp;
@@ -65,12 +80,12 @@ export default {
/>
</div>
<div class="clearfix">
- <button type="button" class="btn btn-primary float-left js-new-label-btn disabled">
+ <gl-button category="secondary" class="float-left js-new-label-btn disabled">
{{ __('Create') }}
- </button>
- <button type="button" class="btn btn-default float-right js-cancel-label-btn">
+ </gl-button>
+ <gl-button category="secondary" class="float-right js-cancel-label-btn">
{{ __('Cancel') }}
- </button>
+ </gl-button>
</div>
</div>
</div>
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_header.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_header.vue
index eb837be165b..7b2802650a2 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_header.vue
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_header.vue
@@ -1,16 +1,22 @@
<script>
-export default {};
+import { GlIcon } from '@gitlab/ui';
+
+export default {
+ components: {
+ GlIcon,
+ },
+};
</script>
<template>
- <div class="dropdown-title">
- <span>{{ __('Assign labels') }}</span>
+ <div class="dropdown-title gl-display-flex gl-justify-content-center">
+ <span class="gl-ml-auto">{{ __('Assign labels') }}</span>
<button
:aria-label="__('Close')"
type="button"
- class="dropdown-title-button dropdown-menu-close"
+ class="dropdown-title-button dropdown-menu-close gl-ml-auto"
>
- <i aria-hidden="true" class="fa fa-times dropdown-menu-close-icon" data-hidden="true"> </i>
+ <gl-icon name="close" aria-hidden="true" class="dropdown-menu-close-icon" />
</button>
</div>
</template>
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed.vue
index 05446903286..c2ebf78d541 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed.vue
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed.vue
@@ -1,4 +1,5 @@
<script>
+import { GlIcon } from '@gitlab/ui';
import { s__, sprintf } from '~/locale';
import tooltip from '~/vue_shared/directives/tooltip';
@@ -6,6 +7,9 @@ export default {
directives: {
tooltip,
},
+ components: {
+ GlIcon,
+ },
props: {
labels: {
type: Array,
@@ -49,7 +53,7 @@ export default {
data-boundary="viewport"
@click="handleClick"
>
- <i aria-hidden="true" data-hidden="true" class="fa fa-tags"> </i>
+ <gl-icon name="labels" />
<span>{{ labels.length }}</span>
</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 248e9929833..34f5517ef99 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
@@ -166,7 +166,11 @@ export default {
!state.showDropdownButton &&
!state.showDropdownContents
) {
- this.handleDropdownClose(state.labels.filter(label => label.touched));
+ let filterFn = label => label.touched;
+ if (this.isDropdownVariantEmbedded) {
+ filterFn = label => label.set;
+ }
+ this.handleDropdownClose(state.labels.filter(filterFn));
}
},
/**
@@ -186,7 +190,7 @@ export default {
].some(
className =>
target?.classList.contains(className) ||
- target?.parentElement.classList.contains(className),
+ target?.parentElement?.classList.contains(className),
);
const hadExceptionParent = ['.js-btn-back', '.js-labels-list'].some(
@@ -248,10 +252,10 @@ export default {
:allow-label-edit="allowLabelEdit"
:labels-select-in-progress="labelsSelectInProgress"
/>
- <dropdown-value v-show="!showDropdownButton">
+ <dropdown-value>
<slot></slot>
</dropdown-value>
- <dropdown-button v-show="dropdownButtonVisible" />
+ <dropdown-button v-show="dropdownButtonVisible" class="gl-mt-2" />
<dropdown-contents
v-if="dropdownButtonVisible && showDropdownContents"
ref="dropdownContents"
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/store/actions.js b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/store/actions.js
index e624bd1eaee..2d236566b3d 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/store/actions.js
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/store/actions.js
@@ -54,5 +54,8 @@ export const createLabel = ({ state, dispatch }, label) => {
});
};
+export const replaceSelectedLabels = ({ commit }, selectedLabels) =>
+ commit(types.REPLACE_SELECTED_LABELS, selectedLabels);
+
export const updateSelectedLabels = ({ commit }, labels) =>
commit(types.UPDATE_SELECTED_LABELS, { labels });
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/store/mutation_types.js b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/store/mutation_types.js
index 2e044dc3b3c..af92665d4eb 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/store/mutation_types.js
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/store/mutation_types.js
@@ -15,6 +15,7 @@ export const RECEIVE_CREATE_LABEL_FAILURE = 'RECEIVE_CREATE_LABEL_FAILURE';
export const TOGGLE_DROPDOWN_BUTTON = 'TOGGLE_DROPDOWN_VISIBILITY';
export const TOGGLE_DROPDOWN_CONTENTS = 'TOGGLE_DROPDOWN_CONTENTS';
+export const REPLACE_SELECTED_LABELS = 'REPLACE_SELECTED_LABELS';
export const UPDATE_SELECTED_LABELS = 'UPDATE_SELECTED_LABELS';
export const TOGGLE_DROPDOWN_CONTENTS_CREATE_VIEW = 'TOGGLE_DROPDOWN_CONTENTS_CREATE_VIEW';
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/store/mutations.js b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/store/mutations.js
index 54f8c78b4e1..7edd290a819 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/store/mutations.js
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/store/mutations.js
@@ -57,6 +57,10 @@ export default {
state.labelCreateInProgress = false;
},
+ [types.REPLACE_SELECTED_LABELS](state, selectedLabels = []) {
+ state.selectedLabels = selectedLabels;
+ },
+
[types.UPDATE_SELECTED_LABELS](state, { labels }) {
// Find the label to update from all the labels
// and change `set` prop value to represent their current state.
diff --git a/app/assets/javascripts/vue_shared/components/timezone_dropdown.vue b/app/assets/javascripts/vue_shared/components/timezone_dropdown.vue
index 148bd501a8e..135b9842cbf 100644
--- a/app/assets/javascripts/vue_shared/components/timezone_dropdown.vue
+++ b/app/assets/javascripts/vue_shared/components/timezone_dropdown.vue
@@ -1,12 +1,12 @@
<script>
-import { GlNewDropdown, GlDeprecatedDropdownItem, GlSearchBoxByType, GlIcon } from '@gitlab/ui';
+import { GlDropdown, GlDeprecatedDropdownItem, GlSearchBoxByType, GlIcon } from '@gitlab/ui';
import { __ } from '~/locale';
import autofocusonshow from '~/vue_shared/directives/autofocusonshow';
export default {
name: 'TimezoneDropdown',
components: {
- GlNewDropdown,
+ GlDropdown,
GlDeprecatedDropdownItem,
GlSearchBoxByType,
GlIcon,
@@ -74,7 +74,7 @@ export default {
};
</script>
<template>
- <gl-new-dropdown :text="value" block lazy menu-class="gl-w-full!">
+ <gl-dropdown :text="value" block lazy menu-class="gl-w-full!">
<template #button-content>
<span class="gl-flex-grow-1" :class="{ 'gl-text-gray-300': !value }">
{{ selectedTimezoneLabel }}
@@ -98,5 +98,5 @@ export default {
<gl-deprecated-dropdown-item v-if="!filteredResults.length" data-testid="noMatchingResults">
{{ $options.tranlations.noResultsText }}
</gl-deprecated-dropdown-item>
- </gl-new-dropdown>
+ </gl-dropdown>
</template>
diff --git a/app/assets/javascripts/vue_shared/components/todo_button.vue b/app/assets/javascripts/vue_shared/components/todo_button.vue
new file mode 100644
index 00000000000..debf19ccca6
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/todo_button.vue
@@ -0,0 +1,28 @@
+<script>
+import { GlButton } from '@gitlab/ui';
+import { __ } from '~/locale';
+
+export default {
+ components: {
+ GlButton,
+ },
+ props: {
+ isTodo: {
+ type: Boolean,
+ required: false,
+ default: true,
+ },
+ },
+ computed: {
+ buttonLabel() {
+ return this.isTodo ? __('Mark as done') : __('Add a To-Do');
+ },
+ },
+};
+</script>
+
+<template>
+ <gl-button v-bind="$attrs" :aria-label="buttonLabel" @click="$emit('click', $event)">
+ {{ buttonLabel }}
+ </gl-button>
+</template>
diff --git a/app/assets/javascripts/vue_shared/components/toggle_button.vue b/app/assets/javascripts/vue_shared/components/toggle_button.vue
index 540edc9f61c..29d4516bece 100644
--- a/app/assets/javascripts/vue_shared/components/toggle_button.vue
+++ b/app/assets/javascripts/vue_shared/components/toggle_button.vue
@@ -73,7 +73,7 @@ export default {
'is-disabled': disabledInput,
'is-loading': isLoading,
}"
- @click="toggleFeature"
+ @click.prevent="toggleFeature"
>
<gl-loading-icon class="loading-icon" />
<span class="toggle-icon">
diff --git a/app/assets/javascripts/vue_shared/components/tooltip_on_truncate.vue b/app/assets/javascripts/vue_shared/components/tooltip_on_truncate.vue
index 4ea3d162da2..579ad53e6db 100644
--- a/app/assets/javascripts/vue_shared/components/tooltip_on_truncate.vue
+++ b/app/assets/javascripts/vue_shared/components/tooltip_on_truncate.vue
@@ -61,9 +61,9 @@ export default {
v-tooltip
:title="title"
:data-placement="placement"
- class="js-show-tooltip"
+ class="js-show-tooltip gl-min-w-0"
>
<slot></slot>
</span>
- <span v-else> <slot></slot> </span>
+ <span v-else class="gl-min-w-0"> <slot></slot> </span>
</template>
diff --git a/app/assets/javascripts/vue_shared/components/url_sync.vue b/app/assets/javascripts/vue_shared/components/url_sync.vue
index 389d42f0829..2844d9e9e94 100644
--- a/app/assets/javascripts/vue_shared/components/url_sync.vue
+++ b/app/assets/javascripts/vue_shared/components/url_sync.vue
@@ -1,6 +1,6 @@
<script>
import { historyPushState } from '~/lib/utils/common_utils';
-import { setUrlParams } from '~/lib/utils/url_utility';
+import { mergeUrlParams } from '~/lib/utils/url_utility';
export default {
props: {
@@ -14,7 +14,7 @@ export default {
immediate: true,
deep: true,
handler(newQuery) {
- historyPushState(setUrlParams(newQuery, window.location.href, true));
+ historyPushState(mergeUrlParams(newQuery, window.location.href, { spreadArrays: true }));
},
},
},
diff --git a/app/assets/javascripts/vue_shared/components/user_popover/user_popover.vue b/app/assets/javascripts/vue_shared/components/user_popover/user_popover.vue
index 699e466e848..6aaff000845 100644
--- a/app/assets/javascripts/vue_shared/components/user_popover/user_popover.vue
+++ b/app/assets/javascripts/vue_shared/components/user_popover/user_popover.vue
@@ -1,6 +1,6 @@
<script>
-import { GlPopover, GlSkeletonLoading } from '@gitlab/ui';
-import Icon from '~/vue_shared/components/icon.vue';
+/* eslint-disable vue/no-v-html */
+import { GlPopover, GlDeprecatedSkeletonLoading as GlSkeletonLoading, GlIcon } from '@gitlab/ui';
import UserAvatarImage from '../user_avatar/user_avatar_image.vue';
import { glEmojiTag } from '../../../emoji';
@@ -10,7 +10,7 @@ export default {
name: 'UserPopover',
maxSkeletonLines: MAX_SKELETON_LINES,
components: {
- Icon,
+ GlIcon,
GlPopover,
GlSkeletonLoading,
UserAvatarImage,
@@ -74,16 +74,16 @@ export default {
</div>
<div class="gl-text-gray-500">
<div v-if="user.bio" class="gl-display-flex gl-mb-2">
- <icon name="profile" class="gl-text-gray-400 gl-flex-shrink-0" />
+ <gl-icon name="profile" class="gl-text-gray-400 gl-flex-shrink-0" />
<span ref="bio" class="gl-ml-2" v-html="user.bioHtml"></span>
</div>
<div v-if="user.workInformation" class="gl-display-flex gl-mb-2">
- <icon name="work" class="gl-text-gray-400 gl-flex-shrink-0" />
+ <gl-icon name="work" class="gl-text-gray-400 gl-flex-shrink-0" />
<span ref="workInformation" class="gl-ml-2">{{ user.workInformation }}</span>
</div>
</div>
<div v-if="user.location" class="js-location gl-text-gray-500 gl-display-flex">
- <icon name="location" class="gl-text-gray-400 flex-shrink-0" />
+ <gl-icon name="location" class="gl-text-gray-400 flex-shrink-0" />
<span class="gl-ml-2">{{ user.location }}</span>
</div>
<div v-if="statusHtml" class="js-user-status gl-mt-3">
diff --git a/app/assets/javascripts/vue_shared/components/web_ide_link.vue b/app/assets/javascripts/vue_shared/components/web_ide_link.vue
new file mode 100644
index 00000000000..8307c6d3b55
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/web_ide_link.vue
@@ -0,0 +1,118 @@
+<script>
+import $ from 'jquery';
+import { __ } from '~/locale';
+import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue';
+import ActionsButton from '~/vue_shared/components/actions_button.vue';
+
+const KEY_WEB_IDE = 'webide';
+const KEY_GITPOD = 'gitpod';
+
+export default {
+ components: {
+ ActionsButton,
+ LocalStorageSync,
+ },
+ props: {
+ webIdeUrl: {
+ type: String,
+ required: true,
+ },
+ needsToFork: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ showWebIdeButton: {
+ type: Boolean,
+ required: false,
+ default: true,
+ },
+ showGitpodButton: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ gitpodUrl: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ gitpodEnabled: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ },
+ data() {
+ return {
+ selection: KEY_WEB_IDE,
+ };
+ },
+ computed: {
+ actions() {
+ return [this.webIdeAction, this.gitpodAction].filter(x => x);
+ },
+ webIdeAction() {
+ if (!this.showWebIdeButton) {
+ return null;
+ }
+
+ const handleOptions = this.needsToFork
+ ? { href: '#modal-confirm-fork', handle: () => this.showModal('#modal-confirm-fork') }
+ : { href: this.webIdeUrl };
+
+ return {
+ key: KEY_WEB_IDE,
+ text: __('Web IDE'),
+ secondaryText: __('Quickly and easily edit multiple files in your project.'),
+ tooltip: '',
+ attrs: {
+ 'data-qa-selector': 'web_ide_button',
+ },
+ ...handleOptions,
+ };
+ },
+ gitpodAction() {
+ if (!this.showGitpodButton) {
+ return null;
+ }
+
+ const handleOptions = this.gitpodEnabled
+ ? { href: this.gitpodUrl }
+ : { href: '#modal-enable-gitpod', handle: () => this.showModal('#modal-enable-gitpod') };
+
+ const secondaryText = __('Launch a ready-to-code development environment for your project.');
+
+ return {
+ key: KEY_GITPOD,
+ text: __('Gitpod'),
+ secondaryText,
+ tooltip: secondaryText,
+ attrs: {
+ 'data-qa-selector': 'gitpod_button',
+ },
+ ...handleOptions,
+ };
+ },
+ },
+ methods: {
+ select(key) {
+ this.selection = key;
+ },
+ showModal(id) {
+ $(id).modal('show');
+ },
+ },
+};
+</script>
+
+<template>
+ <div>
+ <actions-button :actions="actions" :selected-key="selection" @select="select" />
+ <local-storage-sync
+ storage-key="gl-web-ide-button-selected"
+ :value="selection"
+ @input="select"
+ />
+ </div>
+</template>
diff --git a/app/assets/javascripts/vue_shared/mixins/related_issuable_mixin.js b/app/assets/javascripts/vue_shared/mixins/related_issuable_mixin.js
index c628a67f7f5..be5f55a5220 100644
--- a/app/assets/javascripts/vue_shared/mixins/related_issuable_mixin.js
+++ b/app/assets/javascripts/vue_shared/mixins/related_issuable_mixin.js
@@ -2,7 +2,6 @@ import { isEmpty } from 'lodash';
import { sprintf, __ } from '~/locale';
import { formatDate } from '~/lib/utils/datetime_utility';
import tooltip from '~/vue_shared/directives/tooltip';
-import icon from '~/vue_shared/components/icon.vue';
import timeagoMixin from '~/vue_shared/mixins/timeago';
const mixins = {
@@ -100,9 +99,6 @@ const mixins = {
default: () => ({}),
},
},
- components: {
- icon,
- },
directives: {
tooltip,
},
diff --git a/app/assets/javascripts/vuex_shared/bindings.js b/app/assets/javascripts/vuex_shared/bindings.js
index edc31cfa69e..cc18b41e2de 100644
--- a/app/assets/javascripts/vuex_shared/bindings.js
+++ b/app/assets/javascripts/vuex_shared/bindings.js
@@ -9,7 +9,6 @@
* @param {string} root - the key of the state where to search fo they keys described in list
* @returns {Object} a dictionary with all the computed properties generated
*/
-// eslint-disable-next-line import/prefer-default-export
export const mapComputed = (list, defaultUpdateFn, root) => {
const result = {};
list.forEach(item => {
diff --git a/app/assets/javascripts/vuex_shared/modules/members/index.js b/app/assets/javascripts/vuex_shared/modules/members/index.js
new file mode 100644
index 00000000000..ec6a94178f3
--- /dev/null
+++ b/app/assets/javascripts/vuex_shared/modules/members/index.js
@@ -0,0 +1,6 @@
+import createState from './state';
+
+export default initialState => ({
+ namespaced: true,
+ state: createState(initialState),
+});
diff --git a/app/assets/javascripts/vuex_shared/modules/members/state.js b/app/assets/javascripts/vuex_shared/modules/members/state.js
new file mode 100644
index 00000000000..1511961245c
--- /dev/null
+++ b/app/assets/javascripts/vuex_shared/modules/members/state.js
@@ -0,0 +1,5 @@
+export default ({ members, sourceId, currentUserId }) => ({
+ members,
+ sourceId,
+ currentUserId,
+});
diff --git a/app/assets/javascripts/whats_new/components/app.vue b/app/assets/javascripts/whats_new/components/app.vue
index d974556cb9e..a00661c214d 100644
--- a/app/assets/javascripts/whats_new/components/app.vue
+++ b/app/assets/javascripts/whats_new/components/app.vue
@@ -1,16 +1,40 @@
<script>
import { mapState, mapActions } from 'vuex';
-import { GlDrawer } from '@gitlab/ui';
+import { GlDrawer, GlBadge, GlIcon, GlLink } from '@gitlab/ui';
export default {
components: {
GlDrawer,
+ GlBadge,
+ GlIcon,
+ GlLink,
+ },
+ props: {
+ features: {
+ type: String,
+ required: false,
+ default: null,
+ },
},
computed: {
...mapState(['open']),
+ parsedFeatures() {
+ let features;
+
+ try {
+ features = JSON.parse(this.$props.features) || [];
+ } catch (err) {
+ features = [];
+ }
+
+ return features;
+ },
+ },
+ mounted() {
+ this.openDrawer();
},
methods: {
- ...mapActions(['closeDrawer']),
+ ...mapActions(['openDrawer', 'closeDrawer']),
},
};
</script>
@@ -19,11 +43,31 @@ export default {
<div>
<gl-drawer class="mt-6" :open="open" @close="closeDrawer">
<template #header>
- <h4>{{ __("What's new at GitLab") }}</h4>
- </template>
- <template>
- <div></div>
+ <h4 class="page-title my-2">{{ __("What's new at GitLab") }}</h4>
</template>
+ <div class="pb-6">
+ <div v-for="feature in parsedFeatures" :key="feature.title" class="mb-6">
+ <gl-link :href="feature.url" target="_blank">
+ <h5 class="gl-font-base">{{ feature.title }}</h5>
+ </gl-link>
+ <div class="mb-2">
+ <template v-for="package_name in feature.packages">
+ <gl-badge :key="package_name" size="sm" class="whats-new-item-badge mr-1">
+ <gl-icon name="license" />{{ package_name }}
+ </gl-badge>
+ </template>
+ </div>
+ <gl-link :href="feature.url" target="_blank">
+ <img
+ :alt="feature.title"
+ :src="feature.image_url"
+ class="img-thumbnail px-6 py-2 whats-new-item-image"
+ />
+ </gl-link>
+ <p class="pt-2">{{ feature.body }}</p>
+ <gl-link :href="feature.url" target="_blank">{{ __('Learn more') }}</gl-link>
+ </div>
+ </div>
</gl-drawer>
</div>
</template>
diff --git a/app/assets/javascripts/whats_new/components/trigger.vue b/app/assets/javascripts/whats_new/components/trigger.vue
deleted file mode 100644
index e6c48e92888..00000000000
--- a/app/assets/javascripts/whats_new/components/trigger.vue
+++ /dev/null
@@ -1,19 +0,0 @@
-<script>
-import { mapActions } from 'vuex';
-import { GlButton } from '@gitlab/ui';
-
-export default {
- components: {
- GlButton,
- },
- methods: {
- ...mapActions(['openDrawer']),
- },
-};
-</script>
-
-<template>
- <li>
- <gl-button variant="link" @click="openDrawer">{{ __("See what's new at GitLab") }}</gl-button>
- </li>
-</template>
diff --git a/app/assets/javascripts/whats_new/index.js b/app/assets/javascripts/whats_new/index.js
index c9ee3404d2a..19cdb590ae2 100644
--- a/app/assets/javascripts/whats_new/index.js
+++ b/app/assets/javascripts/whats_new/index.js
@@ -1,32 +1,28 @@
import Vue from 'vue';
import App from './components/app.vue';
-import Trigger from './components/trigger.vue';
import store from './store';
-export default () => {
- // eslint-disable-next-line no-new
- new Vue({
- el: document.getElementById('whats-new-app'),
- store,
- components: {
- App,
- },
-
- render(createElement) {
- return createElement('app');
- },
- });
+let whatsNewApp;
- // eslint-disable-next-line no-new
- new Vue({
- el: document.getElementById('whats-new-trigger'),
- store,
- components: {
- Trigger,
- },
+export default () => {
+ if (whatsNewApp) {
+ store.dispatch('openDrawer');
+ } else {
+ const whatsNewElm = document.getElementById('whats-new-app');
- render(createElement) {
- return createElement('trigger');
- },
- });
+ whatsNewApp = new Vue({
+ el: whatsNewElm,
+ store,
+ components: {
+ App,
+ },
+ render(createElement) {
+ return createElement('app', {
+ props: {
+ features: whatsNewElm.getAttribute('data-features'),
+ },
+ });
+ },
+ });
+ }
};