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

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
path: root/app
diff options
context:
space:
mode:
authorClement Ho <ClemMakesApps@gmail.com>2018-05-08 18:49:30 +0300
committerClement Ho <ClemMakesApps@gmail.com>2018-05-08 18:49:30 +0300
commit5955caed321e242bfebe52a4b47346a01a50e4f6 (patch)
treed9710c0732ce21801b4a79a281bec0bd39582a95 /app
parentf9e2b4730f58ba630344c9554eb907ab003abbd5 (diff)
parent533593e95cd3a922a2ec2ea43b345862361dfd67 (diff)
Merge branch 'master' into bootstrap4
Diffstat (limited to 'app')
-rw-r--r--app/assets/javascripts/behaviors/gl_emoji.js27
-rw-r--r--app/assets/javascripts/clusters/clusters_index.js26
-rw-r--r--app/assets/javascripts/clusters/components/gcp_signup_offer.js27
-rw-r--r--app/assets/javascripts/compare.js86
-rw-r--r--app/assets/javascripts/compare_autocomplete.js49
-rw-r--r--app/assets/javascripts/deploy_keys/components/action_btn.vue69
-rw-r--r--app/assets/javascripts/deploy_keys/components/app.vue212
-rw-r--r--app/assets/javascripts/deploy_keys/components/key.vue322
-rw-r--r--app/assets/javascripts/deploy_keys/components/keys_panel.vue101
-rw-r--r--app/assets/javascripts/deploy_keys/index.js39
-rw-r--r--app/assets/javascripts/deploy_keys/service/index.js25
-rw-r--r--app/assets/javascripts/deploy_keys/store/index.js4
-rw-r--r--app/assets/javascripts/emoji/index.js6
-rw-r--r--app/assets/javascripts/emoji/support/unicode_support_map.js40
-rw-r--r--app/assets/javascripts/environments/components/container.vue1
-rw-r--r--app/assets/javascripts/environments/components/environment_actions.vue18
-rw-r--r--app/assets/javascripts/environments/components/environment_external_url.vue14
-rw-r--r--app/assets/javascripts/environments/components/environment_monitoring.vue15
-rw-r--r--app/assets/javascripts/environments/components/environment_rollback.vue3
-rw-r--r--app/assets/javascripts/environments/components/environment_terminal_button.vue18
-rw-r--r--app/assets/javascripts/gfm_auto_complete.js9
-rw-r--r--app/assets/javascripts/gpg_badges.js6
-rw-r--r--app/assets/javascripts/ide/components/activity_bar.vue106
-rw-r--r--app/assets/javascripts/ide/components/changed_file_icon.vue10
-rw-r--r--app/assets/javascripts/ide/components/commit_sidebar/actions.vue16
-rw-r--r--app/assets/javascripts/ide/components/commit_sidebar/empty_state.vue65
-rw-r--r--app/assets/javascripts/ide/components/commit_sidebar/form.vue171
-rw-r--r--app/assets/javascripts/ide/components/commit_sidebar/list.vue103
-rw-r--r--app/assets/javascripts/ide/components/commit_sidebar/list_item.vue5
-rw-r--r--app/assets/javascripts/ide/components/commit_sidebar/radio_group.vue30
-rw-r--r--app/assets/javascripts/ide/components/commit_sidebar/success_message.vue33
-rw-r--r--app/assets/javascripts/ide/components/editor_mode_dropdown.vue80
-rw-r--r--app/assets/javascripts/ide/components/ide.vue225
-rw-r--r--app/assets/javascripts/ide/components/ide_context_bar.vue42
-rw-r--r--app/assets/javascripts/ide/components/ide_external_links.vue43
-rw-r--r--app/assets/javascripts/ide/components/ide_project_branches_tree.vue47
-rw-r--r--app/assets/javascripts/ide/components/ide_project_tree.vue65
-rw-r--r--app/assets/javascripts/ide/components/ide_repo_tree.vue41
-rw-r--r--app/assets/javascripts/ide/components/ide_review.vue62
-rw-r--r--app/assets/javascripts/ide/components/ide_side_bar.vue143
-rw-r--r--app/assets/javascripts/ide/components/ide_status_bar.vue96
-rw-r--r--app/assets/javascripts/ide/components/ide_tree.vue42
-rw-r--r--app/assets/javascripts/ide/components/ide_tree_list.vue76
-rw-r--r--app/assets/javascripts/ide/components/mr_file_icon.vue4
-rw-r--r--app/assets/javascripts/ide/components/new_dropdown/index.vue3
-rw-r--r--app/assets/javascripts/ide/components/repo_commit_section.vue95
-rw-r--r--app/assets/javascripts/ide/components/repo_editor.vue52
-rw-r--r--app/assets/javascripts/ide/components/repo_file.vue102
-rw-r--r--app/assets/javascripts/ide/components/repo_tab.vue36
-rw-r--r--app/assets/javascripts/ide/components/repo_tabs.vue17
-rw-r--r--app/assets/javascripts/ide/constants.js16
-rw-r--r--app/assets/javascripts/ide/ide_router.js7
-rw-r--r--app/assets/javascripts/ide/index.js33
-rw-r--r--app/assets/javascripts/ide/lib/editor.js8
-rw-r--r--app/assets/javascripts/ide/stores/actions.js20
-rw-r--r--app/assets/javascripts/ide/stores/actions/file.js13
-rw-r--r--app/assets/javascripts/ide/stores/actions/project.js24
-rw-r--r--app/assets/javascripts/ide/stores/getters.js46
-rw-r--r--app/assets/javascripts/ide/stores/modules/commit/actions.js49
-rw-r--r--app/assets/javascripts/ide/stores/mutation_types.js4
-rw-r--r--app/assets/javascripts/ide/stores/mutations.js20
-rw-r--r--app/assets/javascripts/ide/stores/mutations/branch.js5
-rw-r--r--app/assets/javascripts/ide/stores/mutations/file.js45
-rw-r--r--app/assets/javascripts/ide/stores/state.js6
-rw-r--r--app/assets/javascripts/ide/stores/utils.js10
-rw-r--r--app/assets/javascripts/issuable_form.js2
-rw-r--r--app/assets/javascripts/lib/utils/text_utility.js6
-rw-r--r--app/assets/javascripts/main.js61
-rw-r--r--app/assets/javascripts/mini_pipeline_graph_dropdown.js4
-rw-r--r--app/assets/javascripts/monitoring/components/graph.vue8
-rw-r--r--app/assets/javascripts/monitoring/components/graph/flag.vue22
-rw-r--r--app/assets/javascripts/monitoring/components/graph/path.vue22
-rw-r--r--app/assets/javascripts/monitoring/components/graph/track_line.vue10
-rw-r--r--app/assets/javascripts/monitoring/mixins/monitoring_mixins.js20
-rw-r--r--app/assets/javascripts/monitoring/utils/date_time_formatters.js2
-rw-r--r--app/assets/javascripts/monitoring/utils/multiple_time_series.js1
-rw-r--r--app/assets/javascripts/notes/components/comment_form.vue6
-rw-r--r--app/assets/javascripts/pages/groups/settings/ci_cd/show/index.js4
-rw-r--r--app/assets/javascripts/pages/ide/index.js9
-rw-r--r--app/assets/javascripts/pages/projects/clusters/gcp/login/index.js3
-rw-r--r--app/assets/javascripts/pages/projects/clusters/new/index.js3
-rw-r--r--app/assets/javascripts/pages/projects/compare/index.js2
-rw-r--r--app/assets/javascripts/pages/projects/compare/show/index.js2
-rw-r--r--app/assets/javascripts/pages/projects/merge_requests/creations/new/compare.js60
-rw-r--r--app/assets/javascripts/pages/projects/merge_requests/creations/new/index.js11
-rw-r--r--app/assets/javascripts/pages/projects/merge_requests/creations/new/target_project_dropdown.js22
-rw-r--r--app/assets/javascripts/pages/projects/pipelines/new/index.js6
-rw-r--r--app/assets/javascripts/pipelines/components/graph/action_component.vue2
-rw-r--r--app/assets/javascripts/pipelines/components/graph/dropdown_job_component.vue6
-rw-r--r--app/assets/javascripts/pipelines/components/graph/job_component.vue2
-rw-r--r--app/assets/javascripts/pipelines/components/stage.vue244
-rw-r--r--app/assets/javascripts/pipelines/pipeline_details_bundle.js4
-rw-r--r--app/assets/javascripts/projects_dropdown/components/app.vue5
-rw-r--r--app/assets/javascripts/projects_dropdown/service/projects_service.js14
-rw-r--r--app/assets/javascripts/shortcuts.js1
-rw-r--r--app/assets/javascripts/sidebar/components/participants/participants.vue4
-rw-r--r--app/assets/javascripts/sidebar/components/subscriptions/sidebar_subscriptions.vue8
-rw-r--r--app/assets/javascripts/sidebar/components/subscriptions/subscriptions.vue22
-rw-r--r--app/assets/javascripts/sidebar/components/time_tracking/no_tracking_pane.js10
-rw-r--r--app/assets/javascripts/sidebar/components/time_tracking/no_tracking_pane.vue13
-rw-r--r--app/assets/javascripts/sidebar/components/time_tracking/sidebar_time_tracking.vue (renamed from app/assets/javascripts/sidebar/components/time_tracking/sidebar_time_tracking.js)35
-rw-r--r--app/assets/javascripts/sidebar/components/time_tracking/spent_only_pane.js15
-rw-r--r--app/assets/javascripts/sidebar/components/time_tracking/spent_only_pane.vue18
-rw-r--r--app/assets/javascripts/sidebar/components/time_tracking/time_tracker.vue8
-rw-r--r--app/assets/javascripts/sidebar/mount_sidebar.js2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.vue14
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/work_in_progress.vue (renamed from app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_wip.js)79
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/dependencies.js2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/mr_widget_options.js4
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js2
-rw-r--r--app/assets/javascripts/vue_shared/components/icon.vue5
-rw-r--r--app/assets/javascripts/vue_shared/components/navigation_tabs.vue88
-rw-r--r--app/assets/stylesheets/emoji_sprites.scss5403
-rw-r--r--app/assets/stylesheets/framework.scss126
-rw-r--r--app/assets/stylesheets/framework/blocks.scss4
-rw-r--r--app/assets/stylesheets/framework/buttons.scss8
-rw-r--r--app/assets/stylesheets/framework/common.scss1
-rw-r--r--app/assets/stylesheets/framework/emoji_sprites.scss1813
-rw-r--r--app/assets/stylesheets/framework/gitlab_theme.scss30
-rw-r--r--app/assets/stylesheets/framework/images.scss35
-rw-r--r--app/assets/stylesheets/framework/markdown_area.scss10
-rw-r--r--app/assets/stylesheets/framework/terms.scss59
-rw-r--r--app/assets/stylesheets/framework/variables.scss20
-rw-r--r--app/assets/stylesheets/pages/boards.scss2
-rw-r--r--app/assets/stylesheets/pages/clusters.scss48
-rw-r--r--app/assets/stylesheets/pages/commits.scss26
-rw-r--r--app/assets/stylesheets/pages/diff.scss6
-rw-r--r--app/assets/stylesheets/pages/environments.scss37
-rw-r--r--app/assets/stylesheets/pages/merge_requests.scss4
-rw-r--r--app/assets/stylesheets/pages/pipelines.scss192
-rw-r--r--app/assets/stylesheets/pages/projects.scss53
-rw-r--r--app/assets/stylesheets/pages/repo.scss422
-rw-r--r--app/assets/stylesheets/pages/repo.scss.orig786
-rw-r--r--app/controllers/application_controller.rb36
-rw-r--r--app/controllers/concerns/continue_params.rb4
-rw-r--r--app/controllers/concerns/internal_redirect.rb35
-rw-r--r--app/controllers/concerns/issuable_actions.rb1
-rw-r--r--app/controllers/concerns/issuable_collections.rb2
-rw-r--r--app/controllers/groups/application_controller.rb2
-rw-r--r--app/controllers/groups/runners_controller.rb58
-rw-r--r--app/controllers/import/base_controller.rb11
-rw-r--r--app/controllers/import/bitbucket_controller.rb6
-rw-r--r--app/controllers/import/fogbugz_controller.rb5
-rw-r--r--app/controllers/import/github_controller.rb5
-rw-r--r--app/controllers/import/gitlab_controller.rb5
-rw-r--r--app/controllers/import/google_code_controller.rb5
-rw-r--r--app/controllers/omniauth_callbacks_controller.rb2
-rw-r--r--app/controllers/profiles/active_sessions_controller.rb14
-rw-r--r--app/controllers/projects/application_controller.rb2
-rw-r--r--app/controllers/projects/compare_controller.rb65
-rw-r--r--app/controllers/projects/lfs_storage_controller.rb3
-rw-r--r--app/controllers/projects/merge_requests/creations_controller.rb7
-rw-r--r--app/controllers/projects/mirrors_controller.rb67
-rw-r--r--app/controllers/projects/pipelines_controller.rb19
-rw-r--r--app/controllers/projects/runner_projects_controller.rb4
-rw-r--r--app/controllers/projects/runners_controller.rb23
-rw-r--r--app/controllers/projects/settings/ci_cd_controller.rb12
-rw-r--r--app/controllers/projects/settings/repository_controller.rb6
-rw-r--r--app/controllers/sent_notifications_controller.rb20
-rw-r--r--app/controllers/sessions_controller.rb9
-rw-r--r--app/controllers/users/terms_controller.rb66
-rw-r--r--app/helpers/active_sessions_helper.rb23
-rw-r--r--app/helpers/application_helper.rb13
-rw-r--r--app/helpers/application_settings_helper.rb5
-rw-r--r--app/helpers/clusters_helper.rb8
-rw-r--r--app/helpers/gitlab_routing_helper.rb8
-rw-r--r--app/helpers/projects_helper.rb2
-rw-r--r--app/helpers/user_callouts_helper.rb5
-rw-r--r--app/helpers/users_helper.rb33
-rw-r--r--app/helpers/webpack_helper.rb45
-rw-r--r--app/mailers/emails/notes.rb5
-rw-r--r--app/mailers/notify.rb13
-rw-r--r--app/models/ability.rb8
-rw-r--r--app/models/active_session.rb110
-rw-r--r--app/models/application_setting.rb22
-rw-r--r--app/models/application_setting/term.rb13
-rw-r--r--app/models/ci/build.rb1
-rw-r--r--app/models/ci/build_trace_chunk.rb180
-rw-r--r--app/models/ci/job_artifact.rb2
-rw-r--r--app/models/ci/pipeline.rb44
-rw-r--r--app/models/ci/pipeline_variable.rb2
-rw-r--r--app/models/ci/runner.rb70
-rw-r--r--app/models/ci/runner_namespace.rb9
-rw-r--r--app/models/ci/stage.rb21
-rw-r--r--app/models/commit.rb6
-rw-r--r--app/models/commit_status.rb7
-rw-r--r--app/models/concerns/fast_destroy_all.rb91
-rw-r--r--app/models/concerns/participable.rb4
-rw-r--r--app/models/concerns/reactive_caching.rb17
-rw-r--r--app/models/concerns/sha_attribute.rb29
-rw-r--r--app/models/group.rb41
-rw-r--r--app/models/identity.rb8
-rw-r--r--app/models/lfs_object.rb2
-rw-r--r--app/models/merge_request.rb6
-rw-r--r--app/models/namespace.rb10
-rw-r--r--app/models/note.rb4
-rw-r--r--app/models/project.rb231
-rw-r--r--app/models/project_ci_cd_setting.rb2
-rw-r--r--app/models/project_import_state.rb55
-rw-r--r--app/models/remote_mirror.rb219
-rw-r--r--app/models/repository.rb16
-rw-r--r--app/models/sent_notification.rb4
-rw-r--r--app/models/system_note_metadata.rb6
-rw-r--r--app/models/term_agreement.rb6
-rw-r--r--app/models/user.rb8
-rw-r--r--app/models/user_callout.rb3
-rw-r--r--app/policies/application_setting/term_policy.rb28
-rw-r--r--app/policies/project_policy.rb7
-rw-r--r--app/policies/user_policy.rb6
-rw-r--r--app/presenters/ci/pipeline_presenter.rb10
-rw-r--r--app/serializers/merge_request_widget_entity.rb7
-rw-r--r--app/serializers/project_mirror_entity.rb11
-rw-r--r--app/serializers/stage_entity.rb16
-rw-r--r--app/serializers/stage_serializer.rb7
-rw-r--r--app/services/application_settings/update_service.rb15
-rw-r--r--app/services/ci/create_pipeline_service.rb1
-rw-r--r--app/services/ci/ensure_stage_service.rb1
-rw-r--r--app/services/ci/register_job_service.rb29
-rw-r--r--app/services/ci/update_build_queue_service.rb16
-rw-r--r--app/services/concerns/exclusive_lease_guard.rb52
-rw-r--r--app/services/concerns/users/participable_service.rb41
-rw-r--r--app/services/git_push_service.rb8
-rw-r--r--app/services/merge_requests/merge_service.rb29
-rw-r--r--app/services/notification_recipient_service.rb35
-rw-r--r--app/services/projects/create_service.rb2
-rw-r--r--app/services/projects/participants_service.rb32
-rw-r--r--app/services/projects/update_remote_mirror_service.rb30
-rw-r--r--app/services/users/respond_to_terms_service.rb24
-rw-r--r--app/services/web_hook_service.rb2
-rw-r--r--app/views/admin/application_settings/_repository_check.html.haml2
-rw-r--r--app/views/admin/application_settings/_repository_mirrors_form.html.haml16
-rw-r--r--app/views/admin/application_settings/_terms.html.haml22
-rw-r--r--app/views/admin/application_settings/show.html.haml122
-rw-r--r--app/views/admin/projects/show.html.haml2
-rw-r--r--app/views/admin/runners/_runner.html.haml4
-rw-r--r--app/views/admin/runners/index.html.haml3
-rw-r--r--app/views/admin/runners/show.html.haml7
-rw-r--r--app/views/admin/services/index.html.haml3
-rw-r--r--app/views/admin/users/index.html.haml14
-rw-r--r--app/views/ci/status/_dropdown_graph_badge.html.haml2
-rw-r--r--app/views/devise/mailer/unlock_instructions.html.haml2
-rw-r--r--app/views/devise/mailer/unlock_instructions.text.erb2
-rw-r--r--app/views/groups/_group_admin_settings.html.haml52
-rw-r--r--app/views/groups/runners/_group_runners.html.haml24
-rw-r--r--app/views/groups/runners/_index.html.haml9
-rw-r--r--app/views/groups/runners/_runner.html.haml27
-rw-r--r--app/views/groups/runners/edit.html.haml6
-rw-r--r--app/views/groups/settings/ci_cd/show.html.haml28
-rw-r--r--app/views/help/_shortcuts.html.haml2
-rw-r--r--app/views/help/ui.html.haml7
-rw-r--r--app/views/ide/index.html.haml3
-rw-r--r--app/views/layouts/_flash.html.haml4
-rw-r--r--app/views/layouts/_head.html.haml3
-rw-r--r--app/views/layouts/_init_auto_complete.html.haml15
-rw-r--r--app/views/layouts/header/_current_user_dropdown.html.haml22
-rw-r--r--app/views/layouts/header/_default.html.haml19
-rw-r--r--app/views/layouts/nav/sidebar/_profile.html.haml11
-rw-r--r--app/views/layouts/terms.html.haml34
-rw-r--r--app/views/peek/_bar.html.haml2
-rw-r--r--app/views/profiles/active_sessions/_active_session.html.haml31
-rw-r--r--app/views/profiles/active_sessions/index.html.haml14
-rw-r--r--app/views/projects/_import_project_pane.html.haml51
-rw-r--r--app/views/projects/clusters/_gcp_signup_offer_banner.html.haml12
-rw-r--r--app/views/projects/clusters/gcp/login.html.haml2
-rw-r--r--app/views/projects/clusters/index.html.haml2
-rw-r--r--app/views/projects/clusters/new.html.haml2
-rw-r--r--app/views/projects/commit/_commit_box.html.haml2
-rw-r--r--app/views/projects/commits/_commit.html.haml8
-rw-r--r--app/views/projects/commits/show.html.haml2
-rw-r--r--app/views/projects/compare/_form.html.haml2
-rw-r--r--app/views/projects/deploy_keys/_index.html.haml2
-rw-r--r--app/views/projects/diffs/_diffs.html.haml2
-rw-r--r--app/views/projects/jobs/_sidebar.html.haml2
-rw-r--r--app/views/projects/merge_requests/creations/_new_compare.html.haml40
-rw-r--r--app/views/projects/merge_requests/dropdowns/_project.html.haml2
-rw-r--r--app/views/projects/mirrors/_instructions.html.haml10
-rw-r--r--app/views/projects/mirrors/_push.html.haml50
-rw-r--r--app/views/projects/mirrors/_show.html.haml3
-rw-r--r--app/views/projects/new.html.haml53
-rw-r--r--app/views/projects/pipelines/_info.html.haml2
-rw-r--r--app/views/projects/pipelines/_with_tabs.html.haml11
-rw-r--r--app/views/projects/pipelines/new.html.haml18
-rw-r--r--app/views/projects/registry/repositories/_tag.html.haml2
-rw-r--r--app/views/projects/runners/_group_runners.html.haml37
-rw-r--r--app/views/projects/runners/_index.html.haml18
-rw-r--r--app/views/projects/runners/_runner.html.haml16
-rw-r--r--app/views/projects/runners/_shared_runners.html.haml4
-rw-r--r--app/views/projects/runners/edit.html.haml2
-rw-r--r--app/views/projects/services/_index.html.haml3
-rw-r--r--app/views/projects/settings/repository/show.html.haml2
-rw-r--r--app/views/projects/triggers/_trigger.html.haml2
-rw-r--r--app/views/shared/_mini_pipeline_graph.html.haml3
-rw-r--r--app/views/shared/_remote_mirror_update_button.html.haml13
-rw-r--r--app/views/shared/boards/components/_board.html.haml3
-rw-r--r--app/views/shared/runners/_form.html.haml (renamed from app/views/projects/runners/_form.html.haml)13
-rw-r--r--app/views/shared/runners/_runner_description.html.haml16
-rw-r--r--app/views/shared/runners/show.html.haml (renamed from app/views/projects/runners/show.html.haml)10
-rw-r--r--app/views/sherlock/transactions/_general.html.haml3
-rw-r--r--app/views/sherlock/transactions/index.html.haml3
-rw-r--r--app/views/users/show.html.haml10
-rw-r--r--app/views/users/terms/index.html.haml13
-rw-r--r--app/workers/admin_email_worker.rb6
-rw-r--r--app/workers/all_queues.yml3
-rw-r--r--app/workers/ci/build_trace_chunk_flush_worker.rb12
-rw-r--r--app/workers/gitlab/github_import/advance_stage_worker.rb9
-rw-r--r--app/workers/gitlab/github_import/refresh_import_jid_worker.rb5
-rw-r--r--app/workers/new_note_worker.rb2
-rw-r--r--app/workers/repository_check/batch_worker.rb35
-rw-r--r--app/workers/repository_check/single_repository_worker.rb48
-rw-r--r--app/workers/repository_remove_remote_worker.rb35
-rw-r--r--app/workers/repository_update_remote_mirror_worker.rb49
-rw-r--r--app/workers/stuck_import_jobs_worker.rb9
312 files changed, 11414 insertions, 5295 deletions
diff --git a/app/assets/javascripts/behaviors/gl_emoji.js b/app/assets/javascripts/behaviors/gl_emoji.js
index 7e98e04303a..56293d5f96f 100644
--- a/app/assets/javascripts/behaviors/gl_emoji.js
+++ b/app/assets/javascripts/behaviors/gl_emoji.js
@@ -7,27 +7,24 @@ export default function installGlEmojiElement() {
const GlEmojiElementProto = Object.create(HTMLElement.prototype);
GlEmojiElementProto.createdCallback = function createdCallback() {
const emojiUnicode = this.textContent.trim();
- const {
- name,
- unicodeVersion,
- fallbackSrc,
- fallbackSpriteClass,
- } = this.dataset;
+ const { name, unicodeVersion, fallbackSrc, fallbackSpriteClass } = this.dataset;
- const isEmojiUnicode = this.childNodes && Array.prototype.every.call(
- this.childNodes,
- childNode => childNode.nodeType === 3,
- );
+ const isEmojiUnicode =
+ this.childNodes &&
+ Array.prototype.every.call(this.childNodes, childNode => childNode.nodeType === 3);
const hasImageFallback = fallbackSrc && fallbackSrc.length > 0;
const hasCssSpriteFalback = fallbackSpriteClass && fallbackSpriteClass.length > 0;
- if (
- emojiUnicode &&
- isEmojiUnicode &&
- !isEmojiUnicodeSupported(emojiUnicode, unicodeVersion)
- ) {
+ if (emojiUnicode && isEmojiUnicode && !isEmojiUnicodeSupported(emojiUnicode, unicodeVersion)) {
// CSS sprite fallback takes precedence over image fallback
if (hasCssSpriteFalback) {
+ if (!gon.emoji_sprites_css_added && gon.emoji_sprites_css_path) {
+ const emojiSpriteLinkTag = document.createElement('link');
+ emojiSpriteLinkTag.setAttribute('rel', 'stylesheet');
+ emojiSpriteLinkTag.setAttribute('href', gon.emoji_sprites_css_path);
+ document.head.appendChild(emojiSpriteLinkTag);
+ gon.emoji_sprites_css_added = true;
+ }
// IE 11 doesn't like adding multiple at once :(
this.classList.add('emoji-icon');
this.classList.add(fallbackSpriteClass);
diff --git a/app/assets/javascripts/clusters/clusters_index.js b/app/assets/javascripts/clusters/clusters_index.js
index 2e3ad244375..1e5c733d151 100644
--- a/app/assets/javascripts/clusters/clusters_index.js
+++ b/app/assets/javascripts/clusters/clusters_index.js
@@ -1,20 +1,24 @@
-import Flash from '../flash';
-import { s__ } from '../locale';
-import setupToggleButtons from '../toggle_buttons';
+import createFlash from '~/flash';
+import { __ } from '~/locale';
+import setupToggleButtons from '~/toggle_buttons';
+import gcpSignupOffer from '~/clusters/components/gcp_signup_offer';
+
import ClustersService from './services/clusters_service';
export default () => {
const clusterList = document.querySelector('.js-clusters-list');
+
+ gcpSignupOffer();
+
// The empty state won't have a clusterList
if (clusterList) {
- setupToggleButtons(
- document.querySelector('.js-clusters-list'),
- (value, toggle) =>
- ClustersService.updateCluster(toggle.dataset.endpoint, { cluster: { enabled: value } })
- .catch((err) => {
- Flash(s__('ClusterIntegration|Something went wrong on our end.'));
- throw err;
- }),
+ setupToggleButtons(document.querySelector('.js-clusters-list'), (value, toggle) =>
+ ClustersService.updateCluster(toggle.dataset.endpoint, { cluster: { enabled: value } }).catch(
+ err => {
+ createFlash(__('Something went wrong on our end.'));
+ throw err;
+ },
+ ),
);
}
};
diff --git a/app/assets/javascripts/clusters/components/gcp_signup_offer.js b/app/assets/javascripts/clusters/components/gcp_signup_offer.js
new file mode 100644
index 00000000000..8bc20a1c09f
--- /dev/null
+++ b/app/assets/javascripts/clusters/components/gcp_signup_offer.js
@@ -0,0 +1,27 @@
+import $ from 'jquery';
+import axios from '~/lib/utils/axios_utils';
+import { __ } from '~/locale';
+import Flash from '~/flash';
+
+export default function gcpSignupOffer() {
+ const alertEl = document.querySelector('.gcp-signup-offer');
+ if (!alertEl) {
+ return;
+ }
+
+ const closeButtonEl = alertEl.getElementsByClassName('close')[0];
+ const { dismissEndpoint, featureId } = closeButtonEl.dataset;
+
+ closeButtonEl.addEventListener('click', () => {
+ axios
+ .post(dismissEndpoint, {
+ feature_name: featureId,
+ })
+ .then(() => {
+ $(alertEl).alert('close');
+ })
+ .catch(() => {
+ Flash(__('An error occurred while dismissing the alert. Refresh the page and try again.'));
+ });
+ });
+}
diff --git a/app/assets/javascripts/compare.js b/app/assets/javascripts/compare.js
deleted file mode 100644
index 303a5bf4a53..00000000000
--- a/app/assets/javascripts/compare.js
+++ /dev/null
@@ -1,86 +0,0 @@
-/* eslint-disable func-names, space-before-function-paren, wrap-iife, quotes, no-var, object-shorthand, consistent-return, no-unused-vars, comma-dangle, vars-on-top, prefer-template, max-len */
-
-import $ from 'jquery';
-import { localTimeAgo } from './lib/utils/datetime_utility';
-import axios from './lib/utils/axios_utils';
-
-export default class Compare {
- constructor(opts) {
- this.opts = opts;
- this.source_loading = $(".js-source-loading");
- this.target_loading = $(".js-target-loading");
- $('.js-compare-dropdown').each((function(_this) {
- return function(i, dropdown) {
- var $dropdown;
- $dropdown = $(dropdown);
- return $dropdown.glDropdown({
- selectable: true,
- fieldName: $dropdown.data('fieldName'),
- filterable: true,
- id: function(obj, $el) {
- return $el.data('id');
- },
- toggleLabel: function(obj, $el) {
- return $el.text().trim();
- },
- clicked: function(e, el) {
- if ($dropdown.is('.js-target-branch')) {
- return _this.getTargetHtml();
- } else if ($dropdown.is('.js-source-branch')) {
- return _this.getSourceHtml();
- } else if ($dropdown.is('.js-target-project')) {
- return _this.getTargetProject();
- }
- }
- });
- };
- })(this));
- this.initialState();
- }
-
- initialState() {
- this.getSourceHtml();
- this.getTargetHtml();
- }
-
- getTargetProject() {
- $('.mr_target_commit').empty();
-
- return axios.get(this.opts.targetProjectUrl, {
- params: {
- target_project_id: $("input[name='merge_request[target_project_id]']").val(),
- },
- }).then(({ data }) => {
- $('.js-target-branch-dropdown .dropdown-content').html(data);
- });
- }
-
- getSourceHtml() {
- return this.constructor.sendAjax(this.opts.sourceBranchUrl, this.source_loading, '.mr_source_commit', {
- ref: $("input[name='merge_request[source_branch]']").val()
- });
- }
-
- getTargetHtml() {
- return this.constructor.sendAjax(this.opts.targetBranchUrl, this.target_loading, '.mr_target_commit', {
- target_project_id: $("input[name='merge_request[target_project_id]']").val(),
- ref: $("input[name='merge_request[target_branch]']").val()
- });
- }
-
- static sendAjax(url, loading, target, params) {
- const $target = $(target);
-
- loading.show();
- $target.empty();
-
- return axios.get(url, {
- params,
- }).then(({ data }) => {
- loading.hide();
- $target.html(data);
- const className = '.' + $target[0].className.replace(' ', '.');
- localTimeAgo($('.js-timeago', className));
- });
- }
-}
diff --git a/app/assets/javascripts/compare_autocomplete.js b/app/assets/javascripts/compare_autocomplete.js
index 3b083496142..a4e84da1eda 100644
--- a/app/assets/javascripts/compare_autocomplete.js
+++ b/app/assets/javascripts/compare_autocomplete.js
@@ -4,8 +4,9 @@ import $ from 'jquery';
import { __ } from './locale';
import axios from './lib/utils/axios_utils';
import flash from './flash';
+import { capitalizeFirstCharacter } from './lib/utils/text_utility';
-export default function initCompareAutocomplete() {
+export default function initCompareAutocomplete(limitTo = null, clickHandler = () => {}) {
$('.js-compare-dropdown').each(function() {
var $dropdown, selected;
$dropdown = $(this);
@@ -15,14 +16,27 @@ export default function initCompareAutocomplete() {
const $filterInput = $('input[type="search"]', $dropdownContainer);
$dropdown.glDropdown({
data: function(term, callback) {
- axios.get($dropdown.data('refsUrl'), {
- params: {
- ref: $dropdown.data('ref'),
- search: term,
- },
- }).then(({ data }) => {
- callback(data);
- }).catch(() => flash(__('Error fetching refs')));
+ const params = {
+ ref: $dropdown.data('ref'),
+ search: term,
+ };
+
+ if (limitTo) {
+ params.find = limitTo;
+ }
+
+ axios
+ .get($dropdown.data('refsUrl'), {
+ params,
+ })
+ .then(({ data }) => {
+ if (limitTo) {
+ callback(data[capitalizeFirstCharacter(limitTo)] || []);
+ } else {
+ callback(data);
+ }
+ })
+ .catch(() => flash(__('Error fetching refs')));
},
selectable: true,
filterable: true,
@@ -32,9 +46,15 @@ export default function initCompareAutocomplete() {
renderRow: function(ref) {
var link;
if (ref.header != null) {
- return $('<li />').addClass('dropdown-header').text(ref.header);
+ return $('<li />')
+ .addClass('dropdown-header')
+ .text(ref.header);
} else {
- link = $('<a />').attr('href', '#').addClass(ref === selected ? 'is-active' : '').text(ref).attr('data-ref', escape(ref));
+ link = $('<a />')
+ .attr('href', '#')
+ .addClass(ref === selected ? 'is-active' : '')
+ .text(ref)
+ .attr('data-ref', escape(ref));
return $('<li />').append(link);
}
},
@@ -43,9 +63,10 @@ export default function initCompareAutocomplete() {
},
toggleLabel: function(obj, $el) {
return $el.text().trim();
- }
+ },
+ clicked: () => clickHandler($dropdown),
});
- $filterInput.on('keyup', (e) => {
+ $filterInput.on('keyup', e => {
const keyCode = e.keyCode || e.which;
if (keyCode !== 13) return;
const text = $filterInput.val();
@@ -54,7 +75,7 @@ export default function initCompareAutocomplete() {
$dropdownContainer.removeClass('open');
});
- $dropdownContainer.on('click', '.dropdown-content a', (e) => {
+ $dropdownContainer.on('click', '.dropdown-content a', e => {
$dropdown.prop('title', e.target.text.replace(/_+?/g, '-'));
if ($dropdown.hasClass('has-tooltip')) {
$dropdown.tooltip('_fixTitle');
diff --git a/app/assets/javascripts/deploy_keys/components/action_btn.vue b/app/assets/javascripts/deploy_keys/components/action_btn.vue
index aff31062906..bdb0cc4efb7 100644
--- a/app/assets/javascripts/deploy_keys/components/action_btn.vue
+++ b/app/assets/javascripts/deploy_keys/components/action_btn.vue
@@ -1,55 +1,46 @@
<script>
- import eventHub from '../eventhub';
- import loadingIcon from '../../vue_shared/components/loading_icon.vue';
+import loadingIcon from '~/vue_shared/components/loading_icon.vue';
+import eventHub from '../eventhub';
- export default {
- components: {
- loadingIcon,
+export default {
+ components: {
+ loadingIcon,
+ },
+ props: {
+ deployKey: {
+ type: Object,
+ required: true,
},
- props: {
- deployKey: {
- type: Object,
- required: true,
- },
- type: {
- type: String,
- required: true,
- },
- btnCssClass: {
- type: String,
- required: false,
- default: 'btn-secondary',
- },
+ btnCssClass: {
+ type: String,
+ required: false,
+ default: 'btn-secondary',
},
- data() {
- return {
- isLoading: false,
- };
- },
- computed: {
- text() {
- return `${this.type.charAt(0).toUpperCase()}${this.type.slice(1)}`;
- },
- },
- methods: {
- doAction() {
- this.isLoading = true;
+ },
+ data() {
+ return {
+ isLoading: false,
+ };
+ },
+ methods: {
+ doAction() {
+ this.isLoading = true;
- eventHub.$emit(`${this.type}.key`, this.deployKey, () => {
- this.isLoading = false;
- });
- },
+ eventHub.$emit(`${this.type}.key`, this.deployKey, () => {
+ this.isLoading = false;
+ });
},
- };
+ },
+};
</script>
<template>
<button
- class="btn btn-sm prepend-left-10"
+ class="btn"
:class="[{ disabled: isLoading }, btnCssClass]"
:disabled="isLoading"
@click="doAction">
- {{ text }}
+ <slot></slot>
<loading-icon
v-if="isLoading"
:inline="true"
diff --git a/app/assets/javascripts/deploy_keys/components/app.vue b/app/assets/javascripts/deploy_keys/components/app.vue
index 5a782237b7d..c41fe55db63 100644
--- a/app/assets/javascripts/deploy_keys/components/app.vue
+++ b/app/assets/javascripts/deploy_keys/components/app.vue
@@ -1,80 +1,115 @@
<script>
- import Flash from '../../flash';
- import eventHub from '../eventhub';
- import DeployKeysService from '../service';
- import DeployKeysStore from '../store';
- import keysPanel from './keys_panel.vue';
- import loadingIcon from '../../vue_shared/components/loading_icon.vue';
+import { s__ } from '~/locale';
+import Flash from '~/flash';
+import LoadingIcon from '~/vue_shared/components/loading_icon.vue';
+import NavigationTabs from '~/vue_shared/components/navigation_tabs.vue';
+import eventHub from '../eventhub';
+import DeployKeysService from '../service';
+import DeployKeysStore from '../store';
+import KeysPanel from './keys_panel.vue';
- export default {
- components: {
- keysPanel,
- loadingIcon,
+export default {
+ components: {
+ KeysPanel,
+ LoadingIcon,
+ NavigationTabs,
+ },
+ props: {
+ endpoint: {
+ type: String,
+ required: true,
},
- props: {
- endpoint: {
- type: String,
- required: true,
- },
+ projectId: {
+ type: String,
+ required: true,
},
- data() {
- return {
- isLoading: false,
- store: new DeployKeysStore(),
- };
+ },
+ data() {
+ return {
+ currentTab: 'enabled_keys',
+ isLoading: false,
+ store: new DeployKeysStore(),
+ };
+ },
+ scopes: {
+ enabled_keys: s__('DeployKeys|Enabled deploy keys'),
+ available_project_keys: s__('DeployKeys|Privately accessible deploy keys'),
+ public_keys: s__('DeployKeys|Publicly accessible deploy keys'),
+ },
+ computed: {
+ tabs() {
+ return Object.keys(this.$options.scopes).map(scope => {
+ const count = Array.isArray(this.keys[scope]) ? this.keys[scope].length : null;
+
+ return {
+ name: this.$options.scopes[scope],
+ scope,
+ isActive: scope === this.currentTab,
+ count,
+ };
+ });
+ },
+ hasKeys() {
+ return Object.keys(this.keys).length;
},
- computed: {
- hasKeys() {
- return Object.keys(this.keys).length;
- },
- keys() {
- return this.store.keys;
- },
+ keys() {
+ return this.store.keys;
},
- created() {
- this.service = new DeployKeysService(this.endpoint);
+ },
+ created() {
+ this.service = new DeployKeysService(this.endpoint);
- eventHub.$on('enable.key', this.enableKey);
- eventHub.$on('remove.key', this.disableKey);
- eventHub.$on('disable.key', this.disableKey);
+ eventHub.$on('enable.key', this.enableKey);
+ eventHub.$on('remove.key', this.disableKey);
+ eventHub.$on('disable.key', this.disableKey);
+ },
+ mounted() {
+ this.fetchKeys();
+ },
+ beforeDestroy() {
+ eventHub.$off('enable.key', this.enableKey);
+ eventHub.$off('remove.key', this.disableKey);
+ eventHub.$off('disable.key', this.disableKey);
+ },
+ methods: {
+ onChangeTab(tab) {
+ this.currentTab = tab;
},
- mounted() {
- this.fetchKeys();
+ fetchKeys() {
+ this.isLoading = true;
+
+ return this.service
+ .getKeys()
+ .then(data => {
+ this.isLoading = false;
+ this.store.keys = data;
+ })
+ .catch(() => {
+ this.isLoading = false;
+ this.store.keys = {};
+ return new Flash(s__('DeployKeys|Error getting deploy keys'));
+ });
},
- beforeDestroy() {
- eventHub.$off('enable.key', this.enableKey);
- eventHub.$off('remove.key', this.disableKey);
- eventHub.$off('disable.key', this.disableKey);
+ enableKey(deployKey) {
+ this.service
+ .enableKey(deployKey.id)
+ .then(this.fetchKeys)
+ .catch(() => new Flash(s__('DeployKeys|Error enabling deploy key')));
},
- methods: {
- fetchKeys() {
- this.isLoading = true;
-
- this.service.getKeys()
- .then((data) => {
- this.isLoading = false;
- this.store.keys = data;
- })
- .catch(() => new Flash('Error getting deploy keys'));
- },
- enableKey(deployKey) {
- this.service.enableKey(deployKey.id)
- .then(() => this.fetchKeys())
- .catch(() => new Flash('Error enabling deploy key'));
- },
- disableKey(deployKey, callback) {
- // eslint-disable-next-line no-alert
- if (confirm('You are going to remove this deploy key. Are you sure?')) {
- this.service.disableKey(deployKey.id)
- .then(() => this.fetchKeys())
- .then(callback)
- .catch(() => new Flash('Error removing deploy key'));
- } else {
- callback();
- }
- },
+ disableKey(deployKey, callback) {
+ // eslint-disable-next-line no-alert
+ if (confirm(s__('DeployKeys|You are going to remove this deploy key. Are you sure?'))) {
+ this.service
+ .disableKey(deployKey.id)
+ .then(this.fetchKeys)
+ .then(callback)
+ .catch(() => new Flash(s__('DeployKeys|Error removing deploy key')));
+ } else {
+ callback();
+ }
},
- };
+ },
+};
</script>
<template>
@@ -82,29 +117,38 @@
<loading-icon
v-if="isLoading && !hasKeys"
size="2"
- label="Loading deploy keys"
+ :label="s__('DeployKeys|Loading deploy keys')"
/>
- <div v-else-if="hasKeys">
+ <template v-else-if="hasKeys">
+ <div class="top-area scrolling-tabs-container inner-page-scroll-tabs">
+ <div class="fade-left">
+ <i
+ class="fa fa-angle-left"
+ aria-hidden="true"
+ >
+ </i>
+ </div>
+ <div class="fade-right">
+ <i
+ class="fa fa-angle-right"
+ aria-hidden="true"
+ >
+ </i>
+ </div>
+
+ <navigation-tabs
+ :tabs="tabs"
+ @onChangeTab="onChangeTab"
+ scope="deployKeys"
+ />
+ </div>
<keys-panel
- title="Enabled deploy keys for this project"
class="qa-project-deploy-keys"
- :keys="keys.enabled_keys"
- :store="store"
- :endpoint="endpoint"
- />
- <keys-panel
- title="Deploy keys from projects you have access to"
- :keys="keys.available_project_keys"
- :store="store"
- :endpoint="endpoint"
- />
- <keys-panel
- v-if="keys.public_keys.length"
- title="Public deploy keys available to any project"
- :keys="keys.public_keys"
+ :project-id="projectId"
+ :keys="keys[currentTab]"
:store="store"
:endpoint="endpoint"
/>
- </div>
+ </template>
</div>
</template>
diff --git a/app/assets/javascripts/deploy_keys/components/key.vue b/app/assets/javascripts/deploy_keys/components/key.vue
index 2a777ff2de2..6c2af7fa768 100644
--- a/app/assets/javascripts/deploy_keys/components/key.vue
+++ b/app/assets/javascripts/deploy_keys/components/key.vue
@@ -1,111 +1,235 @@
<script>
- import actionBtn from './action_btn.vue';
- import { getTimeago } from '../../lib/utils/datetime_utility';
- import tooltip from '../../vue_shared/directives/tooltip';
+import _ from 'underscore';
+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';
- export default {
- components: {
- actionBtn,
- },
- directives: {
- tooltip,
- },
- props: {
- deployKey: {
- type: Object,
- required: true,
- },
- store: {
- type: Object,
- required: true,
- },
- endpoint: {
- type: String,
- required: true,
- },
- },
- computed: {
- timeagoDate() {
- return getTimeago().format(this.deployKey.created_at);
- },
- editDeployKeyPath() {
- return `${this.endpoint}/${this.deployKey.id}/edit`;
- },
- },
- methods: {
- isEnabled(id) {
- return this.store.findEnabledKey(id) !== undefined;
- },
- tooltipTitle(project) {
- return project.can_push ? 'Write access allowed' : 'Read access only';
- },
- },
- };
+import actionBtn from './action_btn.vue';
+
+export default {
+ components: {
+ actionBtn,
+ icon,
+ },
+ directives: {
+ tooltip,
+ },
+ mixins: [timeagoMixin],
+ props: {
+ deployKey: {
+ type: Object,
+ required: true,
+ },
+ store: {
+ type: Object,
+ required: true,
+ },
+ endpoint: {
+ type: String,
+ required: true,
+ },
+ projectId: {
+ type: String,
+ required: false,
+ default: null,
+ },
+ },
+ data() {
+ return {
+ projectsExpanded: false,
+ };
+ },
+ computed: {
+ editDeployKeyPath() {
+ return `${this.endpoint}/${this.deployKey.id}/edit`;
+ },
+ projects() {
+ const projects = [...this.deployKey.deploy_keys_projects];
+
+ if (this.projectId !== null) {
+ const indexOfCurrentProject = _.findIndex(
+ projects,
+ project =>
+ project &&
+ project.project &&
+ project.project.id &&
+ project.project.id.toString() === this.projectId,
+ );
+
+ if (indexOfCurrentProject > -1) {
+ const currentProject = projects.splice(indexOfCurrentProject, 1);
+ currentProject[0].project.full_name = s__('DeployKeys|Current project');
+ return currentProject.concat(projects);
+ }
+ }
+ return projects;
+ },
+ firstProject() {
+ return _.head(this.projects);
+ },
+ restProjects() {
+ return _.tail(this.projects);
+ },
+ restProjectsTooltip() {
+ return sprintf(s__('DeployKeys|Expand %{count} other projects'), {
+ count: this.restProjects.length,
+ });
+ },
+ restProjectsLabel() {
+ return sprintf(s__('DeployKeys|+%{count} others'), { count: this.restProjects.length });
+ },
+ isEnabled() {
+ return this.store.isEnabled(this.deployKey.id);
+ },
+ isRemovable() {
+ return (
+ this.store.isEnabled(this.deployKey.id) &&
+ this.deployKey.destroyed_when_orphaned &&
+ this.deployKey.almost_orphaned
+ );
+ },
+ isExpandable() {
+ return !this.projectsExpanded && this.restProjects.length > 1;
+ },
+ isExpanded() {
+ return this.projectsExpanded || this.restProjects.length === 1;
+ },
+ },
+ methods: {
+ projectTooltipTitle(project) {
+ return project.can_push
+ ? s__('DeployKeys|Write access allowed')
+ : s__('DeployKeys|Read access only');
+ },
+ toggleExpanded() {
+ this.projectsExpanded = !this.projectsExpanded;
+ },
+ },
+};
</script>
<template>
- <div>
- <div class="float-left append-right-10 d-none d-sm-block">
- <i
- aria-hidden="true"
- class="fa fa-key key-icon"
- >
- </i>
+ <div class="gl-responsive-table-row deploy-key">
+ <div class="table-section section-40">
+ <div
+ role="rowheader"
+ class="table-mobile-header">
+ {{ s__('DeployKeys|Deploy key') }}
+ </div>
+ <div class="table-mobile-content">
+ <strong class="title qa-key-title">
+ {{ deployKey.title }}
+ </strong>
+ <div class="fingerprint qa-key-fingerprint">
+ {{ deployKey.fingerprint }}
+ </div>
+ </div>
</div>
- <div class="deploy-key-content key-list-item-info">
- <strong class="title qa-key-title">
- {{ deployKey.title }}
- </strong>
- <div class="description qa-key-fingerprint">
- {{ deployKey.fingerprint }}
+ <div class="table-section section-30 section-wrap">
+ <div
+ role="rowheader"
+ class="table-mobile-header">
+ {{ s__('DeployKeys|Project usage') }}
+ </div>
+ <div class="table-mobile-content deploy-project-list">
+ <template v-if="projects.length > 0">
+ <a
+ class="label deploy-project-label"
+ :title="projectTooltipTitle(firstProject)"
+ v-tooltip
+ >
+ <span>
+ {{ firstProject.project.full_name }}
+ </span>
+ <icon :name="firstProject.can_push ? 'lock-open' : 'lock'"/>
+ </a>
+ <a
+ v-if="isExpandable"
+ class="label deploy-project-label"
+ @click="toggleExpanded"
+ :title="restProjectsTooltip"
+ v-tooltip
+ >
+ <span>{{ restProjectsLabel }}</span>
+ </a>
+ <a
+ v-else-if="isExpanded"
+ v-for="deployKeysProject in restProjects"
+ :key="deployKeysProject.project.full_path"
+ class="label deploy-project-label"
+ :href="deployKeysProject.project.full_path"
+ :title="projectTooltipTitle(deployKeysProject)"
+ v-tooltip
+ >
+ <span>
+ {{ deployKeysProject.project.full_name }}
+ </span>
+ <icon :name="deployKeysProject.can_push ? 'lock-open' : 'lock'"/>
+ </a>
+ </template>
+ <span
+ v-else
+ class="text-secondary">{{ __('None') }}</span>
</div>
</div>
- <div class="deploy-key-content prepend-left-default deploy-key-projects">
- <a
- v-for="(deployKeysProject, i) in deployKey.deploy_keys_projects"
- :key="i"
- class="label deploy-project-label"
- :href="deployKeysProject.project.full_path"
- :title="tooltipTitle(deployKeysProject)"
- v-tooltip
- >
- {{ deployKeysProject.project.full_name }}
- <i
- v-if="!deployKeysProject.can_push"
- aria-hidden="true"
- class="fa fa-lock"
- >
- </i>
- </a>
+ <div class="table-section section-15 text-right">
+ <div
+ role="rowheader"
+ class="table-mobile-header">
+ {{ __('Created') }}
+ </div>
+ <div class="table-mobile-content text-secondary key-created-at">
+ <span
+ :title="tooltipTitle(deployKey.created_at)"
+ v-tooltip>
+ <icon name="calendar"/>
+ <span>{{ timeFormated(deployKey.created_at) }}</span>
+ </span>
+ </div>
</div>
- <div class="deploy-key-content">
- <span class="key-created-at">
- created {{ timeagoDate }}
- </span>
- <a
- v-if="deployKey.can_edit"
- class="btn btn-sm"
- :href="editDeployKeyPath"
- >
- Edit
- </a>
- <action-btn
- v-if="!isEnabled(deployKey.id)"
- :deploy-key="deployKey"
- type="enable"
- />
- <action-btn
- v-else-if="deployKey.destroyed_when_orphaned && deployKey.almost_orphaned"
- :deploy-key="deployKey"
- btn-css-class="btn-warning"
- type="remove"
- />
- <action-btn
- v-else
- :deploy-key="deployKey"
- btn-css-class="btn-warning"
- type="disable"
- />
+ <div class="table-section section-15 table-button-footer deploy-key-actions">
+ <div class="btn-group table-action-buttons">
+ <action-btn
+ v-if="!isEnabled"
+ :deploy-key="deployKey"
+ type="enable"
+ >
+ {{ __('Enable') }}
+ </action-btn>
+ <a
+ v-if="deployKey.can_edit"
+ class="btn btn-default text-secondary"
+ :href="editDeployKeyPath"
+ :title="__('Edit')"
+ data-container="body"
+ v-tooltip
+ >
+ <icon name="pencil"/>
+ </a>
+ <action-btn
+ v-if="isRemovable"
+ :deploy-key="deployKey"
+ btn-css-class="btn-danger"
+ type="remove"
+ :title="__('Remove')"
+ data-container="body"
+ v-tooltip
+ >
+ <icon name="remove"/>
+ </action-btn>
+ <action-btn
+ v-else-if="isEnabled"
+ :deploy-key="deployKey"
+ btn-css-class="btn-warning"
+ type="disable"
+ :title="__('Disable')"
+ data-container="body"
+ v-tooltip
+ >
+ <icon name="cancel"/>
+ </action-btn>
+ </div>
</div>
</div>
</template>
diff --git a/app/assets/javascripts/deploy_keys/components/keys_panel.vue b/app/assets/javascripts/deploy_keys/components/keys_panel.vue
index 92e370cbd73..3b146c7389a 100644
--- a/app/assets/javascripts/deploy_keys/components/keys_panel.vue
+++ b/app/assets/javascripts/deploy_keys/components/keys_panel.vue
@@ -1,63 +1,68 @@
<script>
- import key from './key.vue';
+import deployKey from './key.vue';
- export default {
- components: {
- key,
+export default {
+ components: {
+ deployKey,
+ },
+ props: {
+ keys: {
+ type: Array,
+ required: true,
},
- props: {
- title: {
- type: String,
- required: true,
- },
- keys: {
- type: Array,
- required: true,
- },
- showHelpBox: {
- type: Boolean,
- required: false,
- default: true,
- },
- store: {
- type: Object,
- required: true,
- },
- endpoint: {
- type: String,
- required: true,
- },
+ store: {
+ type: Object,
+ required: true,
},
- };
+ endpoint: {
+ type: String,
+ required: true,
+ },
+ projectId: {
+ type: String,
+ required: false,
+ default: null,
+ },
+ },
+};
</script>
<template>
- <div class="deploy-keys-panel">
- <h5>
- {{ title }}
- ({{ keys.length }})
- </h5>
- <ul
- class="list-group"
- v-if="keys.length"
- >
- <li
- class="list-group-item border-0"
+ <div class="deploy-keys-panel table-holder">
+ <template v-if="keys.length > 0">
+ <div
+ role="row"
+ class="gl-responsive-table-row table-row-header">
+ <div
+ role="rowheader"
+ class="table-section section-40">
+ {{ s__('DeployKeys|Deploy key') }}
+ </div>
+ <div
+ role="rowheader"
+ class="table-section section-30">
+ {{ s__('DeployKeys|Project usage') }}
+ </div>
+ <div
+ role="rowheader"
+ class="table-section section-15 text-right">
+ {{ __('Created') }}
+ </div>
+ </div>
+ <deploy-key
v-for="deployKey in keys"
:key="deployKey.id"
- >
- <key
- :deploy-key="deployKey"
- :store="store"
- :endpoint="endpoint"
- />
- </li>
- </ul>
+ :deploy-key="deployKey"
+ :store="store"
+ :endpoint="endpoint"
+ :project-id="projectId"
+ />
+ </template>
<div
class="settings-message text-center"
- v-else-if="showHelpBox"
+ v-else
>
- No deploy keys found. Create one with the form above.
+ {{ s__('DeployKeys|No deploy keys found. Create one with the form above.') }}
</div>
</div>
</template>
diff --git a/app/assets/javascripts/deploy_keys/index.js b/app/assets/javascripts/deploy_keys/index.js
index b727261648c..6e439be42ae 100644
--- a/app/assets/javascripts/deploy_keys/index.js
+++ b/app/assets/javascripts/deploy_keys/index.js
@@ -1,21 +1,24 @@
import Vue from 'vue';
import deployKeysApp from './components/app.vue';
-export default () => new Vue({
- el: document.getElementById('js-deploy-keys'),
- components: {
- deployKeysApp,
- },
- data() {
- return {
- endpoint: this.$options.el.dataset.endpoint,
- };
- },
- render(createElement) {
- return createElement('deploy-keys-app', {
- props: {
- endpoint: this.endpoint,
- },
- });
- },
-});
+export default () =>
+ new Vue({
+ el: document.getElementById('js-deploy-keys'),
+ components: {
+ deployKeysApp,
+ },
+ data() {
+ return {
+ endpoint: this.$options.el.dataset.endpoint,
+ projectId: this.$options.el.dataset.projectId,
+ };
+ },
+ render(createElement) {
+ return createElement('deploy-keys-app', {
+ props: {
+ endpoint: this.endpoint,
+ projectId: this.projectId,
+ },
+ });
+ },
+ });
diff --git a/app/assets/javascripts/deploy_keys/service/index.js b/app/assets/javascripts/deploy_keys/service/index.js
index fe6dbaa9498..194e95e4fca 100644
--- a/app/assets/javascripts/deploy_keys/service/index.js
+++ b/app/assets/javascripts/deploy_keys/service/index.js
@@ -7,21 +7,24 @@ export default class DeployKeysService {
constructor(endpoint) {
this.endpoint = endpoint;
- this.resource = Vue.resource(`${this.endpoint}{/id}`, {}, {
- enable: {
- method: 'PUT',
- url: `${this.endpoint}{/id}/enable`,
+ this.resource = Vue.resource(
+ `${this.endpoint}{/id}`,
+ {},
+ {
+ enable: {
+ method: 'PUT',
+ url: `${this.endpoint}{/id}/enable`,
+ },
+ disable: {
+ method: 'PUT',
+ url: `${this.endpoint}{/id}/disable`,
+ },
},
- disable: {
- method: 'PUT',
- url: `${this.endpoint}{/id}/disable`,
- },
- });
+ );
}
getKeys() {
- return this.resource.get()
- .then(response => response.json());
+ return this.resource.get().then(response => response.json());
}
enableKey(id) {
diff --git a/app/assets/javascripts/deploy_keys/store/index.js b/app/assets/javascripts/deploy_keys/store/index.js
index 6210361af26..a350bc99a70 100644
--- a/app/assets/javascripts/deploy_keys/store/index.js
+++ b/app/assets/javascripts/deploy_keys/store/index.js
@@ -3,7 +3,7 @@ export default class DeployKeysStore {
this.keys = {};
}
- findEnabledKey(id) {
- return this.keys.enabled_keys.find(key => key.id === id);
+ isEnabled(id) {
+ return this.keys.enabled_keys.some(key => key.id === id);
}
}
diff --git a/app/assets/javascripts/emoji/index.js b/app/assets/javascripts/emoji/index.js
index dc7672560ea..cd8dff40b88 100644
--- a/app/assets/javascripts/emoji/index.js
+++ b/app/assets/javascripts/emoji/index.js
@@ -34,7 +34,7 @@ export function getEmojiCategoryMap() {
symbols: [],
flags: [],
};
- Object.keys(emojiMap).forEach((name) => {
+ Object.keys(emojiMap).forEach(name => {
const emoji = emojiMap[name];
if (emojiCategoryMap[emoji.category]) {
emojiCategoryMap[emoji.category].push(name);
@@ -79,7 +79,9 @@ export function glEmojiTag(inputName, options) {
classList.push(fallbackSpriteClass);
}
const classAttribute = classList.length > 0 ? `class="${classList.join(' ')}"` : '';
- const fallbackSpriteAttribute = opts.sprite ? `data-fallback-sprite-class="${fallbackSpriteClass}"` : '';
+ const fallbackSpriteAttribute = opts.sprite
+ ? `data-fallback-sprite-class="${fallbackSpriteClass}"`
+ : '';
let contents = emojiInfo.moji;
if (opts.forceFallback && !opts.sprite) {
contents = emojiImageTag(name, fallbackImageSrc);
diff --git a/app/assets/javascripts/emoji/support/unicode_support_map.js b/app/assets/javascripts/emoji/support/unicode_support_map.js
index c18d07dad43..8c1861c56db 100644
--- a/app/assets/javascripts/emoji/support/unicode_support_map.js
+++ b/app/assets/javascripts/emoji/support/unicode_support_map.js
@@ -54,7 +54,8 @@ const unicodeSupportTestMap = {
function checkPixelInImageDataArray(pixelOffset, imageDataArray) {
// `4 *` because RGBA
const indexOffset = 4 * pixelOffset;
- const hasColor = imageDataArray[indexOffset + 0] ||
+ const hasColor =
+ imageDataArray[indexOffset + 0] ||
imageDataArray[indexOffset + 1] ||
imageDataArray[indexOffset + 2];
const isVisible = imageDataArray[indexOffset + 3];
@@ -75,23 +76,23 @@ const chromeVersion = chromeMatches && chromeMatches[1] && parseInt(chromeMatche
const fontSize = 16;
function generateUnicodeSupportMap(testMap) {
const testMapKeys = Object.keys(testMap);
- const numTestEntries = testMapKeys
- .reduce((list, testKey) => list.concat(testMap[testKey]), []).length;
+ const numTestEntries = testMapKeys.reduce((list, testKey) => list.concat(testMap[testKey]), [])
+ .length;
const canvas = document.createElement('canvas');
(window.gl || window).testEmojiUnicodeSupportMapCanvas = canvas;
const ctx = canvas.getContext('2d');
- canvas.width = (2 * fontSize);
- canvas.height = (numTestEntries * fontSize);
+ canvas.width = 2 * fontSize;
+ canvas.height = numTestEntries * fontSize;
ctx.fillStyle = '#000000';
ctx.textBaseline = 'middle';
ctx.font = `${fontSize}px "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"`;
// Write each emoji to the canvas vertically
let writeIndex = 0;
- testMapKeys.forEach((testKey) => {
+ testMapKeys.forEach(testKey => {
const testEntry = testMap[testKey];
- [].concat(testEntry).forEach((emojiUnicode) => {
- ctx.fillText(emojiUnicode, 0, (writeIndex * fontSize) + (fontSize / 2));
+ [].concat(testEntry).forEach(emojiUnicode => {
+ ctx.fillText(emojiUnicode, 0, writeIndex * fontSize + fontSize / 2);
writeIndex += 1;
});
});
@@ -99,29 +100,25 @@ function generateUnicodeSupportMap(testMap) {
// Read from the canvas
const resultMap = {};
let readIndex = 0;
- testMapKeys.forEach((testKey) => {
+ testMapKeys.forEach(testKey => {
const testEntry = testMap[testKey];
// This needs to be a `reduce` instead of `every` because we need to
// keep the `readIndex` in sync from the writes by running all entries
- const isTestSatisfied = [].concat(testEntry).reduce((isSatisfied) => {
+ const isTestSatisfied = [].concat(testEntry).reduce(isSatisfied => {
// Sample along the vertical-middle for a couple of characters
- const imageData = ctx.getImageData(
- 0,
- (readIndex * fontSize) + (fontSize / 2),
- 2 * fontSize,
- 1,
- ).data;
+ const imageData = ctx.getImageData(0, readIndex * fontSize + fontSize / 2, 2 * fontSize, 1)
+ .data;
let isValidEmoji = false;
for (let currentPixel = 0; currentPixel < 64; currentPixel += 1) {
const isLookingAtFirstChar = currentPixel < fontSize;
- const isLookingAtSecondChar = currentPixel >= (fontSize + (fontSize / 2));
+ const isLookingAtSecondChar = currentPixel >= fontSize + fontSize / 2;
// Check for the emoji somewhere along the row
if (isLookingAtFirstChar && checkPixelInImageDataArray(currentPixel, imageData)) {
isValidEmoji = true;
- // Check to see that nothing is rendered next to the first character
- // to ensure that the ZWJ sequence rendered as one piece
+ // Check to see that nothing is rendered next to the first character
+ // to ensure that the ZWJ sequence rendered as one piece
} else if (isLookingAtSecondChar && checkPixelInImageDataArray(currentPixel, imageData)) {
isValidEmoji = false;
break;
@@ -170,7 +167,10 @@ export default function getUnicodeSupportMap() {
if (isLocalStorageAvailable) {
window.localStorage.setItem('gl-emoji-version', GL_EMOJI_VERSION);
window.localStorage.setItem('gl-emoji-user-agent', navigator.userAgent);
- window.localStorage.setItem('gl-emoji-unicode-support-map', JSON.stringify(unicodeSupportMap));
+ window.localStorage.setItem(
+ 'gl-emoji-unicode-support-map',
+ JSON.stringify(unicodeSupportMap),
+ );
}
}
diff --git a/app/assets/javascripts/environments/components/container.vue b/app/assets/javascripts/environments/components/container.vue
index dbee81fa320..6bd7c6b49cb 100644
--- a/app/assets/javascripts/environments/components/container.vue
+++ b/app/assets/javascripts/environments/components/container.vue
@@ -43,6 +43,7 @@
<div class="environments-container">
<loading-icon
+ class="prepend-top-default"
label="Loading environments"
v-if="isLoading"
size="3"
diff --git a/app/assets/javascripts/environments/components/environment_actions.vue b/app/assets/javascripts/environments/components/environment_actions.vue
index 8767c55db9c..4584897a117 100644
--- a/app/assets/javascripts/environments/components/environment_actions.vue
+++ b/app/assets/javascripts/environments/components/environment_actions.vue
@@ -1,5 +1,5 @@
<script>
- import playIconSvg from 'icons/_icon_play.svg';
+ import Icon from '~/vue_shared/components/icon.vue';
import eventHub from '../event_hub';
import loadingIcon from '../../vue_shared/components/loading_icon.vue';
import tooltip from '../../vue_shared/directives/tooltip';
@@ -8,9 +8,9 @@
directives: {
tooltip,
},
-
components: {
loadingIcon,
+ Icon,
},
props: {
actions: {
@@ -19,20 +19,16 @@
default: () => [],
},
},
-
data() {
return {
- playIconSvg,
isLoading: false,
};
},
-
computed: {
title() {
return 'Deploy to...';
},
},
-
methods: {
onClickAction(endpoint) {
this.isLoading = true;
@@ -65,7 +61,10 @@
:disabled="isLoading"
>
<span>
- <span v-html="playIconSvg"></span>
+ <icon
+ name="play"
+ :size="12"
+ />
<i
class="fa fa-caret-down"
aria-hidden="true"
@@ -86,7 +85,10 @@
:class="{ disabled: isActionDisabled(action) }"
:disabled="isActionDisabled(action)"
>
- <span v-html="playIconSvg"></span>
+ <icon
+ name="play"
+ :size="12"
+ />
<span>
{{ action.name }}
</span>
diff --git a/app/assets/javascripts/environments/components/environment_external_url.vue b/app/assets/javascripts/environments/components/environment_external_url.vue
index c9a68cface6..ea6f1168c68 100644
--- a/app/assets/javascripts/environments/components/environment_external_url.vue
+++ b/app/assets/javascripts/environments/components/environment_external_url.vue
@@ -1,4 +1,5 @@
<script>
+ import Icon from '~/vue_shared/components/icon.vue';
import tooltip from '../../vue_shared/directives/tooltip';
import { s__ } from '../../locale';
@@ -6,6 +7,9 @@
* Renders the external url link in environments table.
*/
export default {
+ components: {
+ Icon,
+ },
directives: {
tooltip,
},
@@ -15,7 +19,6 @@
required: true,
},
},
-
computed: {
title() {
return s__('Environments|Open');
@@ -34,10 +37,9 @@
:aria-label="title"
:href="externalUrl"
>
- <i
- class="fa fa-external-link"
- aria-hidden="true"
- >
- </i>
+ <icon
+ name="external-link"
+ :size="12"
+ />
</a>
</template>
diff --git a/app/assets/javascripts/environments/components/environment_monitoring.vue b/app/assets/javascripts/environments/components/environment_monitoring.vue
index ac5928f345c..8df1b6317e3 100644
--- a/app/assets/javascripts/environments/components/environment_monitoring.vue
+++ b/app/assets/javascripts/environments/components/environment_monitoring.vue
@@ -2,20 +2,22 @@
/**
* Renders the Monitoring (Metrics) link in environments table.
*/
+ import Icon from '~/vue_shared/components/icon.vue';
import tooltip from '../../vue_shared/directives/tooltip';
export default {
+ components: {
+ Icon,
+ },
directives: {
tooltip,
},
-
props: {
monitoringUrl: {
type: String,
required: true,
},
},
-
computed: {
title() {
return 'Monitoring';
@@ -33,10 +35,9 @@
:title="title"
:aria-label="title"
>
- <i
- class="fa fa-area-chart"
- aria-hidden="true"
- >
- </i>
+ <icon
+ name="chart"
+ :size="12"
+ />
</a>
</template>
diff --git a/app/assets/javascripts/environments/components/environment_rollback.vue b/app/assets/javascripts/environments/components/environment_rollback.vue
index 495c2bb3fa6..7515d711c50 100644
--- a/app/assets/javascripts/environments/components/environment_rollback.vue
+++ b/app/assets/javascripts/environments/components/environment_rollback.vue
@@ -12,7 +12,6 @@
components: {
loadingIcon,
},
-
props: {
retryUrl: {
type: String,
@@ -24,13 +23,11 @@
default: true,
},
},
-
data() {
return {
isLoading: false,
};
},
-
methods: {
onClick() {
this.isLoading = true;
diff --git a/app/assets/javascripts/environments/components/environment_terminal_button.vue b/app/assets/javascripts/environments/components/environment_terminal_button.vue
index a42e6178238..0dbbbb75e07 100644
--- a/app/assets/javascripts/environments/components/environment_terminal_button.vue
+++ b/app/assets/javascripts/environments/components/environment_terminal_button.vue
@@ -3,14 +3,16 @@
* Renders a terminal button to open a web terminal.
* Used in environments table.
*/
- import terminalIconSvg from 'icons/_icon_terminal.svg';
+ import Icon from '~/vue_shared/components/icon.vue';
import tooltip from '../../vue_shared/directives/tooltip';
export default {
+ components: {
+ Icon,
+ },
directives: {
tooltip,
},
-
props: {
terminalPath: {
type: String,
@@ -18,13 +20,6 @@
default: '',
},
},
-
- data() {
- return {
- terminalIconSvg,
- };
- },
-
computed: {
title() {
return 'Terminal';
@@ -40,7 +35,10 @@
:title="title"
:aria-label="title"
:href="terminalPath"
- v-html="terminalIconSvg"
>
+ <icon
+ name="terminal"
+ :size="12"
+ />
</a>
</template>
diff --git a/app/assets/javascripts/gfm_auto_complete.js b/app/assets/javascripts/gfm_auto_complete.js
index 7e9770a9ea2..9de57db48fd 100644
--- a/app/assets/javascripts/gfm_auto_complete.js
+++ b/app/assets/javascripts/gfm_auto_complete.js
@@ -408,7 +408,10 @@ class GfmAutoComplete {
fetchData($input, at) {
if (this.isLoadingData[at]) return;
+
this.isLoadingData[at] = true;
+ const dataSource = this.dataSources[GfmAutoComplete.atTypeMap[at]];
+
if (this.cachedData[at]) {
this.loadData($input, at, this.cachedData[at]);
} else if (GfmAutoComplete.atTypeMap[at] === 'emojis') {
@@ -418,12 +421,14 @@ class GfmAutoComplete {
GfmAutoComplete.glEmojiTag = glEmojiTag;
})
.catch(() => { this.isLoadingData[at] = false; });
- } else {
- AjaxCache.retrieve(this.dataSources[GfmAutoComplete.atTypeMap[at]], true)
+ } else if (dataSource) {
+ AjaxCache.retrieve(dataSource, true)
.then((data) => {
this.loadData($input, at, data);
})
.catch(() => { this.isLoadingData[at] = false; });
+ } else {
+ this.isLoadingData[at] = false;
}
}
diff --git a/app/assets/javascripts/gpg_badges.js b/app/assets/javascripts/gpg_badges.js
index 502e3569321..029fd6a67d4 100644
--- a/app/assets/javascripts/gpg_badges.js
+++ b/app/assets/javascripts/gpg_badges.js
@@ -7,12 +7,12 @@ import { __ } from '~/locale';
export default class GpgBadges {
static fetch() {
const badges = $('.js-loading-gpg-badge');
- const form = $('.commits-search-form');
+ const tag = $('.js-signature-container');
badges.html('<i class="fa fa-spinner fa-spin"></i>');
- const params = parseQueryStringIntoObject(form.serialize());
- return axios.get(form.data('signaturesPath'), { params })
+ const params = parseQueryStringIntoObject(tag.serialize());
+ return axios.get(tag.data('signaturesPath'), { params })
.then(({ data }) => {
data.signatures.forEach((signature) => {
badges.filter(`[data-commit-sha="${signature.commit_sha}"]`).replaceWith(signature.html);
diff --git a/app/assets/javascripts/ide/components/activity_bar.vue b/app/assets/javascripts/ide/components/activity_bar.vue
new file mode 100644
index 00000000000..05dbc1410de
--- /dev/null
+++ b/app/assets/javascripts/ide/components/activity_bar.vue
@@ -0,0 +1,106 @@
+<script>
+import { mapActions, mapGetters, mapState } from 'vuex';
+import Icon from '~/vue_shared/components/icon.vue';
+import tooltip from '~/vue_shared/directives/tooltip';
+import { activityBarViews } from '../constants';
+
+export default {
+ components: {
+ Icon,
+ },
+ directives: {
+ tooltip,
+ },
+ computed: {
+ ...mapGetters(['currentProject', 'hasChanges']),
+ ...mapState(['currentActivityView']),
+ goBackUrl() {
+ return document.referrer || this.currentProject.web_url;
+ },
+ },
+ methods: {
+ ...mapActions(['updateActivityBarView']),
+ },
+ activityBarViews,
+};
+</script>
+
+<template>
+ <nav class="ide-activity-bar">
+ <ul class="list-unstyled">
+ <li v-once>
+ <a
+ v-tooltip
+ data-container="body"
+ data-placement="right"
+ :href="goBackUrl"
+ class="ide-sidebar-link"
+ :title="s__('IDE|Go back')"
+ :aria-label="s__('IDE|Go back')"
+ >
+ <icon
+ :size="16"
+ name="go-back"
+ />
+ </a>
+ </li>
+ <li>
+ <button
+ v-tooltip
+ data-container="body"
+ data-placement="right"
+ type="button"
+ class="ide-sidebar-link js-ide-edit-mode"
+ :class="{
+ active: currentActivityView === $options.activityBarViews.edit
+ }"
+ @click.prevent="updateActivityBarView($options.activityBarViews.edit)"
+ :title="s__('IDE|Edit')"
+ :aria-label="s__('IDE|Edit')"
+ >
+ <icon
+ name="code"
+ />
+ </button>
+ </li>
+ <li>
+ <button
+ v-tooltip
+ data-container="body"
+ data-placement="right"
+ type="button"
+ class="ide-sidebar-link js-ide-review-mode"
+ :class="{
+ active: currentActivityView === $options.activityBarViews.review
+ }"
+ @click.prevent="updateActivityBarView($options.activityBarViews.review)"
+ :title="s__('IDE|Review')"
+ :aria-label="s__('IDE|Review')"
+ >
+ <icon
+ name="file-modified"
+ />
+ </button>
+ </li>
+ <li v-show="hasChanges">
+ <button
+ v-tooltip
+ data-container="body"
+ data-placement="right"
+ type="button"
+ class="ide-sidebar-link js-ide-commit-mode"
+ :class="{
+ active: currentActivityView === $options.activityBarViews.commit
+ }"
+ @click.prevent="updateActivityBarView($options.activityBarViews.commit)"
+ :title="s__('IDE|Commit')"
+ :aria-label="s__('IDE|Commit')"
+ >
+ <icon
+ name="commit"
+ />
+ </button>
+ </li>
+ </ul>
+ </nav>
+</template>
diff --git a/app/assets/javascripts/ide/components/changed_file_icon.vue b/app/assets/javascripts/ide/components/changed_file_icon.vue
index fdbc14a4b8f..1cec84706fc 100644
--- a/app/assets/javascripts/ide/components/changed_file_icon.vue
+++ b/app/assets/javascripts/ide/components/changed_file_icon.vue
@@ -43,7 +43,7 @@ export default {
return `${this.changedIcon}-solid`;
},
changedIconClass() {
- return `multi-${this.changedIcon} prepend-left-5 pull-left`;
+ return `multi-${this.changedIcon} pull-left`;
},
tooltipTitle() {
if (!this.showTooltip) return undefined;
@@ -79,13 +79,7 @@ export default {
class="ide-file-changed-icon"
>
<icon
- v-if="file.staged && showStagedIcon"
- :name="stagedIcon"
- :size="12"
- :css-classes="changedIconClass"
- />
- <icon
- v-if="file.changed || file.tempFile || (file.staged && !showStagedIcon)"
+ v-if="file.changed || file.tempFile || file.staged"
:name="changedIcon"
:size="12"
:css-classes="changedIconClass"
diff --git a/app/assets/javascripts/ide/components/commit_sidebar/actions.vue b/app/assets/javascripts/ide/components/commit_sidebar/actions.vue
index 45321df191c..6a5790c9dff 100644
--- a/app/assets/javascripts/ide/components/commit_sidebar/actions.vue
+++ b/app/assets/javascripts/ide/components/commit_sidebar/actions.vue
@@ -1,5 +1,5 @@
<script>
-import { mapState } from 'vuex';
+import { mapActions, mapState } from 'vuex';
import { sprintf, __ } from '~/locale';
import * as consts from '../../stores/modules/commit/constants';
import RadioGroup from './radio_group.vue';
@@ -9,7 +9,7 @@ export default {
RadioGroup,
},
computed: {
- ...mapState(['currentBranchId']),
+ ...mapState(['currentBranchId', 'changedFiles', 'stagedFiles']),
commitToCurrentBranchText() {
return sprintf(
__('Commit to %{branchName} branch'),
@@ -17,6 +17,17 @@ export default {
false,
);
},
+ disableMergeRequestRadio() {
+ return this.changedFiles.length > 0 && this.stagedFiles.length > 0;
+ },
+ },
+ mounted() {
+ if (this.disableMergeRequestRadio) {
+ this.updateCommitAction(consts.COMMIT_TO_CURRENT_BRANCH);
+ }
+ },
+ methods: {
+ ...mapActions('commit', ['updateCommitAction']),
},
commitToCurrentBranch: consts.COMMIT_TO_CURRENT_BRANCH,
commitToNewBranch: consts.COMMIT_TO_NEW_BRANCH,
@@ -44,6 +55,7 @@ export default {
:value="$options.commitToNewBranchMR"
:label="__('Create a new branch and merge request')"
:show-input="true"
+ :disabled="disableMergeRequestRadio"
/>
</div>
</template>
diff --git a/app/assets/javascripts/ide/components/commit_sidebar/empty_state.vue b/app/assets/javascripts/ide/components/commit_sidebar/empty_state.vue
index 6424b93ce54..d0a60d647e5 100644
--- a/app/assets/javascripts/ide/components/commit_sidebar/empty_state.vue
+++ b/app/assets/javascripts/ide/components/commit_sidebar/empty_state.vue
@@ -1,75 +1,27 @@
<script>
-import { mapActions, mapState, mapGetters } from 'vuex';
-import Icon from '~/vue_shared/components/icon.vue';
-import tooltip from '~/vue_shared/directives/tooltip';
+import { mapState } from 'vuex';
export default {
- components: {
- Icon,
- },
- directives: {
- tooltip,
- },
- props: {
- noChangesStateSvgPath: {
- type: String,
- required: true,
- },
- committedStateSvgPath: {
- type: String,
- required: true,
- },
- },
computed: {
- ...mapState(['lastCommitMsg', 'rightPanelCollapsed']),
- ...mapGetters(['collapseButtonIcon', 'collapseButtonTooltip']),
- statusSvg() {
- return this.lastCommitMsg ? this.committedStateSvgPath : this.noChangesStateSvgPath;
- },
- },
- methods: {
- ...mapActions(['toggleRightPanelCollapsed']),
+ ...mapState(['lastCommitMsg', 'noChangesStateSvgPath']),
},
};
</script>
<template>
<div
+ v-if="!lastCommitMsg"
class="multi-file-commit-panel-section ide-commit-empty-state js-empty-state"
>
- <header
- class="multi-file-commit-panel-header"
- :class="{
- 'is-collapsed': rightPanelCollapsed,
- }"
- >
- <button
- v-tooltip
- :title="collapseButtonTooltip"
- data-container="body"
- data-placement="left"
- type="button"
- class="btn btn-transparent multi-file-commit-panel-collapse-btn"
- :aria-label="__('Toggle sidebar')"
- @click.stop="toggleRightPanelCollapsed"
- >
- <icon
- :name="collapseButtonIcon"
- :size="18"
- />
- </button>
- </header>
<div
class="ide-commit-empty-state-container"
- v-if="!rightPanelCollapsed"
>
<div class="svg-content svg-80">
- <img :src="statusSvg" />
+ <img :src="noChangesStateSvgPath" />
</div>
<div class="append-right-default prepend-left-default">
<div
class="text-content text-center"
- v-if="!lastCommitMsg"
>
<h4>
{{ __('No changes') }}
@@ -78,15 +30,6 @@ export default {
{{ __('Edit files in the editor and commit changes here') }}
</p>
</div>
- <div
- class="text-content text-center"
- v-else
- >
- <h4>
- {{ __('All changes are committed') }}
- </h4>
- <p v-html="lastCommitMsg"></p>
- </div>
</div>
</div>
</div>
diff --git a/app/assets/javascripts/ide/components/commit_sidebar/form.vue b/app/assets/javascripts/ide/components/commit_sidebar/form.vue
new file mode 100644
index 00000000000..4a645c827ad
--- /dev/null
+++ b/app/assets/javascripts/ide/components/commit_sidebar/form.vue
@@ -0,0 +1,171 @@
+<script>
+import { mapState, mapActions, mapGetters } from 'vuex';
+import { sprintf, __ } from '~/locale';
+import LoadingButton from '~/vue_shared/components/loading_button.vue';
+import CommitMessageField from './message_field.vue';
+import Actions from './actions.vue';
+import SuccessMessage from './success_message.vue';
+import { activityBarViews, MAX_WINDOW_HEIGHT_COMPACT, COMMIT_ITEM_PADDING } from '../../constants';
+
+export default {
+ components: {
+ Actions,
+ LoadingButton,
+ CommitMessageField,
+ SuccessMessage,
+ },
+ data() {
+ return {
+ isCompact: true,
+ componentHeight: null,
+ };
+ },
+ computed: {
+ ...mapState(['changedFiles', 'stagedFiles', 'currentActivityView', 'lastCommitMsg']),
+ ...mapState('commit', ['commitMessage', 'submitCommitLoading']),
+ ...mapGetters(['hasChanges']),
+ ...mapGetters('commit', ['commitButtonDisabled', 'discardDraftButtonDisabled']),
+ overviewText() {
+ return sprintf(
+ __(
+ '<strong>%{changedFilesLength} unstaged</strong> and <strong>%{stagedFilesLength} staged</strong> changes',
+ ),
+ {
+ stagedFilesLength: this.stagedFiles.length,
+ changedFilesLength: this.changedFiles.length,
+ },
+ );
+ },
+ },
+ watch: {
+ currentActivityView() {
+ if (this.lastCommitMsg) {
+ this.isCompact = false;
+ } else {
+ this.isCompact = !(
+ this.currentActivityView === activityBarViews.commit &&
+ window.innerHeight >= MAX_WINDOW_HEIGHT_COMPACT
+ );
+ }
+ },
+ lastCommitMsg() {
+ this.isCompact =
+ this.currentActivityView !== activityBarViews.commit && this.lastCommitMsg === '';
+ },
+ },
+ methods: {
+ ...mapActions(['updateActivityBarView']),
+ ...mapActions('commit', ['updateCommitMessage', 'discardDraft', 'commitChanges']),
+ toggleIsSmall() {
+ this.updateActivityBarView(activityBarViews.commit)
+ .then(() => {
+ this.isCompact = !this.isCompact;
+ })
+ .catch(e => {
+ throw e;
+ });
+ },
+ beforeEnterTransition() {
+ const elHeight = this.isCompact
+ ? this.$refs.formEl && this.$refs.formEl.offsetHeight
+ : this.$refs.compactEl && this.$refs.compactEl.offsetHeight;
+
+ this.componentHeight = elHeight + COMMIT_ITEM_PADDING;
+ },
+ enterTransition() {
+ this.$nextTick(() => {
+ const elHeight = this.isCompact
+ ? this.$refs.compactEl && this.$refs.compactEl.offsetHeight
+ : this.$refs.formEl && this.$refs.formEl.offsetHeight;
+
+ this.componentHeight = elHeight + COMMIT_ITEM_PADDING;
+ });
+ },
+ afterEndTransition() {
+ this.componentHeight = null;
+ },
+ },
+ activityBarViews,
+};
+</script>
+
+<template>
+ <div
+ class="multi-file-commit-form"
+ :class="{
+ 'is-compact': isCompact,
+ 'is-full': !isCompact
+ }"
+ :style="{
+ height: componentHeight ? `${componentHeight}px` : null,
+ }"
+ >
+ <transition
+ name="commit-form-slide-up"
+ @before-enter="beforeEnterTransition"
+ @enter="enterTransition"
+ @after-enter="afterEndTransition"
+ >
+ <div
+ v-if="isCompact"
+ class="commit-form-compact"
+ ref="compactEl"
+ >
+ <button
+ type="button"
+ :disabled="!hasChanges"
+ class="btn btn-primary btn-sm btn-block"
+ @click="toggleIsSmall"
+ >
+ {{ __('Commit') }}
+ </button>
+ <p
+ class="text-center"
+ v-html="overviewText"
+ ></p>
+ </div>
+ <form
+ v-if="!isCompact"
+ class="form-horizontal"
+ @submit.prevent.stop="commitChanges"
+ ref="formEl"
+ >
+ <transition name="fade">
+ <success-message
+ v-show="lastCommitMsg"
+ />
+ </transition>
+ <commit-message-field
+ :text="commitMessage"
+ @input="updateCommitMessage"
+ />
+ <div class="clearfix prepend-top-15">
+ <actions />
+ <loading-button
+ :loading="submitCommitLoading"
+ :disabled="commitButtonDisabled"
+ container-class="btn btn-success btn-sm pull-left"
+ :label="__('Commit')"
+ @click="commitChanges"
+ />
+ <button
+ v-if="!discardDraftButtonDisabled"
+ type="button"
+ class="btn btn-default btn-sm pull-right"
+ @click="discardDraft"
+ >
+ {{ __('Discard draft') }}
+ </button>
+ <button
+ v-else
+ type="button"
+ class="btn btn-default btn-sm pull-right"
+ @click="toggleIsSmall"
+ >
+ {{ __('Collapse') }}
+ </button>
+ </div>
+ </form>
+ </transition>
+ </div>
+</template>
diff --git a/app/assets/javascripts/ide/components/commit_sidebar/list.vue b/app/assets/javascripts/ide/components/commit_sidebar/list.vue
index ff05ee8682a..c3ac18bfb83 100644
--- a/app/assets/javascripts/ide/components/commit_sidebar/list.vue
+++ b/app/assets/javascripts/ide/components/commit_sidebar/list.vue
@@ -1,16 +1,14 @@
<script>
-import { mapActions, mapState, mapGetters } from 'vuex';
+import { mapActions } from 'vuex';
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';
-import ListCollapsed from './list_collapsed.vue';
export default {
components: {
Icon,
ListItem,
- ListCollapsed,
},
directives: {
tooltip,
@@ -24,11 +22,6 @@ export default {
type: Array,
required: true,
},
- showToggle: {
- type: Boolean,
- required: false,
- default: true,
- },
iconName: {
type: String,
required: true,
@@ -51,9 +44,12 @@ export default {
default: false,
},
},
+ data() {
+ return {
+ showActionButton: false,
+ };
+ },
computed: {
- ...mapState(['rightPanelCollapsed']),
- ...mapGetters(['collapseButtonIcon', 'collapseButtonTooltip']),
titleText() {
return sprintf(__('%{title} changes'), {
title: this.title,
@@ -61,10 +57,13 @@ export default {
},
},
methods: {
- ...mapActions(['toggleRightPanelCollapsed', 'stageAllChanges', 'unstageAllChanges']),
+ ...mapActions(['stageAllChanges', 'unstageAllChanges']),
actionBtnClicked() {
this[this.action]();
},
+ setShowActionButton(show) {
+ this.showActionButton = show;
+ },
},
};
</script>
@@ -72,19 +71,14 @@ export default {
<template>
<div
class="ide-commit-list-container"
- :class="{
- 'is-collapsed': rightPanelCollapsed,
- }"
>
<header
class="multi-file-commit-panel-header"
+ @mouseenter="setShowActionButton(true)"
+ @mouseleave="setShowActionButton(false)"
>
<div
- v-if="!rightPanelCollapsed"
class="multi-file-commit-panel-header-title"
- :class="{
- 'append-right-10': showToggle,
- }"
>
<icon
v-once
@@ -92,7 +86,14 @@ export default {
:size="18"
/>
{{ titleText }}
+ <span
+ v-show="!showActionButton"
+ class="ide-commit-file-count"
+ >
+ {{ fileList.length }}
+ </span>
<button
+ v-show="showActionButton"
type="button"
class="btn btn-blank btn-link ide-staged-action-btn"
@click="actionBtnClicked"
@@ -100,52 +101,28 @@ export default {
{{ actionBtnText }}
</button>
</div>
- <button
- v-if="showToggle"
- v-tooltip
- :title="collapseButtonTooltip"
- data-container="body"
- data-placement="left"
- type="button"
- class="btn btn-transparent multi-file-commit-panel-collapse-btn"
- :aria-label="__('Toggle sidebar')"
- @click.stop="toggleRightPanelCollapsed"
- >
- <icon
- :name="collapseButtonIcon"
- :size="18"
- />
- </button>
</header>
- <list-collapsed
- v-if="rightPanelCollapsed"
- :files="fileList"
- :icon-name="iconName"
- :title="title"
- />
- <template v-else>
- <ul
- v-if="fileList.length"
- class="multi-file-commit-list list-unstyled append-bottom-0"
- >
- <li
- v-for="file in fileList"
- :key="file.key"
- >
- <list-item
- :file="file"
- :action-component="itemActionComponent"
- :key-prefix="title"
- :staged-list="stagedList"
- />
- </li>
- </ul>
- <p
- v-else
- class="multi-file-commit-list help-block"
+ <ul
+ v-if="fileList.length"
+ class="multi-file-commit-list list-unstyled append-bottom-0"
+ >
+ <li
+ v-for="file in fileList"
+ :key="file.key"
>
- {{ __('No changes') }}
- </p>
- </template>
+ <list-item
+ :file="file"
+ :action-component="itemActionComponent"
+ :key-prefix="title"
+ :staged-list="stagedList"
+ />
+ </li>
+ </ul>
+ <p
+ v-else
+ class="multi-file-commit-list help-block"
+ >
+ {{ __('No changes') }}
+ </p>
</div>
</template>
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 ad4713c40d5..03f3e4de83c 100644
--- a/app/assets/javascripts/ide/components/commit_sidebar/list_item.vue
+++ b/app/assets/javascripts/ide/components/commit_sidebar/list_item.vue
@@ -3,6 +3,7 @@ import { mapActions } from 'vuex';
import Icon from '~/vue_shared/components/icon.vue';
import StageButton from './stage_button.vue';
import UnstageButton from './unstage_button.vue';
+import { viewerTypes } from '../../constants';
export default {
components: {
@@ -36,7 +37,7 @@ export default {
return this.file.tempFile ? `file-addition${prefix}` : `file-modified${prefix}`;
},
iconClass() {
- return `multi-file-${this.file.tempFile ? 'additions' : 'modified'} append-right-8`;
+ return `multi-file-${this.file.tempFile ? 'addition' : 'modified'} append-right-8`;
},
},
methods: {
@@ -53,7 +54,7 @@ export default {
keyPrefix: this.keyPrefix.toLowerCase(),
}).then(changeViewer => {
if (changeViewer) {
- this.updateViewer('diff');
+ this.updateViewer(viewerTypes.diff);
}
});
},
diff --git a/app/assets/javascripts/ide/components/commit_sidebar/radio_group.vue b/app/assets/javascripts/ide/components/commit_sidebar/radio_group.vue
index b660a2961cb..00f2312ae51 100644
--- a/app/assets/javascripts/ide/components/commit_sidebar/radio_group.vue
+++ b/app/assets/javascripts/ide/components/commit_sidebar/radio_group.vue
@@ -1,5 +1,6 @@
<script>
import { mapActions, mapState, mapGetters } from 'vuex';
+import { __ } from '~/locale';
import tooltip from '~/vue_shared/directives/tooltip';
export default {
@@ -26,10 +27,20 @@ export default {
required: false,
default: false,
},
+ disabled: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
},
computed: {
...mapState('commit', ['commitAction']),
...mapGetters('commit', ['newBranchName']),
+ tooltipTitle() {
+ return this.disabled
+ ? __('This option is disabled while you still have unstaged changes')
+ : '';
+ },
},
methods: {
...mapActions('commit', ['updateCommitAction', 'updateBranchName']),
@@ -39,19 +50,28 @@ export default {
<template>
<fieldset>
- <label>
+ <label
+ v-tooltip
+ :title="tooltipTitle"
+ :class="{
+ 'is-disabled': disabled
+ }"
+ >
<input
type="radio"
name="commit-action"
:value="value"
@change="updateCommitAction($event.target.value)"
- :checked="checked"
- v-once
+ :checked="commitAction === value"
+ :disabled="disabled"
/>
<span class="prepend-left-10">
- <template v-if="label">
+ <span
+ v-if="label"
+ class="ide-radio-label"
+ >
{{ label }}
- </template>
+ </span>
<slot v-else></slot>
</span>
</label>
diff --git a/app/assets/javascripts/ide/components/commit_sidebar/success_message.vue b/app/assets/javascripts/ide/components/commit_sidebar/success_message.vue
new file mode 100644
index 00000000000..a6df91b79c2
--- /dev/null
+++ b/app/assets/javascripts/ide/components/commit_sidebar/success_message.vue
@@ -0,0 +1,33 @@
+<script>
+import { mapState } from 'vuex';
+
+export default {
+ computed: {
+ ...mapState(['lastCommitMsg', 'committedStateSvgPath']),
+ },
+};
+</script>
+
+<template>
+ <div
+ class="multi-file-commit-panel-success-message"
+ aria-live="assertive"
+ >
+ <div class="svg-content svg-80">
+ <img
+ :src="committedStateSvgPath"
+ alt=""
+ />
+ </div>
+ <div class="append-right-default prepend-left-default">
+ <div
+ class="text-content text-center"
+ >
+ <h4>
+ {{ __('All changes are committed') }}
+ </h4>
+ <p v-html="lastCommitMsg"></p>
+ </div>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/ide/components/editor_mode_dropdown.vue b/app/assets/javascripts/ide/components/editor_mode_dropdown.vue
index 0c44a755f56..b9af4d27145 100644
--- a/app/assets/javascripts/ide/components/editor_mode_dropdown.vue
+++ b/app/assets/javascripts/ide/components/editor_mode_dropdown.vue
@@ -1,28 +1,15 @@
<script>
-import Icon from '~/vue_shared/components/icon.vue';
import { __, sprintf } from '~/locale';
+import { viewerTypes } from '../constants';
export default {
- components: {
- Icon,
- },
props: {
- hasChanges: {
- type: Boolean,
- required: false,
- default: false,
- },
- mergeRequestId: {
- type: String,
- required: false,
- default: '',
- },
viewer: {
type: String,
required: true,
},
- showShadow: {
- type: Boolean,
+ mergeRequestId: {
+ type: Number,
required: true,
},
},
@@ -38,84 +25,45 @@ export default {
this.$emit('click', mode);
},
},
+ viewerTypes,
};
</script>
<template>
<div
class="dropdown"
- :class="{
- shadow: showShadow,
- }"
>
<button
type="button"
- class="btn btn-primary btn-sm"
- :class="{
- 'btn-inverted': hasChanges,
- }"
+ class="btn btn-link"
data-toggle="dropdown"
>
- <template v-if="viewer === 'mrdiff' && mergeRequestId">
- {{ mergeReviewLine }}
- </template>
- <template v-else-if="viewer === 'editor'">
- {{ __('Editing') }}
- </template>
- <template v-else>
- {{ __('Reviewing') }}
- </template>
- <icon
- name="angle-down"
- :size="12"
- css-classes="caret-down"
- />
+ {{ __('Edit') }}
</button>
<div class="dropdown-menu dropdown-menu-selectable dropdown-open-left">
<ul>
- <template v-if="mergeRequestId">
- <li>
- <a
- href="#"
- @click.prevent="changeMode('mrdiff')"
- :class="{
- 'is-active': viewer === 'mrdiff',
- }"
- >
- <strong class="dropdown-menu-inner-title">
- {{ mergeReviewLine }}
- </strong>
- <span class="dropdown-menu-inner-content">
- {{ __('Compare changes with the merge request target branch') }}
- </span>
- </a>
- </li>
- <li
- role="separator"
- class="divider"
- >
- </li>
- </template>
<li>
<a
href="#"
- @click.prevent="changeMode('editor')"
+ @click.prevent="changeMode($options.viewerTypes.mr)"
:class="{
- 'is-active': viewer === 'editor',
+ 'is-active': viewer === $options.viewerTypes.mr,
}"
>
- <strong class="dropdown-menu-inner-title">{{ __('Editing') }}</strong>
+ <strong class="dropdown-menu-inner-title">
+ {{ mergeReviewLine }}
+ </strong>
<span class="dropdown-menu-inner-content">
- {{ __('View and edit lines') }}
+ {{ __('Compare changes with the merge request target branch') }}
</span>
</a>
</li>
<li>
<a
href="#"
- @click.prevent="changeMode('diff')"
+ @click.prevent="changeMode($options.viewerTypes.diff)"
:class="{
- 'is-active': viewer === 'diff',
+ 'is-active': viewer === $options.viewerTypes.diff,
}"
>
<strong class="dropdown-menu-inner-title">{{ __('Reviewing') }}</strong>
diff --git a/app/assets/javascripts/ide/components/ide.vue b/app/assets/javascripts/ide/components/ide.vue
index c63c130500e..c328d1f36d2 100644
--- a/app/assets/javascripts/ide/components/ide.vue
+++ b/app/assets/javascripts/ide/components/ide.vue
@@ -1,144 +1,127 @@
<script>
- import { mapActions, mapState, mapGetters } from 'vuex';
- import Mousetrap from 'mousetrap';
- import ideSidebar from './ide_side_bar.vue';
- import ideContextbar from './ide_context_bar.vue';
- import repoTabs from './repo_tabs.vue';
- import ideStatusBar from './ide_status_bar.vue';
- import repoEditor from './repo_editor.vue';
- import FindFile from './file_finder/index.vue';
+import Mousetrap from 'mousetrap';
+import { mapActions, mapState, mapGetters } from 'vuex';
+import IdeSidebar from './ide_side_bar.vue';
+import RepoTabs from './repo_tabs.vue';
+import IdeStatusBar from './ide_status_bar.vue';
+import RepoEditor from './repo_editor.vue';
+import FindFile from './file_finder/index.vue';
- const originalStopCallback = Mousetrap.stopCallback;
+const originalStopCallback = Mousetrap.stopCallback;
- export default {
- components: {
- ideSidebar,
- ideContextbar,
- repoTabs,
- ideStatusBar,
- repoEditor,
- FindFile,
- },
- props: {
- emptyStateSvgPath: {
- type: String,
- required: true,
- },
- noChangesStateSvgPath: {
- type: String,
- required: true,
- },
- committedStateSvgPath: {
- type: String,
- required: true,
- },
- },
- computed: {
- ...mapState([
- 'changedFiles',
- 'openFiles',
- 'viewer',
- 'currentMergeRequestId',
- 'fileFindVisible',
- ]),
- ...mapGetters(['activeFile', 'hasChanges']),
- },
- mounted() {
- const returnValue = 'Are you sure you want to lose unsaved changes?';
- window.onbeforeunload = e => {
- if (!this.changedFiles.length) return undefined;
+export default {
+ components: {
+ IdeSidebar,
+ RepoTabs,
+ IdeStatusBar,
+ RepoEditor,
+ FindFile,
+ },
+ computed: {
+ ...mapState([
+ 'changedFiles',
+ 'openFiles',
+ 'viewer',
+ 'currentMergeRequestId',
+ 'fileFindVisible',
+ 'emptyStateSvgPath',
+ ]),
+ ...mapGetters(['activeFile', 'hasChanges']),
+ },
+ mounted() {
+ const returnValue = 'Are you sure you want to lose unsaved changes?';
+ window.onbeforeunload = e => {
+ if (!this.changedFiles.length) return undefined;
- Object.assign(e, {
- returnValue,
- });
- return returnValue;
- };
+ Object.assign(e, {
+ returnValue,
+ });
+ return returnValue;
+ };
- Mousetrap.bind(['t', 'command+p', 'ctrl+p'], e => {
- if (e.preventDefault) {
- e.preventDefault();
- }
+ Mousetrap.bind(['t', 'command+p', 'ctrl+p'], e => {
+ if (e.preventDefault) {
+ e.preventDefault();
+ }
- this.toggleFileFinder(!this.fileFindVisible);
- });
+ this.toggleFileFinder(!this.fileFindVisible);
+ });
- Mousetrap.stopCallback = (e, el, combo) => this.mousetrapStopCallback(e, el, combo);
- },
- methods: {
- ...mapActions(['toggleFileFinder']),
- mousetrapStopCallback(e, el, combo) {
- if (combo === 't' && el.classList.contains('dropdown-input-field')) {
- return true;
- } else if (combo === 'command+p' || combo === 'ctrl+p') {
- return false;
- }
+ Mousetrap.stopCallback = (e, el, combo) => this.mousetrapStopCallback(e, el, combo);
+ },
+ methods: {
+ ...mapActions(['toggleFileFinder']),
+ mousetrapStopCallback(e, el, combo) {
+ if (combo === 't' && el.classList.contains('dropdown-input-field')) {
+ return true;
+ } else if (combo === 'command+p' || combo === 'ctrl+p') {
+ return false;
+ }
- return originalStopCallback(e, el, combo);
- },
+ return originalStopCallback(e, el, combo);
},
- };
+ },
+};
</script>
<template>
- <div
- class="ide-view"
- >
- <find-file
- v-show="fileFindVisible"
- />
- <ide-sidebar />
+ <article class="ide">
<div
- class="multi-file-edit-pane"
+ class="ide-view"
>
- <template
- v-if="activeFile"
- >
- <repo-tabs
- :active-file="activeFile"
- :files="openFiles"
- :viewer="viewer"
- :has-changes="hasChanges"
- :merge-request-id="currentMergeRequestId"
- />
- <repo-editor
- class="multi-file-edit-pane-content"
- :file="activeFile"
- />
- <ide-status-bar
- :file="activeFile"
- />
- </template>
- <template
- v-else
+ <find-file
+ v-show="fileFindVisible"
+ />
+ <ide-sidebar />
+ <div
+ class="multi-file-edit-pane"
>
- <div
- v-once
- class="ide-empty-state"
+ <template
+ v-if="activeFile"
>
- <div class="row js-empty-state">
- <div class="col-12">
- <div class="svg-content svg-250">
- <img :src="emptyStateSvgPath" />
+ <repo-tabs
+ :active-file="activeFile"
+ :files="openFiles"
+ :viewer="viewer"
+ :has-changes="hasChanges"
+ :merge-request-id="currentMergeRequestId"
+ />
+ <repo-editor
+ class="multi-file-edit-pane-content"
+ :file="activeFile"
+ />
+ </template>
+ <template
+ v-else
+ >
+ <div
+ v-once
+ class="ide-empty-state"
+ >
+ <div class="row js-empty-state">
+ <div class="col-12">
+ <div class="svg-content svg-250">
+ <img :src="emptyStateSvgPath" />
+ </div>
</div>
- </div>
- <div class="col-12">
- <div class="text-content text-center">
- <h4>
- Welcome to the GitLab IDE
- </h4>
- <p>
- You can select a file in the left sidebar to begin
- editing and use the right sidebar to commit your changes.
- </p>
+ <div class="col-12">
+ <div class="text-content text-center">
+ <h4>
+ Welcome to the GitLab IDE
+ </h4>
+ <p>
+ You can select a file in the left sidebar to begin
+ editing and use the right sidebar to commit your changes.
+ </p>
+ </div>
</div>
</div>
</div>
- </div>
- </template>
+ </template>
+ </div>
</div>
- <ide-contextbar
- :no-changes-state-svg-path="noChangesStateSvgPath"
- :committed-state-svg-path="committedStateSvgPath"
+ <ide-status-bar
+ :file="activeFile"
/>
- </div>
+ </article>
</template>
diff --git a/app/assets/javascripts/ide/components/ide_context_bar.vue b/app/assets/javascripts/ide/components/ide_context_bar.vue
deleted file mode 100644
index 627fbeb9adf..00000000000
--- a/app/assets/javascripts/ide/components/ide_context_bar.vue
+++ /dev/null
@@ -1,42 +0,0 @@
-<script>
-import icon from '~/vue_shared/components/icon.vue';
-import panelResizer from '~/vue_shared/components/panel_resizer.vue';
-import repoCommitSection from './repo_commit_section.vue';
-import ResizablePanel from './resizable_panel.vue';
-
-export default {
- components: {
- repoCommitSection,
- icon,
- panelResizer,
- ResizablePanel,
- },
- props: {
- noChangesStateSvgPath: {
- type: String,
- required: true,
- },
- committedStateSvgPath: {
- type: String,
- required: true,
- },
- },
-};
-</script>
-
-<template>
- <resizable-panel
- :collapsible="true"
- :initial-width="340"
- side="right"
- >
- <div
- class="multi-file-commit-panel-section"
- >
- <repo-commit-section
- :no-changes-state-svg-path="noChangesStateSvgPath"
- :committed-state-svg-path="committedStateSvgPath"
- />
- </div>
- </resizable-panel>
-</template>
diff --git a/app/assets/javascripts/ide/components/ide_external_links.vue b/app/assets/javascripts/ide/components/ide_external_links.vue
deleted file mode 100644
index c6f6e0d2348..00000000000
--- a/app/assets/javascripts/ide/components/ide_external_links.vue
+++ /dev/null
@@ -1,43 +0,0 @@
-<script>
-import icon from '~/vue_shared/components/icon.vue';
-
-export default {
- components: {
- icon,
- },
- props: {
- projectUrl: {
- type: String,
- required: true,
- },
- },
- computed: {
- goBackUrl() {
- return document.referrer || this.projectUrl;
- },
- },
-};
-</script>
-
-<template>
- <nav
- class="ide-external-links"
- v-once
- >
- <p>
- <a
- :href="goBackUrl"
- class="ide-sidebar-link"
- >
- <icon
- :size="16"
- class="append-right-8"
- name="go-back"
- />
- <span class="ide-external-links-text">
- {{ s__('Go back') }}
- </span>
- </a>
- </p>
- </nav>
-</template>
diff --git a/app/assets/javascripts/ide/components/ide_project_branches_tree.vue b/app/assets/javascripts/ide/components/ide_project_branches_tree.vue
deleted file mode 100644
index eb2749e6151..00000000000
--- a/app/assets/javascripts/ide/components/ide_project_branches_tree.vue
+++ /dev/null
@@ -1,47 +0,0 @@
-<script>
- import icon from '~/vue_shared/components/icon.vue';
- import repoTree from './ide_repo_tree.vue';
- import newDropdown from './new_dropdown/index.vue';
-
- export default {
- components: {
- repoTree,
- icon,
- newDropdown,
- },
- props: {
- projectId: {
- type: String,
- required: true,
- },
- branch: {
- type: Object,
- required: true,
- },
- },
- };
-</script>
-
-<template>
- <div class="branch-container">
- <div class="branch-header">
- <div class="branch-header-title str-truncated ref-name">
- <icon
- name="branch"
- :size="12"
- />
- {{ branch.name }}
- </div>
- <div class="branch-header-btns">
- <new-dropdown
- :project-id="projectId"
- :branch="branch.name"
- path=""
- />
- </div>
- </div>
- <repo-tree
- :tree="branch.tree"
- />
- </div>
-</template>
diff --git a/app/assets/javascripts/ide/components/ide_project_tree.vue b/app/assets/javascripts/ide/components/ide_project_tree.vue
deleted file mode 100644
index a6f40286ac1..00000000000
--- a/app/assets/javascripts/ide/components/ide_project_tree.vue
+++ /dev/null
@@ -1,65 +0,0 @@
-<script>
-import ProjectAvatarImage from '~/vue_shared/components/project_avatar/image.vue';
-import Identicon from '../../vue_shared/components/identicon.vue';
-import BranchesTree from './ide_project_branches_tree.vue';
-import ExternalLinks from './ide_external_links.vue';
-
-export default {
- components: {
- BranchesTree,
- ExternalLinks,
- ProjectAvatarImage,
- Identicon,
- },
- props: {
- project: {
- type: Object,
- required: true,
- },
- },
-};
-</script>
-
-<template>
- <div class="projects-sidebar">
- <div class="context-header">
- <a
- :title="project.name"
- :href="project.web_url"
- >
- <div
- v-if="project.avatar_url"
- class="avatar-container s40 project-avatar"
- >
- <project-avatar-image
- class="avatar-container project-avatar"
- :link-href="project.path"
- :img-src="project.avatar_url"
- :img-alt="project.name"
- :img-size="40"
- />
- </div>
- <identicon
- v-else
- size-class="s40"
- :entity-id="project.id"
- :entity-name="project.name"
- />
- <div class="sidebar-context-title">
- {{ project.name }}
- </div>
- </a>
- </div>
- <external-links
- :project-url="project.web_url"
- />
- <div class="multi-file-commit-panel-inner-scroll">
- <branches-tree
- v-for="branch in project.branches"
- :key="branch.name"
- :project-id="project.path_with_namespace"
- :branch="branch"
- />
- </div>
- </div>
-</template>
diff --git a/app/assets/javascripts/ide/components/ide_repo_tree.vue b/app/assets/javascripts/ide/components/ide_repo_tree.vue
deleted file mode 100644
index e6af88e04bc..00000000000
--- a/app/assets/javascripts/ide/components/ide_repo_tree.vue
+++ /dev/null
@@ -1,41 +0,0 @@
-<script>
-import SkeletonLoadingContainer from '~/vue_shared/components/skeleton_loading_container.vue';
-import RepoFile from './repo_file.vue';
-
-export default {
- components: {
- RepoFile,
- SkeletonLoadingContainer,
- },
- props: {
- tree: {
- type: Object,
- required: true,
- },
- },
-};
-</script>
-
-<template>
- <div
- class="ide-file-list"
- >
- <template v-if="tree.loading">
- <div
- class="multi-file-loading-container"
- v-for="n in 3"
- :key="n"
- >
- <skeleton-loading-container />
- </div>
- </template>
- <template v-else>
- <repo-file
- v-for="file in tree.tree"
- :key="file.key"
- :file="file"
- :level="0"
- />
- </template>
- </div>
-</template>
diff --git a/app/assets/javascripts/ide/components/ide_review.vue b/app/assets/javascripts/ide/components/ide_review.vue
new file mode 100644
index 00000000000..0c9ec3b00f0
--- /dev/null
+++ b/app/assets/javascripts/ide/components/ide_review.vue
@@ -0,0 +1,62 @@
+<script>
+import { mapGetters, mapState, mapActions } from 'vuex';
+import IdeTreeList from './ide_tree_list.vue';
+import EditorModeDropdown from './editor_mode_dropdown.vue';
+import { viewerTypes } from '../constants';
+
+export default {
+ components: {
+ IdeTreeList,
+ EditorModeDropdown,
+ },
+ computed: {
+ ...mapGetters(['currentMergeRequest']),
+ ...mapState(['viewer']),
+ showLatestChangesText() {
+ return !this.currentMergeRequest || this.viewer === viewerTypes.diff;
+ },
+ showMergeRequestText() {
+ return this.currentMergeRequest && this.viewer === viewerTypes.mr;
+ },
+ },
+ mounted() {
+ this.$nextTick(() => {
+ this.updateViewer(this.currentMergeRequest ? viewerTypes.mr : viewerTypes.diff);
+ });
+ },
+ methods: {
+ ...mapActions(['updateViewer']),
+ },
+};
+</script>
+
+<template>
+ <ide-tree-list
+ :viewer-type="viewer"
+ header-class="ide-review-header"
+ :disable-action-dropdown="true"
+ >
+ <template
+ slot="header"
+ >
+ <div class="ide-review-button-holder">
+ {{ __('Review') }}
+ <editor-mode-dropdown
+ v-if="currentMergeRequest"
+ :viewer="viewer"
+ :merge-request-id="currentMergeRequest.iid"
+ @click="updateViewer"
+ />
+ </div>
+ <div class="prepend-top-5 ide-review-sub-header">
+ <template v-if="showLatestChangesText">
+ {{ __('Latest changes') }}
+ </template>
+ <template v-else-if="showMergeRequestText">
+ {{ __('Merge request') }}
+ (<a :href="currentMergeRequest.web_url">!{{ currentMergeRequest.iid }}</a>)
+ </template>
+ </div>
+ </template>
+ </ide-tree-list>
+</template>
diff --git a/app/assets/javascripts/ide/components/ide_side_bar.vue b/app/assets/javascripts/ide/components/ide_side_bar.vue
index 8cf1ccb4fce..3f980203911 100644
--- a/app/assets/javascripts/ide/components/ide_side_bar.vue
+++ b/app/assets/javascripts/ide/components/ide_side_bar.vue
@@ -1,36 +1,82 @@
<script>
- import { mapState, mapGetters } from 'vuex';
- import icon from '~/vue_shared/components/icon.vue';
- import panelResizer from '~/vue_shared/components/panel_resizer.vue';
- import skeletonLoadingContainer from '~/vue_shared/components/skeleton_loading_container.vue';
- import projectTree from './ide_project_tree.vue';
- import ResizablePanel from './resizable_panel.vue';
+import { mapState, mapGetters } from 'vuex';
+import ProjectAvatarImage from '~/vue_shared/components/project_avatar/image.vue';
+import Icon from '~/vue_shared/components/icon.vue';
+import tooltip from '~/vue_shared/directives/tooltip';
+import PanelResizer from '~/vue_shared/components/panel_resizer.vue';
+import SkeletonLoadingContainer from '~/vue_shared/components/skeleton_loading_container.vue';
+import Identicon from '../../vue_shared/components/identicon.vue';
+import IdeTree from './ide_tree.vue';
+import ResizablePanel from './resizable_panel.vue';
+import ActivityBar from './activity_bar.vue';
+import CommitSection from './repo_commit_section.vue';
+import CommitForm from './commit_sidebar/form.vue';
+import IdeReview from './ide_review.vue';
+import SuccessMessage from './commit_sidebar/success_message.vue';
+import { activityBarViews } from '../constants';
- export default {
- components: {
- projectTree,
- icon,
- panelResizer,
- skeletonLoadingContainer,
- ResizablePanel,
+export default {
+ directives: {
+ tooltip,
+ },
+ components: {
+ Icon,
+ PanelResizer,
+ SkeletonLoadingContainer,
+ ResizablePanel,
+ ActivityBar,
+ ProjectAvatarImage,
+ Identicon,
+ CommitSection,
+ IdeTree,
+ CommitForm,
+ IdeReview,
+ SuccessMessage,
+ },
+ data() {
+ return {
+ showTooltip: false,
+ };
+ },
+ computed: {
+ ...mapState([
+ 'loading',
+ 'currentBranchId',
+ 'currentActivityView',
+ 'changedFiles',
+ 'stagedFiles',
+ 'lastCommitMsg',
+ ]),
+ ...mapGetters(['currentProject', 'someUncommitedChanges']),
+ showSuccessMessage() {
+ return (
+ this.currentActivityView === activityBarViews.edit &&
+ (this.lastCommitMsg && !this.someUncommitedChanges)
+ );
},
- computed: {
- ...mapState([
- 'loading',
- ]),
- ...mapGetters([
- 'projectsWithTrees',
- ]),
+ branchTooltipTitle() {
+ return this.showTooltip ? this.currentBranchId : undefined;
},
- };
+ },
+ watch: {
+ currentBranchId() {
+ this.$nextTick(() => {
+ this.showTooltip = this.$refs.branchId.scrollWidth > this.$refs.branchId.offsetWidth;
+ });
+ },
+ },
+};
</script>
<template>
<resizable-panel
:collapsible="false"
- :initial-width="290"
+ :initial-width="340"
side="left"
>
+ <activity-bar
+ v-if="!loading"
+ />
<div class="multi-file-commit-panel-inner">
<template v-if="loading">
<div
@@ -41,11 +87,54 @@
<skeleton-loading-container />
</div>
</template>
- <project-tree
- v-for="project in projectsWithTrees"
- :key="project.id"
- :project="project"
- />
+ <template v-else>
+ <div class="context-header ide-context-header">
+ <a
+ :href="currentProject.web_url"
+ >
+ <div
+ v-if="currentProject.avatar_url"
+ class="avatar-container s40 project-avatar"
+ >
+ <project-avatar-image
+ class="avatar-container project-avatar"
+ :link-href="currentProject.path"
+ :img-src="currentProject.avatar_url"
+ :img-alt="currentProject.name"
+ :img-size="40"
+ />
+ </div>
+ <identicon
+ v-else
+ size-class="s40"
+ :entity-id="currentProject.id"
+ :entity-name="currentProject.name"
+ />
+ <div class="ide-sidebar-project-title">
+ <div class="sidebar-context-title">
+ {{ currentProject.name }}
+ </div>
+ <div
+ class="sidebar-context-title ide-sidebar-branch-title"
+ ref="branchId"
+ v-tooltip
+ :title="branchTooltipTitle"
+ >
+ <icon
+ name="branch"
+ css-classes="append-right-5"
+ />{{ currentBranchId }}
+ </div>
+ </div>
+ </a>
+ </div>
+ <div class="multi-file-commit-panel-inner-scroll">
+ <component
+ :is="currentActivityView"
+ />
+ </div>
+ <commit-form />
+ </template>
</div>
</resizable-panel>
</template>
diff --git a/app/assets/javascripts/ide/components/ide_status_bar.vue b/app/assets/javascripts/ide/components/ide_status_bar.vue
index c13eeeace3f..70c6d53c3ab 100644
--- a/app/assets/javascripts/ide/components/ide_status_bar.vue
+++ b/app/assets/javascripts/ide/components/ide_status_bar.vue
@@ -1,11 +1,14 @@
<script>
+import { mapGetters } from 'vuex';
import icon from '~/vue_shared/components/icon.vue';
import tooltip from '~/vue_shared/directives/tooltip';
import timeAgoMixin from '~/vue_shared/mixins/timeago';
+import userAvatarImage from '../../vue_shared/components/user_avatar/user_avatar_image.vue';
export default {
components: {
icon,
+ userAvatarImage,
},
directives: {
tooltip,
@@ -14,40 +17,93 @@ export default {
props: {
file: {
type: Object,
- required: true,
+ required: false,
+ default: null,
+ },
+ },
+ data() {
+ return {
+ lastCommitFormatedAge: null,
+ };
+ },
+ computed: {
+ ...mapGetters(['currentProject', 'lastCommit']),
+ },
+ mounted() {
+ this.startTimer();
+ },
+ beforeDestroy() {
+ if (this.intervalId) {
+ clearInterval(this.intervalId);
+ }
+ },
+ methods: {
+ startTimer() {
+ this.intervalId = setInterval(() => {
+ this.commitAgeUpdate();
+ }, 1000);
+ },
+ commitAgeUpdate() {
+ if (this.lastCommit) {
+ this.lastCommitFormatedAge = this.timeFormated(this.lastCommit.committed_date);
+ }
+ },
+ getCommitPath(shortSha) {
+ return `${this.currentProject.web_url}/commit/${shortSha}`;
},
},
};
</script>
<template>
- <div class="ide-status-bar">
- <div>
- <div v-if="file.lastCommit && file.lastCommit.id">
- Last commit:
- <a
- v-tooltip
- :title="file.lastCommit.message"
- :href="file.lastCommit.url"
- >
- {{ timeFormated(file.lastCommit.updatedAt) }} by
- {{ file.lastCommit.author }}
- </a>
- </div>
+ <footer class="ide-status-bar">
+ <div
+ class="ide-status-branch"
+ v-if="lastCommit && lastCommitFormatedAge"
+ >
+ <icon
+ name="commit"
+ />
+ <a
+ v-tooltip
+ class="commit-sha"
+ :title="lastCommit.message"
+ :href="getCommitPath(lastCommit.short_id)"
+ >{{ lastCommit.short_id }}</a>
+ by
+ {{ lastCommit.author_name }}
+ <time
+ v-tooltip
+ data-placement="top"
+ data-container="body"
+ :datetime="lastCommit.committed_date"
+ :title="tooltipTitle(lastCommit.committed_date)"
+ >
+ {{ lastCommitFormatedAge }}
+ </time>
</div>
- <div class="text-right">
+ <div
+ v-if="file"
+ class="ide-status-file"
+ >
{{ file.name }}
</div>
- <div class="text-right">
+ <div
+ v-if="file"
+ class="ide-status-file"
+ >
{{ file.eol }}
</div>
<div
- class="text-right"
- v-if="!file.binary">
+ class="ide-status-file"
+ v-if="file && !file.binary">
{{ file.editorRow }}:{{ file.editorColumn }}
</div>
- <div class="text-right">
+ <div
+ v-if="file"
+ class="ide-status-file"
+ >
{{ file.fileLanguage }}
</div>
- </div>
+ </footer>
</template>
diff --git a/app/assets/javascripts/ide/components/ide_tree.vue b/app/assets/javascripts/ide/components/ide_tree.vue
new file mode 100644
index 00000000000..8fc4ebe6ca6
--- /dev/null
+++ b/app/assets/javascripts/ide/components/ide_tree.vue
@@ -0,0 +1,42 @@
+<script>
+import { mapState, mapGetters, mapActions } from 'vuex';
+import NewDropdown from './new_dropdown/index.vue';
+import IdeTreeList from './ide_tree_list.vue';
+
+export default {
+ components: {
+ NewDropdown,
+ IdeTreeList,
+ },
+ computed: {
+ ...mapState(['currentBranchId']),
+ ...mapGetters(['currentProject', 'currentTree', 'activeFile']),
+ },
+ mounted() {
+ if (this.activeFile && this.activeFile.pending) {
+ this.$router.push(`/project${this.activeFile.url}`, () => {
+ this.updateViewer('editor');
+ });
+ }
+ },
+ methods: {
+ ...mapActions(['updateViewer']),
+ },
+};
+</script>
+
+<template>
+ <ide-tree-list
+ viewer-type="editor"
+ >
+ <template
+ slot="header"
+ >
+ {{ __('Edit') }}
+ <new-dropdown
+ :project-id="currentProject.name_with_namespace"
+ :branch="currentBranchId"
+ />
+ </template>
+ </ide-tree-list>
+</template>
diff --git a/app/assets/javascripts/ide/components/ide_tree_list.vue b/app/assets/javascripts/ide/components/ide_tree_list.vue
new file mode 100644
index 00000000000..e64a09fcc90
--- /dev/null
+++ b/app/assets/javascripts/ide/components/ide_tree_list.vue
@@ -0,0 +1,76 @@
+<script>
+import { mapActions, mapGetters, mapState } from 'vuex';
+import Icon from '~/vue_shared/components/icon.vue';
+import SkeletonLoadingContainer from '~/vue_shared/components/skeleton_loading_container.vue';
+import RepoFile from './repo_file.vue';
+import NewDropdown from './new_dropdown/index.vue';
+
+export default {
+ components: {
+ Icon,
+ RepoFile,
+ SkeletonLoadingContainer,
+ NewDropdown,
+ },
+ props: {
+ viewerType: {
+ type: String,
+ required: true,
+ },
+ headerClass: {
+ type: String,
+ required: false,
+ default: null,
+ },
+ disableActionDropdown: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ },
+ computed: {
+ ...mapState(['currentBranchId']),
+ ...mapGetters(['currentProject', 'currentTree']),
+ showLoading() {
+ return !this.currentTree || this.currentTree.loading;
+ },
+ },
+ mounted() {
+ this.updateViewer(this.viewerType);
+ },
+ methods: {
+ ...mapActions(['updateViewer']),
+ },
+};
+</script>
+
+<template>
+ <div
+ class="ide-file-list"
+ >
+ <template v-if="showLoading">
+ <div
+ class="multi-file-loading-container"
+ v-for="n in 3"
+ :key="n"
+ >
+ <skeleton-loading-container />
+ </div>
+ </template>
+ <template v-else>
+ <header
+ class="ide-tree-header"
+ :class="headerClass"
+ >
+ <slot name="header"></slot>
+ </header>
+ <repo-file
+ v-for="file in currentTree.tree"
+ :key="file.key"
+ :file="file"
+ :level="0"
+ :disable-action-dropdown="disableActionDropdown"
+ />
+ </template>
+ </div>
+</template>
diff --git a/app/assets/javascripts/ide/components/mr_file_icon.vue b/app/assets/javascripts/ide/components/mr_file_icon.vue
index 8a440902dfc..179a589d1ac 100644
--- a/app/assets/javascripts/ide/components/mr_file_icon.vue
+++ b/app/assets/javascripts/ide/components/mr_file_icon.vue
@@ -16,8 +16,8 @@ export default {
<icon
name="git-merge"
v-tooltip
- title="__('Part of merge request changes')"
- css-classes="ide-file-changed-icon"
+ :title="__('Part of merge request changes')"
+ css-classes="append-right-8"
:size="12"
/>
</template>
diff --git a/app/assets/javascripts/ide/components/new_dropdown/index.vue b/app/assets/javascripts/ide/components/new_dropdown/index.vue
index cac06125e01..b278b1fd84b 100644
--- a/app/assets/javascripts/ide/components/new_dropdown/index.vue
+++ b/app/assets/javascripts/ide/components/new_dropdown/index.vue
@@ -17,7 +17,8 @@ export default {
},
path: {
type: String,
- required: true,
+ required: false,
+ default: '',
},
},
data() {
diff --git a/app/assets/javascripts/ide/components/repo_commit_section.vue b/app/assets/javascripts/ide/components/repo_commit_section.vue
index 1152de85e5d..c5092d8e04d 100644
--- a/app/assets/javascripts/ide/components/repo_commit_section.vue
+++ b/app/assets/javascripts/ide/components/repo_commit_section.vue
@@ -3,12 +3,10 @@ import { mapState, mapActions, mapGetters } from 'vuex';
import tooltip from '~/vue_shared/directives/tooltip';
import Icon from '~/vue_shared/components/icon.vue';
import DeprecatedModal from '~/vue_shared/components/deprecated_modal.vue';
-import LoadingButton from '~/vue_shared/components/loading_button.vue';
import CommitFilesList from './commit_sidebar/list.vue';
import EmptyState from './commit_sidebar/empty_state.vue';
-import CommitMessageField from './commit_sidebar/message_field.vue';
import * as consts from '../stores/modules/commit/constants';
-import Actions from './commit_sidebar/actions.vue';
+import { activityBarViews } from '../constants';
export default {
components: {
@@ -16,35 +14,50 @@ export default {
Icon,
CommitFilesList,
EmptyState,
- Actions,
- LoadingButton,
- CommitMessageField,
},
directives: {
tooltip,
},
- props: {
- noChangesStateSvgPath: {
- type: String,
- required: true,
+ computed: {
+ ...mapState([
+ 'changedFiles',
+ 'stagedFiles',
+ 'rightPanelCollapsed',
+ 'lastCommitMsg',
+ 'unusedSeal',
+ ]),
+ ...mapState('commit', ['commitMessage', 'submitCommitLoading']),
+ ...mapGetters(['lastOpenedFile', 'hasChanges', 'someUncommitedChanges']),
+ ...mapGetters('commit', ['commitButtonDisabled', 'discardDraftButtonDisabled']),
+ showStageUnstageArea() {
+ return !!(this.someUncommitedChanges || this.lastCommitMsg || !this.unusedSeal);
},
- committedStateSvgPath: {
- type: String,
- required: true,
+ },
+ watch: {
+ hasChanges() {
+ if (!this.hasChanges) {
+ this.updateActivityBarView(activityBarViews.edit);
+ }
},
},
- computed: {
- ...mapState(['changedFiles', 'stagedFiles', 'rightPanelCollapsed']),
- ...mapState('commit', ['commitMessage', 'submitCommitLoading']),
- ...mapGetters('commit', ['commitButtonDisabled', 'discardDraftButtonDisabled', 'branchName']),
+ mounted() {
+ if (this.lastOpenedFile) {
+ this.openPendingTab({
+ file: this.lastOpenedFile,
+ })
+ .then(changeViewer => {
+ if (changeViewer) {
+ this.updateViewer('diff');
+ }
+ })
+ .catch(e => {
+ throw e;
+ });
+ }
},
methods: {
- ...mapActions('commit', [
- 'updateCommitMessage',
- 'discardDraft',
- 'commitChanges',
- 'updateCommitAction',
- ]),
+ ...mapActions(['openPendingTab', 'updateViewer', 'updateActivityBarView']),
+ ...mapActions('commit', ['commitChanges', 'updateCommitAction']),
forceCreateNewBranch() {
return this.updateCommitAction(consts.COMMIT_TO_NEW_BRANCH).then(() => this.commitChanges());
},
@@ -69,9 +82,10 @@ export default {
</template>
</deprecated-modal>
<template
- v-if="changedFiles.length || stagedFiles.length"
+ v-if="showStageUnstageArea"
>
<commit-files-list
+ class="is-first"
icon-name="unstaged"
:title="__('Unstaged')"
:file-list="changedFiles"
@@ -86,42 +100,11 @@ export default {
action="unstageAllChanges"
:action-btn-text="__('Unstage all')"
item-action-component="unstage-button"
- :show-toggle="false"
:staged-list="true"
/>
- <form
- class="multi-file-commit-form"
- @submit.prevent.stop="commitChanges"
- v-if="!rightPanelCollapsed"
- >
- <commit-message-field
- :text="commitMessage"
- @input="updateCommitMessage"
- />
- <div class="clearfix prepend-top-15">
- <actions />
- <loading-button
- :loading="submitCommitLoading"
- :disabled="commitButtonDisabled"
- container-class="btn btn-success btn-sm float-left"
- :label="__('Commit')"
- @click="commitChanges"
- />
- <button
- v-if="!discardDraftButtonDisabled"
- type="button"
- class="btn btn-secondary btn-sm float-right"
- @click="discardDraft"
- >
- {{ __('Discard draft') }}
- </button>
- </div>
- </form>
</template>
<empty-state
- v-else
- :no-changes-state-svg-path="noChangesStateSvgPath"
- :committed-state-svg-path="committedStateSvgPath"
+ v-if="unusedSeal"
/>
</div>
</template>
diff --git a/app/assets/javascripts/ide/components/repo_editor.vue b/app/assets/javascripts/ide/components/repo_editor.vue
index 22d5aaeae11..8e5ddedbb78 100644
--- a/app/assets/javascripts/ide/components/repo_editor.vue
+++ b/app/assets/javascripts/ide/components/repo_editor.vue
@@ -3,6 +3,7 @@
import { mapState, mapGetters, mapActions } from 'vuex';
import flash from '~/flash';
import ContentViewer from '~/vue_shared/components/content_viewer/content_viewer.vue';
+import { activityBarViews, viewerTypes } from '../constants';
import monacoLoader from '../monaco_loader';
import Editor from '../lib/editor';
import IdeFileButtons from './ide_file_buttons.vue';
@@ -19,8 +20,14 @@ export default {
},
},
computed: {
- ...mapState(['rightPanelCollapsed', 'viewer', 'delayViewerUpdated', 'panelResizing']),
- ...mapGetters(['currentMergeRequest', 'getStagedFile']),
+ ...mapState(['rightPanelCollapsed', 'viewer', 'panelResizing', 'currentActivityView']),
+ ...mapGetters([
+ 'currentMergeRequest',
+ 'getStagedFile',
+ 'isEditModeActive',
+ 'isCommitModeActive',
+ 'isReviewModeActive',
+ ]),
shouldHideEditor() {
return this.file && this.file.binary && !this.file.content;
},
@@ -40,6 +47,21 @@ export default {
// Compare key to allow for files opened in review mode to be cached differently
if (newVal.key !== this.file.key) {
this.initMonaco();
+
+ if (this.currentActivityView !== activityBarViews.edit) {
+ this.setFileViewMode({
+ file: this.file,
+ viewMode: 'edit',
+ });
+ }
+ }
+ },
+ currentActivityView() {
+ if (this.currentActivityView !== activityBarViews.edit) {
+ this.setFileViewMode({
+ file: this.file,
+ viewMode: 'edit',
+ });
}
},
rightPanelCollapsed() {
@@ -77,7 +99,6 @@ export default {
'setFileViewMode',
'setFileEOL',
'updateViewer',
- 'updateDelayViewerUpdated',
]),
initMonaco() {
if (this.shouldHideEditor) return;
@@ -89,14 +110,6 @@ export default {
baseSha: this.currentMergeRequest ? this.currentMergeRequest.baseCommitSha : '',
})
.then(() => {
- const viewerPromise = this.delayViewerUpdated
- ? this.updateViewer(this.file.pending ? 'diff' : 'editor')
- : Promise.resolve();
-
- return viewerPromise;
- })
- .then(() => {
- this.updateDelayViewerUpdated(false);
this.createEditorInstance();
})
.catch(err => {
@@ -108,10 +121,10 @@ export default {
this.editor.dispose();
this.$nextTick(() => {
- if (this.viewer === 'editor') {
+ if (this.viewer === viewerTypes.edit) {
this.editor.createInstance(this.$refs.editor);
} else {
- this.editor.createDiffInstance(this.$refs.editor);
+ this.editor.createDiffInstance(this.$refs.editor, !this.isReviewModeActive);
}
this.setupEditor();
@@ -127,7 +140,7 @@ export default {
this.file.staged && this.file.key.indexOf('unstaged-') === 0 ? head : null,
);
- if (this.viewer === 'mrdiff') {
+ if (this.viewer === viewerTypes.mr) {
this.editor.attachMergeRequestModel(this.model);
} else {
this.editor.attachModel(this.model);
@@ -168,6 +181,7 @@ export default {
});
},
},
+ viewerTypes,
};
</script>
@@ -176,16 +190,17 @@ export default {
id="ide"
class="blob-viewer-container blob-editor-container"
>
- <div class="ide-mode-tabs clearfix">
+ <div class="ide-mode-tabs clearfix" >
<ul
class="nav-links float-left"
- v-if="!shouldHideEditor">
+ v-if="!shouldHideEditor && isEditModeActive"
+ >
<li :class="editTabCSS">
<a
href="javascript:void(0);"
role="button"
@click.prevent="setFileViewMode({ file, viewMode: 'edit' })">
- <template v-if="viewer === 'editor'">
+ <template v-if="viewer === $options.viewerTypes.edit">
{{ __('Edit') }}
</template>
<template v-else>
@@ -212,6 +227,9 @@ export default {
v-show="!shouldHideEditor && file.viewMode === 'edit'"
ref="editor"
class="multi-file-editor-holder"
+ :class="{
+ 'is-readonly': isCommitModeActive,
+ }"
>
</div>
<content-viewer
diff --git a/app/assets/javascripts/ide/components/repo_file.vue b/app/assets/javascripts/ide/components/repo_file.vue
index 8829ba5962f..50ebad56cf0 100644
--- a/app/assets/javascripts/ide/components/repo_file.vue
+++ b/app/assets/javascripts/ide/components/repo_file.vue
@@ -1,22 +1,29 @@
<script>
-import { mapActions } from 'vuex';
-import skeletonLoadingContainer from '~/vue_shared/components/skeleton_loading_container.vue';
-import fileIcon from '~/vue_shared/components/file_icon.vue';
+import { mapActions, mapGetters } from 'vuex';
+import { n__, __, sprintf } from '~/locale';
+import tooltip from '~/vue_shared/directives/tooltip';
+import SkeletonLoadingContainer from '~/vue_shared/components/skeleton_loading_container.vue';
+import Icon from '~/vue_shared/components/icon.vue';
+import FileIcon from '~/vue_shared/components/file_icon.vue';
import router from '../ide_router';
-import newDropdown from './new_dropdown/index.vue';
-import fileStatusIcon from './repo_file_status_icon.vue';
-import changedFileIcon from './changed_file_icon.vue';
-import mrFileIcon from './mr_file_icon.vue';
+import NewDropdown from './new_dropdown/index.vue';
+import FileStatusIcon from './repo_file_status_icon.vue';
+import ChangedFileIcon from './changed_file_icon.vue';
+import MrFileIcon from './mr_file_icon.vue';
export default {
name: 'RepoFile',
+ directives: {
+ tooltip,
+ },
components: {
- skeletonLoadingContainer,
- newDropdown,
- fileStatusIcon,
- fileIcon,
- changedFileIcon,
- mrFileIcon,
+ SkeletonLoadingContainer,
+ NewDropdown,
+ FileStatusIcon,
+ FileIcon,
+ ChangedFileIcon,
+ MrFileIcon,
+ Icon,
},
props: {
file: {
@@ -27,8 +34,41 @@ export default {
type: Number,
required: true,
},
+ disableActionDropdown: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
},
computed: {
+ ...mapGetters([
+ 'getChangesInFolder',
+ 'getUnstagedFilesCountForPath',
+ 'getStagedFilesCountForPath',
+ ]),
+ folderUnstagedCount() {
+ return this.getUnstagedFilesCountForPath(this.file.path);
+ },
+ folderStagedCount() {
+ return this.getStagedFilesCountForPath(this.file.path);
+ },
+ changesCount() {
+ return this.getChangesInFolder(this.file.path);
+ },
+ folderChangesTooltip() {
+ if (this.changesCount === 0) return undefined;
+
+ if (this.folderUnstagedCount > 0 && this.folderStagedCount === 0) {
+ return n__('%d unstaged change', '%d unstaged changes', this.folderUnstagedCount);
+ } else if (this.folderUnstagedCount === 0 && this.folderStagedCount > 0) {
+ return n__('%d staged change', '%d staged changes', this.folderStagedCount);
+ }
+
+ return sprintf(__('%{unstaged} unstaged and %{staged} staged changes'), {
+ unstaged: this.folderUnstagedCount,
+ staged: this.folderStagedCount,
+ });
+ },
isTree() {
return this.file.type === 'tree';
},
@@ -48,23 +88,30 @@ export default {
'is-open': this.file.opened,
};
},
+ showTreeChangesCount() {
+ return this.isTree && this.changesCount > 0 && !this.file.opened;
+ },
+ showChangedFileIcon() {
+ return this.file.changed || this.file.tempFile || this.file.staged;
+ },
},
updated() {
if (this.file.type === 'blob' && this.file.active) {
- this.$el.scrollIntoView();
+ this.$el.scrollIntoView({
+ behavior: 'smooth',
+ block: 'nearest',
+ });
}
},
methods: {
- ...mapActions(['toggleTreeOpen', 'updateDelayViewerUpdated']),
+ ...mapActions(['toggleTreeOpen']),
clickFile() {
// Manual Action if a tree is selected/opened
if (this.isTree && this.$router.currentRoute.path === `/project${this.file.url}`) {
this.toggleTreeOpen(this.file.path);
}
- return this.updateDelayViewerUpdated(true).then(() => {
- router.push(`/project${this.file.url}`);
- });
+ router.push(`/project${this.file.url}`);
},
},
};
@@ -101,8 +148,23 @@ export default {
<mr-file-icon
v-if="file.mrChange"
/>
+ <span
+ v-if="showTreeChangesCount"
+ class="ide-tree-changes"
+ >
+ {{ changesCount }}
+ <icon
+ v-tooltip
+ :title="folderChangesTooltip"
+ data-container="body"
+ data-placement="right"
+ name="file-modified"
+ :size="12"
+ css-classes="prepend-left-5 multi-file-modified"
+ />
+ </span>
<changed-file-icon
- v-if="file.changed || file.tempFile || file.staged"
+ v-else-if="showChangedFileIcon"
:file="file"
:show-tooltip="true"
:show-staged-icon="true"
@@ -111,7 +173,7 @@ export default {
/>
</span>
<new-dropdown
- v-if="isTree"
+ v-if="isTree && !disableActionDropdown"
:project-id="file.projectId"
:branch="file.branchId"
:path="file.path"
diff --git a/app/assets/javascripts/ide/components/repo_tab.vue b/app/assets/javascripts/ide/components/repo_tab.vue
index a3ee3184c19..fb26b973236 100644
--- a/app/assets/javascripts/ide/components/repo_tab.vue
+++ b/app/assets/javascripts/ide/components/repo_tab.vue
@@ -32,6 +32,8 @@ export default {
return `Close ${this.tab.name}`;
},
showChangedIcon() {
+ if (this.tab.pending) return true;
+
return this.fileHasChanged ? !this.tabMouseOver : false;
},
fileHasChanged() {
@@ -66,15 +68,32 @@ export default {
<template>
<li
+ :class="{
+ active: tab.active
+ }"
@click="clickFile(tab)"
@mouseover="mouseOverTab"
@mouseout="mouseOutTab"
>
+ <div
+ class="multi-file-tab"
+ :title="tab.url"
+ >
+ <file-icon
+ :file-name="tab.name"
+ :size="16"
+ />
+ {{ tab.name }}
+ <file-status-icon
+ :file="tab"
+ />
+ </div>
<button
type="button"
class="multi-file-tab-close"
@click.stop.prevent="closeFile(tab)"
:aria-label="closeLabel"
+ :disabled="tab.pending"
>
<icon
v-if="!showChangedIcon"
@@ -87,22 +106,5 @@ export default {
:force-modified-icon="true"
/>
</button>
-
- <div
- class="multi-file-tab"
- :class="{
- active: tab.active
- }"
- :title="tab.url"
- >
- <file-icon
- :file-name="tab.name"
- :size="16"
- />
- {{ tab.name }}
- <file-status-icon
- :file="tab"
- />
- </div>
</li>
</template>
diff --git a/app/assets/javascripts/ide/components/repo_tabs.vue b/app/assets/javascripts/ide/components/repo_tabs.vue
index 7bd646ba9b0..99e51097e12 100644
--- a/app/assets/javascripts/ide/components/repo_tabs.vue
+++ b/app/assets/javascripts/ide/components/repo_tabs.vue
@@ -32,16 +32,6 @@ export default {
default: '',
},
},
- data() {
- return {
- showShadow: false,
- };
- },
- updated() {
- if (!this.$refs.tabsScroller) return;
-
- this.showShadow = this.$refs.tabsScroller.scrollWidth > this.$refs.tabsScroller.offsetWidth;
- },
methods: {
...mapActions(['updateViewer', 'removePendingTab']),
openFileViewer(viewer) {
@@ -71,12 +61,5 @@ export default {
:tab="tab"
/>
</ul>
- <editor-mode
- :viewer="viewer"
- :show-shadow="showShadow"
- :has-changes="hasChanges"
- :merge-request-id="mergeRequestId"
- @click="openFileViewer"
- />
</div>
</template>
diff --git a/app/assets/javascripts/ide/constants.js b/app/assets/javascripts/ide/constants.js
index b06da9f95d1..48d4cc43198 100644
--- a/app/assets/javascripts/ide/constants.js
+++ b/app/assets/javascripts/ide/constants.js
@@ -3,6 +3,22 @@ export const MAX_FILE_FINDER_RESULTS = 40;
export const FILE_FINDER_ROW_HEIGHT = 55;
export const FILE_FINDER_EMPTY_ROW_HEIGHT = 33;
+export const MAX_WINDOW_HEIGHT_COMPACT = 750;
+
+export const COMMIT_ITEM_PADDING = 32;
+
// Commit message textarea
export const MAX_TITLE_LENGTH = 50;
export const MAX_BODY_LENGTH = 72;
+
+export const activityBarViews = {
+ edit: 'ide-tree',
+ commit: 'commit-section',
+ review: 'ide-review',
+};
+
+export const viewerTypes = {
+ mr: 'mrdiff',
+ edit: 'editor',
+ diff: 'diff',
+};
diff --git a/app/assets/javascripts/ide/ide_router.js b/app/assets/javascripts/ide/ide_router.js
index 4a0a303d5a6..adca85dc65b 100644
--- a/app/assets/javascripts/ide/ide_router.js
+++ b/app/assets/javascripts/ide/ide_router.js
@@ -2,6 +2,7 @@ import Vue from 'vue';
import VueRouter from 'vue-router';
import flash from '~/flash';
import store from './stores';
+import { activityBarViews } from './constants';
Vue.use(VueRouter);
@@ -63,6 +64,8 @@ router.beforeEach((to, from, next) => {
const fullProjectId = `${to.params.namespace}/${to.params.project}`;
if (to.params.branch) {
+ store.dispatch('setCurrentBranchId', to.params.branch);
+
store.dispatch('getBranchData', {
projectId: fullProjectId,
branchId: to.params.branch,
@@ -99,14 +102,14 @@ router.beforeEach((to, from, next) => {
throw e;
});
} else if (to.params.mrid) {
- store.dispatch('updateViewer', 'mrdiff');
-
store
.dispatch('getMergeRequestData', {
projectId: fullProjectId,
mergeRequestId: to.params.mrid,
})
.then(mr => {
+ store.dispatch('updateActivityBarView', activityBarViews.review);
+
store.dispatch('getBranchData', {
projectId: fullProjectId,
branchId: mr.source_branch,
diff --git a/app/assets/javascripts/ide/index.js b/app/assets/javascripts/ide/index.js
index cbfb3dc54f2..c5835cd3b06 100644
--- a/app/assets/javascripts/ide/index.js
+++ b/app/assets/javascripts/ide/index.js
@@ -4,7 +4,9 @@ import ide from './components/ide.vue';
import store from './stores';
import router from './ide_router';
-function initIde(el) {
+Vue.use(Translate);
+
+export function initIde(el) {
if (!el) return null;
return new Vue({
@@ -14,20 +16,25 @@ function initIde(el) {
components: {
ide,
},
- render(createElement) {
- return createElement('ide', {
- props: {
- emptyStateSvgPath: el.dataset.emptyStateSvgPath,
- noChangesStateSvgPath: el.dataset.noChangesStateSvgPath,
- committedStateSvgPath: el.dataset.committedStateSvgPath,
- },
+ created() {
+ this.$store.dispatch('setEmptyStateSvgs', {
+ emptyStateSvgPath: el.dataset.emptyStateSvgPath,
+ noChangesStateSvgPath: el.dataset.noChangesStateSvgPath,
+ committedStateSvgPath: el.dataset.committedStateSvgPath,
});
},
+ render(createElement) {
+ return createElement('ide');
+ },
});
}
-const ideElement = document.getElementById('ide');
-
-Vue.use(Translate);
-
-initIde(ideElement);
+// tell webpack to load assets from origin so that web workers don't break
+export function resetServiceWorkersPublicPath() {
+ // __webpack_public_path__ is a global variable that can be used to adjust
+ // the webpack publicPath setting at runtime.
+ // see: https://webpack.js.org/guides/public-path/
+ const relativeRootPath = (gon && gon.relative_url_root) || '';
+ const webpackAssetPath = `${relativeRootPath}/assets/webpack/`;
+ __webpack_public_path__ = webpackAssetPath; // eslint-disable-line camelcase
+}
diff --git a/app/assets/javascripts/ide/lib/editor.js b/app/assets/javascripts/ide/lib/editor.js
index b65d9c68a0b..9c3bb9cc17d 100644
--- a/app/assets/javascripts/ide/lib/editor.js
+++ b/app/assets/javascripts/ide/lib/editor.js
@@ -61,19 +61,19 @@ export default class Editor {
}
}
- createDiffInstance(domElement) {
+ createDiffInstance(domElement, readOnly = true) {
if (!this.instance) {
clearDomElement(domElement);
this.disposable.add(
(this.instance = this.monaco.editor.createDiffEditor(domElement, {
...defaultEditorOptions,
- readOnly: true,
quickSuggestions: false,
occurrencesHighlight: false,
- renderLineHighlight: 'none',
- hideCursorInOverviewRuler: true,
renderSideBySide: Editor.renderSideBySide(domElement),
+ readOnly,
+ renderLineHighlight: readOnly ? 'all' : 'none',
+ hideCursorInOverviewRuler: !readOnly,
})),
);
diff --git a/app/assets/javascripts/ide/stores/actions.js b/app/assets/javascripts/ide/stores/actions.js
index 4c8c997e376..1a98b42761e 100644
--- a/app/assets/javascripts/ide/stores/actions.js
+++ b/app/assets/javascripts/ide/stores/actions.js
@@ -123,6 +123,8 @@ export const scrollToTab = () => {
};
export const stageAllChanges = ({ state, commit }) => {
+ commit(types.SET_LAST_COMMIT_MSG, '');
+
state.changedFiles.forEach(file => commit(types.STAGE_CHANGE, file.path));
};
@@ -138,6 +140,18 @@ export const updateDelayViewerUpdated = ({ commit }, delay) => {
commit(types.UPDATE_DELAY_VIEWER_CHANGE, delay);
};
+export const updateActivityBarView = ({ commit }, view) => {
+ commit(types.UPDATE_ACTIVITY_BAR_VIEW, view);
+};
+
+export const setEmptyStateSvgs = ({ commit }, svgs) => {
+ commit(types.SET_EMPTY_STATE_SVGS, svgs);
+};
+
+export const setCurrentBranchId = ({ commit }, currentBranchId) => {
+ commit(types.SET_CURRENT_BRANCH, currentBranchId);
+};
+
export const updateTempFlagForEntry = ({ commit, dispatch, state }, { file, tempFile }) => {
commit(types.UPDATE_TEMP_FLAG, { path: file.path, tempFile });
@@ -149,6 +163,12 @@ export const updateTempFlagForEntry = ({ commit, dispatch, state }, { file, temp
export const toggleFileFinder = ({ commit }, fileFindVisible) =>
commit(types.TOGGLE_FILE_FINDER, fileFindVisible);
+export const burstUnusedSeal = ({ state, commit }) => {
+ if (state.unusedSeal) {
+ commit(types.BURST_UNUSED_SEAL);
+ }
+};
+
export * from './actions/tree';
export * from './actions/file';
export * from './actions/project';
diff --git a/app/assets/javascripts/ide/stores/actions/file.js b/app/assets/javascripts/ide/stores/actions/file.js
index fcdb3b753b2..3ac9b9222ca 100644
--- a/app/assets/javascripts/ide/stores/actions/file.js
+++ b/app/assets/javascripts/ide/stores/actions/file.js
@@ -5,6 +5,7 @@ import service from '../../services';
import * as types from '../mutation_types';
import router from '../../ide_router';
import { setPageTitle } from '../utils';
+import { viewerTypes } from '../../constants';
export const closeFile = ({ commit, state, dispatch }, file) => {
const path = file.path;
@@ -23,13 +24,12 @@ export const closeFile = ({ commit, state, dispatch }, file) => {
const nextFileToOpen = state.openFiles[nextIndexToOpen];
if (nextFileToOpen.pending) {
- dispatch('updateViewer', 'diff');
+ dispatch('updateViewer', viewerTypes.diff);
dispatch('openPendingTab', {
file: nextFileToOpen,
keyPrefix: nextFileToOpen.staged ? 'staged' : 'unstaged',
});
} else {
- dispatch('updateDelayViewerUpdated', true);
router.push(`/project${nextFileToOpen.url}`);
}
} else if (!state.openFiles.length) {
@@ -117,7 +117,7 @@ export const getRawFileData = ({ state, commit, dispatch }, { path, baseSha }) =
});
};
-export const changeFileContent = ({ state, commit }, { path, content }) => {
+export const changeFileContent = ({ commit, dispatch, state }, { path, content }) => {
const file = state.entries[path];
commit(types.UPDATE_FILE_CONTENT, { path, content });
@@ -128,6 +128,8 @@ export const changeFileContent = ({ state, commit }, { path, content }) => {
} else if (!file.changed && indexOfChangedFile !== -1) {
commit(types.REMOVE_FILE_FROM_CHANGED, path);
}
+
+ dispatch('burstUnusedSeal', {}, { root: true });
};
export const setFileLanguage = ({ getters, commit }, { fileLanguage }) => {
@@ -182,6 +184,7 @@ export const stageChange = ({ commit, state }, path) => {
const stagedFile = state.stagedFiles.find(f => f.path === path);
commit(types.STAGE_CHANGE, path);
+ commit(types.SET_LAST_COMMIT_MSG, '');
if (stagedFile) {
eventHub.$emit(`editor.update.model.new.content.staged-${stagedFile.key}`, stagedFile.content);
@@ -193,9 +196,7 @@ export const unstageChange = ({ commit }, path) => {
};
export const openPendingTab = ({ commit, getters, dispatch, state }, { file, keyPrefix }) => {
- if (getters.activeFile && getters.activeFile === file && state.viewer === 'diff') {
- return false;
- }
+ state.openFiles.forEach(f => eventHub.$emit(`editor.update.model.dispose.${f.key}`));
commit(types.ADD_PENDING_TAB, { file, keyPrefix });
diff --git a/app/assets/javascripts/ide/stores/actions/project.js b/app/assets/javascripts/ide/stores/actions/project.js
index 4eb23b2ee0f..eff9bc03651 100644
--- a/app/assets/javascripts/ide/stores/actions/project.js
+++ b/app/assets/javascripts/ide/stores/actions/project.js
@@ -55,7 +55,6 @@ export const getBranchData = (
branch: data,
});
commit(types.SET_BRANCH_WORKING_REFERENCE, { projectId, branchId, reference: id });
- commit(types.SET_CURRENT_BRANCH, branchId);
resolve(data);
})
.catch(() => {
@@ -73,3 +72,26 @@ export const getBranchData = (
resolve(state.projects[`${projectId}`].branches[branchId]);
}
});
+
+export const refreshLastCommitData = (
+ { commit, state, dispatch },
+ { projectId, branchId } = {},
+) => service
+ .getBranchData(projectId, branchId)
+ .then(({ data }) => {
+ commit(types.SET_BRANCH_COMMIT, {
+ projectId,
+ branchId,
+ commit: data.commit,
+ });
+ })
+ .catch(() => {
+ flash(
+ 'Error loading last commit.',
+ 'alert',
+ document,
+ null,
+ false,
+ true,
+ );
+ });
diff --git a/app/assets/javascripts/ide/stores/getters.js b/app/assets/javascripts/ide/stores/getters.js
index ec1ea155aee..b239a605371 100644
--- a/app/assets/javascripts/ide/stores/getters.js
+++ b/app/assets/javascripts/ide/stores/getters.js
@@ -1,4 +1,5 @@
-import { __ } from '~/locale';
+import { getChangesCountForFiles, filePathMatches } from './utils';
+import { activityBarViews } from '../constants';
export const activeFile = state => state.openFiles.find(file => file.active) || null;
@@ -30,15 +31,12 @@ export const currentMergeRequest = state => {
return null;
};
-// eslint-disable-next-line no-confusing-arrow
-export const collapseButtonIcon = state =>
- state.rightPanelCollapsed ? 'angle-double-left' : 'angle-double-right';
+export const currentProject = state => state.projects[state.currentProjectId];
-export const hasChanges = state => !!state.changedFiles.length || !!state.stagedFiles.length;
+export const currentTree = state =>
+ state.trees[`${state.currentProjectId}/${state.currentBranchId}`];
-// eslint-disable-next-line no-confusing-arrow
-export const collapseButtonTooltip = state =>
- state.rightPanelCollapsed ? __('Expand sidebar') : __('Collapse sidebar');
+export const hasChanges = state => !!state.changedFiles.length || !!state.stagedFiles.length;
export const hasMergeRequest = state => !!state.currentMergeRequestId;
@@ -55,7 +53,39 @@ export const allBlobs = state =>
}, [])
.sort((a, b) => b.lastOpenedAt - a.lastOpenedAt);
+export const getChangedFile = state => path => state.changedFiles.find(f => f.path === path);
export const getStagedFile = state => path => state.stagedFiles.find(f => f.path === path);
+export const lastOpenedFile = state =>
+ [...state.changedFiles, ...state.stagedFiles].sort((a, b) => b.lastOpenedAt - a.lastOpenedAt)[0];
+
+export const isEditModeActive = state => state.currentActivityView === activityBarViews.edit;
+export const isCommitModeActive = state => state.currentActivityView === activityBarViews.commit;
+export const isReviewModeActive = state => state.currentActivityView === activityBarViews.review;
+
+export const someUncommitedChanges = state =>
+ !!(state.changedFiles.length || state.stagedFiles.length);
+
+export const getChangesInFolder = state => path => {
+ const changedFilesCount = state.changedFiles.filter(f => filePathMatches(f, path)).length;
+ const stagedFilesCount = state.stagedFiles.filter(
+ f => filePathMatches(f, path) && !getChangedFile(state)(f.path),
+ ).length;
+
+ return changedFilesCount + stagedFilesCount;
+};
+
+export const getUnstagedFilesCountForPath = state => path =>
+ getChangesCountForFiles(state.changedFiles, path);
+
+export const getStagedFilesCountForPath = state => path =>
+ getChangesCountForFiles(state.stagedFiles, path);
+
+export const lastCommit = (state, getters) => {
+ const branch = getters.currentProject && getters.currentProject.branches[state.currentBranchId];
+
+ return branch ? branch.commit : null;
+};
+
// prevent babel-plugin-rewire from generating an invalid default during karma tests
export default () => {};
diff --git a/app/assets/javascripts/ide/stores/modules/commit/actions.js b/app/assets/javascripts/ide/stores/modules/commit/actions.js
index 349ff68f1e3..b85246b2502 100644
--- a/app/assets/javascripts/ide/stores/modules/commit/actions.js
+++ b/app/assets/javascripts/ide/stores/modules/commit/actions.js
@@ -8,6 +8,7 @@ import router from '../../../ide_router';
import service from '../../../services';
import * as types from './mutation_types';
import * as consts from './constants';
+import { activityBarViews } from '../../../constants';
import eventHub from '../../../eventhub';
export const updateCommitMessage = ({ commit }, message) => {
@@ -75,7 +76,7 @@ export const checkCommitStatus = ({ rootState }) =>
export const updateFilesAfterCommit = (
{ commit, dispatch, state, rootState, rootGetters },
- { data, branch },
+ { data },
) => {
const selectedProject = rootState.projects[rootState.currentProjectId];
const lastCommit = {
@@ -126,15 +127,9 @@ export const updateFilesAfterCommit = (
changed: !!changedFile,
});
});
-
- if (state.commitAction === consts.COMMIT_TO_NEW_BRANCH && rootGetters.activeFile) {
- router.push(
- `/project/${rootState.currentProjectId}/blob/${branch}/${rootGetters.activeFile.path}`,
- );
- }
};
-export const commitChanges = ({ commit, state, getters, dispatch, rootState }) => {
+export const commitChanges = ({ commit, state, getters, dispatch, rootState, rootGetters }) => {
const newBranch = state.commitAction !== consts.COMMIT_TO_CURRENT_BRANCH;
const payload = createCommitPayload(getters.branchName, newBranch, state, rootState);
const getCommitStatus = newBranch ? Promise.resolve(false) : dispatch('checkCommitStatus');
@@ -182,8 +177,44 @@ export const commitChanges = ({ commit, state, getters, dispatch, rootState }) =
}
commit(rootTypes.CLEAR_STAGED_CHANGES, null, { root: true });
+
+ setTimeout(() => {
+ commit(rootTypes.SET_LAST_COMMIT_MSG, '', { root: true });
+ }, 5000);
+ })
+ .then(() => {
+ if (rootGetters.lastOpenedFile) {
+ dispatch(
+ 'openPendingTab',
+ {
+ file: rootGetters.lastOpenedFile,
+ },
+ { root: true },
+ )
+ .then(changeViewer => {
+ if (changeViewer) {
+ dispatch('updateViewer', 'diff', { root: true });
+ }
+ })
+ .catch(e => {
+ throw e;
+ });
+ } else {
+ dispatch('updateActivityBarView', activityBarViews.edit, { root: true });
+ dispatch('updateViewer', 'editor', { root: true });
+
+ router.push(
+ `/project/${rootState.currentProjectId}/blob/${getters.branchName}/${
+ rootGetters.activeFile.path
+ }`,
+ );
+ }
})
- .then(() => dispatch('updateCommitAction', consts.COMMIT_TO_CURRENT_BRANCH));
+ .then(() => dispatch('updateCommitAction', consts.COMMIT_TO_CURRENT_BRANCH))
+ .then(() => dispatch('refreshLastCommitData', {
+ projectId: rootState.currentProjectId,
+ branchId: rootState.currentBranchId,
+ }, { root: true }));
})
.catch(err => {
let errMsg = __('Error committing changes. Please try again.');
diff --git a/app/assets/javascripts/ide/stores/mutation_types.js b/app/assets/javascripts/ide/stores/mutation_types.js
index f5c12db6db0..a3fb3232f1d 100644
--- a/app/assets/javascripts/ide/stores/mutation_types.js
+++ b/app/assets/javascripts/ide/stores/mutation_types.js
@@ -5,6 +5,7 @@ export const SET_LAST_COMMIT_MSG = 'SET_LAST_COMMIT_MSG';
export const SET_LEFT_PANEL_COLLAPSED = 'SET_LEFT_PANEL_COLLAPSED';
export const SET_RIGHT_PANEL_COLLAPSED = 'SET_RIGHT_PANEL_COLLAPSED';
export const SET_RESIZING_STATUS = 'SET_RESIZING_STATUS';
+export const SET_EMPTY_STATE_SVGS = 'SET_EMPTY_STATE_SVGS';
// Project Mutation Types
export const SET_PROJECT = 'SET_PROJECT';
@@ -19,6 +20,7 @@ export const SET_MERGE_REQUEST_VERSIONS = 'SET_MERGE_REQUEST_VERSIONS';
// Branch Mutation Types
export const SET_BRANCH = 'SET_BRANCH';
+export const SET_BRANCH_COMMIT = 'SET_BRANCH_COMMIT';
export const SET_BRANCH_WORKING_REFERENCE = 'SET_BRANCH_WORKING_REFERENCE';
export const TOGGLE_BRANCH_OPEN = 'TOGGLE_BRANCH_OPEN';
@@ -59,5 +61,7 @@ export const UPDATE_FILE_AFTER_COMMIT = 'UPDATE_FILE_AFTER_COMMIT';
export const ADD_PENDING_TAB = 'ADD_PENDING_TAB';
export const REMOVE_PENDING_TAB = 'REMOVE_PENDING_TAB';
+export const UPDATE_ACTIVITY_BAR_VIEW = 'UPDATE_ACTIVITY_BAR_VIEW';
export const UPDATE_TEMP_FLAG = 'UPDATE_TEMP_FLAG';
export const TOGGLE_FILE_FINDER = 'TOGGLE_FILE_FINDER';
+export const BURST_UNUSED_SEAL = 'BURST_UNUSED_SEAL';
diff --git a/app/assets/javascripts/ide/stores/mutations.js b/app/assets/javascripts/ide/stores/mutations.js
index 0c1d720df09..a257e2ef025 100644
--- a/app/assets/javascripts/ide/stores/mutations.js
+++ b/app/assets/javascripts/ide/stores/mutations.js
@@ -107,6 +107,21 @@ export default {
delayViewerUpdated,
});
},
+ [types.UPDATE_ACTIVITY_BAR_VIEW](state, currentActivityView) {
+ Object.assign(state, {
+ currentActivityView,
+ });
+ },
+ [types.SET_EMPTY_STATE_SVGS](
+ state,
+ { emptyStateSvgPath, noChangesStateSvgPath, committedStateSvgPath },
+ ) {
+ Object.assign(state, {
+ emptyStateSvgPath,
+ noChangesStateSvgPath,
+ committedStateSvgPath,
+ });
+ },
[types.TOGGLE_FILE_FINDER](state, fileFindVisible) {
Object.assign(state, {
fileFindVisible,
@@ -128,6 +143,11 @@ export default {
}),
});
},
+ [types.BURST_UNUSED_SEAL](state) {
+ Object.assign(state, {
+ unusedSeal: false,
+ });
+ },
...projectMutations,
...mergeRequestMutation,
...fileMutations,
diff --git a/app/assets/javascripts/ide/stores/mutations/branch.js b/app/assets/javascripts/ide/stores/mutations/branch.js
index 2972ba5e38e..e09f88878f4 100644
--- a/app/assets/javascripts/ide/stores/mutations/branch.js
+++ b/app/assets/javascripts/ide/stores/mutations/branch.js
@@ -23,4 +23,9 @@ export default {
workingReference: reference,
});
},
+ [types.SET_BRANCH_COMMIT](state, { projectId, branchId, commit }) {
+ Object.assign(state.projects[projectId].branches[branchId], {
+ commit,
+ });
+ },
};
diff --git a/app/assets/javascripts/ide/stores/mutations/file.js b/app/assets/javascripts/ide/stores/mutations/file.js
index c3041c77199..13f123b6630 100644
--- a/app/assets/javascripts/ide/stores/mutations/file.js
+++ b/app/assets/javascripts/ide/stores/mutations/file.js
@@ -1,3 +1,4 @@
+/* eslint-disable no-param-reassign */
import * as types from '../mutation_types';
export default {
@@ -169,32 +170,24 @@ export default {
});
},
[types.ADD_PENDING_TAB](state, { file, keyPrefix = 'pending' }) {
- const key = `${keyPrefix}-${file.key}`;
- const pendingTab = state.openFiles.find(f => f.key === key && f.pending);
- let openFiles = state.openFiles.map(f => Object.assign(f, { active: false, opened: false }));
-
- if (!pendingTab) {
- const openFile = openFiles.find(f => f.path === file.path);
-
- openFiles = openFiles.concat(openFile ? null : file).reduce((acc, f) => {
- if (!f) return acc;
-
- if (f.path === file.path) {
- return acc.concat({
- ...f,
- content: file.content,
- active: true,
- pending: true,
- opened: true,
- key,
- });
- }
-
- return acc.concat(f);
- }, []);
- }
-
- Object.assign(state, { openFiles });
+ state.entries[file.path].opened = false;
+ state.entries[file.path].active = false;
+ state.entries[file.path].lastOpenedAt = new Date().getTime();
+ state.openFiles.forEach(f =>
+ Object.assign(f, {
+ opened: false,
+ active: false,
+ }),
+ );
+ state.openFiles = [
+ {
+ ...file,
+ key: `${keyPrefix}-${file.key}`,
+ pending: true,
+ opened: true,
+ active: true,
+ },
+ ];
},
[types.REMOVE_PENDING_TAB](state, file) {
Object.assign(state, {
diff --git a/app/assets/javascripts/ide/stores/state.js b/app/assets/javascripts/ide/stores/state.js
index 3470bb9aec0..e7411f16a4f 100644
--- a/app/assets/javascripts/ide/stores/state.js
+++ b/app/assets/javascripts/ide/stores/state.js
@@ -1,3 +1,5 @@
+import { activityBarViews, viewerTypes } from '../constants';
+
export default () => ({
currentProjectId: '',
currentBranchId: '',
@@ -16,7 +18,9 @@ export default () => ({
rightPanelCollapsed: false,
panelResizing: false,
entries: {},
- viewer: 'editor',
+ viewer: viewerTypes.edit,
delayViewerUpdated: false,
+ currentActivityView: activityBarViews.edit,
+ unusedSeal: true,
fileFindVisible: false,
});
diff --git a/app/assets/javascripts/ide/stores/utils.js b/app/assets/javascripts/ide/stores/utils.js
index 59185f8f0ad..bc79ff4a542 100644
--- a/app/assets/javascripts/ide/stores/utils.js
+++ b/app/assets/javascripts/ide/stores/utils.js
@@ -33,7 +33,6 @@ export const dataStructure = () => ({
raw: '',
content: '',
parentTreeUrl: '',
- parentPath: '',
renderError: false,
base64: false,
editorRow: 1,
@@ -43,6 +42,7 @@ export const dataStructure = () => ({
viewMode: 'edit',
previewMode: null,
size: 0,
+ parentPath: null,
lastOpenedAt: 0,
});
@@ -83,7 +83,6 @@ export const decorateData = entity => {
opened,
active,
parentTreeUrl,
- parentPath,
changed,
renderError,
content,
@@ -91,6 +90,7 @@ export const decorateData = entity => {
previewMode,
file_lock,
html,
+ parentPath,
};
};
@@ -137,3 +137,9 @@ export const sortTree = sortedTree =>
}),
)
.sort(sortTreesByTypeAndName);
+
+export const filePathMatches = (f, path) =>
+ f.path.replace(new RegExp(`${f.name}$`), '').indexOf(`${path}/`) === 0;
+
+export const getChangesCountForFiles = (files, path) =>
+ files.filter(f => filePathMatches(f, path)).length;
diff --git a/app/assets/javascripts/issuable_form.js b/app/assets/javascripts/issuable_form.js
index bb8b3d91e40..90d4e19e90b 100644
--- a/app/assets/javascripts/issuable_form.js
+++ b/app/assets/javascripts/issuable_form.js
@@ -30,7 +30,7 @@ export default class IssuableForm {
}
this.initAutosave();
- this.form.on('submit', this.handleSubmit);
+ this.form.on('submit:success', this.handleSubmit);
this.form.on('click', '.btn-cancel', this.resetAutosave);
this.initWip();
diff --git a/app/assets/javascripts/lib/utils/text_utility.js b/app/assets/javascripts/lib/utils/text_utility.js
index b54ecd2d543..5e786ee6935 100644
--- a/app/assets/javascripts/lib/utils/text_utility.js
+++ b/app/assets/javascripts/lib/utils/text_utility.js
@@ -74,7 +74,11 @@ export function capitalizeFirstCharacter(text) {
* @param {*} replace
* @returns {String}
*/
-export const stripHtml = (string, replace = '') => string.replace(/<[^>]*>/g, replace);
+export const stripHtml = (string, replace = '') => {
+ if (!string) return string;
+
+ return string.replace(/<[^>]*>/g, replace);
+};
/**
* Converts snake_case string to camelCase
diff --git a/app/assets/javascripts/main.js b/app/assets/javascripts/main.js
index eac7ae6017a..1308065783e 100644
--- a/app/assets/javascripts/main.js
+++ b/app/assets/javascripts/main.js
@@ -1,22 +1,19 @@
-/* eslint-disable import/first */
/* global $ */
import jQuery from 'jquery';
import Cookies from 'js-cookie';
import svg4everybody from 'svg4everybody';
-// expose common libraries as globals (TODO: remove these)
-window.jQuery = jQuery;
-window.$ = jQuery;
+// bootstrap webpack, common libs, polyfills, and behaviors
+import './webpack';
+import './commons';
+import './behaviors';
// lib/utils
import { handleLocationHash, addSelectOnFocusBehaviour } from './lib/utils/common_utils';
import { localTimeAgo } from './lib/utils/datetime_utility';
import { getLocationHash, visitUrl } from './lib/utils/url_utility';
-// behaviors
-import './behaviors/';
-
// everything else
import loadAwardsHandler from './awards_handler';
import bp from './breakpoints';
@@ -31,9 +28,12 @@ import initLogoAnimation from './logo';
import './milestone_select';
import './projects_dropdown';
import initBreadcrumbs from './breadcrumb';
-
import initDispatcher from './dispatcher';
+// expose jQuery as global (TODO: remove these)
+window.jQuery = jQuery;
+window.$ = jQuery;
+
// inject test utilities if necessary
if (process.env.NODE_ENV !== 'production' && gon && gon.test_env) {
$.fx.off = true;
@@ -52,10 +52,14 @@ document.addEventListener('beforeunload', () => {
});
window.addEventListener('hashchange', handleLocationHash);
-window.addEventListener('load', function onLoad() {
- window.removeEventListener('load', onLoad, false);
- handleLocationHash();
-}, false);
+window.addEventListener(
+ 'load',
+ function onLoad() {
+ window.removeEventListener('load', onLoad, false);
+ handleLocationHash();
+ },
+ false,
+);
gl.lazyLoader = new LazyLoader({
scrollContainer: window,
@@ -89,9 +93,7 @@ document.addEventListener('DOMContentLoaded', () => {
if (bootstrapBreakpoint === 'xs') {
const $rightSidebar = $('aside.right-sidebar, .layout-page');
- $rightSidebar
- .removeClass('right-sidebar-expanded')
- .addClass('right-sidebar-collapsed');
+ $rightSidebar.removeClass('right-sidebar-expanded').addClass('right-sidebar-collapsed');
}
// prevent default action for disabled buttons
@@ -108,7 +110,8 @@ document.addEventListener('DOMContentLoaded', () => {
addSelectOnFocusBehaviour('.js-select-on-focus');
$('.remove-row').on('ajax:success', function removeRowAjaxSuccessCallback() {
- $(this).tooltip('dispose')
+ $(this)
+ .tooltip('dispose')
.closest('li')
.fadeOut();
});
@@ -118,7 +121,9 @@ document.addEventListener('DOMContentLoaded', () => {
});
$('.js-remove-tr').on('ajax:success', function removeTRAjaxSuccessCallback() {
- $(this).closest('tr').fadeOut();
+ $(this)
+ .closest('tr')
+ .fadeOut();
});
// Initialize select2 selects
@@ -154,7 +159,9 @@ document.addEventListener('DOMContentLoaded', () => {
// Form submitter
$('.trigger-submit').on('change', function triggerSubmitCallback() {
- $(this).parents('form').submit();
+ $(this)
+ .parents('form')
+ .submit();
});
localTimeAgo($('abbr.timeago, .js-timeago'), true);
@@ -203,9 +210,15 @@ document.addEventListener('DOMContentLoaded', () => {
$this.toggleClass('active');
if ($this.hasClass('active')) {
- notesHolders.show().find('.hide, .content').show();
+ notesHolders
+ .show()
+ .find('.hide, .content')
+ .show();
} else {
- notesHolders.hide().find('.content').hide();
+ notesHolders
+ .hide()
+ .find('.content')
+ .hide();
}
$(document).trigger('toggle.comments');
@@ -246,9 +259,11 @@ document.addEventListener('DOMContentLoaded', () => {
const flashContainer = document.querySelector('.flash-container');
if (flashContainer && flashContainer.children.length) {
- flashContainer.querySelectorAll('.flash-alert, .flash-notice, .flash-success').forEach((flashEl) => {
- removeFlashClickListener(flashEl);
- });
+ flashContainer
+ .querySelectorAll('.flash-alert, .flash-notice, .flash-success')
+ .forEach(flashEl => {
+ removeFlashClickListener(flashEl);
+ });
}
initDispatcher();
diff --git a/app/assets/javascripts/mini_pipeline_graph_dropdown.js b/app/assets/javascripts/mini_pipeline_graph_dropdown.js
index 01399de4c62..f8257b6abab 100644
--- a/app/assets/javascripts/mini_pipeline_graph_dropdown.js
+++ b/app/assets/javascripts/mini_pipeline_graph_dropdown.js
@@ -1,5 +1,3 @@
-/* eslint-disable no-new */
-
import $ from 'jquery';
import flash from './flash';
import axios from './lib/utils/axios_utils';
@@ -62,7 +60,7 @@ export default class MiniPipelineGraph {
*/
renderBuildsList(stageContainer, data) {
const dropdownContainer = stageContainer.parentElement.querySelector(
- `${this.dropdownListSelector} .js-builds-dropdown-list`,
+ `${this.dropdownListSelector} .js-builds-dropdown-list ul`,
);
dropdownContainer.innerHTML = data;
diff --git a/app/assets/javascripts/monitoring/components/graph.vue b/app/assets/javascripts/monitoring/components/graph.vue
index f93b1da4f58..de6755e0414 100644
--- a/app/assets/javascripts/monitoring/components/graph.vue
+++ b/app/assets/javascripts/monitoring/components/graph.vue
@@ -81,9 +81,8 @@ export default {
time: new Date(),
value: 0,
},
- currentDataIndex: 0,
currentXCoordinate: 0,
- currentFlagPosition: 0,
+ currentCoordinates: [],
showFlag: false,
showFlagContent: false,
timeSeries: [],
@@ -273,6 +272,9 @@ export default {
:line-style="path.lineStyle"
:line-color="path.lineColor"
:area-color="path.areaColor"
+ :current-coordinates="currentCoordinates[index]"
+ :current-time-series-index="index"
+ :show-dot="showFlagContent"
/>
<graph-deployment
:deployment-data="reducedDeploymentData"
@@ -298,9 +300,9 @@ export default {
:show-flag-content="showFlagContent"
:time-series="timeSeries"
:unit-of-display="unitOfDisplay"
- :current-data-index="currentDataIndex"
:legend-title="legendTitle"
:deployment-flag-data="deploymentFlagData"
+ :current-coordinates="currentCoordinates"
/>
</div>
<graph-legend
diff --git a/app/assets/javascripts/monitoring/components/graph/flag.vue b/app/assets/javascripts/monitoring/components/graph/flag.vue
index b8202e25685..8a771107de8 100644
--- a/app/assets/javascripts/monitoring/components/graph/flag.vue
+++ b/app/assets/javascripts/monitoring/components/graph/flag.vue
@@ -47,14 +47,14 @@ export default {
type: String,
required: true,
},
- currentDataIndex: {
- type: Number,
- required: true,
- },
legendTitle: {
type: String,
required: true,
},
+ currentCoordinates: {
+ type: Array,
+ required: true,
+ },
},
computed: {
formatTime() {
@@ -90,10 +90,12 @@ export default {
},
},
methods: {
- seriesMetricValue(series) {
+ seriesMetricValue(seriesIndex, series) {
+ const indexFromCoordinates = this.currentCoordinates[seriesIndex]
+ ? this.currentCoordinates[seriesIndex].currentDataIndex : 0;
const index = this.deploymentFlagData
? this.deploymentFlagData.seriesIndex
- : this.currentDataIndex;
+ : indexFromCoordinates;
const value = series.values[index] && series.values[index].value;
if (isNaN(value)) {
return '-';
@@ -128,7 +130,7 @@ export default {
<h5 v-if="deploymentFlagData">
Deployed
</h5>
- {{ formatDate }} at
+ {{ formatDate }}
<strong>{{ formatTime }}</strong>
</div>
<div
@@ -163,9 +165,11 @@ export default {
:key="index"
>
<track-line :track="series"/>
- <td>{{ series.track }} {{ seriesMetricLabel(index, series) }}</td>
<td>
- <strong>{{ seriesMetricValue(series) }}</strong>
+ {{ series.track }} {{ seriesMetricLabel(index, series) }}
+ </td>
+ <td>
+ <strong>{{ seriesMetricValue(index, series) }}</strong>
</td>
</tr>
</table>
diff --git a/app/assets/javascripts/monitoring/components/graph/path.vue b/app/assets/javascripts/monitoring/components/graph/path.vue
index 881560124a5..52f8aa2ee3f 100644
--- a/app/assets/javascripts/monitoring/components/graph/path.vue
+++ b/app/assets/javascripts/monitoring/components/graph/path.vue
@@ -22,6 +22,15 @@ export default {
type: String,
required: true,
},
+ currentCoordinates: {
+ type: Object,
+ required: false,
+ default: () => ({ currentX: 0, currentY: 0 }),
+ },
+ showDot: {
+ type: Boolean,
+ required: true,
+ },
},
computed: {
strokeDashArray() {
@@ -33,12 +42,20 @@ export default {
};
</script>
<template>
- <g>
+ <g transform="translate(-5, 20)">
+ <circle
+ class="circle-path"
+ :cx="currentCoordinates.currentX"
+ :cy="currentCoordinates.currentY"
+ :fill="lineColor"
+ :stroke="lineColor"
+ r="3"
+ v-if="showDot"
+ />
<path
class="metric-area"
:d="generatedAreaPath"
:fill="areaColor"
- transform="translate(-5, 20)"
/>
<path
class="metric-line"
@@ -47,7 +64,6 @@ export default {
fill="none"
stroke-width="1"
:stroke-dasharray="strokeDashArray"
- transform="translate(-5, 20)"
/>
</g>
</template>
diff --git a/app/assets/javascripts/monitoring/components/graph/track_line.vue b/app/assets/javascripts/monitoring/components/graph/track_line.vue
index 79b322e2e42..18be65fd1ef 100644
--- a/app/assets/javascripts/monitoring/components/graph/track_line.vue
+++ b/app/assets/javascripts/monitoring/components/graph/track_line.vue
@@ -19,16 +19,16 @@ export default {
<template>
<td>
<svg
- width="15"
- height="6">
+ width="16"
+ height="8">
<line
:stroke-dasharray="stylizedLine"
:stroke="track.lineColor"
stroke-width="4"
:x1="0"
- :x2="15"
- :y1="2"
- :y2="2"
+ :x2="16"
+ :y1="4"
+ :y2="4"
/>
</svg>
</td>
diff --git a/app/assets/javascripts/monitoring/mixins/monitoring_mixins.js b/app/assets/javascripts/monitoring/mixins/monitoring_mixins.js
index 6cc67ba57ee..4f23814ff3e 100644
--- a/app/assets/javascripts/monitoring/mixins/monitoring_mixins.js
+++ b/app/assets/javascripts/monitoring/mixins/monitoring_mixins.js
@@ -52,14 +52,22 @@ const mixins = {
positionFlag() {
const timeSeries = this.timeSeries[0];
const hoveredDataIndex = bisectDate(timeSeries.values, this.hoverData.hoveredDate, 1);
+
this.currentData = timeSeries.values[hoveredDataIndex];
- this.currentDataIndex = hoveredDataIndex;
this.currentXCoordinate = Math.floor(timeSeries.timeSeriesScaleX(this.currentData.time));
- if (this.currentXCoordinate > (this.graphWidth - 200)) {
- this.currentFlagPosition = this.currentXCoordinate - 103;
- } else {
- this.currentFlagPosition = this.currentXCoordinate;
- }
+
+ this.currentCoordinates = this.timeSeries.map((series) => {
+ const currentDataIndex = bisectDate(series.values, this.hoverData.hoveredDate, 1);
+ const currentData = series.values[currentDataIndex];
+ const currentX = Math.floor(series.timeSeriesScaleX(currentData.time));
+ const currentY = Math.floor(series.timeSeriesScaleY(currentData.value));
+
+ return {
+ currentX,
+ currentY,
+ currentDataIndex,
+ };
+ });
if (this.hoverData.currentDeployXPos) {
this.showFlag = false;
diff --git a/app/assets/javascripts/monitoring/utils/date_time_formatters.js b/app/assets/javascripts/monitoring/utils/date_time_formatters.js
index f3c9acdd93e..d88c13609dc 100644
--- a/app/assets/javascripts/monitoring/utils/date_time_formatters.js
+++ b/app/assets/javascripts/monitoring/utils/date_time_formatters.js
@@ -14,7 +14,7 @@ const d3 = {
timeYear,
};
-export const dateFormat = d3.time('%a, %b %-d');
+export const dateFormat = d3.time('%d %b %Y, ');
export const timeFormat = d3.time('%-I:%M%p');
export const dateFormatWithName = d3.time('%a, %b %-d');
export const bisectDate = d3.bisector(d => d.time).left;
diff --git a/app/assets/javascripts/monitoring/utils/multiple_time_series.js b/app/assets/javascripts/monitoring/utils/multiple_time_series.js
index 8a93c7e6bae..4d3f1f1a7cc 100644
--- a/app/assets/javascripts/monitoring/utils/multiple_time_series.js
+++ b/app/assets/javascripts/monitoring/utils/multiple_time_series.js
@@ -123,6 +123,7 @@ function queryTimeSeries(query, graphWidth, graphHeight, graphHeightOffset, xDom
linePath: lineFunction(timeSeries.values),
areaPath: areaFunction(timeSeries.values),
timeSeriesScaleX,
+ timeSeriesScaleY,
values: timeSeries.values,
max: maximumValue,
average: accum / timeSeries.values.length,
diff --git a/app/assets/javascripts/notes/components/comment_form.vue b/app/assets/javascripts/notes/components/comment_form.vue
index e0acb8f4794..72d7e22fba0 100644
--- a/app/assets/javascripts/notes/components/comment_form.vue
+++ b/app/assets/javascripts/notes/components/comment_form.vue
@@ -99,10 +99,6 @@ export default {
'js-note-target-reopen': !this.isOpen,
};
},
- supportQuickActions() {
- // Disable quick actions support for Epics
- return this.noteableType !== constants.EPIC_NOTEABLE_TYPE;
- },
markdownDocsPath() {
return this.getNotesData.markdownDocsPath;
},
@@ -359,7 +355,7 @@ Please check your network connection and try again.`;
name="note[note]"
class="note-textarea js-vue-comment-form
js-gfm-input js-autosize markdown-area js-vue-textarea"
- :data-supports-quick-actions="supportQuickActions"
+ data-supports-quick-actions="true"
aria-label="Description"
v-model="note"
ref="textarea"
diff --git a/app/assets/javascripts/pages/groups/settings/ci_cd/show/index.js b/app/assets/javascripts/pages/groups/settings/ci_cd/show/index.js
index 04a0d8117cc..d3b2656743d 100644
--- a/app/assets/javascripts/pages/groups/settings/ci_cd/show/index.js
+++ b/app/assets/javascripts/pages/groups/settings/ci_cd/show/index.js
@@ -1,6 +1,10 @@
+import initSettingsPanels from '~/settings_panels';
import AjaxVariableList from '~/ci_variable_list/ajax_variable_list';
document.addEventListener('DOMContentLoaded', () => {
+ // Initialize expandable settings panels
+ initSettingsPanels();
+
const variableListEl = document.querySelector('.js-ci-variable-list-section');
// eslint-disable-next-line no-new
new AjaxVariableList({
diff --git a/app/assets/javascripts/pages/ide/index.js b/app/assets/javascripts/pages/ide/index.js
new file mode 100644
index 00000000000..efadf6967aa
--- /dev/null
+++ b/app/assets/javascripts/pages/ide/index.js
@@ -0,0 +1,9 @@
+import { initIde, resetServiceWorkersPublicPath } from '~/ide/index';
+
+document.addEventListener('DOMContentLoaded', () => {
+ const ideElement = document.getElementById('ide');
+ if (ideElement) {
+ resetServiceWorkersPublicPath();
+ initIde(ideElement);
+ }
+});
diff --git a/app/assets/javascripts/pages/projects/clusters/gcp/login/index.js b/app/assets/javascripts/pages/projects/clusters/gcp/login/index.js
new file mode 100644
index 00000000000..0c2d7d7c96a
--- /dev/null
+++ b/app/assets/javascripts/pages/projects/clusters/gcp/login/index.js
@@ -0,0 +1,3 @@
+import gcpSignupOffer from '~/clusters/components/gcp_signup_offer';
+
+gcpSignupOffer();
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..0c2d7d7c96a
--- /dev/null
+++ b/app/assets/javascripts/pages/projects/clusters/new/index.js
@@ -0,0 +1,3 @@
+import gcpSignupOffer from '~/clusters/components/gcp_signup_offer';
+
+gcpSignupOffer();
diff --git a/app/assets/javascripts/pages/projects/compare/index.js b/app/assets/javascripts/pages/projects/compare/index.js
index d1c78bd61db..768da8fb236 100644
--- a/app/assets/javascripts/pages/projects/compare/index.js
+++ b/app/assets/javascripts/pages/projects/compare/index.js
@@ -1,3 +1,3 @@
import initCompareAutocomplete from '~/compare_autocomplete';
-document.addEventListener('DOMContentLoaded', initCompareAutocomplete);
+document.addEventListener('DOMContentLoaded', () => initCompareAutocomplete());
diff --git a/app/assets/javascripts/pages/projects/compare/show/index.js b/app/assets/javascripts/pages/projects/compare/show/index.js
index 2b4fd3c47c0..a626ed2d30b 100644
--- a/app/assets/javascripts/pages/projects/compare/show/index.js
+++ b/app/assets/javascripts/pages/projects/compare/show/index.js
@@ -1,8 +1,10 @@
import Diff from '~/diff';
import initChangesDropdown from '~/init_changes_dropdown';
+import GpgBadges from '~/gpg_badges';
document.addEventListener('DOMContentLoaded', () => {
new Diff(); // eslint-disable-line no-new
const paddingTop = 16;
initChangesDropdown(document.querySelector('.navbar-gitlab').offsetHeight - paddingTop);
+ GpgBadges.fetch();
});
diff --git a/app/assets/javascripts/pages/projects/merge_requests/creations/new/compare.js b/app/assets/javascripts/pages/projects/merge_requests/creations/new/compare.js
new file mode 100644
index 00000000000..46f3f55a400
--- /dev/null
+++ b/app/assets/javascripts/pages/projects/merge_requests/creations/new/compare.js
@@ -0,0 +1,60 @@
+import $ from 'jquery';
+import { localTimeAgo } from '~/lib/utils/datetime_utility';
+import axios from '~/lib/utils/axios_utils';
+import initCompareAutocomplete from '~/compare_autocomplete';
+import initTargetProjectDropdown from './target_project_dropdown';
+
+const updateCommitList = (url, $loadingIndicator, $commitList, params) => {
+ $loadingIndicator.show();
+ $commitList.empty();
+
+ return axios
+ .get(url, {
+ params,
+ })
+ .then(({ data }) => {
+ $loadingIndicator.hide();
+ $commitList.html(data);
+ localTimeAgo($('.js-timeago', $commitList));
+ });
+};
+
+export default mrNewCompareNode => {
+ const { sourceBranchUrl, targetBranchUrl } = mrNewCompareNode.dataset;
+ initTargetProjectDropdown();
+
+ const updateSourceBranchCommitList = () =>
+ updateCommitList(
+ sourceBranchUrl,
+ $(mrNewCompareNode).find('.js-source-loading'),
+ $(mrNewCompareNode).find('.mr_source_commit'),
+ {
+ ref: $(mrNewCompareNode)
+ .find("input[name='merge_request[source_branch]']")
+ .val(),
+ },
+ );
+ const updateTargetBranchCommitList = () =>
+ updateCommitList(
+ targetBranchUrl,
+ $(mrNewCompareNode).find('.js-target-loading'),
+ $(mrNewCompareNode).find('.mr_target_commit'),
+ {
+ target_project_id: $(mrNewCompareNode)
+ .find("input[name='merge_request[target_project_id]']")
+ .val(),
+ ref: $(mrNewCompareNode)
+ .find("input[name='merge_request[target_branch]']")
+ .val(),
+ },
+ );
+ initCompareAutocomplete('branches', $dropdown => {
+ if ($dropdown.is('.js-target-branch')) {
+ updateTargetBranchCommitList();
+ } else if ($dropdown.is('.js-source-branch')) {
+ updateSourceBranchCommitList();
+ }
+ });
+ updateSourceBranchCommitList();
+ updateTargetBranchCommitList();
+};
diff --git a/app/assets/javascripts/pages/projects/merge_requests/creations/new/index.js b/app/assets/javascripts/pages/projects/merge_requests/creations/new/index.js
index 6c9afddefac..01a0b4870c1 100644
--- a/app/assets/javascripts/pages/projects/merge_requests/creations/new/index.js
+++ b/app/assets/javascripts/pages/projects/merge_requests/creations/new/index.js
@@ -1,18 +1,15 @@
-import Compare from '~/compare';
import MergeRequest from '~/merge_request';
import initPipelines from '~/commit/pipelines/pipelines_bundle';
+import initCompare from './compare';
document.addEventListener('DOMContentLoaded', () => {
const mrNewCompareNode = document.querySelector('.js-merge-request-new-compare');
if (mrNewCompareNode) {
- new Compare({ // eslint-disable-line no-new
- targetProjectUrl: mrNewCompareNode.dataset.targetProjectUrl,
- sourceBranchUrl: mrNewCompareNode.dataset.sourceBranchUrl,
- targetBranchUrl: mrNewCompareNode.dataset.targetBranchUrl,
- });
+ initCompare(mrNewCompareNode);
} else {
const mrNewSubmitNode = document.querySelector('.js-merge-request-new-submit');
- new MergeRequest({ // eslint-disable-line no-new
+ // eslint-disable-next-line no-new
+ new MergeRequest({
action: mrNewSubmitNode.dataset.mrSubmitAction,
});
initPipelines();
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
new file mode 100644
index 00000000000..b72fe6681df
--- /dev/null
+++ b/app/assets/javascripts/pages/projects/merge_requests/creations/new/target_project_dropdown.js
@@ -0,0 +1,22 @@
+import $ from 'jquery';
+
+export default () => {
+ const $targetProjectDropdown = $('.js-target-project');
+ $targetProjectDropdown.glDropdown({
+ selectable: true,
+ fieldName: $targetProjectDropdown.data('fieldName'),
+ filterable: true,
+ id(obj, $el) {
+ return $el.data('id');
+ },
+ toggleLabel(obj, $el) {
+ return $el.text().trim();
+ },
+ clicked({ $el }) {
+ $('.mr_target_commit').empty();
+ const $targetBranchDropdown = $('.js-target-branch');
+ $targetBranchDropdown.data('refsUrl', $el.data('refsUrl'));
+ $targetBranchDropdown.data('glDropdown').clearMenu();
+ },
+ });
+};
diff --git a/app/assets/javascripts/pages/projects/pipelines/new/index.js b/app/assets/javascripts/pages/projects/pipelines/new/index.js
index 9aa8945e268..b0b077a5e4c 100644
--- a/app/assets/javascripts/pages/projects/pipelines/new/index.js
+++ b/app/assets/javascripts/pages/projects/pipelines/new/index.js
@@ -1,6 +1,12 @@
import $ from 'jquery';
import NewBranchForm from '~/new_branch_form';
+import setupNativeFormVariableList from '~/ci_variable_list/native_form_variable_list';
document.addEventListener('DOMContentLoaded', () => {
new NewBranchForm($('.js-new-pipeline-form')); // eslint-disable-line no-new
+
+ setupNativeFormVariableList({
+ container: $('.js-ci-variable-list-section'),
+ formField: 'variables_attributes',
+ });
});
diff --git a/app/assets/javascripts/pipelines/components/graph/action_component.vue b/app/assets/javascripts/pipelines/components/graph/action_component.vue
index 29ee73a2a6f..fd3491c7fe0 100644
--- a/app/assets/javascripts/pipelines/components/graph/action_component.vue
+++ b/app/assets/javascripts/pipelines/components/graph/action_component.vue
@@ -61,7 +61,7 @@ export default {
methods: {
onClickAction() {
$(this.$el).tooltip('hide');
- eventHub.$emit('graphAction', this.link);
+ eventHub.$emit('postAction', this.link);
this.linkRequested = this.link;
this.isDisabled = true;
},
diff --git a/app/assets/javascripts/pipelines/components/graph/dropdown_job_component.vue b/app/assets/javascripts/pipelines/components/graph/dropdown_job_component.vue
index 43121dd38f3..4027d26098f 100644
--- a/app/assets/javascripts/pipelines/components/graph/dropdown_job_component.vue
+++ b/app/assets/javascripts/pipelines/components/graph/dropdown_job_component.vue
@@ -87,7 +87,8 @@ export default {
data-toggle="dropdown"
data-container="body"
class="dropdown-menu-toggle build-content"
- :title="tooltipText">
+ :title="tooltipText"
+ >
<job-name-component
:name="job.name"
@@ -104,7 +105,8 @@ export default {
<ul>
<li
v-for="(item, i) in job.jobs"
- :key="i">
+ :key="i"
+ >
<job-component
:job="item"
css-class-job-name="mini-pipeline-graph-dropdown-item"
diff --git a/app/assets/javascripts/pipelines/components/graph/job_component.vue b/app/assets/javascripts/pipelines/components/graph/job_component.vue
index 4fcd4b79f4a..c1f0f051b63 100644
--- a/app/assets/javascripts/pipelines/components/graph/job_component.vue
+++ b/app/assets/javascripts/pipelines/components/graph/job_component.vue
@@ -108,7 +108,7 @@ export default {
<div
v-else
v-tooltip
- class="js-job-component-tooltip"
+ class="js-job-component-tooltip non-details-job-component"
:title="tooltipText"
:class="cssClassJobName"
data-html="true"
diff --git a/app/assets/javascripts/pipelines/components/stage.vue b/app/assets/javascripts/pipelines/components/stage.vue
index 32cf3dba3c3..a65485c05eb 100644
--- a/app/assets/javascripts/pipelines/components/stage.vue
+++ b/app/assets/javascripts/pipelines/components/stage.vue
@@ -1,135 +1,140 @@
<script>
-
- /**
- * Renders each stage of the pipeline mini graph.
- *
- * Given the provided endpoint will make a request to
- * fetch the dropdown data when the stage is clicked.
- *
- * Request is made inside this component to make it reusable between:
- * 1. Pipelines main table
- * 2. Pipelines table in commit and Merge request views
- * 3. Merge request widget
- * 4. Commit widget
- */
-
- import $ from 'jquery';
- import Flash from '../../flash';
- import axios from '../../lib/utils/axios_utils';
- import eventHub from '../event_hub';
- import Icon from '../../vue_shared/components/icon.vue';
- import LoadingIcon from '../../vue_shared/components/loading_icon.vue';
- import tooltip from '../../vue_shared/directives/tooltip';
-
- export default {
- components: {
- LoadingIcon,
- Icon,
+/**
+ * Renders each stage of the pipeline mini graph.
+ *
+ * Given the provided endpoint will make a request to
+ * fetch the dropdown data when the stage is clicked.
+ *
+ * Request is made inside this component to make it reusable between:
+ * 1. Pipelines main table
+ * 2. Pipelines table in commit and Merge request views
+ * 3. Merge request widget
+ * 4. Commit widget
+ */
+
+import $ from 'jquery';
+import { __ } from '../../locale';
+import Flash from '../../flash';
+import axios from '../../lib/utils/axios_utils';
+import eventHub from '../event_hub';
+import Icon from '../../vue_shared/components/icon.vue';
+import LoadingIcon from '../../vue_shared/components/loading_icon.vue';
+import JobComponent from './graph/job_component.vue';
+import tooltip from '../../vue_shared/directives/tooltip';
+
+export default {
+ components: {
+ LoadingIcon,
+ Icon,
+ JobComponent,
+ },
+
+ directives: {
+ tooltip,
+ },
+
+ props: {
+ stage: {
+ type: Object,
+ required: true,
},
- directives: {
- tooltip,
+ updateDropdown: {
+ type: Boolean,
+ required: false,
+ default: false,
},
-
- props: {
- stage: {
- type: Object,
- required: true,
- },
-
- updateDropdown: {
- type: Boolean,
- required: false,
- default: false,
- },
+ },
+
+ data() {
+ return {
+ isLoading: false,
+ dropdownContent: '',
+ };
+ },
+
+ computed: {
+ dropdownClass() {
+ return this.dropdownContent.length > 0
+ ? 'js-builds-dropdown-container'
+ : 'js-builds-dropdown-loading';
},
- data() {
- return {
- isLoading: false,
- dropdownContent: '',
- };
+ triggerButtonClass() {
+ return `ci-status-icon-${this.stage.status.group}`;
},
- computed: {
- dropdownClass() {
- return this.dropdownContent.length > 0 ? 'js-builds-dropdown-container' : 'js-builds-dropdown-loading';
- },
+ borderlessIcon() {
+ return `${this.stage.status.icon}_borderless`;
+ },
+ },
- triggerButtonClass() {
- return `ci-status-icon-${this.stage.status.group}`;
- },
+ watch: {
+ updateDropdown() {
+ if (this.updateDropdown && this.isDropdownOpen() && !this.isLoading) {
+ this.fetchJobs();
+ }
+ },
+ },
+
+ updated() {
+ if (this.dropdownContent.length > 0) {
+ this.stopDropdownClickPropagation();
+ }
+ },
+
+ methods: {
+ onClickStage() {
+ if (!this.isDropdownOpen()) {
+ eventHub.$emit('clickedDropdown');
+ this.isLoading = true;
+ this.fetchJobs();
+ }
+ },
- borderlessIcon() {
- return `${this.stage.status.icon}_borderless`;
- },
+ fetchJobs() {
+ axios
+ .get(this.stage.dropdown_path)
+ .then(({ data }) => {
+ this.dropdownContent = data.latest_statuses;
+ this.isLoading = false;
+ })
+ .catch(() => {
+ this.closeDropdown();
+ this.isLoading = false;
+
+ Flash(__('Something went wrong on our end.'));
+ });
},
- watch: {
- updateDropdown() {
- if (this.updateDropdown &&
- this.isDropdownOpen() &&
- !this.isLoading) {
- this.fetchJobs();
- }
- },
+ /**
+ * When the user right clicks or cmd/ctrl + click in the job name
+ * the dropdown should not be closed and the link should open in another tab,
+ * so we stop propagation of the click event inside the dropdown.
+ *
+ * Since this component is rendered multiple times per page we need to guarantee we only
+ * target the click event of this component.
+ */
+ stopDropdownClickPropagation() {
+ $(
+ '.js-builds-dropdown-list button, .js-builds-dropdown-list a.mini-pipeline-graph-dropdown-item',
+ this.$el,
+ ).on('click', e => {
+ e.stopPropagation();
+ });
},
- updated() {
- if (this.dropdownContent.length > 0) {
- this.stopDropdownClickPropagation();
+ closeDropdown() {
+ if (this.isDropdownOpen()) {
+ $(this.$refs.dropdown).dropdown('toggle');
}
},
- methods: {
- onClickStage() {
- if (!this.isDropdownOpen()) {
- eventHub.$emit('clickedDropdown');
- this.isLoading = true;
- this.fetchJobs();
- }
- },
-
- fetchJobs() {
- axios.get(this.stage.dropdown_path)
- .then(({ data }) => {
- this.dropdownContent = data.html;
- this.isLoading = false;
- })
- .catch(() => {
- this.closeDropdown();
- this.isLoading = false;
-
- Flash('Something went wrong on our end.');
- });
- },
-
- /**
- * When the user right clicks or cmd/ctrl + click in the job name
- * the dropdown should not be closed and the link should open in another tab,
- * so we stop propagation of the click event inside the dropdown.
- *
- * Since this component is rendered multiple times per page we need to guarantee we only
- * target the click event of this component.
- */
- stopDropdownClickPropagation() {
- $(this.$el.querySelectorAll('.js-builds-dropdown-list a.mini-pipeline-graph-dropdown-item'))
- .on('click', (e) => {
- e.stopPropagation();
- });
- },
-
- closeDropdown() {
- if (this.isDropdownOpen()) {
- $(this.$refs.dropdown).dropdown('toggle');
- }
- },
-
- isDropdownOpen() {
- return this.$el.classList.contains('open');
- },
+ isDropdownOpen() {
+ return this.$el.classList.contains('open');
},
- };
+ },
+};
</script>
<template>
@@ -168,7 +173,6 @@
>
<li
- :class="dropdownClass"
class="js-builds-dropdown-list scrollable-menu"
>
@@ -176,8 +180,16 @@
<ul
v-else
- v-html="dropdownContent"
>
+ <li
+ v-for="job in dropdownContent"
+ :key="job.id"
+ >
+ <job-component
+ :job="job"
+ css-class-job-name="mini-pipeline-graph-dropdown-item"
+ />
+ </li>
</ul>
</li>
</ul>
diff --git a/app/assets/javascripts/pipelines/pipeline_details_bundle.js b/app/assets/javascripts/pipelines/pipeline_details_bundle.js
index 6584f96130b..04fe7958fe6 100644
--- a/app/assets/javascripts/pipelines/pipeline_details_bundle.js
+++ b/app/assets/javascripts/pipelines/pipeline_details_bundle.js
@@ -29,10 +29,10 @@ export default () => {
};
},
created() {
- eventHub.$on('graphAction', this.postAction);
+ eventHub.$on('postAction', this.postAction);
},
beforeDestroy() {
- eventHub.$off('graphAction', this.postAction);
+ eventHub.$off('postAction', this.postAction);
},
methods: {
postAction(action) {
diff --git a/app/assets/javascripts/projects_dropdown/components/app.vue b/app/assets/javascripts/projects_dropdown/components/app.vue
index 34a60dd574b..0bbd8a41753 100644
--- a/app/assets/javascripts/projects_dropdown/components/app.vue
+++ b/app/assets/javascripts/projects_dropdown/components/app.vue
@@ -100,9 +100,10 @@ export default {
fetchSearchedProjects(searchQuery) {
this.searchQuery = searchQuery;
this.toggleLoader(true);
- this.service.getSearchedProjects(this.searchQuery)
+ this.service
+ .getSearchedProjects(this.searchQuery)
.then(res => res.json())
- .then((results) => {
+ .then(results => {
this.toggleSearchProjectsList(true);
this.store.setSearchedProjects(results);
})
diff --git a/app/assets/javascripts/projects_dropdown/service/projects_service.js b/app/assets/javascripts/projects_dropdown/service/projects_service.js
index 7231f520933..ed1c3deead2 100644
--- a/app/assets/javascripts/projects_dropdown/service/projects_service.js
+++ b/app/assets/javascripts/projects_dropdown/service/projects_service.js
@@ -50,7 +50,7 @@ export default class ProjectsService {
} else {
// Check if project is already present in frequents list
// When found, update metadata of it.
- storedFrequentProjects = JSON.parse(storedRawProjects).map((projectItem) => {
+ storedFrequentProjects = JSON.parse(storedRawProjects).map(projectItem => {
if (projectItem.id === project.id) {
matchFound = true;
const diff = Math.abs(project.lastAccessedOn - projectItem.lastAccessedOn) / HOUR_IN_MS;
@@ -104,13 +104,17 @@ export default class ProjectsService {
return [];
}
- if (bp.getBreakpointSize() === 'sm' ||
- bp.getBreakpointSize() === 'xs') {
+ if (bp.getBreakpointSize() === 'sm' || bp.getBreakpointSize() === 'xs') {
frequentProjectsCount = FREQUENT_PROJECTS.LIST_COUNT_MOBILE;
}
- const frequentProjects = storedFrequentProjects
- .filter(project => project.frequency >= FREQUENT_PROJECTS.ELIGIBLE_FREQUENCY);
+ const frequentProjects = storedFrequentProjects.filter(
+ project => project.frequency >= FREQUENT_PROJECTS.ELIGIBLE_FREQUENCY,
+ );
+
+ if (!frequentProjects || frequentProjects.length === 0) {
+ return [];
+ }
// Sort all frequent projects in decending order of frequency
// and then by lastAccessedOn with recent most first
diff --git a/app/assets/javascripts/shortcuts.js b/app/assets/javascripts/shortcuts.js
index e31e067033f..99c71d6524a 100644
--- a/app/assets/javascripts/shortcuts.js
+++ b/app/assets/javascripts/shortcuts.js
@@ -85,6 +85,7 @@ export default class Shortcuts {
if ($modal.length) {
$modal.modal('toggle');
+ return null;
}
return axios.get(gon.shortcuts_path, {
diff --git a/app/assets/javascripts/sidebar/components/participants/participants.vue b/app/assets/javascripts/sidebar/components/participants/participants.vue
index 6d95153af28..8f9e6761d20 100644
--- a/app/assets/javascripts/sidebar/components/participants/participants.vue
+++ b/app/assets/javascripts/sidebar/components/participants/participants.vue
@@ -70,6 +70,9 @@
toggleMoreParticipants() {
this.isShowingMoreParticipants = !this.isShowingMoreParticipants;
},
+ onClickCollapsedIcon() {
+ this.$emit('toggleSidebar');
+ },
},
};
</script>
@@ -82,6 +85,7 @@
data-container="body"
data-placement="left"
:title="participantLabel"
+ @click="onClickCollapsedIcon"
>
<i
class="fa fa-users"
diff --git a/app/assets/javascripts/sidebar/components/subscriptions/sidebar_subscriptions.vue b/app/assets/javascripts/sidebar/components/subscriptions/sidebar_subscriptions.vue
index 3e8cc7a6630..385717e7c1e 100644
--- a/app/assets/javascripts/sidebar/components/subscriptions/sidebar_subscriptions.vue
+++ b/app/assets/javascripts/sidebar/components/subscriptions/sidebar_subscriptions.vue
@@ -1,6 +1,5 @@
<script>
import Store from '../../stores/sidebar_store';
-import eventHub from '../../event_hub';
import Flash from '../../../flash';
import { __ } from '../../../locale';
import subscriptions from './subscriptions.vue';
@@ -20,12 +19,6 @@ export default {
store: new Store(),
};
},
- created() {
- eventHub.$on('toggleSubscription', this.onToggleSubscription);
- },
- beforeDestroy() {
- eventHub.$off('toggleSubscription', this.onToggleSubscription);
- },
methods: {
onToggleSubscription() {
this.mediator.toggleSubscription()
@@ -42,6 +35,7 @@ export default {
<subscriptions
:loading="store.isFetching.subscriptions"
:subscribed="store.subscribed"
+ @toggleSubscription="onToggleSubscription"
/>
</div>
</template>
diff --git a/app/assets/javascripts/sidebar/components/subscriptions/subscriptions.vue b/app/assets/javascripts/sidebar/components/subscriptions/subscriptions.vue
index c4c63794072..c50296d99d5 100644
--- a/app/assets/javascripts/sidebar/components/subscriptions/subscriptions.vue
+++ b/app/assets/javascripts/sidebar/components/subscriptions/subscriptions.vue
@@ -47,8 +47,25 @@
},
},
methods: {
+ /**
+ * We need to emit this event on both component & eventHub
+ * for 2 dependencies;
+ *
+ * 1. eventHub: This component is used in Issue Boards sidebar
+ * where component template is part of HAML
+ * and event listeners are tied to app's eventHub.
+ * 2. Component: This compone is also used in Epics in EE
+ * where listeners are tied to component event.
+ */
toggleSubscription() {
+ // App's eventHub event emission.
eventHub.$emit('toggleSubscription', this.id);
+
+ // Component event emission.
+ this.$emit('toggleSubscription', this.id);
+ },
+ onClickCollapsedIcon() {
+ this.$emit('toggleSidebar');
},
},
};
@@ -56,7 +73,10 @@
<template>
<div>
- <div class="sidebar-collapsed-icon">
+ <div
+ class="sidebar-collapsed-icon"
+ @click="onClickCollapsedIcon"
+ >
<span
v-tooltip
:title="notificationTooltip"
diff --git a/app/assets/javascripts/sidebar/components/time_tracking/no_tracking_pane.js b/app/assets/javascripts/sidebar/components/time_tracking/no_tracking_pane.js
deleted file mode 100644
index 38da76c6771..00000000000
--- a/app/assets/javascripts/sidebar/components/time_tracking/no_tracking_pane.js
+++ /dev/null
@@ -1,10 +0,0 @@
-export default {
- name: 'time-tracking-no-tracking-pane',
- template: `
- <div class="time-tracking-no-tracking-pane">
- <span class="no-value">
- {{ __('No estimate or time spent') }}
- </span>
- </div>
- `,
-};
diff --git a/app/assets/javascripts/sidebar/components/time_tracking/no_tracking_pane.vue b/app/assets/javascripts/sidebar/components/time_tracking/no_tracking_pane.vue
new file mode 100644
index 00000000000..9228184df5b
--- /dev/null
+++ b/app/assets/javascripts/sidebar/components/time_tracking/no_tracking_pane.vue
@@ -0,0 +1,13 @@
+<script>
+export default {
+ name: 'TimeTrackingNoTrackingPane',
+};
+</script>
+
+<template>
+ <div class="time-tracking-no-tracking-pane">
+ <span class="no-value">
+ {{ __('No estimate or time spent') }}
+ </span>
+ </div>
+</template>
diff --git a/app/assets/javascripts/sidebar/components/time_tracking/sidebar_time_tracking.js b/app/assets/javascripts/sidebar/components/time_tracking/sidebar_time_tracking.vue
index 5626cccc022..2e1d6e9643a 100644
--- a/app/assets/javascripts/sidebar/components/time_tracking/sidebar_time_tracking.js
+++ b/app/assets/javascripts/sidebar/components/time_tracking/sidebar_time_tracking.vue
@@ -1,3 +1,4 @@
+<script>
import $ from 'jquery';
import _ from 'underscore';
@@ -10,14 +11,17 @@ import Mediator from '../../sidebar_mediator';
import eventHub from '../../event_hub';
export default {
+ components: {
+ IssuableTimeTracker,
+ },
data() {
return {
mediator: new Mediator(),
store: new Store(),
};
},
- components: {
- IssuableTimeTracker,
+ mounted() {
+ this.listenForQuickActions();
},
methods: {
listenForQuickActions() {
@@ -41,18 +45,17 @@ export default {
}
},
},
- mounted() {
- this.listenForQuickActions();
- },
- template: `
- <div class="block">
- <issuable-time-tracker
- :time_estimate="store.timeEstimate"
- :time_spent="store.totalTimeSpent"
- :human_time_estimate="store.humanTimeEstimate"
- :human_time_spent="store.humanTotalTimeSpent"
- :rootPath="store.rootPath"
- />
- </div>
- `,
};
+</script>
+
+<template>
+ <div class="block">
+ <issuable-time-tracker
+ :time_estimate="store.timeEstimate"
+ :time_spent="store.totalTimeSpent"
+ :human_time_estimate="store.humanTimeEstimate"
+ :human_time_spent="store.humanTotalTimeSpent"
+ :root-path="store.rootPath"
+ />
+ </div>
+</template>
diff --git a/app/assets/javascripts/sidebar/components/time_tracking/spent_only_pane.js b/app/assets/javascripts/sidebar/components/time_tracking/spent_only_pane.js
deleted file mode 100644
index bf987562647..00000000000
--- a/app/assets/javascripts/sidebar/components/time_tracking/spent_only_pane.js
+++ /dev/null
@@ -1,15 +0,0 @@
-export default {
- name: 'time-tracking-spent-only-pane',
- props: {
- timeSpentHumanReadable: {
- type: String,
- required: true,
- },
- },
- template: `
- <div class="time-tracking-spend-only-pane">
- <span class="bold">Spent:</span>
- {{ timeSpentHumanReadable }}
- </div>
- `,
-};
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
new file mode 100644
index 00000000000..59cd99f8f14
--- /dev/null
+++ b/app/assets/javascripts/sidebar/components/time_tracking/spent_only_pane.vue
@@ -0,0 +1,18 @@
+<script>
+export default {
+ name: 'TimeTrackingSpentOnlyPane',
+ props: {
+ timeSpentHumanReadable: {
+ type: String,
+ required: true,
+ },
+ },
+};
+</script>
+
+<template>
+ <div class="time-tracking-spend-only-pane">
+ <span class="bold">Spent:</span>
+ {{ timeSpentHumanReadable }}
+ </div>
+</template>
diff --git a/app/assets/javascripts/sidebar/components/time_tracking/time_tracker.vue b/app/assets/javascripts/sidebar/components/time_tracking/time_tracker.vue
index 1ef0764bf2b..7d56d2fa5ee 100644
--- a/app/assets/javascripts/sidebar/components/time_tracking/time_tracker.vue
+++ b/app/assets/javascripts/sidebar/components/time_tracking/time_tracker.vue
@@ -1,8 +1,8 @@
<script>
import TimeTrackingHelpState from './help_state.vue';
import TimeTrackingCollapsedState from './collapsed_state.vue';
-import timeTrackingSpentOnlyPane from './spent_only_pane';
-import timeTrackingNoTrackingPane from './no_tracking_pane';
+import TimeTrackingSpentOnlyPane from './spent_only_pane.vue';
+import TimeTrackingNoTrackingPane from './no_tracking_pane.vue';
import TimeTrackingEstimateOnlyPane from './estimate_only_pane.vue';
import TimeTrackingComparisonPane from './comparison_pane.vue';
@@ -13,8 +13,8 @@ export default {
components: {
TimeTrackingCollapsedState,
TimeTrackingEstimateOnlyPane,
- 'time-tracking-spent-only-pane': timeTrackingSpentOnlyPane,
- 'time-tracking-no-tracking-pane': timeTrackingNoTrackingPane,
+ TimeTrackingSpentOnlyPane,
+ TimeTrackingNoTrackingPane,
TimeTrackingComparisonPane,
TimeTrackingHelpState,
},
diff --git a/app/assets/javascripts/sidebar/mount_sidebar.js b/app/assets/javascripts/sidebar/mount_sidebar.js
index 26eb4cffba3..3086e7d0fc9 100644
--- a/app/assets/javascripts/sidebar/mount_sidebar.js
+++ b/app/assets/javascripts/sidebar/mount_sidebar.js
@@ -1,6 +1,6 @@
import $ from 'jquery';
import Vue from 'vue';
-import SidebarTimeTracking from './components/time_tracking/sidebar_time_tracking';
+import SidebarTimeTracking from './components/time_tracking/sidebar_time_tracking.vue';
import SidebarAssignees from './components/assignees/sidebar_assignees.vue';
import ConfidentialIssueSidebar from './components/confidential/confidential_issue_sidebar.vue';
import SidebarMoveIssue from './lib/sidebar_move_issue';
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 50724aa32ad..96b50b034ea 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
@@ -3,6 +3,7 @@
import tooltip from '~/vue_shared/directives/tooltip';
import loadingIcon from '~/vue_shared/components/loading_icon.vue';
import { s__, __ } from '~/locale';
+ import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import mrWidgetAuthorTime from '../../components/mr_widget_author_time.vue';
import statusIcon from '../mr_widget_status_icon.vue';
import eventHub from '../../event_hub';
@@ -16,6 +17,7 @@
mrWidgetAuthorTime,
loadingIcon,
statusIcon,
+ ClipboardButton,
},
props: {
mr: {
@@ -162,6 +164,18 @@
<span class="label-branch">
<a :href="mr.targetBranchPath">{{ mr.targetBranch }}</a>
</span>
+ with
+ <a
+ :href="mr.mergeCommitPath"
+ class="commit-sha js-mr-merged-commit-sha"
+ >
+ {{ mr.shortMergeCommitSha }}
+ </a>
+ <clipboard-button
+ :title="__('Copy commit SHA to clipboard')"
+ :text="mr.shortMergeCommitSha"
+ css-class="btn-default btn-transparent btn-clipboard js-mr-merged-copy-sha"
+ />
</p>
<p v-if="mr.sourceBranchRemoved">
{{ s__("mrWidget|The source branch has been removed") }}
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_wip.js b/app/assets/javascripts/vue_merge_request_widget/components/states/work_in_progress.vue
index 8bbcea39787..fe2608e8212 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_wip.js
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/work_in_progress.vue
@@ -1,25 +1,26 @@
+<script>
import $ from 'jquery';
import statusIcon from '../mr_widget_status_icon.vue';
import tooltip from '../../../vue_shared/directives/tooltip';
import eventHub from '../../event_hub';
export default {
- name: 'MRWidgetWIP',
- props: {
- mr: { type: Object, required: true },
- service: { type: Object, required: true },
+ name: 'WorkInProgress',
+ components: {
+ statusIcon,
},
directives: {
tooltip,
},
+ props: {
+ mr: { type: Object, required: true },
+ service: { type: Object, required: true },
+ },
data() {
return {
isMakingRequest: false,
};
},
- components: {
- statusIcon,
- },
methods: {
removeWIP() {
this.isMakingRequest = true;
@@ -36,32 +37,40 @@ export default {
});
},
},
- template: `
- <div class="mr-widget-body media">
- <status-icon status="warning" :show-disabled-button="Boolean(mr.removeWIPPath)" />
- <div class="media-body space-children">
- <span class="bold">
- This is a Work in Progress
- <i
- v-tooltip
- class="fa fa-question-circle"
- title="When this merge request is ready, remove the WIP: prefix from the title to allow it to be merged"
- aria-label="When this merge request is ready, remove the WIP: prefix from the title to allow it to be merged">
- </i>
- </span>
- <button
- v-if="mr.removeWIPPath"
- @click="removeWIP"
- :disabled="isMakingRequest"
- type="button"
- class="btn btn-default btn-sm js-remove-wip">
- <i
- v-if="isMakingRequest"
- class="fa fa-spinner fa-spin"
- aria-hidden="true" />
- Resolve WIP status
- </button>
- </div>
- </div>
- `,
};
+</script>
+
+<template>
+ <div class="mr-widget-body media">
+ <status-icon
+ status="warning"
+ :show-disabled-button="Boolean(mr.removeWIPPath)"
+ />
+ <div class="media-body space-children">
+ <span class="bold">
+ This is a Work in Progress
+ <i
+ v-tooltip
+ class="fa fa-question-circle"
+ title="When this merge request is ready,
+ remove the WIP: prefix from the title to allow it to be merged"
+ aria-label="When this merge request is ready,
+ remove the WIP: prefix from the title to allow it to be merged">
+ </i>
+ </span>
+ <button
+ v-if="mr.removeWIPPath"
+ @click="removeWIP"
+ :disabled="isMakingRequest"
+ type="button"
+ class="btn btn-default btn-xs js-remove-wip">
+ <i
+ v-if="isMakingRequest"
+ class="fa fa-spinner fa-spin"
+ aria-hidden="true">
+ </i>
+ Resolve WIP status
+ </button>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/vue_merge_request_widget/dependencies.js b/app/assets/javascripts/vue_merge_request_widget/dependencies.js
index 3b5c973e4a0..7f5f28091da 100644
--- a/app/assets/javascripts/vue_merge_request_widget/dependencies.js
+++ b/app/assets/javascripts/vue_merge_request_widget/dependencies.js
@@ -21,7 +21,7 @@ export { default as MergedState } from './components/states/mr_widget_merged.vue
export { default as FailedToMerge } from './components/states/mr_widget_failed_to_merge.vue';
export { default as ClosedState } from './components/states/mr_widget_closed.vue';
export { default as MergingState } from './components/states/mr_widget_merging.vue';
-export { default as WipState } from './components/states/mr_widget_wip';
+export { default as WorkInProgressState } from './components/states/work_in_progress.vue';
export { default as ArchivedState } from './components/states/mr_widget_archived.vue';
export { default as ConflictsState } from './components/states/mr_widget_conflicts.vue';
export { default as NothingToMergeState } from './components/states/nothing_to_merge.vue';
diff --git a/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.js b/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.js
index 0be5d9e5a55..345f9ac1b4b 100644
--- a/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.js
+++ b/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.js
@@ -12,7 +12,7 @@ import {
ClosedState,
MergingState,
RebaseState,
- WipState,
+ WorkInProgressState,
ArchivedState,
ConflictsState,
NothingToMergeState,
@@ -220,7 +220,7 @@ export default {
'mr-widget-closed': ClosedState,
'mr-widget-merging': MergingState,
'mr-widget-failed-to-merge': FailedToMerge,
- 'mr-widget-wip': WipState,
+ 'mr-widget-wip': WorkInProgressState,
'mr-widget-archived': ArchivedState,
'mr-widget-conflicts': ConflictsState,
'mr-widget-nothing-to-merge': NothingToMergeState,
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 a47ca9fae86..83b7b054e6f 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
@@ -20,6 +20,7 @@ export default class MergeRequestStore {
this.sourceBranch = data.source_branch;
this.mergeStatus = data.merge_status;
this.commitMessage = data.merge_commit_message;
+ this.shortMergeCommitSha = data.short_merge_commit_sha;
this.commitMessageWithDescription = data.merge_commit_message_with_description;
this.commitsCount = data.commits_count;
this.divergedCommitsCount = data.diverged_commits_count;
@@ -65,6 +66,7 @@ export default class MergeRequestStore {
this.createIssueToResolveDiscussionsPath = data.create_issue_to_resolve_discussions_path;
this.mergeCheckPath = data.merge_check_path;
this.mergeActionsContentPath = data.commit_change_content_path;
+ this.mergeCommitPath = data.merge_commit_path;
this.isRemovingSourceBranch = this.isRemovingSourceBranch || false;
this.isOpen = data.state === 'opened';
this.hasMergeableDiscussionsState = data.mergeable_discussions_state === false;
diff --git a/app/assets/javascripts/vue_shared/components/icon.vue b/app/assets/javascripts/vue_shared/components/icon.vue
index 1a0df49bc29..c42c4a1fbe7 100644
--- a/app/assets/javascripts/vue_shared/components/icon.vue
+++ b/app/assets/javascripts/vue_shared/components/icon.vue
@@ -65,6 +65,9 @@ export default {
spriteHref() {
return `${gon.sprite_icons}#${this.name}`;
},
+ iconTestClass() {
+ return `ic-${this.name}`;
+ },
iconSizeClass() {
return this.size ? `s${this.size}` : '';
},
@@ -74,7 +77,7 @@ export default {
<template>
<svg
- :class="[iconSizeClass, cssClasses]"
+ :class="[iconSizeClass, iconTestClass, cssClasses]"
:width="width"
:height="height"
:x="x"
diff --git a/app/assets/javascripts/vue_shared/components/navigation_tabs.vue b/app/assets/javascripts/vue_shared/components/navigation_tabs.vue
index dcaccde7ef9..08d4936f480 100644
--- a/app/assets/javascripts/vue_shared/components/navigation_tabs.vue
+++ b/app/assets/javascripts/vue_shared/components/navigation_tabs.vue
@@ -1,53 +1,53 @@
<script>
- import $ from 'jquery';
+import $ from 'jquery';
- /**
- * Given an array of tabs, renders non linked bootstrap tabs.
- * When a tab is clicked it will trigger an event and provide the clicked scope.
- *
- * This component is used in apps that handle the API call.
- * If you only need to change the URL this component should not be used.
- *
- * @example
- * <navigation-tabs
- * :tabs="[
- * {
- * name: String,
- * scope: String,
- * count: Number || Undefined,
- * isActive: Boolean,
- * },
- * ]"
- * @onChangeTab="onChangeTab"
- * />
- */
- export default {
- name: 'NavigationTabs',
- props: {
- tabs: {
- type: Array,
- required: true,
- },
- scope: {
- type: String,
- required: false,
- default: '',
- },
+/**
+ * Given an array of tabs, renders non linked bootstrap tabs.
+ * When a tab is clicked it will trigger an event and provide the clicked scope.
+ *
+ * This component is used in apps that handle the API call.
+ * If you only need to change the URL this component should not be used.
+ *
+ * @example
+ * <navigation-tabs
+ * :tabs="[
+ * {
+ * name: String,
+ * scope: String,
+ * count: Number || Undefined || Null,
+ * isActive: Boolean,
+ * },
+ * ]"
+ * @onChangeTab="onChangeTab"
+ * />
+ */
+export default {
+ name: 'NavigationTabs',
+ props: {
+ tabs: {
+ type: Array,
+ required: true,
},
- mounted() {
- $(document).trigger('init.scrolling-tabs');
+ scope: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ },
+ mounted() {
+ $(document).trigger('init.scrolling-tabs');
+ },
+ methods: {
+ shouldRenderBadge(count) {
+ // 0 is valid in a badge, but evaluates to false, we need to check for undefined or null
+ return !(count === undefined || count === null);
},
- methods: {
- shouldRenderBadge(count) {
- // 0 is valid in a badge, but evaluates to false, we need to check for undefined
- return count !== undefined;
- },
- onTabClick(tab) {
- this.$emit('onChangeTab', tab.scope);
- },
+ onTabClick(tab) {
+ this.$emit('onChangeTab', tab.scope);
},
- };
+ },
+};
</script>
<template>
<ul class="nav-links scrolling-tabs separator">
diff --git a/app/assets/stylesheets/emoji_sprites.scss b/app/assets/stylesheets/emoji_sprites.scss
new file mode 100644
index 00000000000..8f6134c474b
--- /dev/null
+++ b/app/assets/stylesheets/emoji_sprites.scss
@@ -0,0 +1,5403 @@
+// Automatic Prettier Formatting for this big file
+// scss-lint:disable EmptyLineBetweenBlocks
+.emoji-zzz {
+ background-position: 0 0;
+}
+.emoji-1234 {
+ background-position: -20px 0;
+}
+.emoji-1F627 {
+ background-position: 0 -20px;
+}
+.emoji-8ball {
+ background-position: -20px -20px;
+}
+.emoji-a {
+ background-position: -40px 0;
+}
+.emoji-ab {
+ background-position: -40px -20px;
+}
+.emoji-abc {
+ background-position: 0 -40px;
+}
+.emoji-abcd {
+ background-position: -20px -40px;
+}
+.emoji-accept {
+ background-position: -40px -40px;
+}
+.emoji-aerial_tramway {
+ background-position: -60px 0;
+}
+.emoji-airplane {
+ background-position: -60px -20px;
+}
+.emoji-airplane_arriving {
+ background-position: -60px -40px;
+}
+.emoji-airplane_departure {
+ background-position: 0 -60px;
+}
+.emoji-airplane_small {
+ background-position: -20px -60px;
+}
+.emoji-alarm_clock {
+ background-position: -40px -60px;
+}
+.emoji-alembic {
+ background-position: -60px -60px;
+}
+.emoji-alien {
+ background-position: -80px 0;
+}
+.emoji-ambulance {
+ background-position: -80px -20px;
+}
+.emoji-amphora {
+ background-position: -80px -40px;
+}
+.emoji-anchor {
+ background-position: -80px -60px;
+}
+.emoji-angel {
+ background-position: 0 -80px;
+}
+.emoji-angel_tone1 {
+ background-position: -20px -80px;
+}
+.emoji-angel_tone2 {
+ background-position: -40px -80px;
+}
+.emoji-angel_tone3 {
+ background-position: -60px -80px;
+}
+.emoji-angel_tone4 {
+ background-position: -80px -80px;
+}
+.emoji-angel_tone5 {
+ background-position: -100px 0;
+}
+.emoji-anger {
+ background-position: -100px -20px;
+}
+.emoji-anger_right {
+ background-position: -100px -40px;
+}
+.emoji-angry {
+ background-position: -100px -60px;
+}
+.emoji-ant {
+ background-position: -100px -80px;
+}
+.emoji-apple {
+ background-position: 0 -100px;
+}
+.emoji-aquarius {
+ background-position: -20px -100px;
+}
+.emoji-aries {
+ background-position: -40px -100px;
+}
+.emoji-arrow_backward {
+ background-position: -60px -100px;
+}
+.emoji-arrow_double_down {
+ background-position: -80px -100px;
+}
+.emoji-arrow_double_up {
+ background-position: -100px -100px;
+}
+.emoji-arrow_down {
+ background-position: -120px 0;
+}
+.emoji-arrow_down_small {
+ background-position: -120px -20px;
+}
+.emoji-arrow_forward {
+ background-position: -120px -40px;
+}
+.emoji-arrow_heading_down {
+ background-position: -120px -60px;
+}
+.emoji-arrow_heading_up {
+ background-position: -120px -80px;
+}
+.emoji-arrow_left {
+ background-position: -120px -100px;
+}
+.emoji-arrow_lower_left {
+ background-position: 0 -120px;
+}
+.emoji-arrow_lower_right {
+ background-position: -20px -120px;
+}
+.emoji-arrow_right {
+ background-position: -40px -120px;
+}
+.emoji-arrow_right_hook {
+ background-position: -60px -120px;
+}
+.emoji-arrow_up {
+ background-position: -80px -120px;
+}
+.emoji-arrow_up_down {
+ background-position: -100px -120px;
+}
+.emoji-arrow_up_small {
+ background-position: -120px -120px;
+}
+.emoji-arrow_upper_left {
+ background-position: -140px 0;
+}
+.emoji-arrow_upper_right {
+ background-position: -140px -20px;
+}
+.emoji-arrows_clockwise {
+ background-position: -140px -40px;
+}
+.emoji-arrows_counterclockwise {
+ background-position: -140px -60px;
+}
+.emoji-art {
+ background-position: -140px -80px;
+}
+.emoji-articulated_lorry {
+ background-position: -140px -100px;
+}
+.emoji-asterisk {
+ background-position: -140px -120px;
+}
+.emoji-astonished {
+ background-position: 0 -140px;
+}
+.emoji-athletic_shoe {
+ background-position: -20px -140px;
+}
+.emoji-atm {
+ background-position: -40px -140px;
+}
+.emoji-atom {
+ background-position: -60px -140px;
+}
+.emoji-avocado {
+ background-position: -80px -140px;
+}
+.emoji-b {
+ background-position: -100px -140px;
+}
+.emoji-baby {
+ background-position: -120px -140px;
+}
+.emoji-baby_bottle {
+ background-position: -140px -140px;
+}
+.emoji-baby_chick {
+ background-position: -160px 0;
+}
+.emoji-baby_symbol {
+ background-position: -160px -20px;
+}
+.emoji-baby_tone1 {
+ background-position: -160px -40px;
+}
+.emoji-baby_tone2 {
+ background-position: -160px -60px;
+}
+.emoji-baby_tone3 {
+ background-position: -160px -80px;
+}
+.emoji-baby_tone4 {
+ background-position: -160px -100px;
+}
+.emoji-baby_tone5 {
+ background-position: -160px -120px;
+}
+.emoji-back {
+ background-position: -160px -140px;
+}
+.emoji-bacon {
+ background-position: 0 -160px;
+}
+.emoji-badminton {
+ background-position: -20px -160px;
+}
+.emoji-baggage_claim {
+ background-position: -40px -160px;
+}
+.emoji-balloon {
+ background-position: -60px -160px;
+}
+.emoji-ballot_box {
+ background-position: -80px -160px;
+}
+.emoji-ballot_box_with_check {
+ background-position: -100px -160px;
+}
+.emoji-bamboo {
+ background-position: -120px -160px;
+}
+.emoji-banana {
+ background-position: -140px -160px;
+}
+.emoji-bangbang {
+ background-position: -160px -160px;
+}
+.emoji-bank {
+ background-position: -180px 0;
+}
+.emoji-bar_chart {
+ background-position: -180px -20px;
+}
+.emoji-barber {
+ background-position: -180px -40px;
+}
+.emoji-baseball {
+ background-position: -180px -60px;
+}
+.emoji-basketball {
+ background-position: -180px -80px;
+}
+.emoji-basketball_player {
+ background-position: -180px -100px;
+}
+.emoji-basketball_player_tone1 {
+ background-position: -180px -120px;
+}
+.emoji-basketball_player_tone2 {
+ background-position: -180px -140px;
+}
+.emoji-basketball_player_tone3 {
+ background-position: -180px -160px;
+}
+.emoji-basketball_player_tone4 {
+ background-position: 0 -180px;
+}
+.emoji-basketball_player_tone5 {
+ background-position: -20px -180px;
+}
+.emoji-bat {
+ background-position: -40px -180px;
+}
+.emoji-bath {
+ background-position: -60px -180px;
+}
+.emoji-bath_tone1 {
+ background-position: -80px -180px;
+}
+.emoji-bath_tone2 {
+ background-position: -100px -180px;
+}
+.emoji-bath_tone3 {
+ background-position: -120px -180px;
+}
+.emoji-bath_tone4 {
+ background-position: -140px -180px;
+}
+.emoji-bath_tone5 {
+ background-position: -160px -180px;
+}
+.emoji-bathtub {
+ background-position: -180px -180px;
+}
+.emoji-battery {
+ background-position: -200px 0;
+}
+.emoji-beach {
+ background-position: -200px -20px;
+}
+.emoji-beach_umbrella {
+ background-position: -200px -40px;
+}
+.emoji-bear {
+ background-position: -200px -60px;
+}
+.emoji-bed {
+ background-position: -200px -80px;
+}
+.emoji-bee {
+ background-position: -200px -100px;
+}
+.emoji-beer {
+ background-position: -200px -120px;
+}
+.emoji-beers {
+ background-position: -200px -140px;
+}
+.emoji-beetle {
+ background-position: -200px -160px;
+}
+.emoji-beginner {
+ background-position: -200px -180px;
+}
+.emoji-bell {
+ background-position: 0 -200px;
+}
+.emoji-bellhop {
+ background-position: -20px -200px;
+}
+.emoji-bento {
+ background-position: -40px -200px;
+}
+.emoji-bicyclist {
+ background-position: -60px -200px;
+}
+.emoji-bicyclist_tone1 {
+ background-position: -80px -200px;
+}
+.emoji-bicyclist_tone2 {
+ background-position: -100px -200px;
+}
+.emoji-bicyclist_tone3 {
+ background-position: -120px -200px;
+}
+.emoji-bicyclist_tone4 {
+ background-position: -140px -200px;
+}
+.emoji-bicyclist_tone5 {
+ background-position: -160px -200px;
+}
+.emoji-bike {
+ background-position: -180px -200px;
+}
+.emoji-bikini {
+ background-position: -200px -200px;
+}
+.emoji-biohazard {
+ background-position: -220px 0;
+}
+.emoji-bird {
+ background-position: -220px -20px;
+}
+.emoji-birthday {
+ background-position: -220px -40px;
+}
+.emoji-black_circle {
+ background-position: -220px -60px;
+}
+.emoji-black_heart {
+ background-position: -220px -80px;
+}
+.emoji-black_joker {
+ background-position: -220px -100px;
+}
+.emoji-black_large_square {
+ background-position: -220px -120px;
+}
+.emoji-black_medium_small_square {
+ background-position: -220px -140px;
+}
+.emoji-black_medium_square {
+ background-position: -220px -160px;
+}
+.emoji-black_nib {
+ background-position: -220px -180px;
+}
+.emoji-black_small_square {
+ background-position: -220px -200px;
+}
+.emoji-black_square_button {
+ background-position: 0 -220px;
+}
+.emoji-blossom {
+ background-position: -20px -220px;
+}
+.emoji-blowfish {
+ background-position: -40px -220px;
+}
+.emoji-blue_book {
+ background-position: -60px -220px;
+}
+.emoji-blue_car {
+ background-position: -80px -220px;
+}
+.emoji-blue_heart {
+ background-position: -100px -220px;
+}
+.emoji-blush {
+ background-position: -120px -220px;
+}
+.emoji-boar {
+ background-position: -140px -220px;
+}
+.emoji-bomb {
+ background-position: -160px -220px;
+}
+.emoji-book {
+ background-position: -180px -220px;
+}
+.emoji-bookmark {
+ background-position: -200px -220px;
+}
+.emoji-bookmark_tabs {
+ background-position: -220px -220px;
+}
+.emoji-books {
+ background-position: -240px 0;
+}
+.emoji-boom {
+ background-position: -240px -20px;
+}
+.emoji-boot {
+ background-position: -240px -40px;
+}
+.emoji-bouquet {
+ background-position: -240px -60px;
+}
+.emoji-bow {
+ background-position: -240px -80px;
+}
+.emoji-bow_and_arrow {
+ background-position: -240px -100px;
+}
+.emoji-bow_tone1 {
+ background-position: -240px -120px;
+}
+.emoji-bow_tone2 {
+ background-position: -240px -140px;
+}
+.emoji-bow_tone3 {
+ background-position: -240px -160px;
+}
+.emoji-bow_tone4 {
+ background-position: -240px -180px;
+}
+.emoji-bow_tone5 {
+ background-position: -240px -200px;
+}
+.emoji-bowling {
+ background-position: -240px -220px;
+}
+.emoji-boxing_glove {
+ background-position: 0 -240px;
+}
+.emoji-boy {
+ background-position: -20px -240px;
+}
+.emoji-boy_tone1 {
+ background-position: -40px -240px;
+}
+.emoji-boy_tone2 {
+ background-position: -60px -240px;
+}
+.emoji-boy_tone3 {
+ background-position: -80px -240px;
+}
+.emoji-boy_tone4 {
+ background-position: -100px -240px;
+}
+.emoji-boy_tone5 {
+ background-position: -120px -240px;
+}
+.emoji-bread {
+ background-position: -140px -240px;
+}
+.emoji-bride_with_veil {
+ background-position: -160px -240px;
+}
+.emoji-bride_with_veil_tone1 {
+ background-position: -180px -240px;
+}
+.emoji-bride_with_veil_tone2 {
+ background-position: -200px -240px;
+}
+.emoji-bride_with_veil_tone3 {
+ background-position: -220px -240px;
+}
+.emoji-bride_with_veil_tone4 {
+ background-position: -240px -240px;
+}
+.emoji-bride_with_veil_tone5 {
+ background-position: -260px 0;
+}
+.emoji-bridge_at_night {
+ background-position: -260px -20px;
+}
+.emoji-briefcase {
+ background-position: -260px -40px;
+}
+.emoji-broken_heart {
+ background-position: -260px -60px;
+}
+.emoji-bug {
+ background-position: -260px -80px;
+}
+.emoji-bulb {
+ background-position: -260px -100px;
+}
+.emoji-bullettrain_front {
+ background-position: -260px -120px;
+}
+.emoji-bullettrain_side {
+ background-position: -260px -140px;
+}
+.emoji-burrito {
+ background-position: -260px -160px;
+}
+.emoji-bus {
+ background-position: -260px -180px;
+}
+.emoji-busstop {
+ background-position: -260px -200px;
+}
+.emoji-bust_in_silhouette {
+ background-position: -260px -220px;
+}
+.emoji-busts_in_silhouette {
+ background-position: -260px -240px;
+}
+.emoji-butterfly {
+ background-position: 0 -260px;
+}
+.emoji-cactus {
+ background-position: -20px -260px;
+}
+.emoji-cake {
+ background-position: -40px -260px;
+}
+.emoji-calendar {
+ background-position: -60px -260px;
+}
+.emoji-calendar_spiral {
+ background-position: -80px -260px;
+}
+.emoji-call_me {
+ background-position: -100px -260px;
+}
+.emoji-call_me_tone1 {
+ background-position: -120px -260px;
+}
+.emoji-call_me_tone2 {
+ background-position: -140px -260px;
+}
+.emoji-call_me_tone3 {
+ background-position: -160px -260px;
+}
+.emoji-call_me_tone4 {
+ background-position: -180px -260px;
+}
+.emoji-call_me_tone5 {
+ background-position: -200px -260px;
+}
+.emoji-calling {
+ background-position: -220px -260px;
+}
+.emoji-camel {
+ background-position: -240px -260px;
+}
+.emoji-camera {
+ background-position: -260px -260px;
+}
+.emoji-camera_with_flash {
+ background-position: -280px 0;
+}
+.emoji-camping {
+ background-position: -280px -20px;
+}
+.emoji-cancer {
+ background-position: -280px -40px;
+}
+.emoji-candle {
+ background-position: -280px -60px;
+}
+.emoji-candy {
+ background-position: -280px -80px;
+}
+.emoji-canoe {
+ background-position: -280px -100px;
+}
+.emoji-capital_abcd {
+ background-position: -280px -120px;
+}
+.emoji-capricorn {
+ background-position: -280px -140px;
+}
+.emoji-card_box {
+ background-position: -280px -160px;
+}
+.emoji-card_index {
+ background-position: -280px -180px;
+}
+.emoji-carousel_horse {
+ background-position: -280px -200px;
+}
+.emoji-carrot {
+ background-position: -280px -220px;
+}
+.emoji-cartwheel {
+ background-position: -280px -240px;
+}
+.emoji-cartwheel_tone1 {
+ background-position: -280px -260px;
+}
+.emoji-cartwheel_tone2 {
+ background-position: 0 -280px;
+}
+.emoji-cartwheel_tone3 {
+ background-position: -20px -280px;
+}
+.emoji-cartwheel_tone4 {
+ background-position: -40px -280px;
+}
+.emoji-cartwheel_tone5 {
+ background-position: -60px -280px;
+}
+.emoji-cat {
+ background-position: -80px -280px;
+}
+.emoji-cat2 {
+ background-position: -100px -280px;
+}
+.emoji-cd {
+ background-position: -120px -280px;
+}
+.emoji-chains {
+ background-position: -140px -280px;
+}
+.emoji-champagne {
+ background-position: -160px -280px;
+}
+.emoji-champagne_glass {
+ background-position: -180px -280px;
+}
+.emoji-chart {
+ background-position: -200px -280px;
+}
+.emoji-chart_with_downwards_trend {
+ background-position: -220px -280px;
+}
+.emoji-chart_with_upwards_trend {
+ background-position: -240px -280px;
+}
+.emoji-checkered_flag {
+ background-position: -260px -280px;
+}
+.emoji-cheese {
+ background-position: -280px -280px;
+}
+.emoji-cherries {
+ background-position: -300px 0;
+}
+.emoji-cherry_blossom {
+ background-position: -300px -20px;
+}
+.emoji-chestnut {
+ background-position: -300px -40px;
+}
+.emoji-chicken {
+ background-position: -300px -60px;
+}
+.emoji-children_crossing {
+ background-position: -300px -80px;
+}
+.emoji-chipmunk {
+ background-position: -300px -100px;
+}
+.emoji-chocolate_bar {
+ background-position: -300px -120px;
+}
+.emoji-christmas_tree {
+ background-position: -300px -140px;
+}
+.emoji-church {
+ background-position: -300px -160px;
+}
+.emoji-cinema {
+ background-position: -300px -180px;
+}
+.emoji-circus_tent {
+ background-position: -300px -200px;
+}
+.emoji-city_dusk {
+ background-position: -300px -220px;
+}
+.emoji-city_sunset {
+ background-position: -300px -240px;
+}
+.emoji-cityscape {
+ background-position: -300px -260px;
+}
+.emoji-cl {
+ background-position: -300px -280px;
+}
+.emoji-clap {
+ background-position: 0 -300px;
+}
+.emoji-clap_tone1 {
+ background-position: -20px -300px;
+}
+.emoji-clap_tone2 {
+ background-position: -40px -300px;
+}
+.emoji-clap_tone3 {
+ background-position: -60px -300px;
+}
+.emoji-clap_tone4 {
+ background-position: -80px -300px;
+}
+.emoji-clap_tone5 {
+ background-position: -100px -300px;
+}
+.emoji-clapper {
+ background-position: -120px -300px;
+}
+.emoji-classical_building {
+ background-position: -140px -300px;
+}
+.emoji-clipboard {
+ background-position: -160px -300px;
+}
+.emoji-clock {
+ background-position: -180px -300px;
+}
+.emoji-clock1 {
+ background-position: -200px -300px;
+}
+.emoji-clock10 {
+ background-position: -220px -300px;
+}
+.emoji-clock1030 {
+ background-position: -240px -300px;
+}
+.emoji-clock11 {
+ background-position: -260px -300px;
+}
+.emoji-clock1130 {
+ background-position: -280px -300px;
+}
+.emoji-clock12 {
+ background-position: -300px -300px;
+}
+.emoji-clock1230 {
+ background-position: -320px 0;
+}
+.emoji-clock130 {
+ background-position: -320px -20px;
+}
+.emoji-clock2 {
+ background-position: -320px -40px;
+}
+.emoji-clock230 {
+ background-position: -320px -60px;
+}
+.emoji-clock3 {
+ background-position: -320px -80px;
+}
+.emoji-clock330 {
+ background-position: -320px -100px;
+}
+.emoji-clock4 {
+ background-position: -320px -120px;
+}
+.emoji-clock430 {
+ background-position: -320px -140px;
+}
+.emoji-clock5 {
+ background-position: -320px -160px;
+}
+.emoji-clock530 {
+ background-position: -320px -180px;
+}
+.emoji-clock6 {
+ background-position: -320px -200px;
+}
+.emoji-clock630 {
+ background-position: -320px -220px;
+}
+.emoji-clock7 {
+ background-position: -320px -240px;
+}
+.emoji-clock730 {
+ background-position: -320px -260px;
+}
+.emoji-clock8 {
+ background-position: -320px -280px;
+}
+.emoji-clock830 {
+ background-position: -320px -300px;
+}
+.emoji-clock9 {
+ background-position: 0 -320px;
+}
+.emoji-clock930 {
+ background-position: -20px -320px;
+}
+.emoji-closed_book {
+ background-position: -40px -320px;
+}
+.emoji-closed_lock_with_key {
+ background-position: -60px -320px;
+}
+.emoji-closed_umbrella {
+ background-position: -80px -320px;
+}
+.emoji-cloud {
+ background-position: -100px -320px;
+}
+.emoji-cloud_lightning {
+ background-position: -120px -320px;
+}
+.emoji-cloud_rain {
+ background-position: -140px -320px;
+}
+.emoji-cloud_snow {
+ background-position: -160px -320px;
+}
+.emoji-cloud_tornado {
+ background-position: -180px -320px;
+}
+.emoji-clown {
+ background-position: -200px -320px;
+}
+.emoji-clubs {
+ background-position: -220px -320px;
+}
+.emoji-cocktail {
+ background-position: -240px -320px;
+}
+.emoji-coffee {
+ background-position: -260px -320px;
+}
+.emoji-coffin {
+ background-position: -280px -320px;
+}
+.emoji-cold_sweat {
+ background-position: -300px -320px;
+}
+.emoji-comet {
+ background-position: -320px -320px;
+}
+.emoji-compression {
+ background-position: -340px 0;
+}
+.emoji-computer {
+ background-position: -340px -20px;
+}
+.emoji-confetti_ball {
+ background-position: -340px -40px;
+}
+.emoji-confounded {
+ background-position: -340px -60px;
+}
+.emoji-confused {
+ background-position: -340px -80px;
+}
+.emoji-congratulations {
+ background-position: -340px -100px;
+}
+.emoji-construction {
+ background-position: -340px -120px;
+}
+.emoji-construction_site {
+ background-position: -340px -140px;
+}
+.emoji-construction_worker {
+ background-position: -340px -160px;
+}
+.emoji-construction_worker_tone1 {
+ background-position: -340px -180px;
+}
+.emoji-construction_worker_tone2 {
+ background-position: -340px -200px;
+}
+.emoji-construction_worker_tone3 {
+ background-position: -340px -220px;
+}
+.emoji-construction_worker_tone4 {
+ background-position: -340px -240px;
+}
+.emoji-construction_worker_tone5 {
+ background-position: -340px -260px;
+}
+.emoji-control_knobs {
+ background-position: -340px -280px;
+}
+.emoji-convenience_store {
+ background-position: -340px -300px;
+}
+.emoji-cookie {
+ background-position: -340px -320px;
+}
+.emoji-cooking {
+ background-position: 0 -340px;
+}
+.emoji-cool {
+ background-position: -20px -340px;
+}
+.emoji-cop {
+ background-position: -40px -340px;
+}
+.emoji-cop_tone1 {
+ background-position: -60px -340px;
+}
+.emoji-cop_tone2 {
+ background-position: -80px -340px;
+}
+.emoji-cop_tone3 {
+ background-position: -100px -340px;
+}
+.emoji-cop_tone4 {
+ background-position: -120px -340px;
+}
+.emoji-cop_tone5 {
+ background-position: -140px -340px;
+}
+.emoji-copyright {
+ background-position: -160px -340px;
+}
+.emoji-corn {
+ background-position: -180px -340px;
+}
+.emoji-couch {
+ background-position: -200px -340px;
+}
+.emoji-couple {
+ background-position: -220px -340px;
+}
+.emoji-couple_mm {
+ background-position: -240px -340px;
+}
+.emoji-couple_with_heart {
+ background-position: -260px -340px;
+}
+.emoji-couple_ww {
+ background-position: -280px -340px;
+}
+.emoji-couplekiss {
+ background-position: -300px -340px;
+}
+.emoji-cow {
+ background-position: -320px -340px;
+}
+.emoji-cow2 {
+ background-position: -340px -340px;
+}
+.emoji-cowboy {
+ background-position: -360px 0;
+}
+.emoji-crab {
+ background-position: -360px -20px;
+}
+.emoji-crayon {
+ background-position: -360px -40px;
+}
+.emoji-credit_card {
+ background-position: -360px -60px;
+}
+.emoji-crescent_moon {
+ background-position: -360px -80px;
+}
+.emoji-cricket {
+ background-position: -360px -100px;
+}
+.emoji-crocodile {
+ background-position: -360px -120px;
+}
+.emoji-croissant {
+ background-position: -360px -140px;
+}
+.emoji-cross {
+ background-position: -360px -160px;
+}
+.emoji-crossed_flags {
+ background-position: -360px -180px;
+}
+.emoji-crossed_swords {
+ background-position: -360px -200px;
+}
+.emoji-crown {
+ background-position: -360px -220px;
+}
+.emoji-cruise_ship {
+ background-position: -360px -240px;
+}
+.emoji-cry {
+ background-position: -360px -260px;
+}
+.emoji-crying_cat_face {
+ background-position: -360px -280px;
+}
+.emoji-crystal_ball {
+ background-position: -360px -300px;
+}
+.emoji-cucumber {
+ background-position: -360px -320px;
+}
+.emoji-cupid {
+ background-position: -360px -340px;
+}
+.emoji-curly_loop {
+ background-position: 0 -360px;
+}
+.emoji-currency_exchange {
+ background-position: -20px -360px;
+}
+.emoji-curry {
+ background-position: -40px -360px;
+}
+.emoji-custard {
+ background-position: -60px -360px;
+}
+.emoji-customs {
+ background-position: -80px -360px;
+}
+.emoji-cyclone {
+ background-position: -100px -360px;
+}
+.emoji-dagger {
+ background-position: -120px -360px;
+}
+.emoji-dancer {
+ background-position: -140px -360px;
+}
+.emoji-dancer_tone1 {
+ background-position: -160px -360px;
+}
+.emoji-dancer_tone2 {
+ background-position: -180px -360px;
+}
+.emoji-dancer_tone3 {
+ background-position: -200px -360px;
+}
+.emoji-dancer_tone4 {
+ background-position: -220px -360px;
+}
+.emoji-dancer_tone5 {
+ background-position: -240px -360px;
+}
+.emoji-dancers {
+ background-position: -260px -360px;
+}
+.emoji-dango {
+ background-position: -280px -360px;
+}
+.emoji-dark_sunglasses {
+ background-position: -300px -360px;
+}
+.emoji-dart {
+ background-position: -320px -360px;
+}
+.emoji-dash {
+ background-position: -340px -360px;
+}
+.emoji-date {
+ background-position: -360px -360px;
+}
+.emoji-deciduous_tree {
+ background-position: -380px 0;
+}
+.emoji-deer {
+ background-position: -380px -20px;
+}
+.emoji-department_store {
+ background-position: -380px -40px;
+}
+.emoji-desert {
+ background-position: -380px -60px;
+}
+.emoji-desktop {
+ background-position: -380px -80px;
+}
+.emoji-diamond_shape_with_a_dot_inside {
+ background-position: -380px -100px;
+}
+.emoji-diamonds {
+ background-position: -380px -120px;
+}
+.emoji-disappointed {
+ background-position: -380px -140px;
+}
+.emoji-disappointed_relieved {
+ background-position: -380px -160px;
+}
+.emoji-dividers {
+ background-position: -380px -180px;
+}
+.emoji-dizzy {
+ background-position: -380px -200px;
+}
+.emoji-dizzy_face {
+ background-position: -380px -220px;
+}
+.emoji-do_not_litter {
+ background-position: -380px -240px;
+}
+.emoji-dog {
+ background-position: -380px -260px;
+}
+.emoji-dog2 {
+ background-position: -380px -280px;
+}
+.emoji-dollar {
+ background-position: -380px -300px;
+}
+.emoji-dolls {
+ background-position: -380px -320px;
+}
+.emoji-dolphin {
+ background-position: -380px -340px;
+}
+.emoji-door {
+ background-position: -380px -360px;
+}
+.emoji-doughnut {
+ background-position: 0 -380px;
+}
+.emoji-dove {
+ background-position: -20px -380px;
+}
+.emoji-dragon {
+ background-position: -40px -380px;
+}
+.emoji-dragon_face {
+ background-position: -60px -380px;
+}
+.emoji-dress {
+ background-position: -80px -380px;
+}
+.emoji-dromedary_camel {
+ background-position: -100px -380px;
+}
+.emoji-drooling_face {
+ background-position: -120px -380px;
+}
+.emoji-droplet {
+ background-position: -140px -380px;
+}
+.emoji-drum {
+ background-position: -160px -380px;
+}
+.emoji-duck {
+ background-position: -180px -380px;
+}
+.emoji-dvd {
+ background-position: -200px -380px;
+}
+.emoji-e-mail {
+ background-position: -220px -380px;
+}
+.emoji-eagle {
+ background-position: -240px -380px;
+}
+.emoji-ear {
+ background-position: -260px -380px;
+}
+.emoji-ear_of_rice {
+ background-position: -280px -380px;
+}
+.emoji-ear_tone1 {
+ background-position: -300px -380px;
+}
+.emoji-ear_tone2 {
+ background-position: -320px -380px;
+}
+.emoji-ear_tone3 {
+ background-position: -340px -380px;
+}
+.emoji-ear_tone4 {
+ background-position: -360px -380px;
+}
+.emoji-ear_tone5 {
+ background-position: -380px -380px;
+}
+.emoji-earth_africa {
+ background-position: -400px 0;
+}
+.emoji-earth_americas {
+ background-position: -400px -20px;
+}
+.emoji-earth_asia {
+ background-position: -400px -40px;
+}
+.emoji-egg {
+ background-position: -400px -60px;
+}
+.emoji-eggplant {
+ background-position: -400px -80px;
+}
+.emoji-eight {
+ background-position: -400px -100px;
+}
+.emoji-eight_pointed_black_star {
+ background-position: -400px -120px;
+}
+.emoji-eight_spoked_asterisk {
+ background-position: -400px -140px;
+}
+.emoji-eject {
+ background-position: -400px -160px;
+}
+.emoji-electric_plug {
+ background-position: -400px -180px;
+}
+.emoji-elephant {
+ background-position: -400px -200px;
+}
+.emoji-end {
+ background-position: -400px -220px;
+}
+.emoji-envelope {
+ background-position: -400px -240px;
+}
+.emoji-envelope_with_arrow {
+ background-position: -400px -260px;
+}
+.emoji-euro {
+ background-position: -400px -280px;
+}
+.emoji-european_castle {
+ background-position: -400px -300px;
+}
+.emoji-european_post_office {
+ background-position: -400px -320px;
+}
+.emoji-evergreen_tree {
+ background-position: -400px -340px;
+}
+.emoji-exclamation {
+ background-position: -400px -360px;
+}
+.emoji-expressionless {
+ background-position: -400px -380px;
+}
+.emoji-eye {
+ background-position: 0 -400px;
+}
+.emoji-eye_in_speech_bubble {
+ background-position: -20px -400px;
+}
+.emoji-eyeglasses {
+ background-position: -40px -400px;
+}
+.emoji-eyes {
+ background-position: -60px -400px;
+}
+.emoji-face_palm {
+ background-position: -80px -400px;
+}
+.emoji-face_palm_tone1 {
+ background-position: -100px -400px;
+}
+.emoji-face_palm_tone2 {
+ background-position: -120px -400px;
+}
+.emoji-face_palm_tone3 {
+ background-position: -140px -400px;
+}
+.emoji-face_palm_tone4 {
+ background-position: -160px -400px;
+}
+.emoji-face_palm_tone5 {
+ background-position: -180px -400px;
+}
+.emoji-factory {
+ background-position: -200px -400px;
+}
+.emoji-fallen_leaf {
+ background-position: -220px -400px;
+}
+.emoji-family {
+ background-position: -240px -400px;
+}
+.emoji-family_mmb {
+ background-position: -260px -400px;
+}
+.emoji-family_mmbb {
+ background-position: -280px -400px;
+}
+.emoji-family_mmg {
+ background-position: -300px -400px;
+}
+.emoji-family_mmgb {
+ background-position: -320px -400px;
+}
+.emoji-family_mmgg {
+ background-position: -340px -400px;
+}
+.emoji-family_mwbb {
+ background-position: -360px -400px;
+}
+.emoji-family_mwg {
+ background-position: -380px -400px;
+}
+.emoji-family_mwgb {
+ background-position: -400px -400px;
+}
+.emoji-family_mwgg {
+ background-position: -420px 0;
+}
+.emoji-family_wwb {
+ background-position: -420px -20px;
+}
+.emoji-family_wwbb {
+ background-position: -420px -40px;
+}
+.emoji-family_wwg {
+ background-position: -420px -60px;
+}
+.emoji-family_wwgb {
+ background-position: -420px -80px;
+}
+.emoji-family_wwgg {
+ background-position: -420px -100px;
+}
+.emoji-fast_forward {
+ background-position: -420px -120px;
+}
+.emoji-fax {
+ background-position: -420px -140px;
+}
+.emoji-fearful {
+ background-position: -420px -160px;
+}
+.emoji-feet {
+ background-position: -420px -180px;
+}
+.emoji-fencer {
+ background-position: -420px -200px;
+}
+.emoji-ferris_wheel {
+ background-position: -420px -220px;
+}
+.emoji-ferry {
+ background-position: -420px -240px;
+}
+.emoji-field_hockey {
+ background-position: -420px -260px;
+}
+.emoji-file_cabinet {
+ background-position: -420px -280px;
+}
+.emoji-file_folder {
+ background-position: -420px -300px;
+}
+.emoji-film_frames {
+ background-position: -420px -320px;
+}
+.emoji-fingers_crossed {
+ background-position: -420px -340px;
+}
+.emoji-fingers_crossed_tone1 {
+ background-position: -420px -360px;
+}
+.emoji-fingers_crossed_tone2 {
+ background-position: -420px -380px;
+}
+.emoji-fingers_crossed_tone3 {
+ background-position: -420px -400px;
+}
+.emoji-fingers_crossed_tone4 {
+ background-position: 0 -420px;
+}
+.emoji-fingers_crossed_tone5 {
+ background-position: -20px -420px;
+}
+.emoji-fire {
+ background-position: -40px -420px;
+}
+.emoji-fire_engine {
+ background-position: -60px -420px;
+}
+.emoji-fireworks {
+ background-position: -80px -420px;
+}
+.emoji-first_place {
+ background-position: -100px -420px;
+}
+.emoji-first_quarter_moon {
+ background-position: -120px -420px;
+}
+.emoji-first_quarter_moon_with_face {
+ background-position: -140px -420px;
+}
+.emoji-fish {
+ background-position: -160px -420px;
+}
+.emoji-fish_cake {
+ background-position: -180px -420px;
+}
+.emoji-fishing_pole_and_fish {
+ background-position: -200px -420px;
+}
+.emoji-fist {
+ background-position: -220px -420px;
+}
+.emoji-fist_tone1 {
+ background-position: -240px -420px;
+}
+.emoji-fist_tone2 {
+ background-position: -260px -420px;
+}
+.emoji-fist_tone3 {
+ background-position: -280px -420px;
+}
+.emoji-fist_tone4 {
+ background-position: -300px -420px;
+}
+.emoji-fist_tone5 {
+ background-position: -320px -420px;
+}
+.emoji-five {
+ background-position: -340px -420px;
+}
+.emoji-flag_ac {
+ background-position: -360px -420px;
+}
+.emoji-flag_ad {
+ background-position: -380px -420px;
+}
+.emoji-flag_ae {
+ background-position: -400px -420px;
+}
+.emoji-flag_af {
+ background-position: -420px -420px;
+}
+.emoji-flag_ag {
+ background-position: -440px 0;
+}
+.emoji-flag_ai {
+ background-position: -440px -20px;
+}
+.emoji-flag_al {
+ background-position: -440px -40px;
+}
+.emoji-flag_am {
+ background-position: -440px -60px;
+}
+.emoji-flag_ao {
+ background-position: -440px -80px;
+}
+.emoji-flag_aq {
+ background-position: -440px -100px;
+}
+.emoji-flag_ar {
+ background-position: -440px -120px;
+}
+.emoji-flag_as {
+ background-position: -440px -140px;
+}
+.emoji-flag_at {
+ background-position: -440px -160px;
+}
+.emoji-flag_au {
+ background-position: -440px -180px;
+}
+.emoji-flag_aw {
+ background-position: -440px -200px;
+}
+.emoji-flag_ax {
+ background-position: -440px -220px;
+}
+.emoji-flag_az {
+ background-position: -440px -240px;
+}
+.emoji-flag_ba {
+ background-position: -440px -260px;
+}
+.emoji-flag_bb {
+ background-position: -440px -280px;
+}
+.emoji-flag_bd {
+ background-position: -440px -300px;
+}
+.emoji-flag_be {
+ background-position: -440px -320px;
+}
+.emoji-flag_bf {
+ background-position: -440px -340px;
+}
+.emoji-flag_bg {
+ background-position: -440px -360px;
+}
+.emoji-flag_bh {
+ background-position: -440px -380px;
+}
+.emoji-flag_bi {
+ background-position: -440px -400px;
+}
+.emoji-flag_bj {
+ background-position: -440px -420px;
+}
+.emoji-flag_bl {
+ background-position: 0 -440px;
+}
+.emoji-flag_black {
+ background-position: -20px -440px;
+}
+.emoji-flag_bm {
+ background-position: -40px -440px;
+}
+.emoji-flag_bn {
+ background-position: -60px -440px;
+}
+.emoji-flag_bo {
+ background-position: -80px -440px;
+}
+.emoji-flag_bq {
+ background-position: -100px -440px;
+}
+.emoji-flag_br {
+ background-position: -120px -440px;
+}
+.emoji-flag_bs {
+ background-position: -140px -440px;
+}
+.emoji-flag_bt {
+ background-position: -160px -440px;
+}
+.emoji-flag_bv {
+ background-position: -180px -440px;
+}
+.emoji-flag_bw {
+ background-position: -200px -440px;
+}
+.emoji-flag_by {
+ background-position: -220px -440px;
+}
+.emoji-flag_bz {
+ background-position: -240px -440px;
+}
+.emoji-flag_ca {
+ background-position: -260px -440px;
+}
+.emoji-flag_cc {
+ background-position: -280px -440px;
+}
+.emoji-flag_cd {
+ background-position: -300px -440px;
+}
+.emoji-flag_cf {
+ background-position: -320px -440px;
+}
+.emoji-flag_cg {
+ background-position: -340px -440px;
+}
+.emoji-flag_ch {
+ background-position: -360px -440px;
+}
+.emoji-flag_ci {
+ background-position: -380px -440px;
+}
+.emoji-flag_ck {
+ background-position: -400px -440px;
+}
+.emoji-flag_cl {
+ background-position: -420px -440px;
+}
+.emoji-flag_cm {
+ background-position: -440px -440px;
+}
+.emoji-flag_cn {
+ background-position: -460px 0;
+}
+.emoji-flag_co {
+ background-position: -460px -20px;
+}
+.emoji-flag_cp {
+ background-position: -460px -40px;
+}
+.emoji-flag_cr {
+ background-position: -460px -60px;
+}
+.emoji-flag_cu {
+ background-position: -460px -80px;
+}
+.emoji-flag_cv {
+ background-position: -460px -100px;
+}
+.emoji-flag_cw {
+ background-position: -460px -120px;
+}
+.emoji-flag_cx {
+ background-position: -460px -140px;
+}
+.emoji-flag_cy {
+ background-position: -460px -160px;
+}
+.emoji-flag_cz {
+ background-position: -460px -180px;
+}
+.emoji-flag_de {
+ background-position: -460px -200px;
+}
+.emoji-flag_dg {
+ background-position: -460px -220px;
+}
+.emoji-flag_dj {
+ background-position: -460px -240px;
+}
+.emoji-flag_dk {
+ background-position: -460px -260px;
+}
+.emoji-flag_dm {
+ background-position: -460px -280px;
+}
+.emoji-flag_do {
+ background-position: -460px -300px;
+}
+.emoji-flag_dz {
+ background-position: -460px -320px;
+}
+.emoji-flag_ea {
+ background-position: -460px -340px;
+}
+.emoji-flag_ec {
+ background-position: -460px -360px;
+}
+.emoji-flag_ee {
+ background-position: -460px -380px;
+}
+.emoji-flag_eg {
+ background-position: -460px -400px;
+}
+.emoji-flag_eh {
+ background-position: -460px -420px;
+}
+.emoji-flag_er {
+ background-position: -460px -440px;
+}
+.emoji-flag_es {
+ background-position: 0 -460px;
+}
+.emoji-flag_et {
+ background-position: -20px -460px;
+}
+.emoji-flag_eu {
+ background-position: -40px -460px;
+}
+.emoji-flag_fi {
+ background-position: -60px -460px;
+}
+.emoji-flag_fj {
+ background-position: -80px -460px;
+}
+.emoji-flag_fk {
+ background-position: -100px -460px;
+}
+.emoji-flag_fm {
+ background-position: -120px -460px;
+}
+.emoji-flag_fo {
+ background-position: -140px -460px;
+}
+.emoji-flag_fr {
+ background-position: -160px -460px;
+}
+.emoji-flag_ga {
+ background-position: -180px -460px;
+}
+.emoji-flag_gb {
+ background-position: -200px -460px;
+}
+.emoji-flag_gd {
+ background-position: -220px -460px;
+}
+.emoji-flag_ge {
+ background-position: -240px -460px;
+}
+.emoji-flag_gf {
+ background-position: -260px -460px;
+}
+.emoji-flag_gg {
+ background-position: -280px -460px;
+}
+.emoji-flag_gh {
+ background-position: -300px -460px;
+}
+.emoji-flag_gi {
+ background-position: -320px -460px;
+}
+.emoji-flag_gl {
+ background-position: -340px -460px;
+}
+.emoji-flag_gm {
+ background-position: -360px -460px;
+}
+.emoji-flag_gn {
+ background-position: -380px -460px;
+}
+.emoji-flag_gp {
+ background-position: -400px -460px;
+}
+.emoji-flag_gq {
+ background-position: -420px -460px;
+}
+.emoji-flag_gr {
+ background-position: -440px -460px;
+}
+.emoji-flag_gs {
+ background-position: -460px -460px;
+}
+.emoji-flag_gt {
+ background-position: -480px 0;
+}
+.emoji-flag_gu {
+ background-position: -480px -20px;
+}
+.emoji-flag_gw {
+ background-position: -480px -40px;
+}
+.emoji-flag_gy {
+ background-position: -480px -60px;
+}
+.emoji-flag_hk {
+ background-position: -480px -80px;
+}
+.emoji-flag_hm {
+ background-position: -480px -100px;
+}
+.emoji-flag_hn {
+ background-position: -480px -120px;
+}
+.emoji-flag_hr {
+ background-position: -480px -140px;
+}
+.emoji-flag_ht {
+ background-position: -480px -160px;
+}
+.emoji-flag_hu {
+ background-position: -480px -180px;
+}
+.emoji-flag_ic {
+ background-position: -480px -200px;
+}
+.emoji-flag_id {
+ background-position: -480px -220px;
+}
+.emoji-flag_ie {
+ background-position: -480px -240px;
+}
+.emoji-flag_il {
+ background-position: -480px -260px;
+}
+.emoji-flag_im {
+ background-position: -480px -280px;
+}
+.emoji-flag_in {
+ background-position: -480px -300px;
+}
+.emoji-flag_io {
+ background-position: -480px -320px;
+}
+.emoji-flag_iq {
+ background-position: -480px -340px;
+}
+.emoji-flag_ir {
+ background-position: -480px -360px;
+}
+.emoji-flag_is {
+ background-position: -480px -380px;
+}
+.emoji-flag_it {
+ background-position: -480px -400px;
+}
+.emoji-flag_je {
+ background-position: -480px -420px;
+}
+.emoji-flag_jm {
+ background-position: -480px -440px;
+}
+.emoji-flag_jo {
+ background-position: -480px -460px;
+}
+.emoji-flag_jp {
+ background-position: 0 -480px;
+}
+.emoji-flag_ke {
+ background-position: -20px -480px;
+}
+.emoji-flag_kg {
+ background-position: -40px -480px;
+}
+.emoji-flag_kh {
+ background-position: -60px -480px;
+}
+.emoji-flag_ki {
+ background-position: -80px -480px;
+}
+.emoji-flag_km {
+ background-position: -100px -480px;
+}
+.emoji-flag_kn {
+ background-position: -120px -480px;
+}
+.emoji-flag_kp {
+ background-position: -140px -480px;
+}
+.emoji-flag_kr {
+ background-position: -160px -480px;
+}
+.emoji-flag_kw {
+ background-position: -180px -480px;
+}
+.emoji-flag_ky {
+ background-position: -200px -480px;
+}
+.emoji-flag_kz {
+ background-position: -220px -480px;
+}
+.emoji-flag_la {
+ background-position: -240px -480px;
+}
+.emoji-flag_lb {
+ background-position: -260px -480px;
+}
+.emoji-flag_lc {
+ background-position: -280px -480px;
+}
+.emoji-flag_li {
+ background-position: -300px -480px;
+}
+.emoji-flag_lk {
+ background-position: -320px -480px;
+}
+.emoji-flag_lr {
+ background-position: -340px -480px;
+}
+.emoji-flag_ls {
+ background-position: -360px -480px;
+}
+.emoji-flag_lt {
+ background-position: -380px -480px;
+}
+.emoji-flag_lu {
+ background-position: -400px -480px;
+}
+.emoji-flag_lv {
+ background-position: -420px -480px;
+}
+.emoji-flag_ly {
+ background-position: -440px -480px;
+}
+.emoji-flag_ma {
+ background-position: -460px -480px;
+}
+.emoji-flag_mc {
+ background-position: -480px -480px;
+}
+.emoji-flag_md {
+ background-position: -500px 0;
+}
+.emoji-flag_me {
+ background-position: -500px -20px;
+}
+.emoji-flag_mf {
+ background-position: -500px -40px;
+}
+.emoji-flag_mg {
+ background-position: -500px -60px;
+}
+.emoji-flag_mh {
+ background-position: -500px -80px;
+}
+.emoji-flag_mk {
+ background-position: -500px -100px;
+}
+.emoji-flag_ml {
+ background-position: -500px -120px;
+}
+.emoji-flag_mm {
+ background-position: -500px -140px;
+}
+.emoji-flag_mn {
+ background-position: -500px -160px;
+}
+.emoji-flag_mo {
+ background-position: -500px -180px;
+}
+.emoji-flag_mp {
+ background-position: -500px -200px;
+}
+.emoji-flag_mq {
+ background-position: -500px -220px;
+}
+.emoji-flag_mr {
+ background-position: -500px -240px;
+}
+.emoji-flag_ms {
+ background-position: -500px -260px;
+}
+.emoji-flag_mt {
+ background-position: -500px -280px;
+}
+.emoji-flag_mu {
+ background-position: -500px -300px;
+}
+.emoji-flag_mv {
+ background-position: -500px -320px;
+}
+.emoji-flag_mw {
+ background-position: -500px -340px;
+}
+.emoji-flag_mx {
+ background-position: -500px -360px;
+}
+.emoji-flag_my {
+ background-position: -500px -380px;
+}
+.emoji-flag_mz {
+ background-position: -500px -400px;
+}
+.emoji-flag_na {
+ background-position: -500px -420px;
+}
+.emoji-flag_nc {
+ background-position: -500px -440px;
+}
+.emoji-flag_ne {
+ background-position: -500px -460px;
+}
+.emoji-flag_nf {
+ background-position: -500px -480px;
+}
+.emoji-flag_ng {
+ background-position: 0 -500px;
+}
+.emoji-flag_ni {
+ background-position: -20px -500px;
+}
+.emoji-flag_nl {
+ background-position: -40px -500px;
+}
+.emoji-flag_no {
+ background-position: -60px -500px;
+}
+.emoji-flag_np {
+ background-position: -80px -500px;
+}
+.emoji-flag_nr {
+ background-position: -100px -500px;
+}
+.emoji-flag_nu {
+ background-position: -120px -500px;
+}
+.emoji-flag_nz {
+ background-position: -140px -500px;
+}
+.emoji-flag_om {
+ background-position: -160px -500px;
+}
+.emoji-flag_pa {
+ background-position: -180px -500px;
+}
+.emoji-flag_pe {
+ background-position: -200px -500px;
+}
+.emoji-flag_pf {
+ background-position: -220px -500px;
+}
+.emoji-flag_pg {
+ background-position: -240px -500px;
+}
+.emoji-flag_ph {
+ background-position: -260px -500px;
+}
+.emoji-flag_pk {
+ background-position: -280px -500px;
+}
+.emoji-flag_pl {
+ background-position: -300px -500px;
+}
+.emoji-flag_pm {
+ background-position: -320px -500px;
+}
+.emoji-flag_pn {
+ background-position: -340px -500px;
+}
+.emoji-flag_pr {
+ background-position: -360px -500px;
+}
+.emoji-flag_ps {
+ background-position: -380px -500px;
+}
+.emoji-flag_pt {
+ background-position: -400px -500px;
+}
+.emoji-flag_pw {
+ background-position: -420px -500px;
+}
+.emoji-flag_py {
+ background-position: -440px -500px;
+}
+.emoji-flag_qa {
+ background-position: -460px -500px;
+}
+.emoji-flag_re {
+ background-position: -480px -500px;
+}
+.emoji-flag_ro {
+ background-position: -500px -500px;
+}
+.emoji-flag_rs {
+ background-position: -520px 0;
+}
+.emoji-flag_ru {
+ background-position: -520px -20px;
+}
+.emoji-flag_rw {
+ background-position: -520px -40px;
+}
+.emoji-flag_sa {
+ background-position: -520px -60px;
+}
+.emoji-flag_sb {
+ background-position: -520px -80px;
+}
+.emoji-flag_sc {
+ background-position: -520px -100px;
+}
+.emoji-flag_sd {
+ background-position: -520px -120px;
+}
+.emoji-flag_se {
+ background-position: -520px -140px;
+}
+.emoji-flag_sg {
+ background-position: -520px -160px;
+}
+.emoji-flag_sh {
+ background-position: -520px -180px;
+}
+.emoji-flag_si {
+ background-position: -520px -200px;
+}
+.emoji-flag_sj {
+ background-position: -520px -220px;
+}
+.emoji-flag_sk {
+ background-position: -520px -240px;
+}
+.emoji-flag_sl {
+ background-position: -520px -260px;
+}
+.emoji-flag_sm {
+ background-position: -520px -280px;
+}
+.emoji-flag_sn {
+ background-position: -520px -300px;
+}
+.emoji-flag_so {
+ background-position: -520px -320px;
+}
+.emoji-flag_sr {
+ background-position: -520px -340px;
+}
+.emoji-flag_ss {
+ background-position: -520px -360px;
+}
+.emoji-flag_st {
+ background-position: -520px -380px;
+}
+.emoji-flag_sv {
+ background-position: -520px -400px;
+}
+.emoji-flag_sx {
+ background-position: -520px -420px;
+}
+.emoji-flag_sy {
+ background-position: -520px -440px;
+}
+.emoji-flag_sz {
+ background-position: -520px -460px;
+}
+.emoji-flag_ta {
+ background-position: -520px -480px;
+}
+.emoji-flag_tc {
+ background-position: -520px -500px;
+}
+.emoji-flag_td {
+ background-position: 0 -520px;
+}
+.emoji-flag_tf {
+ background-position: -20px -520px;
+}
+.emoji-flag_tg {
+ background-position: -40px -520px;
+}
+.emoji-flag_th {
+ background-position: -60px -520px;
+}
+.emoji-flag_tj {
+ background-position: -80px -520px;
+}
+.emoji-flag_tk {
+ background-position: -100px -520px;
+}
+.emoji-flag_tl {
+ background-position: -120px -520px;
+}
+.emoji-flag_tm {
+ background-position: -140px -520px;
+}
+.emoji-flag_tn {
+ background-position: -160px -520px;
+}
+.emoji-flag_to {
+ background-position: -180px -520px;
+}
+.emoji-flag_tr {
+ background-position: -200px -520px;
+}
+.emoji-flag_tt {
+ background-position: -220px -520px;
+}
+.emoji-flag_tv {
+ background-position: -240px -520px;
+}
+.emoji-flag_tw {
+ background-position: -260px -520px;
+}
+.emoji-flag_tz {
+ background-position: -280px -520px;
+}
+.emoji-flag_ua {
+ background-position: -300px -520px;
+}
+.emoji-flag_ug {
+ background-position: -320px -520px;
+}
+.emoji-flag_um {
+ background-position: -340px -520px;
+}
+.emoji-flag_us {
+ background-position: -360px -520px;
+}
+.emoji-flag_uy {
+ background-position: -380px -520px;
+}
+.emoji-flag_uz {
+ background-position: -400px -520px;
+}
+.emoji-flag_va {
+ background-position: -420px -520px;
+}
+.emoji-flag_vc {
+ background-position: -440px -520px;
+}
+.emoji-flag_ve {
+ background-position: -460px -520px;
+}
+.emoji-flag_vg {
+ background-position: -480px -520px;
+}
+.emoji-flag_vi {
+ background-position: -500px -520px;
+}
+.emoji-flag_vn {
+ background-position: -520px -520px;
+}
+.emoji-flag_vu {
+ background-position: -540px 0;
+}
+.emoji-flag_wf {
+ background-position: -540px -20px;
+}
+.emoji-flag_white {
+ background-position: -540px -40px;
+}
+.emoji-flag_ws {
+ background-position: -540px -60px;
+}
+.emoji-flag_xk {
+ background-position: -540px -80px;
+}
+.emoji-flag_ye {
+ background-position: -540px -100px;
+}
+.emoji-flag_yt {
+ background-position: -540px -120px;
+}
+.emoji-flag_za {
+ background-position: -540px -140px;
+}
+.emoji-flag_zm {
+ background-position: -540px -160px;
+}
+.emoji-flag_zw {
+ background-position: -540px -180px;
+}
+.emoji-flags {
+ background-position: -540px -200px;
+}
+.emoji-flashlight {
+ background-position: -540px -220px;
+}
+.emoji-fleur-de-lis {
+ background-position: -540px -240px;
+}
+.emoji-floppy_disk {
+ background-position: -540px -260px;
+}
+.emoji-flower_playing_cards {
+ background-position: -540px -280px;
+}
+.emoji-flushed {
+ background-position: -540px -300px;
+}
+.emoji-fog {
+ background-position: -540px -320px;
+}
+.emoji-foggy {
+ background-position: -540px -340px;
+}
+.emoji-football {
+ background-position: -540px -360px;
+}
+.emoji-footprints {
+ background-position: -540px -380px;
+}
+.emoji-fork_and_knife {
+ background-position: -540px -400px;
+}
+.emoji-fork_knife_plate {
+ background-position: -540px -420px;
+}
+.emoji-fountain {
+ background-position: -540px -440px;
+}
+.emoji-four {
+ background-position: -540px -460px;
+}
+.emoji-four_leaf_clover {
+ background-position: -540px -480px;
+}
+.emoji-fox {
+ background-position: -540px -500px;
+}
+.emoji-frame_photo {
+ background-position: -540px -520px;
+}
+.emoji-free {
+ background-position: 0 -540px;
+}
+.emoji-french_bread {
+ background-position: -20px -540px;
+}
+.emoji-fried_shrimp {
+ background-position: -40px -540px;
+}
+.emoji-fries {
+ background-position: -60px -540px;
+}
+.emoji-frog {
+ background-position: -80px -540px;
+}
+.emoji-frowning {
+ background-position: -100px -540px;
+}
+.emoji-frowning2 {
+ background-position: -120px -540px;
+}
+.emoji-fuelpump {
+ background-position: -140px -540px;
+}
+.emoji-full_moon {
+ background-position: -160px -540px;
+}
+.emoji-full_moon_with_face {
+ background-position: -180px -540px;
+}
+.emoji-game_die {
+ background-position: -200px -540px;
+}
+.emoji-gay_pride_flag {
+ background-position: -220px -540px;
+}
+.emoji-gear {
+ background-position: -240px -540px;
+}
+.emoji-gem {
+ background-position: -260px -540px;
+}
+.emoji-gemini {
+ background-position: -280px -540px;
+}
+.emoji-ghost {
+ background-position: -300px -540px;
+}
+.emoji-gift {
+ background-position: -320px -540px;
+}
+.emoji-gift_heart {
+ background-position: -340px -540px;
+}
+.emoji-girl {
+ background-position: -360px -540px;
+}
+.emoji-girl_tone1 {
+ background-position: -380px -540px;
+}
+.emoji-girl_tone2 {
+ background-position: -400px -540px;
+}
+.emoji-girl_tone3 {
+ background-position: -420px -540px;
+}
+.emoji-girl_tone4 {
+ background-position: -440px -540px;
+}
+.emoji-girl_tone5 {
+ background-position: -460px -540px;
+}
+.emoji-globe_with_meridians {
+ background-position: -480px -540px;
+}
+.emoji-goal {
+ background-position: -500px -540px;
+}
+.emoji-goat {
+ background-position: -520px -540px;
+}
+.emoji-golf {
+ background-position: -540px -540px;
+}
+.emoji-golfer {
+ background-position: -560px 0;
+}
+.emoji-gorilla {
+ background-position: -560px -20px;
+}
+.emoji-grapes {
+ background-position: -560px -40px;
+}
+.emoji-green_apple {
+ background-position: -560px -60px;
+}
+.emoji-green_book {
+ background-position: -560px -80px;
+}
+.emoji-green_heart {
+ background-position: -560px -100px;
+}
+.emoji-grey_exclamation {
+ background-position: -560px -120px;
+}
+.emoji-grey_question {
+ background-position: -560px -140px;
+}
+.emoji-grimacing {
+ background-position: -560px -160px;
+}
+.emoji-grin {
+ background-position: -560px -180px;
+}
+.emoji-grinning {
+ background-position: -560px -200px;
+}
+.emoji-guardsman {
+ background-position: -560px -220px;
+}
+.emoji-guardsman_tone1 {
+ background-position: -560px -240px;
+}
+.emoji-guardsman_tone2 {
+ background-position: -560px -260px;
+}
+.emoji-guardsman_tone3 {
+ background-position: -560px -280px;
+}
+.emoji-guardsman_tone4 {
+ background-position: -560px -300px;
+}
+.emoji-guardsman_tone5 {
+ background-position: -560px -320px;
+}
+.emoji-guitar {
+ background-position: -560px -340px;
+}
+.emoji-gun {
+ background-position: -560px -360px;
+}
+.emoji-haircut {
+ background-position: -560px -380px;
+}
+.emoji-haircut_tone1 {
+ background-position: -560px -400px;
+}
+.emoji-haircut_tone2 {
+ background-position: -560px -420px;
+}
+.emoji-haircut_tone3 {
+ background-position: -560px -440px;
+}
+.emoji-haircut_tone4 {
+ background-position: -560px -460px;
+}
+.emoji-haircut_tone5 {
+ background-position: -560px -480px;
+}
+.emoji-hamburger {
+ background-position: -560px -500px;
+}
+.emoji-hammer {
+ background-position: -560px -520px;
+}
+.emoji-hammer_pick {
+ background-position: -560px -540px;
+}
+.emoji-hamster {
+ background-position: 0 -560px;
+}
+.emoji-hand_splayed {
+ background-position: -20px -560px;
+}
+.emoji-hand_splayed_tone1 {
+ background-position: -40px -560px;
+}
+.emoji-hand_splayed_tone2 {
+ background-position: -60px -560px;
+}
+.emoji-hand_splayed_tone3 {
+ background-position: -80px -560px;
+}
+.emoji-hand_splayed_tone4 {
+ background-position: -100px -560px;
+}
+.emoji-hand_splayed_tone5 {
+ background-position: -120px -560px;
+}
+.emoji-handbag {
+ background-position: -140px -560px;
+}
+.emoji-handball {
+ background-position: -160px -560px;
+}
+.emoji-handball_tone1 {
+ background-position: -180px -560px;
+}
+.emoji-handball_tone2 {
+ background-position: -200px -560px;
+}
+.emoji-handball_tone3 {
+ background-position: -220px -560px;
+}
+.emoji-handball_tone4 {
+ background-position: -240px -560px;
+}
+.emoji-handball_tone5 {
+ background-position: -260px -560px;
+}
+.emoji-handshake {
+ background-position: -280px -560px;
+}
+.emoji-handshake_tone1 {
+ background-position: -300px -560px;
+}
+.emoji-handshake_tone2 {
+ background-position: -320px -560px;
+}
+.emoji-handshake_tone3 {
+ background-position: -340px -560px;
+}
+.emoji-handshake_tone4 {
+ background-position: -360px -560px;
+}
+.emoji-handshake_tone5 {
+ background-position: -380px -560px;
+}
+.emoji-hash {
+ background-position: -400px -560px;
+}
+.emoji-hatched_chick {
+ background-position: -420px -560px;
+}
+.emoji-hatching_chick {
+ background-position: -440px -560px;
+}
+.emoji-head_bandage {
+ background-position: -460px -560px;
+}
+.emoji-headphones {
+ background-position: -480px -560px;
+}
+.emoji-hear_no_evil {
+ background-position: -500px -560px;
+}
+.emoji-heart {
+ background-position: -520px -560px;
+}
+.emoji-heart_decoration {
+ background-position: -540px -560px;
+}
+.emoji-heart_exclamation {
+ background-position: -560px -560px;
+}
+.emoji-heart_eyes {
+ background-position: -580px 0;
+}
+.emoji-heart_eyes_cat {
+ background-position: -580px -20px;
+}
+.emoji-heartbeat {
+ background-position: -580px -40px;
+}
+.emoji-heartpulse {
+ background-position: -580px -60px;
+}
+.emoji-hearts {
+ background-position: -580px -80px;
+}
+.emoji-heavy_check_mark {
+ background-position: -580px -100px;
+}
+.emoji-heavy_division_sign {
+ background-position: -580px -120px;
+}
+.emoji-heavy_dollar_sign {
+ background-position: -580px -140px;
+}
+.emoji-heavy_minus_sign {
+ background-position: -580px -160px;
+}
+.emoji-heavy_multiplication_x {
+ background-position: -580px -180px;
+}
+.emoji-heavy_plus_sign {
+ background-position: -580px -200px;
+}
+.emoji-helicopter {
+ background-position: -580px -220px;
+}
+.emoji-helmet_with_cross {
+ background-position: -580px -240px;
+}
+.emoji-herb {
+ background-position: -580px -260px;
+}
+.emoji-hibiscus {
+ background-position: -580px -280px;
+}
+.emoji-high_brightness {
+ background-position: -580px -300px;
+}
+.emoji-high_heel {
+ background-position: -580px -320px;
+}
+.emoji-hockey {
+ background-position: -580px -340px;
+}
+.emoji-hole {
+ background-position: -580px -360px;
+}
+.emoji-homes {
+ background-position: -580px -380px;
+}
+.emoji-honey_pot {
+ background-position: -580px -400px;
+}
+.emoji-horse {
+ background-position: -580px -420px;
+}
+.emoji-horse_racing {
+ background-position: -580px -440px;
+}
+.emoji-horse_racing_tone1 {
+ background-position: -580px -460px;
+}
+.emoji-horse_racing_tone2 {
+ background-position: -580px -480px;
+}
+.emoji-horse_racing_tone3 {
+ background-position: -580px -500px;
+}
+.emoji-horse_racing_tone4 {
+ background-position: -580px -520px;
+}
+.emoji-horse_racing_tone5 {
+ background-position: -580px -540px;
+}
+.emoji-hospital {
+ background-position: -580px -560px;
+}
+.emoji-hot_pepper {
+ background-position: 0 -580px;
+}
+.emoji-hotdog {
+ background-position: -20px -580px;
+}
+.emoji-hotel {
+ background-position: -40px -580px;
+}
+.emoji-hotsprings {
+ background-position: -60px -580px;
+}
+.emoji-hourglass {
+ background-position: -80px -580px;
+}
+.emoji-hourglass_flowing_sand {
+ background-position: -100px -580px;
+}
+.emoji-house {
+ background-position: -120px -580px;
+}
+.emoji-house_abandoned {
+ background-position: -140px -580px;
+}
+.emoji-house_with_garden {
+ background-position: -160px -580px;
+}
+.emoji-hugging {
+ background-position: -180px -580px;
+}
+.emoji-hushed {
+ background-position: -200px -580px;
+}
+.emoji-ice_cream {
+ background-position: -220px -580px;
+}
+.emoji-ice_skate {
+ background-position: -240px -580px;
+}
+.emoji-icecream {
+ background-position: -260px -580px;
+}
+.emoji-id {
+ background-position: -280px -580px;
+}
+.emoji-ideograph_advantage {
+ background-position: -300px -580px;
+}
+.emoji-imp {
+ background-position: -320px -580px;
+}
+.emoji-inbox_tray {
+ background-position: -340px -580px;
+}
+.emoji-incoming_envelope {
+ background-position: -360px -580px;
+}
+.emoji-information_desk_person {
+ background-position: -380px -580px;
+}
+.emoji-information_desk_person_tone1 {
+ background-position: -400px -580px;
+}
+.emoji-information_desk_person_tone2 {
+ background-position: -420px -580px;
+}
+.emoji-information_desk_person_tone3 {
+ background-position: -440px -580px;
+}
+.emoji-information_desk_person_tone4 {
+ background-position: -460px -580px;
+}
+.emoji-information_desk_person_tone5 {
+ background-position: -480px -580px;
+}
+.emoji-information_source {
+ background-position: -500px -580px;
+}
+.emoji-innocent {
+ background-position: -520px -580px;
+}
+.emoji-interrobang {
+ background-position: -540px -580px;
+}
+.emoji-iphone {
+ background-position: -560px -580px;
+}
+.emoji-island {
+ background-position: -580px -580px;
+}
+.emoji-izakaya_lantern {
+ background-position: -600px 0;
+}
+.emoji-jack_o_lantern {
+ background-position: -600px -20px;
+}
+.emoji-japan {
+ background-position: -600px -40px;
+}
+.emoji-japanese_castle {
+ background-position: -600px -60px;
+}
+.emoji-japanese_goblin {
+ background-position: -600px -80px;
+}
+.emoji-japanese_ogre {
+ background-position: -600px -100px;
+}
+.emoji-jeans {
+ background-position: -600px -120px;
+}
+.emoji-joy {
+ background-position: -600px -140px;
+}
+.emoji-joy_cat {
+ background-position: -600px -160px;
+}
+.emoji-joystick {
+ background-position: -600px -180px;
+}
+.emoji-juggling {
+ background-position: -600px -200px;
+}
+.emoji-juggling_tone1 {
+ background-position: -600px -220px;
+}
+.emoji-juggling_tone2 {
+ background-position: -600px -240px;
+}
+.emoji-juggling_tone3 {
+ background-position: -600px -260px;
+}
+.emoji-juggling_tone4 {
+ background-position: -600px -280px;
+}
+.emoji-juggling_tone5 {
+ background-position: -600px -300px;
+}
+.emoji-kaaba {
+ background-position: -600px -320px;
+}
+.emoji-key {
+ background-position: -600px -340px;
+}
+.emoji-key2 {
+ background-position: -600px -360px;
+}
+.emoji-keyboard {
+ background-position: -600px -380px;
+}
+.emoji-kimono {
+ background-position: -600px -400px;
+}
+.emoji-kiss {
+ background-position: -600px -420px;
+}
+.emoji-kiss_mm {
+ background-position: -600px -440px;
+}
+.emoji-kiss_ww {
+ background-position: -600px -460px;
+}
+.emoji-kissing {
+ background-position: -600px -480px;
+}
+.emoji-kissing_cat {
+ background-position: -600px -500px;
+}
+.emoji-kissing_closed_eyes {
+ background-position: -600px -520px;
+}
+.emoji-kissing_heart {
+ background-position: -600px -540px;
+}
+.emoji-kissing_smiling_eyes {
+ background-position: -600px -560px;
+}
+.emoji-kiwi {
+ background-position: -600px -580px;
+}
+.emoji-knife {
+ background-position: 0 -600px;
+}
+.emoji-koala {
+ background-position: -20px -600px;
+}
+.emoji-koko {
+ background-position: -40px -600px;
+}
+.emoji-label {
+ background-position: -60px -600px;
+}
+.emoji-large_blue_circle {
+ background-position: -80px -600px;
+}
+.emoji-large_blue_diamond {
+ background-position: -100px -600px;
+}
+.emoji-large_orange_diamond {
+ background-position: -120px -600px;
+}
+.emoji-last_quarter_moon {
+ background-position: -140px -600px;
+}
+.emoji-last_quarter_moon_with_face {
+ background-position: -160px -600px;
+}
+.emoji-laughing {
+ background-position: -180px -600px;
+}
+.emoji-leaves {
+ background-position: -200px -600px;
+}
+.emoji-ledger {
+ background-position: -220px -600px;
+}
+.emoji-left_facing_fist {
+ background-position: -240px -600px;
+}
+.emoji-left_facing_fist_tone1 {
+ background-position: -260px -600px;
+}
+.emoji-left_facing_fist_tone2 {
+ background-position: -280px -600px;
+}
+.emoji-left_facing_fist_tone3 {
+ background-position: -300px -600px;
+}
+.emoji-left_facing_fist_tone4 {
+ background-position: -320px -600px;
+}
+.emoji-left_facing_fist_tone5 {
+ background-position: -340px -600px;
+}
+.emoji-left_luggage {
+ background-position: -360px -600px;
+}
+.emoji-left_right_arrow {
+ background-position: -380px -600px;
+}
+.emoji-leftwards_arrow_with_hook {
+ background-position: -400px -600px;
+}
+.emoji-lemon {
+ background-position: -420px -600px;
+}
+.emoji-leo {
+ background-position: -440px -600px;
+}
+.emoji-leopard {
+ background-position: -460px -600px;
+}
+.emoji-level_slider {
+ background-position: -480px -600px;
+}
+.emoji-levitate {
+ background-position: -500px -600px;
+}
+.emoji-libra {
+ background-position: -520px -600px;
+}
+.emoji-lifter {
+ background-position: -540px -600px;
+}
+.emoji-lifter_tone1 {
+ background-position: -560px -600px;
+}
+.emoji-lifter_tone2 {
+ background-position: -580px -600px;
+}
+.emoji-lifter_tone3 {
+ background-position: -600px -600px;
+}
+.emoji-lifter_tone4 {
+ background-position: -620px 0;
+}
+.emoji-lifter_tone5 {
+ background-position: -620px -20px;
+}
+.emoji-light_rail {
+ background-position: -620px -40px;
+}
+.emoji-link {
+ background-position: -620px -60px;
+}
+.emoji-lion_face {
+ background-position: -620px -80px;
+}
+.emoji-lips {
+ background-position: -620px -100px;
+}
+.emoji-lipstick {
+ background-position: -620px -120px;
+}
+.emoji-lizard {
+ background-position: -620px -140px;
+}
+.emoji-lock {
+ background-position: -620px -160px;
+}
+.emoji-lock_with_ink_pen {
+ background-position: -620px -180px;
+}
+.emoji-lollipop {
+ background-position: -620px -200px;
+}
+.emoji-loop {
+ background-position: -620px -220px;
+}
+.emoji-loud_sound {
+ background-position: -620px -240px;
+}
+.emoji-loudspeaker {
+ background-position: -620px -260px;
+}
+.emoji-love_hotel {
+ background-position: -620px -280px;
+}
+.emoji-love_letter {
+ background-position: -620px -300px;
+}
+.emoji-low_brightness {
+ background-position: -620px -320px;
+}
+.emoji-lying_face {
+ background-position: -620px -340px;
+}
+.emoji-m {
+ background-position: -620px -360px;
+}
+.emoji-mag {
+ background-position: -620px -380px;
+}
+.emoji-mag_right {
+ background-position: -620px -400px;
+}
+.emoji-mahjong {
+ background-position: -620px -420px;
+}
+.emoji-mailbox {
+ background-position: -620px -440px;
+}
+.emoji-mailbox_closed {
+ background-position: -620px -460px;
+}
+.emoji-mailbox_with_mail {
+ background-position: -620px -480px;
+}
+.emoji-mailbox_with_no_mail {
+ background-position: -620px -500px;
+}
+.emoji-man {
+ background-position: -620px -520px;
+}
+.emoji-man_dancing {
+ background-position: -620px -540px;
+}
+.emoji-man_dancing_tone1 {
+ background-position: -620px -560px;
+}
+.emoji-man_dancing_tone2 {
+ background-position: -620px -580px;
+}
+.emoji-man_dancing_tone3 {
+ background-position: -620px -600px;
+}
+.emoji-man_dancing_tone4 {
+ background-position: 0 -620px;
+}
+.emoji-man_dancing_tone5 {
+ background-position: -20px -620px;
+}
+.emoji-man_in_tuxedo {
+ background-position: -40px -620px;
+}
+.emoji-man_in_tuxedo_tone1 {
+ background-position: -60px -620px;
+}
+.emoji-man_in_tuxedo_tone2 {
+ background-position: -80px -620px;
+}
+.emoji-man_in_tuxedo_tone3 {
+ background-position: -100px -620px;
+}
+.emoji-man_in_tuxedo_tone4 {
+ background-position: -120px -620px;
+}
+.emoji-man_in_tuxedo_tone5 {
+ background-position: -140px -620px;
+}
+.emoji-man_tone1 {
+ background-position: -160px -620px;
+}
+.emoji-man_tone2 {
+ background-position: -180px -620px;
+}
+.emoji-man_tone3 {
+ background-position: -200px -620px;
+}
+.emoji-man_tone4 {
+ background-position: -220px -620px;
+}
+.emoji-man_tone5 {
+ background-position: -240px -620px;
+}
+.emoji-man_with_gua_pi_mao {
+ background-position: -260px -620px;
+}
+.emoji-man_with_gua_pi_mao_tone1 {
+ background-position: -280px -620px;
+}
+.emoji-man_with_gua_pi_mao_tone2 {
+ background-position: -300px -620px;
+}
+.emoji-man_with_gua_pi_mao_tone3 {
+ background-position: -320px -620px;
+}
+.emoji-man_with_gua_pi_mao_tone4 {
+ background-position: -340px -620px;
+}
+.emoji-man_with_gua_pi_mao_tone5 {
+ background-position: -360px -620px;
+}
+.emoji-man_with_turban {
+ background-position: -380px -620px;
+}
+.emoji-man_with_turban_tone1 {
+ background-position: -400px -620px;
+}
+.emoji-man_with_turban_tone2 {
+ background-position: -420px -620px;
+}
+.emoji-man_with_turban_tone3 {
+ background-position: -440px -620px;
+}
+.emoji-man_with_turban_tone4 {
+ background-position: -460px -620px;
+}
+.emoji-man_with_turban_tone5 {
+ background-position: -480px -620px;
+}
+.emoji-mans_shoe {
+ background-position: -500px -620px;
+}
+.emoji-map {
+ background-position: -520px -620px;
+}
+.emoji-maple_leaf {
+ background-position: -540px -620px;
+}
+.emoji-martial_arts_uniform {
+ background-position: -560px -620px;
+}
+.emoji-mask {
+ background-position: -580px -620px;
+}
+.emoji-massage {
+ background-position: -600px -620px;
+}
+.emoji-massage_tone1 {
+ background-position: -620px -620px;
+}
+.emoji-massage_tone2 {
+ background-position: -640px 0;
+}
+.emoji-massage_tone3 {
+ background-position: -640px -20px;
+}
+.emoji-massage_tone4 {
+ background-position: -640px -40px;
+}
+.emoji-massage_tone5 {
+ background-position: -640px -60px;
+}
+.emoji-meat_on_bone {
+ background-position: -640px -80px;
+}
+.emoji-medal {
+ background-position: -640px -100px;
+}
+.emoji-mega {
+ background-position: -640px -120px;
+}
+.emoji-melon {
+ background-position: -640px -140px;
+}
+.emoji-menorah {
+ background-position: -640px -160px;
+}
+.emoji-mens {
+ background-position: -640px -180px;
+}
+.emoji-metal {
+ background-position: -640px -200px;
+}
+.emoji-metal_tone1 {
+ background-position: -640px -220px;
+}
+.emoji-metal_tone2 {
+ background-position: -640px -240px;
+}
+.emoji-metal_tone3 {
+ background-position: -640px -260px;
+}
+.emoji-metal_tone4 {
+ background-position: -640px -280px;
+}
+.emoji-metal_tone5 {
+ background-position: -640px -300px;
+}
+.emoji-metro {
+ background-position: -640px -320px;
+}
+.emoji-microphone {
+ background-position: -640px -340px;
+}
+.emoji-microphone2 {
+ background-position: -640px -360px;
+}
+.emoji-microscope {
+ background-position: -640px -380px;
+}
+.emoji-middle_finger {
+ background-position: -640px -400px;
+}
+.emoji-middle_finger_tone1 {
+ background-position: -640px -420px;
+}
+.emoji-middle_finger_tone2 {
+ background-position: -640px -440px;
+}
+.emoji-middle_finger_tone3 {
+ background-position: -640px -460px;
+}
+.emoji-middle_finger_tone4 {
+ background-position: -640px -480px;
+}
+.emoji-middle_finger_tone5 {
+ background-position: -640px -500px;
+}
+.emoji-military_medal {
+ background-position: -640px -520px;
+}
+.emoji-milk {
+ background-position: -640px -540px;
+}
+.emoji-milky_way {
+ background-position: -640px -560px;
+}
+.emoji-minibus {
+ background-position: -640px -580px;
+}
+.emoji-minidisc {
+ background-position: -640px -600px;
+}
+.emoji-mobile_phone_off {
+ background-position: -640px -620px;
+}
+.emoji-money_mouth {
+ background-position: 0 -640px;
+}
+.emoji-money_with_wings {
+ background-position: -20px -640px;
+}
+.emoji-moneybag {
+ background-position: -40px -640px;
+}
+.emoji-monkey {
+ background-position: -60px -640px;
+}
+.emoji-monkey_face {
+ background-position: -80px -640px;
+}
+.emoji-monorail {
+ background-position: -100px -640px;
+}
+.emoji-mortar_board {
+ background-position: -120px -640px;
+}
+.emoji-mosque {
+ background-position: -140px -640px;
+}
+.emoji-motor_scooter {
+ background-position: -160px -640px;
+}
+.emoji-motorboat {
+ background-position: -180px -640px;
+}
+.emoji-motorcycle {
+ background-position: -200px -640px;
+}
+.emoji-motorway {
+ background-position: -220px -640px;
+}
+.emoji-mount_fuji {
+ background-position: -240px -640px;
+}
+.emoji-mountain {
+ background-position: -260px -640px;
+}
+.emoji-mountain_bicyclist {
+ background-position: -280px -640px;
+}
+.emoji-mountain_bicyclist_tone1 {
+ background-position: -300px -640px;
+}
+.emoji-mountain_bicyclist_tone2 {
+ background-position: -320px -640px;
+}
+.emoji-mountain_bicyclist_tone3 {
+ background-position: -340px -640px;
+}
+.emoji-mountain_bicyclist_tone4 {
+ background-position: -360px -640px;
+}
+.emoji-mountain_bicyclist_tone5 {
+ background-position: -380px -640px;
+}
+.emoji-mountain_cableway {
+ background-position: -400px -640px;
+}
+.emoji-mountain_railway {
+ background-position: -420px -640px;
+}
+.emoji-mountain_snow {
+ background-position: -440px -640px;
+}
+.emoji-mouse {
+ background-position: -460px -640px;
+}
+.emoji-mouse2 {
+ background-position: -480px -640px;
+}
+.emoji-mouse_three_button {
+ background-position: -500px -640px;
+}
+.emoji-movie_camera {
+ background-position: -520px -640px;
+}
+.emoji-moyai {
+ background-position: -540px -640px;
+}
+.emoji-mrs_claus {
+ background-position: -560px -640px;
+}
+.emoji-mrs_claus_tone1 {
+ background-position: -580px -640px;
+}
+.emoji-mrs_claus_tone2 {
+ background-position: -600px -640px;
+}
+.emoji-mrs_claus_tone3 {
+ background-position: -620px -640px;
+}
+.emoji-mrs_claus_tone4 {
+ background-position: -640px -640px;
+}
+.emoji-mrs_claus_tone5 {
+ background-position: -660px 0;
+}
+.emoji-muscle {
+ background-position: -660px -20px;
+}
+.emoji-muscle_tone1 {
+ background-position: -660px -40px;
+}
+.emoji-muscle_tone2 {
+ background-position: -660px -60px;
+}
+.emoji-muscle_tone3 {
+ background-position: -660px -80px;
+}
+.emoji-muscle_tone4 {
+ background-position: -660px -100px;
+}
+.emoji-muscle_tone5 {
+ background-position: -660px -120px;
+}
+.emoji-mushroom {
+ background-position: -660px -140px;
+}
+.emoji-musical_keyboard {
+ background-position: -660px -160px;
+}
+.emoji-musical_note {
+ background-position: -660px -180px;
+}
+.emoji-musical_score {
+ background-position: -660px -200px;
+}
+.emoji-mute {
+ background-position: -660px -220px;
+}
+.emoji-nail_care {
+ background-position: -660px -240px;
+}
+.emoji-nail_care_tone1 {
+ background-position: -660px -260px;
+}
+.emoji-nail_care_tone2 {
+ background-position: -660px -280px;
+}
+.emoji-nail_care_tone3 {
+ background-position: -660px -300px;
+}
+.emoji-nail_care_tone4 {
+ background-position: -660px -320px;
+}
+.emoji-nail_care_tone5 {
+ background-position: -660px -340px;
+}
+.emoji-name_badge {
+ background-position: -660px -360px;
+}
+.emoji-nauseated_face {
+ background-position: -660px -380px;
+}
+.emoji-necktie {
+ background-position: -660px -400px;
+}
+.emoji-negative_squared_cross_mark {
+ background-position: -660px -420px;
+}
+.emoji-nerd {
+ background-position: -660px -440px;
+}
+.emoji-neutral_face {
+ background-position: -660px -460px;
+}
+.emoji-new {
+ background-position: -660px -480px;
+}
+.emoji-new_moon {
+ background-position: -660px -500px;
+}
+.emoji-new_moon_with_face {
+ background-position: -660px -520px;
+}
+.emoji-newspaper {
+ background-position: -660px -540px;
+}
+.emoji-newspaper2 {
+ background-position: -660px -560px;
+}
+.emoji-ng {
+ background-position: -660px -580px;
+}
+.emoji-night_with_stars {
+ background-position: -660px -600px;
+}
+.emoji-nine {
+ background-position: -660px -620px;
+}
+.emoji-no_bell {
+ background-position: -660px -640px;
+}
+.emoji-no_bicycles {
+ background-position: 0 -660px;
+}
+.emoji-no_entry {
+ background-position: -20px -660px;
+}
+.emoji-no_entry_sign {
+ background-position: -40px -660px;
+}
+.emoji-no_good {
+ background-position: -60px -660px;
+}
+.emoji-no_good_tone1 {
+ background-position: -80px -660px;
+}
+.emoji-no_good_tone2 {
+ background-position: -100px -660px;
+}
+.emoji-no_good_tone3 {
+ background-position: -120px -660px;
+}
+.emoji-no_good_tone4 {
+ background-position: -140px -660px;
+}
+.emoji-no_good_tone5 {
+ background-position: -160px -660px;
+}
+.emoji-no_mobile_phones {
+ background-position: -180px -660px;
+}
+.emoji-no_mouth {
+ background-position: -200px -660px;
+}
+.emoji-no_pedestrians {
+ background-position: -220px -660px;
+}
+.emoji-no_smoking {
+ background-position: -240px -660px;
+}
+.emoji-non-potable_water {
+ background-position: -260px -660px;
+}
+.emoji-nose {
+ background-position: -280px -660px;
+}
+.emoji-nose_tone1 {
+ background-position: -300px -660px;
+}
+.emoji-nose_tone2 {
+ background-position: -320px -660px;
+}
+.emoji-nose_tone3 {
+ background-position: -340px -660px;
+}
+.emoji-nose_tone4 {
+ background-position: -360px -660px;
+}
+.emoji-nose_tone5 {
+ background-position: -380px -660px;
+}
+.emoji-notebook {
+ background-position: -400px -660px;
+}
+.emoji-notebook_with_decorative_cover {
+ background-position: -420px -660px;
+}
+.emoji-notepad_spiral {
+ background-position: -440px -660px;
+}
+.emoji-notes {
+ background-position: -460px -660px;
+}
+.emoji-nut_and_bolt {
+ background-position: -480px -660px;
+}
+.emoji-o {
+ background-position: -500px -660px;
+}
+.emoji-o2 {
+ background-position: -520px -660px;
+}
+.emoji-ocean {
+ background-position: -540px -660px;
+}
+.emoji-octagonal_sign {
+ background-position: -560px -660px;
+}
+.emoji-octopus {
+ background-position: -580px -660px;
+}
+.emoji-oden {
+ background-position: -600px -660px;
+}
+.emoji-office {
+ background-position: -620px -660px;
+}
+.emoji-oil {
+ background-position: -640px -660px;
+}
+.emoji-ok {
+ background-position: -660px -660px;
+}
+.emoji-ok_hand {
+ background-position: -680px 0;
+}
+.emoji-ok_hand_tone1 {
+ background-position: -680px -20px;
+}
+.emoji-ok_hand_tone2 {
+ background-position: -680px -40px;
+}
+.emoji-ok_hand_tone3 {
+ background-position: -680px -60px;
+}
+.emoji-ok_hand_tone4 {
+ background-position: -680px -80px;
+}
+.emoji-ok_hand_tone5 {
+ background-position: -680px -100px;
+}
+.emoji-ok_woman {
+ background-position: -680px -120px;
+}
+.emoji-ok_woman_tone1 {
+ background-position: -680px -140px;
+}
+.emoji-ok_woman_tone2 {
+ background-position: -680px -160px;
+}
+.emoji-ok_woman_tone3 {
+ background-position: -680px -180px;
+}
+.emoji-ok_woman_tone4 {
+ background-position: -680px -200px;
+}
+.emoji-ok_woman_tone5 {
+ background-position: -680px -220px;
+}
+.emoji-older_man {
+ background-position: -680px -240px;
+}
+.emoji-older_man_tone1 {
+ background-position: -680px -260px;
+}
+.emoji-older_man_tone2 {
+ background-position: -680px -280px;
+}
+.emoji-older_man_tone3 {
+ background-position: -680px -300px;
+}
+.emoji-older_man_tone4 {
+ background-position: -680px -320px;
+}
+.emoji-older_man_tone5 {
+ background-position: -680px -340px;
+}
+.emoji-older_woman {
+ background-position: -680px -360px;
+}
+.emoji-older_woman_tone1 {
+ background-position: -680px -380px;
+}
+.emoji-older_woman_tone2 {
+ background-position: -680px -400px;
+}
+.emoji-older_woman_tone3 {
+ background-position: -680px -420px;
+}
+.emoji-older_woman_tone4 {
+ background-position: -680px -440px;
+}
+.emoji-older_woman_tone5 {
+ background-position: -680px -460px;
+}
+.emoji-om_symbol {
+ background-position: -680px -480px;
+}
+.emoji-on {
+ background-position: -680px -500px;
+}
+.emoji-oncoming_automobile {
+ background-position: -680px -520px;
+}
+.emoji-oncoming_bus {
+ background-position: -680px -540px;
+}
+.emoji-oncoming_police_car {
+ background-position: -680px -560px;
+}
+.emoji-oncoming_taxi {
+ background-position: -680px -580px;
+}
+.emoji-one {
+ background-position: -680px -600px;
+}
+.emoji-open_file_folder {
+ background-position: -680px -620px;
+}
+.emoji-open_hands {
+ background-position: -680px -640px;
+}
+.emoji-open_hands_tone1 {
+ background-position: -680px -660px;
+}
+.emoji-open_hands_tone2 {
+ background-position: 0 -680px;
+}
+.emoji-open_hands_tone3 {
+ background-position: -20px -680px;
+}
+.emoji-open_hands_tone4 {
+ background-position: -40px -680px;
+}
+.emoji-open_hands_tone5 {
+ background-position: -60px -680px;
+}
+.emoji-open_mouth {
+ background-position: -80px -680px;
+}
+.emoji-ophiuchus {
+ background-position: -100px -680px;
+}
+.emoji-orange_book {
+ background-position: -120px -680px;
+}
+.emoji-orthodox_cross {
+ background-position: -140px -680px;
+}
+.emoji-outbox_tray {
+ background-position: -160px -680px;
+}
+.emoji-owl {
+ background-position: -180px -680px;
+}
+.emoji-ox {
+ background-position: -200px -680px;
+}
+.emoji-package {
+ background-position: -220px -680px;
+}
+.emoji-page_facing_up {
+ background-position: -240px -680px;
+}
+.emoji-page_with_curl {
+ background-position: -260px -680px;
+}
+.emoji-pager {
+ background-position: -280px -680px;
+}
+.emoji-paintbrush {
+ background-position: -300px -680px;
+}
+.emoji-palm_tree {
+ background-position: -320px -680px;
+}
+.emoji-pancakes {
+ background-position: -340px -680px;
+}
+.emoji-panda_face {
+ background-position: -360px -680px;
+}
+.emoji-paperclip {
+ background-position: -380px -680px;
+}
+.emoji-paperclips {
+ background-position: -400px -680px;
+}
+.emoji-park {
+ background-position: -420px -680px;
+}
+.emoji-parking {
+ background-position: -440px -680px;
+}
+.emoji-part_alternation_mark {
+ background-position: -460px -680px;
+}
+.emoji-partly_sunny {
+ background-position: -480px -680px;
+}
+.emoji-passport_control {
+ background-position: -500px -680px;
+}
+.emoji-pause_button {
+ background-position: -520px -680px;
+}
+.emoji-peace {
+ background-position: -540px -680px;
+}
+.emoji-peach {
+ background-position: -560px -680px;
+}
+.emoji-peanuts {
+ background-position: -580px -680px;
+}
+.emoji-pear {
+ background-position: -600px -680px;
+}
+.emoji-pen_ballpoint {
+ background-position: -620px -680px;
+}
+.emoji-pen_fountain {
+ background-position: -640px -680px;
+}
+.emoji-pencil {
+ background-position: -660px -680px;
+}
+.emoji-pencil2 {
+ background-position: -680px -680px;
+}
+.emoji-penguin {
+ background-position: -700px 0;
+}
+.emoji-pensive {
+ background-position: -700px -20px;
+}
+.emoji-performing_arts {
+ background-position: -700px -40px;
+}
+.emoji-persevere {
+ background-position: -700px -60px;
+}
+.emoji-person_frowning {
+ background-position: -700px -80px;
+}
+.emoji-person_frowning_tone1 {
+ background-position: -700px -100px;
+}
+.emoji-person_frowning_tone2 {
+ background-position: -700px -120px;
+}
+.emoji-person_frowning_tone3 {
+ background-position: -700px -140px;
+}
+.emoji-person_frowning_tone4 {
+ background-position: -700px -160px;
+}
+.emoji-person_frowning_tone5 {
+ background-position: -700px -180px;
+}
+.emoji-person_with_blond_hair {
+ background-position: -700px -200px;
+}
+.emoji-person_with_blond_hair_tone1 {
+ background-position: -700px -220px;
+}
+.emoji-person_with_blond_hair_tone2 {
+ background-position: -700px -240px;
+}
+.emoji-person_with_blond_hair_tone3 {
+ background-position: -700px -260px;
+}
+.emoji-person_with_blond_hair_tone4 {
+ background-position: -700px -280px;
+}
+.emoji-person_with_blond_hair_tone5 {
+ background-position: -700px -300px;
+}
+.emoji-person_with_pouting_face {
+ background-position: -700px -320px;
+}
+.emoji-person_with_pouting_face_tone1 {
+ background-position: -700px -340px;
+}
+.emoji-person_with_pouting_face_tone2 {
+ background-position: -700px -360px;
+}
+.emoji-person_with_pouting_face_tone3 {
+ background-position: -700px -380px;
+}
+.emoji-person_with_pouting_face_tone4 {
+ background-position: -700px -400px;
+}
+.emoji-person_with_pouting_face_tone5 {
+ background-position: -700px -420px;
+}
+.emoji-pick {
+ background-position: -700px -440px;
+}
+.emoji-pig {
+ background-position: -700px -460px;
+}
+.emoji-pig2 {
+ background-position: -700px -480px;
+}
+.emoji-pig_nose {
+ background-position: -700px -500px;
+}
+.emoji-pill {
+ background-position: -700px -520px;
+}
+.emoji-pineapple {
+ background-position: -700px -540px;
+}
+.emoji-ping_pong {
+ background-position: -700px -560px;
+}
+.emoji-pisces {
+ background-position: -700px -580px;
+}
+.emoji-pizza {
+ background-position: -700px -600px;
+}
+.emoji-place_of_worship {
+ background-position: -700px -620px;
+}
+.emoji-play_pause {
+ background-position: -700px -640px;
+}
+.emoji-point_down {
+ background-position: -700px -660px;
+}
+.emoji-point_down_tone1 {
+ background-position: -700px -680px;
+}
+.emoji-point_down_tone2 {
+ background-position: 0 -700px;
+}
+.emoji-point_down_tone3 {
+ background-position: -20px -700px;
+}
+.emoji-point_down_tone4 {
+ background-position: -40px -700px;
+}
+.emoji-point_down_tone5 {
+ background-position: -60px -700px;
+}
+.emoji-point_left {
+ background-position: -80px -700px;
+}
+.emoji-point_left_tone1 {
+ background-position: -100px -700px;
+}
+.emoji-point_left_tone2 {
+ background-position: -120px -700px;
+}
+.emoji-point_left_tone3 {
+ background-position: -140px -700px;
+}
+.emoji-point_left_tone4 {
+ background-position: -160px -700px;
+}
+.emoji-point_left_tone5 {
+ background-position: -180px -700px;
+}
+.emoji-point_right {
+ background-position: -200px -700px;
+}
+.emoji-point_right_tone1 {
+ background-position: -220px -700px;
+}
+.emoji-point_right_tone2 {
+ background-position: -240px -700px;
+}
+.emoji-point_right_tone3 {
+ background-position: -260px -700px;
+}
+.emoji-point_right_tone4 {
+ background-position: -280px -700px;
+}
+.emoji-point_right_tone5 {
+ background-position: -300px -700px;
+}
+.emoji-point_up {
+ background-position: -320px -700px;
+}
+.emoji-point_up_2 {
+ background-position: -340px -700px;
+}
+.emoji-point_up_2_tone1 {
+ background-position: -360px -700px;
+}
+.emoji-point_up_2_tone2 {
+ background-position: -380px -700px;
+}
+.emoji-point_up_2_tone3 {
+ background-position: -400px -700px;
+}
+.emoji-point_up_2_tone4 {
+ background-position: -420px -700px;
+}
+.emoji-point_up_2_tone5 {
+ background-position: -440px -700px;
+}
+.emoji-point_up_tone1 {
+ background-position: -460px -700px;
+}
+.emoji-point_up_tone2 {
+ background-position: -480px -700px;
+}
+.emoji-point_up_tone3 {
+ background-position: -500px -700px;
+}
+.emoji-point_up_tone4 {
+ background-position: -520px -700px;
+}
+.emoji-point_up_tone5 {
+ background-position: -540px -700px;
+}
+.emoji-police_car {
+ background-position: -560px -700px;
+}
+.emoji-poodle {
+ background-position: -580px -700px;
+}
+.emoji-poop {
+ background-position: -600px -700px;
+}
+.emoji-popcorn {
+ background-position: -620px -700px;
+}
+.emoji-post_office {
+ background-position: -640px -700px;
+}
+.emoji-postal_horn {
+ background-position: -660px -700px;
+}
+.emoji-postbox {
+ background-position: -680px -700px;
+}
+.emoji-potable_water {
+ background-position: -700px -700px;
+}
+.emoji-potato {
+ background-position: -720px 0;
+}
+.emoji-pouch {
+ background-position: -720px -20px;
+}
+.emoji-poultry_leg {
+ background-position: -720px -40px;
+}
+.emoji-pound {
+ background-position: -720px -60px;
+}
+.emoji-pouting_cat {
+ background-position: -720px -80px;
+}
+.emoji-pray {
+ background-position: -720px -100px;
+}
+.emoji-pray_tone1 {
+ background-position: -720px -120px;
+}
+.emoji-pray_tone2 {
+ background-position: -720px -140px;
+}
+.emoji-pray_tone3 {
+ background-position: -720px -160px;
+}
+.emoji-pray_tone4 {
+ background-position: -720px -180px;
+}
+.emoji-pray_tone5 {
+ background-position: -720px -200px;
+}
+.emoji-prayer_beads {
+ background-position: -720px -220px;
+}
+.emoji-pregnant_woman {
+ background-position: -720px -240px;
+}
+.emoji-pregnant_woman_tone1 {
+ background-position: -720px -260px;
+}
+.emoji-pregnant_woman_tone2 {
+ background-position: -720px -280px;
+}
+.emoji-pregnant_woman_tone3 {
+ background-position: -720px -300px;
+}
+.emoji-pregnant_woman_tone4 {
+ background-position: -720px -320px;
+}
+.emoji-pregnant_woman_tone5 {
+ background-position: -720px -340px;
+}
+.emoji-prince {
+ background-position: -720px -360px;
+}
+.emoji-prince_tone1 {
+ background-position: -720px -380px;
+}
+.emoji-prince_tone2 {
+ background-position: -720px -400px;
+}
+.emoji-prince_tone3 {
+ background-position: -720px -420px;
+}
+.emoji-prince_tone4 {
+ background-position: -720px -440px;
+}
+.emoji-prince_tone5 {
+ background-position: -720px -460px;
+}
+.emoji-princess {
+ background-position: -720px -480px;
+}
+.emoji-princess_tone1 {
+ background-position: -720px -500px;
+}
+.emoji-princess_tone2 {
+ background-position: -720px -520px;
+}
+.emoji-princess_tone3 {
+ background-position: -720px -540px;
+}
+.emoji-princess_tone4 {
+ background-position: -720px -560px;
+}
+.emoji-princess_tone5 {
+ background-position: -720px -580px;
+}
+.emoji-printer {
+ background-position: -720px -600px;
+}
+.emoji-projector {
+ background-position: -720px -620px;
+}
+.emoji-punch {
+ background-position: -720px -640px;
+}
+.emoji-punch_tone1 {
+ background-position: -720px -660px;
+}
+.emoji-punch_tone2 {
+ background-position: -720px -680px;
+}
+.emoji-punch_tone3 {
+ background-position: -720px -700px;
+}
+.emoji-punch_tone4 {
+ background-position: 0 -720px;
+}
+.emoji-punch_tone5 {
+ background-position: -20px -720px;
+}
+.emoji-purple_heart {
+ background-position: -40px -720px;
+}
+.emoji-purse {
+ background-position: -60px -720px;
+}
+.emoji-pushpin {
+ background-position: -80px -720px;
+}
+.emoji-put_litter_in_its_place {
+ background-position: -100px -720px;
+}
+.emoji-question {
+ background-position: -120px -720px;
+}
+.emoji-rabbit {
+ background-position: -140px -720px;
+}
+.emoji-rabbit2 {
+ background-position: -160px -720px;
+}
+.emoji-race_car {
+ background-position: -180px -720px;
+}
+.emoji-racehorse {
+ background-position: -200px -720px;
+}
+.emoji-radio {
+ background-position: -220px -720px;
+}
+.emoji-radio_button {
+ background-position: -240px -720px;
+}
+.emoji-radioactive {
+ background-position: -260px -720px;
+}
+.emoji-rage {
+ background-position: -280px -720px;
+}
+.emoji-railway_car {
+ background-position: -300px -720px;
+}
+.emoji-railway_track {
+ background-position: -320px -720px;
+}
+.emoji-rainbow {
+ background-position: -340px -720px;
+}
+.emoji-raised_back_of_hand {
+ background-position: -360px -720px;
+}
+.emoji-raised_back_of_hand_tone1 {
+ background-position: -380px -720px;
+}
+.emoji-raised_back_of_hand_tone2 {
+ background-position: -400px -720px;
+}
+.emoji-raised_back_of_hand_tone3 {
+ background-position: -420px -720px;
+}
+.emoji-raised_back_of_hand_tone4 {
+ background-position: -440px -720px;
+}
+.emoji-raised_back_of_hand_tone5 {
+ background-position: -460px -720px;
+}
+.emoji-raised_hand {
+ background-position: -480px -720px;
+}
+.emoji-raised_hand_tone1 {
+ background-position: -500px -720px;
+}
+.emoji-raised_hand_tone2 {
+ background-position: -520px -720px;
+}
+.emoji-raised_hand_tone3 {
+ background-position: -540px -720px;
+}
+.emoji-raised_hand_tone4 {
+ background-position: -560px -720px;
+}
+.emoji-raised_hand_tone5 {
+ background-position: -580px -720px;
+}
+.emoji-raised_hands {
+ background-position: -600px -720px;
+}
+.emoji-raised_hands_tone1 {
+ background-position: -620px -720px;
+}
+.emoji-raised_hands_tone2 {
+ background-position: -640px -720px;
+}
+.emoji-raised_hands_tone3 {
+ background-position: -660px -720px;
+}
+.emoji-raised_hands_tone4 {
+ background-position: -680px -720px;
+}
+.emoji-raised_hands_tone5 {
+ background-position: -700px -720px;
+}
+.emoji-raising_hand {
+ background-position: -720px -720px;
+}
+.emoji-raising_hand_tone1 {
+ background-position: -740px 0;
+}
+.emoji-raising_hand_tone2 {
+ background-position: -740px -20px;
+}
+.emoji-raising_hand_tone3 {
+ background-position: -740px -40px;
+}
+.emoji-raising_hand_tone4 {
+ background-position: -740px -60px;
+}
+.emoji-raising_hand_tone5 {
+ background-position: -740px -80px;
+}
+.emoji-ram {
+ background-position: -740px -100px;
+}
+.emoji-ramen {
+ background-position: -740px -120px;
+}
+.emoji-rat {
+ background-position: -740px -140px;
+}
+.emoji-record_button {
+ background-position: -740px -160px;
+}
+.emoji-recycle {
+ background-position: -740px -180px;
+}
+.emoji-red_car {
+ background-position: -740px -200px;
+}
+.emoji-red_circle {
+ background-position: -740px -220px;
+}
+.emoji-registered {
+ background-position: -740px -240px;
+}
+.emoji-relaxed {
+ background-position: -740px -260px;
+}
+.emoji-relieved {
+ background-position: -740px -280px;
+}
+.emoji-reminder_ribbon {
+ background-position: -740px -300px;
+}
+.emoji-repeat {
+ background-position: -740px -320px;
+}
+.emoji-repeat_one {
+ background-position: -740px -340px;
+}
+.emoji-restroom {
+ background-position: -740px -360px;
+}
+.emoji-revolving_hearts {
+ background-position: -740px -380px;
+}
+.emoji-rewind {
+ background-position: -740px -400px;
+}
+.emoji-rhino {
+ background-position: -740px -420px;
+}
+.emoji-ribbon {
+ background-position: -740px -440px;
+}
+.emoji-rice {
+ background-position: -740px -460px;
+}
+.emoji-rice_ball {
+ background-position: -740px -480px;
+}
+.emoji-rice_cracker {
+ background-position: -740px -500px;
+}
+.emoji-rice_scene {
+ background-position: -740px -520px;
+}
+.emoji-right_facing_fist {
+ background-position: -740px -540px;
+}
+.emoji-right_facing_fist_tone1 {
+ background-position: -740px -560px;
+}
+.emoji-right_facing_fist_tone2 {
+ background-position: -740px -580px;
+}
+.emoji-right_facing_fist_tone3 {
+ background-position: -740px -600px;
+}
+.emoji-right_facing_fist_tone4 {
+ background-position: -740px -620px;
+}
+.emoji-right_facing_fist_tone5 {
+ background-position: -740px -640px;
+}
+.emoji-ring {
+ background-position: -740px -660px;
+}
+.emoji-robot {
+ background-position: -740px -680px;
+}
+.emoji-rocket {
+ background-position: -740px -700px;
+}
+.emoji-rofl {
+ background-position: -740px -720px;
+}
+.emoji-roller_coaster {
+ background-position: 0 -740px;
+}
+.emoji-rolling_eyes {
+ background-position: -20px -740px;
+}
+.emoji-rooster {
+ background-position: -40px -740px;
+}
+.emoji-rose {
+ background-position: -60px -740px;
+}
+.emoji-rosette {
+ background-position: -80px -740px;
+}
+.emoji-rotating_light {
+ background-position: -100px -740px;
+}
+.emoji-round_pushpin {
+ background-position: -120px -740px;
+}
+.emoji-rowboat {
+ background-position: -140px -740px;
+}
+.emoji-rowboat_tone1 {
+ background-position: -160px -740px;
+}
+.emoji-rowboat_tone2 {
+ background-position: -180px -740px;
+}
+.emoji-rowboat_tone3 {
+ background-position: -200px -740px;
+}
+.emoji-rowboat_tone4 {
+ background-position: -220px -740px;
+}
+.emoji-rowboat_tone5 {
+ background-position: -240px -740px;
+}
+.emoji-rugby_football {
+ background-position: -260px -740px;
+}
+.emoji-runner {
+ background-position: -280px -740px;
+}
+.emoji-runner_tone1 {
+ background-position: -300px -740px;
+}
+.emoji-runner_tone2 {
+ background-position: -320px -740px;
+}
+.emoji-runner_tone3 {
+ background-position: -340px -740px;
+}
+.emoji-runner_tone4 {
+ background-position: -360px -740px;
+}
+.emoji-runner_tone5 {
+ background-position: -380px -740px;
+}
+.emoji-running_shirt_with_sash {
+ background-position: -400px -740px;
+}
+.emoji-sa {
+ background-position: -420px -740px;
+}
+.emoji-sagittarius {
+ background-position: -440px -740px;
+}
+.emoji-sailboat {
+ background-position: -460px -740px;
+}
+.emoji-sake {
+ background-position: -480px -740px;
+}
+.emoji-salad {
+ background-position: -500px -740px;
+}
+.emoji-sandal {
+ background-position: -520px -740px;
+}
+.emoji-santa {
+ background-position: -540px -740px;
+}
+.emoji-santa_tone1 {
+ background-position: -560px -740px;
+}
+.emoji-santa_tone2 {
+ background-position: -580px -740px;
+}
+.emoji-santa_tone3 {
+ background-position: -600px -740px;
+}
+.emoji-santa_tone4 {
+ background-position: -620px -740px;
+}
+.emoji-santa_tone5 {
+ background-position: -640px -740px;
+}
+.emoji-satellite {
+ background-position: -660px -740px;
+}
+.emoji-satellite_orbital {
+ background-position: -680px -740px;
+}
+.emoji-saxophone {
+ background-position: -700px -740px;
+}
+.emoji-scales {
+ background-position: -720px -740px;
+}
+.emoji-school {
+ background-position: -740px -740px;
+}
+.emoji-school_satchel {
+ background-position: -760px 0;
+}
+.emoji-scissors {
+ background-position: -760px -20px;
+}
+.emoji-scooter {
+ background-position: -760px -40px;
+}
+.emoji-scorpion {
+ background-position: -760px -60px;
+}
+.emoji-scorpius {
+ background-position: -760px -80px;
+}
+.emoji-scream {
+ background-position: -760px -100px;
+}
+.emoji-scream_cat {
+ background-position: -760px -120px;
+}
+.emoji-scroll {
+ background-position: -760px -140px;
+}
+.emoji-seat {
+ background-position: -760px -160px;
+}
+.emoji-second_place {
+ background-position: -760px -180px;
+}
+.emoji-secret {
+ background-position: -760px -200px;
+}
+.emoji-see_no_evil {
+ background-position: -760px -220px;
+}
+.emoji-seedling {
+ background-position: -760px -240px;
+}
+.emoji-selfie {
+ background-position: -760px -260px;
+}
+.emoji-selfie_tone1 {
+ background-position: -760px -280px;
+}
+.emoji-selfie_tone2 {
+ background-position: -760px -300px;
+}
+.emoji-selfie_tone3 {
+ background-position: -760px -320px;
+}
+.emoji-selfie_tone4 {
+ background-position: -760px -340px;
+}
+.emoji-selfie_tone5 {
+ background-position: -760px -360px;
+}
+.emoji-seven {
+ background-position: -760px -380px;
+}
+.emoji-shallow_pan_of_food {
+ background-position: -760px -400px;
+}
+.emoji-shamrock {
+ background-position: -760px -420px;
+}
+.emoji-shark {
+ background-position: -760px -440px;
+}
+.emoji-shaved_ice {
+ background-position: -760px -460px;
+}
+.emoji-sheep {
+ background-position: -760px -480px;
+}
+.emoji-shell {
+ background-position: -760px -500px;
+}
+.emoji-shield {
+ background-position: -760px -520px;
+}
+.emoji-shinto_shrine {
+ background-position: -760px -540px;
+}
+.emoji-ship {
+ background-position: -760px -560px;
+}
+.emoji-shirt {
+ background-position: -760px -580px;
+}
+.emoji-shopping_bags {
+ background-position: -760px -600px;
+}
+.emoji-shopping_cart {
+ background-position: -760px -620px;
+}
+.emoji-shower {
+ background-position: -760px -640px;
+}
+.emoji-shrimp {
+ background-position: -760px -660px;
+}
+.emoji-shrug {
+ background-position: -760px -680px;
+}
+.emoji-shrug_tone1 {
+ background-position: -760px -700px;
+}
+.emoji-shrug_tone2 {
+ background-position: -760px -720px;
+}
+.emoji-shrug_tone3 {
+ background-position: -760px -740px;
+}
+.emoji-shrug_tone4 {
+ background-position: 0 -760px;
+}
+.emoji-shrug_tone5 {
+ background-position: -20px -760px;
+}
+.emoji-signal_strength {
+ background-position: -40px -760px;
+}
+.emoji-six {
+ background-position: -60px -760px;
+}
+.emoji-six_pointed_star {
+ background-position: -80px -760px;
+}
+.emoji-ski {
+ background-position: -100px -760px;
+}
+.emoji-skier {
+ background-position: -120px -760px;
+}
+.emoji-skull {
+ background-position: -140px -760px;
+}
+.emoji-skull_crossbones {
+ background-position: -160px -760px;
+}
+.emoji-sleeping {
+ background-position: -180px -760px;
+}
+.emoji-sleeping_accommodation {
+ background-position: -200px -760px;
+}
+.emoji-sleepy {
+ background-position: -220px -760px;
+}
+.emoji-slight_frown {
+ background-position: -240px -760px;
+}
+.emoji-slight_smile {
+ background-position: -260px -760px;
+}
+.emoji-slot_machine {
+ background-position: -280px -760px;
+}
+.emoji-small_blue_diamond {
+ background-position: -300px -760px;
+}
+.emoji-small_orange_diamond {
+ background-position: -320px -760px;
+}
+.emoji-small_red_triangle {
+ background-position: -340px -760px;
+}
+.emoji-small_red_triangle_down {
+ background-position: -360px -760px;
+}
+.emoji-smile {
+ background-position: -380px -760px;
+}
+.emoji-smile_cat {
+ background-position: -400px -760px;
+}
+.emoji-smiley {
+ background-position: -420px -760px;
+}
+.emoji-smiley_cat {
+ background-position: -440px -760px;
+}
+.emoji-smiling_imp {
+ background-position: -460px -760px;
+}
+.emoji-smirk {
+ background-position: -480px -760px;
+}
+.emoji-smirk_cat {
+ background-position: -500px -760px;
+}
+.emoji-smoking {
+ background-position: -520px -760px;
+}
+.emoji-snail {
+ background-position: -540px -760px;
+}
+.emoji-snake {
+ background-position: -560px -760px;
+}
+.emoji-sneezing_face {
+ background-position: -580px -760px;
+}
+.emoji-snowboarder {
+ background-position: -600px -760px;
+}
+.emoji-snowflake {
+ background-position: -620px -760px;
+}
+.emoji-snowman {
+ background-position: -640px -760px;
+}
+.emoji-snowman2 {
+ background-position: -660px -760px;
+}
+.emoji-sob {
+ background-position: -680px -760px;
+}
+.emoji-soccer {
+ background-position: -700px -760px;
+}
+.emoji-soon {
+ background-position: -720px -760px;
+}
+.emoji-sos {
+ background-position: -740px -760px;
+}
+.emoji-sound {
+ background-position: -760px -760px;
+}
+.emoji-space_invader {
+ background-position: -780px 0;
+}
+.emoji-spades {
+ background-position: -780px -20px;
+}
+.emoji-spaghetti {
+ background-position: -780px -40px;
+}
+.emoji-sparkle {
+ background-position: -780px -60px;
+}
+.emoji-sparkler {
+ background-position: -780px -80px;
+}
+.emoji-sparkles {
+ background-position: -780px -100px;
+}
+.emoji-sparkling_heart {
+ background-position: -780px -120px;
+}
+.emoji-speak_no_evil {
+ background-position: -780px -140px;
+}
+.emoji-speaker {
+ background-position: -780px -160px;
+}
+.emoji-speaking_head {
+ background-position: -780px -180px;
+}
+.emoji-speech_balloon {
+ background-position: -780px -200px;
+}
+.emoji-speech_left {
+ background-position: -780px -220px;
+}
+.emoji-speedboat {
+ background-position: -780px -240px;
+}
+.emoji-spider {
+ background-position: -780px -260px;
+}
+.emoji-spider_web {
+ background-position: -780px -280px;
+}
+.emoji-spoon {
+ background-position: -780px -300px;
+}
+.emoji-spy {
+ background-position: -780px -320px;
+}
+.emoji-spy_tone1 {
+ background-position: -780px -340px;
+}
+.emoji-spy_tone2 {
+ background-position: -780px -360px;
+}
+.emoji-spy_tone3 {
+ background-position: -780px -380px;
+}
+.emoji-spy_tone4 {
+ background-position: -780px -400px;
+}
+.emoji-spy_tone5 {
+ background-position: -780px -420px;
+}
+.emoji-squid {
+ background-position: -780px -440px;
+}
+.emoji-stadium {
+ background-position: -780px -460px;
+}
+.emoji-star {
+ background-position: -780px -480px;
+}
+.emoji-star2 {
+ background-position: -780px -500px;
+}
+.emoji-star_and_crescent {
+ background-position: -780px -520px;
+}
+.emoji-star_of_david {
+ background-position: -780px -540px;
+}
+.emoji-stars {
+ background-position: -780px -560px;
+}
+.emoji-station {
+ background-position: -780px -580px;
+}
+.emoji-statue_of_liberty {
+ background-position: -780px -600px;
+}
+.emoji-steam_locomotive {
+ background-position: -780px -620px;
+}
+.emoji-stew {
+ background-position: -780px -640px;
+}
+.emoji-stop_button {
+ background-position: -780px -660px;
+}
+.emoji-stopwatch {
+ background-position: -780px -680px;
+}
+.emoji-straight_ruler {
+ background-position: -780px -700px;
+}
+.emoji-strawberry {
+ background-position: -780px -720px;
+}
+.emoji-stuck_out_tongue {
+ background-position: -780px -740px;
+}
+.emoji-stuck_out_tongue_closed_eyes {
+ background-position: -780px -760px;
+}
+.emoji-stuck_out_tongue_winking_eye {
+ background-position: 0 -780px;
+}
+.emoji-stuffed_flatbread {
+ background-position: -20px -780px;
+}
+.emoji-sun_with_face {
+ background-position: -40px -780px;
+}
+.emoji-sunflower {
+ background-position: -60px -780px;
+}
+.emoji-sunglasses {
+ background-position: -80px -780px;
+}
+.emoji-sunny {
+ background-position: -100px -780px;
+}
+.emoji-sunrise {
+ background-position: -120px -780px;
+}
+.emoji-sunrise_over_mountains {
+ background-position: -140px -780px;
+}
+.emoji-surfer {
+ background-position: -160px -780px;
+}
+.emoji-surfer_tone1 {
+ background-position: -180px -780px;
+}
+.emoji-surfer_tone2 {
+ background-position: -200px -780px;
+}
+.emoji-surfer_tone3 {
+ background-position: -220px -780px;
+}
+.emoji-surfer_tone4 {
+ background-position: -240px -780px;
+}
+.emoji-surfer_tone5 {
+ background-position: -260px -780px;
+}
+.emoji-sushi {
+ background-position: -280px -780px;
+}
+.emoji-suspension_railway {
+ background-position: -300px -780px;
+}
+.emoji-sweat {
+ background-position: -320px -780px;
+}
+.emoji-sweat_drops {
+ background-position: -340px -780px;
+}
+.emoji-sweat_smile {
+ background-position: -360px -780px;
+}
+.emoji-sweet_potato {
+ background-position: -380px -780px;
+}
+.emoji-swimmer {
+ background-position: -400px -780px;
+}
+.emoji-swimmer_tone1 {
+ background-position: -420px -780px;
+}
+.emoji-swimmer_tone2 {
+ background-position: -440px -780px;
+}
+.emoji-swimmer_tone3 {
+ background-position: -460px -780px;
+}
+.emoji-swimmer_tone4 {
+ background-position: -480px -780px;
+}
+.emoji-swimmer_tone5 {
+ background-position: -500px -780px;
+}
+.emoji-symbols {
+ background-position: -520px -780px;
+}
+.emoji-synagogue {
+ background-position: -540px -780px;
+}
+.emoji-syringe {
+ background-position: -560px -780px;
+}
+.emoji-taco {
+ background-position: -580px -780px;
+}
+.emoji-tada {
+ background-position: -600px -780px;
+}
+.emoji-tanabata_tree {
+ background-position: -620px -780px;
+}
+.emoji-tangerine {
+ background-position: -640px -780px;
+}
+.emoji-taurus {
+ background-position: -660px -780px;
+}
+.emoji-taxi {
+ background-position: -680px -780px;
+}
+.emoji-tea {
+ background-position: -700px -780px;
+}
+.emoji-telephone {
+ background-position: -720px -780px;
+}
+.emoji-telephone_receiver {
+ background-position: -740px -780px;
+}
+.emoji-telescope {
+ background-position: -760px -780px;
+}
+.emoji-ten {
+ background-position: -780px -780px;
+}
+.emoji-tennis {
+ background-position: -800px 0;
+}
+.emoji-tent {
+ background-position: -800px -20px;
+}
+.emoji-thermometer {
+ background-position: -800px -40px;
+}
+.emoji-thermometer_face {
+ background-position: -800px -60px;
+}
+.emoji-thinking {
+ background-position: -800px -80px;
+}
+.emoji-third_place {
+ background-position: -800px -100px;
+}
+.emoji-thought_balloon {
+ background-position: -800px -120px;
+}
+.emoji-three {
+ background-position: -800px -140px;
+}
+.emoji-thumbsdown {
+ background-position: -800px -160px;
+}
+.emoji-thumbsdown_tone1 {
+ background-position: -800px -180px;
+}
+.emoji-thumbsdown_tone2 {
+ background-position: -800px -200px;
+}
+.emoji-thumbsdown_tone3 {
+ background-position: -800px -220px;
+}
+.emoji-thumbsdown_tone4 {
+ background-position: -800px -240px;
+}
+.emoji-thumbsdown_tone5 {
+ background-position: -800px -260px;
+}
+.emoji-thumbsup {
+ background-position: -800px -280px;
+}
+.emoji-thumbsup_tone1 {
+ background-position: -800px -300px;
+}
+.emoji-thumbsup_tone2 {
+ background-position: -800px -320px;
+}
+.emoji-thumbsup_tone3 {
+ background-position: -800px -340px;
+}
+.emoji-thumbsup_tone4 {
+ background-position: -800px -360px;
+}
+.emoji-thumbsup_tone5 {
+ background-position: -800px -380px;
+}
+.emoji-thunder_cloud_rain {
+ background-position: -800px -400px;
+}
+.emoji-ticket {
+ background-position: -800px -420px;
+}
+.emoji-tickets {
+ background-position: -800px -440px;
+}
+.emoji-tiger {
+ background-position: -800px -460px;
+}
+.emoji-tiger2 {
+ background-position: -800px -480px;
+}
+.emoji-timer {
+ background-position: -800px -500px;
+}
+.emoji-tired_face {
+ background-position: -800px -520px;
+}
+.emoji-tm {
+ background-position: -800px -540px;
+}
+.emoji-toilet {
+ background-position: -800px -560px;
+}
+.emoji-tokyo_tower {
+ background-position: -800px -580px;
+}
+.emoji-tomato {
+ background-position: -800px -600px;
+}
+.emoji-tone1 {
+ background-position: -800px -620px;
+}
+.emoji-tone2 {
+ background-position: -800px -640px;
+}
+.emoji-tone3 {
+ background-position: -800px -660px;
+}
+.emoji-tone4 {
+ background-position: -800px -680px;
+}
+.emoji-tone5 {
+ background-position: -800px -700px;
+}
+.emoji-tongue {
+ background-position: -800px -720px;
+}
+.emoji-tools {
+ background-position: -800px -740px;
+}
+.emoji-top {
+ background-position: -800px -760px;
+}
+.emoji-tophat {
+ background-position: -800px -780px;
+}
+.emoji-track_next {
+ background-position: 0 -800px;
+}
+.emoji-track_previous {
+ background-position: -20px -800px;
+}
+.emoji-trackball {
+ background-position: -40px -800px;
+}
+.emoji-tractor {
+ background-position: -60px -800px;
+}
+.emoji-traffic_light {
+ background-position: -80px -800px;
+}
+.emoji-train {
+ background-position: -100px -800px;
+}
+.emoji-train2 {
+ background-position: -120px -800px;
+}
+.emoji-tram {
+ background-position: -140px -800px;
+}
+.emoji-triangular_flag_on_post {
+ background-position: -160px -800px;
+}
+.emoji-triangular_ruler {
+ background-position: -180px -800px;
+}
+.emoji-trident {
+ background-position: -200px -800px;
+}
+.emoji-triumph {
+ background-position: -220px -800px;
+}
+.emoji-trolleybus {
+ background-position: -240px -800px;
+}
+.emoji-trophy {
+ background-position: -260px -800px;
+}
+.emoji-tropical_drink {
+ background-position: -280px -800px;
+}
+.emoji-tropical_fish {
+ background-position: -300px -800px;
+}
+.emoji-truck {
+ background-position: -320px -800px;
+}
+.emoji-trumpet {
+ background-position: -340px -800px;
+}
+.emoji-tulip {
+ background-position: -360px -800px;
+}
+.emoji-tumbler_glass {
+ background-position: -380px -800px;
+}
+.emoji-turkey {
+ background-position: -400px -800px;
+}
+.emoji-turtle {
+ background-position: -420px -800px;
+}
+.emoji-tv {
+ background-position: -440px -800px;
+}
+.emoji-twisted_rightwards_arrows {
+ background-position: -460px -800px;
+}
+.emoji-two {
+ background-position: -480px -800px;
+}
+.emoji-two_hearts {
+ background-position: -500px -800px;
+}
+.emoji-two_men_holding_hands {
+ background-position: -520px -800px;
+}
+.emoji-two_women_holding_hands {
+ background-position: -540px -800px;
+}
+.emoji-u5272 {
+ background-position: -560px -800px;
+}
+.emoji-u5408 {
+ background-position: -580px -800px;
+}
+.emoji-u55b6 {
+ background-position: -600px -800px;
+}
+.emoji-u6307 {
+ background-position: -620px -800px;
+}
+.emoji-u6708 {
+ background-position: -640px -800px;
+}
+.emoji-u6709 {
+ background-position: -660px -800px;
+}
+.emoji-u6e80 {
+ background-position: -680px -800px;
+}
+.emoji-u7121 {
+ background-position: -700px -800px;
+}
+.emoji-u7533 {
+ background-position: -720px -800px;
+}
+.emoji-u7981 {
+ background-position: -740px -800px;
+}
+.emoji-u7a7a {
+ background-position: -760px -800px;
+}
+.emoji-umbrella {
+ background-position: -780px -800px;
+}
+.emoji-umbrella2 {
+ background-position: -800px -800px;
+}
+.emoji-unamused {
+ background-position: -820px 0;
+}
+.emoji-underage {
+ background-position: -820px -20px;
+}
+.emoji-unicorn {
+ background-position: -820px -40px;
+}
+.emoji-unlock {
+ background-position: -820px -60px;
+}
+.emoji-up {
+ background-position: -820px -80px;
+}
+.emoji-upside_down {
+ background-position: -820px -100px;
+}
+.emoji-urn {
+ background-position: -820px -120px;
+}
+.emoji-v {
+ background-position: -820px -140px;
+}
+.emoji-v_tone1 {
+ background-position: -820px -160px;
+}
+.emoji-v_tone2 {
+ background-position: -820px -180px;
+}
+.emoji-v_tone3 {
+ background-position: -820px -200px;
+}
+.emoji-v_tone4 {
+ background-position: -820px -220px;
+}
+.emoji-v_tone5 {
+ background-position: -820px -240px;
+}
+.emoji-vertical_traffic_light {
+ background-position: -820px -260px;
+}
+.emoji-vhs {
+ background-position: -820px -280px;
+}
+.emoji-vibration_mode {
+ background-position: -820px -300px;
+}
+.emoji-video_camera {
+ background-position: -820px -320px;
+}
+.emoji-video_game {
+ background-position: -820px -340px;
+}
+.emoji-violin {
+ background-position: -820px -360px;
+}
+.emoji-virgo {
+ background-position: -820px -380px;
+}
+.emoji-volcano {
+ background-position: -820px -400px;
+}
+.emoji-volleyball {
+ background-position: -820px -420px;
+}
+.emoji-vs {
+ background-position: -820px -440px;
+}
+.emoji-vulcan {
+ background-position: -820px -460px;
+}
+.emoji-vulcan_tone1 {
+ background-position: -820px -480px;
+}
+.emoji-vulcan_tone2 {
+ background-position: -820px -500px;
+}
+.emoji-vulcan_tone3 {
+ background-position: -820px -520px;
+}
+.emoji-vulcan_tone4 {
+ background-position: -820px -540px;
+}
+.emoji-vulcan_tone5 {
+ background-position: -820px -560px;
+}
+.emoji-walking {
+ background-position: -820px -580px;
+}
+.emoji-walking_tone1 {
+ background-position: -820px -600px;
+}
+.emoji-walking_tone2 {
+ background-position: -820px -620px;
+}
+.emoji-walking_tone3 {
+ background-position: -820px -640px;
+}
+.emoji-walking_tone4 {
+ background-position: -820px -660px;
+}
+.emoji-walking_tone5 {
+ background-position: -820px -680px;
+}
+.emoji-waning_crescent_moon {
+ background-position: -820px -700px;
+}
+.emoji-waning_gibbous_moon {
+ background-position: -820px -720px;
+}
+.emoji-warning {
+ background-position: -820px -740px;
+}
+.emoji-wastebasket {
+ background-position: -820px -760px;
+}
+.emoji-watch {
+ background-position: -820px -780px;
+}
+.emoji-water_buffalo {
+ background-position: -820px -800px;
+}
+.emoji-water_polo {
+ background-position: 0 -820px;
+}
+.emoji-water_polo_tone1 {
+ background-position: -20px -820px;
+}
+.emoji-water_polo_tone2 {
+ background-position: -40px -820px;
+}
+.emoji-water_polo_tone3 {
+ background-position: -60px -820px;
+}
+.emoji-water_polo_tone4 {
+ background-position: -80px -820px;
+}
+.emoji-water_polo_tone5 {
+ background-position: -100px -820px;
+}
+.emoji-watermelon {
+ background-position: -120px -820px;
+}
+.emoji-wave {
+ background-position: -140px -820px;
+}
+.emoji-wave_tone1 {
+ background-position: -160px -820px;
+}
+.emoji-wave_tone2 {
+ background-position: -180px -820px;
+}
+.emoji-wave_tone3 {
+ background-position: -200px -820px;
+}
+.emoji-wave_tone4 {
+ background-position: -220px -820px;
+}
+.emoji-wave_tone5 {
+ background-position: -240px -820px;
+}
+.emoji-wavy_dash {
+ background-position: -260px -820px;
+}
+.emoji-waxing_crescent_moon {
+ background-position: -280px -820px;
+}
+.emoji-waxing_gibbous_moon {
+ background-position: -300px -820px;
+}
+.emoji-wc {
+ background-position: -320px -820px;
+}
+.emoji-weary {
+ background-position: -340px -820px;
+}
+.emoji-wedding {
+ background-position: -360px -820px;
+}
+.emoji-whale {
+ background-position: -380px -820px;
+}
+.emoji-whale2 {
+ background-position: -400px -820px;
+}
+.emoji-wheel_of_dharma {
+ background-position: -420px -820px;
+}
+.emoji-wheelchair {
+ background-position: -440px -820px;
+}
+.emoji-white_check_mark {
+ background-position: -460px -820px;
+}
+.emoji-white_circle {
+ background-position: -480px -820px;
+}
+.emoji-white_flower {
+ background-position: -500px -820px;
+}
+.emoji-white_large_square {
+ background-position: -520px -820px;
+}
+.emoji-white_medium_small_square {
+ background-position: -540px -820px;
+}
+.emoji-white_medium_square {
+ background-position: -560px -820px;
+}
+.emoji-white_small_square {
+ background-position: -580px -820px;
+}
+.emoji-white_square_button {
+ background-position: -600px -820px;
+}
+.emoji-white_sun_cloud {
+ background-position: -620px -820px;
+}
+.emoji-white_sun_rain_cloud {
+ background-position: -640px -820px;
+}
+.emoji-white_sun_small_cloud {
+ background-position: -660px -820px;
+}
+.emoji-wilted_rose {
+ background-position: -680px -820px;
+}
+.emoji-wind_blowing_face {
+ background-position: -700px -820px;
+}
+.emoji-wind_chime {
+ background-position: -720px -820px;
+}
+.emoji-wine_glass {
+ background-position: -740px -820px;
+}
+.emoji-wink {
+ background-position: -760px -820px;
+}
+.emoji-wolf {
+ background-position: -780px -820px;
+}
+.emoji-woman {
+ background-position: -800px -820px;
+}
+.emoji-woman_tone1 {
+ background-position: -820px -820px;
+}
+.emoji-woman_tone2 {
+ background-position: -840px 0;
+}
+.emoji-woman_tone3 {
+ background-position: -840px -20px;
+}
+.emoji-woman_tone4 {
+ background-position: -840px -40px;
+}
+.emoji-woman_tone5 {
+ background-position: -840px -60px;
+}
+.emoji-womans_clothes {
+ background-position: -840px -80px;
+}
+.emoji-womans_hat {
+ background-position: -840px -100px;
+}
+.emoji-womens {
+ background-position: -840px -120px;
+}
+.emoji-worried {
+ background-position: -840px -140px;
+}
+.emoji-wrench {
+ background-position: -840px -160px;
+}
+.emoji-wrestlers {
+ background-position: -840px -180px;
+}
+.emoji-wrestlers_tone1 {
+ background-position: -840px -200px;
+}
+.emoji-wrestlers_tone2 {
+ background-position: -840px -220px;
+}
+.emoji-wrestlers_tone3 {
+ background-position: -840px -240px;
+}
+.emoji-wrestlers_tone4 {
+ background-position: -840px -260px;
+}
+.emoji-wrestlers_tone5 {
+ background-position: -840px -280px;
+}
+.emoji-writing_hand {
+ background-position: -840px -300px;
+}
+.emoji-writing_hand_tone1 {
+ background-position: -840px -320px;
+}
+.emoji-writing_hand_tone2 {
+ background-position: -840px -340px;
+}
+.emoji-writing_hand_tone3 {
+ background-position: -840px -360px;
+}
+.emoji-writing_hand_tone4 {
+ background-position: -840px -380px;
+}
+.emoji-writing_hand_tone5 {
+ background-position: -840px -400px;
+}
+.emoji-x {
+ background-position: -840px -420px;
+}
+.emoji-yellow_heart {
+ background-position: -840px -440px;
+}
+.emoji-yen {
+ background-position: -840px -460px;
+}
+.emoji-yin_yang {
+ background-position: -840px -480px;
+}
+.emoji-yum {
+ background-position: -840px -500px;
+}
+.emoji-zap {
+ background-position: -840px -520px;
+}
+.emoji-zero {
+ background-position: -840px -540px;
+}
+.emoji-zipper_mouth {
+ background-position: -840px -560px;
+}
+.emoji-100 {
+ background-position: -840px -580px;
+}
+
+.emoji-icon {
+ background-image: image-url('emoji.png');
+ background-repeat: no-repeat;
+ color: transparent;
+ text-indent: -99em;
+ height: 20px;
+ width: 20px;
+
+ @media only screen and (-webkit-min-device-pixel-ratio: 2),
+ only screen and (min--moz-device-pixel-ratio: 2),
+ only screen and (-o-min-device-pixel-ratio: 2/1),
+ only screen and (min-device-pixel-ratio: 2),
+ only screen and (min-resolution: 192dpi),
+ only screen and (min-resolution: 2dppx) {
+ background-image: image-url('emoji@2x.png');
+ background-size: 860px 840px;
+ }
+}
diff --git a/app/assets/stylesheets/framework.scss b/app/assets/stylesheets/framework.scss
index 14e7b38fffc..7c28024001f 100644
--- a/app/assets/stylesheets/framework.scss
+++ b/app/assets/stylesheets/framework.scss
@@ -1,64 +1,64 @@
-@import "framework/variables";
-@import "framework/mixins";
-@import "../../../node_modules/bootstrap/scss/bootstrap";
-@import "bootstrap_migration";
-@import "framework/layout";
+@import 'framework/variables';
+@import 'framework/mixins';
+@import '../../../node_modules/bootstrap/scss/bootstrap';
+@import 'bootstrap_migration';
+@import 'framework/layout';
-@import "framework/animations";
-@import "framework/vue_transitions";
-@import "framework/avatar";
-@import "framework/asciidoctor";
-@import "framework/banner";
-@import "framework/blocks";
-@import "framework/buttons";
-@import "framework/badges";
-@import "framework/calendar";
-@import "framework/callout";
-@import "framework/common";
-@import "framework/dropdowns";
-@import "framework/files";
-@import "framework/filters";
-@import "framework/flash";
-@import "framework/forms";
-@import "framework/gfm";
-@import "framework/gitlab_theme";
-@import "framework/header";
-@import "framework/highlight";
-@import "framework/issue_box";
-@import "framework/jquery";
-@import "framework/lists";
-@import "framework/logo";
-@import "framework/markdown_area";
-@import "framework/media_object";
-@import "framework/mobile";
-@import "framework/modal";
-@import "framework/pagination";
-@import "framework/panels";
-@import "framework/popup";
-@import "framework/secondary_navigation_elements";
-@import "framework/selects";
-@import "framework/sidebar";
-@import "framework/contextual_sidebar";
-@import "framework/tables";
-@import "framework/notes";
-@import "framework/tabs";
-@import "framework/timeline";
-@import "framework/tooltips";
-@import "framework/toggle";
-@import "framework/typography";
-@import "framework/zen";
-@import "framework/blank";
-@import "framework/wells";
-@import "framework/page_header";
-@import "framework/awards";
-@import "framework/images";
-@import "framework/broadcast_messages";
-@import "framework/emojis";
-@import "framework/emoji_sprites";
-@import "framework/icons";
-@import "framework/snippets";
-@import "framework/memory_graph";
-@import "framework/responsive_tables";
-@import "framework/stacked_progress_bar";
-@import "framework/ci_variable_list";
-@import "framework/feature_highlight";
+@import 'framework/animations';
+@import 'framework/vue_transitions';
+@import 'framework/avatar';
+@import 'framework/asciidoctor';
+@import 'framework/banner';
+@import 'framework/blocks';
+@import 'framework/buttons';
+@import 'framework/badges';
+@import 'framework/calendar';
+@import 'framework/callout';
+@import 'framework/common';
+@import 'framework/dropdowns';
+@import 'framework/files';
+@import 'framework/filters';
+@import 'framework/flash';
+@import 'framework/forms';
+@import 'framework/gfm';
+@import 'framework/gitlab_theme';
+@import 'framework/header';
+@import 'framework/highlight';
+@import 'framework/issue_box';
+@import 'framework/jquery';
+@import 'framework/lists';
+@import 'framework/logo';
+@import 'framework/markdown_area';
+@import 'framework/media_object';
+@import 'framework/mobile';
+@import 'framework/modal';
+@import 'framework/pagination';
+@import 'framework/panels';
+@import 'framework/popup';
+@import 'framework/secondary_navigation_elements';
+@import 'framework/selects';
+@import 'framework/sidebar';
+@import 'framework/contextual_sidebar';
+@import 'framework/tables';
+@import 'framework/notes';
+@import 'framework/tabs';
+@import 'framework/timeline';
+@import 'framework/tooltips';
+@import 'framework/toggle';
+@import 'framework/typography';
+@import 'framework/zen';
+@import 'framework/blank';
+@import 'framework/wells';
+@import 'framework/page_header';
+@import 'framework/awards';
+@import 'framework/images';
+@import 'framework/broadcast_messages';
+@import 'framework/emojis';
+@import 'framework/icons';
+@import 'framework/snippets';
+@import 'framework/memory_graph';
+@import 'framework/responsive_tables';
+@import 'framework/stacked_progress_bar';
+@import 'framework/ci_variable_list';
+@import 'framework/feature_highlight';
+@import 'framework/terms';
diff --git a/app/assets/stylesheets/framework/blocks.scss b/app/assets/stylesheets/framework/blocks.scss
index 4505fd53618..c5be27f2d29 100644
--- a/app/assets/stylesheets/framework/blocks.scss
+++ b/app/assets/stylesheets/framework/blocks.scss
@@ -46,7 +46,7 @@
}
&.middle-block {
- margin-top: 0;
+ margin-top: $gl-padding-24;
margin-bottom: 0;
}
@@ -61,7 +61,7 @@
}
&.footer-block {
- margin-top: 0;
+ margin-top: $gl-padding-24;
border-bottom: 0;
margin-bottom: -$gl-padding;
}
diff --git a/app/assets/stylesheets/framework/buttons.scss b/app/assets/stylesheets/framework/buttons.scss
index ed9cd0750f7..0115f542c88 100644
--- a/app/assets/stylesheets/framework/buttons.scss
+++ b/app/assets/stylesheets/framework/buttons.scss
@@ -106,10 +106,6 @@
@include btn-color($red-500, $red-600, $red-600, $red-700, $red-700, $red-800, $white-light);
}
-@mixin btn-gray {
- @include btn-color($gray-light, $border-gray-normal, $gray-normal, $border-gray-normal, $gray-dark, $border-gray-dark, $gl-text-color);
-}
-
@mixin btn-white {
@include btn-color($white-light, $border-color, $white-normal, $border-white-normal, $white-dark, $border-gray-dark, $gl-text-color);
}
@@ -183,10 +179,6 @@
}
}
- &.btn-gray {
- @include btn-gray;
- }
-
&.btn-info,
&.btn-primary,
&.btn-register {
diff --git a/app/assets/stylesheets/framework/common.scss b/app/assets/stylesheets/framework/common.scss
index 915b0c3be8e..1e7b9534275 100644
--- a/app/assets/stylesheets/framework/common.scss
+++ b/app/assets/stylesheets/framework/common.scss
@@ -452,6 +452,7 @@ img.emoji {
/** COMMON CLASSES **/
.prepend-top-0 { margin-top: 0; }
+.prepend-top-2 { margin-top: 2px; }
.prepend-top-5 { margin-top: 5px; }
.prepend-top-8 { margin-top: $grid-size; }
.prepend-top-10 { margin-top: 10px; }
diff --git a/app/assets/stylesheets/framework/emoji_sprites.scss b/app/assets/stylesheets/framework/emoji_sprites.scss
deleted file mode 100644
index 0174e17b660..00000000000
--- a/app/assets/stylesheets/framework/emoji_sprites.scss
+++ /dev/null
@@ -1,1813 +0,0 @@
-.emoji-zzz { background-position: 0 0; }
-.emoji-1234 { background-position: -20px 0; }
-.emoji-1F627 { background-position: 0 -20px; }
-.emoji-8ball { background-position: -20px -20px; }
-.emoji-a { background-position: -40px 0; }
-.emoji-ab { background-position: -40px -20px; }
-.emoji-abc { background-position: 0 -40px; }
-.emoji-abcd { background-position: -20px -40px; }
-.emoji-accept { background-position: -40px -40px; }
-.emoji-aerial_tramway { background-position: -60px 0; }
-.emoji-airplane { background-position: -60px -20px; }
-.emoji-airplane_arriving { background-position: -60px -40px; }
-.emoji-airplane_departure { background-position: 0 -60px; }
-.emoji-airplane_small { background-position: -20px -60px; }
-.emoji-alarm_clock { background-position: -40px -60px; }
-.emoji-alembic { background-position: -60px -60px; }
-.emoji-alien { background-position: -80px 0; }
-.emoji-ambulance { background-position: -80px -20px; }
-.emoji-amphora { background-position: -80px -40px; }
-.emoji-anchor { background-position: -80px -60px; }
-.emoji-angel { background-position: 0 -80px; }
-.emoji-angel_tone1 { background-position: -20px -80px; }
-.emoji-angel_tone2 { background-position: -40px -80px; }
-.emoji-angel_tone3 { background-position: -60px -80px; }
-.emoji-angel_tone4 { background-position: -80px -80px; }
-.emoji-angel_tone5 { background-position: -100px 0; }
-.emoji-anger { background-position: -100px -20px; }
-.emoji-anger_right { background-position: -100px -40px; }
-.emoji-angry { background-position: -100px -60px; }
-.emoji-ant { background-position: -100px -80px; }
-.emoji-apple { background-position: 0 -100px; }
-.emoji-aquarius { background-position: -20px -100px; }
-.emoji-aries { background-position: -40px -100px; }
-.emoji-arrow_backward { background-position: -60px -100px; }
-.emoji-arrow_double_down { background-position: -80px -100px; }
-.emoji-arrow_double_up { background-position: -100px -100px; }
-.emoji-arrow_down { background-position: -120px 0; }
-.emoji-arrow_down_small { background-position: -120px -20px; }
-.emoji-arrow_forward { background-position: -120px -40px; }
-.emoji-arrow_heading_down { background-position: -120px -60px; }
-.emoji-arrow_heading_up { background-position: -120px -80px; }
-.emoji-arrow_left { background-position: -120px -100px; }
-.emoji-arrow_lower_left { background-position: 0 -120px; }
-.emoji-arrow_lower_right { background-position: -20px -120px; }
-.emoji-arrow_right { background-position: -40px -120px; }
-.emoji-arrow_right_hook { background-position: -60px -120px; }
-.emoji-arrow_up { background-position: -80px -120px; }
-.emoji-arrow_up_down { background-position: -100px -120px; }
-.emoji-arrow_up_small { background-position: -120px -120px; }
-.emoji-arrow_upper_left { background-position: -140px 0; }
-.emoji-arrow_upper_right { background-position: -140px -20px; }
-.emoji-arrows_clockwise { background-position: -140px -40px; }
-.emoji-arrows_counterclockwise { background-position: -140px -60px; }
-.emoji-art { background-position: -140px -80px; }
-.emoji-articulated_lorry { background-position: -140px -100px; }
-.emoji-asterisk { background-position: -140px -120px; }
-.emoji-astonished { background-position: 0 -140px; }
-.emoji-athletic_shoe { background-position: -20px -140px; }
-.emoji-atm { background-position: -40px -140px; }
-.emoji-atom { background-position: -60px -140px; }
-.emoji-avocado { background-position: -80px -140px; }
-.emoji-b { background-position: -100px -140px; }
-.emoji-baby { background-position: -120px -140px; }
-.emoji-baby_bottle { background-position: -140px -140px; }
-.emoji-baby_chick { background-position: -160px 0; }
-.emoji-baby_symbol { background-position: -160px -20px; }
-.emoji-baby_tone1 { background-position: -160px -40px; }
-.emoji-baby_tone2 { background-position: -160px -60px; }
-.emoji-baby_tone3 { background-position: -160px -80px; }
-.emoji-baby_tone4 { background-position: -160px -100px; }
-.emoji-baby_tone5 { background-position: -160px -120px; }
-.emoji-back { background-position: -160px -140px; }
-.emoji-bacon { background-position: 0 -160px; }
-.emoji-badminton { background-position: -20px -160px; }
-.emoji-baggage_claim { background-position: -40px -160px; }
-.emoji-balloon { background-position: -60px -160px; }
-.emoji-ballot_box { background-position: -80px -160px; }
-.emoji-ballot_box_with_check { background-position: -100px -160px; }
-.emoji-bamboo { background-position: -120px -160px; }
-.emoji-banana { background-position: -140px -160px; }
-.emoji-bangbang { background-position: -160px -160px; }
-.emoji-bank { background-position: -180px 0; }
-.emoji-bar_chart { background-position: -180px -20px; }
-.emoji-barber { background-position: -180px -40px; }
-.emoji-baseball { background-position: -180px -60px; }
-.emoji-basketball { background-position: -180px -80px; }
-.emoji-basketball_player { background-position: -180px -100px; }
-.emoji-basketball_player_tone1 { background-position: -180px -120px; }
-.emoji-basketball_player_tone2 { background-position: -180px -140px; }
-.emoji-basketball_player_tone3 { background-position: -180px -160px; }
-.emoji-basketball_player_tone4 { background-position: 0 -180px; }
-.emoji-basketball_player_tone5 { background-position: -20px -180px; }
-.emoji-bat { background-position: -40px -180px; }
-.emoji-bath { background-position: -60px -180px; }
-.emoji-bath_tone1 { background-position: -80px -180px; }
-.emoji-bath_tone2 { background-position: -100px -180px; }
-.emoji-bath_tone3 { background-position: -120px -180px; }
-.emoji-bath_tone4 { background-position: -140px -180px; }
-.emoji-bath_tone5 { background-position: -160px -180px; }
-.emoji-bathtub { background-position: -180px -180px; }
-.emoji-battery { background-position: -200px 0; }
-.emoji-beach { background-position: -200px -20px; }
-.emoji-beach_umbrella { background-position: -200px -40px; }
-.emoji-bear { background-position: -200px -60px; }
-.emoji-bed { background-position: -200px -80px; }
-.emoji-bee { background-position: -200px -100px; }
-.emoji-beer { background-position: -200px -120px; }
-.emoji-beers { background-position: -200px -140px; }
-.emoji-beetle { background-position: -200px -160px; }
-.emoji-beginner { background-position: -200px -180px; }
-.emoji-bell { background-position: 0 -200px; }
-.emoji-bellhop { background-position: -20px -200px; }
-.emoji-bento { background-position: -40px -200px; }
-.emoji-bicyclist { background-position: -60px -200px; }
-.emoji-bicyclist_tone1 { background-position: -80px -200px; }
-.emoji-bicyclist_tone2 { background-position: -100px -200px; }
-.emoji-bicyclist_tone3 { background-position: -120px -200px; }
-.emoji-bicyclist_tone4 { background-position: -140px -200px; }
-.emoji-bicyclist_tone5 { background-position: -160px -200px; }
-.emoji-bike { background-position: -180px -200px; }
-.emoji-bikini { background-position: -200px -200px; }
-.emoji-biohazard { background-position: -220px 0; }
-.emoji-bird { background-position: -220px -20px; }
-.emoji-birthday { background-position: -220px -40px; }
-.emoji-black_circle { background-position: -220px -60px; }
-.emoji-black_heart { background-position: -220px -80px; }
-.emoji-black_joker { background-position: -220px -100px; }
-.emoji-black_large_square { background-position: -220px -120px; }
-.emoji-black_medium_small_square { background-position: -220px -140px; }
-.emoji-black_medium_square { background-position: -220px -160px; }
-.emoji-black_nib { background-position: -220px -180px; }
-.emoji-black_small_square { background-position: -220px -200px; }
-.emoji-black_square_button { background-position: 0 -220px; }
-.emoji-blossom { background-position: -20px -220px; }
-.emoji-blowfish { background-position: -40px -220px; }
-.emoji-blue_book { background-position: -60px -220px; }
-.emoji-blue_car { background-position: -80px -220px; }
-.emoji-blue_heart { background-position: -100px -220px; }
-.emoji-blush { background-position: -120px -220px; }
-.emoji-boar { background-position: -140px -220px; }
-.emoji-bomb { background-position: -160px -220px; }
-.emoji-book { background-position: -180px -220px; }
-.emoji-bookmark { background-position: -200px -220px; }
-.emoji-bookmark_tabs { background-position: -220px -220px; }
-.emoji-books { background-position: -240px 0; }
-.emoji-boom { background-position: -240px -20px; }
-.emoji-boot { background-position: -240px -40px; }
-.emoji-bouquet { background-position: -240px -60px; }
-.emoji-bow { background-position: -240px -80px; }
-.emoji-bow_and_arrow { background-position: -240px -100px; }
-.emoji-bow_tone1 { background-position: -240px -120px; }
-.emoji-bow_tone2 { background-position: -240px -140px; }
-.emoji-bow_tone3 { background-position: -240px -160px; }
-.emoji-bow_tone4 { background-position: -240px -180px; }
-.emoji-bow_tone5 { background-position: -240px -200px; }
-.emoji-bowling { background-position: -240px -220px; }
-.emoji-boxing_glove { background-position: 0 -240px; }
-.emoji-boy { background-position: -20px -240px; }
-.emoji-boy_tone1 { background-position: -40px -240px; }
-.emoji-boy_tone2 { background-position: -60px -240px; }
-.emoji-boy_tone3 { background-position: -80px -240px; }
-.emoji-boy_tone4 { background-position: -100px -240px; }
-.emoji-boy_tone5 { background-position: -120px -240px; }
-.emoji-bread { background-position: -140px -240px; }
-.emoji-bride_with_veil { background-position: -160px -240px; }
-.emoji-bride_with_veil_tone1 { background-position: -180px -240px; }
-.emoji-bride_with_veil_tone2 { background-position: -200px -240px; }
-.emoji-bride_with_veil_tone3 { background-position: -220px -240px; }
-.emoji-bride_with_veil_tone4 { background-position: -240px -240px; }
-.emoji-bride_with_veil_tone5 { background-position: -260px 0; }
-.emoji-bridge_at_night { background-position: -260px -20px; }
-.emoji-briefcase { background-position: -260px -40px; }
-.emoji-broken_heart { background-position: -260px -60px; }
-.emoji-bug { background-position: -260px -80px; }
-.emoji-bulb { background-position: -260px -100px; }
-.emoji-bullettrain_front { background-position: -260px -120px; }
-.emoji-bullettrain_side { background-position: -260px -140px; }
-.emoji-burrito { background-position: -260px -160px; }
-.emoji-bus { background-position: -260px -180px; }
-.emoji-busstop { background-position: -260px -200px; }
-.emoji-bust_in_silhouette { background-position: -260px -220px; }
-.emoji-busts_in_silhouette { background-position: -260px -240px; }
-.emoji-butterfly { background-position: 0 -260px; }
-.emoji-cactus { background-position: -20px -260px; }
-.emoji-cake { background-position: -40px -260px; }
-.emoji-calendar { background-position: -60px -260px; }
-.emoji-calendar_spiral { background-position: -80px -260px; }
-.emoji-call_me { background-position: -100px -260px; }
-.emoji-call_me_tone1 { background-position: -120px -260px; }
-.emoji-call_me_tone2 { background-position: -140px -260px; }
-.emoji-call_me_tone3 { background-position: -160px -260px; }
-.emoji-call_me_tone4 { background-position: -180px -260px; }
-.emoji-call_me_tone5 { background-position: -200px -260px; }
-.emoji-calling { background-position: -220px -260px; }
-.emoji-camel { background-position: -240px -260px; }
-.emoji-camera { background-position: -260px -260px; }
-.emoji-camera_with_flash { background-position: -280px 0; }
-.emoji-camping { background-position: -280px -20px; }
-.emoji-cancer { background-position: -280px -40px; }
-.emoji-candle { background-position: -280px -60px; }
-.emoji-candy { background-position: -280px -80px; }
-.emoji-canoe { background-position: -280px -100px; }
-.emoji-capital_abcd { background-position: -280px -120px; }
-.emoji-capricorn { background-position: -280px -140px; }
-.emoji-card_box { background-position: -280px -160px; }
-.emoji-card_index { background-position: -280px -180px; }
-.emoji-carousel_horse { background-position: -280px -200px; }
-.emoji-carrot { background-position: -280px -220px; }
-.emoji-cartwheel { background-position: -280px -240px; }
-.emoji-cartwheel_tone1 { background-position: -280px -260px; }
-.emoji-cartwheel_tone2 { background-position: 0 -280px; }
-.emoji-cartwheel_tone3 { background-position: -20px -280px; }
-.emoji-cartwheel_tone4 { background-position: -40px -280px; }
-.emoji-cartwheel_tone5 { background-position: -60px -280px; }
-.emoji-cat { background-position: -80px -280px; }
-.emoji-cat2 { background-position: -100px -280px; }
-.emoji-cd { background-position: -120px -280px; }
-.emoji-chains { background-position: -140px -280px; }
-.emoji-champagne { background-position: -160px -280px; }
-.emoji-champagne_glass { background-position: -180px -280px; }
-.emoji-chart { background-position: -200px -280px; }
-.emoji-chart_with_downwards_trend { background-position: -220px -280px; }
-.emoji-chart_with_upwards_trend { background-position: -240px -280px; }
-.emoji-checkered_flag { background-position: -260px -280px; }
-.emoji-cheese { background-position: -280px -280px; }
-.emoji-cherries { background-position: -300px 0; }
-.emoji-cherry_blossom { background-position: -300px -20px; }
-.emoji-chestnut { background-position: -300px -40px; }
-.emoji-chicken { background-position: -300px -60px; }
-.emoji-children_crossing { background-position: -300px -80px; }
-.emoji-chipmunk { background-position: -300px -100px; }
-.emoji-chocolate_bar { background-position: -300px -120px; }
-.emoji-christmas_tree { background-position: -300px -140px; }
-.emoji-church { background-position: -300px -160px; }
-.emoji-cinema { background-position: -300px -180px; }
-.emoji-circus_tent { background-position: -300px -200px; }
-.emoji-city_dusk { background-position: -300px -220px; }
-.emoji-city_sunset { background-position: -300px -240px; }
-.emoji-cityscape { background-position: -300px -260px; }
-.emoji-cl { background-position: -300px -280px; }
-.emoji-clap { background-position: 0 -300px; }
-.emoji-clap_tone1 { background-position: -20px -300px; }
-.emoji-clap_tone2 { background-position: -40px -300px; }
-.emoji-clap_tone3 { background-position: -60px -300px; }
-.emoji-clap_tone4 { background-position: -80px -300px; }
-.emoji-clap_tone5 { background-position: -100px -300px; }
-.emoji-clapper { background-position: -120px -300px; }
-.emoji-classical_building { background-position: -140px -300px; }
-.emoji-clipboard { background-position: -160px -300px; }
-.emoji-clock { background-position: -180px -300px; }
-.emoji-clock1 { background-position: -200px -300px; }
-.emoji-clock10 { background-position: -220px -300px; }
-.emoji-clock1030 { background-position: -240px -300px; }
-.emoji-clock11 { background-position: -260px -300px; }
-.emoji-clock1130 { background-position: -280px -300px; }
-.emoji-clock12 { background-position: -300px -300px; }
-.emoji-clock1230 { background-position: -320px 0; }
-.emoji-clock130 { background-position: -320px -20px; }
-.emoji-clock2 { background-position: -320px -40px; }
-.emoji-clock230 { background-position: -320px -60px; }
-.emoji-clock3 { background-position: -320px -80px; }
-.emoji-clock330 { background-position: -320px -100px; }
-.emoji-clock4 { background-position: -320px -120px; }
-.emoji-clock430 { background-position: -320px -140px; }
-.emoji-clock5 { background-position: -320px -160px; }
-.emoji-clock530 { background-position: -320px -180px; }
-.emoji-clock6 { background-position: -320px -200px; }
-.emoji-clock630 { background-position: -320px -220px; }
-.emoji-clock7 { background-position: -320px -240px; }
-.emoji-clock730 { background-position: -320px -260px; }
-.emoji-clock8 { background-position: -320px -280px; }
-.emoji-clock830 { background-position: -320px -300px; }
-.emoji-clock9 { background-position: 0 -320px; }
-.emoji-clock930 { background-position: -20px -320px; }
-.emoji-closed_book { background-position: -40px -320px; }
-.emoji-closed_lock_with_key { background-position: -60px -320px; }
-.emoji-closed_umbrella { background-position: -80px -320px; }
-.emoji-cloud { background-position: -100px -320px; }
-.emoji-cloud_lightning { background-position: -120px -320px; }
-.emoji-cloud_rain { background-position: -140px -320px; }
-.emoji-cloud_snow { background-position: -160px -320px; }
-.emoji-cloud_tornado { background-position: -180px -320px; }
-.emoji-clown { background-position: -200px -320px; }
-.emoji-clubs { background-position: -220px -320px; }
-.emoji-cocktail { background-position: -240px -320px; }
-.emoji-coffee { background-position: -260px -320px; }
-.emoji-coffin { background-position: -280px -320px; }
-.emoji-cold_sweat { background-position: -300px -320px; }
-.emoji-comet { background-position: -320px -320px; }
-.emoji-compression { background-position: -340px 0; }
-.emoji-computer { background-position: -340px -20px; }
-.emoji-confetti_ball { background-position: -340px -40px; }
-.emoji-confounded { background-position: -340px -60px; }
-.emoji-confused { background-position: -340px -80px; }
-.emoji-congratulations { background-position: -340px -100px; }
-.emoji-construction { background-position: -340px -120px; }
-.emoji-construction_site { background-position: -340px -140px; }
-.emoji-construction_worker { background-position: -340px -160px; }
-.emoji-construction_worker_tone1 { background-position: -340px -180px; }
-.emoji-construction_worker_tone2 { background-position: -340px -200px; }
-.emoji-construction_worker_tone3 { background-position: -340px -220px; }
-.emoji-construction_worker_tone4 { background-position: -340px -240px; }
-.emoji-construction_worker_tone5 { background-position: -340px -260px; }
-.emoji-control_knobs { background-position: -340px -280px; }
-.emoji-convenience_store { background-position: -340px -300px; }
-.emoji-cookie { background-position: -340px -320px; }
-.emoji-cooking { background-position: 0 -340px; }
-.emoji-cool { background-position: -20px -340px; }
-.emoji-cop { background-position: -40px -340px; }
-.emoji-cop_tone1 { background-position: -60px -340px; }
-.emoji-cop_tone2 { background-position: -80px -340px; }
-.emoji-cop_tone3 { background-position: -100px -340px; }
-.emoji-cop_tone4 { background-position: -120px -340px; }
-.emoji-cop_tone5 { background-position: -140px -340px; }
-.emoji-copyright { background-position: -160px -340px; }
-.emoji-corn { background-position: -180px -340px; }
-.emoji-couch { background-position: -200px -340px; }
-.emoji-couple { background-position: -220px -340px; }
-.emoji-couple_mm { background-position: -240px -340px; }
-.emoji-couple_with_heart { background-position: -260px -340px; }
-.emoji-couple_ww { background-position: -280px -340px; }
-.emoji-couplekiss { background-position: -300px -340px; }
-.emoji-cow { background-position: -320px -340px; }
-.emoji-cow2 { background-position: -340px -340px; }
-.emoji-cowboy { background-position: -360px 0; }
-.emoji-crab { background-position: -360px -20px; }
-.emoji-crayon { background-position: -360px -40px; }
-.emoji-credit_card { background-position: -360px -60px; }
-.emoji-crescent_moon { background-position: -360px -80px; }
-.emoji-cricket { background-position: -360px -100px; }
-.emoji-crocodile { background-position: -360px -120px; }
-.emoji-croissant { background-position: -360px -140px; }
-.emoji-cross { background-position: -360px -160px; }
-.emoji-crossed_flags { background-position: -360px -180px; }
-.emoji-crossed_swords { background-position: -360px -200px; }
-.emoji-crown { background-position: -360px -220px; }
-.emoji-cruise_ship { background-position: -360px -240px; }
-.emoji-cry { background-position: -360px -260px; }
-.emoji-crying_cat_face { background-position: -360px -280px; }
-.emoji-crystal_ball { background-position: -360px -300px; }
-.emoji-cucumber { background-position: -360px -320px; }
-.emoji-cupid { background-position: -360px -340px; }
-.emoji-curly_loop { background-position: 0 -360px; }
-.emoji-currency_exchange { background-position: -20px -360px; }
-.emoji-curry { background-position: -40px -360px; }
-.emoji-custard { background-position: -60px -360px; }
-.emoji-customs { background-position: -80px -360px; }
-.emoji-cyclone { background-position: -100px -360px; }
-.emoji-dagger { background-position: -120px -360px; }
-.emoji-dancer { background-position: -140px -360px; }
-.emoji-dancer_tone1 { background-position: -160px -360px; }
-.emoji-dancer_tone2 { background-position: -180px -360px; }
-.emoji-dancer_tone3 { background-position: -200px -360px; }
-.emoji-dancer_tone4 { background-position: -220px -360px; }
-.emoji-dancer_tone5 { background-position: -240px -360px; }
-.emoji-dancers { background-position: -260px -360px; }
-.emoji-dango { background-position: -280px -360px; }
-.emoji-dark_sunglasses { background-position: -300px -360px; }
-.emoji-dart { background-position: -320px -360px; }
-.emoji-dash { background-position: -340px -360px; }
-.emoji-date { background-position: -360px -360px; }
-.emoji-deciduous_tree { background-position: -380px 0; }
-.emoji-deer { background-position: -380px -20px; }
-.emoji-department_store { background-position: -380px -40px; }
-.emoji-desert { background-position: -380px -60px; }
-.emoji-desktop { background-position: -380px -80px; }
-.emoji-diamond_shape_with_a_dot_inside { background-position: -380px -100px; }
-.emoji-diamonds { background-position: -380px -120px; }
-.emoji-disappointed { background-position: -380px -140px; }
-.emoji-disappointed_relieved { background-position: -380px -160px; }
-.emoji-dividers { background-position: -380px -180px; }
-.emoji-dizzy { background-position: -380px -200px; }
-.emoji-dizzy_face { background-position: -380px -220px; }
-.emoji-do_not_litter { background-position: -380px -240px; }
-.emoji-dog { background-position: -380px -260px; }
-.emoji-dog2 { background-position: -380px -280px; }
-.emoji-dollar { background-position: -380px -300px; }
-.emoji-dolls { background-position: -380px -320px; }
-.emoji-dolphin { background-position: -380px -340px; }
-.emoji-door { background-position: -380px -360px; }
-.emoji-doughnut { background-position: 0 -380px; }
-.emoji-dove { background-position: -20px -380px; }
-.emoji-dragon { background-position: -40px -380px; }
-.emoji-dragon_face { background-position: -60px -380px; }
-.emoji-dress { background-position: -80px -380px; }
-.emoji-dromedary_camel { background-position: -100px -380px; }
-.emoji-drooling_face { background-position: -120px -380px; }
-.emoji-droplet { background-position: -140px -380px; }
-.emoji-drum { background-position: -160px -380px; }
-.emoji-duck { background-position: -180px -380px; }
-.emoji-dvd { background-position: -200px -380px; }
-.emoji-e-mail { background-position: -220px -380px; }
-.emoji-eagle { background-position: -240px -380px; }
-.emoji-ear { background-position: -260px -380px; }
-.emoji-ear_of_rice { background-position: -280px -380px; }
-.emoji-ear_tone1 { background-position: -300px -380px; }
-.emoji-ear_tone2 { background-position: -320px -380px; }
-.emoji-ear_tone3 { background-position: -340px -380px; }
-.emoji-ear_tone4 { background-position: -360px -380px; }
-.emoji-ear_tone5 { background-position: -380px -380px; }
-.emoji-earth_africa { background-position: -400px 0; }
-.emoji-earth_americas { background-position: -400px -20px; }
-.emoji-earth_asia { background-position: -400px -40px; }
-.emoji-egg { background-position: -400px -60px; }
-.emoji-eggplant { background-position: -400px -80px; }
-.emoji-eight { background-position: -400px -100px; }
-.emoji-eight_pointed_black_star { background-position: -400px -120px; }
-.emoji-eight_spoked_asterisk { background-position: -400px -140px; }
-.emoji-eject { background-position: -400px -160px; }
-.emoji-electric_plug { background-position: -400px -180px; }
-.emoji-elephant { background-position: -400px -200px; }
-.emoji-end { background-position: -400px -220px; }
-.emoji-envelope { background-position: -400px -240px; }
-.emoji-envelope_with_arrow { background-position: -400px -260px; }
-.emoji-euro { background-position: -400px -280px; }
-.emoji-european_castle { background-position: -400px -300px; }
-.emoji-european_post_office { background-position: -400px -320px; }
-.emoji-evergreen_tree { background-position: -400px -340px; }
-.emoji-exclamation { background-position: -400px -360px; }
-.emoji-expressionless { background-position: -400px -380px; }
-.emoji-eye { background-position: 0 -400px; }
-.emoji-eye_in_speech_bubble { background-position: -20px -400px; }
-.emoji-eyeglasses { background-position: -40px -400px; }
-.emoji-eyes { background-position: -60px -400px; }
-.emoji-face_palm { background-position: -80px -400px; }
-.emoji-face_palm_tone1 { background-position: -100px -400px; }
-.emoji-face_palm_tone2 { background-position: -120px -400px; }
-.emoji-face_palm_tone3 { background-position: -140px -400px; }
-.emoji-face_palm_tone4 { background-position: -160px -400px; }
-.emoji-face_palm_tone5 { background-position: -180px -400px; }
-.emoji-factory { background-position: -200px -400px; }
-.emoji-fallen_leaf { background-position: -220px -400px; }
-.emoji-family { background-position: -240px -400px; }
-.emoji-family_mmb { background-position: -260px -400px; }
-.emoji-family_mmbb { background-position: -280px -400px; }
-.emoji-family_mmg { background-position: -300px -400px; }
-.emoji-family_mmgb { background-position: -320px -400px; }
-.emoji-family_mmgg { background-position: -340px -400px; }
-.emoji-family_mwbb { background-position: -360px -400px; }
-.emoji-family_mwg { background-position: -380px -400px; }
-.emoji-family_mwgb { background-position: -400px -400px; }
-.emoji-family_mwgg { background-position: -420px 0; }
-.emoji-family_wwb { background-position: -420px -20px; }
-.emoji-family_wwbb { background-position: -420px -40px; }
-.emoji-family_wwg { background-position: -420px -60px; }
-.emoji-family_wwgb { background-position: -420px -80px; }
-.emoji-family_wwgg { background-position: -420px -100px; }
-.emoji-fast_forward { background-position: -420px -120px; }
-.emoji-fax { background-position: -420px -140px; }
-.emoji-fearful { background-position: -420px -160px; }
-.emoji-feet { background-position: -420px -180px; }
-.emoji-fencer { background-position: -420px -200px; }
-.emoji-ferris_wheel { background-position: -420px -220px; }
-.emoji-ferry { background-position: -420px -240px; }
-.emoji-field_hockey { background-position: -420px -260px; }
-.emoji-file_cabinet { background-position: -420px -280px; }
-.emoji-file_folder { background-position: -420px -300px; }
-.emoji-film_frames { background-position: -420px -320px; }
-.emoji-fingers_crossed { background-position: -420px -340px; }
-.emoji-fingers_crossed_tone1 { background-position: -420px -360px; }
-.emoji-fingers_crossed_tone2 { background-position: -420px -380px; }
-.emoji-fingers_crossed_tone3 { background-position: -420px -400px; }
-.emoji-fingers_crossed_tone4 { background-position: 0 -420px; }
-.emoji-fingers_crossed_tone5 { background-position: -20px -420px; }
-.emoji-fire { background-position: -40px -420px; }
-.emoji-fire_engine { background-position: -60px -420px; }
-.emoji-fireworks { background-position: -80px -420px; }
-.emoji-first_place { background-position: -100px -420px; }
-.emoji-first_quarter_moon { background-position: -120px -420px; }
-.emoji-first_quarter_moon_with_face { background-position: -140px -420px; }
-.emoji-fish { background-position: -160px -420px; }
-.emoji-fish_cake { background-position: -180px -420px; }
-.emoji-fishing_pole_and_fish { background-position: -200px -420px; }
-.emoji-fist { background-position: -220px -420px; }
-.emoji-fist_tone1 { background-position: -240px -420px; }
-.emoji-fist_tone2 { background-position: -260px -420px; }
-.emoji-fist_tone3 { background-position: -280px -420px; }
-.emoji-fist_tone4 { background-position: -300px -420px; }
-.emoji-fist_tone5 { background-position: -320px -420px; }
-.emoji-five { background-position: -340px -420px; }
-.emoji-flag_ac { background-position: -360px -420px; }
-.emoji-flag_ad { background-position: -380px -420px; }
-.emoji-flag_ae { background-position: -400px -420px; }
-.emoji-flag_af { background-position: -420px -420px; }
-.emoji-flag_ag { background-position: -440px 0; }
-.emoji-flag_ai { background-position: -440px -20px; }
-.emoji-flag_al { background-position: -440px -40px; }
-.emoji-flag_am { background-position: -440px -60px; }
-.emoji-flag_ao { background-position: -440px -80px; }
-.emoji-flag_aq { background-position: -440px -100px; }
-.emoji-flag_ar { background-position: -440px -120px; }
-.emoji-flag_as { background-position: -440px -140px; }
-.emoji-flag_at { background-position: -440px -160px; }
-.emoji-flag_au { background-position: -440px -180px; }
-.emoji-flag_aw { background-position: -440px -200px; }
-.emoji-flag_ax { background-position: -440px -220px; }
-.emoji-flag_az { background-position: -440px -240px; }
-.emoji-flag_ba { background-position: -440px -260px; }
-.emoji-flag_bb { background-position: -440px -280px; }
-.emoji-flag_bd { background-position: -440px -300px; }
-.emoji-flag_be { background-position: -440px -320px; }
-.emoji-flag_bf { background-position: -440px -340px; }
-.emoji-flag_bg { background-position: -440px -360px; }
-.emoji-flag_bh { background-position: -440px -380px; }
-.emoji-flag_bi { background-position: -440px -400px; }
-.emoji-flag_bj { background-position: -440px -420px; }
-.emoji-flag_bl { background-position: 0 -440px; }
-.emoji-flag_black { background-position: -20px -440px; }
-.emoji-flag_bm { background-position: -40px -440px; }
-.emoji-flag_bn { background-position: -60px -440px; }
-.emoji-flag_bo { background-position: -80px -440px; }
-.emoji-flag_bq { background-position: -100px -440px; }
-.emoji-flag_br { background-position: -120px -440px; }
-.emoji-flag_bs { background-position: -140px -440px; }
-.emoji-flag_bt { background-position: -160px -440px; }
-.emoji-flag_bv { background-position: -180px -440px; }
-.emoji-flag_bw { background-position: -200px -440px; }
-.emoji-flag_by { background-position: -220px -440px; }
-.emoji-flag_bz { background-position: -240px -440px; }
-.emoji-flag_ca { background-position: -260px -440px; }
-.emoji-flag_cc { background-position: -280px -440px; }
-.emoji-flag_cd { background-position: -300px -440px; }
-.emoji-flag_cf { background-position: -320px -440px; }
-.emoji-flag_cg { background-position: -340px -440px; }
-.emoji-flag_ch { background-position: -360px -440px; }
-.emoji-flag_ci { background-position: -380px -440px; }
-.emoji-flag_ck { background-position: -400px -440px; }
-.emoji-flag_cl { background-position: -420px -440px; }
-.emoji-flag_cm { background-position: -440px -440px; }
-.emoji-flag_cn { background-position: -460px 0; }
-.emoji-flag_co { background-position: -460px -20px; }
-.emoji-flag_cp { background-position: -460px -40px; }
-.emoji-flag_cr { background-position: -460px -60px; }
-.emoji-flag_cu { background-position: -460px -80px; }
-.emoji-flag_cv { background-position: -460px -100px; }
-.emoji-flag_cw { background-position: -460px -120px; }
-.emoji-flag_cx { background-position: -460px -140px; }
-.emoji-flag_cy { background-position: -460px -160px; }
-.emoji-flag_cz { background-position: -460px -180px; }
-.emoji-flag_de { background-position: -460px -200px; }
-.emoji-flag_dg { background-position: -460px -220px; }
-.emoji-flag_dj { background-position: -460px -240px; }
-.emoji-flag_dk { background-position: -460px -260px; }
-.emoji-flag_dm { background-position: -460px -280px; }
-.emoji-flag_do { background-position: -460px -300px; }
-.emoji-flag_dz { background-position: -460px -320px; }
-.emoji-flag_ea { background-position: -460px -340px; }
-.emoji-flag_ec { background-position: -460px -360px; }
-.emoji-flag_ee { background-position: -460px -380px; }
-.emoji-flag_eg { background-position: -460px -400px; }
-.emoji-flag_eh { background-position: -460px -420px; }
-.emoji-flag_er { background-position: -460px -440px; }
-.emoji-flag_es { background-position: 0 -460px; }
-.emoji-flag_et { background-position: -20px -460px; }
-.emoji-flag_eu { background-position: -40px -460px; }
-.emoji-flag_fi { background-position: -60px -460px; }
-.emoji-flag_fj { background-position: -80px -460px; }
-.emoji-flag_fk { background-position: -100px -460px; }
-.emoji-flag_fm { background-position: -120px -460px; }
-.emoji-flag_fo { background-position: -140px -460px; }
-.emoji-flag_fr { background-position: -160px -460px; }
-.emoji-flag_ga { background-position: -180px -460px; }
-.emoji-flag_gb { background-position: -200px -460px; }
-.emoji-flag_gd { background-position: -220px -460px; }
-.emoji-flag_ge { background-position: -240px -460px; }
-.emoji-flag_gf { background-position: -260px -460px; }
-.emoji-flag_gg { background-position: -280px -460px; }
-.emoji-flag_gh { background-position: -300px -460px; }
-.emoji-flag_gi { background-position: -320px -460px; }
-.emoji-flag_gl { background-position: -340px -460px; }
-.emoji-flag_gm { background-position: -360px -460px; }
-.emoji-flag_gn { background-position: -380px -460px; }
-.emoji-flag_gp { background-position: -400px -460px; }
-.emoji-flag_gq { background-position: -420px -460px; }
-.emoji-flag_gr { background-position: -440px -460px; }
-.emoji-flag_gs { background-position: -460px -460px; }
-.emoji-flag_gt { background-position: -480px 0; }
-.emoji-flag_gu { background-position: -480px -20px; }
-.emoji-flag_gw { background-position: -480px -40px; }
-.emoji-flag_gy { background-position: -480px -60px; }
-.emoji-flag_hk { background-position: -480px -80px; }
-.emoji-flag_hm { background-position: -480px -100px; }
-.emoji-flag_hn { background-position: -480px -120px; }
-.emoji-flag_hr { background-position: -480px -140px; }
-.emoji-flag_ht { background-position: -480px -160px; }
-.emoji-flag_hu { background-position: -480px -180px; }
-.emoji-flag_ic { background-position: -480px -200px; }
-.emoji-flag_id { background-position: -480px -220px; }
-.emoji-flag_ie { background-position: -480px -240px; }
-.emoji-flag_il { background-position: -480px -260px; }
-.emoji-flag_im { background-position: -480px -280px; }
-.emoji-flag_in { background-position: -480px -300px; }
-.emoji-flag_io { background-position: -480px -320px; }
-.emoji-flag_iq { background-position: -480px -340px; }
-.emoji-flag_ir { background-position: -480px -360px; }
-.emoji-flag_is { background-position: -480px -380px; }
-.emoji-flag_it { background-position: -480px -400px; }
-.emoji-flag_je { background-position: -480px -420px; }
-.emoji-flag_jm { background-position: -480px -440px; }
-.emoji-flag_jo { background-position: -480px -460px; }
-.emoji-flag_jp { background-position: 0 -480px; }
-.emoji-flag_ke { background-position: -20px -480px; }
-.emoji-flag_kg { background-position: -40px -480px; }
-.emoji-flag_kh { background-position: -60px -480px; }
-.emoji-flag_ki { background-position: -80px -480px; }
-.emoji-flag_km { background-position: -100px -480px; }
-.emoji-flag_kn { background-position: -120px -480px; }
-.emoji-flag_kp { background-position: -140px -480px; }
-.emoji-flag_kr { background-position: -160px -480px; }
-.emoji-flag_kw { background-position: -180px -480px; }
-.emoji-flag_ky { background-position: -200px -480px; }
-.emoji-flag_kz { background-position: -220px -480px; }
-.emoji-flag_la { background-position: -240px -480px; }
-.emoji-flag_lb { background-position: -260px -480px; }
-.emoji-flag_lc { background-position: -280px -480px; }
-.emoji-flag_li { background-position: -300px -480px; }
-.emoji-flag_lk { background-position: -320px -480px; }
-.emoji-flag_lr { background-position: -340px -480px; }
-.emoji-flag_ls { background-position: -360px -480px; }
-.emoji-flag_lt { background-position: -380px -480px; }
-.emoji-flag_lu { background-position: -400px -480px; }
-.emoji-flag_lv { background-position: -420px -480px; }
-.emoji-flag_ly { background-position: -440px -480px; }
-.emoji-flag_ma { background-position: -460px -480px; }
-.emoji-flag_mc { background-position: -480px -480px; }
-.emoji-flag_md { background-position: -500px 0; }
-.emoji-flag_me { background-position: -500px -20px; }
-.emoji-flag_mf { background-position: -500px -40px; }
-.emoji-flag_mg { background-position: -500px -60px; }
-.emoji-flag_mh { background-position: -500px -80px; }
-.emoji-flag_mk { background-position: -500px -100px; }
-.emoji-flag_ml { background-position: -500px -120px; }
-.emoji-flag_mm { background-position: -500px -140px; }
-.emoji-flag_mn { background-position: -500px -160px; }
-.emoji-flag_mo { background-position: -500px -180px; }
-.emoji-flag_mp { background-position: -500px -200px; }
-.emoji-flag_mq { background-position: -500px -220px; }
-.emoji-flag_mr { background-position: -500px -240px; }
-.emoji-flag_ms { background-position: -500px -260px; }
-.emoji-flag_mt { background-position: -500px -280px; }
-.emoji-flag_mu { background-position: -500px -300px; }
-.emoji-flag_mv { background-position: -500px -320px; }
-.emoji-flag_mw { background-position: -500px -340px; }
-.emoji-flag_mx { background-position: -500px -360px; }
-.emoji-flag_my { background-position: -500px -380px; }
-.emoji-flag_mz { background-position: -500px -400px; }
-.emoji-flag_na { background-position: -500px -420px; }
-.emoji-flag_nc { background-position: -500px -440px; }
-.emoji-flag_ne { background-position: -500px -460px; }
-.emoji-flag_nf { background-position: -500px -480px; }
-.emoji-flag_ng { background-position: 0 -500px; }
-.emoji-flag_ni { background-position: -20px -500px; }
-.emoji-flag_nl { background-position: -40px -500px; }
-.emoji-flag_no { background-position: -60px -500px; }
-.emoji-flag_np { background-position: -80px -500px; }
-.emoji-flag_nr { background-position: -100px -500px; }
-.emoji-flag_nu { background-position: -120px -500px; }
-.emoji-flag_nz { background-position: -140px -500px; }
-.emoji-flag_om { background-position: -160px -500px; }
-.emoji-flag_pa { background-position: -180px -500px; }
-.emoji-flag_pe { background-position: -200px -500px; }
-.emoji-flag_pf { background-position: -220px -500px; }
-.emoji-flag_pg { background-position: -240px -500px; }
-.emoji-flag_ph { background-position: -260px -500px; }
-.emoji-flag_pk { background-position: -280px -500px; }
-.emoji-flag_pl { background-position: -300px -500px; }
-.emoji-flag_pm { background-position: -320px -500px; }
-.emoji-flag_pn { background-position: -340px -500px; }
-.emoji-flag_pr { background-position: -360px -500px; }
-.emoji-flag_ps { background-position: -380px -500px; }
-.emoji-flag_pt { background-position: -400px -500px; }
-.emoji-flag_pw { background-position: -420px -500px; }
-.emoji-flag_py { background-position: -440px -500px; }
-.emoji-flag_qa { background-position: -460px -500px; }
-.emoji-flag_re { background-position: -480px -500px; }
-.emoji-flag_ro { background-position: -500px -500px; }
-.emoji-flag_rs { background-position: -520px 0; }
-.emoji-flag_ru { background-position: -520px -20px; }
-.emoji-flag_rw { background-position: -520px -40px; }
-.emoji-flag_sa { background-position: -520px -60px; }
-.emoji-flag_sb { background-position: -520px -80px; }
-.emoji-flag_sc { background-position: -520px -100px; }
-.emoji-flag_sd { background-position: -520px -120px; }
-.emoji-flag_se { background-position: -520px -140px; }
-.emoji-flag_sg { background-position: -520px -160px; }
-.emoji-flag_sh { background-position: -520px -180px; }
-.emoji-flag_si { background-position: -520px -200px; }
-.emoji-flag_sj { background-position: -520px -220px; }
-.emoji-flag_sk { background-position: -520px -240px; }
-.emoji-flag_sl { background-position: -520px -260px; }
-.emoji-flag_sm { background-position: -520px -280px; }
-.emoji-flag_sn { background-position: -520px -300px; }
-.emoji-flag_so { background-position: -520px -320px; }
-.emoji-flag_sr { background-position: -520px -340px; }
-.emoji-flag_ss { background-position: -520px -360px; }
-.emoji-flag_st { background-position: -520px -380px; }
-.emoji-flag_sv { background-position: -520px -400px; }
-.emoji-flag_sx { background-position: -520px -420px; }
-.emoji-flag_sy { background-position: -520px -440px; }
-.emoji-flag_sz { background-position: -520px -460px; }
-.emoji-flag_ta { background-position: -520px -480px; }
-.emoji-flag_tc { background-position: -520px -500px; }
-.emoji-flag_td { background-position: 0 -520px; }
-.emoji-flag_tf { background-position: -20px -520px; }
-.emoji-flag_tg { background-position: -40px -520px; }
-.emoji-flag_th { background-position: -60px -520px; }
-.emoji-flag_tj { background-position: -80px -520px; }
-.emoji-flag_tk { background-position: -100px -520px; }
-.emoji-flag_tl { background-position: -120px -520px; }
-.emoji-flag_tm { background-position: -140px -520px; }
-.emoji-flag_tn { background-position: -160px -520px; }
-.emoji-flag_to { background-position: -180px -520px; }
-.emoji-flag_tr { background-position: -200px -520px; }
-.emoji-flag_tt { background-position: -220px -520px; }
-.emoji-flag_tv { background-position: -240px -520px; }
-.emoji-flag_tw { background-position: -260px -520px; }
-.emoji-flag_tz { background-position: -280px -520px; }
-.emoji-flag_ua { background-position: -300px -520px; }
-.emoji-flag_ug { background-position: -320px -520px; }
-.emoji-flag_um { background-position: -340px -520px; }
-.emoji-flag_us { background-position: -360px -520px; }
-.emoji-flag_uy { background-position: -380px -520px; }
-.emoji-flag_uz { background-position: -400px -520px; }
-.emoji-flag_va { background-position: -420px -520px; }
-.emoji-flag_vc { background-position: -440px -520px; }
-.emoji-flag_ve { background-position: -460px -520px; }
-.emoji-flag_vg { background-position: -480px -520px; }
-.emoji-flag_vi { background-position: -500px -520px; }
-.emoji-flag_vn { background-position: -520px -520px; }
-.emoji-flag_vu { background-position: -540px 0; }
-.emoji-flag_wf { background-position: -540px -20px; }
-.emoji-flag_white { background-position: -540px -40px; }
-.emoji-flag_ws { background-position: -540px -60px; }
-.emoji-flag_xk { background-position: -540px -80px; }
-.emoji-flag_ye { background-position: -540px -100px; }
-.emoji-flag_yt { background-position: -540px -120px; }
-.emoji-flag_za { background-position: -540px -140px; }
-.emoji-flag_zm { background-position: -540px -160px; }
-.emoji-flag_zw { background-position: -540px -180px; }
-.emoji-flags { background-position: -540px -200px; }
-.emoji-flashlight { background-position: -540px -220px; }
-.emoji-fleur-de-lis { background-position: -540px -240px; }
-.emoji-floppy_disk { background-position: -540px -260px; }
-.emoji-flower_playing_cards { background-position: -540px -280px; }
-.emoji-flushed { background-position: -540px -300px; }
-.emoji-fog { background-position: -540px -320px; }
-.emoji-foggy { background-position: -540px -340px; }
-.emoji-football { background-position: -540px -360px; }
-.emoji-footprints { background-position: -540px -380px; }
-.emoji-fork_and_knife { background-position: -540px -400px; }
-.emoji-fork_knife_plate { background-position: -540px -420px; }
-.emoji-fountain { background-position: -540px -440px; }
-.emoji-four { background-position: -540px -460px; }
-.emoji-four_leaf_clover { background-position: -540px -480px; }
-.emoji-fox { background-position: -540px -500px; }
-.emoji-frame_photo { background-position: -540px -520px; }
-.emoji-free { background-position: 0 -540px; }
-.emoji-french_bread { background-position: -20px -540px; }
-.emoji-fried_shrimp { background-position: -40px -540px; }
-.emoji-fries { background-position: -60px -540px; }
-.emoji-frog { background-position: -80px -540px; }
-.emoji-frowning { background-position: -100px -540px; }
-.emoji-frowning2 { background-position: -120px -540px; }
-.emoji-fuelpump { background-position: -140px -540px; }
-.emoji-full_moon { background-position: -160px -540px; }
-.emoji-full_moon_with_face { background-position: -180px -540px; }
-.emoji-game_die { background-position: -200px -540px; }
-.emoji-gay_pride_flag { background-position: -220px -540px; }
-.emoji-gear { background-position: -240px -540px; }
-.emoji-gem { background-position: -260px -540px; }
-.emoji-gemini { background-position: -280px -540px; }
-.emoji-ghost { background-position: -300px -540px; }
-.emoji-gift { background-position: -320px -540px; }
-.emoji-gift_heart { background-position: -340px -540px; }
-.emoji-girl { background-position: -360px -540px; }
-.emoji-girl_tone1 { background-position: -380px -540px; }
-.emoji-girl_tone2 { background-position: -400px -540px; }
-.emoji-girl_tone3 { background-position: -420px -540px; }
-.emoji-girl_tone4 { background-position: -440px -540px; }
-.emoji-girl_tone5 { background-position: -460px -540px; }
-.emoji-globe_with_meridians { background-position: -480px -540px; }
-.emoji-goal { background-position: -500px -540px; }
-.emoji-goat { background-position: -520px -540px; }
-.emoji-golf { background-position: -540px -540px; }
-.emoji-golfer { background-position: -560px 0; }
-.emoji-gorilla { background-position: -560px -20px; }
-.emoji-grapes { background-position: -560px -40px; }
-.emoji-green_apple { background-position: -560px -60px; }
-.emoji-green_book { background-position: -560px -80px; }
-.emoji-green_heart { background-position: -560px -100px; }
-.emoji-grey_exclamation { background-position: -560px -120px; }
-.emoji-grey_question { background-position: -560px -140px; }
-.emoji-grimacing { background-position: -560px -160px; }
-.emoji-grin { background-position: -560px -180px; }
-.emoji-grinning { background-position: -560px -200px; }
-.emoji-guardsman { background-position: -560px -220px; }
-.emoji-guardsman_tone1 { background-position: -560px -240px; }
-.emoji-guardsman_tone2 { background-position: -560px -260px; }
-.emoji-guardsman_tone3 { background-position: -560px -280px; }
-.emoji-guardsman_tone4 { background-position: -560px -300px; }
-.emoji-guardsman_tone5 { background-position: -560px -320px; }
-.emoji-guitar { background-position: -560px -340px; }
-.emoji-gun { background-position: -560px -360px; }
-.emoji-haircut { background-position: -560px -380px; }
-.emoji-haircut_tone1 { background-position: -560px -400px; }
-.emoji-haircut_tone2 { background-position: -560px -420px; }
-.emoji-haircut_tone3 { background-position: -560px -440px; }
-.emoji-haircut_tone4 { background-position: -560px -460px; }
-.emoji-haircut_tone5 { background-position: -560px -480px; }
-.emoji-hamburger { background-position: -560px -500px; }
-.emoji-hammer { background-position: -560px -520px; }
-.emoji-hammer_pick { background-position: -560px -540px; }
-.emoji-hamster { background-position: 0 -560px; }
-.emoji-hand_splayed { background-position: -20px -560px; }
-.emoji-hand_splayed_tone1 { background-position: -40px -560px; }
-.emoji-hand_splayed_tone2 { background-position: -60px -560px; }
-.emoji-hand_splayed_tone3 { background-position: -80px -560px; }
-.emoji-hand_splayed_tone4 { background-position: -100px -560px; }
-.emoji-hand_splayed_tone5 { background-position: -120px -560px; }
-.emoji-handbag { background-position: -140px -560px; }
-.emoji-handball { background-position: -160px -560px; }
-.emoji-handball_tone1 { background-position: -180px -560px; }
-.emoji-handball_tone2 { background-position: -200px -560px; }
-.emoji-handball_tone3 { background-position: -220px -560px; }
-.emoji-handball_tone4 { background-position: -240px -560px; }
-.emoji-handball_tone5 { background-position: -260px -560px; }
-.emoji-handshake { background-position: -280px -560px; }
-.emoji-handshake_tone1 { background-position: -300px -560px; }
-.emoji-handshake_tone2 { background-position: -320px -560px; }
-.emoji-handshake_tone3 { background-position: -340px -560px; }
-.emoji-handshake_tone4 { background-position: -360px -560px; }
-.emoji-handshake_tone5 { background-position: -380px -560px; }
-.emoji-hash { background-position: -400px -560px; }
-.emoji-hatched_chick { background-position: -420px -560px; }
-.emoji-hatching_chick { background-position: -440px -560px; }
-.emoji-head_bandage { background-position: -460px -560px; }
-.emoji-headphones { background-position: -480px -560px; }
-.emoji-hear_no_evil { background-position: -500px -560px; }
-.emoji-heart { background-position: -520px -560px; }
-.emoji-heart_decoration { background-position: -540px -560px; }
-.emoji-heart_exclamation { background-position: -560px -560px; }
-.emoji-heart_eyes { background-position: -580px 0; }
-.emoji-heart_eyes_cat { background-position: -580px -20px; }
-.emoji-heartbeat { background-position: -580px -40px; }
-.emoji-heartpulse { background-position: -580px -60px; }
-.emoji-hearts { background-position: -580px -80px; }
-.emoji-heavy_check_mark { background-position: -580px -100px; }
-.emoji-heavy_division_sign { background-position: -580px -120px; }
-.emoji-heavy_dollar_sign { background-position: -580px -140px; }
-.emoji-heavy_minus_sign { background-position: -580px -160px; }
-.emoji-heavy_multiplication_x { background-position: -580px -180px; }
-.emoji-heavy_plus_sign { background-position: -580px -200px; }
-.emoji-helicopter { background-position: -580px -220px; }
-.emoji-helmet_with_cross { background-position: -580px -240px; }
-.emoji-herb { background-position: -580px -260px; }
-.emoji-hibiscus { background-position: -580px -280px; }
-.emoji-high_brightness { background-position: -580px -300px; }
-.emoji-high_heel { background-position: -580px -320px; }
-.emoji-hockey { background-position: -580px -340px; }
-.emoji-hole { background-position: -580px -360px; }
-.emoji-homes { background-position: -580px -380px; }
-.emoji-honey_pot { background-position: -580px -400px; }
-.emoji-horse { background-position: -580px -420px; }
-.emoji-horse_racing { background-position: -580px -440px; }
-.emoji-horse_racing_tone1 { background-position: -580px -460px; }
-.emoji-horse_racing_tone2 { background-position: -580px -480px; }
-.emoji-horse_racing_tone3 { background-position: -580px -500px; }
-.emoji-horse_racing_tone4 { background-position: -580px -520px; }
-.emoji-horse_racing_tone5 { background-position: -580px -540px; }
-.emoji-hospital { background-position: -580px -560px; }
-.emoji-hot_pepper { background-position: 0 -580px; }
-.emoji-hotdog { background-position: -20px -580px; }
-.emoji-hotel { background-position: -40px -580px; }
-.emoji-hotsprings { background-position: -60px -580px; }
-.emoji-hourglass { background-position: -80px -580px; }
-.emoji-hourglass_flowing_sand { background-position: -100px -580px; }
-.emoji-house { background-position: -120px -580px; }
-.emoji-house_abandoned { background-position: -140px -580px; }
-.emoji-house_with_garden { background-position: -160px -580px; }
-.emoji-hugging { background-position: -180px -580px; }
-.emoji-hushed { background-position: -200px -580px; }
-.emoji-ice_cream { background-position: -220px -580px; }
-.emoji-ice_skate { background-position: -240px -580px; }
-.emoji-icecream { background-position: -260px -580px; }
-.emoji-id { background-position: -280px -580px; }
-.emoji-ideograph_advantage { background-position: -300px -580px; }
-.emoji-imp { background-position: -320px -580px; }
-.emoji-inbox_tray { background-position: -340px -580px; }
-.emoji-incoming_envelope { background-position: -360px -580px; }
-.emoji-information_desk_person { background-position: -380px -580px; }
-.emoji-information_desk_person_tone1 { background-position: -400px -580px; }
-.emoji-information_desk_person_tone2 { background-position: -420px -580px; }
-.emoji-information_desk_person_tone3 { background-position: -440px -580px; }
-.emoji-information_desk_person_tone4 { background-position: -460px -580px; }
-.emoji-information_desk_person_tone5 { background-position: -480px -580px; }
-.emoji-information_source { background-position: -500px -580px; }
-.emoji-innocent { background-position: -520px -580px; }
-.emoji-interrobang { background-position: -540px -580px; }
-.emoji-iphone { background-position: -560px -580px; }
-.emoji-island { background-position: -580px -580px; }
-.emoji-izakaya_lantern { background-position: -600px 0; }
-.emoji-jack_o_lantern { background-position: -600px -20px; }
-.emoji-japan { background-position: -600px -40px; }
-.emoji-japanese_castle { background-position: -600px -60px; }
-.emoji-japanese_goblin { background-position: -600px -80px; }
-.emoji-japanese_ogre { background-position: -600px -100px; }
-.emoji-jeans { background-position: -600px -120px; }
-.emoji-joy { background-position: -600px -140px; }
-.emoji-joy_cat { background-position: -600px -160px; }
-.emoji-joystick { background-position: -600px -180px; }
-.emoji-juggling { background-position: -600px -200px; }
-.emoji-juggling_tone1 { background-position: -600px -220px; }
-.emoji-juggling_tone2 { background-position: -600px -240px; }
-.emoji-juggling_tone3 { background-position: -600px -260px; }
-.emoji-juggling_tone4 { background-position: -600px -280px; }
-.emoji-juggling_tone5 { background-position: -600px -300px; }
-.emoji-kaaba { background-position: -600px -320px; }
-.emoji-key { background-position: -600px -340px; }
-.emoji-key2 { background-position: -600px -360px; }
-.emoji-keyboard { background-position: -600px -380px; }
-.emoji-kimono { background-position: -600px -400px; }
-.emoji-kiss { background-position: -600px -420px; }
-.emoji-kiss_mm { background-position: -600px -440px; }
-.emoji-kiss_ww { background-position: -600px -460px; }
-.emoji-kissing { background-position: -600px -480px; }
-.emoji-kissing_cat { background-position: -600px -500px; }
-.emoji-kissing_closed_eyes { background-position: -600px -520px; }
-.emoji-kissing_heart { background-position: -600px -540px; }
-.emoji-kissing_smiling_eyes { background-position: -600px -560px; }
-.emoji-kiwi { background-position: -600px -580px; }
-.emoji-knife { background-position: 0 -600px; }
-.emoji-koala { background-position: -20px -600px; }
-.emoji-koko { background-position: -40px -600px; }
-.emoji-label { background-position: -60px -600px; }
-.emoji-large_blue_circle { background-position: -80px -600px; }
-.emoji-large_blue_diamond { background-position: -100px -600px; }
-.emoji-large_orange_diamond { background-position: -120px -600px; }
-.emoji-last_quarter_moon { background-position: -140px -600px; }
-.emoji-last_quarter_moon_with_face { background-position: -160px -600px; }
-.emoji-laughing { background-position: -180px -600px; }
-.emoji-leaves { background-position: -200px -600px; }
-.emoji-ledger { background-position: -220px -600px; }
-.emoji-left_facing_fist { background-position: -240px -600px; }
-.emoji-left_facing_fist_tone1 { background-position: -260px -600px; }
-.emoji-left_facing_fist_tone2 { background-position: -280px -600px; }
-.emoji-left_facing_fist_tone3 { background-position: -300px -600px; }
-.emoji-left_facing_fist_tone4 { background-position: -320px -600px; }
-.emoji-left_facing_fist_tone5 { background-position: -340px -600px; }
-.emoji-left_luggage { background-position: -360px -600px; }
-.emoji-left_right_arrow { background-position: -380px -600px; }
-.emoji-leftwards_arrow_with_hook { background-position: -400px -600px; }
-.emoji-lemon { background-position: -420px -600px; }
-.emoji-leo { background-position: -440px -600px; }
-.emoji-leopard { background-position: -460px -600px; }
-.emoji-level_slider { background-position: -480px -600px; }
-.emoji-levitate { background-position: -500px -600px; }
-.emoji-libra { background-position: -520px -600px; }
-.emoji-lifter { background-position: -540px -600px; }
-.emoji-lifter_tone1 { background-position: -560px -600px; }
-.emoji-lifter_tone2 { background-position: -580px -600px; }
-.emoji-lifter_tone3 { background-position: -600px -600px; }
-.emoji-lifter_tone4 { background-position: -620px 0; }
-.emoji-lifter_tone5 { background-position: -620px -20px; }
-.emoji-light_rail { background-position: -620px -40px; }
-.emoji-link { background-position: -620px -60px; }
-.emoji-lion_face { background-position: -620px -80px; }
-.emoji-lips { background-position: -620px -100px; }
-.emoji-lipstick { background-position: -620px -120px; }
-.emoji-lizard { background-position: -620px -140px; }
-.emoji-lock { background-position: -620px -160px; }
-.emoji-lock_with_ink_pen { background-position: -620px -180px; }
-.emoji-lollipop { background-position: -620px -200px; }
-.emoji-loop { background-position: -620px -220px; }
-.emoji-loud_sound { background-position: -620px -240px; }
-.emoji-loudspeaker { background-position: -620px -260px; }
-.emoji-love_hotel { background-position: -620px -280px; }
-.emoji-love_letter { background-position: -620px -300px; }
-.emoji-low_brightness { background-position: -620px -320px; }
-.emoji-lying_face { background-position: -620px -340px; }
-.emoji-m { background-position: -620px -360px; }
-.emoji-mag { background-position: -620px -380px; }
-.emoji-mag_right { background-position: -620px -400px; }
-.emoji-mahjong { background-position: -620px -420px; }
-.emoji-mailbox { background-position: -620px -440px; }
-.emoji-mailbox_closed { background-position: -620px -460px; }
-.emoji-mailbox_with_mail { background-position: -620px -480px; }
-.emoji-mailbox_with_no_mail { background-position: -620px -500px; }
-.emoji-man { background-position: -620px -520px; }
-.emoji-man_dancing { background-position: -620px -540px; }
-.emoji-man_dancing_tone1 { background-position: -620px -560px; }
-.emoji-man_dancing_tone2 { background-position: -620px -580px; }
-.emoji-man_dancing_tone3 { background-position: -620px -600px; }
-.emoji-man_dancing_tone4 { background-position: 0 -620px; }
-.emoji-man_dancing_tone5 { background-position: -20px -620px; }
-.emoji-man_in_tuxedo { background-position: -40px -620px; }
-.emoji-man_in_tuxedo_tone1 { background-position: -60px -620px; }
-.emoji-man_in_tuxedo_tone2 { background-position: -80px -620px; }
-.emoji-man_in_tuxedo_tone3 { background-position: -100px -620px; }
-.emoji-man_in_tuxedo_tone4 { background-position: -120px -620px; }
-.emoji-man_in_tuxedo_tone5 { background-position: -140px -620px; }
-.emoji-man_tone1 { background-position: -160px -620px; }
-.emoji-man_tone2 { background-position: -180px -620px; }
-.emoji-man_tone3 { background-position: -200px -620px; }
-.emoji-man_tone4 { background-position: -220px -620px; }
-.emoji-man_tone5 { background-position: -240px -620px; }
-.emoji-man_with_gua_pi_mao { background-position: -260px -620px; }
-.emoji-man_with_gua_pi_mao_tone1 { background-position: -280px -620px; }
-.emoji-man_with_gua_pi_mao_tone2 { background-position: -300px -620px; }
-.emoji-man_with_gua_pi_mao_tone3 { background-position: -320px -620px; }
-.emoji-man_with_gua_pi_mao_tone4 { background-position: -340px -620px; }
-.emoji-man_with_gua_pi_mao_tone5 { background-position: -360px -620px; }
-.emoji-man_with_turban { background-position: -380px -620px; }
-.emoji-man_with_turban_tone1 { background-position: -400px -620px; }
-.emoji-man_with_turban_tone2 { background-position: -420px -620px; }
-.emoji-man_with_turban_tone3 { background-position: -440px -620px; }
-.emoji-man_with_turban_tone4 { background-position: -460px -620px; }
-.emoji-man_with_turban_tone5 { background-position: -480px -620px; }
-.emoji-mans_shoe { background-position: -500px -620px; }
-.emoji-map { background-position: -520px -620px; }
-.emoji-maple_leaf { background-position: -540px -620px; }
-.emoji-martial_arts_uniform { background-position: -560px -620px; }
-.emoji-mask { background-position: -580px -620px; }
-.emoji-massage { background-position: -600px -620px; }
-.emoji-massage_tone1 { background-position: -620px -620px; }
-.emoji-massage_tone2 { background-position: -640px 0; }
-.emoji-massage_tone3 { background-position: -640px -20px; }
-.emoji-massage_tone4 { background-position: -640px -40px; }
-.emoji-massage_tone5 { background-position: -640px -60px; }
-.emoji-meat_on_bone { background-position: -640px -80px; }
-.emoji-medal { background-position: -640px -100px; }
-.emoji-mega { background-position: -640px -120px; }
-.emoji-melon { background-position: -640px -140px; }
-.emoji-menorah { background-position: -640px -160px; }
-.emoji-mens { background-position: -640px -180px; }
-.emoji-metal { background-position: -640px -200px; }
-.emoji-metal_tone1 { background-position: -640px -220px; }
-.emoji-metal_tone2 { background-position: -640px -240px; }
-.emoji-metal_tone3 { background-position: -640px -260px; }
-.emoji-metal_tone4 { background-position: -640px -280px; }
-.emoji-metal_tone5 { background-position: -640px -300px; }
-.emoji-metro { background-position: -640px -320px; }
-.emoji-microphone { background-position: -640px -340px; }
-.emoji-microphone2 { background-position: -640px -360px; }
-.emoji-microscope { background-position: -640px -380px; }
-.emoji-middle_finger { background-position: -640px -400px; }
-.emoji-middle_finger_tone1 { background-position: -640px -420px; }
-.emoji-middle_finger_tone2 { background-position: -640px -440px; }
-.emoji-middle_finger_tone3 { background-position: -640px -460px; }
-.emoji-middle_finger_tone4 { background-position: -640px -480px; }
-.emoji-middle_finger_tone5 { background-position: -640px -500px; }
-.emoji-military_medal { background-position: -640px -520px; }
-.emoji-milk { background-position: -640px -540px; }
-.emoji-milky_way { background-position: -640px -560px; }
-.emoji-minibus { background-position: -640px -580px; }
-.emoji-minidisc { background-position: -640px -600px; }
-.emoji-mobile_phone_off { background-position: -640px -620px; }
-.emoji-money_mouth { background-position: 0 -640px; }
-.emoji-money_with_wings { background-position: -20px -640px; }
-.emoji-moneybag { background-position: -40px -640px; }
-.emoji-monkey { background-position: -60px -640px; }
-.emoji-monkey_face { background-position: -80px -640px; }
-.emoji-monorail { background-position: -100px -640px; }
-.emoji-mortar_board { background-position: -120px -640px; }
-.emoji-mosque { background-position: -140px -640px; }
-.emoji-motor_scooter { background-position: -160px -640px; }
-.emoji-motorboat { background-position: -180px -640px; }
-.emoji-motorcycle { background-position: -200px -640px; }
-.emoji-motorway { background-position: -220px -640px; }
-.emoji-mount_fuji { background-position: -240px -640px; }
-.emoji-mountain { background-position: -260px -640px; }
-.emoji-mountain_bicyclist { background-position: -280px -640px; }
-.emoji-mountain_bicyclist_tone1 { background-position: -300px -640px; }
-.emoji-mountain_bicyclist_tone2 { background-position: -320px -640px; }
-.emoji-mountain_bicyclist_tone3 { background-position: -340px -640px; }
-.emoji-mountain_bicyclist_tone4 { background-position: -360px -640px; }
-.emoji-mountain_bicyclist_tone5 { background-position: -380px -640px; }
-.emoji-mountain_cableway { background-position: -400px -640px; }
-.emoji-mountain_railway { background-position: -420px -640px; }
-.emoji-mountain_snow { background-position: -440px -640px; }
-.emoji-mouse { background-position: -460px -640px; }
-.emoji-mouse2 { background-position: -480px -640px; }
-.emoji-mouse_three_button { background-position: -500px -640px; }
-.emoji-movie_camera { background-position: -520px -640px; }
-.emoji-moyai { background-position: -540px -640px; }
-.emoji-mrs_claus { background-position: -560px -640px; }
-.emoji-mrs_claus_tone1 { background-position: -580px -640px; }
-.emoji-mrs_claus_tone2 { background-position: -600px -640px; }
-.emoji-mrs_claus_tone3 { background-position: -620px -640px; }
-.emoji-mrs_claus_tone4 { background-position: -640px -640px; }
-.emoji-mrs_claus_tone5 { background-position: -660px 0; }
-.emoji-muscle { background-position: -660px -20px; }
-.emoji-muscle_tone1 { background-position: -660px -40px; }
-.emoji-muscle_tone2 { background-position: -660px -60px; }
-.emoji-muscle_tone3 { background-position: -660px -80px; }
-.emoji-muscle_tone4 { background-position: -660px -100px; }
-.emoji-muscle_tone5 { background-position: -660px -120px; }
-.emoji-mushroom { background-position: -660px -140px; }
-.emoji-musical_keyboard { background-position: -660px -160px; }
-.emoji-musical_note { background-position: -660px -180px; }
-.emoji-musical_score { background-position: -660px -200px; }
-.emoji-mute { background-position: -660px -220px; }
-.emoji-nail_care { background-position: -660px -240px; }
-.emoji-nail_care_tone1 { background-position: -660px -260px; }
-.emoji-nail_care_tone2 { background-position: -660px -280px; }
-.emoji-nail_care_tone3 { background-position: -660px -300px; }
-.emoji-nail_care_tone4 { background-position: -660px -320px; }
-.emoji-nail_care_tone5 { background-position: -660px -340px; }
-.emoji-name_badge { background-position: -660px -360px; }
-.emoji-nauseated_face { background-position: -660px -380px; }
-.emoji-necktie { background-position: -660px -400px; }
-.emoji-negative_squared_cross_mark { background-position: -660px -420px; }
-.emoji-nerd { background-position: -660px -440px; }
-.emoji-neutral_face { background-position: -660px -460px; }
-.emoji-new { background-position: -660px -480px; }
-.emoji-new_moon { background-position: -660px -500px; }
-.emoji-new_moon_with_face { background-position: -660px -520px; }
-.emoji-newspaper { background-position: -660px -540px; }
-.emoji-newspaper2 { background-position: -660px -560px; }
-.emoji-ng { background-position: -660px -580px; }
-.emoji-night_with_stars { background-position: -660px -600px; }
-.emoji-nine { background-position: -660px -620px; }
-.emoji-no_bell { background-position: -660px -640px; }
-.emoji-no_bicycles { background-position: 0 -660px; }
-.emoji-no_entry { background-position: -20px -660px; }
-.emoji-no_entry_sign { background-position: -40px -660px; }
-.emoji-no_good { background-position: -60px -660px; }
-.emoji-no_good_tone1 { background-position: -80px -660px; }
-.emoji-no_good_tone2 { background-position: -100px -660px; }
-.emoji-no_good_tone3 { background-position: -120px -660px; }
-.emoji-no_good_tone4 { background-position: -140px -660px; }
-.emoji-no_good_tone5 { background-position: -160px -660px; }
-.emoji-no_mobile_phones { background-position: -180px -660px; }
-.emoji-no_mouth { background-position: -200px -660px; }
-.emoji-no_pedestrians { background-position: -220px -660px; }
-.emoji-no_smoking { background-position: -240px -660px; }
-.emoji-non-potable_water { background-position: -260px -660px; }
-.emoji-nose { background-position: -280px -660px; }
-.emoji-nose_tone1 { background-position: -300px -660px; }
-.emoji-nose_tone2 { background-position: -320px -660px; }
-.emoji-nose_tone3 { background-position: -340px -660px; }
-.emoji-nose_tone4 { background-position: -360px -660px; }
-.emoji-nose_tone5 { background-position: -380px -660px; }
-.emoji-notebook { background-position: -400px -660px; }
-.emoji-notebook_with_decorative_cover { background-position: -420px -660px; }
-.emoji-notepad_spiral { background-position: -440px -660px; }
-.emoji-notes { background-position: -460px -660px; }
-.emoji-nut_and_bolt { background-position: -480px -660px; }
-.emoji-o { background-position: -500px -660px; }
-.emoji-o2 { background-position: -520px -660px; }
-.emoji-ocean { background-position: -540px -660px; }
-.emoji-octagonal_sign { background-position: -560px -660px; }
-.emoji-octopus { background-position: -580px -660px; }
-.emoji-oden { background-position: -600px -660px; }
-.emoji-office { background-position: -620px -660px; }
-.emoji-oil { background-position: -640px -660px; }
-.emoji-ok { background-position: -660px -660px; }
-.emoji-ok_hand { background-position: -680px 0; }
-.emoji-ok_hand_tone1 { background-position: -680px -20px; }
-.emoji-ok_hand_tone2 { background-position: -680px -40px; }
-.emoji-ok_hand_tone3 { background-position: -680px -60px; }
-.emoji-ok_hand_tone4 { background-position: -680px -80px; }
-.emoji-ok_hand_tone5 { background-position: -680px -100px; }
-.emoji-ok_woman { background-position: -680px -120px; }
-.emoji-ok_woman_tone1 { background-position: -680px -140px; }
-.emoji-ok_woman_tone2 { background-position: -680px -160px; }
-.emoji-ok_woman_tone3 { background-position: -680px -180px; }
-.emoji-ok_woman_tone4 { background-position: -680px -200px; }
-.emoji-ok_woman_tone5 { background-position: -680px -220px; }
-.emoji-older_man { background-position: -680px -240px; }
-.emoji-older_man_tone1 { background-position: -680px -260px; }
-.emoji-older_man_tone2 { background-position: -680px -280px; }
-.emoji-older_man_tone3 { background-position: -680px -300px; }
-.emoji-older_man_tone4 { background-position: -680px -320px; }
-.emoji-older_man_tone5 { background-position: -680px -340px; }
-.emoji-older_woman { background-position: -680px -360px; }
-.emoji-older_woman_tone1 { background-position: -680px -380px; }
-.emoji-older_woman_tone2 { background-position: -680px -400px; }
-.emoji-older_woman_tone3 { background-position: -680px -420px; }
-.emoji-older_woman_tone4 { background-position: -680px -440px; }
-.emoji-older_woman_tone5 { background-position: -680px -460px; }
-.emoji-om_symbol { background-position: -680px -480px; }
-.emoji-on { background-position: -680px -500px; }
-.emoji-oncoming_automobile { background-position: -680px -520px; }
-.emoji-oncoming_bus { background-position: -680px -540px; }
-.emoji-oncoming_police_car { background-position: -680px -560px; }
-.emoji-oncoming_taxi { background-position: -680px -580px; }
-.emoji-one { background-position: -680px -600px; }
-.emoji-open_file_folder { background-position: -680px -620px; }
-.emoji-open_hands { background-position: -680px -640px; }
-.emoji-open_hands_tone1 { background-position: -680px -660px; }
-.emoji-open_hands_tone2 { background-position: 0 -680px; }
-.emoji-open_hands_tone3 { background-position: -20px -680px; }
-.emoji-open_hands_tone4 { background-position: -40px -680px; }
-.emoji-open_hands_tone5 { background-position: -60px -680px; }
-.emoji-open_mouth { background-position: -80px -680px; }
-.emoji-ophiuchus { background-position: -100px -680px; }
-.emoji-orange_book { background-position: -120px -680px; }
-.emoji-orthodox_cross { background-position: -140px -680px; }
-.emoji-outbox_tray { background-position: -160px -680px; }
-.emoji-owl { background-position: -180px -680px; }
-.emoji-ox { background-position: -200px -680px; }
-.emoji-package { background-position: -220px -680px; }
-.emoji-page_facing_up { background-position: -240px -680px; }
-.emoji-page_with_curl { background-position: -260px -680px; }
-.emoji-pager { background-position: -280px -680px; }
-.emoji-paintbrush { background-position: -300px -680px; }
-.emoji-palm_tree { background-position: -320px -680px; }
-.emoji-pancakes { background-position: -340px -680px; }
-.emoji-panda_face { background-position: -360px -680px; }
-.emoji-paperclip { background-position: -380px -680px; }
-.emoji-paperclips { background-position: -400px -680px; }
-.emoji-park { background-position: -420px -680px; }
-.emoji-parking { background-position: -440px -680px; }
-.emoji-part_alternation_mark { background-position: -460px -680px; }
-.emoji-partly_sunny { background-position: -480px -680px; }
-.emoji-passport_control { background-position: -500px -680px; }
-.emoji-pause_button { background-position: -520px -680px; }
-.emoji-peace { background-position: -540px -680px; }
-.emoji-peach { background-position: -560px -680px; }
-.emoji-peanuts { background-position: -580px -680px; }
-.emoji-pear { background-position: -600px -680px; }
-.emoji-pen_ballpoint { background-position: -620px -680px; }
-.emoji-pen_fountain { background-position: -640px -680px; }
-.emoji-pencil { background-position: -660px -680px; }
-.emoji-pencil2 { background-position: -680px -680px; }
-.emoji-penguin { background-position: -700px 0; }
-.emoji-pensive { background-position: -700px -20px; }
-.emoji-performing_arts { background-position: -700px -40px; }
-.emoji-persevere { background-position: -700px -60px; }
-.emoji-person_frowning { background-position: -700px -80px; }
-.emoji-person_frowning_tone1 { background-position: -700px -100px; }
-.emoji-person_frowning_tone2 { background-position: -700px -120px; }
-.emoji-person_frowning_tone3 { background-position: -700px -140px; }
-.emoji-person_frowning_tone4 { background-position: -700px -160px; }
-.emoji-person_frowning_tone5 { background-position: -700px -180px; }
-.emoji-person_with_blond_hair { background-position: -700px -200px; }
-.emoji-person_with_blond_hair_tone1 { background-position: -700px -220px; }
-.emoji-person_with_blond_hair_tone2 { background-position: -700px -240px; }
-.emoji-person_with_blond_hair_tone3 { background-position: -700px -260px; }
-.emoji-person_with_blond_hair_tone4 { background-position: -700px -280px; }
-.emoji-person_with_blond_hair_tone5 { background-position: -700px -300px; }
-.emoji-person_with_pouting_face { background-position: -700px -320px; }
-.emoji-person_with_pouting_face_tone1 { background-position: -700px -340px; }
-.emoji-person_with_pouting_face_tone2 { background-position: -700px -360px; }
-.emoji-person_with_pouting_face_tone3 { background-position: -700px -380px; }
-.emoji-person_with_pouting_face_tone4 { background-position: -700px -400px; }
-.emoji-person_with_pouting_face_tone5 { background-position: -700px -420px; }
-.emoji-pick { background-position: -700px -440px; }
-.emoji-pig { background-position: -700px -460px; }
-.emoji-pig2 { background-position: -700px -480px; }
-.emoji-pig_nose { background-position: -700px -500px; }
-.emoji-pill { background-position: -700px -520px; }
-.emoji-pineapple { background-position: -700px -540px; }
-.emoji-ping_pong { background-position: -700px -560px; }
-.emoji-pisces { background-position: -700px -580px; }
-.emoji-pizza { background-position: -700px -600px; }
-.emoji-place_of_worship { background-position: -700px -620px; }
-.emoji-play_pause { background-position: -700px -640px; }
-.emoji-point_down { background-position: -700px -660px; }
-.emoji-point_down_tone1 { background-position: -700px -680px; }
-.emoji-point_down_tone2 { background-position: 0 -700px; }
-.emoji-point_down_tone3 { background-position: -20px -700px; }
-.emoji-point_down_tone4 { background-position: -40px -700px; }
-.emoji-point_down_tone5 { background-position: -60px -700px; }
-.emoji-point_left { background-position: -80px -700px; }
-.emoji-point_left_tone1 { background-position: -100px -700px; }
-.emoji-point_left_tone2 { background-position: -120px -700px; }
-.emoji-point_left_tone3 { background-position: -140px -700px; }
-.emoji-point_left_tone4 { background-position: -160px -700px; }
-.emoji-point_left_tone5 { background-position: -180px -700px; }
-.emoji-point_right { background-position: -200px -700px; }
-.emoji-point_right_tone1 { background-position: -220px -700px; }
-.emoji-point_right_tone2 { background-position: -240px -700px; }
-.emoji-point_right_tone3 { background-position: -260px -700px; }
-.emoji-point_right_tone4 { background-position: -280px -700px; }
-.emoji-point_right_tone5 { background-position: -300px -700px; }
-.emoji-point_up { background-position: -320px -700px; }
-.emoji-point_up_2 { background-position: -340px -700px; }
-.emoji-point_up_2_tone1 { background-position: -360px -700px; }
-.emoji-point_up_2_tone2 { background-position: -380px -700px; }
-.emoji-point_up_2_tone3 { background-position: -400px -700px; }
-.emoji-point_up_2_tone4 { background-position: -420px -700px; }
-.emoji-point_up_2_tone5 { background-position: -440px -700px; }
-.emoji-point_up_tone1 { background-position: -460px -700px; }
-.emoji-point_up_tone2 { background-position: -480px -700px; }
-.emoji-point_up_tone3 { background-position: -500px -700px; }
-.emoji-point_up_tone4 { background-position: -520px -700px; }
-.emoji-point_up_tone5 { background-position: -540px -700px; }
-.emoji-police_car { background-position: -560px -700px; }
-.emoji-poodle { background-position: -580px -700px; }
-.emoji-poop { background-position: -600px -700px; }
-.emoji-popcorn { background-position: -620px -700px; }
-.emoji-post_office { background-position: -640px -700px; }
-.emoji-postal_horn { background-position: -660px -700px; }
-.emoji-postbox { background-position: -680px -700px; }
-.emoji-potable_water { background-position: -700px -700px; }
-.emoji-potato { background-position: -720px 0; }
-.emoji-pouch { background-position: -720px -20px; }
-.emoji-poultry_leg { background-position: -720px -40px; }
-.emoji-pound { background-position: -720px -60px; }
-.emoji-pouting_cat { background-position: -720px -80px; }
-.emoji-pray { background-position: -720px -100px; }
-.emoji-pray_tone1 { background-position: -720px -120px; }
-.emoji-pray_tone2 { background-position: -720px -140px; }
-.emoji-pray_tone3 { background-position: -720px -160px; }
-.emoji-pray_tone4 { background-position: -720px -180px; }
-.emoji-pray_tone5 { background-position: -720px -200px; }
-.emoji-prayer_beads { background-position: -720px -220px; }
-.emoji-pregnant_woman { background-position: -720px -240px; }
-.emoji-pregnant_woman_tone1 { background-position: -720px -260px; }
-.emoji-pregnant_woman_tone2 { background-position: -720px -280px; }
-.emoji-pregnant_woman_tone3 { background-position: -720px -300px; }
-.emoji-pregnant_woman_tone4 { background-position: -720px -320px; }
-.emoji-pregnant_woman_tone5 { background-position: -720px -340px; }
-.emoji-prince { background-position: -720px -360px; }
-.emoji-prince_tone1 { background-position: -720px -380px; }
-.emoji-prince_tone2 { background-position: -720px -400px; }
-.emoji-prince_tone3 { background-position: -720px -420px; }
-.emoji-prince_tone4 { background-position: -720px -440px; }
-.emoji-prince_tone5 { background-position: -720px -460px; }
-.emoji-princess { background-position: -720px -480px; }
-.emoji-princess_tone1 { background-position: -720px -500px; }
-.emoji-princess_tone2 { background-position: -720px -520px; }
-.emoji-princess_tone3 { background-position: -720px -540px; }
-.emoji-princess_tone4 { background-position: -720px -560px; }
-.emoji-princess_tone5 { background-position: -720px -580px; }
-.emoji-printer { background-position: -720px -600px; }
-.emoji-projector { background-position: -720px -620px; }
-.emoji-punch { background-position: -720px -640px; }
-.emoji-punch_tone1 { background-position: -720px -660px; }
-.emoji-punch_tone2 { background-position: -720px -680px; }
-.emoji-punch_tone3 { background-position: -720px -700px; }
-.emoji-punch_tone4 { background-position: 0 -720px; }
-.emoji-punch_tone5 { background-position: -20px -720px; }
-.emoji-purple_heart { background-position: -40px -720px; }
-.emoji-purse { background-position: -60px -720px; }
-.emoji-pushpin { background-position: -80px -720px; }
-.emoji-put_litter_in_its_place { background-position: -100px -720px; }
-.emoji-question { background-position: -120px -720px; }
-.emoji-rabbit { background-position: -140px -720px; }
-.emoji-rabbit2 { background-position: -160px -720px; }
-.emoji-race_car { background-position: -180px -720px; }
-.emoji-racehorse { background-position: -200px -720px; }
-.emoji-radio { background-position: -220px -720px; }
-.emoji-radio_button { background-position: -240px -720px; }
-.emoji-radioactive { background-position: -260px -720px; }
-.emoji-rage { background-position: -280px -720px; }
-.emoji-railway_car { background-position: -300px -720px; }
-.emoji-railway_track { background-position: -320px -720px; }
-.emoji-rainbow { background-position: -340px -720px; }
-.emoji-raised_back_of_hand { background-position: -360px -720px; }
-.emoji-raised_back_of_hand_tone1 { background-position: -380px -720px; }
-.emoji-raised_back_of_hand_tone2 { background-position: -400px -720px; }
-.emoji-raised_back_of_hand_tone3 { background-position: -420px -720px; }
-.emoji-raised_back_of_hand_tone4 { background-position: -440px -720px; }
-.emoji-raised_back_of_hand_tone5 { background-position: -460px -720px; }
-.emoji-raised_hand { background-position: -480px -720px; }
-.emoji-raised_hand_tone1 { background-position: -500px -720px; }
-.emoji-raised_hand_tone2 { background-position: -520px -720px; }
-.emoji-raised_hand_tone3 { background-position: -540px -720px; }
-.emoji-raised_hand_tone4 { background-position: -560px -720px; }
-.emoji-raised_hand_tone5 { background-position: -580px -720px; }
-.emoji-raised_hands { background-position: -600px -720px; }
-.emoji-raised_hands_tone1 { background-position: -620px -720px; }
-.emoji-raised_hands_tone2 { background-position: -640px -720px; }
-.emoji-raised_hands_tone3 { background-position: -660px -720px; }
-.emoji-raised_hands_tone4 { background-position: -680px -720px; }
-.emoji-raised_hands_tone5 { background-position: -700px -720px; }
-.emoji-raising_hand { background-position: -720px -720px; }
-.emoji-raising_hand_tone1 { background-position: -740px 0; }
-.emoji-raising_hand_tone2 { background-position: -740px -20px; }
-.emoji-raising_hand_tone3 { background-position: -740px -40px; }
-.emoji-raising_hand_tone4 { background-position: -740px -60px; }
-.emoji-raising_hand_tone5 { background-position: -740px -80px; }
-.emoji-ram { background-position: -740px -100px; }
-.emoji-ramen { background-position: -740px -120px; }
-.emoji-rat { background-position: -740px -140px; }
-.emoji-record_button { background-position: -740px -160px; }
-.emoji-recycle { background-position: -740px -180px; }
-.emoji-red_car { background-position: -740px -200px; }
-.emoji-red_circle { background-position: -740px -220px; }
-.emoji-registered { background-position: -740px -240px; }
-.emoji-relaxed { background-position: -740px -260px; }
-.emoji-relieved { background-position: -740px -280px; }
-.emoji-reminder_ribbon { background-position: -740px -300px; }
-.emoji-repeat { background-position: -740px -320px; }
-.emoji-repeat_one { background-position: -740px -340px; }
-.emoji-restroom { background-position: -740px -360px; }
-.emoji-revolving_hearts { background-position: -740px -380px; }
-.emoji-rewind { background-position: -740px -400px; }
-.emoji-rhino { background-position: -740px -420px; }
-.emoji-ribbon { background-position: -740px -440px; }
-.emoji-rice { background-position: -740px -460px; }
-.emoji-rice_ball { background-position: -740px -480px; }
-.emoji-rice_cracker { background-position: -740px -500px; }
-.emoji-rice_scene { background-position: -740px -520px; }
-.emoji-right_facing_fist { background-position: -740px -540px; }
-.emoji-right_facing_fist_tone1 { background-position: -740px -560px; }
-.emoji-right_facing_fist_tone2 { background-position: -740px -580px; }
-.emoji-right_facing_fist_tone3 { background-position: -740px -600px; }
-.emoji-right_facing_fist_tone4 { background-position: -740px -620px; }
-.emoji-right_facing_fist_tone5 { background-position: -740px -640px; }
-.emoji-ring { background-position: -740px -660px; }
-.emoji-robot { background-position: -740px -680px; }
-.emoji-rocket { background-position: -740px -700px; }
-.emoji-rofl { background-position: -740px -720px; }
-.emoji-roller_coaster { background-position: 0 -740px; }
-.emoji-rolling_eyes { background-position: -20px -740px; }
-.emoji-rooster { background-position: -40px -740px; }
-.emoji-rose { background-position: -60px -740px; }
-.emoji-rosette { background-position: -80px -740px; }
-.emoji-rotating_light { background-position: -100px -740px; }
-.emoji-round_pushpin { background-position: -120px -740px; }
-.emoji-rowboat { background-position: -140px -740px; }
-.emoji-rowboat_tone1 { background-position: -160px -740px; }
-.emoji-rowboat_tone2 { background-position: -180px -740px; }
-.emoji-rowboat_tone3 { background-position: -200px -740px; }
-.emoji-rowboat_tone4 { background-position: -220px -740px; }
-.emoji-rowboat_tone5 { background-position: -240px -740px; }
-.emoji-rugby_football { background-position: -260px -740px; }
-.emoji-runner { background-position: -280px -740px; }
-.emoji-runner_tone1 { background-position: -300px -740px; }
-.emoji-runner_tone2 { background-position: -320px -740px; }
-.emoji-runner_tone3 { background-position: -340px -740px; }
-.emoji-runner_tone4 { background-position: -360px -740px; }
-.emoji-runner_tone5 { background-position: -380px -740px; }
-.emoji-running_shirt_with_sash { background-position: -400px -740px; }
-.emoji-sa { background-position: -420px -740px; }
-.emoji-sagittarius { background-position: -440px -740px; }
-.emoji-sailboat { background-position: -460px -740px; }
-.emoji-sake { background-position: -480px -740px; }
-.emoji-salad { background-position: -500px -740px; }
-.emoji-sandal { background-position: -520px -740px; }
-.emoji-santa { background-position: -540px -740px; }
-.emoji-santa_tone1 { background-position: -560px -740px; }
-.emoji-santa_tone2 { background-position: -580px -740px; }
-.emoji-santa_tone3 { background-position: -600px -740px; }
-.emoji-santa_tone4 { background-position: -620px -740px; }
-.emoji-santa_tone5 { background-position: -640px -740px; }
-.emoji-satellite { background-position: -660px -740px; }
-.emoji-satellite_orbital { background-position: -680px -740px; }
-.emoji-saxophone { background-position: -700px -740px; }
-.emoji-scales { background-position: -720px -740px; }
-.emoji-school { background-position: -740px -740px; }
-.emoji-school_satchel { background-position: -760px 0; }
-.emoji-scissors { background-position: -760px -20px; }
-.emoji-scooter { background-position: -760px -40px; }
-.emoji-scorpion { background-position: -760px -60px; }
-.emoji-scorpius { background-position: -760px -80px; }
-.emoji-scream { background-position: -760px -100px; }
-.emoji-scream_cat { background-position: -760px -120px; }
-.emoji-scroll { background-position: -760px -140px; }
-.emoji-seat { background-position: -760px -160px; }
-.emoji-second_place { background-position: -760px -180px; }
-.emoji-secret { background-position: -760px -200px; }
-.emoji-see_no_evil { background-position: -760px -220px; }
-.emoji-seedling { background-position: -760px -240px; }
-.emoji-selfie { background-position: -760px -260px; }
-.emoji-selfie_tone1 { background-position: -760px -280px; }
-.emoji-selfie_tone2 { background-position: -760px -300px; }
-.emoji-selfie_tone3 { background-position: -760px -320px; }
-.emoji-selfie_tone4 { background-position: -760px -340px; }
-.emoji-selfie_tone5 { background-position: -760px -360px; }
-.emoji-seven { background-position: -760px -380px; }
-.emoji-shallow_pan_of_food { background-position: -760px -400px; }
-.emoji-shamrock { background-position: -760px -420px; }
-.emoji-shark { background-position: -760px -440px; }
-.emoji-shaved_ice { background-position: -760px -460px; }
-.emoji-sheep { background-position: -760px -480px; }
-.emoji-shell { background-position: -760px -500px; }
-.emoji-shield { background-position: -760px -520px; }
-.emoji-shinto_shrine { background-position: -760px -540px; }
-.emoji-ship { background-position: -760px -560px; }
-.emoji-shirt { background-position: -760px -580px; }
-.emoji-shopping_bags { background-position: -760px -600px; }
-.emoji-shopping_cart { background-position: -760px -620px; }
-.emoji-shower { background-position: -760px -640px; }
-.emoji-shrimp { background-position: -760px -660px; }
-.emoji-shrug { background-position: -760px -680px; }
-.emoji-shrug_tone1 { background-position: -760px -700px; }
-.emoji-shrug_tone2 { background-position: -760px -720px; }
-.emoji-shrug_tone3 { background-position: -760px -740px; }
-.emoji-shrug_tone4 { background-position: 0 -760px; }
-.emoji-shrug_tone5 { background-position: -20px -760px; }
-.emoji-signal_strength { background-position: -40px -760px; }
-.emoji-six { background-position: -60px -760px; }
-.emoji-six_pointed_star { background-position: -80px -760px; }
-.emoji-ski { background-position: -100px -760px; }
-.emoji-skier { background-position: -120px -760px; }
-.emoji-skull { background-position: -140px -760px; }
-.emoji-skull_crossbones { background-position: -160px -760px; }
-.emoji-sleeping { background-position: -180px -760px; }
-.emoji-sleeping_accommodation { background-position: -200px -760px; }
-.emoji-sleepy { background-position: -220px -760px; }
-.emoji-slight_frown { background-position: -240px -760px; }
-.emoji-slight_smile { background-position: -260px -760px; }
-.emoji-slot_machine { background-position: -280px -760px; }
-.emoji-small_blue_diamond { background-position: -300px -760px; }
-.emoji-small_orange_diamond { background-position: -320px -760px; }
-.emoji-small_red_triangle { background-position: -340px -760px; }
-.emoji-small_red_triangle_down { background-position: -360px -760px; }
-.emoji-smile { background-position: -380px -760px; }
-.emoji-smile_cat { background-position: -400px -760px; }
-.emoji-smiley { background-position: -420px -760px; }
-.emoji-smiley_cat { background-position: -440px -760px; }
-.emoji-smiling_imp { background-position: -460px -760px; }
-.emoji-smirk { background-position: -480px -760px; }
-.emoji-smirk_cat { background-position: -500px -760px; }
-.emoji-smoking { background-position: -520px -760px; }
-.emoji-snail { background-position: -540px -760px; }
-.emoji-snake { background-position: -560px -760px; }
-.emoji-sneezing_face { background-position: -580px -760px; }
-.emoji-snowboarder { background-position: -600px -760px; }
-.emoji-snowflake { background-position: -620px -760px; }
-.emoji-snowman { background-position: -640px -760px; }
-.emoji-snowman2 { background-position: -660px -760px; }
-.emoji-sob { background-position: -680px -760px; }
-.emoji-soccer { background-position: -700px -760px; }
-.emoji-soon { background-position: -720px -760px; }
-.emoji-sos { background-position: -740px -760px; }
-.emoji-sound { background-position: -760px -760px; }
-.emoji-space_invader { background-position: -780px 0; }
-.emoji-spades { background-position: -780px -20px; }
-.emoji-spaghetti { background-position: -780px -40px; }
-.emoji-sparkle { background-position: -780px -60px; }
-.emoji-sparkler { background-position: -780px -80px; }
-.emoji-sparkles { background-position: -780px -100px; }
-.emoji-sparkling_heart { background-position: -780px -120px; }
-.emoji-speak_no_evil { background-position: -780px -140px; }
-.emoji-speaker { background-position: -780px -160px; }
-.emoji-speaking_head { background-position: -780px -180px; }
-.emoji-speech_balloon { background-position: -780px -200px; }
-.emoji-speech_left { background-position: -780px -220px; }
-.emoji-speedboat { background-position: -780px -240px; }
-.emoji-spider { background-position: -780px -260px; }
-.emoji-spider_web { background-position: -780px -280px; }
-.emoji-spoon { background-position: -780px -300px; }
-.emoji-spy { background-position: -780px -320px; }
-.emoji-spy_tone1 { background-position: -780px -340px; }
-.emoji-spy_tone2 { background-position: -780px -360px; }
-.emoji-spy_tone3 { background-position: -780px -380px; }
-.emoji-spy_tone4 { background-position: -780px -400px; }
-.emoji-spy_tone5 { background-position: -780px -420px; }
-.emoji-squid { background-position: -780px -440px; }
-.emoji-stadium { background-position: -780px -460px; }
-.emoji-star { background-position: -780px -480px; }
-.emoji-star2 { background-position: -780px -500px; }
-.emoji-star_and_crescent { background-position: -780px -520px; }
-.emoji-star_of_david { background-position: -780px -540px; }
-.emoji-stars { background-position: -780px -560px; }
-.emoji-station { background-position: -780px -580px; }
-.emoji-statue_of_liberty { background-position: -780px -600px; }
-.emoji-steam_locomotive { background-position: -780px -620px; }
-.emoji-stew { background-position: -780px -640px; }
-.emoji-stop_button { background-position: -780px -660px; }
-.emoji-stopwatch { background-position: -780px -680px; }
-.emoji-straight_ruler { background-position: -780px -700px; }
-.emoji-strawberry { background-position: -780px -720px; }
-.emoji-stuck_out_tongue { background-position: -780px -740px; }
-.emoji-stuck_out_tongue_closed_eyes { background-position: -780px -760px; }
-.emoji-stuck_out_tongue_winking_eye { background-position: 0 -780px; }
-.emoji-stuffed_flatbread { background-position: -20px -780px; }
-.emoji-sun_with_face { background-position: -40px -780px; }
-.emoji-sunflower { background-position: -60px -780px; }
-.emoji-sunglasses { background-position: -80px -780px; }
-.emoji-sunny { background-position: -100px -780px; }
-.emoji-sunrise { background-position: -120px -780px; }
-.emoji-sunrise_over_mountains { background-position: -140px -780px; }
-.emoji-surfer { background-position: -160px -780px; }
-.emoji-surfer_tone1 { background-position: -180px -780px; }
-.emoji-surfer_tone2 { background-position: -200px -780px; }
-.emoji-surfer_tone3 { background-position: -220px -780px; }
-.emoji-surfer_tone4 { background-position: -240px -780px; }
-.emoji-surfer_tone5 { background-position: -260px -780px; }
-.emoji-sushi { background-position: -280px -780px; }
-.emoji-suspension_railway { background-position: -300px -780px; }
-.emoji-sweat { background-position: -320px -780px; }
-.emoji-sweat_drops { background-position: -340px -780px; }
-.emoji-sweat_smile { background-position: -360px -780px; }
-.emoji-sweet_potato { background-position: -380px -780px; }
-.emoji-swimmer { background-position: -400px -780px; }
-.emoji-swimmer_tone1 { background-position: -420px -780px; }
-.emoji-swimmer_tone2 { background-position: -440px -780px; }
-.emoji-swimmer_tone3 { background-position: -460px -780px; }
-.emoji-swimmer_tone4 { background-position: -480px -780px; }
-.emoji-swimmer_tone5 { background-position: -500px -780px; }
-.emoji-symbols { background-position: -520px -780px; }
-.emoji-synagogue { background-position: -540px -780px; }
-.emoji-syringe { background-position: -560px -780px; }
-.emoji-taco { background-position: -580px -780px; }
-.emoji-tada { background-position: -600px -780px; }
-.emoji-tanabata_tree { background-position: -620px -780px; }
-.emoji-tangerine { background-position: -640px -780px; }
-.emoji-taurus { background-position: -660px -780px; }
-.emoji-taxi { background-position: -680px -780px; }
-.emoji-tea { background-position: -700px -780px; }
-.emoji-telephone { background-position: -720px -780px; }
-.emoji-telephone_receiver { background-position: -740px -780px; }
-.emoji-telescope { background-position: -760px -780px; }
-.emoji-ten { background-position: -780px -780px; }
-.emoji-tennis { background-position: -800px 0; }
-.emoji-tent { background-position: -800px -20px; }
-.emoji-thermometer { background-position: -800px -40px; }
-.emoji-thermometer_face { background-position: -800px -60px; }
-.emoji-thinking { background-position: -800px -80px; }
-.emoji-third_place { background-position: -800px -100px; }
-.emoji-thought_balloon { background-position: -800px -120px; }
-.emoji-three { background-position: -800px -140px; }
-.emoji-thumbsdown { background-position: -800px -160px; }
-.emoji-thumbsdown_tone1 { background-position: -800px -180px; }
-.emoji-thumbsdown_tone2 { background-position: -800px -200px; }
-.emoji-thumbsdown_tone3 { background-position: -800px -220px; }
-.emoji-thumbsdown_tone4 { background-position: -800px -240px; }
-.emoji-thumbsdown_tone5 { background-position: -800px -260px; }
-.emoji-thumbsup { background-position: -800px -280px; }
-.emoji-thumbsup_tone1 { background-position: -800px -300px; }
-.emoji-thumbsup_tone2 { background-position: -800px -320px; }
-.emoji-thumbsup_tone3 { background-position: -800px -340px; }
-.emoji-thumbsup_tone4 { background-position: -800px -360px; }
-.emoji-thumbsup_tone5 { background-position: -800px -380px; }
-.emoji-thunder_cloud_rain { background-position: -800px -400px; }
-.emoji-ticket { background-position: -800px -420px; }
-.emoji-tickets { background-position: -800px -440px; }
-.emoji-tiger { background-position: -800px -460px; }
-.emoji-tiger2 { background-position: -800px -480px; }
-.emoji-timer { background-position: -800px -500px; }
-.emoji-tired_face { background-position: -800px -520px; }
-.emoji-tm { background-position: -800px -540px; }
-.emoji-toilet { background-position: -800px -560px; }
-.emoji-tokyo_tower { background-position: -800px -580px; }
-.emoji-tomato { background-position: -800px -600px; }
-.emoji-tone1 { background-position: -800px -620px; }
-.emoji-tone2 { background-position: -800px -640px; }
-.emoji-tone3 { background-position: -800px -660px; }
-.emoji-tone4 { background-position: -800px -680px; }
-.emoji-tone5 { background-position: -800px -700px; }
-.emoji-tongue { background-position: -800px -720px; }
-.emoji-tools { background-position: -800px -740px; }
-.emoji-top { background-position: -800px -760px; }
-.emoji-tophat { background-position: -800px -780px; }
-.emoji-track_next { background-position: 0 -800px; }
-.emoji-track_previous { background-position: -20px -800px; }
-.emoji-trackball { background-position: -40px -800px; }
-.emoji-tractor { background-position: -60px -800px; }
-.emoji-traffic_light { background-position: -80px -800px; }
-.emoji-train { background-position: -100px -800px; }
-.emoji-train2 { background-position: -120px -800px; }
-.emoji-tram { background-position: -140px -800px; }
-.emoji-triangular_flag_on_post { background-position: -160px -800px; }
-.emoji-triangular_ruler { background-position: -180px -800px; }
-.emoji-trident { background-position: -200px -800px; }
-.emoji-triumph { background-position: -220px -800px; }
-.emoji-trolleybus { background-position: -240px -800px; }
-.emoji-trophy { background-position: -260px -800px; }
-.emoji-tropical_drink { background-position: -280px -800px; }
-.emoji-tropical_fish { background-position: -300px -800px; }
-.emoji-truck { background-position: -320px -800px; }
-.emoji-trumpet { background-position: -340px -800px; }
-.emoji-tulip { background-position: -360px -800px; }
-.emoji-tumbler_glass { background-position: -380px -800px; }
-.emoji-turkey { background-position: -400px -800px; }
-.emoji-turtle { background-position: -420px -800px; }
-.emoji-tv { background-position: -440px -800px; }
-.emoji-twisted_rightwards_arrows { background-position: -460px -800px; }
-.emoji-two { background-position: -480px -800px; }
-.emoji-two_hearts { background-position: -500px -800px; }
-.emoji-two_men_holding_hands { background-position: -520px -800px; }
-.emoji-two_women_holding_hands { background-position: -540px -800px; }
-.emoji-u5272 { background-position: -560px -800px; }
-.emoji-u5408 { background-position: -580px -800px; }
-.emoji-u55b6 { background-position: -600px -800px; }
-.emoji-u6307 { background-position: -620px -800px; }
-.emoji-u6708 { background-position: -640px -800px; }
-.emoji-u6709 { background-position: -660px -800px; }
-.emoji-u6e80 { background-position: -680px -800px; }
-.emoji-u7121 { background-position: -700px -800px; }
-.emoji-u7533 { background-position: -720px -800px; }
-.emoji-u7981 { background-position: -740px -800px; }
-.emoji-u7a7a { background-position: -760px -800px; }
-.emoji-umbrella { background-position: -780px -800px; }
-.emoji-umbrella2 { background-position: -800px -800px; }
-.emoji-unamused { background-position: -820px 0; }
-.emoji-underage { background-position: -820px -20px; }
-.emoji-unicorn { background-position: -820px -40px; }
-.emoji-unlock { background-position: -820px -60px; }
-.emoji-up { background-position: -820px -80px; }
-.emoji-upside_down { background-position: -820px -100px; }
-.emoji-urn { background-position: -820px -120px; }
-.emoji-v { background-position: -820px -140px; }
-.emoji-v_tone1 { background-position: -820px -160px; }
-.emoji-v_tone2 { background-position: -820px -180px; }
-.emoji-v_tone3 { background-position: -820px -200px; }
-.emoji-v_tone4 { background-position: -820px -220px; }
-.emoji-v_tone5 { background-position: -820px -240px; }
-.emoji-vertical_traffic_light { background-position: -820px -260px; }
-.emoji-vhs { background-position: -820px -280px; }
-.emoji-vibration_mode { background-position: -820px -300px; }
-.emoji-video_camera { background-position: -820px -320px; }
-.emoji-video_game { background-position: -820px -340px; }
-.emoji-violin { background-position: -820px -360px; }
-.emoji-virgo { background-position: -820px -380px; }
-.emoji-volcano { background-position: -820px -400px; }
-.emoji-volleyball { background-position: -820px -420px; }
-.emoji-vs { background-position: -820px -440px; }
-.emoji-vulcan { background-position: -820px -460px; }
-.emoji-vulcan_tone1 { background-position: -820px -480px; }
-.emoji-vulcan_tone2 { background-position: -820px -500px; }
-.emoji-vulcan_tone3 { background-position: -820px -520px; }
-.emoji-vulcan_tone4 { background-position: -820px -540px; }
-.emoji-vulcan_tone5 { background-position: -820px -560px; }
-.emoji-walking { background-position: -820px -580px; }
-.emoji-walking_tone1 { background-position: -820px -600px; }
-.emoji-walking_tone2 { background-position: -820px -620px; }
-.emoji-walking_tone3 { background-position: -820px -640px; }
-.emoji-walking_tone4 { background-position: -820px -660px; }
-.emoji-walking_tone5 { background-position: -820px -680px; }
-.emoji-waning_crescent_moon { background-position: -820px -700px; }
-.emoji-waning_gibbous_moon { background-position: -820px -720px; }
-.emoji-warning { background-position: -820px -740px; }
-.emoji-wastebasket { background-position: -820px -760px; }
-.emoji-watch { background-position: -820px -780px; }
-.emoji-water_buffalo { background-position: -820px -800px; }
-.emoji-water_polo { background-position: 0 -820px; }
-.emoji-water_polo_tone1 { background-position: -20px -820px; }
-.emoji-water_polo_tone2 { background-position: -40px -820px; }
-.emoji-water_polo_tone3 { background-position: -60px -820px; }
-.emoji-water_polo_tone4 { background-position: -80px -820px; }
-.emoji-water_polo_tone5 { background-position: -100px -820px; }
-.emoji-watermelon { background-position: -120px -820px; }
-.emoji-wave { background-position: -140px -820px; }
-.emoji-wave_tone1 { background-position: -160px -820px; }
-.emoji-wave_tone2 { background-position: -180px -820px; }
-.emoji-wave_tone3 { background-position: -200px -820px; }
-.emoji-wave_tone4 { background-position: -220px -820px; }
-.emoji-wave_tone5 { background-position: -240px -820px; }
-.emoji-wavy_dash { background-position: -260px -820px; }
-.emoji-waxing_crescent_moon { background-position: -280px -820px; }
-.emoji-waxing_gibbous_moon { background-position: -300px -820px; }
-.emoji-wc { background-position: -320px -820px; }
-.emoji-weary { background-position: -340px -820px; }
-.emoji-wedding { background-position: -360px -820px; }
-.emoji-whale { background-position: -380px -820px; }
-.emoji-whale2 { background-position: -400px -820px; }
-.emoji-wheel_of_dharma { background-position: -420px -820px; }
-.emoji-wheelchair { background-position: -440px -820px; }
-.emoji-white_check_mark { background-position: -460px -820px; }
-.emoji-white_circle { background-position: -480px -820px; }
-.emoji-white_flower { background-position: -500px -820px; }
-.emoji-white_large_square { background-position: -520px -820px; }
-.emoji-white_medium_small_square { background-position: -540px -820px; }
-.emoji-white_medium_square { background-position: -560px -820px; }
-.emoji-white_small_square { background-position: -580px -820px; }
-.emoji-white_square_button { background-position: -600px -820px; }
-.emoji-white_sun_cloud { background-position: -620px -820px; }
-.emoji-white_sun_rain_cloud { background-position: -640px -820px; }
-.emoji-white_sun_small_cloud { background-position: -660px -820px; }
-.emoji-wilted_rose { background-position: -680px -820px; }
-.emoji-wind_blowing_face { background-position: -700px -820px; }
-.emoji-wind_chime { background-position: -720px -820px; }
-.emoji-wine_glass { background-position: -740px -820px; }
-.emoji-wink { background-position: -760px -820px; }
-.emoji-wolf { background-position: -780px -820px; }
-.emoji-woman { background-position: -800px -820px; }
-.emoji-woman_tone1 { background-position: -820px -820px; }
-.emoji-woman_tone2 { background-position: -840px 0; }
-.emoji-woman_tone3 { background-position: -840px -20px; }
-.emoji-woman_tone4 { background-position: -840px -40px; }
-.emoji-woman_tone5 { background-position: -840px -60px; }
-.emoji-womans_clothes { background-position: -840px -80px; }
-.emoji-womans_hat { background-position: -840px -100px; }
-.emoji-womens { background-position: -840px -120px; }
-.emoji-worried { background-position: -840px -140px; }
-.emoji-wrench { background-position: -840px -160px; }
-.emoji-wrestlers { background-position: -840px -180px; }
-.emoji-wrestlers_tone1 { background-position: -840px -200px; }
-.emoji-wrestlers_tone2 { background-position: -840px -220px; }
-.emoji-wrestlers_tone3 { background-position: -840px -240px; }
-.emoji-wrestlers_tone4 { background-position: -840px -260px; }
-.emoji-wrestlers_tone5 { background-position: -840px -280px; }
-.emoji-writing_hand { background-position: -840px -300px; }
-.emoji-writing_hand_tone1 { background-position: -840px -320px; }
-.emoji-writing_hand_tone2 { background-position: -840px -340px; }
-.emoji-writing_hand_tone3 { background-position: -840px -360px; }
-.emoji-writing_hand_tone4 { background-position: -840px -380px; }
-.emoji-writing_hand_tone5 { background-position: -840px -400px; }
-.emoji-x { background-position: -840px -420px; }
-.emoji-yellow_heart { background-position: -840px -440px; }
-.emoji-yen { background-position: -840px -460px; }
-.emoji-yin_yang { background-position: -840px -480px; }
-.emoji-yum { background-position: -840px -500px; }
-.emoji-zap { background-position: -840px -520px; }
-.emoji-zero { background-position: -840px -540px; }
-.emoji-zipper_mouth { background-position: -840px -560px; }
-.emoji-100 { background-position: -840px -580px; }
-
-.emoji-icon {
- background-image: image-url('emoji.png');
- background-repeat: no-repeat;
- color: transparent;
- text-indent: -99em;
- height: 20px;
- width: 20px;
-
- @media only screen and (-webkit-min-device-pixel-ratio: 2),
- only screen and (min--moz-device-pixel-ratio: 2),
- only screen and (-o-min-device-pixel-ratio: 2/1),
- only screen and (min-device-pixel-ratio: 2),
- only screen and (min-resolution: 192dpi),
- only screen and (min-resolution: 2dppx) {
- background-image: image-url('emoji@2x.png');
- background-size: 860px 840px;
- }
-}
diff --git a/app/assets/stylesheets/framework/gitlab_theme.scss b/app/assets/stylesheets/framework/gitlab_theme.scss
index 23c7e0a2a0d..12d313a373c 100644
--- a/app/assets/stylesheets/framework/gitlab_theme.scss
+++ b/app/assets/stylesheets/framework/gitlab_theme.scss
@@ -177,25 +177,6 @@
}
}
- // Web IDE
- .ide-sidebar-link {
- color: $color-200;
- background-color: $color-700;
-
- &:hover,
- &:focus {
- background-color: $color-500;
- }
-
- &:active {
- background: $color-800;
- }
- }
-
- .branch-container {
- border-left-color: $color-700;
- }
-
.branch-header-title {
color: $color-700;
}
@@ -203,6 +184,13 @@
.ide-file-list .file.file-active {
color: $color-700;
}
+
+ .ide-sidebar-link {
+ &.active {
+ color: $color-700;
+ box-shadow: inset 3px 0 $color-700;
+ }
+ }
}
body {
@@ -343,9 +331,5 @@ body {
.sidebar-top-level-items > li.active .badge.badge-pill {
color: $theme-gray-900;
}
-
- .ide-sidebar-link {
- color: $white-light;
- }
}
}
diff --git a/app/assets/stylesheets/framework/images.scss b/app/assets/stylesheets/framework/images.scss
index 62a0fba3da3..ab3cceceae9 100644
--- a/app/assets/stylesheets/framework/images.scss
+++ b/app/assets/stylesheets/framework/images.scss
@@ -39,35 +39,10 @@
svg {
fill: currentColor;
- &.s8 {
- @include svg-size(8px);
- }
-
- &.s12 {
- @include svg-size(12px);
- }
-
- &.s16 {
- @include svg-size(16px);
- }
-
- &.s18 {
- @include svg-size(18px);
- }
-
- &.s24 {
- @include svg-size(24px);
- }
-
- &.s32 {
- @include svg-size(32px);
- }
-
- &.s48 {
- @include svg-size(48px);
- }
-
- &.s72 {
- @include svg-size(72px);
+ $svg-sizes: 8 12 16 18 24 32 48 72;
+ @each $svg-size in $svg-sizes {
+ &.s#{$svg-size} {
+ @include svg-size(#{$svg-size}px);
+ }
}
}
diff --git a/app/assets/stylesheets/framework/markdown_area.scss b/app/assets/stylesheets/framework/markdown_area.scss
index 82a2755aad2..b893151e4fe 100644
--- a/app/assets/stylesheets/framework/markdown_area.scss
+++ b/app/assets/stylesheets/framework/markdown_area.scss
@@ -107,6 +107,16 @@
padding-top: 10px;
}
+.referenced-commands {
+ background: $blue-50;
+ padding: $gl-padding-8 $gl-padding;
+ border-radius: $border-radius-default;
+
+ p {
+ margin: 0;
+ }
+}
+
.md-preview-holder {
min-height: 167px;
padding: 10px 0;
diff --git a/app/assets/stylesheets/framework/terms.scss b/app/assets/stylesheets/framework/terms.scss
new file mode 100644
index 00000000000..16293d32dfa
--- /dev/null
+++ b/app/assets/stylesheets/framework/terms.scss
@@ -0,0 +1,59 @@
+.terms {
+ .with-performance-bar & {
+ margin-top: 0;
+ }
+
+ .alert-wrapper {
+ min-height: $header-height + $gl-padding;
+ }
+
+ .content {
+ padding-top: $gl-padding;
+ }
+
+ .panel {
+ .panel-heading {
+ display: -webkit-flex;
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+
+ .title {
+ display: flex;
+ align-items: center;
+
+ .logo-text {
+ width: 55px;
+ height: 24px;
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ }
+ }
+
+ .navbar-collapse {
+ padding-right: 0;
+ }
+
+ .nav li a {
+ color: $theme-gray-700;
+ }
+ }
+
+ .panel-content {
+ padding: $gl-padding;
+
+ *:first-child {
+ margin-top: 0;
+ }
+
+ *:last-child {
+ margin-bottom: 0;
+ }
+ }
+
+ .footer-block {
+ margin: 0;
+ }
+ }
+}
diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss
index 8c44ebc85ef..b5505538541 100644
--- a/app/assets/stylesheets/framework/variables.scss
+++ b/app/assets/stylesheets/framework/variables.scss
@@ -212,6 +212,7 @@ $tooltip-font-size: 12px;
/*
* Padding
*/
+$gl-padding-24: 24px;
$gl-padding: 16px;
$gl-padding-8: 8px;
$gl-padding-4: 4px;
@@ -229,6 +230,7 @@ $row-hover: $blue-50;
$row-hover-border: $blue-200;
$progress-color: #c0392b;
$header-height: 40px;
+$ide-statusbar-height: 27px;
$fixed-layout-width: 1280px;
$limited-layout-width: 990px;
$limited-layout-width-sm: 790px;
@@ -263,6 +265,7 @@ $performance-bar-height: 35px;
$flash-height: 52px;
$context-header-height: 60px;
$breadcrumb-min-height: 48px;
+$gcp-signup-offer-icon-max-width: 125px;
/*
* Common component specific colors
@@ -332,11 +335,10 @@ $diff-jagged-border-gradient-color: darken($white-normal, 8%);
/*
* Fonts
*/
-$monospace_font: 'Menlo', 'DejaVu Sans Mono', 'Liberation Mono', 'Consolas',
- 'Ubuntu Mono', 'Courier New', 'andale mono', 'lucida console', monospace;
-$regular_font: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto,
- Oxygen-Sans, Ubuntu, Cantarell, 'Helvetica Neue', sans-serif,
- 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol';
+$monospace_font: 'Menlo', 'DejaVu Sans Mono', 'Liberation Mono', 'Consolas', 'Ubuntu Mono',
+ 'Courier New', 'andale mono', 'lucida console', monospace;
+$regular_font: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen-Sans, Ubuntu, Cantarell,
+ 'Helvetica Neue', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol';
/*
* Dropdowns
@@ -464,11 +466,9 @@ $issue-boards-card-shadow: rgba(186, 186, 186, 0.5);
*/
$issue-boards-filter-height: 68px;
$issue-boards-breadcrumbs-height-xs: 63px;
-$issue-board-list-difference-xs: $header-height +
- $issue-boards-breadcrumbs-height-xs;
+$issue-board-list-difference-xs: $header-height + $issue-boards-breadcrumbs-height-xs;
$issue-board-list-difference-sm: $header-height + $breadcrumb-min-height;
-$issue-board-list-difference-md: $issue-board-list-difference-sm +
- $issue-boards-filter-height;
+$issue-board-list-difference-md: $issue-board-list-difference-sm + $issue-boards-filter-height;
/*
* Avatar
@@ -689,6 +689,8 @@ $stage-hover-bg: $gray-darker;
$ci-action-icon-size: 22px;
$pipeline-dropdown-line-height: 20px;
$pipeline-dropdown-status-icon-size: 18px;
+$ci-action-dropdown-button-size: 24px;
+$ci-action-dropdown-svg-size: 12px;
/*
CI variable lists
diff --git a/app/assets/stylesheets/pages/boards.scss b/app/assets/stylesheets/pages/boards.scss
index d4509afc0bc..cd47f34d565 100644
--- a/app/assets/stylesheets/pages/boards.scss
+++ b/app/assets/stylesheets/pages/boards.scss
@@ -317,6 +317,7 @@
a {
color: $gl-text-color;
word-wrap: break-word;
+ word-break: break-word;
margin-right: 2px;
}
}
@@ -462,6 +463,7 @@
.issuable-header-text {
padding-right: 35px;
+ word-break: break-word;
> strong {
font-weight: $gl-font-weight-bold;
diff --git a/app/assets/stylesheets/pages/clusters.scss b/app/assets/stylesheets/pages/clusters.scss
index 7b8ee026357..2b6d92016d5 100644
--- a/app/assets/stylesheets/pages/clusters.scss
+++ b/app/assets/stylesheets/pages/clusters.scss
@@ -26,3 +26,51 @@
margin-right: 0;
}
}
+
+.gcp-signup-offer {
+ background-color: $blue-50;
+ border: 1px solid $blue-300;
+ border-radius: $border-radius-default;
+
+ // TODO: To be superceded by cssLab
+ &.alert {
+ padding: 24px 16px;
+
+ &-dismissable {
+ padding-right: 32px;
+
+ .close {
+ top: -8px;
+ right: -16px;
+ color: $blue-500;
+ opacity: 1;
+ }
+ }
+ }
+
+ .gcp-logo {
+ margin-bottom: $gl-padding;
+ text-align: center;
+ }
+
+ img {
+ max-width: $gcp-signup-offer-icon-max-width;
+ }
+
+ a:not(.btn) {
+ color: $gl-link-color;
+ font-weight: normal;
+ text-decoration: none;
+ }
+
+ @include media-breakpoint-up(sm) {
+ > div {
+ display: flex;
+ align-items: center;
+ }
+
+ .gcp-logo {
+ margin: 0;
+ }
+ }
+}
diff --git a/app/assets/stylesheets/pages/commits.scss b/app/assets/stylesheets/pages/commits.scss
index 2339bfd263c..122bb48f0ee 100644
--- a/app/assets/stylesheets/pages/commits.scss
+++ b/app/assets/stylesheets/pages/commits.scss
@@ -2,7 +2,6 @@
background: none;
border: 0;
padding: 0;
- margin-top: 10px;
word-break: normal;
white-space: pre-wrap;
}
@@ -21,10 +20,6 @@
margin: 0;
color: $gl-text-color;
}
-
- .commit-description {
- margin-top: 15px;
- }
}
.commit-hash-full {
@@ -178,7 +173,7 @@
.commit-detail {
display: flex;
justify-content: space-between;
- align-items: center;
+ align-items: start;
flex-grow: 1;
}
@@ -268,21 +263,16 @@
.commit-row-description {
font-size: 14px;
- padding: 10px 15px;
- margin: 10px 0;
- background: $gray-light;
+ padding: 0 0 0 $gl-padding-8;
+ border: 0;
display: none;
white-space: pre-wrap;
word-break: normal;
- border: 1px solid $border-color;
-
- pre {
- border: 0;
- background: inherit;
- padding: 0;
- margin: 0;
- white-space: pre-wrap;
- }
+ color: $gl-text-color-secondary;
+ background: none;
+ font-family: inherit;
+ border-left: 2px solid $theme-gray-300;
+ border-radius: unset;
a {
color: $gl-text-color;
diff --git a/app/assets/stylesheets/pages/diff.scss b/app/assets/stylesheets/pages/diff.scss
index 4ba76fa1512..2b2e4620435 100644
--- a/app/assets/stylesheets/pages/diff.scss
+++ b/app/assets/stylesheets/pages/diff.scss
@@ -44,6 +44,12 @@
}
}
+ .note-text {
+ table {
+ font-family: $font-family-sans-serif;
+ }
+ }
+
table {
width: 100%;
font-family: $monospace_font;
diff --git a/app/assets/stylesheets/pages/environments.scss b/app/assets/stylesheets/pages/environments.scss
index 030948ca57a..cd0d67613c3 100644
--- a/app/assets/stylesheets/pages/environments.scss
+++ b/app/assets/stylesheets/pages/environments.scss
@@ -283,28 +283,59 @@
}
&.popover {
+ padding: 0;
+ border: 1px solid $border-color;
+
&.left {
left: auto;
right: 0;
margin-right: 10px;
+
+ > .arrow {
+ right: -16px;
+ border-left-color: $border-color;
+ }
+
+ > .arrow::after {
+ border-left-color: $theme-gray-50;
+ }
}
&.right {
left: 0;
right: auto;
margin-left: 10px;
+
+ > .arrow {
+ left: -16px;
+ border-right-color: $border-color;
+ }
+
+ > .arrow::after {
+ border-right-color: $theme-gray-50;
+ }
}
> .arrow {
- top: 40px;
+ top: 16px;
+ margin-top: -8px;
+ border-width: 8px;
}
> .popover-title,
> .popover-content {
- padding: 5px 8px;
+ padding: 8px;
font-size: 12px;
white-space: nowrap;
}
+
+ > .popover-title {
+ background-color: $theme-gray-50;
+ }
+ }
+
+ strong {
+ font-weight: 600;
}
}
@@ -317,7 +348,7 @@
vertical-align: middle;
+ td {
- padding-left: 5px;
+ padding-left: 8px;
vertical-align: top;
}
}
diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss
index bb5ff3caaed..9eceb3e9a33 100644
--- a/app/assets/stylesheets/pages/merge_requests.scss
+++ b/app/assets/stylesheets/pages/merge_requests.scss
@@ -156,10 +156,6 @@
.dropdown-menu {
z-index: 300;
}
-
- .ci-action-icon-wrapper {
- line-height: 16px;
- }
}
.mini-pipeline-graph-dropdown-toggle {
diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss
index cc58dad15f5..15e26033197 100644
--- a/app/assets/stylesheets/pages/pipelines.scss
+++ b/app/assets/stylesheets/pages/pipelines.scss
@@ -22,7 +22,6 @@
}
.ci-table {
-
.badge {
margin-bottom: 3px;
}
@@ -123,7 +122,6 @@
}
.branch-commit {
-
.ref-name {
font-weight: $gl-font-weight-bold;
max-width: 100px;
@@ -481,43 +479,6 @@
@extend .build-content:hover;
}
- .ci-action-icon-container {
- position: absolute;
- right: 5px;
- top: 5px;
-
- // Action Icons in big pipeline-graph nodes
- &.ci-action-icon-wrapper {
- height: 30px;
- width: 30px;
- background: $white-light;
- border: 1px solid $border-color;
- border-radius: 100%;
- display: block;
-
- &:hover {
- background-color: $stage-hover-bg;
- border: 1px solid $dropdown-toggle-active-border-color;
-
- svg {
- fill: $gl-text-color;
- }
- }
-
- svg {
- fill: $gl-text-color-secondary;
- position: relative;
- top: -1px;
- }
-
- &.play {
- svg {
- left: 2px;
- }
- }
- }
- }
-
.ci-status-icon svg {
height: 20px;
width: 20px;
@@ -548,7 +509,6 @@
border: 1px solid $dropdown-toggle-active-border-color;
}
-
// Connect first build in each stage with right horizontal line
&:first-child {
&::after {
@@ -602,6 +562,43 @@
}
}
}
+
+ .ci-action-icon-container {
+ position: absolute;
+ right: 5px;
+ top: 5px;
+
+ // Action Icons in big pipeline-graph nodes
+ &.ci-action-icon-wrapper {
+ height: 30px;
+ width: 30px;
+ background: $white-light;
+ border: 1px solid $border-color;
+ border-radius: 100%;
+ display: block;
+
+ &:hover {
+ background-color: $stage-hover-bg;
+ border: 1px solid $dropdown-toggle-active-border-color;
+
+ svg {
+ fill: $gl-text-color;
+ }
+ }
+
+ svg {
+ fill: $gl-text-color-secondary;
+ position: relative;
+ top: -1px;
+ }
+
+ &.play {
+ svg {
+ left: 2px;
+ }
+ }
+ }
+ }
}
// Triggers the dropdown in the big pipeline graph
@@ -710,93 +707,77 @@ button.mini-pipeline-graph-dropdown-toggle {
}
}
-// dropdown content for big and mini pipeline
+/**
+ Action icons inside dropdowns:
+ - mini graph in pipelines table
+ - dropdown in big graph
+ - mini graph in MR widget pipeline
+ - mini graph in Commit widget pipeline
+*/
.big-pipeline-graph-dropdown-menu,
.mini-pipeline-graph-dropdown-menu {
width: 240px;
max-width: 240px;
- .scrollable-menu {
+ // override dropdown.scss
+ &.dropdown-menu li button,
+ &.dropdown-menu li a.ci-action-icon-container {
padding: 0;
- max-height: 245px;
- overflow: auto;
+ text-align: center;
}
- li {
- position: relative;
+ .ci-action-icon-container {
+ position: absolute;
+ right: 8px;
+ top: 8px;
- // ensure .mini-pipeline-graph-dropdown-item has hover style when action-icon is hovered
- &:hover > .mini-pipeline-graph-dropdown-item,
- &:hover > .ci-job-component > .mini-pipeline-graph-dropdown-item {
- @extend .mini-pipeline-graph-dropdown-item:hover;
- }
+ &.ci-action-icon-wrapper {
+ height: $ci-action-dropdown-button-size;
+ width: $ci-action-dropdown-button-size;
- // Action icon on the right
- a.ci-action-icon-wrapper {
- border-radius: 50%;
+ background: $white-light;
border: 1px solid $border-color;
- width: $ci-action-icon-size;
- height: $ci-action-icon-size;
- padding: 2px 0 0 5px;
- font-size: 12px;
- background-color: $white-light;
- position: absolute;
- top: 50%;
- right: $gl-padding;
- margin-top: -#{$ci-action-icon-size / 2};
+ border-radius: 50%;
+ display: block;
- &:hover,
- &:focus {
+ &:hover {
background-color: $stage-hover-bg;
border: 1px solid $dropdown-toggle-active-border-color;
+
+ svg {
+ fill: $gl-text-color;
+ }
}
svg {
+ width: $ci-action-dropdown-svg-size;
+ height: $ci-action-dropdown-svg-size;
fill: $gl-text-color-secondary;
- width: #{$ci-action-icon-size - 6};
- height: #{$ci-action-icon-size - 6};
- left: -3px;
position: relative;
- top: -1px;
-
- &.icon-action-stop,
- &.icon-action-cancel {
- width: 12px;
- height: 12px;
- top: 1px;
- left: -1px;
- }
-
- &.icon-action-play {
- width: 11px;
- height: 11px;
- top: 1px;
- left: 1px;
- }
-
- &.icon-action-retry {
- width: 16px;
- height: 16px;
- top: 0;
- left: -3px;
- }
+ top: 0;
+ vertical-align: initial;
}
+ }
+ }
- &:hover svg,
- &:focus svg {
- fill: $gl-text-color;
- }
+ // SVGs in the commit widget and mr widget
+ a.ci-action-icon-container.ci-action-icon-wrapper svg {
+ top: 2px;
+ }
- &.icon-action-retry,
- &.icon-action-play {
- svg {
- width: #{$ci-action-icon-size - 6};
- height: #{$ci-action-icon-size - 6};
- left: 8px;
- }
- }
+ .scrollable-menu {
+ padding: 0;
+ max-height: 245px;
+ overflow: auto;
+ }
+ li {
+ position: relative;
+ // ensure .mini-pipeline-graph-dropdown-item has hover style when action-icon is hovered
+ &:hover > .mini-pipeline-graph-dropdown-item,
+ &:hover > .ci-job-component > .mini-pipeline-graph-dropdown-item {
+ @extend .mini-pipeline-graph-dropdown-item:hover;
}
// link to the build
@@ -808,6 +789,11 @@ button.mini-pipeline-graph-dropdown-toggle {
line-height: $line-height-base;
white-space: nowrap;
+ // Match dropdown.scss for all `a` tags
+ &.non-details-job-component {
+ padding: 8px 16px;
+ }
+
.ci-job-name-component {
align-items: center;
display: flex;
diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss
index 868282bc5cb..c48f2d7f63c 100644
--- a/app/assets/stylesheets/pages/projects.scss
+++ b/app/assets/stylesheets/pages/projects.scss
@@ -206,7 +206,6 @@
.project-repo-buttons,
.group-buttons {
.btn {
- @include btn-gray;
padding: 3px 10px;
&:last-child {
@@ -295,7 +294,7 @@
}
.count {
- @include btn-gray;
+ @include btn-white;
display: inline-block;
background: $white-light;
border-radius: 2px;
@@ -355,30 +354,48 @@
min-width: 200px;
}
-.deploy-key-content {
- @include media-breakpoint-up(sm) {
- float: left;
+.deploy-keys {
+ .scrolling-tabs-container {
+ position: relative;
+ }
+}
- &:last-child {
- float: right;
+.deploy-key {
+ // Ensure that the fingerprint does not overflow on small screens
+ .fingerprint {
+ word-break: break-all;
+ white-space: normal;
+ }
+
+ .deploy-project-label,
+ .key-created-at {
+ svg {
+ vertical-align: text-top;
}
}
-}
-.deploy-key-projects {
- @include media-breakpoint-up(sm) {
- line-height: 42px;
+ .btn svg {
+ vertical-align: top;
+ }
+
+ .key-created-at {
+ line-height: unset;
}
}
-a.deploy-project-label {
- padding: 5px;
- margin-right: 5px;
- color: $gl-text-color;
- background-color: $row-hover;
+.deploy-project-list {
+ margin-bottom: -$gl-padding-4;
- &:hover {
- color: $gl-link-color;
+ a.deploy-project-label {
+ margin-right: $gl-padding-4;
+ margin-bottom: $gl-padding-4;
+ color: $gl-text-color-secondary;
+ background-color: $theme-gray-100;
+ line-height: $gl-btn-line-height;
+
+ &:hover {
+ color: $gl-link-color;
+ }
}
}
diff --git a/app/assets/stylesheets/pages/repo.scss b/app/assets/stylesheets/pages/repo.scss
index f4f887c4fa8..6ac4180e2ea 100644
--- a/app/assets/stylesheets/pages/repo.scss
+++ b/app/assets/stylesheets/pages/repo.scss
@@ -23,6 +23,7 @@
margin-top: 0;
border-top: 1px solid $white-dark;
border-bottom: 1px solid $white-dark;
+ padding-bottom: $ide-statusbar-height;
&.is-collapsed {
.ide-file-list {
@@ -121,14 +122,6 @@
.multi-file-loading-container {
margin-top: 10px;
padding: 10px;
-
- .animation-container {
- background: $gray-light;
-
- div {
- background: $gray-light;
- }
- }
}
.multi-file-table-col-commit-message {
@@ -155,69 +148,56 @@
}
li {
- position: relative;
- }
-
- .dropdown {
display: flex;
- margin-left: auto;
- margin-bottom: 1px;
- padding: 0 $grid-size;
- border-left: 1px solid $white-dark;
- background-color: $white-light;
-
- &.shadow {
- box-shadow: 0 0 10px $dropdown-shadow-color;
- }
+ align-items: center;
+ padding: $grid-size $gl-padding;
+ background-color: $gray-normal;
+ border-right: 1px solid $white-dark;
+ border-bottom: 1px solid $white-dark;
- .btn {
- margin-top: auto;
- margin-bottom: auto;
+ &.active {
+ background-color: $white-light;
+ border-bottom-color: $white-light;
}
}
}
.multi-file-tab {
- @include str-truncated(150px);
- padding: ($gl-padding / 2) ($gl-padding + 12) ($gl-padding / 2) $gl-padding;
- background-color: $gray-normal;
- border-right: 1px solid $white-dark;
- border-bottom: 1px solid $white-dark;
+ @include str-truncated(141px);
cursor: pointer;
svg {
vertical-align: middle;
}
-
- &.active {
- background-color: $white-light;
- border-bottom-color: $white-light;
- }
}
.multi-file-tab-close {
- position: absolute;
- right: 8px;
- top: 50%;
width: 16px;
height: 16px;
padding: 0;
+ margin-left: $grid-size;
background: none;
border: 0;
border-radius: $border-radius-default;
color: $theme-gray-900;
- transform: translateY(-50%);
svg {
position: relative;
top: -1px;
}
- &:hover {
+ .ide-file-changed-icon {
+ display: block;
+ position: relative;
+ top: 1px;
+ right: -2px;
+ }
+
+ &:not([disabled]):hover {
background-color: $theme-gray-200;
}
- &:focus {
+ &:not([disabled]):focus {
background-color: $blue-500;
color: $white-light;
outline: 0;
@@ -248,6 +228,17 @@
display: none;
}
+ .is-readonly,
+ .editor.original {
+ .view-lines {
+ cursor: default;
+ }
+
+ .cursors-layer {
+ display: none;
+ }
+ }
+
.monaco-diff-editor.vs {
.editor.modified {
box-shadow: none;
@@ -306,15 +297,12 @@
.margin-view-overlays .delete-sign {
opacity: 0.4;
}
-
- .cursors-layer {
- display: none;
- }
}
}
.multi-file-editor-holder {
height: 100%;
+ min-height: 0;
}
.preview-container {
@@ -380,6 +368,7 @@
.ide-btn-group {
padding: $gl-padding-4 $gl-vert-padding;
+ line-height: 24px;
}
.ide-status-bar {
@@ -387,7 +376,13 @@
padding: $gl-bar-padding $gl-padding;
background: $white-light;
display: flex;
- justify-content: flex-end;
+ justify-content: space-between;
+ height: $ide-statusbar-height;
+
+ position: absolute;
+ bottom: 0;
+ left: 0;
+ width: 100%;
> div + div {
padding-left: $gl-padding;
@@ -398,6 +393,14 @@
}
}
+.ide-status-file {
+ text-align: right;
+
+ .ide-status-branch + &,
+ &:first-child {
+ margin-left: auto;
+ }
+}
// Not great, but this is to deal with our current output
.multi-file-preview-holder {
height: 100%;
@@ -433,27 +436,35 @@
.multi-file-commit-panel {
display: flex;
position: relative;
- flex-direction: column;
width: 340px;
padding: 0;
background-color: $gray-light;
- padding-right: 3px;
+ padding-right: 1px;
+
+ .context-header {
+ width: auto;
+ margin-right: 0;
+
+ a:hover,
+ a:focus {
+ text-decoration: none;
+ }
+ }
.projects-sidebar {
+ min-height: 0;
display: flex;
flex-direction: column;
flex: 1;
-
- .context-header {
- width: auto;
- margin-right: 0;
- }
}
.multi-file-commit-panel-inner {
+ position: relative;
display: flex;
flex-direction: column;
height: 100%;
+ min-width: 0;
+ width: 100%;
}
.multi-file-commit-panel-inner-scroll {
@@ -461,68 +472,10 @@
flex: 1;
flex-direction: column;
overflow: auto;
- }
-
- &.is-collapsed {
- width: 60px;
-
- .multi-file-commit-list {
- padding-top: $gl-padding;
- overflow: hidden;
- }
-
- .multi-file-context-bar-icon {
- align-items: center;
-
- svg {
- float: none;
- margin: 0;
- }
- }
- }
-
- .branch-container {
- border-left: 4px solid;
- margin-bottom: $gl-bar-padding;
- }
-
- .branch-header {
- background: $white-dark;
- display: flex;
- }
-
- .branch-header-title {
- flex: 1;
- padding: $grid-size $gl-padding;
- font-weight: $gl-font-weight-bold;
-
- svg {
- vertical-align: middle;
- }
- }
-
- .branch-header-btns {
- padding: $gl-vert-padding $gl-padding;
- }
-
- .left-collapse-btn {
- display: none;
- background: $gray-light;
- text-align: left;
+ background-color: $white-light;
+ border-left: 1px solid $white-dark;
border-top: 1px solid $white-dark;
-
- svg {
- vertical-align: middle;
- }
- }
-}
-
-.multi-file-context-bar-icon {
- padding: 10px;
-
- svg {
- margin-right: 10px;
- float: left;
+ border-top-left-radius: $border-radius-small;
}
}
@@ -548,13 +501,13 @@
align-items: center;
margin-bottom: 0;
border-bottom: 1px solid $white-dark;
- padding: $gl-btn-padding 0;
+ padding: $gl-btn-padding $gl-padding;
}
.multi-file-commit-panel-header-title {
display: flex;
flex: 1;
- padding-left: $grid-size;
+ align-items: center;
svg {
margin-right: $gl-btn-padding;
@@ -570,7 +523,7 @@
.multi-file-commit-list {
flex: 1;
overflow: auto;
- padding: $gl-padding 0;
+ padding: $gl-padding;
min-height: 60px;
}
@@ -602,14 +555,14 @@
}
}
-.multi-file-additions,
-.multi-file-additions-solid {
- fill: $green-500;
+.multi-file-addition,
+.multi-file-addition-solid {
+ color: $green-500;
}
.multi-file-modified,
.multi-file-modified-solid {
- fill: $orange-500;
+ color: $orange-500;
}
.multi-file-commit-list-collapsed {
@@ -665,12 +618,24 @@
}
.multi-file-commit-form {
+ position: relative;
padding: $gl-padding;
+ background-color: $white-light;
border-top: 1px solid $white-dark;
+ border-left: 1px solid $white-dark;
+ transition: all 0.3s ease;
.btn {
font-size: $gl-font-size;
}
+
+ .multi-file-commit-panel-success-message {
+ top: 0;
+ }
+}
+
+.multi-file-commit-panel-bottom {
+ position: relative;
}
.dirty-diff {
@@ -806,7 +771,7 @@
position: absolute;
top: 0;
bottom: 0;
- width: 3px;
+ width: 1px;
background-color: $white-dark;
&.dragright {
@@ -820,42 +785,40 @@
.ide-commit-list-container {
display: flex;
+ flex: 1;
flex-direction: column;
width: 100%;
- padding: 0 16px;
-
- &:not(.is-collapsed) {
- flex: 1;
- min-height: 140px;
- }
-
- &.is-collapsed {
- .multi-file-commit-panel-header {
- margin-left: -$gl-padding;
- margin-right: -$gl-padding;
-
- svg {
- margin-left: auto;
- margin-right: auto;
- }
+ min-height: 140px;
- .multi-file-commit-panel-collapse-btn {
- margin-right: auto;
- margin-left: auto;
- border-left: 0;
- }
- }
+ &.is-first {
+ border-bottom: 1px solid $white-dark;
}
}
.ide-staged-action-btn {
margin-left: auto;
- color: $gl-link-color;
+ line-height: 22px;
+}
+
+.ide-commit-file-count {
+ min-width: 22px;
+ margin-left: auto;
+ background-color: $gray-light;
+ border-radius: $border-radius-default;
+ border: 1px solid $white-dark;
+ line-height: 20px;
+ text-align: center;
}
.ide-commit-radios {
label {
font-weight: normal;
+
+ &.is-disabled {
+ .ide-radio-label {
+ text-decoration: line-through;
+ }
+ }
}
.form-text.text-muted {
@@ -868,17 +831,58 @@
margin-left: 25px;
}
-.ide-external-links {
- p {
- margin: 0;
- }
-}
-
.ide-sidebar-link {
- padding: $gl-padding-8 $gl-padding;
display: flex;
align-items: center;
- font-weight: $gl-font-weight-bold;
+ position: relative;
+ height: 60px;
+ width: 100%;
+ padding: 0 $gl-padding;
+ color: $gl-text-color-secondary;
+ background-color: transparent;
+ border: 0;
+ border-top: 1px solid transparent;
+ border-bottom: 1px solid transparent;
+ outline: 0;
+
+ svg {
+ margin: 0 auto;
+ }
+
+ &:hover {
+ color: $gl-text-color;
+ background-color: $theme-gray-100;
+ }
+
+ &:focus {
+ color: $gl-text-color;
+ background-color: $theme-gray-200;
+ }
+
+ &.active {
+ // extend width over border of sidebar section
+ width: calc(100% + 1px);
+ padding-right: $gl-padding + 1px;
+ background-color: $white-light;
+ border-top-color: $white-dark;
+ border-bottom-color: $white-dark;
+
+ &::after {
+ content: '';
+ position: absolute;
+ right: -1px;
+ top: 0;
+ bottom: 0;
+ width: 1px;
+ background: $white-light;
+ }
+ }
+}
+
+.ide-activity-bar {
+ position: relative;
+ flex: 0 0 60px;
+ z-index: 1;
}
.ide-file-finder-overlay {
@@ -972,6 +976,120 @@
resize: none;
}
+.ide-tree-header {
+ display: flex;
+ align-items: center;
+ padding: 10px 0;
+ margin-left: 10px;
+ margin-right: 10px;
+ border-bottom: 1px solid $white-dark;
+
+ .ide-new-btn {
+ margin-left: auto;
+ }
+}
+
+.ide-sidebar-branch-title {
+ font-weight: $gl-font-weight-normal;
+
+ svg {
+ position: relative;
+ top: 3px;
+ margin-top: -1px;
+ }
+}
+
+.commit-form-compact {
+ .btn {
+ margin-bottom: 8px;
+ }
+
+ p {
+ margin-bottom: 0;
+ }
+}
+
+.commit-form-slide-up-enter-active,
+.commit-form-slide-up-leave-active {
+ position: absolute;
+ top: 16px;
+ left: 16px;
+ right: 16px;
+ transition: all 0.3s ease;
+}
+
+.is-full .commit-form-slide-up-enter,
+.is-compact .commit-form-slide-up-leave-to {
+ transform: translateY(100%);
+}
+
+.is-full .commit-form-slide-up-enter-to,
+.is-compact .commit-form-slide-up-leave {
+ transform: translateY(0);
+}
+
+.commit-form-slide-up-enter,
+.commit-form-slide-up-leave-to {
+ opacity: 0;
+}
+
+.ide-review-header {
+ flex-direction: column;
+ align-items: flex-start;
+
+ .dropdown {
+ margin-left: auto;
+ }
+
+ a {
+ color: $gl-link-color;
+ }
+}
+
+.ide-review-sub-header {
+ color: $gl-text-color-secondary;
+}
+
+.ide-tree-changes {
+ display: flex;
+ align-items: center;
+ font-size: 12px;
+}
+
.ide-new-modal-label {
line-height: 34px;
}
+
+.multi-file-commit-panel-success-message {
+ position: absolute;
+ top: 61px;
+ left: 1px;
+ bottom: 0;
+ right: 0;
+ z-index: 10;
+ background: $white-light;
+ overflow: auto;
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+}
+
+.ide-review-button-holder {
+ display: flex;
+ width: 100%;
+ align-items: center;
+}
+
+.ide-context-header {
+ .avatar {
+ flex: 0 0 40px;
+ }
+}
+
+.ide-sidebar-project-title {
+ min-width: 0;
+
+ .sidebar-context-title {
+ white-space: nowrap;
+ }
+}
diff --git a/app/assets/stylesheets/pages/repo.scss.orig b/app/assets/stylesheets/pages/repo.scss.orig
deleted file mode 100644
index 0bf29c7fe2f..00000000000
--- a/app/assets/stylesheets/pages/repo.scss.orig
+++ /dev/null
@@ -1,786 +0,0 @@
-.project-refs-form,
-.project-refs-target-form {
- display: inline-block;
-}
-
-.fade-enter,
-.fade-leave-to {
- opacity: 0;
-}
-
-.commit-message {
- @include str-truncated(250px);
-}
-
-.editable-mode {
- display: inline-block;
-}
-
-.ide-view {
- display: flex;
- height: calc(100vh - #{$header-height});
- margin-top: 40px;
- color: $almost-black;
- border-top: 1px solid $white-dark;
- border-bottom: 1px solid $white-dark;
-
- &.is-collapsed {
- .ide-file-list {
- max-width: 250px;
- }
- }
-
- .file-status-icon {
- width: 10px;
- height: 10px;
- }
-}
-
-.ide-file-list {
- flex: 1;
-
- .file {
- cursor: pointer;
-
- &.file-open {
- background: $white-normal;
- }
-
- .ide-file-name {
- flex: 1;
- white-space: nowrap;
- text-overflow: ellipsis;
-
- svg {
- vertical-align: middle;
- margin-right: 2px;
- }
-
- .loading-container {
- margin-right: 4px;
- display: inline-block;
- }
- }
-
- .ide-file-changed-icon {
- margin-left: auto;
- }
-
- .ide-new-btn {
- display: none;
- margin-bottom: -4px;
- margin-right: -8px;
- }
-
- &:hover {
- .ide-new-btn {
- display: block;
- }
- }
-
- &.folder {
- svg {
- fill: $gl-text-color-secondary;
- }
- }
- }
-
- a {
- color: $gl-text-color;
- }
-
- th {
- position: sticky;
- top: 0;
- }
-}
-
-.file-name,
-.file-col-commit-message {
- display: flex;
- overflow: visible;
- padding: 6px 12px;
-}
-
-.multi-file-loading-container {
- margin-top: 10px;
- padding: 10px;
-
- .animation-container {
- background: $gray-light;
-
- div {
- background: $gray-light;
- }
- }
-}
-
-.multi-file-table-col-commit-message {
- white-space: nowrap;
- width: 50%;
-}
-
-.multi-file-edit-pane {
- display: flex;
- flex-direction: column;
- flex: 1;
- border-left: 1px solid $white-dark;
- overflow: hidden;
-}
-
-.multi-file-tabs {
- display: flex;
- background-color: $white-normal;
- box-shadow: inset 0 -1px $white-dark;
-
- > ul {
- display: flex;
- overflow-x: auto;
- }
-
- li {
- position: relative;
- }
-
- .dropdown {
- display: flex;
- margin-left: auto;
- margin-bottom: 1px;
- padding: 0 $grid-size;
- border-left: 1px solid $white-dark;
- background-color: $white-light;
-
- &.shadow {
- box-shadow: 0 0 10px $dropdown-shadow-color;
- }
-
- .btn {
- margin-top: auto;
- margin-bottom: auto;
- }
- }
-}
-
-.multi-file-tab {
- @include str-truncated(150px);
- padding: ($gl-padding / 2) ($gl-padding + 12) ($gl-padding / 2) $gl-padding;
- background-color: $gray-normal;
- border-right: 1px solid $white-dark;
- border-bottom: 1px solid $white-dark;
- cursor: pointer;
-
- svg {
- vertical-align: middle;
- }
-
- &.active {
- background-color: $white-light;
- border-bottom-color: $white-light;
- }
-}
-
-.multi-file-tab-close {
- position: absolute;
- right: 8px;
- top: 50%;
- width: 16px;
- height: 16px;
- padding: 0;
- background: none;
- border: 0;
- border-radius: $border-radius-default;
- color: $theme-gray-900;
- transform: translateY(-50%);
-
- svg {
- position: relative;
- top: -1px;
- }
-
- &:hover {
- background-color: $theme-gray-200;
- }
-
- &:focus {
- background-color: $blue-500;
- color: $white-light;
- outline: 0;
-
- svg {
- fill: currentColor;
- }
- }
-}
-
-.multi-file-edit-pane-content {
- flex: 1;
- height: 0;
-}
-
-.blob-editor-container {
- flex: 1;
- height: 0;
- display: flex;
- flex-direction: column;
- justify-content: center;
-
- .vertical-center {
- min-height: auto;
- }
-
- .monaco-editor .lines-content .cigr {
- display: none;
- }
-
- .monaco-diff-editor.vs {
- .editor.modified {
- box-shadow: none;
- }
-
- .diagonal-fill {
- display: none !important;
- }
-
- .diffOverview {
- background-color: $white-light;
- border-left: 1px solid $white-dark;
- cursor: ns-resize;
- }
-
- .diffViewport {
- display: none;
- }
-
- .char-insert {
- background-color: $line-added-dark;
- }
-
- .char-delete {
- background-color: $line-removed-dark;
- }
-
- .line-numbers {
- color: $black-transparent;
- }
-
- .view-overlays {
- .line-insert {
- background-color: $line-added;
- }
-
- .line-delete {
- background-color: $line-removed;
- }
- }
-
- .margin {
- background-color: $gray-light;
- border-right: 1px solid $white-normal;
-
- .line-insert {
- border-right: 1px solid $line-added-dark;
- }
-
- .line-delete {
- border-right: 1px solid $line-removed-dark;
- }
- }
-
- .margin-view-overlays .insert-sign,
- .margin-view-overlays .delete-sign {
- opacity: 0.4;
- }
-
- .cursors-layer {
- display: none;
- }
- }
-}
-
-.multi-file-editor-holder {
- height: 100%;
-}
-
-.multi-file-editor-btn-group {
- padding: $gl-bar-padding $gl-padding;
- border-top: 1px solid $white-dark;
- border-bottom: 1px solid $white-dark;
- background: $white-light;
-}
-
-.ide-status-bar {
- padding: $gl-bar-padding $gl-padding;
- background: $white-light;
- display: flex;
- justify-content: space-between;
-
- svg {
- vertical-align: middle;
- }
-}
-
-// Not great, but this is to deal with our current output
-.multi-file-preview-holder {
- height: 100%;
- overflow: scroll;
-
- .file-content.code {
- display: flex;
-
- i {
- margin-left: -10px;
- }
- }
-
- .line-numbers {
- min-width: 50px;
- }
-
- .file-content,
- .line-numbers,
- .blob-content,
- .code {
- min-height: 100%;
- }
-}
-
-.file-content.blob-no-preview {
- a {
- margin-left: auto;
- margin-right: auto;
- }
-}
-
-.multi-file-commit-panel {
- display: flex;
- position: relative;
- flex-direction: column;
- width: 340px;
- padding: 0;
- background-color: $gray-light;
- padding-right: 3px;
-
- .projects-sidebar {
- display: flex;
- flex-direction: column;
-
- .context-header {
- width: auto;
- margin-right: 0;
- }
- }
-
- .multi-file-commit-panel-inner {
- display: flex;
- flex: 1;
- flex-direction: column;
- }
-
- .multi-file-commit-panel-inner-scroll {
- display: flex;
- flex: 1;
- flex-direction: column;
- overflow: auto;
- }
-
- &.is-collapsed {
- width: 60px;
-
- .multi-file-commit-list {
- padding-top: $gl-padding;
- overflow: hidden;
- }
-
- .multi-file-context-bar-icon {
- align-items: center;
-
- svg {
- float: none;
- margin: 0;
- }
- }
- }
-
- .branch-container {
- border-left: 4px solid $indigo-700;
- margin-bottom: $gl-bar-padding;
- }
-
- .branch-header {
- background: $white-dark;
- display: flex;
- }
-
- .branch-header-title {
- flex: 1;
- padding: $grid-size $gl-padding;
- color: $indigo-700;
- font-weight: $gl-font-weight-bold;
-
- svg {
- vertical-align: middle;
- }
- }
-
- .branch-header-btns {
- padding: $gl-vert-padding $gl-padding;
- }
-
- .left-collapse-btn {
- display: none;
- background: $gray-light;
- text-align: left;
- border-top: 1px solid $white-dark;
-
- svg {
- vertical-align: middle;
- }
- }
-}
-
-.multi-file-context-bar-icon {
- padding: 10px;
-
- svg {
- margin-right: 10px;
- float: left;
- }
-}
-
-.multi-file-commit-panel-section {
- display: flex;
- flex-direction: column;
- flex: 1;
-}
-
-.multi-file-commit-empty-state-container {
- align-items: center;
- justify-content: center;
-}
-
-.multi-file-commit-panel-header {
- display: flex;
- align-items: center;
- margin-bottom: 12px;
- border-bottom: 1px solid $white-dark;
- padding: $gl-btn-padding 0;
-
- &.is-collapsed {
- border-bottom: 1px solid $white-dark;
-
- svg {
- margin-left: auto;
- margin-right: auto;
- }
-
- .multi-file-commit-panel-collapse-btn {
- margin-right: auto;
- margin-left: auto;
- border-left: 0;
- }
- }
-}
-
-.multi-file-commit-panel-header-title {
- display: flex;
- flex: 1;
- padding: 0 $gl-btn-padding;
-
- svg {
- margin-right: $gl-btn-padding;
- }
-}
-
-.multi-file-commit-panel-collapse-btn {
- border-left: 1px solid $white-dark;
-}
-
-.multi-file-commit-list {
- flex: 1;
- overflow: auto;
- padding: $gl-padding 0;
- min-height: 60px;
-}
-
-.multi-file-commit-list-item {
- display: flex;
- padding: 0;
- align-items: center;
-
- .multi-file-discard-btn {
- display: none;
- margin-left: auto;
- color: $gl-link-color;
- padding: 0 2px;
-
- &:focus,
- &:hover {
- text-decoration: underline;
- }
- }
-
- &:hover {
- background: $white-normal;
-
- .multi-file-discard-btn {
- display: block;
- }
- }
-}
-
-.multi-file-addition {
- fill: $green-500;
-}
-
-.multi-file-modified {
- fill: $orange-500;
-}
-
-.multi-file-commit-list-collapsed {
- display: flex;
- flex-direction: column;
-
- > svg {
- margin-left: auto;
- margin-right: auto;
- }
-
- .file-status-icon {
- width: 10px;
- height: 10px;
- margin-left: 3px;
- }
-}
-
-.multi-file-commit-list-path {
- padding: $grid-size / 2;
- padding-left: $gl-padding;
- background: none;
- border: 0;
- text-align: left;
- width: 100%;
- min-width: 0;
-
- svg {
- min-width: 16px;
- vertical-align: middle;
- display: inline-block;
- }
-
- &:hover,
- &:focus {
- outline: 0;
- }
-}
-
-.multi-file-commit-list-file-path {
- @include str-truncated(100%);
-
- &:hover {
- text-decoration: underline;
- }
-
- &:active {
- text-decoration: none;
- }
-}
-
-.multi-file-commit-form {
- padding: $gl-padding;
- border-top: 1px solid $white-dark;
-
- .btn {
- font-size: $gl-font-size;
- }
-}
-
-.multi-file-commit-message.form-control {
- height: 160px;
- resize: none;
-}
-
-.dirty-diff {
- // !important need to override monaco inline style
- width: 4px !important;
- left: 0 !important;
-
- &-modified {
- background-color: $blue-500;
- }
-
- &-added {
- background-color: $green-600;
- }
-
- &-removed {
- height: 0 !important;
- width: 0 !important;
- bottom: -2px;
- border-style: solid;
- border-width: 5px;
- border-color: transparent transparent transparent $red-500;
-
- &::before {
- content: '';
- position: absolute;
- left: 0;
- top: 0;
- width: 100px;
- height: 1px;
- background-color: rgba($red-500, 0.5);
- }
- }
-}
-
-.ide-loading {
- display: flex;
- height: 100vh;
- align-items: center;
- justify-content: center;
-}
-
-.ide-empty-state {
- display: flex;
- height: 100vh;
- align-items: center;
- justify-content: center;
-}
-
-.ide-new-btn {
- .dropdown-toggle svg {
- margin-top: -2px;
- margin-bottom: 2px;
- }
-
- .dropdown-menu {
- left: auto;
- right: 0;
-
- label {
- font-weight: $gl-font-weight-normal;
- padding: 5px 8px;
- margin-bottom: 0;
- }
- }
-}
-
-.ide {
- overflow: hidden;
-
- &.nav-only {
- .flash-container {
- margin-top: $header-height;
- margin-bottom: 0;
- }
-
- .alert-wrapper .flash-container .flash-alert:last-child,
- .alert-wrapper .flash-container .flash-notice:last-child {
- margin-bottom: 0;
- }
-
- .content-wrapper {
- margin-top: $header-height;
- padding-bottom: 0;
- }
-
- &.flash-shown {
- .content-wrapper {
- margin-top: 0;
- }
-
- .ide-view {
- height: calc(100vh - #{$header-height + $flash-height});
- }
- }
-
- .projects-sidebar {
- .multi-file-commit-panel-inner-scroll {
- flex: 1;
- }
- }
- }
-}
-
-.with-performance-bar .ide.nav-only {
- .flash-container {
- margin-top: #{$header-height + $performance-bar-height};
- }
-
- .content-wrapper {
- margin-top: #{$header-height + $performance-bar-height};
- padding-bottom: 0;
- }
-
- .ide-view {
- height: calc(100vh - #{$header-height + $performance-bar-height});
- }
-
- &.flash-shown {
- .content-wrapper {
- margin-top: 0;
- }
-
- .ide-view {
- height: calc(
- 100vh - #{$header-height + $performance-bar-height + $flash-height}
- );
- }
- }
-}
-
-.dragHandle {
- position: absolute;
- top: 0;
- bottom: 0;
- width: 3px;
- background-color: $white-dark;
-
- &.dragright {
- right: 0;
- }
-
- &.dragleft {
- left: 0;
- }
-}
-
-.ide-commit-radios {
- label {
- font-weight: normal;
- }
-
- .form-text.text-muted {
- margin-top: 0;
- line-height: 0;
- }
-}
-
-.ide-commit-new-branch {
- margin-left: 25px;
-}
-
-.ide-external-links {
- p {
- margin: 0;
- }
-}
-
-.ide-sidebar-link {
- padding: $gl-padding-8 $gl-padding;
- background: $indigo-700;
- color: $white-light;
- text-decoration: none;
- display: flex;
- align-items: center;
-
- &:focus,
- &:hover {
- color: $white-light;
- text-decoration: underline;
- background: $indigo-500;
- }
-
- &:active {
- background: $indigo-800;
- }
-}
diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb
index 0fdd4d2cb47..2caffec66ac 100644
--- a/app/controllers/application_controller.rb
+++ b/app/controllers/application_controller.rb
@@ -13,12 +13,14 @@ class ApplicationController < ActionController::Base
before_action :authenticate_sessionless_user!
before_action :authenticate_user!
+ before_action :enforce_terms!, if: -> { Gitlab::CurrentSettings.current_application_settings.enforce_terms },
+ unless: :peek_request?
before_action :validate_user_service_ticket!
before_action :check_password_expiration
before_action :ldap_security_check
before_action :sentry_context
before_action :default_headers
- before_action :add_gon_variables, unless: -> { request.path.start_with?('/-/peek') }
+ before_action :add_gon_variables, unless: :peek_request?
before_action :configure_permitted_parameters, if: :devise_controller?
before_action :require_email, unless: :devise_controller?
@@ -110,7 +112,8 @@ class ApplicationController < ActionController::Base
def log_exception(exception)
Raven.capture_exception(exception) if sentry_enabled?
- application_trace = ActionDispatch::ExceptionWrapper.new(env, exception).application_trace
+ backtrace_cleaner = Gitlab.rails5? ? env["action_dispatch.backtrace_cleaner"] : env
+ application_trace = ActionDispatch::ExceptionWrapper.new(backtrace_cleaner, exception).application_trace
application_trace.map! { |t| " #{t}\n" }
logger.error "\n#{exception.class.name} (#{exception.message}):\n#{application_trace.join}"
end
@@ -268,6 +271,27 @@ class ApplicationController < ActionController::Base
end
end
+ def enforce_terms!
+ return unless current_user
+ return if current_user.terms_accepted?
+
+ if sessionless_user?
+ render_403
+ else
+ # Redirect to the destination if the request is a get.
+ # Redirect to the source if it was a post, so the user can re-submit after
+ # accepting the terms.
+ redirect_path = if request.get?
+ request.fullpath
+ else
+ URI(request.referer).path if request.referer
+ end
+
+ flash[:notice] = _("Please accept the Terms of Service before continuing.")
+ redirect_to terms_path(redirect: redirect_path), status: :found
+ end
+ end
+
def import_sources_enabled?
!Gitlab::CurrentSettings.import_sources.empty?
end
@@ -341,4 +365,12 @@ class ApplicationController < ActionController::Base
# Per https://tools.ietf.org/html/rfc5987, headers need to be ISO-8859-1, not UTF-8
response.headers['Page-Title'] = URI.escape(page_title('GitLab'))
end
+
+ def sessionless_user?
+ current_user && !session.keys.include?('warden.user.user.key')
+ end
+
+ def peek_request?
+ request.path.start_with?('/-/peek')
+ end
end
diff --git a/app/controllers/concerns/continue_params.rb b/app/controllers/concerns/continue_params.rb
index eb3a623acdd..8b7355974df 100644
--- a/app/controllers/concerns/continue_params.rb
+++ b/app/controllers/concerns/continue_params.rb
@@ -1,4 +1,5 @@
module ContinueParams
+ include InternalRedirect
extend ActiveSupport::Concern
def continue_params
@@ -6,8 +7,7 @@ module ContinueParams
return nil unless continue_params
continue_params = continue_params.permit(:to, :notice, :notice_now)
- return unless continue_params[:to] && continue_params[:to].start_with?('/')
- return if continue_params[:to].start_with?('//')
+ continue_params[:to] = safe_redirect_path(continue_params[:to])
continue_params
end
diff --git a/app/controllers/concerns/internal_redirect.rb b/app/controllers/concerns/internal_redirect.rb
new file mode 100644
index 00000000000..7409b2e89a5
--- /dev/null
+++ b/app/controllers/concerns/internal_redirect.rb
@@ -0,0 +1,35 @@
+module InternalRedirect
+ extend ActiveSupport::Concern
+
+ def safe_redirect_path(path)
+ return unless path
+ # Verify that the string starts with a `/` but not a double `/`.
+ return unless path =~ %r{^/\w.*$}
+
+ uri = URI(path)
+ # Ignore anything path of the redirect except for the path, querystring and,
+ # fragment, forcing the redirect within the same host.
+ full_path_for_uri(uri)
+ rescue URI::InvalidURIError
+ nil
+ end
+
+ def safe_redirect_path_for_url(url)
+ return unless url
+
+ uri = URI(url)
+ safe_redirect_path(full_path_for_uri(uri)) if host_allowed?(uri)
+ rescue URI::InvalidURIError
+ nil
+ end
+
+ def host_allowed?(uri)
+ uri.host == request.host &&
+ uri.port == request.port
+ end
+
+ def full_path_for_uri(uri)
+ path_with_query = [uri.path, uri.query].compact.join('?')
+ [path_with_query, uri.fragment].compact.join("#")
+ end
+end
diff --git a/app/controllers/concerns/issuable_actions.rb b/app/controllers/concerns/issuable_actions.rb
index 0379f76fc3d..c925b4aada5 100644
--- a/app/controllers/concerns/issuable_actions.rb
+++ b/app/controllers/concerns/issuable_actions.rb
@@ -18,7 +18,6 @@ module IssuableActions
def update
@issuable = update_service.execute(issuable) # rubocop:disable Gitlab/ModuleWithInstanceVariables
-
respond_to do |format|
format.html do
recaptcha_check_if_spammable { render :edit }
diff --git a/app/controllers/concerns/issuable_collections.rb b/app/controllers/concerns/issuable_collections.rb
index 34228cf0b82..ca1b80a36a0 100644
--- a/app/controllers/concerns/issuable_collections.rb
+++ b/app/controllers/concerns/issuable_collections.rb
@@ -57,7 +57,7 @@ module IssuableCollections
out_of_range = @issuables.current_page > total_pages # rubocop:disable Gitlab/ModuleWithInstanceVariables
if out_of_range
- redirect_to(url_for(params.merge(page: total_pages, only_path: true)))
+ redirect_to(url_for(safe_params.merge(page: total_pages, only_path: true)))
end
out_of_range
diff --git a/app/controllers/groups/application_controller.rb b/app/controllers/groups/application_controller.rb
index 9f3bb60b4cc..62213561898 100644
--- a/app/controllers/groups/application_controller.rb
+++ b/app/controllers/groups/application_controller.rb
@@ -33,6 +33,6 @@ class Groups::ApplicationController < ApplicationController
def build_canonical_path(group)
params[:group_id] = group.to_param
- url_for(params)
+ url_for(safe_params)
end
end
diff --git a/app/controllers/groups/runners_controller.rb b/app/controllers/groups/runners_controller.rb
new file mode 100644
index 00000000000..78992ec7f46
--- /dev/null
+++ b/app/controllers/groups/runners_controller.rb
@@ -0,0 +1,58 @@
+class Groups::RunnersController < Groups::ApplicationController
+ # Proper policies should be implemented per
+ # https://gitlab.com/gitlab-org/gitlab-ce/issues/45894
+ before_action :authorize_admin_pipeline!
+
+ before_action :runner, only: [:edit, :update, :destroy, :pause, :resume, :show]
+
+ def show
+ render 'shared/runners/show'
+ end
+
+ def edit
+ end
+
+ def update
+ if Ci::UpdateRunnerService.new(@runner).update(runner_params)
+ redirect_to group_runner_path(@group, @runner), notice: 'Runner was successfully updated.'
+ else
+ render 'edit'
+ end
+ end
+
+ def destroy
+ @runner.destroy
+
+ redirect_to group_settings_ci_cd_path(@group, anchor: 'runners-settings'), status: 302
+ end
+
+ def resume
+ if Ci::UpdateRunnerService.new(@runner).update(active: true)
+ redirect_to group_settings_ci_cd_path(@group, anchor: 'runners-settings'), notice: 'Runner was successfully updated.'
+ else
+ redirect_to group_settings_ci_cd_path(@group, anchor: 'runners-settings'), alert: 'Runner was not updated.'
+ end
+ end
+
+ def pause
+ if Ci::UpdateRunnerService.new(@runner).update(active: false)
+ redirect_to group_settings_ci_cd_path(@group, anchor: 'runners-settings'), notice: 'Runner was successfully updated.'
+ else
+ redirect_to group_settings_ci_cd_path(@group, anchor: 'runners-settings'), alert: 'Runner was not updated.'
+ end
+ end
+
+ private
+
+ def runner
+ @runner ||= @group.runners.find(params[:id])
+ end
+
+ def authorize_admin_pipeline!
+ return render_404 unless can?(current_user, :admin_pipeline, group)
+ end
+
+ def runner_params
+ params.require(:runner).permit(Ci::Runner::FORM_EDITABLE)
+ end
+end
diff --git a/app/controllers/import/base_controller.rb b/app/controllers/import/base_controller.rb
index c84fc2d305d..bcb856ce3f4 100644
--- a/app/controllers/import/base_controller.rb
+++ b/app/controllers/import/base_controller.rb
@@ -1,6 +1,17 @@
class Import::BaseController < ApplicationController
private
+ def find_already_added_projects(import_type)
+ current_user.created_projects.where(import_type: import_type).includes(:import_state)
+ end
+
+ def find_jobs(import_type)
+ current_user.created_projects
+ .includes(:import_state)
+ .where(import_type: import_type)
+ .to_json(only: [:id], methods: [:import_status])
+ end
+
def find_or_create_namespace(names, owner)
names = params[:target_namespace].presence || names
diff --git a/app/controllers/import/bitbucket_controller.rb b/app/controllers/import/bitbucket_controller.rb
index 61d81ad8a71..77af5fb9c4f 100644
--- a/app/controllers/import/bitbucket_controller.rb
+++ b/app/controllers/import/bitbucket_controller.rb
@@ -22,16 +22,14 @@ class Import::BitbucketController < Import::BaseController
@repos, @incompatible_repos = repos.partition { |repo| repo.valid? }
- @already_added_projects = current_user.created_projects.where(import_type: 'bitbucket')
+ @already_added_projects = find_already_added_projects('bitbucket')
already_added_projects_names = @already_added_projects.pluck(:import_source)
@repos.to_a.reject! { |repo| already_added_projects_names.include?(repo.full_name) }
end
def jobs
- render json: current_user.created_projects
- .where(import_type: 'bitbucket')
- .to_json(only: [:id, :import_status])
+ render json: find_jobs('bitbucket')
end
def create
diff --git a/app/controllers/import/fogbugz_controller.rb b/app/controllers/import/fogbugz_controller.rb
index 669eb31a995..25ec13b8075 100644
--- a/app/controllers/import/fogbugz_controller.rb
+++ b/app/controllers/import/fogbugz_controller.rb
@@ -46,15 +46,14 @@ class Import::FogbugzController < Import::BaseController
@repos = client.repos
- @already_added_projects = current_user.created_projects.where(import_type: 'fogbugz')
+ @already_added_projects = find_already_added_projects('fogbugz')
already_added_projects_names = @already_added_projects.pluck(:import_source)
@repos.reject! { |repo| already_added_projects_names.include? repo.name }
end
def jobs
- jobs = current_user.created_projects.where(import_type: 'fogbugz').to_json(only: [:id, :import_status])
- render json: jobs
+ render json: find_jobs('fogbugz')
end
def create
diff --git a/app/controllers/import/github_controller.rb b/app/controllers/import/github_controller.rb
index eb7d5fca367..f67ec4c248b 100644
--- a/app/controllers/import/github_controller.rb
+++ b/app/controllers/import/github_controller.rb
@@ -24,15 +24,14 @@ class Import::GithubController < Import::BaseController
def status
@repos = client.repos
- @already_added_projects = current_user.created_projects.where(import_type: provider)
+ @already_added_projects = find_already_added_projects(provider)
already_added_projects_names = @already_added_projects.pluck(:import_source)
@repos.reject! { |repo| already_added_projects_names.include? repo.full_name }
end
def jobs
- jobs = current_user.created_projects.where(import_type: provider).to_json(only: [:id, :import_status])
- render json: jobs
+ render json: find_jobs(provider)
end
def create
diff --git a/app/controllers/import/gitlab_controller.rb b/app/controllers/import/gitlab_controller.rb
index 18f1d20f5a9..39e2e9e094b 100644
--- a/app/controllers/import/gitlab_controller.rb
+++ b/app/controllers/import/gitlab_controller.rb
@@ -12,15 +12,14 @@ class Import::GitlabController < Import::BaseController
def status
@repos = client.projects
- @already_added_projects = current_user.created_projects.where(import_type: "gitlab")
+ @already_added_projects = find_already_added_projects('gitlab')
already_added_projects_names = @already_added_projects.pluck(:import_source)
@repos = @repos.to_a.reject { |repo| already_added_projects_names.include? repo["path_with_namespace"] }
end
def jobs
- jobs = current_user.created_projects.where(import_type: "gitlab").to_json(only: [:id, :import_status])
- render json: jobs
+ render json: find_jobs('gitlab')
end
def create
diff --git a/app/controllers/import/google_code_controller.rb b/app/controllers/import/google_code_controller.rb
index baa19fb383d..9b26a00f7c7 100644
--- a/app/controllers/import/google_code_controller.rb
+++ b/app/controllers/import/google_code_controller.rb
@@ -73,15 +73,14 @@ class Import::GoogleCodeController < Import::BaseController
@repos = client.repos
@incompatible_repos = client.incompatible_repos
- @already_added_projects = current_user.created_projects.where(import_type: "google_code")
+ @already_added_projects = find_already_added_projects('google_code')
already_added_projects_names = @already_added_projects.pluck(:import_source)
@repos.reject! { |repo| already_added_projects_names.include? repo.name }
end
def jobs
- jobs = current_user.created_projects.where(import_type: "google_code").to_json(only: [:id, :import_status])
- render json: jobs
+ render json: find_jobs('google_code')
end
def create
diff --git a/app/controllers/omniauth_callbacks_controller.rb b/app/controllers/omniauth_callbacks_controller.rb
index 40d9fa18a10..ed89bed029b 100644
--- a/app/controllers/omniauth_callbacks_controller.rb
+++ b/app/controllers/omniauth_callbacks_controller.rb
@@ -82,7 +82,7 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController
if identity_linker.changed?
redirect_identity_linked
- elsif identity_linker.error_message.present?
+ elsif identity_linker.failed?
redirect_identity_link_failed(identity_linker.error_message)
else
redirect_identity_exists
diff --git a/app/controllers/profiles/active_sessions_controller.rb b/app/controllers/profiles/active_sessions_controller.rb
new file mode 100644
index 00000000000..f0cdc228366
--- /dev/null
+++ b/app/controllers/profiles/active_sessions_controller.rb
@@ -0,0 +1,14 @@
+class Profiles::ActiveSessionsController < Profiles::ApplicationController
+ def index
+ @sessions = ActiveSession.list(current_user)
+ end
+
+ def destroy
+ ActiveSession.destroy(current_user, params[:id])
+
+ respond_to do |format|
+ format.html { redirect_to profile_active_sessions_url, status: 302 }
+ format.js { head :ok }
+ end
+ end
+end
diff --git a/app/controllers/projects/application_controller.rb b/app/controllers/projects/application_controller.rb
index 032bb2267e7..5ab6d103c89 100644
--- a/app/controllers/projects/application_controller.rb
+++ b/app/controllers/projects/application_controller.rb
@@ -25,7 +25,7 @@ class Projects::ApplicationController < ApplicationController
params[:namespace_id] = project.namespace.to_param
params[:project_id] = project.to_param
- url_for(params)
+ url_for(safe_params)
end
def repository
diff --git a/app/controllers/projects/compare_controller.rb b/app/controllers/projects/compare_controller.rb
index 2b0c2ca97c0..f93e500a07a 100644
--- a/app/controllers/projects/compare_controller.rb
+++ b/app/controllers/projects/compare_controller.rb
@@ -8,8 +8,11 @@ class Projects::CompareController < Projects::ApplicationController
# Authorize
before_action :require_non_empty_project
before_action :authorize_download_code!
- before_action :define_ref_vars, only: [:index, :show, :diff_for_path]
- before_action :define_diff_vars, only: [:show, :diff_for_path]
+ # Defining ivars
+ before_action :define_diffs, only: [:show, :diff_for_path]
+ before_action :define_environment, only: [:show]
+ before_action :define_diff_notes_disabled, only: [:show, :diff_for_path]
+ before_action :define_commits, only: [:show, :diff_for_path, :signatures]
before_action :merge_request, only: [:index, :show]
def index
@@ -22,9 +25,9 @@ class Projects::CompareController < Projects::ApplicationController
end
def diff_for_path
- return render_404 unless @compare
+ return render_404 unless compare
- render_diff_for_path(@compare.diffs(diff_options))
+ render_diff_for_path(compare.diffs(diff_options))
end
def create
@@ -41,30 +44,60 @@ class Projects::CompareController < Projects::ApplicationController
end
end
+ def signatures
+ respond_to do |format|
+ format.json do
+ render json: {
+ signatures: @commits.select(&:has_signature?).map do |commit|
+ {
+ commit_sha: commit.sha,
+ html: view_to_html_string('projects/commit/_signature', signature: commit.signature)
+ }
+ end
+ }
+ end
+ end
+ end
+
private
- def define_ref_vars
- @start_ref = Addressable::URI.unescape(params[:from])
+ def compare
+ return @compare if defined?(@compare)
+
+ @compare = CompareService.new(@project, head_ref).execute(@project, start_ref)
+ end
+
+ def start_ref
+ @start_ref ||= Addressable::URI.unescape(params[:from])
+ end
+
+ def head_ref
+ return @ref if defined?(@ref)
+
@ref = @head_ref = Addressable::URI.unescape(params[:to])
end
- def define_diff_vars
- @compare = CompareService.new(@project, @head_ref)
- .execute(@project, @start_ref)
+ def define_commits
+ @commits = compare.present? ? prepare_commits_for_rendering(compare.commits) : []
+ end
- if @compare
- @commits = prepare_commits_for_rendering(@compare.commits)
- @diffs = @compare.diffs(diff_options)
+ def define_diffs
+ @diffs = compare.present? ? compare.diffs(diff_options) : []
+ end
- environment_params = @repository.branch_exists?(@head_ref) ? { ref: @head_ref } : { commit: @compare.commit }
+ def define_environment
+ if compare
+ environment_params = @repository.branch_exists?(head_ref) ? { ref: head_ref } : { commit: compare.commit }
@environment = EnvironmentsFinder.new(@project, current_user, environment_params).execute.last
-
- @diff_notes_disabled = true
end
end
+ def define_diff_notes_disabled
+ @diff_notes_disabled = compare.present?
+ end
+
def merge_request
@merge_request ||= MergeRequestsFinder.new(current_user, project_id: @project.id).execute.opened
- .find_by(source_project: @project, source_branch: @head_ref, target_branch: @start_ref)
+ .find_by(source_project: @project, source_branch: head_ref, target_branch: start_ref)
end
end
diff --git a/app/controllers/projects/lfs_storage_controller.rb b/app/controllers/projects/lfs_storage_controller.rb
index ebde0df1f7b..43d8867a536 100644
--- a/app/controllers/projects/lfs_storage_controller.rb
+++ b/app/controllers/projects/lfs_storage_controller.rb
@@ -77,8 +77,7 @@ class Projects::LfsStorageController < Projects::GitHttpClientController
def link_to_project!(object)
if object && !object.projects.exists?(storage_project.id)
- object.projects << storage_project
- object.save!
+ object.lfs_objects_projects.create!(project: storage_project)
end
end
end
diff --git a/app/controllers/projects/merge_requests/creations_controller.rb b/app/controllers/projects/merge_requests/creations_controller.rb
index 4a377fefc62..81129456ad8 100644
--- a/app/controllers/projects/merge_requests/creations_controller.rb
+++ b/app/controllers/projects/merge_requests/creations_controller.rb
@@ -83,13 +83,6 @@ class Projects::MergeRequests::CreationsController < Projects::MergeRequests::Ap
render layout: false
end
- def update_branches
- @target_project = selected_target_project
- @target_branches = @target_project ? @target_project.repository.branch_names : []
-
- render layout: false
- end
-
private
def build_merge_request
diff --git a/app/controllers/projects/mirrors_controller.rb b/app/controllers/projects/mirrors_controller.rb
new file mode 100644
index 00000000000..5698ff4e706
--- /dev/null
+++ b/app/controllers/projects/mirrors_controller.rb
@@ -0,0 +1,67 @@
+class Projects::MirrorsController < Projects::ApplicationController
+ include RepositorySettingsRedirect
+
+ # Authorize
+ before_action :remote_mirror, only: [:update]
+ before_action :check_mirror_available!
+ before_action :authorize_admin_project!
+
+ layout "project_settings"
+
+ def show
+ redirect_to_repository_settings(project)
+ end
+
+ def update
+ if project.update_attributes(mirror_params)
+ flash[:notice] = 'Mirroring settings were successfully updated.'
+ else
+ flash[:alert] = project.errors.full_messages.join(', ').html_safe
+ end
+
+ respond_to do |format|
+ format.html { redirect_to_repository_settings(project) }
+ format.json do
+ if project.errors.present?
+ render json: project.errors, status: :unprocessable_entity
+ else
+ render json: ProjectMirrorSerializer.new.represent(project)
+ end
+ end
+ end
+ end
+
+ def update_now
+ if params[:sync_remote]
+ project.update_remote_mirrors
+ flash[:notice] = "The remote repository is being updated..."
+ end
+
+ redirect_to_repository_settings(project)
+ end
+
+ private
+
+ def remote_mirror
+ @remote_mirror = project.remote_mirrors.first_or_initialize
+ end
+
+ def check_mirror_available!
+ Gitlab::CurrentSettings.current_application_settings.mirror_available || current_user&.admin?
+ end
+
+ def mirror_params_attributes
+ [
+ remote_mirrors_attributes: %i[
+ url
+ id
+ enabled
+ only_protected_branches
+ ]
+ ]
+ end
+
+ def mirror_params
+ params.require(:project).permit(mirror_params_attributes)
+ end
+end
diff --git a/app/controllers/projects/pipelines_controller.rb b/app/controllers/projects/pipelines_controller.rb
index 78d109cf33e..0b1b46944aa 100644
--- a/app/controllers/projects/pipelines_controller.rb
+++ b/app/controllers/projects/pipelines_controller.rb
@@ -87,7 +87,7 @@ class Projects::PipelinesController < Projects::ApplicationController
end
def failures
- if @pipeline.statuses.latest.failed.present?
+ if @pipeline.failed_builds.present?
render_show
else
redirect_to pipeline_path(@pipeline)
@@ -104,9 +104,18 @@ class Projects::PipelinesController < Projects::ApplicationController
@stage = pipeline.legacy_stage(params[:stage])
return not_found unless @stage
- respond_to do |format|
- format.json { render json: { html: view_to_html_string('projects/pipelines/_stage') } }
- end
+ render json: StageSerializer
+ .new(project: @project, current_user: @current_user)
+ .represent(@stage, details: true)
+ end
+
+ # TODO: This endpoint is used by mini-pipeline-graph
+ # TODO: This endpoint should be migrated to `stage.json`
+ def stage_ajax
+ @stage = pipeline.legacy_stage(params[:stage])
+ return not_found unless @stage
+
+ render json: { html: view_to_html_string('projects/pipelines/_stage') }
end
def retry
@@ -157,7 +166,7 @@ class Projects::PipelinesController < Projects::ApplicationController
end
def create_params
- params.require(:pipeline).permit(:ref)
+ params.require(:pipeline).permit(:ref, variables_attributes: %i[key secret_value])
end
def pipeline
diff --git a/app/controllers/projects/runner_projects_controller.rb b/app/controllers/projects/runner_projects_controller.rb
index 3cb01405b05..0ec2490655f 100644
--- a/app/controllers/projects/runner_projects_controller.rb
+++ b/app/controllers/projects/runner_projects_controller.rb
@@ -8,7 +8,7 @@ class Projects::RunnerProjectsController < Projects::ApplicationController
return head(403) unless can?(current_user, :assign_runner, @runner)
- path = runners_path(project)
+ path = project_runners_path(project)
runner_project = @runner.assign_to(project, current_user)
if runner_project.persisted?
@@ -22,6 +22,6 @@ class Projects::RunnerProjectsController < Projects::ApplicationController
runner_project = project.runner_projects.find(params[:id])
runner_project.destroy
- redirect_to runners_path(project), status: 302
+ redirect_to project_runners_path(project), status: 302
end
end
diff --git a/app/controllers/projects/runners_controller.rb b/app/controllers/projects/runners_controller.rb
index c950d0f7001..bef94cea989 100644
--- a/app/controllers/projects/runners_controller.rb
+++ b/app/controllers/projects/runners_controller.rb
@@ -1,6 +1,6 @@
class Projects::RunnersController < Projects::ApplicationController
before_action :authorize_admin_build!
- before_action :set_runner, only: [:edit, :update, :destroy, :pause, :resume, :show]
+ before_action :runner, only: [:edit, :update, :destroy, :pause, :resume, :show]
layout 'project_settings'
@@ -13,7 +13,7 @@ class Projects::RunnersController < Projects::ApplicationController
def update
if Ci::UpdateRunnerService.new(@runner).update(runner_params)
- redirect_to runner_path(@runner), notice: 'Runner was successfully updated.'
+ redirect_to project_runner_path(@project, @runner), notice: 'Runner was successfully updated.'
else
render 'edit'
end
@@ -24,26 +24,27 @@ class Projects::RunnersController < Projects::ApplicationController
@runner.destroy
end
- redirect_to runners_path(@project), status: 302
+ redirect_to project_runners_path(@project), status: 302
end
def resume
if Ci::UpdateRunnerService.new(@runner).update(active: true)
- redirect_to runners_path(@project), notice: 'Runner was successfully updated.'
+ redirect_to project_runners_path(@project), notice: 'Runner was successfully updated.'
else
- redirect_to runners_path(@project), alert: 'Runner was not updated.'
+ redirect_to project_runners_path(@project), alert: 'Runner was not updated.'
end
end
def pause
if Ci::UpdateRunnerService.new(@runner).update(active: false)
- redirect_to runners_path(@project), notice: 'Runner was successfully updated.'
+ redirect_to project_runners_path(@project), notice: 'Runner was successfully updated.'
else
- redirect_to runners_path(@project), alert: 'Runner was not updated.'
+ redirect_to project_runners_path(@project), alert: 'Runner was not updated.'
end
end
def show
+ render 'shared/runners/show'
end
def toggle_shared_runners
@@ -52,9 +53,15 @@ class Projects::RunnersController < Projects::ApplicationController
redirect_to project_settings_ci_cd_path(@project)
end
+ def toggle_group_runners
+ project.toggle_ci_cd_settings!(:group_runners_enabled)
+
+ redirect_to project_settings_ci_cd_path(@project)
+ end
+
protected
- def set_runner
+ def runner
@runner ||= project.runners.find(params[:id])
end
diff --git a/app/controllers/projects/settings/ci_cd_controller.rb b/app/controllers/projects/settings/ci_cd_controller.rb
index d80ef8113aa..177c8a54099 100644
--- a/app/controllers/projects/settings/ci_cd_controller.rb
+++ b/app/controllers/projects/settings/ci_cd_controller.rb
@@ -67,10 +67,18 @@ module Projects
def define_runners_variables
@project_runners = @project.runners.ordered
- @assignable_runners = current_user.ci_authorized_runners
- .assignable_for(project).ordered.page(params[:page]).per(20)
+
+ @assignable_runners = current_user
+ .ci_authorized_runners
+ .assignable_for(project)
+ .ordered
+ .page(params[:page]).per(20)
+
@shared_runners = ::Ci::Runner.shared.active
+
@shared_runners_count = @shared_runners.count(:all)
+
+ @group_runners = ::Ci::Runner.belonging_to_parent_group_of_project(@project.id)
end
def define_secret_variables
diff --git a/app/controllers/projects/settings/repository_controller.rb b/app/controllers/projects/settings/repository_controller.rb
index f17056f13e0..4697af4f26a 100644
--- a/app/controllers/projects/settings/repository_controller.rb
+++ b/app/controllers/projects/settings/repository_controller.rb
@@ -2,6 +2,7 @@ module Projects
module Settings
class RepositoryController < Projects::ApplicationController
before_action :authorize_admin_project!
+ before_action :remote_mirror, only: [:show]
def show
render_show
@@ -25,6 +26,7 @@ module Projects
define_deploy_token
define_protected_refs
+ remote_mirror
render 'show'
end
@@ -41,6 +43,10 @@ module Projects
load_gon_index
end
+ def remote_mirror
+ @remote_mirror = project.remote_mirrors.first_or_initialize
+ end
+
def access_levels_options
{
create_access_levels: levels_for_dropdown,
diff --git a/app/controllers/sent_notifications_controller.rb b/app/controllers/sent_notifications_controller.rb
index 04c36b3ebfe..93a71103a09 100644
--- a/app/controllers/sent_notifications_controller.rb
+++ b/app/controllers/sent_notifications_controller.rb
@@ -17,16 +17,20 @@ class SentNotificationsController < ApplicationController
flash[:notice] = "You have been unsubscribed from this thread."
if current_user
- case noteable
- when Issue
- redirect_to issue_path(noteable)
- when MergeRequest
- redirect_to merge_request_path(noteable)
- else
- redirect_to root_path
- end
+ redirect_to noteable_path(noteable)
else
redirect_to new_user_session_path
end
end
+
+ def noteable_path(noteable)
+ case noteable
+ when Issue
+ issue_path(noteable)
+ when MergeRequest
+ merge_request_path(noteable)
+ else
+ root_path
+ end
+ end
end
diff --git a/app/controllers/sessions_controller.rb b/app/controllers/sessions_controller.rb
index f3a4aa849c7..1a339f76d26 100644
--- a/app/controllers/sessions_controller.rb
+++ b/app/controllers/sessions_controller.rb
@@ -1,4 +1,5 @@
class SessionsController < Devise::SessionsController
+ include InternalRedirect
include AuthenticatesWithTwoFactor
include Devise::Controllers::Rememberable
include Recaptcha::ClientHelper
@@ -102,18 +103,12 @@ class SessionsController < Devise::SessionsController
# we should never redirect to '/users/sign_in' after signing in successfully.
return true if redirect_uri.path == new_user_session_path
- redirect_to = redirect_uri.to_s if redirect_allowed_to?(redirect_uri)
+ redirect_to = redirect_uri.to_s if host_allowed?(redirect_uri)
@redirect_to = redirect_to
store_location_for(:redirect, redirect_to)
end
- # Overridden in EE
- def redirect_allowed_to?(uri)
- uri.host == Gitlab.config.gitlab.host &&
- uri.port == Gitlab.config.gitlab.port
- end
-
def two_factor_enabled?
find_user&.two_factor_enabled?
end
diff --git a/app/controllers/users/terms_controller.rb b/app/controllers/users/terms_controller.rb
new file mode 100644
index 00000000000..95c5c3432d5
--- /dev/null
+++ b/app/controllers/users/terms_controller.rb
@@ -0,0 +1,66 @@
+module Users
+ class TermsController < ApplicationController
+ include InternalRedirect
+
+ skip_before_action :enforce_terms!
+ before_action :terms
+
+ layout 'terms'
+
+ def index
+ @redirect = redirect_path
+ end
+
+ def accept
+ agreement = Users::RespondToTermsService.new(current_user, viewed_term)
+ .execute(accepted: true)
+
+ if agreement.persisted?
+ redirect_to redirect_path
+ else
+ flash[:alert] = agreement.errors.full_messages.join(', ')
+ redirect_to terms_path, redirect: redirect_path
+ end
+ end
+
+ def decline
+ agreement = Users::RespondToTermsService.new(current_user, viewed_term)
+ .execute(accepted: false)
+
+ if agreement.persisted?
+ sign_out(current_user)
+ redirect_to root_path
+ else
+ flash[:alert] = agreement.errors.full_messages.join(', ')
+ redirect_to terms_path, redirect: redirect_path
+ end
+ end
+
+ private
+
+ def viewed_term
+ @viewed_term ||= ApplicationSetting::Term.find(params[:id])
+ end
+
+ def terms
+ unless @term = Gitlab::CurrentSettings.current_application_settings.latest_terms
+ redirect_to redirect_path
+ end
+ end
+
+ def redirect_path
+ redirect_to_path = safe_redirect_path(params[:redirect]) || safe_redirect_path_for_url(request.referer)
+
+ if redirect_to_path &&
+ excluded_redirect_paths.none? { |excluded| redirect_to_path.include?(excluded) }
+ redirect_to_path
+ else
+ root_path
+ end
+ end
+
+ def excluded_redirect_paths
+ [terms_path, new_user_session_path]
+ end
+ end
+end
diff --git a/app/helpers/active_sessions_helper.rb b/app/helpers/active_sessions_helper.rb
new file mode 100644
index 00000000000..97b6dac67c5
--- /dev/null
+++ b/app/helpers/active_sessions_helper.rb
@@ -0,0 +1,23 @@
+module ActiveSessionsHelper
+ # Maps a device type as defined in `ActiveSession` to an svg icon name and
+ # outputs the icon html.
+ #
+ # see `DeviceDetector::Device::DEVICE_NAMES` about the available device types
+ def active_session_device_type_icon(active_session)
+ icon_name =
+ case active_session.device_type
+ when 'smartphone', 'feature phone', 'phablet'
+ 'mobile'
+ when 'tablet'
+ 'tablet'
+ when 'tv', 'smart display', 'camera', 'portable media player', 'console'
+ 'media'
+ when 'car browser'
+ 'car'
+ else
+ 'monitor-o'
+ end
+
+ sprite_icon(icon_name, size: 16, css_class: 'prepend-top-2')
+ end
+end
diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb
index 6aa307b4db4..aa4569500b8 100644
--- a/app/helpers/application_helper.rb
+++ b/app/helpers/application_helper.rb
@@ -258,4 +258,17 @@ module ApplicationHelper
_('You are on a read-only GitLab instance.')
end
+
+ def autocomplete_data_sources(object, noteable_type)
+ return {} unless object && noteable_type
+
+ {
+ members: members_project_autocomplete_sources_path(object, type: noteable_type, type_id: params[:id]),
+ issues: issues_project_autocomplete_sources_path(object),
+ merge_requests: merge_requests_project_autocomplete_sources_path(object),
+ labels: labels_project_autocomplete_sources_path(object, type: noteable_type, type_id: params[:id]),
+ milestones: milestones_project_autocomplete_sources_path(object),
+ commands: commands_project_autocomplete_sources_path(object, type: noteable_type, type_id: params[:id])
+ }
+ end
end
diff --git a/app/helpers/application_settings_helper.rb b/app/helpers/application_settings_helper.rb
index 3fbb32c5229..b948e431882 100644
--- a/app/helpers/application_settings_helper.rb
+++ b/app/helpers/application_settings_helper.rb
@@ -248,7 +248,10 @@ module ApplicationSettingsHelper
:user_default_external,
:user_oauth_applications,
:version_check_enabled,
- :allow_local_requests_from_hooks_and_services
+ :allow_local_requests_from_hooks_and_services,
+ :enforce_terms,
+ :terms,
+ :mirror_available
]
end
end
diff --git a/app/helpers/clusters_helper.rb b/app/helpers/clusters_helper.rb
index 7e4eb06b99d..c24d340d184 100644
--- a/app/helpers/clusters_helper.rb
+++ b/app/helpers/clusters_helper.rb
@@ -2,4 +2,12 @@ module ClustersHelper
def has_multiple_clusters?(project)
false
end
+
+ def render_gcp_signup_offer
+ return unless show_gcp_signup_offer?
+
+ content_tag :section, class: 'no-animate expanded' do
+ render 'projects/clusters/gcp_signup_offer_banner'
+ end
+ end
end
diff --git a/app/helpers/gitlab_routing_helper.rb b/app/helpers/gitlab_routing_helper.rb
index 40073f714ee..61e12b0f31e 100644
--- a/app/helpers/gitlab_routing_helper.rb
+++ b/app/helpers/gitlab_routing_helper.rb
@@ -19,14 +19,6 @@ module GitlabRoutingHelper
project_commits_path(project, ref_name, *args)
end
- def runners_path(project, *args)
- project_runners_path(project, *args)
- end
-
- def runner_path(runner, *args)
- project_runner_path(@project, runner, *args)
- end
-
def environment_path(environment, *args)
project_environment_path(environment.project, environment, *args)
end
diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb
index 801e624e1de..eb81dc2de43 100644
--- a/app/helpers/projects_helper.rb
+++ b/app/helpers/projects_helper.rb
@@ -442,7 +442,7 @@ module ProjectsHelper
visibilityHelpPath: help_page_path('public_access/public_access'),
registryAvailable: Gitlab.config.registry.enabled,
registryHelpPath: help_page_path('user/project/container_registry'),
- lfsAvailable: Gitlab.config.lfs.enabled && current_user.admin?,
+ lfsAvailable: Gitlab.config.lfs.enabled,
lfsHelpPath: help_page_path('workflow/lfs/manage_large_binaries_with_git_lfs')
}
diff --git a/app/helpers/user_callouts_helper.rb b/app/helpers/user_callouts_helper.rb
index 36abfaf19a5..da5fe25c07d 100644
--- a/app/helpers/user_callouts_helper.rb
+++ b/app/helpers/user_callouts_helper.rb
@@ -1,11 +1,16 @@
module UserCalloutsHelper
GKE_CLUSTER_INTEGRATION = 'gke_cluster_integration'.freeze
+ GCP_SIGNUP_OFFER = 'gcp_signup_offer'.freeze
def show_gke_cluster_integration_callout?(project)
can?(current_user, :create_cluster, project) &&
!user_dismissed?(GKE_CLUSTER_INTEGRATION)
end
+ def show_gcp_signup_offer?
+ !user_dismissed?(GCP_SIGNUP_OFFER)
+ end
+
private
def user_dismissed?(feature_name)
diff --git a/app/helpers/users_helper.rb b/app/helpers/users_helper.rb
index 01af68088df..e803cd3a8d8 100644
--- a/app/helpers/users_helper.rb
+++ b/app/helpers/users_helper.rb
@@ -23,9 +23,42 @@ module UsersHelper
profile_tabs.include?(tab)
end
+ def current_user_menu_items
+ @current_user_menu_items ||= get_current_user_menu_items
+ end
+
+ def current_user_menu?(item)
+ current_user_menu_items.include?(item)
+ end
+
private
def get_profile_tabs
[:activity, :groups, :contributed, :projects, :snippets]
end
+
+ def get_current_user_menu_items
+ items = []
+
+ items << :sign_out if current_user
+
+ # TODO: Remove these conditions when the permissions are prevented in
+ # https://gitlab.com/gitlab-org/gitlab-ce/issues/45849
+ terms_not_enforced = !Gitlab::CurrentSettings
+ .current_application_settings
+ .enforce_terms?
+ required_terms_accepted = terms_not_enforced || current_user.terms_accepted?
+
+ items << :help if required_terms_accepted
+
+ if can?(current_user, :read_user, current_user) && required_terms_accepted
+ items << :profile
+ end
+
+ if can?(current_user, :update_user, current_user) && required_terms_accepted
+ items << :settings
+ end
+
+ items
+ end
end
diff --git a/app/helpers/webpack_helper.rb b/app/helpers/webpack_helper.rb
index 8bcced70d63..e12e4ba70e9 100644
--- a/app/helpers/webpack_helper.rb
+++ b/app/helpers/webpack_helper.rb
@@ -1,12 +1,12 @@
-require 'webpack/rails/manifest'
+require 'gitlab/webpack/manifest'
module WebpackHelper
- def webpack_bundle_tag(bundle, force_same_domain: false)
- javascript_include_tag(*gitlab_webpack_asset_paths(bundle, force_same_domain: force_same_domain))
+ def webpack_bundle_tag(bundle)
+ javascript_include_tag(*webpack_entrypoint_paths(bundle))
end
def webpack_controller_bundle_tags
- bundles = []
+ chunks = []
action = case controller.action_name
when 'create' then 'new'
@@ -16,37 +16,44 @@ module WebpackHelper
route = [*controller.controller_path.split('/'), action].compact
- until route.empty?
+ until chunks.any? || route.empty?
+ entrypoint = "pages.#{route.join('.')}"
begin
- asset_paths = gitlab_webpack_asset_paths("pages.#{route.join('.')}", extension: 'js')
- bundles.unshift(*asset_paths)
- rescue Webpack::Rails::Manifest::EntryPointMissingError
+ chunks = webpack_entrypoint_paths(entrypoint, extension: 'js')
+ rescue Gitlab::Webpack::Manifest::AssetMissingError
# no bundle exists for this path
end
-
route.pop
end
- javascript_include_tag(*bundles)
+ if chunks.empty?
+ chunks = webpack_entrypoint_paths("default", extension: 'js')
+ end
+
+ javascript_include_tag(*chunks)
end
- # override webpack-rails gem helper until changes can make it upstream
- def gitlab_webpack_asset_paths(source, extension: nil, force_same_domain: false)
+ def webpack_entrypoint_paths(source, extension: nil, exclude_duplicates: true)
return "" unless source.present?
- paths = Webpack::Rails::Manifest.asset_paths(source)
+ paths = Gitlab::Webpack::Manifest.entrypoint_paths(source)
if extension
paths.select! { |p| p.ends_with? ".#{extension}" }
end
- unless force_same_domain
- force_host = webpack_public_host
- if force_host
- paths.map! { |p| "#{force_host}#{p}" }
- end
+ force_host = webpack_public_host
+ if force_host
+ paths.map! { |p| "#{force_host}#{p}" }
end
- paths
+ if exclude_duplicates
+ @used_paths ||= []
+ new_paths = paths - @used_paths
+ @used_paths += new_paths
+ new_paths
+ else
+ paths
+ end
end
def webpack_public_host
diff --git a/app/mailers/emails/notes.rb b/app/mailers/emails/notes.rb
index 50e17fe7717..d9a6fe2a41e 100644
--- a/app/mailers/emails/notes.rb
+++ b/app/mailers/emails/notes.rb
@@ -43,7 +43,7 @@ module Emails
private
def note_target_url_options
- [@project, @note.noteable, anchor: "note_#{@note.id}"]
+ [@project || @group, @note.noteable, anchor: "note_#{@note.id}"]
end
def note_thread_options(recipient_id)
@@ -58,8 +58,9 @@ module Emails
# `note_id` is a `Note` when originating in `NotifyPreview`
@note = note_id.is_a?(Note) ? note_id : Note.find(note_id)
@project = @note.project
+ @group = @note.noteable.try(:group)
- if @project && @note.persisted?
+ if (@project || @group) && @note.persisted?
@sent_notification = SentNotification.record_note(@note, recipient_id, reply_key)
end
end
diff --git a/app/mailers/notify.rb b/app/mailers/notify.rb
index 3646e08a15f..1db1482d6b7 100644
--- a/app/mailers/notify.rb
+++ b/app/mailers/notify.rb
@@ -94,6 +94,7 @@ class Notify < BaseMailer
def subject(*extra)
subject = ""
subject << "#{@project.name} | " if @project
+ subject << "#{@group.name} | " if @group
subject << extra.join(' | ') if extra.present?
subject << " | #{Gitlab.config.gitlab.email_subject_suffix}" if Gitlab.config.gitlab.email_subject_suffix.present?
subject
@@ -117,10 +118,9 @@ class Notify < BaseMailer
@reason = headers['X-GitLab-NotificationReason']
if Gitlab::IncomingEmail.enabled? && @sent_notification
- address = Mail::Address.new(Gitlab::IncomingEmail.reply_address(reply_key))
- address.display_name = @project.full_name
-
- headers['Reply-To'] = address
+ headers['Reply-To'] = Mail::Address.new(Gitlab::IncomingEmail.reply_address(reply_key)).tap do |address|
+ address.display_name = reply_display_name(model)
+ end
fallback_reply_message_id = "<reply-#{reply_key}@#{Gitlab.config.gitlab.host}>".freeze
headers['References'] ||= []
@@ -132,6 +132,11 @@ class Notify < BaseMailer
mail(headers)
end
+ # `model` is used on EE code
+ def reply_display_name(_model)
+ @project.full_name
+ end
+
# Send an email that starts a new conversation thread,
# with headers suitable for grouping by thread in email clients.
#
diff --git a/app/models/ability.rb b/app/models/ability.rb
index 618d4af4272..bb600eaccba 100644
--- a/app/models/ability.rb
+++ b/app/models/ability.rb
@@ -10,6 +10,14 @@ class Ability
end
end
+ # Given a list of users and a group this method returns the users that can
+ # read the given group.
+ def users_that_can_read_group(users, group)
+ DeclarativePolicy.subject_scope do
+ users.select { |u| allowed?(u, :read_group, group) }
+ end
+ end
+
# Given a list of users and a snippet this method returns the users that can
# read the given snippet.
def users_that_can_read_personal_snippet(users, snippet)
diff --git a/app/models/active_session.rb b/app/models/active_session.rb
new file mode 100644
index 00000000000..b4a86dbb331
--- /dev/null
+++ b/app/models/active_session.rb
@@ -0,0 +1,110 @@
+class ActiveSession
+ include ActiveModel::Model
+
+ attr_accessor :created_at, :updated_at,
+ :session_id, :ip_address,
+ :browser, :os, :device_name, :device_type
+
+ def current?(session)
+ return false if session_id.nil? || session.id.nil?
+
+ session_id == session.id
+ end
+
+ def human_device_type
+ device_type&.titleize
+ end
+
+ def self.set(user, request)
+ Gitlab::Redis::SharedState.with do |redis|
+ session_id = request.session.id
+ client = DeviceDetector.new(request.user_agent)
+ timestamp = Time.current
+
+ active_user_session = new(
+ ip_address: request.ip,
+ browser: client.name,
+ os: client.os_name,
+ device_name: client.device_name,
+ device_type: client.device_type,
+ created_at: user.current_sign_in_at || timestamp,
+ updated_at: timestamp,
+ session_id: session_id
+ )
+
+ redis.pipelined do
+ redis.setex(
+ key_name(user.id, session_id),
+ Settings.gitlab['session_expire_delay'] * 60,
+ Marshal.dump(active_user_session)
+ )
+
+ redis.sadd(
+ lookup_key_name(user.id),
+ session_id
+ )
+ end
+ end
+ end
+
+ def self.list(user)
+ Gitlab::Redis::SharedState.with do |redis|
+ cleaned_up_lookup_entries(redis, user.id).map do |entry|
+ # rubocop:disable Security/MarshalLoad
+ Marshal.load(entry)
+ # rubocop:enable Security/MarshalLoad
+ end
+ end
+ end
+
+ def self.destroy(user, session_id)
+ Gitlab::Redis::SharedState.with do |redis|
+ redis.srem(lookup_key_name(user.id), session_id)
+
+ deleted_keys = redis.del(key_name(user.id, session_id))
+
+ # only allow deleting the devise session if we could actually find a
+ # related active session. this prevents another user from deleting
+ # someone else's session.
+ if deleted_keys > 0
+ redis.del("#{Gitlab::Redis::SharedState::SESSION_NAMESPACE}:#{session_id}")
+ end
+ end
+ end
+
+ def self.cleanup(user)
+ Gitlab::Redis::SharedState.with do |redis|
+ cleaned_up_lookup_entries(redis, user.id)
+ end
+ end
+
+ def self.key_name(user_id, session_id = '*')
+ "#{Gitlab::Redis::SharedState::USER_SESSIONS_NAMESPACE}:#{user_id}:#{session_id}"
+ end
+
+ def self.lookup_key_name(user_id)
+ "#{Gitlab::Redis::SharedState::USER_SESSIONS_LOOKUP_NAMESPACE}:#{user_id}"
+ end
+
+ def self.cleaned_up_lookup_entries(redis, user_id)
+ lookup_key = lookup_key_name(user_id)
+
+ session_ids = redis.smembers(lookup_key)
+
+ entry_keys = session_ids.map { |session_id| key_name(user_id, session_id) }
+ return [] if entry_keys.empty?
+
+ entries = redis.mget(entry_keys)
+
+ session_ids_and_entries = session_ids.zip(entries)
+
+ # remove expired keys.
+ # only the single key entries are automatically expired by redis, the
+ # lookup entries in the set need to be removed manually.
+ session_ids_and_entries.reject { |_session_id, entry| entry }.each do |session_id, _entry|
+ redis.srem(lookup_key, session_id)
+ end
+
+ session_ids_and_entries.select { |_session_id, entry| entry }.map { |_session_id, entry| entry }
+ end
+end
diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb
index 862933bf127..451e512aef7 100644
--- a/app/models/application_setting.rb
+++ b/app/models/application_setting.rb
@@ -220,12 +220,15 @@ class ApplicationSetting < ActiveRecord::Base
end
end
+ validate :terms_exist, if: :enforce_terms?
+
before_validation :ensure_uuid!
before_save :ensure_runners_registration_token
before_save :ensure_health_check_access_token
after_commit do
+ reset_memoized_terms
Rails.cache.write(CACHE_KEY, self)
end
@@ -331,7 +334,8 @@ class ApplicationSetting < ActiveRecord::Base
gitaly_timeout_fast: 10,
gitaly_timeout_medium: 30,
gitaly_timeout_default: 55,
- allow_local_requests_from_hooks_and_services: false
+ allow_local_requests_from_hooks_and_services: false,
+ mirror_available: true
}
end
@@ -507,6 +511,16 @@ class ApplicationSetting < ActiveRecord::Base
password_authentication_enabled_for_web? || password_authentication_enabled_for_git?
end
+ delegate :terms, to: :latest_terms, allow_nil: true
+ def latest_terms
+ @latest_terms ||= Term.latest
+ end
+
+ def reset_memoized_terms
+ @latest_terms = nil
+ latest_terms
+ end
+
private
def ensure_uuid!
@@ -520,4 +534,10 @@ class ApplicationSetting < ActiveRecord::Base
errors.add(:repository_storages, "can't include: #{invalid.join(", ")}") unless
invalid.empty?
end
+
+ def terms_exist
+ return unless enforce_terms?
+
+ errors.add(:terms, "You need to set terms to be enforced") unless terms.present?
+ end
end
diff --git a/app/models/application_setting/term.rb b/app/models/application_setting/term.rb
new file mode 100644
index 00000000000..e8ce0ccbb71
--- /dev/null
+++ b/app/models/application_setting/term.rb
@@ -0,0 +1,13 @@
+class ApplicationSetting
+ class Term < ActiveRecord::Base
+ include CacheMarkdownField
+
+ validates :terms, presence: true
+
+ cache_markdown_field :terms
+
+ def self.latest
+ order(:id).last
+ end
+ end
+end
diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb
index 9000ad860e9..61c10c427dd 100644
--- a/app/models/ci/build.rb
+++ b/app/models/ci/build.rb
@@ -19,6 +19,7 @@ module Ci
has_one :last_deployment, -> { order('deployments.id DESC') }, as: :deployable, class_name: 'Deployment'
has_many :trace_sections, class_name: 'Ci::BuildTraceSection'
+ has_many :trace_chunks, class_name: 'Ci::BuildTraceChunk', foreign_key: :build_id
has_many :job_artifacts, class_name: 'Ci::JobArtifact', foreign_key: :job_id, dependent: :destroy, inverse_of: :job # rubocop:disable Cop/ActiveRecordDependent
has_one :job_artifacts_archive, -> { where(file_type: Ci::JobArtifact.file_types[:archive]) }, class_name: 'Ci::JobArtifact', inverse_of: :job, foreign_key: :job_id
diff --git a/app/models/ci/build_trace_chunk.rb b/app/models/ci/build_trace_chunk.rb
new file mode 100644
index 00000000000..4856f10846c
--- /dev/null
+++ b/app/models/ci/build_trace_chunk.rb
@@ -0,0 +1,180 @@
+module Ci
+ class BuildTraceChunk < ActiveRecord::Base
+ include FastDestroyAll
+ extend Gitlab::Ci::Model
+
+ belongs_to :build, class_name: "Ci::Build", foreign_key: :build_id
+
+ default_value_for :data_store, :redis
+
+ WriteError = Class.new(StandardError)
+
+ CHUNK_SIZE = 128.kilobytes
+ CHUNK_REDIS_TTL = 1.week
+ WRITE_LOCK_RETRY = 10
+ WRITE_LOCK_SLEEP = 0.01.seconds
+ WRITE_LOCK_TTL = 1.minute
+
+ enum data_store: {
+ redis: 1,
+ db: 2
+ }
+
+ class << self
+ def redis_data_key(build_id, chunk_index)
+ "gitlab:ci:trace:#{build_id}:chunks:#{chunk_index}"
+ end
+
+ def redis_data_keys
+ redis.pluck(:build_id, :chunk_index).map do |data|
+ redis_data_key(data.first, data.second)
+ end
+ end
+
+ def redis_delete_data(keys)
+ return if keys.empty?
+
+ Gitlab::Redis::SharedState.with do |redis|
+ redis.del(keys)
+ end
+ end
+
+ ##
+ # FastDestroyAll concerns
+ def begin_fast_destroy
+ redis_data_keys
+ end
+
+ ##
+ # FastDestroyAll concerns
+ def finalize_fast_destroy(keys)
+ redis_delete_data(keys)
+ end
+ end
+
+ ##
+ # Data is memoized for optimizing #size and #end_offset
+ def data
+ @data ||= get_data.to_s
+ end
+
+ def truncate(offset = 0)
+ raise ArgumentError, 'Offset is out of range' if offset > size || offset < 0
+ return if offset == size # Skip the following process as it doesn't affect anything
+
+ self.append("", offset)
+ end
+
+ def append(new_data, offset)
+ raise ArgumentError, 'Offset is out of range' if offset > size || offset < 0
+ raise ArgumentError, 'Chunk size overflow' if CHUNK_SIZE < (offset + new_data.bytesize)
+
+ set_data(data.byteslice(0, offset) + new_data)
+ end
+
+ def size
+ data&.bytesize.to_i
+ end
+
+ def start_offset
+ chunk_index * CHUNK_SIZE
+ end
+
+ def end_offset
+ start_offset + size
+ end
+
+ def range
+ (start_offset...end_offset)
+ end
+
+ def use_database!
+ in_lock do
+ break if db?
+ break unless size > 0
+
+ self.update!(raw_data: data, data_store: :db)
+ self.class.redis_delete_data([redis_data_key])
+ end
+ end
+
+ private
+
+ def get_data
+ if redis?
+ redis_data
+ elsif db?
+ raw_data
+ else
+ raise 'Unsupported data store'
+ end&.force_encoding(Encoding::BINARY) # Redis/Database return UTF-8 string as default
+ end
+
+ def set_data(value)
+ raise ArgumentError, 'too much data' if value.bytesize > CHUNK_SIZE
+
+ in_lock do
+ if redis?
+ redis_set_data(value)
+ elsif db?
+ self.raw_data = value
+ else
+ raise 'Unsupported data store'
+ end
+
+ @data = value
+
+ save! if changed?
+ end
+
+ schedule_to_db if full?
+ end
+
+ def schedule_to_db
+ return if db?
+
+ Ci::BuildTraceChunkFlushWorker.perform_async(id)
+ end
+
+ def full?
+ size == CHUNK_SIZE
+ end
+
+ def redis_data
+ Gitlab::Redis::SharedState.with do |redis|
+ redis.get(redis_data_key)
+ end
+ end
+
+ def redis_set_data(data)
+ Gitlab::Redis::SharedState.with do |redis|
+ redis.set(redis_data_key, data, ex: CHUNK_REDIS_TTL)
+ end
+ end
+
+ def redis_data_key
+ self.class.redis_data_key(build_id, chunk_index)
+ end
+
+ def in_lock
+ write_lock_key = "trace_write:#{build_id}:chunks:#{chunk_index}"
+
+ lease = Gitlab::ExclusiveLease.new(write_lock_key, timeout: WRITE_LOCK_TTL)
+ retry_count = 0
+
+ until uuid = lease.try_obtain
+ # Keep trying until we obtain the lease. To prevent hammering Redis too
+ # much we'll wait for a bit between retries.
+ sleep(WRITE_LOCK_SLEEP)
+ break if WRITE_LOCK_RETRY < (retry_count += 1)
+ end
+
+ raise WriteError, 'Failed to obtain write lock' unless uuid
+
+ self.reload if self.persisted?
+ return yield
+ ensure
+ Gitlab::ExclusiveLease.cancel(write_lock_key, uuid)
+ end
+ end
+end
diff --git a/app/models/ci/job_artifact.rb b/app/models/ci/job_artifact.rb
index 39676efa08c..3b952391b7e 100644
--- a/app/models/ci/job_artifact.rb
+++ b/app/models/ci/job_artifact.rb
@@ -13,7 +13,7 @@ module Ci
after_save :update_project_statistics_after_save, if: :size_changed?
after_destroy :update_project_statistics_after_destroy, unless: :project_destroyed?
- after_save :update_file_store
+ after_save :update_file_store, if: :file_changed?
scope :with_files_stored_locally, -> { where(file_store: [nil, ::JobArtifactUploader::Store::LOCAL]) }
diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb
index 434b9b64c65..0b90834d415 100644
--- a/app/models/ci/pipeline.rb
+++ b/app/models/ci/pipeline.rb
@@ -32,6 +32,8 @@ module Ci
has_many :auto_canceled_pipelines, class_name: 'Ci::Pipeline', foreign_key: 'auto_canceled_by_id'
has_many :auto_canceled_jobs, class_name: 'CommitStatus', foreign_key: 'auto_canceled_by_id'
+ accepts_nested_attributes_for :variables, reject_if: :persisted?
+
delegate :id, to: :project, prefix: true
delegate :full_path, to: :project, prefix: true
@@ -269,19 +271,39 @@ module Ci
end
def git_author_name
- commit.try(:author_name)
+ strong_memoize(:git_author_name) do
+ commit.try(:author_name)
+ end
end
def git_author_email
- commit.try(:author_email)
+ strong_memoize(:git_author_email) do
+ commit.try(:author_email)
+ end
end
def git_commit_message
- commit.try(:message)
+ strong_memoize(:git_commit_message) do
+ commit.try(:message)
+ end
end
def git_commit_title
- commit.try(:title)
+ strong_memoize(:git_commit_title) do
+ commit.try(:title)
+ end
+ end
+
+ def git_commit_full_title
+ strong_memoize(:git_commit_full_title) do
+ commit.try(:full_title)
+ end
+ end
+
+ def git_commit_description
+ strong_memoize(:git_commit_description) do
+ commit.try(:description)
+ end
end
def short_sha
@@ -491,6 +513,9 @@ module Ci
.append(key: 'CI_PIPELINE_ID', value: id.to_s)
.append(key: 'CI_CONFIG_PATH', value: ci_yaml_file_path)
.append(key: 'CI_PIPELINE_SOURCE', value: source.to_s)
+ .append(key: 'CI_COMMIT_MESSAGE', value: git_commit_message)
+ .append(key: 'CI_COMMIT_TITLE', value: git_commit_full_title)
+ .append(key: 'CI_COMMIT_DESCRIPTION', value: git_commit_description)
end
def queued_duration
@@ -530,6 +555,17 @@ module Ci
@latest_builds_with_artifacts ||= builds.latest.with_artifacts_archive.to_a
end
+ # Rails 5.0 autogenerated question mark enum methods return wrong result if enum value is nil.
+ # They always return `false`.
+ # These methods overwrite autogenerated ones to return correct results.
+ def unknown?
+ Gitlab.rails5? ? source.nil? : super
+ end
+
+ def unknown_source?
+ Gitlab.rails5? ? config_source.nil? : super
+ end
+
private
def ci_yaml_from_repo
diff --git a/app/models/ci/pipeline_variable.rb b/app/models/ci/pipeline_variable.rb
index de5aae17a15..38e14ffbc0c 100644
--- a/app/models/ci/pipeline_variable.rb
+++ b/app/models/ci/pipeline_variable.rb
@@ -5,6 +5,8 @@ module Ci
belongs_to :pipeline
+ alias_attribute :secret_value, :value
+
validates :key, uniqueness: { scope: :pipeline_id }
end
end
diff --git a/app/models/ci/runner.rb b/app/models/ci/runner.rb
index 5a4c56ec0dc..23078f1c3ed 100644
--- a/app/models/ci/runner.rb
+++ b/app/models/ci/runner.rb
@@ -14,31 +14,49 @@ module Ci
has_many :builds
has_many :runner_projects, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
has_many :projects, through: :runner_projects
+ has_many :runner_namespaces
+ has_many :groups, through: :runner_namespaces
has_one :last_build, ->() { order('id DESC') }, class_name: 'Ci::Build'
before_validation :set_default_values
- scope :specific, ->() { where(is_shared: false) }
- scope :shared, ->() { where(is_shared: true) }
- scope :active, ->() { where(active: true) }
- scope :paused, ->() { where(active: false) }
- scope :online, ->() { where('contacted_at > ?', contact_time_deadline) }
- scope :ordered, ->() { order(id: :desc) }
+ scope :specific, -> { where(is_shared: false) }
+ scope :shared, -> { where(is_shared: true) }
+ scope :active, -> { where(active: true) }
+ scope :paused, -> { where(active: false) }
+ scope :online, -> { where('contacted_at > ?', contact_time_deadline) }
+ scope :ordered, -> { order(id: :desc) }
- scope :owned_or_shared, ->(project_id) do
- joins('LEFT JOIN ci_runner_projects ON ci_runner_projects.runner_id = ci_runners.id')
- .where("ci_runner_projects.project_id = :project_id OR ci_runners.is_shared = true", project_id: project_id)
+ scope :belonging_to_project, -> (project_id) {
+ joins(:runner_projects).where(ci_runner_projects: { project_id: project_id })
+ }
+
+ scope :belonging_to_parent_group_of_project, -> (project_id) {
+ project_groups = ::Group.joins(:projects).where(projects: { id: project_id })
+ hierarchy_groups = Gitlab::GroupHierarchy.new(project_groups).base_and_ancestors
+
+ joins(:groups).where(namespaces: { id: hierarchy_groups })
+ }
+
+ scope :owned_or_shared, -> (project_id) do
+ union = Gitlab::SQL::Union.new(
+ [belonging_to_project(project_id), belonging_to_parent_group_of_project(project_id), shared],
+ remove_duplicates: false
+ )
+ from("(#{union.to_sql}) ci_runners")
end
scope :assignable_for, ->(project) do
# FIXME: That `to_sql` is needed to workaround a weird Rails bug.
# Without that, placeholders would miss one and couldn't match.
where(locked: false)
- .where.not("id IN (#{project.runners.select(:id).to_sql})").specific
+ .where.not("ci_runners.id IN (#{project.runners.select(:id).to_sql})")
+ .specific
end
validate :tag_constraints
+ validate :either_projects_or_group
validates :access_level, presence: true
acts_as_taggable
@@ -50,6 +68,12 @@ module Ci
ref_protected: 1
}
+ enum runner_type: {
+ instance_type: 1,
+ group_type: 2,
+ project_type: 3
+ }
+
cached_attr_reader :version, :revision, :platform, :architecture, :contacted_at, :ip_address
chronic_duration_attr :maximum_timeout_human_readable, :maximum_timeout
@@ -120,6 +144,14 @@ module Ci
!shared?
end
+ def assigned_to_group?
+ runner_namespaces.any?
+ end
+
+ def assigned_to_project?
+ runner_projects.any?
+ end
+
def can_pick?(build)
return false if self.ref_protected? && !build.protected?
@@ -174,6 +206,12 @@ module Ci
end
end
+ def pick_build!(build)
+ if can_pick?(build)
+ tick_runner_queue
+ end
+ end
+
private
def cleanup_runner_queue
@@ -205,7 +243,17 @@ module Ci
end
def assignable_for?(project_id)
- is_shared? || projects.exists?(id: project_id)
+ self.class.owned_or_shared(project_id).where(id: self.id).any?
+ end
+
+ def either_projects_or_group
+ if groups.many?
+ errors.add(:runner, 'can only be assigned to one group')
+ end
+
+ if assigned_to_group? && assigned_to_project?
+ errors.add(:runner, 'can only be assigned either to projects or to a group')
+ end
end
def accepting_tags?(build)
diff --git a/app/models/ci/runner_namespace.rb b/app/models/ci/runner_namespace.rb
new file mode 100644
index 00000000000..3269f86e8ca
--- /dev/null
+++ b/app/models/ci/runner_namespace.rb
@@ -0,0 +1,9 @@
+module Ci
+ class RunnerNamespace < ActiveRecord::Base
+ extend Gitlab::Ci::Model
+
+ belongs_to :runner
+ belongs_to :namespace, class_name: '::Namespace'
+ belongs_to :group, class_name: '::Group', foreign_key: :namespace_id
+ end
+end
diff --git a/app/models/ci/stage.rb b/app/models/ci/stage.rb
index 75b8ea2a371..5a1eeb966aa 100644
--- a/app/models/ci/stage.rb
+++ b/app/models/ci/stage.rb
@@ -13,14 +13,27 @@ module Ci
has_many :statuses, class_name: 'CommitStatus', foreign_key: :stage_id
has_many :builds, foreign_key: :stage_id
- validates :project, presence: true, unless: :importing?
- validates :pipeline, presence: true, unless: :importing?
- validates :name, presence: true, unless: :importing?
+ with_options unless: :importing? do
+ validates :project, presence: true
+ validates :pipeline, presence: true
+ validates :name, presence: true
+ validates :position, presence: true
+ end
- after_initialize do |stage|
+ after_initialize do
self.status = DEFAULT_STATUS if self.status.nil?
end
+ before_validation unless: :importing? do
+ next if position.present?
+
+ self.position = statuses.select(:stage_idx)
+ .where('stage_idx IS NOT NULL')
+ .group(:stage_idx)
+ .order('COUNT(*) DESC')
+ .first&.stage_idx.to_i
+ end
+
state_machine :status, initial: :created do
event :enqueue do
transition created: :pending
diff --git a/app/models/commit.rb b/app/models/commit.rb
index 32f3d90595a..b46f9f34689 100644
--- a/app/models/commit.rb
+++ b/app/models/commit.rb
@@ -424,6 +424,12 @@ class Commit
# no-op but needs to be defined since #persisted? is defined
end
+ def touch_later
+ # No-op.
+ # This method is called by ActiveRecord.
+ # We don't want to do anything for `Commit` model, so this is empty.
+ end
+
WIP_REGEX = /\A\s*(((?i)(\[WIP\]|WIP:|WIP)\s|WIP$))|(fixup!|squash!)\s/.freeze
def work_in_progress?
diff --git a/app/models/commit_status.rb b/app/models/commit_status.rb
index b6276c2fb50..97d89422594 100644
--- a/app/models/commit_status.rb
+++ b/app/models/commit_status.rb
@@ -189,4 +189,11 @@ class CommitStatus < ActiveRecord::Base
v =~ /\d+/ ? v.to_i : v
end
end
+
+ # Rails 5.0 autogenerated question mark enum methods return wrong result if enum value is nil.
+ # They always return `false`.
+ # This method overwrites the autogenerated one to return correct result.
+ def unknown_failure?
+ Gitlab.rails5? ? failure_reason.nil? : super
+ end
end
diff --git a/app/models/concerns/fast_destroy_all.rb b/app/models/concerns/fast_destroy_all.rb
new file mode 100644
index 00000000000..7ea042c6742
--- /dev/null
+++ b/app/models/concerns/fast_destroy_all.rb
@@ -0,0 +1,91 @@
+##
+# This module is for replacing `dependent: :destroy` and `before_destroy` hooks.
+#
+# In general, `destroy_all` is inefficient because it calls each callback with `DELETE` queries i.e. O(n), whereas,
+# `delete_all` is efficient as it deletes all rows with a single `DELETE` query.
+#
+# It's better to use `delete_all` as our best practice, however,
+# if external data (e.g. ObjectStorage, FileStorage or Redis) are assosiated with database records,
+# it is difficult to accomplish it.
+#
+# This module defines a format to use `delete_all` and delete associated external data.
+# Here is an exmaple
+#
+# Situation
+# - `Project` has many `Ci::BuildTraceChunk` through `Ci::Build`
+# - `Ci::BuildTraceChunk` stores associated data in Redis, so it relies on `dependent: :destroy` and `before_destroy` for the deletion
+#
+# How to use
+# - Define `use_fast_destroy :build_trace_chunks` in `Project` model.
+# - Define `begin_fast_destroy` and `finalize_fast_destroy(params)` in `Ci::BuildTraceChunk` model.
+# - Use `fast_destroy_all` instead of `destroy` and `destroy_all`
+# - Remove `dependent: :destroy` and `before_destroy` as it's no longer need
+#
+# Expectation
+# - When a project is `destroy`ed, the associated trace_chunks will be deleted by `delete_all`,
+# and the associated data will be removed, too.
+# - When `fast_destroy_all` is called, it also performns as same.
+module FastDestroyAll
+ extend ActiveSupport::Concern
+
+ ForbiddenActionError = Class.new(StandardError)
+
+ included do
+ before_destroy do
+ raise ForbiddenActionError, '`destroy` and `destroy_all` are forbbiden. Please use `fast_destroy_all`'
+ end
+ end
+
+ class_methods do
+ ##
+ # This method delete rows and associated external data efficiently
+ #
+ # This method can replace `destroy` and `destroy_all` without having `after_destroy` hook
+ def fast_destroy_all
+ params = begin_fast_destroy
+
+ delete_all
+
+ finalize_fast_destroy(params)
+ end
+
+ ##
+ # This method returns identifiers to delete associated external data (e.g. file paths, redis keys)
+ #
+ # This method must be defined in fast destroyable model
+ def begin_fast_destroy
+ raise NotImplementedError
+ end
+
+ ##
+ # This method deletes associated external data with the identifiers returned by `begin_fast_destroy`
+ #
+ # This method must be defined in fast destroyable model
+ def finalize_fast_destroy(params)
+ raise NotImplementedError
+ end
+ end
+
+ module Helpers
+ extend ActiveSupport::Concern
+
+ class_methods do
+ ##
+ # This method is to be defined on models which have fast destroyable models as children,
+ # and let us avoid to use `dependent: :destroy` hook
+ def use_fast_destroy(relation)
+ before_destroy(prepend: true) do
+ perform_fast_destroy(public_send(relation)) # rubocop:disable GitlabSecurity/PublicSend
+ end
+ end
+ end
+
+ def perform_fast_destroy(subject)
+ params = subject.begin_fast_destroy
+
+ run_after_commit do
+ subject.finalize_fast_destroy(params)
+ end
+ end
+ end
+end
diff --git a/app/models/concerns/participable.rb b/app/models/concerns/participable.rb
index e48bc0be410..01b1ef9f82c 100644
--- a/app/models/concerns/participable.rb
+++ b/app/models/concerns/participable.rb
@@ -98,6 +98,10 @@ module Participable
participants.merge(ext.users)
+ filter_by_ability(participants)
+ end
+
+ def filter_by_ability(participants)
case self
when PersonalSnippet
Ability.users_that_can_read_personal_snippet(participants.to_a, self)
diff --git a/app/models/concerns/reactive_caching.rb b/app/models/concerns/reactive_caching.rb
index 2589215ad19..eef9caf1c8e 100644
--- a/app/models/concerns/reactive_caching.rb
+++ b/app/models/concerns/reactive_caching.rb
@@ -60,13 +60,16 @@ module ReactiveCaching
end
def with_reactive_cache(*args, &blk)
- within_reactive_cache_lifetime(*args) do
+ bootstrap = !within_reactive_cache_lifetime?(*args)
+ Rails.cache.write(alive_reactive_cache_key(*args), true, expires_in: self.class.reactive_cache_lifetime)
+
+ if bootstrap
+ ReactiveCachingWorker.perform_async(self.class, id, *args)
+ nil
+ else
data = Rails.cache.read(full_reactive_cache_key(*args))
yield data if data.present?
end
- ensure
- Rails.cache.write(alive_reactive_cache_key(*args), true, expires_in: self.class.reactive_cache_lifetime)
- ReactiveCachingWorker.perform_async(self.class, id, *args)
end
def clear_reactive_cache!(*args)
@@ -75,7 +78,7 @@ module ReactiveCaching
def exclusively_update_reactive_cache!(*args)
locking_reactive_cache(*args) do
- within_reactive_cache_lifetime(*args) do
+ if within_reactive_cache_lifetime?(*args)
enqueuing_update(*args) do
value = calculate_reactive_cache(*args)
Rails.cache.write(full_reactive_cache_key(*args), value)
@@ -105,8 +108,8 @@ module ReactiveCaching
Gitlab::ExclusiveLease.cancel(full_reactive_cache_key(*args), uuid)
end
- def within_reactive_cache_lifetime(*args)
- yield if Rails.cache.read(alive_reactive_cache_key(*args))
+ def within_reactive_cache_lifetime?(*args)
+ !!Rails.cache.read(alive_reactive_cache_key(*args))
end
def enqueuing_update(*args)
diff --git a/app/models/concerns/sha_attribute.rb b/app/models/concerns/sha_attribute.rb
index 703a72c355c..3340dc96e9f 100644
--- a/app/models/concerns/sha_attribute.rb
+++ b/app/models/concerns/sha_attribute.rb
@@ -4,18 +4,33 @@ module ShaAttribute
module ClassMethods
def sha_attribute(name)
return if ENV['STATIC_VERIFICATION']
- return unless table_exists?
+
+ validate_binary_column_exists!(name) unless Rails.env.production?
+
+ attribute(name, Gitlab::Database::ShaAttribute.new)
+ end
+
+ # This only gets executed in non-production environments as an additional check to ensure
+ # the column is the correct type. In production it should behave like any other attribute.
+ # See https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/5502 for more discussion
+ def validate_binary_column_exists!(name)
+ unless table_exists?
+ warn "WARNING: sha_attribute #{name.inspect} is invalid since the table doesn't exist - you may need to run database migrations"
+ return
+ end
column = columns.find { |c| c.name == name.to_s }
- # In case the table doesn't exist we won't be able to find the column,
- # thus we will only check the type if the column is present.
- if column && column.type != :binary
- raise ArgumentError,
- "sha_attribute #{name.inspect} is invalid since the column type is not :binary"
+ unless column
+ raise ArgumentError.new("sha_attribute #{name.inspect} is invalid since the column doesn't exist")
end
- attribute(name, Gitlab::Database::ShaAttribute.new)
+ unless column.type == :binary
+ raise ArgumentError.new("sha_attribute #{name.inspect} is invalid since the column type is not :binary")
+ end
+ rescue => error
+ Gitlab::AppLogger.error "ShaAttribute initialization: #{error.message}"
+ raise
end
end
end
diff --git a/app/models/group.rb b/app/models/group.rb
index 9b42bbf99be..cefca316399 100644
--- a/app/models/group.rb
+++ b/app/models/group.rb
@@ -9,6 +9,7 @@ class Group < Namespace
include SelectForProjectAuthorization
include LoadedInGroupList
include GroupDescendant
+ include TokenAuthenticatable
has_many :group_members, -> { where(requested_at: nil) }, dependent: :destroy, as: :source # rubocop:disable Cop/ActiveRecordDependent
alias_method :members, :group_members
@@ -43,6 +44,8 @@ class Group < Namespace
validates :two_factor_grace_period, presence: true, numericality: { greater_than_or_equal_to: 0 }
+ add_authentication_token_field :runners_token
+
after_create :post_create_hook
after_destroy :post_destroy_hook
after_save :update_two_factor_requirement
@@ -238,6 +241,13 @@ class Group < Namespace
.where(source_id: self_and_descendants.reorder(nil).select(:id))
end
+ # Returns all members that are part of the group, it's subgroups, and ancestor groups
+ def direct_and_indirect_members
+ GroupMember
+ .active_without_invites_and_requests
+ .where(source_id: self_and_hierarchy.reorder(nil).select(:id))
+ end
+
def users_with_parents
User
.where(id: members_with_parents.select(:user_id))
@@ -250,6 +260,30 @@ class Group < Namespace
.reorder(nil)
end
+ # Returns all users that are members of the group because:
+ # 1. They belong to the group
+ # 2. They belong to a project that belongs to the group
+ # 3. They belong to a sub-group or project in such sub-group
+ # 4. They belong to an ancestor group
+ def direct_and_indirect_users
+ union = Gitlab::SQL::Union.new([
+ User
+ .where(id: direct_and_indirect_members.select(:user_id))
+ .reorder(nil),
+ project_users_with_descendants
+ ])
+
+ User.from("(#{union.to_sql}) #{User.table_name}")
+ end
+
+ # Returns all users that are members of projects
+ # belonging to the current group or sub-groups
+ def project_users_with_descendants
+ User
+ .joins(projects: :group)
+ .where(namespaces: { id: self_and_descendants.select(:id) })
+ end
+
def max_member_access_for_user(user)
return GroupMember::OWNER if user.admin?
@@ -294,6 +328,13 @@ class Group < Namespace
refresh_members_authorized_projects(blocking: false)
end
+ # each existing group needs to have a `runners_token`.
+ # we do this on read since migrating all existing groups is not a feasible
+ # solution.
+ def runners_token
+ ensure_runners_token!
+ end
+
private
def update_two_factor_requirement
diff --git a/app/models/identity.rb b/app/models/identity.rb
index 1011b9f1109..3fd0c5e751d 100644
--- a/app/models/identity.rb
+++ b/app/models/identity.rb
@@ -1,12 +1,16 @@
class Identity < ActiveRecord::Base
+ def self.uniqueness_scope
+ :provider
+ end
+
include Sortable
include CaseSensitivity
belongs_to :user
validates :provider, presence: true
- validates :extern_uid, allow_blank: true, uniqueness: { scope: :provider, case_sensitive: false }
- validates :user_id, uniqueness: { scope: :provider }
+ validates :extern_uid, allow_blank: true, uniqueness: { scope: uniqueness_scope, case_sensitive: false }
+ validates :user_id, uniqueness: { scope: uniqueness_scope }
before_save :ensure_normalized_extern_uid, if: :extern_uid_changed?
after_destroy :clear_user_synced_attributes, if: :user_synced_attributes_metadata_from_provider?
diff --git a/app/models/lfs_object.rb b/app/models/lfs_object.rb
index 6b7f280fb70..84487031ee5 100644
--- a/app/models/lfs_object.rb
+++ b/app/models/lfs_object.rb
@@ -11,7 +11,7 @@ class LfsObject < ActiveRecord::Base
mount_uploader :file, LfsObjectUploader
- after_save :update_file_store
+ after_save :update_file_store, if: :file_changed?
def update_file_store
# The file.object_store is set during `uploader.store!`
diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb
index 8f964a488aa..628c61d5d69 100644
--- a/app/models/merge_request.rb
+++ b/app/models/merge_request.rb
@@ -323,7 +323,7 @@ class MergeRequest < ActiveRecord::Base
# updates `merge_jid` with the MergeWorker#jid.
# This helps tracking enqueued and ongoing merge jobs.
def merge_async(user_id, params)
- jid = MergeWorker.perform_async(id, user_id, params)
+ jid = MergeWorker.perform_async(id, user_id, params.to_h)
update_column(:merge_jid, jid)
end
@@ -1007,6 +1007,10 @@ class MergeRequest < ActiveRecord::Base
@merge_commit ||= project.commit(merge_commit_sha) if merge_commit_sha
end
+ def short_merge_commit_sha
+ Commit.truncate_sha(merge_commit_sha) if merge_commit_sha
+ end
+
def can_be_reverted?(current_user)
return false unless merge_commit
diff --git a/app/models/namespace.rb b/app/models/namespace.rb
index c29a53e5ce7..3dad4277713 100644
--- a/app/models/namespace.rb
+++ b/app/models/namespace.rb
@@ -21,6 +21,9 @@ class Namespace < ActiveRecord::Base
has_many :projects, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
has_many :project_statistics
+ has_many :runner_namespaces, class_name: 'Ci::RunnerNamespace'
+ has_many :runners, through: :runner_namespaces, source: :runner, class_name: 'Ci::Runner'
+
# This should _not_ be `inverse_of: :namespace`, because that would also set
# `user.namespace` when this user creates a group with themselves as `owner`.
belongs_to :owner, class_name: "User"
@@ -163,6 +166,13 @@ class Namespace < ActiveRecord::Base
projects.with_shared_runners.any?
end
+ # Returns all ancestors, self, and descendants of the current namespace.
+ def self_and_hierarchy
+ Gitlab::GroupHierarchy
+ .new(self.class.where(id: id))
+ .all_groups
+ end
+
# Returns all the ancestors of the current namespaces.
def ancestors
return self.class.none unless parent_id
diff --git a/app/models/note.rb b/app/models/note.rb
index e426f84832b..109405d3f17 100644
--- a/app/models/note.rb
+++ b/app/models/note.rb
@@ -317,10 +317,6 @@ class Note < ActiveRecord::Base
!system? && !for_snippet?
end
- def can_create_notification?
- true
- end
-
def discussion_class(noteable = nil)
# When commit notes are rendered on an MR's Discussion page, they are
# displayed in one discussion instead of individually.
diff --git a/app/models/project.rb b/app/models/project.rb
index d4e9e51c7be..32d34f5e9b8 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -22,6 +22,7 @@ class Project < ActiveRecord::Base
include DeploymentPlatform
include ::Gitlab::Utils::StrongMemoize
include ChronicDurationAttribute
+ include FastDestroyAll::Helpers
extend Gitlab::ConfigHelper
@@ -64,9 +65,15 @@ class Project < ActiveRecord::Base
default_value_for :only_allow_merge_if_all_discussions_are_resolved, false
add_authentication_token_field :runners_token
+
+ before_validation :mark_remote_mirrors_for_removal, if: -> { ActiveRecord::Base.connection.table_exists?(:remote_mirrors) }
+
before_save :ensure_runners_token
after_save :update_project_statistics, if: :namespace_id_changed?
+
+ after_save :create_import_state, if: ->(project) { project.import? && project.import_state.nil? }
+
after_create :create_project_feature, unless: :project_feature
after_create :create_ci_cd_settings,
@@ -78,6 +85,9 @@ class Project < ActiveRecord::Base
after_update :update_forks_visibility_level
before_destroy :remove_private_deploy_keys
+
+ use_fast_destroy :build_trace_chunks
+
after_destroy -> { run_after_commit { remove_pages } }
after_destroy :remove_exports
@@ -157,6 +167,8 @@ class Project < ActiveRecord::Base
has_one :fork_network_member
has_one :fork_network, through: :fork_network_member
+ has_one :import_state, autosave: true, class_name: 'ProjectImportState', inverse_of: :project
+
# Merge Requests for target project should be removed with it
has_many :merge_requests, foreign_key: 'target_project_id'
has_many :source_of_merge_requests, foreign_key: 'source_project_id', class_name: 'MergeRequest'
@@ -220,6 +232,7 @@ class Project < ActiveRecord::Base
# still using `dependent: :destroy` here.
has_many :builds, class_name: 'Ci::Build', inverse_of: :project, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
has_many :build_trace_section_names, class_name: 'Ci::BuildTraceSectionName'
+ has_many :build_trace_chunks, class_name: 'Ci::BuildTraceChunk', through: :builds, source: :trace_chunks
has_many :runner_projects, class_name: 'Ci::RunnerProject'
has_many :runners, through: :runner_projects, source: :runner, class_name: 'Ci::Runner'
has_many :variables, class_name: 'Ci::Variable'
@@ -230,23 +243,28 @@ class Project < ActiveRecord::Base
has_many :project_deploy_tokens
has_many :deploy_tokens, through: :project_deploy_tokens
- has_many :active_runners, -> { active }, through: :runner_projects, source: :runner, class_name: 'Ci::Runner'
-
has_one :auto_devops, class_name: 'ProjectAutoDevops'
has_many :custom_attributes, class_name: 'ProjectCustomAttribute'
has_many :project_badges, class_name: 'ProjectBadge'
- has_one :ci_cd_settings, class_name: 'ProjectCiCdSetting'
+ has_one :ci_cd_settings, class_name: 'ProjectCiCdSetting', inverse_of: :project, autosave: true, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
+
+ has_many :remote_mirrors, inverse_of: :project
accepts_nested_attributes_for :variables, allow_destroy: true
accepts_nested_attributes_for :project_feature, update_only: true
accepts_nested_attributes_for :import_data
accepts_nested_attributes_for :auto_devops, update_only: true
+ accepts_nested_attributes_for :remote_mirrors,
+ allow_destroy: true,
+ reject_if: ->(attrs) { attrs[:id].blank? && attrs[:url].blank? }
+
delegate :name, to: :owner, allow_nil: true, prefix: true
delegate :members, to: :team, prefix: true
delegate :add_user, :add_users, to: :team
delegate :add_guest, :add_reporter, :add_developer, :add_master, :add_role, to: :team
+ delegate :group_runners_enabled, :group_runners_enabled=, :group_runners_enabled?, to: :ci_cd_settings
# Validations
validates :creator, presence: true, on: :create
@@ -331,6 +349,12 @@ class Project < ActiveRecord::Base
scope :with_issues_enabled, -> { with_feature_enabled(:issues) }
scope :with_issues_available_for_user, ->(current_user) { with_feature_available_for_user(:issues, current_user) }
scope :with_merge_requests_enabled, -> { with_feature_enabled(:merge_requests) }
+ scope :with_remote_mirrors, -> { joins(:remote_mirrors).where(remote_mirrors: { enabled: true }).distinct }
+
+ scope :with_group_runners_enabled, -> do
+ joins(:ci_cd_settings)
+ .where(project_ci_cd_settings: { group_runners_enabled: true })
+ end
enum auto_cancel_pending_pipelines: { disabled: 0, enabled: 1 }
@@ -381,55 +405,9 @@ class Project < ActiveRecord::Base
scope :abandoned, -> { where('projects.last_activity_at < ?', 6.months.ago) }
scope :excluding_project, ->(project) { where.not(id: project) }
- scope :import_started, -> { where(import_status: 'started') }
-
- state_machine :import_status, initial: :none do
- event :import_schedule do
- transition [:none, :finished, :failed] => :scheduled
- end
-
- event :force_import_start do
- transition [:none, :finished, :failed] => :started
- end
-
- event :import_start do
- transition scheduled: :started
- end
-
- event :import_finish do
- transition started: :finished
- end
-
- event :import_fail do
- transition [:scheduled, :started] => :failed
- end
-
- event :import_retry do
- transition failed: :started
- end
-
- state :scheduled
- state :started
- state :finished
- state :failed
-
- after_transition [:none, :finished, :failed] => :scheduled do |project, _|
- project.run_after_commit do
- job_id = add_import_job
- update(import_jid: job_id) if job_id
- end
- end
- after_transition started: :finished do |project, _|
- project.reset_cache_and_import_attrs
-
- if Gitlab::ImportSources.importer_names.include?(project.import_type) && project.repo_exists?
- project.run_after_commit do
- Projects::AfterImportService.new(project).execute
- end
- end
- end
- end
+ scope :joins_import_state, -> { joins("LEFT JOIN project_mirror_data import_state ON import_state.project_id = projects.id") }
+ scope :import_started, -> { joins_import_state.where("import_state.status = 'started' OR projects.import_status = 'started'") }
class << self
# Searches for a list of projects based on the query given in `query`.
@@ -659,10 +637,6 @@ class Project < ActiveRecord::Base
external_import? || forked? || gitlab_project_import? || bare_repository_import?
end
- def no_import?
- import_status == 'none'
- end
-
def external_import?
import_url.present?
end
@@ -675,6 +649,99 @@ class Project < ActiveRecord::Base
import_started? || import_scheduled?
end
+ def import_state_args
+ {
+ status: self[:import_status],
+ jid: self[:import_jid],
+ last_error: self[:import_error]
+ }
+ end
+
+ def ensure_import_state(force: false)
+ return if !force && (self[:import_status] == 'none' || self[:import_status].nil?)
+ return unless import_state.nil?
+
+ if persisted?
+ create_import_state(import_state_args)
+
+ update_column(:import_status, 'none')
+ else
+ build_import_state(import_state_args)
+
+ self[:import_status] = 'none'
+ end
+ end
+
+ def import_schedule
+ ensure_import_state(force: true)
+
+ import_state.schedule
+ end
+
+ def force_import_start
+ ensure_import_state(force: true)
+
+ import_state.force_start
+ end
+
+ def import_start
+ ensure_import_state(force: true)
+
+ import_state.start
+ end
+
+ def import_fail
+ ensure_import_state(force: true)
+
+ import_state.fail_op
+ end
+
+ def import_finish
+ ensure_import_state(force: true)
+
+ import_state.finish
+ end
+
+ def import_jid=(new_jid)
+ ensure_import_state(force: true)
+
+ import_state.jid = new_jid
+ end
+
+ def import_jid
+ ensure_import_state
+
+ import_state&.jid
+ end
+
+ def import_error=(new_error)
+ ensure_import_state(force: true)
+
+ import_state.last_error = new_error
+ end
+
+ def import_error
+ ensure_import_state
+
+ import_state&.last_error
+ end
+
+ def import_status=(new_status)
+ ensure_import_state(force: true)
+
+ import_state.status = new_status
+ end
+
+ def import_status
+ ensure_import_state
+
+ import_state&.status || 'none'
+ end
+
+ def no_import?
+ import_status == 'none'
+ end
+
def import_started?
# import? does SQL work so only run it if it looks like there's an import running
import_status == 'started' && import?
@@ -708,6 +775,37 @@ class Project < ActiveRecord::Base
import_type == 'gitea'
end
+ def has_remote_mirror?
+ remote_mirror_available? && remote_mirrors.enabled.exists?
+ end
+
+ def updating_remote_mirror?
+ remote_mirrors.enabled.started.exists?
+ end
+
+ def update_remote_mirrors
+ return unless remote_mirror_available?
+
+ remote_mirrors.enabled.each(&:sync)
+ end
+
+ def mark_stuck_remote_mirrors_as_failed!
+ remote_mirrors.stuck.update_all(
+ update_status: :failed,
+ last_error: 'The remote mirror took to long to complete.',
+ last_update_at: Time.now
+ )
+ end
+
+ def mark_remote_mirrors_for_removal
+ remote_mirrors.each(&:mark_for_delete_if_blank_url)
+ end
+
+ def remote_mirror_available?
+ remote_mirror_available_overridden ||
+ ::Gitlab::CurrentSettings.mirror_available
+ end
+
def check_limit
unless creator.can_create_project? || namespace.kind == 'group'
projects_limit = creator.projects_limit
@@ -1301,12 +1399,17 @@ class Project < ActiveRecord::Base
@shared_runners ||= shared_runners_available? ? Ci::Runner.shared : Ci::Runner.none
end
- def active_shared_runners
- @active_shared_runners ||= shared_runners.active
+ def group_runners
+ @group_runners ||= group_runners_enabled? ? Ci::Runner.belonging_to_parent_group_of_project(self.id) : Ci::Runner.none
+ end
+
+ def all_runners
+ union = Gitlab::SQL::Union.new([runners, group_runners, shared_runners])
+ Ci::Runner.from("(#{union.to_sql}) ci_runners")
end
def any_runners?(&block)
- active_runners.any?(&block) || active_shared_runners.any?(&block)
+ all_runners.active.any?(&block)
end
def valid_runners_token?(token)
@@ -1471,7 +1574,7 @@ class Project < ActiveRecord::Base
def rename_repo_notify!
# When we import a project overwriting the original project, there
# is a move operation. In that case we don't want to send the instructions.
- send_move_instructions(full_path_was) unless started?
+ send_move_instructions(full_path_was) unless import_started?
expires_full_path_cache
self.old_path_with_namespace = full_path_was
@@ -1525,7 +1628,8 @@ class Project < ActiveRecord::Base
return unless import_jid
Gitlab::SidekiqStatus.unset(import_jid)
- update_column(:import_jid, nil)
+
+ import_state.update_column(:jid, nil)
end
def running_or_pending_build_count(force: false)
@@ -1544,7 +1648,8 @@ class Project < ActiveRecord::Base
sanitized_message = Gitlab::UrlSanitizer.sanitize(error_message)
import_fail
- update_column(:import_error, sanitized_message)
+
+ import_state.update_column(:last_error, sanitized_message)
rescue ActiveRecord::ActiveRecordError => e
Rails.logger.error("Error setting import status to failed: #{e.message}. Original error: #{sanitized_message}")
ensure
@@ -1874,6 +1979,10 @@ class Project < ActiveRecord::Base
[]
end
+ def toggle_ci_cd_settings!(settings_attribute)
+ ci_cd_settings.toggle!(settings_attribute)
+ end
+
def gitlab_deploy_token
@gitlab_deploy_token ||= deploy_tokens.gitlab_deploy_token
end
diff --git a/app/models/project_ci_cd_setting.rb b/app/models/project_ci_cd_setting.rb
index 9f10a93148c..588cced5781 100644
--- a/app/models/project_ci_cd_setting.rb
+++ b/app/models/project_ci_cd_setting.rb
@@ -1,5 +1,5 @@
class ProjectCiCdSetting < ActiveRecord::Base
- belongs_to :project
+ belongs_to :project, inverse_of: :ci_cd_settings
# The version of the schema that first introduced this model/table.
MINIMUM_SCHEMA_VERSION = 20180403035759
diff --git a/app/models/project_import_state.rb b/app/models/project_import_state.rb
new file mode 100644
index 00000000000..1605317ae14
--- /dev/null
+++ b/app/models/project_import_state.rb
@@ -0,0 +1,55 @@
+class ProjectImportState < ActiveRecord::Base
+ include AfterCommitQueue
+
+ self.table_name = "project_mirror_data"
+
+ belongs_to :project, inverse_of: :import_state
+
+ validates :project, presence: true
+
+ state_machine :status, initial: :none do
+ event :schedule do
+ transition [:none, :finished, :failed] => :scheduled
+ end
+
+ event :force_start do
+ transition [:none, :finished, :failed] => :started
+ end
+
+ event :start do
+ transition scheduled: :started
+ end
+
+ event :finish do
+ transition started: :finished
+ end
+
+ event :fail_op do
+ transition [:scheduled, :started] => :failed
+ end
+
+ state :scheduled
+ state :started
+ state :finished
+ state :failed
+
+ after_transition [:none, :finished, :failed] => :scheduled do |state, _|
+ state.run_after_commit do
+ job_id = project.add_import_job
+ update(jid: job_id) if job_id
+ end
+ end
+
+ after_transition started: :finished do |state, _|
+ project = state.project
+
+ project.reset_cache_and_import_attrs
+
+ if Gitlab::ImportSources.importer_names.include?(project.import_type) && project.repo_exists?
+ state.run_after_commit do
+ Projects::AfterImportService.new(project).execute
+ end
+ end
+ end
+ end
+end
diff --git a/app/models/remote_mirror.rb b/app/models/remote_mirror.rb
new file mode 100644
index 00000000000..bbf8fd9c6a7
--- /dev/null
+++ b/app/models/remote_mirror.rb
@@ -0,0 +1,219 @@
+class RemoteMirror < ActiveRecord::Base
+ include AfterCommitQueue
+
+ PROTECTED_BACKOFF_DELAY = 1.minute
+ UNPROTECTED_BACKOFF_DELAY = 5.minutes
+
+ attr_encrypted :credentials,
+ key: Gitlab::Application.secrets.db_key_base,
+ marshal: true,
+ encode: true,
+ mode: :per_attribute_iv_and_salt,
+ insecure_mode: true,
+ algorithm: 'aes-256-cbc'
+
+ default_value_for :only_protected_branches, true
+
+ belongs_to :project, inverse_of: :remote_mirrors
+
+ validates :url, presence: true, url: { protocols: %w(ssh git http https), allow_blank: true }
+ validates :url, addressable_url: true, if: :url_changed?
+
+ before_save :set_new_remote_name, if: :mirror_url_changed?
+
+ after_save :set_override_remote_mirror_available, unless: -> { Gitlab::CurrentSettings.current_application_settings.mirror_available }
+ after_save :refresh_remote, if: :mirror_url_changed?
+ after_update :reset_fields, if: :mirror_url_changed?
+
+ after_commit :remove_remote, on: :destroy
+
+ scope :enabled, -> { where(enabled: true) }
+ scope :started, -> { with_update_status(:started) }
+ scope :stuck, -> { started.where('last_update_at < ? OR (last_update_at IS NULL AND updated_at < ?)', 1.day.ago, 1.day.ago) }
+
+ state_machine :update_status, initial: :none do
+ event :update_start do
+ transition [:none, :finished, :failed] => :started
+ end
+
+ event :update_finish do
+ transition started: :finished
+ end
+
+ event :update_fail do
+ transition started: :failed
+ end
+
+ state :started
+ state :finished
+ state :failed
+
+ after_transition any => :started do |remote_mirror, _|
+ Gitlab::Metrics.add_event(:remote_mirrors_running, path: remote_mirror.project.full_path)
+
+ remote_mirror.update(last_update_started_at: Time.now)
+ end
+
+ after_transition started: :finished do |remote_mirror, _|
+ Gitlab::Metrics.add_event(:remote_mirrors_finished, path: remote_mirror.project.full_path)
+
+ timestamp = Time.now
+ remote_mirror.update_attributes!(
+ last_update_at: timestamp, last_successful_update_at: timestamp, last_error: nil
+ )
+ end
+
+ after_transition started: :failed do |remote_mirror, _|
+ Gitlab::Metrics.add_event(:remote_mirrors_failed, path: remote_mirror.project.full_path)
+
+ remote_mirror.update(last_update_at: Time.now)
+ end
+ end
+
+ def remote_name
+ super || fallback_remote_name
+ end
+
+ def update_failed?
+ update_status == 'failed'
+ end
+
+ def update_in_progress?
+ update_status == 'started'
+ end
+
+ def update_repository(options)
+ raw.update(options)
+ end
+
+ def sync?
+ enabled?
+ end
+
+ def sync
+ return unless sync?
+
+ if recently_scheduled?
+ RepositoryUpdateRemoteMirrorWorker.perform_in(backoff_delay, self.id, Time.now)
+ else
+ RepositoryUpdateRemoteMirrorWorker.perform_async(self.id, Time.now)
+ end
+ end
+
+ def enabled
+ return false unless project && super
+ return false unless project.remote_mirror_available?
+ return false unless project.repository_exists?
+ return false if project.pending_delete?
+
+ true
+ end
+ alias_method :enabled?, :enabled
+
+ def updated_since?(timestamp)
+ last_update_started_at && last_update_started_at > timestamp && !update_failed?
+ end
+
+ def mark_for_delete_if_blank_url
+ mark_for_destruction if url.blank?
+ end
+
+ def mark_as_failed(error_message)
+ update_fail
+ update_column(:last_error, Gitlab::UrlSanitizer.sanitize(error_message))
+ end
+
+ def url=(value)
+ super(value) && return unless Gitlab::UrlSanitizer.valid?(value)
+
+ mirror_url = Gitlab::UrlSanitizer.new(value)
+ self.credentials = mirror_url.credentials
+
+ super(mirror_url.sanitized_url)
+ end
+
+ def url
+ if super
+ Gitlab::UrlSanitizer.new(super, credentials: credentials).full_url
+ end
+ rescue
+ super
+ end
+
+ def safe_url
+ return if url.nil?
+
+ result = URI.parse(url)
+ result.password = '*****' if result.password
+ result.user = '*****' if result.user && result.user != "git" # tokens or other data may be saved as user
+ result.to_s
+ end
+
+ private
+
+ def raw
+ @raw ||= Gitlab::Git::RemoteMirror.new(project.repository.raw, remote_name)
+ end
+
+ def fallback_remote_name
+ return unless id
+
+ "remote_mirror_#{id}"
+ end
+
+ def recently_scheduled?
+ return false unless self.last_update_started_at
+
+ self.last_update_started_at >= Time.now - backoff_delay
+ end
+
+ def backoff_delay
+ if self.only_protected_branches
+ PROTECTED_BACKOFF_DELAY
+ else
+ UNPROTECTED_BACKOFF_DELAY
+ end
+ end
+
+ def reset_fields
+ update_columns(
+ last_error: nil,
+ last_update_at: nil,
+ last_successful_update_at: nil,
+ update_status: 'finished'
+ )
+ end
+
+ def set_override_remote_mirror_available
+ enabled = read_attribute(:enabled)
+
+ project.update(remote_mirror_available_overridden: enabled)
+ end
+
+ def set_new_remote_name
+ self.remote_name = "remote_mirror_#{SecureRandom.hex}"
+ end
+
+ def refresh_remote
+ return unless project
+
+ # Before adding a new remote we have to delete the data from
+ # the previous remote name
+ prev_remote_name = remote_name_was || fallback_remote_name
+ run_after_commit do
+ project.repository.async_remove_remote(prev_remote_name)
+ end
+
+ project.repository.add_remote(remote_name, url)
+ end
+
+ def remove_remote
+ return unless project # could be pending to delete so don't need to touch the git repository
+
+ project.repository.async_remove_remote(remote_name)
+ end
+
+ def mirror_url_changed?
+ url_changed? || encrypted_credentials_changed?
+ end
+end
diff --git a/app/models/repository.rb b/app/models/repository.rb
index 6831305fb93..b75c4aca982 100644
--- a/app/models/repository.rb
+++ b/app/models/repository.rb
@@ -854,13 +854,27 @@ class Repository
add_remote(remote_name, url, mirror_refmap: refmap)
fetch_remote(remote_name, forced: forced, prune: prune)
ensure
- remove_remote(remote_name) if tmp_remote_name
+ async_remove_remote(remote_name) if tmp_remote_name
end
def fetch_remote(remote, forced: false, ssh_auth: nil, no_tags: false, prune: true)
gitlab_shell.fetch_remote(raw_repository, remote, ssh_auth: ssh_auth, forced: forced, no_tags: no_tags, prune: prune)
end
+ def async_remove_remote(remote_name)
+ return unless remote_name
+
+ job_id = RepositoryRemoveRemoteWorker.perform_async(project.id, remote_name)
+
+ if job_id
+ Rails.logger.info("Remove remote job scheduled for #{project.id} with remote name: #{remote_name} job ID #{job_id}.")
+ else
+ Rails.logger.info("Remove remote job failed to create for #{project.id} with remote name #{remote_name}.")
+ end
+
+ job_id
+ end
+
def fetch_source_branch!(source_repository, source_branch, local_ref)
raw_repository.fetch_source_branch!(source_repository.raw_repository, source_branch, local_ref)
end
diff --git a/app/models/sent_notification.rb b/app/models/sent_notification.rb
index 6e311806be1..3da7c301d28 100644
--- a/app/models/sent_notification.rb
+++ b/app/models/sent_notification.rb
@@ -5,14 +5,14 @@ class SentNotification < ActiveRecord::Base
belongs_to :noteable, polymorphic: true # rubocop:disable Cop/PolymorphicAssociations
belongs_to :recipient, class_name: "User"
- validates :project, :recipient, presence: true
+ validates :recipient, presence: true
validates :reply_key, presence: true, uniqueness: true
validates :noteable_id, presence: true, unless: :for_commit?
validates :commit_id, presence: true, if: :for_commit?
validates :in_reply_to_discussion_id, format: { with: /\A\h{40}\z/, allow_nil: true }
validate :note_valid
- after_save :keep_around_commit
+ after_save :keep_around_commit, if: :for_commit?
class << self
def reply_key
diff --git a/app/models/system_note_metadata.rb b/app/models/system_note_metadata.rb
index 29035480371..1c2161accc4 100644
--- a/app/models/system_note_metadata.rb
+++ b/app/models/system_note_metadata.rb
@@ -17,7 +17,11 @@ class SystemNoteMetadata < ActiveRecord::Base
].freeze
validates :note, presence: true
- validates :action, inclusion: ICON_TYPES, allow_nil: true
+ validates :action, inclusion: { in: :icon_types }, allow_nil: true
belongs_to :note
+
+ def icon_types
+ ICON_TYPES
+ end
end
diff --git a/app/models/term_agreement.rb b/app/models/term_agreement.rb
new file mode 100644
index 00000000000..8458a231bbd
--- /dev/null
+++ b/app/models/term_agreement.rb
@@ -0,0 +1,6 @@
+class TermAgreement < ActiveRecord::Base
+ belongs_to :term, class_name: 'ApplicationSetting::Term'
+ belongs_to :user
+
+ validates :user, :term, presence: true
+end
diff --git a/app/models/user.rb b/app/models/user.rb
index b0668148972..a9cfd39f604 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -138,6 +138,8 @@ class User < ActiveRecord::Base
has_many :custom_attributes, class_name: 'UserCustomAttribute'
has_many :callouts, class_name: 'UserCallout'
has_many :uploads, as: :model, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
+ has_many :term_agreements
+ belongs_to :accepted_term, class_name: 'ApplicationSetting::Term'
#
# Validations
@@ -910,7 +912,7 @@ class User < ActiveRecord::Base
def delete_async(deleted_by:, params: {})
block if params[:hard_delete]
- DeleteUserWorker.perform_async(deleted_by.id, id, params)
+ DeleteUserWorker.perform_async(deleted_by.id, id, params.to_h)
end
def notification_service
@@ -1187,6 +1189,10 @@ class User < ActiveRecord::Base
max_member_access_for_group_ids([group_id])[group_id]
end
+ def terms_accepted?
+ accepted_term_id.present?
+ end
+
protected
# override, from Devise::Validatable
diff --git a/app/models/user_callout.rb b/app/models/user_callout.rb
index e4b69382626..9d461c6750a 100644
--- a/app/models/user_callout.rb
+++ b/app/models/user_callout.rb
@@ -2,7 +2,8 @@ class UserCallout < ActiveRecord::Base
belongs_to :user
enum feature_name: {
- gke_cluster_integration: 1
+ gke_cluster_integration: 1,
+ gcp_signup_offer: 2
}
validates :user, presence: true
diff --git a/app/policies/application_setting/term_policy.rb b/app/policies/application_setting/term_policy.rb
new file mode 100644
index 00000000000..f03bf748c76
--- /dev/null
+++ b/app/policies/application_setting/term_policy.rb
@@ -0,0 +1,28 @@
+class ApplicationSetting
+ class TermPolicy < BasePolicy
+ include Gitlab::Utils::StrongMemoize
+
+ condition(:current_terms, scope: :subject) do
+ Gitlab::CurrentSettings.current_application_settings.latest_terms == @subject
+ end
+
+ condition(:terms_accepted, score: 1) do
+ agreement&.accepted
+ end
+
+ rule { ~anonymous & current_terms }.policy do
+ enable :accept_terms
+ enable :decline_terms
+ end
+
+ rule { terms_accepted }.prevent :accept_terms
+
+ def agreement
+ strong_memoize(:agreement) do
+ next nil if @user.nil? || @subject.nil?
+
+ @user.term_agreements.find_by(term: @subject)
+ end
+ end
+ end
+end
diff --git a/app/policies/project_policy.rb b/app/policies/project_policy.rb
index 3529d0aa60c..5759b1a376f 100644
--- a/app/policies/project_policy.rb
+++ b/app/policies/project_policy.rb
@@ -80,6 +80,11 @@ class ProjectPolicy < BasePolicy
project.merge_requests_allowing_push_to_user(user).any?
end
+ with_scope :global
+ condition(:mirror_available, score: 0) do
+ ::Gitlab::CurrentSettings.current_application_settings.mirror_available
+ end
+
# We aren't checking `:read_issue` or `:read_merge_request` in this case
# because it could be possible for a user to see an issuable-iid
# (`:read_issue_iid` or `:read_merge_request_iid`) but then wouldn't be
@@ -246,6 +251,8 @@ class ProjectPolicy < BasePolicy
enable :create_cluster
end
+ rule { (mirror_available & can?(:admin_project)) | admin }.enable :admin_remote_mirror
+
rule { archived }.policy do
prevent :push_code
prevent :push_to_delete_protected_branch
diff --git a/app/policies/user_policy.rb b/app/policies/user_policy.rb
index 0905ddd9b38..ee219f0a0d0 100644
--- a/app/policies/user_policy.rb
+++ b/app/policies/user_policy.rb
@@ -8,6 +8,8 @@ class UserPolicy < BasePolicy
rule { ~restricted_public_level }.enable :read_user
rule { ~anonymous }.enable :read_user
- rule { user_is_self | admin }.enable :destroy_user
- rule { subject_ghost }.prevent :destroy_user
+ rule { ~subject_ghost & (user_is_self | admin) }.policy do
+ enable :destroy_user
+ enable :update_user
+ end
end
diff --git a/app/presenters/ci/pipeline_presenter.rb b/app/presenters/ci/pipeline_presenter.rb
index 099b4720fb6..cc2bce9862d 100644
--- a/app/presenters/ci/pipeline_presenter.rb
+++ b/app/presenters/ci/pipeline_presenter.rb
@@ -1,11 +1,21 @@
module Ci
class PipelinePresenter < Gitlab::View::Presenter::Delegated
+ include Gitlab::Utils::StrongMemoize
+
FAILURE_REASONS = {
config_error: 'CI/CD YAML configuration error!'
}.freeze
presents :pipeline
+ def failed_builds
+ return [] unless can?(current_user, :read_build, pipeline)
+
+ strong_memoize(:failed_builds) do
+ pipeline.builds.latest.failed
+ end
+ end
+
def failure_reason
return unless pipeline.failure_reason?
diff --git a/app/serializers/merge_request_widget_entity.rb b/app/serializers/merge_request_widget_entity.rb
index 4a812e39ee1..d0165c148eb 100644
--- a/app/serializers/merge_request_widget_entity.rb
+++ b/app/serializers/merge_request_widget_entity.rb
@@ -2,6 +2,7 @@ class MergeRequestWidgetEntity < IssuableEntity
expose :state
expose :in_progress_merge_commit_sha
expose :merge_commit_sha
+ expose :short_merge_commit_sha
expose :merge_error
expose :merge_params
expose :merge_status
@@ -207,6 +208,12 @@ class MergeRequestWidgetEntity < IssuableEntity
commit_change_content_project_merge_request_path(merge_request.project, merge_request)
end
+ expose :merge_commit_path do |merge_request|
+ if merge_request.merge_commit_sha
+ project_commit_path(merge_request.project, merge_request.merge_commit_sha)
+ end
+ end
+
private
delegate :current_user, to: :request
diff --git a/app/serializers/project_mirror_entity.rb b/app/serializers/project_mirror_entity.rb
new file mode 100644
index 00000000000..a9c08ac021a
--- /dev/null
+++ b/app/serializers/project_mirror_entity.rb
@@ -0,0 +1,11 @@
+class ProjectMirrorEntity < Grape::Entity
+ expose :id
+
+ expose :remote_mirrors_attributes do |project|
+ next [] unless project.remote_mirrors.present?
+
+ project.remote_mirrors.map do |remote|
+ remote.as_json(only: %i[id url enabled])
+ end
+ end
+end
diff --git a/app/serializers/stage_entity.rb b/app/serializers/stage_entity.rb
index 4523b15152e..2516df70ad9 100644
--- a/app/serializers/stage_entity.rb
+++ b/app/serializers/stage_entity.rb
@@ -11,6 +11,12 @@ class StageEntity < Grape::Entity
if: -> (_, opts) { opts[:grouped] },
with: JobGroupEntity
+ expose :latest_statuses,
+ if: -> (_, opts) { opts[:details] },
+ with: JobEntity do |stage|
+ latest_statuses
+ end
+
expose :detailed_status, as: :status, with: StatusEntity
expose :path do |stage|
@@ -35,4 +41,14 @@ class StageEntity < Grape::Entity
def detailed_status
stage.detailed_status(request.current_user)
end
+
+ def grouped_statuses
+ @grouped_statuses ||= stage.statuses.latest_ordered.group_by(&:status)
+ end
+
+ def latest_statuses
+ HasStatus::ORDERED_STATUSES.map do |ordered_status|
+ grouped_statuses.fetch(ordered_status, [])
+ end.flatten
+ end
end
diff --git a/app/serializers/stage_serializer.rb b/app/serializers/stage_serializer.rb
new file mode 100644
index 00000000000..091d8e91e43
--- /dev/null
+++ b/app/serializers/stage_serializer.rb
@@ -0,0 +1,7 @@
+class StageSerializer < BaseSerializer
+ include WithPagination
+
+ InvalidResourceError = Class.new(StandardError)
+
+ entity StageEntity
+end
diff --git a/app/services/application_settings/update_service.rb b/app/services/application_settings/update_service.rb
index 61589a07250..d6d3a661dab 100644
--- a/app/services/application_settings/update_service.rb
+++ b/app/services/application_settings/update_service.rb
@@ -1,7 +1,22 @@
module ApplicationSettings
class UpdateService < ApplicationSettings::BaseService
def execute
+ update_terms(@params.delete(:terms))
+
@application_setting.update(@params)
end
+
+ private
+
+ def update_terms(terms)
+ return unless terms.present?
+
+ # Avoid creating a new terms record if the text is exactly the same.
+ terms = terms.strip
+ return if terms == @application_setting.terms
+
+ ApplicationSetting::Term.create(terms: terms)
+ @application_setting.reset_memoized_terms
+ end
end
end
diff --git a/app/services/ci/create_pipeline_service.rb b/app/services/ci/create_pipeline_service.rb
index 6ce86983287..17a53b6a8fd 100644
--- a/app/services/ci/create_pipeline_service.rb
+++ b/app/services/ci/create_pipeline_service.rb
@@ -24,6 +24,7 @@ module Ci
ignore_skip_ci: ignore_skip_ci,
save_incompleted: save_on_errors,
seeds_block: block,
+ variables_attributes: params[:variables_attributes],
project: project,
current_user: current_user)
diff --git a/app/services/ci/ensure_stage_service.rb b/app/services/ci/ensure_stage_service.rb
index 87f19b333de..b8c7be2d350 100644
--- a/app/services/ci/ensure_stage_service.rb
+++ b/app/services/ci/ensure_stage_service.rb
@@ -42,6 +42,7 @@ module Ci
def create_stage
Ci::Stage.create!(name: @build.stage,
+ position: @build.stage_idx,
pipeline: @build.pipeline,
project: @build.project)
end
diff --git a/app/services/ci/register_job_service.rb b/app/services/ci/register_job_service.rb
index 0b087ad73da..4291631913a 100644
--- a/app/services/ci/register_job_service.rb
+++ b/app/services/ci/register_job_service.rb
@@ -17,8 +17,10 @@ module Ci
builds =
if runner.shared?
builds_for_shared_runner
+ elsif runner.group_type?
+ builds_for_group_runner
else
- builds_for_specific_runner
+ builds_for_project_runner
end
valid = true
@@ -75,15 +77,24 @@ module Ci
.joins('LEFT JOIN project_features ON ci_builds.project_id = project_features.project_id')
.where('project_features.builds_access_level IS NULL or project_features.builds_access_level > 0').
- # Implement fair scheduling
- # this returns builds that are ordered by number of running builds
- # we prefer projects that don't use shared runners at all
- joins("LEFT JOIN (#{running_builds_for_shared_runners.to_sql}) AS project_builds ON ci_builds.project_id=project_builds.project_id")
+ # Implement fair scheduling
+ # this returns builds that are ordered by number of running builds
+ # we prefer projects that don't use shared runners at all
+ joins("LEFT JOIN (#{running_builds_for_shared_runners.to_sql}) AS project_builds ON ci_builds.project_id=project_builds.project_id")
.order('COALESCE(project_builds.running_builds, 0) ASC', 'ci_builds.id ASC')
end
- def builds_for_specific_runner
- new_builds.where(project: runner.projects.without_deleted.with_builds_enabled).order('created_at ASC')
+ def builds_for_project_runner
+ new_builds.where(project: runner.projects.without_deleted.with_builds_enabled).order('id ASC')
+ end
+
+ def builds_for_group_runner
+ hierarchy_groups = Gitlab::GroupHierarchy.new(runner.groups).base_and_descendants
+ projects = Project.where(namespace_id: hierarchy_groups)
+ .with_group_runners_enabled
+ .with_builds_enabled
+ .without_deleted
+ new_builds.where(project: projects).order('id ASC')
end
def running_builds_for_shared_runners
@@ -97,10 +108,6 @@ module Ci
builds
end
- def shared_runner_build_limits_feature_enabled?
- ENV['DISABLE_SHARED_RUNNER_BUILD_MINUTES_LIMIT'].to_s != 'true'
- end
-
def register_failure
failed_attempt_counter.increment
attempt_counter.increment
diff --git a/app/services/ci/update_build_queue_service.rb b/app/services/ci/update_build_queue_service.rb
index 152c8ae5006..41b1c144c3e 100644
--- a/app/services/ci/update_build_queue_service.rb
+++ b/app/services/ci/update_build_queue_service.rb
@@ -1,18 +1,14 @@
module Ci
class UpdateBuildQueueService
def execute(build)
- build.project.runners.each do |runner|
- if runner.can_pick?(build)
- runner.tick_runner_queue
- end
- end
+ tick_for(build, build.project.all_runners)
+ end
- return unless build.project.shared_runners_enabled?
+ private
- Ci::Runner.shared.each do |runner|
- if runner.can_pick?(build)
- runner.tick_runner_queue
- end
+ def tick_for(build, runners)
+ runners.each do |runner|
+ runner.pick_build!(build)
end
end
end
diff --git a/app/services/concerns/exclusive_lease_guard.rb b/app/services/concerns/exclusive_lease_guard.rb
new file mode 100644
index 00000000000..30be6accc32
--- /dev/null
+++ b/app/services/concerns/exclusive_lease_guard.rb
@@ -0,0 +1,52 @@
+#
+# Concern that helps with getting an exclusive lease for running a block
+# of code.
+#
+# `#try_obtain_lease` takes a block which will be run if it was able to
+# obtain the lease. Implement `#lease_timeout` to configure the timeout
+# for the exclusive lease. Optionally override `#lease_key` to set the
+# lease key, it defaults to the class name with underscores.
+#
+module ExclusiveLeaseGuard
+ extend ActiveSupport::Concern
+
+ def try_obtain_lease
+ lease = exclusive_lease.try_obtain
+
+ unless lease
+ log_error('Cannot obtain an exclusive lease. There must be another instance already in execution.')
+ return
+ end
+
+ begin
+ yield lease
+ ensure
+ release_lease(lease)
+ end
+ end
+
+ def exclusive_lease
+ @lease ||= Gitlab::ExclusiveLease.new(lease_key, timeout: lease_timeout)
+ end
+
+ def lease_key
+ @lease_key ||= self.class.name.underscore
+ end
+
+ def lease_timeout
+ raise NotImplementedError,
+ "#{self.class.name} does not implement #{__method__}"
+ end
+
+ def release_lease(uuid)
+ Gitlab::ExclusiveLease.cancel(lease_key, uuid)
+ end
+
+ def renew_lease!
+ exclusive_lease.renew
+ end
+
+ def log_error(message, extra_args = {})
+ logger.error(message)
+ end
+end
diff --git a/app/services/concerns/users/participable_service.rb b/app/services/concerns/users/participable_service.rb
new file mode 100644
index 00000000000..bf60b96938d
--- /dev/null
+++ b/app/services/concerns/users/participable_service.rb
@@ -0,0 +1,41 @@
+module Users
+ module ParticipableService
+ extend ActiveSupport::Concern
+
+ included do
+ attr_reader :noteable
+ end
+
+ def noteable_owner
+ return [] unless noteable && noteable.author.present?
+
+ [as_hash(noteable.author)]
+ end
+
+ def participants_in_noteable
+ return [] unless noteable
+
+ users = noteable.participants(current_user)
+ sorted(users)
+ end
+
+ def sorted(users)
+ users.uniq.to_a.compact.sort_by(&:username).map do |user|
+ as_hash(user)
+ end
+ end
+
+ def groups
+ current_user.authorized_groups.sort_by(&:path).map do |group|
+ count = group.users.count
+ { username: group.full_path, name: group.full_name, count: count, avatar_url: group.avatar_url }
+ end
+ end
+
+ private
+
+ def as_hash(user)
+ { username: user.username, name: user.name, avatar_url: user.avatar_url }
+ end
+ end
+end
diff --git a/app/services/git_push_service.rb b/app/services/git_push_service.rb
index c037141fcde..f3bfc53dcd3 100644
--- a/app/services/git_push_service.rb
+++ b/app/services/git_push_service.rb
@@ -55,6 +55,7 @@ class GitPushService < BaseService
execute_related_hooks
perform_housekeeping
+ update_remote_mirrors
update_caches
update_signatures
@@ -119,6 +120,13 @@ class GitPushService < BaseService
protected
+ def update_remote_mirrors
+ return unless @project.has_remote_mirror?
+
+ @project.mark_stuck_remote_mirrors_as_failed!
+ @project.update_remote_mirrors
+ end
+
def execute_related_hooks
# Update merge requests that may be affected by this push. A new branch
# could cause the last commit of a merge request to change.
diff --git a/app/services/merge_requests/merge_service.rb b/app/services/merge_requests/merge_service.rb
index cedfcb50e09..2209a60a840 100644
--- a/app/services/merge_requests/merge_service.rb
+++ b/app/services/merge_requests/merge_service.rb
@@ -50,21 +50,30 @@ module MergeRequests
end
def commit
- message = params[:commit_message] || merge_request.merge_commit_message
-
log_info("Git merge started on JID #{merge_jid}")
- commit_id = repository.merge(current_user, source, merge_request, message)
- log_info("Git merge finished on JID #{merge_jid} commit #{commit_id}")
+ commit_id = try_merge
+
+ if commit_id
+ log_info("Git merge finished on JID #{merge_jid} commit #{commit_id}")
+ else
+ raise MergeError, 'Conflicts detected during merge'
+ end
- raise MergeError, 'Conflicts detected during merge' unless commit_id
+ merge_request.update!(merge_commit_sha: commit_id)
+ end
+
+ def try_merge
+ message = params[:commit_message] || merge_request.merge_commit_message
- merge_request.update(merge_commit_sha: commit_id)
+ repository.merge(current_user, source, merge_request, message)
rescue Gitlab::Git::HooksService::PreReceiveError => e
- raise MergeError, e.message
- rescue StandardError => e
- raise MergeError, "Something went wrong during merge: #{e.message}"
+ handle_merge_error(log_message: e.message)
+ raise MergeError, 'Something went wrong during merge pre-receive hook'
+ rescue => e
+ handle_merge_error(log_message: e.message)
+ raise MergeError, 'Something went wrong during merge'
ensure
- merge_request.update(in_progress_merge_commit_sha: nil)
+ merge_request.update!(in_progress_merge_commit_sha: nil)
end
def after_merge
diff --git a/app/services/notification_recipient_service.rb b/app/services/notification_recipient_service.rb
index 83e59a649b6..5658699664d 100644
--- a/app/services/notification_recipient_service.rb
+++ b/app/services/notification_recipient_service.rb
@@ -45,6 +45,10 @@ module NotificationRecipientService
target.project
end
+ def group
+ project&.group || target.try(:group)
+ end
+
def recipients
@recipients ||= []
end
@@ -67,6 +71,7 @@ module NotificationRecipientService
user, type,
reason: reason,
project: project,
+ group: group,
custom_action: custom_action,
target: target,
acting_user: acting_user
@@ -107,11 +112,11 @@ module NotificationRecipientService
# Users with a notification setting on group or project
user_ids += user_ids_notifiable_on(project, :custom)
- user_ids += user_ids_notifiable_on(project.group, :custom)
+ user_ids += user_ids_notifiable_on(group, :custom)
# Users with global level custom
user_ids_with_project_level_global = user_ids_notifiable_on(project, :global)
- user_ids_with_group_level_global = user_ids_notifiable_on(project.group, :global)
+ user_ids_with_group_level_global = user_ids_notifiable_on(group, :global)
global_users_ids = user_ids_with_project_level_global.concat(user_ids_with_group_level_global)
user_ids += user_ids_with_global_level_custom(global_users_ids, custom_action)
@@ -123,6 +128,10 @@ module NotificationRecipientService
add_recipients(project_watchers, :watch, nil)
end
+ def add_group_watchers
+ add_recipients(group_watchers, :watch, nil)
+ end
+
# Get project users with WATCH notification level
def project_watchers
project_members_ids = user_ids_notifiable_on(project)
@@ -138,6 +147,14 @@ module NotificationRecipientService
user_scope.where(id: user_ids_with_project_setting.concat(user_ids_with_group_setting).uniq)
end
+ def group_watchers
+ user_ids_with_group_global = user_ids_notifiable_on(group, :global)
+ user_ids = user_ids_with_global_level_watch(user_ids_with_group_global)
+ user_ids_with_group_setting = select_group_members_ids(group, [], user_ids_with_group_global, user_ids)
+
+ user_scope.where(id: user_ids_with_group_setting)
+ end
+
def add_subscribed_users
return unless target.respond_to? :subscribers
@@ -281,6 +298,14 @@ module NotificationRecipientService
note.project
end
+ def group
+ if note.for_project_noteable?
+ project.group
+ else
+ target.try(:group)
+ end
+ end
+
def build!
# Add all users participating in the thread (author, assignee, comment authors)
add_participants(note.author)
@@ -289,11 +314,11 @@ module NotificationRecipientService
if note.for_project_noteable?
# Merge project watchers
add_project_watchers
-
- # Merge project with custom notification
- add_custom_notifications
+ else
+ add_group_watchers
end
+ add_custom_notifications
add_subscribed_users
end
diff --git a/app/services/projects/create_service.rb b/app/services/projects/create_service.rb
index d361d070993..d16ecdb7b9b 100644
--- a/app/services/projects/create_service.rb
+++ b/app/services/projects/create_service.rb
@@ -142,7 +142,7 @@ module Projects
if @project
@project.errors.add(:base, message)
- @project.mark_import_as_failed(message) if @project.import?
+ @project.mark_import_as_failed(message) if @project.persisted? && @project.import?
end
@project
diff --git a/app/services/projects/participants_service.rb b/app/services/projects/participants_service.rb
index e6193fcacee..eb0472c6024 100644
--- a/app/services/projects/participants_service.rb
+++ b/app/services/projects/participants_service.rb
@@ -1,6 +1,6 @@
module Projects
class ParticipantsService < BaseService
- attr_reader :noteable
+ include Users::ParticipableService
def execute(noteable)
@noteable = noteable
@@ -10,36 +10,6 @@ module Projects
participants.uniq
end
- def noteable_owner
- return [] unless noteable && noteable.author.present?
-
- [{
- name: noteable.author.name,
- username: noteable.author.username,
- avatar_url: noteable.author.avatar_url
- }]
- end
-
- def participants_in_noteable
- return [] unless noteable
-
- users = noteable.participants(current_user)
- sorted(users)
- end
-
- def sorted(users)
- users.uniq.to_a.compact.sort_by(&:username).map do |user|
- { username: user.username, name: user.name, avatar_url: user.avatar_url }
- end
- end
-
- def groups
- current_user.authorized_groups.sort_by(&:path).map do |group|
- count = group.users.count
- { username: group.full_path, name: group.full_name, count: count, avatar_url: group.avatar_url }
- end
- end
-
def all_members
count = project.team.members.flatten.count
[{ username: "all", name: "All Project and Group Members", count: count }]
diff --git a/app/services/projects/update_remote_mirror_service.rb b/app/services/projects/update_remote_mirror_service.rb
new file mode 100644
index 00000000000..8183a2f26d7
--- /dev/null
+++ b/app/services/projects/update_remote_mirror_service.rb
@@ -0,0 +1,30 @@
+module Projects
+ class UpdateRemoteMirrorService < BaseService
+ attr_reader :errors
+
+ def execute(remote_mirror)
+ @errors = []
+
+ return success unless remote_mirror.enabled?
+
+ begin
+ repository.fetch_remote(remote_mirror.remote_name, no_tags: true)
+
+ opts = {}
+ if remote_mirror.only_protected_branches?
+ opts[:only_branches_matching] = project.protected_branches.select(:name).map(&:name)
+ end
+
+ remote_mirror.update_repository(opts)
+ rescue => e
+ errors << e.message.strip
+ end
+
+ if errors.present?
+ error(errors.join("\n\n"))
+ else
+ success
+ end
+ end
+ end
+end
diff --git a/app/services/users/respond_to_terms_service.rb b/app/services/users/respond_to_terms_service.rb
new file mode 100644
index 00000000000..06d660186cf
--- /dev/null
+++ b/app/services/users/respond_to_terms_service.rb
@@ -0,0 +1,24 @@
+module Users
+ class RespondToTermsService
+ def initialize(user, term)
+ @user, @term = user, term
+ end
+
+ def execute(accepted:)
+ agreement = @user.term_agreements.find_or_initialize_by(term: @term)
+ agreement.accepted = accepted
+
+ if agreement.save
+ store_accepted_term(accepted)
+ end
+
+ agreement
+ end
+
+ private
+
+ def store_accepted_term(accepted)
+ @user.update_column(:accepted_term_id, accepted ? @term.id : nil)
+ end
+ end
+end
diff --git a/app/services/web_hook_service.rb b/app/services/web_hook_service.rb
index 809ce1303d8..7ec52b6ce2b 100644
--- a/app/services/web_hook_service.rb
+++ b/app/services/web_hook_service.rb
@@ -41,7 +41,7 @@ class WebHookService
http_status: response.code,
message: response.to_s
}
- rescue SocketError, OpenSSL::SSL::SSLError, Errno::ECONNRESET, Errno::ECONNREFUSED, Errno::EHOSTUNREACH, Net::OpenTimeout, Net::ReadTimeout => e
+ rescue SocketError, OpenSSL::SSL::SSLError, Errno::ECONNRESET, Errno::ECONNREFUSED, Errno::EHOSTUNREACH, Net::OpenTimeout, Net::ReadTimeout, Gitlab::HTTP::BlockedUrlError => e
log_execution(
trigger: hook_name,
url: hook.url,
diff --git a/app/views/admin/application_settings/_repository_check.html.haml b/app/views/admin/application_settings/_repository_check.html.haml
index dfb82030962..739ed359e16 100644
--- a/app/views/admin/application_settings/_repository_check.html.haml
+++ b/app/views/admin/application_settings/_repository_check.html.haml
@@ -12,7 +12,7 @@
Enable Repository Checks
.form-text.text-muted
GitLab will periodically run
- %a{ href: 'https://www.kernel.org/pub/software/scm/git/docs/git-fsck.html', target: 'blank' } 'git fsck'
+ %a{ href: 'https://git-scm.com/docs/git-fsck', target: 'blank' } 'git fsck'
in all project and wiki repositories to look for silent disk corruption issues.
.form-group.row
.offset-sm-2.col-sm-10
diff --git a/app/views/admin/application_settings/_repository_mirrors_form.html.haml b/app/views/admin/application_settings/_repository_mirrors_form.html.haml
new file mode 100644
index 00000000000..09183ec6260
--- /dev/null
+++ b/app/views/admin/application_settings/_repository_mirrors_form.html.haml
@@ -0,0 +1,16 @@
+= form_for @application_setting, url: admin_application_settings_path, html: { class: 'form-horizontal fieldset-form' } do |f|
+ = form_errors(@application_setting)
+
+ %fieldset
+ .form-group
+ = f.label :mirror_available, 'Enable mirror configuration', class: 'control-label col-sm-2'
+ .col-sm-10
+ .checkbox
+ = f.label :mirror_available do
+ = f.check_box :mirror_available
+ Allow mirrors to be setup for projects
+ %span.help-block
+ If disabled, only admins will be able to setup mirrors in projects.
+ = link_to icon('question-circle'), help_page_path('workflow/repository_mirroring')
+
+ = f.submit 'Save changes', class: "btn btn-success"
diff --git a/app/views/admin/application_settings/_terms.html.haml b/app/views/admin/application_settings/_terms.html.haml
new file mode 100644
index 00000000000..724246ab7e7
--- /dev/null
+++ b/app/views/admin/application_settings/_terms.html.haml
@@ -0,0 +1,22 @@
+= form_for @application_setting, url: admin_application_settings_path, html: { class: 'form-horizontal fieldset-form' } do |f|
+ = form_errors(@application_setting)
+
+ %fieldset
+ .form-group
+ .col-sm-12
+ .checkbox
+ = f.label :enforce_terms do
+ = f.check_box :enforce_terms
+ = _("Require all users to accept Terms of Service when they access GitLab.")
+ .help-block
+ = _("When enabled, users cannot use GitLab until the terms have been accepted.")
+ .form-group
+ .col-sm-12
+ = f.label :terms do
+ = _("Terms of Service Agreement")
+ .col-sm-12
+ = f.text_area :terms, class: 'form-control', rows: 8
+ .help-block
+ = _("Markdown enabled")
+
+ = f.submit _("Save changes"), class: "btn btn-success"
diff --git a/app/views/admin/application_settings/show.html.haml b/app/views/admin/application_settings/show.html.haml
index caaa93aa1e2..3f440c76ee0 100644
--- a/app/views/admin/application_settings/show.html.haml
+++ b/app/views/admin/application_settings/show.html.haml
@@ -8,7 +8,7 @@
%h4
= _('Visibility and access controls')
%button.btn.js-settings-toggle{ type: 'button' }
- = expanded ? 'Collapse' : 'Expand'
+ = expanded ? _('Collapse') : _('Expand')
%p
= _('Set default and restrict visibility levels. Configure import sources and git access protocol.')
.settings-content
@@ -19,7 +19,7 @@
%h4
= _('Account and limit settings')
%button.btn.js-settings-toggle{ type: 'button' }
- = expanded ? 'Collapse' : 'Expand'
+ = expanded ? _('Collapse') : _('Expand')
%p
= _('Session expiration, projects limit and attachment size.')
.settings-content
@@ -30,7 +30,7 @@
%h4
= _('Sign-up restrictions')
%button.btn.js-settings-toggle{ type: 'button' }
- = expanded ? 'Collapse' : 'Expand'
+ = expanded ? _('Collapse') : _('Expand')
%p
= _('Configure the way a user creates a new account.')
.settings-content
@@ -41,18 +41,29 @@
%h4
= _('Sign-in restrictions')
%button.btn.js-settings-toggle{ type: 'button' }
- = expanded ? 'Collapse' : 'Expand'
+ = expanded ? _('Collapse') : _('Expand')
%p
= _('Set requirements for a user to sign-in. Enable mandatory two-factor authentication.')
.settings-content
= render 'signin'
+%section.settings.as-terms.no-animate#js-terms-settings{ class: ('expanded' if expanded) }
+ .settings-header
+ %h4
+ = _('Terms of Service')
+ %button.btn.btn-default.js-settings-toggle{ type: 'button' }
+ = expanded ? _('Collapse') : _('Expand')
+ %p
+ = _('Include a Terms of Service agreement that all users must accept.')
+ .settings-content
+ = render 'terms'
+
%section.settings.as-help-page.no-animate#js-help-settings{ class: ('expanded' if expanded) }
.settings-header
%h4
= _('Help page')
- %button.btn.js-settings-toggle{ type: 'button' }
- = expanded ? 'Collapse' : 'Expand'
+ %button.btn.btn-default.js-settings-toggle{ type: 'button' }
+ = expanded ? _('Collapse') : _('Expand')
%p
= _('Help page text and support page url.')
.settings-content
@@ -62,8 +73,8 @@
.settings-header
%h4
= _('Pages')
- %button.btn.js-settings-toggle{ type: 'button' }
- = expanded ? 'Collapse' : 'Expand'
+ %button.btn.btn-default.js-settings-toggle{ type: 'button' }
+ = expanded ? _('Collapse') : _('Expand')
%p
= _('Size and domain settings for static websites')
.settings-content
@@ -73,8 +84,8 @@
.settings-header
%h4
= _('Continuous Integration and Deployment')
- %button.btn.js-settings-toggle{ type: 'button' }
- = expanded ? 'Collapse' : 'Expand'
+ %button.btn.btn-default.js-settings-toggle{ type: 'button' }
+ = expanded ? _('Collapse') : _('Expand')
%p
= _('Auto DevOps, runners and job artifacts')
.settings-content
@@ -84,8 +95,8 @@
.settings-header
%h4
= _('Metrics - Influx')
- %button.btn.js-settings-toggle{ type: 'button' }
- = expanded ? 'Collapse' : 'Expand'
+ %button.btn.btn-default.js-settings-toggle{ type: 'button' }
+ = expanded ? _('Collapse') : _('Expand')
%p
= _('Enable and configure InfluxDB metrics.')
.settings-content
@@ -95,8 +106,8 @@
.settings-header
%h4
= _('Metrics - Prometheus')
- %button.btn.js-settings-toggle{ type: 'button' }
- = expanded ? 'Collapse' : 'Expand'
+ %button.btn.btn-default.js-settings-toggle{ type: 'button' }
+ = expanded ? _('Collapse') : _('Expand')
%p
= _('Enable and configure Prometheus metrics.')
.settings-content
@@ -106,8 +117,8 @@
.settings-header
%h4
= _('Profiling - Performance bar')
- %button.btn.js-settings-toggle{ type: 'button' }
- = expanded ? 'Collapse' : 'Expand'
+ %button.btn.btn-default.js-settings-toggle{ type: 'button' }
+ = expanded ? _('Collapse') : _('Expand')
%p
= _('Enable the Performance Bar for a given group.')
= link_to icon('question-circle'), help_page_path('administration/monitoring/performance/performance_bar')
@@ -118,8 +129,8 @@
.settings-header
%h4
= _('Background jobs')
- %button.btn.js-settings-toggle{ type: 'button' }
- = expanded ? 'Collapse' : 'Expand'
+ %button.btn.btn-default.js-settings-toggle{ type: 'button' }
+ = expanded ? _('Collapse') : _('Expand')
%p
= _('Configure Sidekiq job throttling.')
.settings-content
@@ -129,8 +140,8 @@
.settings-header
%h4
= _('Spam and Anti-bot Protection')
- %button.btn.js-settings-toggle{ type: 'button' }
- = expanded ? 'Collapse' : 'Expand'
+ %button.btn.btn-default.js-settings-toggle{ type: 'button' }
+ = expanded ? _('Collapse') : _('Expand')
%p
= _('Enable reCAPTCHA or Akismet and set IP limits.')
.settings-content
@@ -140,8 +151,8 @@
.settings-header
%h4
= _('Abuse reports')
- %button.btn.js-settings-toggle{ type: 'button' }
- = expanded ? 'Collapse' : 'Expand'
+ %button.btn.btn-default.js-settings-toggle{ type: 'button' }
+ = expanded ? _('Collapse') : _('Expand')
%p
= _('Set notification email for abuse reports.')
.settings-content
@@ -151,8 +162,8 @@
.settings-header
%h4
= _('Error Reporting and Logging')
- %button.btn.js-settings-toggle{ type: 'button' }
- = expanded ? 'Collapse' : 'Expand'
+ %button.btn.btn-default.js-settings-toggle{ type: 'button' }
+ = expanded ? _('Collapse') : _('Expand')
%p
= _('Enable Sentry for error reporting and logging.')
.settings-content
@@ -162,8 +173,8 @@
.settings-header
%h4
= _('Repository storage')
- %button.btn.js-settings-toggle{ type: 'button' }
- = expanded ? 'Collapse' : 'Expand'
+ %button.btn.btn-default.js-settings-toggle{ type: 'button' }
+ = expanded ? _('Collapse') : _('Expand')
%p
= _('Configure storage path and circuit breaker settings.')
.settings-content
@@ -173,8 +184,8 @@
.settings-header
%h4
= _('Repository maintenance')
- %button.btn.js-settings-toggle{ type: 'button' }
- = expanded ? 'Collapse' : 'Expand'
+ %button.btn.btn-default.js-settings-toggle{ type: 'button' }
+ = expanded ? _('Collapse') : _('Expand')
%p
= _('Configure automatic git checks and housekeeping on repositories.')
.settings-content
@@ -185,8 +196,8 @@
.settings-header
%h4
= _('Container Registry')
- %button.btn.js-settings-toggle{ type: 'button' }
- = expanded ? 'Collapse' : 'Expand'
+ %button.btn.btn-default.js-settings-toggle{ type: 'button' }
+ = expanded ? _('Collapse') : _('Expand')
%p
= _('Various container registry settings.')
.settings-content
@@ -197,8 +208,8 @@
.settings-header
%h4
= _('Koding')
- %button.btn.js-settings-toggle{ type: 'button' }
- = expanded ? 'Collapse' : 'Expand'
+ %button.btn.btn-default.js-settings-toggle{ type: 'button' }
+ = expanded ? _('Collapse') : _('Expand')
%p
= _('Online IDE integration settings.')
.settings-content
@@ -208,8 +219,8 @@
.settings-header
%h4
= _('PlantUML')
- %button.btn.js-settings-toggle{ type: 'button' }
- = expanded ? 'Collapse' : 'Expand'
+ %button.btn.btn-default.js-settings-toggle{ type: 'button' }
+ = expanded ? _('Collapse') : _('Expand')
%p
= _('Allow rendering of PlantUML diagrams in Asciidoc documents.')
.settings-content
@@ -219,8 +230,8 @@
.settings-header#usage-statistics
%h4
= _('Usage statistics')
- %button.btn.js-settings-toggle{ type: 'button' }
- = expanded ? 'Collapse' : 'Expand'
+ %button.btn.btn-default.js-settings-toggle{ type: 'button' }
+ = expanded ? _('Collapse') : _('Expand')
%p
= _('Enable or disable version check and usage ping.')
.settings-content
@@ -230,8 +241,8 @@
.settings-header
%h4
= _('Email')
- %button.btn.js-settings-toggle{ type: 'button' }
- = expanded ? 'Collapse' : 'Expand'
+ %button.btn.btn-default.js-settings-toggle{ type: 'button' }
+ = expanded ? _('Collapse') : _('Expand')
%p
= _('Various email settings.')
.settings-content
@@ -241,8 +252,8 @@
.settings-header
%h4
= _('Gitaly')
- %button.btn.js-settings-toggle{ type: 'button' }
- = expanded ? 'Collapse' : 'Expand'
+ %button.btn.btn-default.js-settings-toggle{ type: 'button' }
+ = expanded ? _('Collapse') : _('Expand')
%p
= _('Configure Gitaly timeouts.')
.settings-content
@@ -252,8 +263,8 @@
.settings-header
%h4
= _('Web terminal')
- %button.btn.js-settings-toggle{ type: 'button' }
- = expanded ? 'Collapse' : 'Expand'
+ %button.btn.btn-default.js-settings-toggle{ type: 'button' }
+ = expanded ? _('Collapse') : _('Expand')
%p
= _('Set max session time for web terminal.')
.settings-content
@@ -263,8 +274,8 @@
.settings-header
%h4
= _('Real-time features')
- %button.btn.js-settings-toggle{ type: 'button' }
- = expanded ? 'Collapse' : 'Expand'
+ %button.btn.btn-default.js-settings-toggle{ type: 'button' }
+ = expanded ? _('Collapse') : _('Expand')
%p
= _('Change this value to influence how frequently the GitLab UI polls for updates.')
.settings-content
@@ -274,8 +285,8 @@
.settings-header
%h4
= _('Performance optimization')
- %button.btn.js-settings-toggle{ type: 'button' }
- = expanded ? 'Collapse' : 'Expand'
+ %button.btn.btn-default.js-settings-toggle{ type: 'button' }
+ = expanded ? _('Collapse') : _('Expand')
%p
= _('Various settings that affect GitLab performance.')
.settings-content
@@ -285,8 +296,8 @@
.settings-header
%h4
= _('User and IP Rate Limits')
- %button.btn.js-settings-toggle{ type: 'button' }
- = expanded ? 'Collapse' : 'Expand'
+ %button.btn.btn-default.js-settings-toggle{ type: 'button' }
+ = expanded ? _('Collapse') : _('Expand')
%p
= _('Configure limits for web and API requests.')
.settings-content
@@ -296,9 +307,20 @@
.settings-header
%h4
= _('Outbound requests')
- %button.btn.js-settings-toggle{ type: 'button' }
- = expanded ? 'Collapse' : 'Expand'
+ %button.btn.btn-default.js-settings-toggle{ type: 'button' }
+ = expanded ? _('Collapse') : _('Expand')
%p
= _('Allow requests to the local network from hooks and services.')
.settings-content
= render 'outbound'
+
+%section.settings.as-mirror.no-animate#js-mirror-settings{ class: ('expanded' if expanded) }
+ .settings-header
+ %h4
+ = _('Repository mirror settings')
+ %button.btn.js-settings-toggle{ type: 'button' }
+ = expanded ? 'Collapse' : 'Expand'
+ %p
+ = _('Configure push mirrors.')
+ .settings-content
+ = render partial: 'repository_mirrors_form'
diff --git a/app/views/admin/projects/show.html.haml b/app/views/admin/projects/show.html.haml
index 49a4cde73d9..29ec712b6b7 100644
--- a/app/views/admin/projects/show.html.haml
+++ b/app/views/admin/projects/show.html.haml
@@ -13,7 +13,7 @@
.card
.card-header.alert.alert-danger
Last repository check
- = "(#{time_ago_in_words(@project.last_repository_check_at)} ago)"
+ = "(#{time_ago_with_tooltip(@project.last_repository_check_at)})"
failed. See
= link_to 'repocheck.log', admin_logs_path
for error messages.
diff --git a/app/views/admin/runners/_runner.html.haml b/app/views/admin/runners/_runner.html.haml
index 8fcd50b70a8..ac2bb753fd6 100644
--- a/app/views/admin/runners/_runner.html.haml
+++ b/app/views/admin/runners/_runner.html.haml
@@ -2,6 +2,8 @@
%td
- if runner.shared?
%span.badge.badge-success shared
+ - elsif runner.group_type?
+ %span.badge.badge-success group
- else
%span.badge.badge-info specific
- if runner.locked?
@@ -19,7 +21,7 @@
%td
= runner.ip_address
%td
- - if runner.shared?
+ - if runner.shared? || runner.group_type?
n/a
- else
= runner.projects.count(:all)
diff --git a/app/views/admin/runners/index.html.haml b/app/views/admin/runners/index.html.haml
index e782ed5c527..8a130782b46 100644
--- a/app/views/admin/runners/index.html.haml
+++ b/app/views/admin/runners/index.html.haml
@@ -17,6 +17,9 @@
%span.badge.badge-success shared
\- Runner runs jobs from all unassigned projects
%li
+ %span.badge.badge-success group
+ \- Runner runs jobs from all unassigned projects in its group
+ %li
%span.badge.badge-info specific
\- Runner runs jobs from assigned projects
%li
diff --git a/app/views/admin/runners/show.html.haml b/app/views/admin/runners/show.html.haml
index 30309f501bd..8a0c2bf4c5f 100644
--- a/app/views/admin/runners/show.html.haml
+++ b/app/views/admin/runners/show.html.haml
@@ -19,6 +19,9 @@
%p
If you want Runners to build only specific projects, enable them in the table below.
Keep in mind that this is a one way transition.
+- elsif @runner.group_type?
+ .bs-callout.bs-callout-success
+ %h4 This runner will process jobs from all projects in its group and subgroups
- else
.bs-callout.bs-callout-info
%h4 This Runner will process jobs only from ASSIGNED projects
@@ -26,7 +29,7 @@
%hr
.append-bottom-20
- = render '/projects/runners/form', runner: @runner, runner_form_url: admin_runner_path(@runner)
+ = render 'shared/runners/form', runner: @runner, runner_form_url: admin_runner_path(@runner)
.row
.col-md-6
@@ -108,4 +111,4 @@
%td.timestamp
- if build.finished_at
- %span #{time_ago_in_words build.finished_at} ago
+ %span= time_ago_with_tooltip build.finished_at
diff --git a/app/views/admin/services/index.html.haml b/app/views/admin/services/index.html.haml
index 50132572096..89872c1b91a 100644
--- a/app/views/admin/services/index.html.haml
+++ b/app/views/admin/services/index.html.haml
@@ -20,5 +20,4 @@
%td
= service.description
%td.light
- = time_ago_in_words service.updated_at
- ago
+ = time_ago_with_tooltip service.updated_at
diff --git a/app/views/admin/users/index.html.haml b/app/views/admin/users/index.html.haml
index 0a68c8b3a23..faeb82656ba 100644
--- a/app/views/admin/users/index.html.haml
+++ b/app/views/admin/users/index.html.haml
@@ -42,31 +42,31 @@
= nav_link(html_options: { class: active_when(params[:filter].nil?) }) do
= link_to admin_users_path do
Active
- %small.badge.badge-pill= number_with_delimiter(User.active.count)
+ %small.badge.badge-pill= limited_counter_with_delimiter(User.active)
= nav_link(html_options: { class: active_when(params[:filter] == 'admins') }) do
= link_to admin_users_path(filter: "admins") do
Admins
- %small.badge.badge-pill= number_with_delimiter(User.admins.count)
+ %small.badge.badge-pill= limited_counter_with_delimiter(User.admins)
= nav_link(html_options: { class: "#{active_when(params[:filter] == 'two_factor_enabled')} filter-two-factor-enabled" }) do
= link_to admin_users_path(filter: 'two_factor_enabled') do
2FA Enabled
- %small.badge.badge-pill= number_with_delimiter(User.with_two_factor.count)
+ %small.badge.badge-pill= limited_counter_with_delimiter(User.with_two_factor)
= nav_link(html_options: { class: "#{active_when(params[:filter] == 'two_factor_disabled')} filter-two-factor-disabled" }) do
= link_to admin_users_path(filter: 'two_factor_disabled') do
2FA Disabled
- %small.badge.badge-pill= number_with_delimiter(User.without_two_factor.count)
+ %small.badge.badge-pill= limited_counter_with_delimiter(User.without_two_factor)
= nav_link(html_options: { class: active_when(params[:filter] == 'external') }) do
= link_to admin_users_path(filter: 'external') do
External
- %small.badge.badge-pill= number_with_delimiter(User.external.count)
+ %small.badge.badge-pill= limited_counter_with_delimiter(User.external)
= nav_link(html_options: { class: active_when(params[:filter] == 'blocked') }) do
= link_to admin_users_path(filter: "blocked") do
Blocked
- %small.badge.badge-pill= number_with_delimiter(User.blocked.count)
+ %small.badge.badge-pill= limited_counter_with_delimiter(User.blocked)
= nav_link(html_options: { class: active_when(params[:filter] == 'wop') }) do
= link_to admin_users_path(filter: "wop") do
Without projects
- %small.badge.badge-pill= number_with_delimiter(User.without_projects.count)
+ %small.badge.badge-pill= limited_counter_with_delimiter(User.without_projects)
%ul.flex-list.content-list
- if @users.empty?
diff --git a/app/views/ci/status/_dropdown_graph_badge.html.haml b/app/views/ci/status/_dropdown_graph_badge.html.haml
index 5445a70ebae..8b0463db000 100644
--- a/app/views/ci/status/_dropdown_graph_badge.html.haml
+++ b/app/views/ci/status/_dropdown_graph_badge.html.haml
@@ -16,5 +16,5 @@
%span.ci-build-text= subject.name
- if status.has_action?
- = link_to status.action_path, class: "ci-action-icon-wrapper js-ci-action-icon", method: status.action_method, data: { toggle: 'tooltip', title: status.action_title, container: 'body' } do
+ = link_to status.action_path, class: "ci-action-icon-container ci-action-icon-wrapper js-ci-action-icon", method: status.action_method, data: { toggle: 'tooltip', title: status.action_title, container: 'body' } do
= sprite_icon(status.action_icon, css_class: "icon-action-#{status.action_icon}")
diff --git a/app/views/devise/mailer/unlock_instructions.html.haml b/app/views/devise/mailer/unlock_instructions.html.haml
index 79e3a35cc9a..8ddfd3ea74a 100644
--- a/app/views/devise/mailer/unlock_instructions.html.haml
+++ b/app/views/devise/mailer/unlock_instructions.html.haml
@@ -2,7 +2,7 @@
= email_default_heading("Hello, #{@resource.name}!")
%p
Your GitLab account has been locked due to an excessive amount of unsuccessful
- sign in attempts. Your account will automatically unlock in #{time_ago_in_words(Devise.unlock_in.from_now)}
+ sign in attempts. Your account will automatically unlock in #{distance_of_time_in_words(Devise.unlock_in)}
or you may click the link below to unlock now.
#cta
= link_to('Unlock account', unlock_url(@resource, unlock_token: @token))
diff --git a/app/views/devise/mailer/unlock_instructions.text.erb b/app/views/devise/mailer/unlock_instructions.text.erb
index 3aea3e20145..8d4abbf3500 100644
--- a/app/views/devise/mailer/unlock_instructions.text.erb
+++ b/app/views/devise/mailer/unlock_instructions.text.erb
@@ -1,7 +1,7 @@
Hello, <%= @resource.name %>!
Your GitLab account has been locked due to an excessive amount of unsuccessful
-sign in attempts. Your account will automatically unlock in <%= time_ago_in_words(Devise.unlock_in.from_now) %>
+sign in attempts. Your account will automatically unlock in <%= distance_of_time_in_words(Devise.unlock_in) %>
or you may click the link below to unlock now.
<%= unlock_url(@resource, unlock_token: @token) %>
diff --git a/app/views/groups/_group_admin_settings.html.haml b/app/views/groups/_group_admin_settings.html.haml
index 0de2288ce0a..5b96351a7c9 100644
--- a/app/views/groups/_group_admin_settings.html.haml
+++ b/app/views/groups/_group_admin_settings.html.haml
@@ -1,28 +1,26 @@
-- if current_user.admin?
- .form-group
- = f.label :lfs_enabled, 'Large File Storage', class: 'col-form-label'
- .col-sm-10
- .form-check
- = f.label :lfs_enabled do
- = f.check_box :lfs_enabled, checked: @group.lfs_enabled?
- %strong
- Allow projects within this group to use Git LFS
- = link_to icon('question-circle'), help_page_path('workflow/lfs/manage_large_binaries_with_git_lfs')
- %br/
- %span.descr This setting can be overridden in each project.
+.form-group
+ = f.label :lfs_enabled, 'Large File Storage', class: 'col-form-label'
+ .col-sm-10
+ .form-check
+ = f.label :lfs_enabled do
+ = f.check_box :lfs_enabled, checked: @group.lfs_enabled?
+ %strong
+ Allow projects within this group to use Git LFS
+ = link_to icon('question-circle'), help_page_path('workflow/lfs/manage_large_binaries_with_git_lfs')
+ %br/
+ %span.descr This setting can be overridden in each project.
-- if can? current_user, :admin_group, @group
- .form-group
- = f.label :require_two_factor_authentication, 'Two-factor authentication', class: 'col-form-label col-sm-2'
- .col-sm-10
- .form-check
- = f.label :require_two_factor_authentication do
- = f.check_box :require_two_factor_authentication
- %strong
- Require all users in this group to setup Two-factor authentication
- = link_to icon('question-circle'), help_page_path('security/two_factor_authentication', anchor: 'enforcing-2fa-for-all-users-in-a-group')
- .form-group
- .offset-sm-2.col-sm-10
- .form-check
- = f.text_field :two_factor_grace_period, class: 'form-control'
- .form-text.text-muted Amount of time (in hours) that users are allowed to skip forced configuration of two-factor authentication
+.form-group
+ = f.label :require_two_factor_authentication, 'Two-factor authentication', class: 'col-form-label col-sm-2'
+ .col-sm-10
+ .form-check
+ = f.label :require_two_factor_authentication do
+ = f.check_box :require_two_factor_authentication
+ %strong
+ Require all users in this group to setup Two-factor authentication
+ = link_to icon('question-circle'), help_page_path('security/two_factor_authentication', anchor: 'enforcing-2fa-for-all-users-in-a-group')
+.form-group
+ .col-sm-offset-2.col-sm-10
+ .form-check
+ = f.text_field :two_factor_grace_period, class: 'col-form-label'
+ .form-text.text-muted Amount of time (in hours) that users are allowed to skip forced configuration of two-factor authentication
diff --git a/app/views/groups/runners/_group_runners.html.haml b/app/views/groups/runners/_group_runners.html.haml
new file mode 100644
index 00000000000..1f9b43e8727
--- /dev/null
+++ b/app/views/groups/runners/_group_runners.html.haml
@@ -0,0 +1,24 @@
+- link = link_to 'Runners API', help_page_path('api/runners.md')
+
+%h3
+ = _('Group Runners')
+
+.bs-callout.bs-callout-warning
+ = _('GitLab Group Runners can execute code for all the projects in this group.')
+ = _('They can be managed using the %{link}.').html_safe % { link: link }
+
+-# Proper policies should be implemented per
+-# https://gitlab.com/gitlab-org/gitlab-ce/issues/45894
+- if can?(current_user, :admin_pipeline, @group)
+ = render partial: 'ci/runner/how_to_setup_runner',
+ locals: { registration_token: @group.runners_token, type: 'group' }
+
+- if @group.runners.empty?
+ %h4.underlined-title
+ = _('This group does not provide any group Runners yet.')
+
+- else
+ %h4.underlined-title
+ = _('Available group Runners : %{runners}.').html_safe % { runners: @group.runners.count }
+ %ul.bordered-list
+ = render partial: 'groups/runners/runner', collection: @group.runners, as: :runner
diff --git a/app/views/groups/runners/_index.html.haml b/app/views/groups/runners/_index.html.haml
new file mode 100644
index 00000000000..0cf9011b471
--- /dev/null
+++ b/app/views/groups/runners/_index.html.haml
@@ -0,0 +1,9 @@
+= render 'shared/runners/runner_description'
+
+%hr
+
+%p.lead
+ = _('To start serving your jobs you can add Runners to your group')
+.row
+ .col-sm-6
+ = render 'groups/runners/group_runners'
diff --git a/app/views/groups/runners/_runner.html.haml b/app/views/groups/runners/_runner.html.haml
new file mode 100644
index 00000000000..76650a961d6
--- /dev/null
+++ b/app/views/groups/runners/_runner.html.haml
@@ -0,0 +1,27 @@
+%li.runner{ id: dom_id(runner) }
+ %h4
+ = runner_status_icon(runner)
+
+ = link_to runner.short_sha, group_runner_path(@group, runner), class: 'commit-sha'
+
+ %small.edit-runner
+ = link_to edit_group_runner_path(@group, runner) do
+ = icon('edit')
+
+ .pull-right
+ - if runner.active?
+ = link_to _('Pause'), pause_group_runner_path(@group, runner), method: :post, class: 'btn btn-sm btn-danger', data: { confirm: _("Are you sure?") }
+ - else
+ = link_to _('Resume'), resume_group_runner_path(@group, runner), method: :post, class: 'btn btn-success btn-sm'
+ = link_to _('Remove Runner'), group_runner_path(@group, runner), data: { confirm: _("Are you sure?") }, method: :delete, class: 'btn btn-danger btn-sm'
+ .pull-right
+ %small.light
+ \##{runner.id}
+ - if runner.description.present?
+ %p.runner-description
+ = runner.description
+ - if runner.tag_list.present?
+ %p
+ - runner.tag_list.sort.each do |tag|
+ %span.label.label-primary
+ = tag
diff --git a/app/views/groups/runners/edit.html.haml b/app/views/groups/runners/edit.html.haml
new file mode 100644
index 00000000000..d4993d9c235
--- /dev/null
+++ b/app/views/groups/runners/edit.html.haml
@@ -0,0 +1,6 @@
+- page_title "Edit", "#{@runner.description} ##{@runner.id}", "Runners"
+
+%h4 Runner ##{@runner.id}
+
+%hr
+ = render 'shared/runners/form', runner: @runner, runner_form_url: group_runner_path(@group, @runner)
diff --git a/app/views/groups/settings/ci_cd/show.html.haml b/app/views/groups/settings/ci_cd/show.html.haml
index dd82922ec55..082e1b7befa 100644
--- a/app/views/groups/settings/ci_cd/show.html.haml
+++ b/app/views/groups/settings/ci_cd/show.html.haml
@@ -1,11 +1,27 @@
- breadcrumb_title "CI / CD Settings"
- page_title "CI / CD"
-%h4
- = _('Secret variables')
- = link_to icon('question-circle'), help_page_path('ci/variables/README', anchor: 'secret-variables'), target: '_blank', rel: 'noopener noreferrer'
+- expanded = Rails.env.test?
-%p
- = render "ci/variables/content"
+%section.settings#secret-variables.no-animate{ class: ('expanded' if expanded) }
+ .settings-header
+ %h4
+ = _('Secret variables')
+ = link_to icon('question-circle'), help_page_path('ci/variables/README', anchor: 'secret-variables'), target: '_blank', rel: 'noopener noreferrer'
+ %button.btn.btn-default.js-settings-toggle{ type: "button" }
+ = expanded ? _('Collapse') : _('Expand')
+ %p.append-bottom-0
+ = render "ci/variables/content"
+ .settings-content
+ = render 'ci/variables/index', save_endpoint: group_variables_path
-= render 'ci/variables/index', save_endpoint: group_variables_path
+%section.settings#runners-settings.no-animate{ class: ('expanded' if expanded) }
+ .settings-header
+ %h4
+ = _('Runners settings')
+ %button.btn.btn-default.js-settings-toggle{ type: "button" }
+ = expanded ? _('Collapse') : _('Expand')
+ %p
+ = _('Register and see your runners for this group.')
+ .settings-content
+ = render 'groups/runners/index'
diff --git a/app/views/help/_shortcuts.html.haml b/app/views/help/_shortcuts.html.haml
index 29b23ae2e52..1c5b4aecabb 100644
--- a/app/views/help/_shortcuts.html.haml
+++ b/app/views/help/_shortcuts.html.haml
@@ -1,5 +1,5 @@
#modal-shortcuts.modal{ tabindex: -1 }
- .modal-dialog
+ .modal-dialog.modal-lg
.modal-content
.modal-header
%a.close{ href: "#", "data-dismiss" => "modal" } ×
diff --git a/app/views/help/ui.html.haml b/app/views/help/ui.html.haml
index dc96d17527b..6a055f77f4a 100644
--- a/app/views/help/ui.html.haml
+++ b/app/views/help/ui.html.haml
@@ -74,10 +74,10 @@
= lorem
.cover-controls
- = link_to '#', class: 'btn btn-gray' do
+ = link_to '#', class: 'btn btn-default' do
= icon('pencil')
&nbsp;
- = link_to '#', class: 'btn btn-gray' do
+ = link_to '#', class: 'btn btn-default' do
= icon('rss')
%h2#lists Lists
@@ -205,8 +205,7 @@
%h2#buttons Buttons
.example
- %button.btn.btn-secondary{ :type => "button" } Default
- %button.btn.btn-gray{ :type => "button" } Gray
+ %button.btn.btn-secondary{ :type => "button" } Secondary
%button.btn.btn-primary{ :type => "button" } Primary
%button.btn.btn-success{ :type => "button" } Success
%button.btn.btn-info{ :type => "button" } Info
diff --git a/app/views/ide/index.html.haml b/app/views/ide/index.html.haml
index e0e8fe548d0..da9331b45dd 100644
--- a/app/views/ide/index.html.haml
+++ b/app/views/ide/index.html.haml
@@ -1,9 +1,6 @@
- @body_class = 'ide'
- page_title 'IDE'
-- content_for :page_specific_javascripts do
- = webpack_bundle_tag 'ide', force_same_domain: true
-
#ide.ide-loading{ data: {"empty-state-svg-path" => image_path('illustrations/multi_file_editor_empty.svg'),
"no-changes-state-svg-path" => image_path('illustrations/multi-editor_no_changes_empty.svg'),
"committed-state-svg-path" => image_path('illustrations/multi-editor_all_changes_committed_empty.svg') } }
diff --git a/app/views/layouts/_flash.html.haml b/app/views/layouts/_flash.html.haml
index 05ddd0ec733..8bd5708d490 100644
--- a/app/views/layouts/_flash.html.haml
+++ b/app/views/layouts/_flash.html.haml
@@ -1,8 +1,10 @@
+- extra_flash_class = local_assigns.fetch(:extra_flash_class, nil)
+
.flash-container.flash-container-page
-# We currently only support `alert`, `notice`, `success`
- flash.each do |key, value|
-# Don't show a flash message if the message is nil
- if value
%div{ class: "flash-#{key}" }
- %div{ class: (container_class) }
+ %div{ class: "#{container_class} #{extra_flash_class}" }
%span= value
diff --git a/app/views/layouts/_head.html.haml b/app/views/layouts/_head.html.haml
index b981b5fdafa..02bdfe9aa3c 100644
--- a/app/views/layouts/_head.html.haml
+++ b/app/views/layouts/_head.html.haml
@@ -38,9 +38,6 @@
= yield :library_javascripts
= javascript_include_tag locale_path unless I18n.locale == :en
- = webpack_bundle_tag "webpack_runtime"
- = webpack_bundle_tag "common"
- = webpack_bundle_tag "main"
= webpack_bundle_tag "raven" if Gitlab::CurrentSettings.clientside_sentry_enabled
- if content_for?(:page_specific_javascripts)
diff --git a/app/views/layouts/_init_auto_complete.html.haml b/app/views/layouts/_init_auto_complete.html.haml
index 4276e6ee4bb..240e03a5d53 100644
--- a/app/views/layouts/_init_auto_complete.html.haml
+++ b/app/views/layouts/_init_auto_complete.html.haml
@@ -1,16 +1,11 @@
-- project = @target_project || @project
+- object = @target_project || @project || @group
- noteable_type = @noteable.class if @noteable.present?
-- if project
+- datasources = autocomplete_data_sources(object, noteable_type)
+
+- if object
-# haml-lint:disable InlineJavaScript
:javascript
gl = window.gl || {};
gl.GfmAutoComplete = gl.GfmAutoComplete || {};
- gl.GfmAutoComplete.dataSources = {
- members: "#{members_project_autocomplete_sources_path(project, type: noteable_type, type_id: params[:id])}",
- issues: "#{issues_project_autocomplete_sources_path(project)}",
- mergeRequests: "#{merge_requests_project_autocomplete_sources_path(project)}",
- labels: "#{labels_project_autocomplete_sources_path(project, type: noteable_type, type_id: params[:id])}",
- milestones: "#{milestones_project_autocomplete_sources_path(project)}",
- commands: "#{commands_project_autocomplete_sources_path(project, type: noteable_type, type_id: params[:id])}"
- };
+ gl.GfmAutoComplete.dataSources = #{datasources.to_json};
diff --git a/app/views/layouts/header/_current_user_dropdown.html.haml b/app/views/layouts/header/_current_user_dropdown.html.haml
new file mode 100644
index 00000000000..24b6c490a5a
--- /dev/null
+++ b/app/views/layouts/header/_current_user_dropdown.html.haml
@@ -0,0 +1,22 @@
+- return unless current_user
+
+%ul
+ %li.current-user
+ .user-name.bold
+ = current_user.name
+ = current_user.to_reference
+ %li.divider
+ - if current_user_menu?(:profile)
+ %li
+ = link_to s_("CurrentUser|Profile"), current_user, class: 'profile-link', data: { user: current_user.username }
+ - if current_user_menu?(:settings)
+ %li
+ = link_to s_("CurrentUser|Settings"), profile_path
+ - if current_user_menu?(:help)
+ %li
+ = link_to _("Help"), help_path
+ - if current_user_menu?(:help) || current_user_menu?(:settings) || current_user_menu?(:profile)
+ %li.divider
+ - if current_user_menu?(:sign_out)
+ %li
+ = link_to _("Sign out"), destroy_user_session_path, class: "sign-out-link"
diff --git a/app/views/layouts/header/_default.html.haml b/app/views/layouts/header/_default.html.haml
index 9cb1b155a0f..e07e8683679 100644
--- a/app/views/layouts/header/_default.html.haml
+++ b/app/views/layouts/header/_default.html.haml
@@ -52,23 +52,8 @@
= link_to current_user, class: user_dropdown_class, data: { toggle: "dropdown" } do
= image_tag avatar_icon_for_user(current_user, 23), width: 23, height: 23, class: "header-user-avatar qa-user-avatar"
= sprite_icon('angle-down', css_class: 'caret-down')
- .dropdown-menu.dropdown-menu-nav.dropdown-menu-right
- %ul
- %li.current-user
- .user-name.bold
- = current_user.name
- @#{current_user.username}
- %li.divider
- %li
- = link_to "Profile", current_user, class: 'profile-link', data: { user: current_user.username }
- %li
- = link_to "Settings", profile_path
- - if current_user
- %li
- = link_to "Help", help_path
- %li.divider
- %li
- = link_to "Sign out", destroy_user_session_path, class: "sign-out-link"
+ .dropdown-menu-nav.dropdown-menu-align-right
+ = render 'layouts/header/current_user_dropdown'
- if header_link?(:admin_impersonation)
%li.nav-item.impersonation
= link_to admin_impersonation_path, class: 'nav-link impersonation-btn', method: :delete, title: "Stop impersonation", aria: { label: 'Stop impersonation' }, data: { toggle: 'tooltip', placement: 'bottom', container: 'body' } do
diff --git a/app/views/layouts/nav/sidebar/_profile.html.haml b/app/views/layouts/nav/sidebar/_profile.html.haml
index c878fcf2808..6cbd163dd41 100644
--- a/app/views/layouts/nav/sidebar/_profile.html.haml
+++ b/app/views/layouts/nav/sidebar/_profile.html.haml
@@ -129,6 +129,17 @@
= link_to profile_preferences_path do
%strong.fly-out-top-item-name
#{ _('Preferences') }
+ = nav_link(controller: :active_sessions) do
+ = link_to profile_active_sessions_path do
+ .nav-icon-container
+ = sprite_icon('monitor-lines')
+ %span.nav-item-name
+ Active Sessions
+ %ul.sidebar-sub-level-items.is-fly-out-only
+ = nav_link(controller: :active_sessions, html_options: { class: "fly-out-top-item" } ) do
+ = link_to profile_active_sessions_path do
+ %strong.fly-out-top-item-name
+ #{ _('Active Sessions') }
= nav_link(path: 'profiles#audit_log') do
= link_to audit_log_profile_path do
.nav-icon-container
diff --git a/app/views/layouts/terms.html.haml b/app/views/layouts/terms.html.haml
new file mode 100644
index 00000000000..a30d6e2688c
--- /dev/null
+++ b/app/views/layouts/terms.html.haml
@@ -0,0 +1,34 @@
+!!! 5
+- @hide_breadcrumbs = true
+%html{ lang: I18n.locale, class: page_class }
+ = render "layouts/head"
+
+ %body{ data: { page: body_data_page } }
+ .layout-page.terms{ class: page_class }
+ .content-wrapper.prepend-top-0
+ .mobile-overlay
+ .alert-wrapper
+ = render "layouts/broadcast"
+ = render 'layouts/header/read_only_banner'
+ = render "layouts/flash", extra_flash_class: 'limit-container-width'
+
+ %div{ class: "#{container_class} limit-container-width" }
+ .content{ id: "content-body" }
+ .panel.panel-default
+ .panel-heading
+ .title
+ = brand_header_logo
+ - logo_text = brand_header_logo_type
+ - if logo_text.present?
+ %span.logo-text.hidden-xs.prepend-left-8
+ = logo_text
+ - if header_link?(:user_dropdown)
+ .navbar-collapse.collapse
+ %ul.nav.navbar-nav
+ %li.header-user.dropdown
+ = link_to current_user, class: user_dropdown_class, data: { toggle: "dropdown" } do
+ = image_tag avatar_icon_for_user(current_user, 23), width: 23, height: 23, class: "header-user-avatar qa-user-avatar"
+ = sprite_icon('angle-down', css_class: 'caret-down')
+ .dropdown-menu-nav.dropdown-menu-align-right
+ = render 'layouts/header/current_user_dropdown'
+ = yield
diff --git a/app/views/peek/_bar.html.haml b/app/views/peek/_bar.html.haml
index a911449672b..cb0cccb8f8a 100644
--- a/app/views/peek/_bar.html.haml
+++ b/app/views/peek/_bar.html.haml
@@ -3,5 +3,5 @@
#js-peek{ data: { env: Peek.env,
request_id: Peek.request_id,
peek_url: peek_routes.results_url,
- profile_url: url_for(params.merge(lineprofiler: 'true')) },
+ profile_url: url_for(safe_params.merge(lineprofiler: 'true')) },
class: Peek.env }
diff --git a/app/views/profiles/active_sessions/_active_session.html.haml b/app/views/profiles/active_sessions/_active_session.html.haml
new file mode 100644
index 00000000000..d40b771f48b
--- /dev/null
+++ b/app/views/profiles/active_sessions/_active_session.html.haml
@@ -0,0 +1,31 @@
+- is_current_session = active_session.current?(session)
+
+%li
+ .pull-left.append-right-10{ data: { toggle: 'tooltip' }, title: active_session.human_device_type }
+ = active_session_device_type_icon(active_session)
+
+ .description.pull-left
+ %div
+ %strong= active_session.ip_address
+ - if is_current_session
+ %div This is your current session
+ - else
+ %div
+ Last accessed on
+ = l(active_session.updated_at, format: :short)
+
+ %div
+ %strong= active_session.browser
+ on
+ %strong= active_session.os
+
+ %div
+ %strong Signed in
+ on
+ = l(active_session.created_at, format: :short)
+
+ - unless is_current_session
+ .pull-right
+ = link_to profile_active_session_path(active_session.session_id), data: { confirm: 'Are you sure? The device will be signed out of GitLab.' }, method: :delete, class: "btn btn-danger prepend-left-10" do
+ %span.sr-only Revoke
+ Revoke
diff --git a/app/views/profiles/active_sessions/index.html.haml b/app/views/profiles/active_sessions/index.html.haml
new file mode 100644
index 00000000000..d0250bb4eab
--- /dev/null
+++ b/app/views/profiles/active_sessions/index.html.haml
@@ -0,0 +1,14 @@
+- page_title 'Active Sessions'
+- @content_class = "limit-container-width" unless fluid_layout
+
+.row.prepend-top-default
+ .col-lg-4.profile-settings-sidebar
+ %h4.prepend-top-0
+ = page_title
+ %p
+ This is a list of devices that have logged into your account. Revoke any sessions that you do not recognize.
+ .col-lg-8
+ .append-bottom-default
+
+ %ul.well-list
+ = render partial: 'profiles/active_sessions/active_session', collection: @sessions
diff --git a/app/views/projects/_import_project_pane.html.haml b/app/views/projects/_import_project_pane.html.haml
new file mode 100644
index 00000000000..4bee6cb97eb
--- /dev/null
+++ b/app/views/projects/_import_project_pane.html.haml
@@ -0,0 +1,51 @@
+- active_tab = local_assigns.fetch(:active_tab, 'blank')
+- f = local_assigns.fetch(:f)
+
+.project-import.row
+ .col-lg-12
+ .form-group.import-btn-container.clearfix
+ = f.label :visibility_level, class: 'label-light' do #the label here seems wrong
+ Import project from
+ .import-buttons
+ - if gitlab_project_import_enabled?
+ .import_gitlab_project.has-tooltip{ data: { container: 'body' } }
+ = link_to new_import_gitlab_project_path, class: 'btn btn_import_gitlab_project project-submit' do
+ = icon('gitlab', text: 'GitLab export')
+ %div
+ - if github_import_enabled?
+ = link_to new_import_github_path, class: 'btn js-import-github' do
+ = icon('github', text: 'GitHub')
+ %div
+ - if bitbucket_import_enabled?
+ = link_to status_import_bitbucket_path, class: "btn import_bitbucket #{'how_to_import_link' unless bitbucket_import_configured?}" do
+ = icon('bitbucket', text: 'Bitbucket')
+ - unless bitbucket_import_configured?
+ = render 'bitbucket_import_modal'
+ %div
+ - if gitlab_import_enabled?
+ = link_to status_import_gitlab_path, class: "btn import_gitlab #{'how_to_import_link' unless gitlab_import_configured?}" do
+ = icon('gitlab', text: 'GitLab.com')
+ - unless gitlab_import_configured?
+ = render 'gitlab_import_modal'
+ %div
+ - if google_code_import_enabled?
+ = link_to new_import_google_code_path, class: 'btn import_google_code' do
+ = icon('google', text: 'Google Code')
+ %div
+ - if fogbugz_import_enabled?
+ = link_to new_import_fogbugz_path, class: 'btn import_fogbugz' do
+ = icon('bug', text: 'Fogbugz')
+ %div
+ - if gitea_import_enabled?
+ = link_to new_import_gitea_path, class: 'btn import_gitea' do
+ = custom_icon('go_logo')
+ Gitea
+ %div
+ - if git_import_enabled?
+ %button.btn.js-toggle-button.js-import-git-toggle-button{ type: "button", data: { toggle_open_class: 'active' } }
+ = icon('git', text: 'Repo by URL')
+ .col-lg-12
+ .js-toggle-content.toggle-import-form{ class: ('hide' if active_tab != 'import') }
+ %hr
+ = render "shared/import_form", f: f
+ = render 'new_project_fields', f: f, project_name_id: "import-url-name"
diff --git a/app/views/projects/clusters/_gcp_signup_offer_banner.html.haml b/app/views/projects/clusters/_gcp_signup_offer_banner.html.haml
new file mode 100644
index 00000000000..d0402197821
--- /dev/null
+++ b/app/views/projects/clusters/_gcp_signup_offer_banner.html.haml
@@ -0,0 +1,12 @@
+- link = link_to(s_('ClusterIntegration|sign up'), 'https://console.cloud.google.com/freetrial?utm_campaign=2018_cpanel&utm_source=gitlab&utm_medium=referral', target: '_blank', rel: 'noopener noreferrer')
+.gcp-signup-offer.alert.alert-block.alert-dismissable.prepend-top-default.append-bottom-default{ role: 'alert' }
+ %button.close{ type: "button", data: { feature_id: UserCalloutsHelper::GCP_SIGNUP_OFFER, dismiss_endpoint: user_callouts_path } } &times;
+ %div
+ .col-sm-2.gcp-logo
+ = image_tag 'illustrations/logos/google-cloud-platform_logo.svg'
+ .col-sm-10
+ %h4= s_('ClusterIntegration|Redeem up to $500 in free credit for Google Cloud Platform')
+ %p= s_('ClusterIntegration|Every new Google Cloud Platform (GCP) account receives $300 in credit upon %{sign_up_link}. In partnership with Google, GitLab is able to offer an additional $200 for new GCP accounts to get started with GitLab\'s Google Kubernetes Engine Integration.').html_safe % { sign_up_link: link }
+ %a.btn.btn-info{ href: 'https://goo.gl/AaJzRW', target: '_blank', rel: 'noopener noreferrer' }
+ Apply for credit
+
diff --git a/app/views/projects/clusters/gcp/login.html.haml b/app/views/projects/clusters/gcp/login.html.haml
index 5d8e2e812ca..f1771349a53 100644
--- a/app/views/projects/clusters/gcp/login.html.haml
+++ b/app/views/projects/clusters/gcp/login.html.haml
@@ -1,6 +1,8 @@
- breadcrumb_title 'Kubernetes'
- page_title _("Login")
+= render_gcp_signup_offer
+
.row.prepend-top-default
.col-sm-4
= render 'projects/clusters/sidebar'
diff --git a/app/views/projects/clusters/index.html.haml b/app/views/projects/clusters/index.html.haml
index 17b244f4bf7..a55de84b5cd 100644
--- a/app/views/projects/clusters/index.html.haml
+++ b/app/views/projects/clusters/index.html.haml
@@ -1,6 +1,8 @@
- breadcrumb_title 'Kubernetes'
- page_title "Kubernetes Clusters"
+= render_gcp_signup_offer
+
.clusters-container
- if @clusters.empty?
= render "empty_state"
diff --git a/app/views/projects/clusters/new.html.haml b/app/views/projects/clusters/new.html.haml
index e004966bdcc..828e2a84753 100644
--- a/app/views/projects/clusters/new.html.haml
+++ b/app/views/projects/clusters/new.html.haml
@@ -1,6 +1,8 @@
- breadcrumb_title 'Kubernetes'
- page_title _("Kubernetes Cluster")
+= render_gcp_signup_offer
+
.row.prepend-top-default
.col-sm-4
= render 'sidebar'
diff --git a/app/views/projects/commit/_commit_box.html.haml b/app/views/projects/commit/_commit_box.html.haml
index 6481d948138..5e108f876b4 100644
--- a/app/views/projects/commit/_commit_box.html.haml
+++ b/app/views/projects/commit/_commit_box.html.haml
@@ -54,7 +54,7 @@
%h3.commit-title
= markdown_field(@commit, :title)
- if @commit.description.present?
- %pre.commit-description
+ .commit-description<
= preserve(markdown_field(@commit, :description))
.info-well
diff --git a/app/views/projects/commits/_commit.html.haml b/app/views/projects/commits/_commit.html.haml
index 3bbc701109b..296939d13cf 100644
--- a/app/views/projects/commits/_commit.html.haml
+++ b/app/views/projects/commits/_commit.html.haml
@@ -36,16 +36,16 @@
- if commit.description?
%button.text-expander.d-none.d-sm-inline-block.js-toggle-button{ type: "button" } ...
- - if commit.description?
- %pre.commit-row-description.js-toggle-content
- = preserve(markdown_field(commit, :description))
-
.commiter
- commit_author_link = commit_author_link(commit, avatar: false, size: 24)
- commit_timeago = time_ago_with_tooltip(commit.authored_date, placement: 'bottom')
- commit_text = _('%{commit_author_link} authored %{commit_timeago}') % { commit_author_link: commit_author_link, commit_timeago: commit_timeago }
#{ commit_text.html_safe }
+ - if commit.description?
+ %pre.commit-row-description.js-toggle-content.prepend-top-8.append-bottom-8
+ = preserve(markdown_field(commit, :description))
+
.commit-actions.flex-row.d-none.d-sm-flex
- if request.xhr?
= render partial: 'projects/commit/signature', object: commit.signature
diff --git a/app/views/projects/commits/show.html.haml b/app/views/projects/commits/show.html.haml
index f17f4af5199..9d254463fb6 100644
--- a/app/views/projects/commits/show.html.haml
+++ b/app/views/projects/commits/show.html.haml
@@ -24,7 +24,7 @@
= link_to _("Create merge request"), create_mr_path(@repository.root_ref, @ref), class: 'btn btn-success'
.control
- = form_tag(project_commits_path(@project, @id), method: :get, class: 'commits-search-form', data: { 'signatures-path' => namespace_project_signatures_path }) do
+ = form_tag(project_commits_path(@project, @id), method: :get, class: 'commits-search-form js-signature-container', data: { 'signatures-path' => namespace_project_signatures_path }) do
= search_field_tag :search, params[:search], { placeholder: _('Filter by commit message'), id: 'commits-search', class: 'form-control search-text-input input-short', spellcheck: false }
.control
= link_to project_commits_path(@project, @ref, rss_url_options), title: _("Commits feed"), class: 'btn' do
diff --git a/app/views/projects/compare/_form.html.haml b/app/views/projects/compare/_form.html.haml
index 3d666f31a1e..ad21bc25405 100644
--- a/app/views/projects/compare/_form.html.haml
+++ b/app/views/projects/compare/_form.html.haml
@@ -1,4 +1,4 @@
-= form_tag project_compare_index_path(@project), method: :post, class: 'form-inline js-requires-input' do
+= form_tag project_compare_index_path(@project), method: :post, class: 'form-inline js-requires-input js-signature-container', data: { 'signatures-path' => signatures_namespace_project_compare_index_path } do
.clearfix
- if params[:to] && params[:from]
.compare-switch-container
diff --git a/app/views/projects/deploy_keys/_index.html.haml b/app/views/projects/deploy_keys/_index.html.haml
index 7dd8dc28e5b..6af57d3ab26 100644
--- a/app/views/projects/deploy_keys/_index.html.haml
+++ b/app/views/projects/deploy_keys/_index.html.haml
@@ -12,4 +12,4 @@
Create a new deploy key for this project
= render @deploy_keys.form_partial_path
%hr
- #js-deploy-keys{ data: { endpoint: project_deploy_keys_path(@project) } }
+ #js-deploy-keys{ data: { endpoint: project_deploy_keys_path(@project), project_id: @project.id } }
diff --git a/app/views/projects/diffs/_diffs.html.haml b/app/views/projects/diffs/_diffs.html.haml
index 14d95855e9b..d3b82e72d32 100644
--- a/app/views/projects/diffs/_diffs.html.haml
+++ b/app/views/projects/diffs/_diffs.html.haml
@@ -8,7 +8,7 @@
.files-changed-inner
.inline-parallel-buttons.d-none.d-sm-none.d-md-block
- if !diffs_expanded? && diff_files.any? { |diff_file| diff_file.collapsed? }
- = link_to 'Expand all', url_for(params.merge(expanded: 1, format: nil)), class: 'btn btn-secondary'
+ = link_to 'Expand all', url_for(safe_params.merge(expanded: 1, format: nil)), class: 'btn btn-secondary'
- if show_whitespace_toggle
- if current_controller?(:commit)
= commit_diff_whitespace_link(diffs.project, @commit, class: 'd-none d-sm-inline-block')
diff --git a/app/views/projects/jobs/_sidebar.html.haml b/app/views/projects/jobs/_sidebar.html.haml
index 9c2b9b4bf34..05eee5bb5d2 100644
--- a/app/views/projects/jobs/_sidebar.html.haml
+++ b/app/views/projects/jobs/_sidebar.html.haml
@@ -15,7 +15,7 @@
- elsif @build.has_expiring_artifacts?
%p.build-detail-row
The artifacts will be removed in
- %span= time_ago_in_words @build.artifacts_expire_at
+ %span= time_ago_with_tooltip @build.artifacts_expire_at
- if @build.artifacts?
.btn-group.btn-group.d-flex{ role: :group }
diff --git a/app/views/projects/merge_requests/creations/_new_compare.html.haml b/app/views/projects/merge_requests/creations/_new_compare.html.haml
index 03174a21fe2..ce2bb868ee0 100644
--- a/app/views/projects/merge_requests/creations/_new_compare.html.haml
+++ b/app/views/projects/merge_requests/creations/_new_compare.html.haml
@@ -3,15 +3,15 @@
= form_for [@project.namespace.becomes(Namespace), @project, @merge_request], url: project_new_merge_request_path(@project), method: :get, html: { class: "merge-request-form form-inline js-requires-input" } do |f|
.hide.alert.alert-danger.mr-compare-errors
- .js-merge-request-new-compare.row.col-md-12{ 'data-target-project-url': project_new_merge_request_update_branches_path(@source_project), 'data-source-branch-url': project_new_merge_request_branch_from_path(@source_project), 'data-target-branch-url': project_new_merge_request_branch_to_path(@source_project) }
- .col-md-6.pl-0
+ .js-merge-request-new-compare.row{ 'data-source-branch-url': project_new_merge_request_branch_from_path(@source_project), 'data-target-branch-url': project_new_merge_request_branch_to_path(@source_project) }
+ .col-lg-6
.card.card-new-merge-request
.card-header
Source branch
.card-body.clearfix
.merge-request-select.dropdown
= f.hidden_field :source_project_id
- = dropdown_toggle @merge_request.source_project_path, { toggle: "dropdown", field_name: "#{f.object_name}[source_project_id]", disabled: @merge_request.persisted? }, { toggle_class: "js-compare-dropdown js-source-project" }
+ = dropdown_toggle @merge_request.source_project_path, { toggle: "dropdown", 'field-name': "#{f.object_name}[source_project_id]", disabled: @merge_request.persisted? }, { toggle_class: "js-compare-dropdown js-source-project" }
.dropdown-menu.dropdown-menu-selectable.dropdown-source-project
= dropdown_title("Select source project")
= dropdown_filter("Search projects")
@@ -21,19 +21,17 @@
selected: f.object.source_project_id
.merge-request-select.dropdown
= f.hidden_field :source_branch
- = dropdown_toggle f.object.source_branch || "Select source branch", { toggle: "dropdown", display: 'static', field_name: "#{f.object_name}[source_branch]" }, { toggle_class: "js-compare-dropdown js-source-branch git-revision-dropdown-toggle" }
- .dropdown-menu.dropdown-menu-selectable.dropdown-source-branch.git-revision-dropdown
- = dropdown_title("Select source branch")
- = dropdown_filter("Search branches")
- = dropdown_content do
- = render 'projects/merge_requests/dropdowns/branch',
- branches: @merge_request.source_branches,
- selected: f.object.source_branch
- .card-footer
+ = dropdown_toggle f.object.source_branch || _("Select source branch"), { toggle: "dropdown", 'field-name': "#{f.object_name}[source_branch]", 'refs-url': refs_project_path(@source_project), selected: f.object.source_branch }, { toggle_class: "js-compare-dropdown js-source-branch git-revision-dropdown-toggle" }
+ .dropdown-menu.dropdown-menu-selectable.js-source-branch-dropdown.git-revision-dropdown
+ = dropdown_title(_("Select source branch"))
+ = dropdown_filter(_("Search branches"))
+ = dropdown_content
+ = dropdown_loading
+ .panel-footer
.text-center= icon('spinner spin', class: 'js-source-loading')
%ul.list-unstyled.mr_source_commit
- .col-md-6.pl-0
+ .col-lg-6
.card.card-new-merge-request
.card-header
Target branch
@@ -41,7 +39,7 @@
- projects = target_projects(@project)
.merge-request-select.dropdown
= f.hidden_field :target_project_id
- = dropdown_toggle f.object.target_project.full_path, { toggle: "dropdown", field_name: "#{f.object_name}[target_project_id]", disabled: @merge_request.persisted? }, { toggle_class: "js-compare-dropdown js-target-project" }
+ = dropdown_toggle f.object.target_project.full_path, { toggle: "dropdown", 'field-name': "#{f.object_name}[target_project_id]", disabled: @merge_request.persisted? }, { toggle_class: "js-compare-dropdown js-target-project" }
.dropdown-menu.dropdown-menu-selectable.dropdown-target-project
= dropdown_title("Select target project")
= dropdown_filter("Search projects")
@@ -51,14 +49,12 @@
selected: f.object.target_project_id
.merge-request-select.dropdown
= f.hidden_field :target_branch
- = dropdown_toggle f.object.target_branch, { toggle: "dropdown", display: 'static', field_name: "#{f.object_name}[target_branch]" }, { toggle_class: "js-compare-dropdown js-target-branch git-revision-dropdown-toggle" }
- .dropdown-menu.dropdown-menu-selectable.dropdown-target-branch.js-target-branch-dropdown.git-revision-dropdown
- = dropdown_title("Select target branch")
- = dropdown_filter("Search branches")
- = dropdown_content do
- = render 'projects/merge_requests/dropdowns/branch',
- branches: @merge_request.target_branches,
- selected: f.object.target_branch
+ = dropdown_toggle f.object.target_branch, { toggle: "dropdown", 'field-name': "#{f.object_name}[target_branch]", 'refs-url': refs_project_path(f.object.target_project), selected: f.object.target_branch }, { toggle_class: "js-compare-dropdown js-target-branch git-revision-dropdown-toggle" }
+ .dropdown-menu.dropdown-menu-selectable.js-target-branch-dropdown.git-revision-dropdown
+ = dropdown_title(_("Select target branch"))
+ = dropdown_filter(_("Search branches"))
+ = dropdown_content
+ = dropdown_loading
.card-footer
.text-center= icon('spinner spin', class: "js-target-loading")
%ul.list-unstyled.mr_target_commit
diff --git a/app/views/projects/merge_requests/dropdowns/_project.html.haml b/app/views/projects/merge_requests/dropdowns/_project.html.haml
index aaf1ab00eeb..b3cf3c1d369 100644
--- a/app/views/projects/merge_requests/dropdowns/_project.html.haml
+++ b/app/views/projects/merge_requests/dropdowns/_project.html.haml
@@ -1,5 +1,5 @@
%ul
- projects.each do |project|
%li
- %a{ href: "#", class: "#{('is-active' if selected == project.id)}", data: { id: project.id } }
+ %a{ href: "#", class: "#{('is-active' if selected == project.id)}", data: { id: project.id, 'refs-url': refs_project_path(project) } }
= project.full_path
diff --git a/app/views/projects/mirrors/_instructions.html.haml b/app/views/projects/mirrors/_instructions.html.haml
new file mode 100644
index 00000000000..64f0fde30cf
--- /dev/null
+++ b/app/views/projects/mirrors/_instructions.html.haml
@@ -0,0 +1,10 @@
+.account-well.prepend-top-default.append-bottom-default
+ %ul
+ %li
+ The repository must be accessible over <code>http://</code>, <code>https://</code>, <code>ssh://</code> or <code>git://</code>.
+ %li
+ Include the username in the URL if required: <code>https://username@gitlab.company.com/group/project.git</code>.
+ %li
+ The update action will time out after 10 minutes. For big repositories, use a clone/push combination.
+ %li
+ The Git LFS objects will <strong>not</strong> be synced.
diff --git a/app/views/projects/mirrors/_push.html.haml b/app/views/projects/mirrors/_push.html.haml
new file mode 100644
index 00000000000..4a6aefce351
--- /dev/null
+++ b/app/views/projects/mirrors/_push.html.haml
@@ -0,0 +1,50 @@
+- expanded = Rails.env.test?
+%section.settings.no-animate{ class: ('expanded' if expanded) }
+ .settings-header
+ %h4
+ Push to a remote repository
+ %button.btn.js-settings-toggle
+ = expanded ? 'Collapse' : 'Expand'
+ %p
+ Set up the remote repository that you want to update with the content of the current repository
+ every time someone pushes to it.
+ = link_to 'Read more', help_page_path('workflow/repository_mirroring', anchor: 'pushing-to-a-remote-repository'), target: '_blank'
+ .settings-content
+ = form_for @project, url: project_mirror_path(@project) do |f|
+ %div
+ = form_errors(@project)
+ = render "shared/remote_mirror_update_button", remote_mirror: @remote_mirror
+ - if @remote_mirror.last_error.present?
+ .panel.panel-danger
+ .panel-heading
+ - if @remote_mirror.last_update_at
+ The remote repository failed to update #{time_ago_with_tooltip(@remote_mirror.last_update_at)}.
+ - else
+ The remote repository failed to update.
+
+ - if @remote_mirror.last_successful_update_at
+ Last successful update #{time_ago_with_tooltip(@remote_mirror.last_successful_update_at)}.
+ .panel-body
+ %pre
+ :preserve
+ #{h(@remote_mirror.last_error.strip)}
+ = f.fields_for :remote_mirrors, @remote_mirror do |rm_form|
+ .form-group
+ = rm_form.check_box :enabled, class: "pull-left"
+ .prepend-left-20
+ = rm_form.label :enabled, "Remote mirror repository", class: "label-light append-bottom-0"
+ %p.light.append-bottom-0
+ Automatically update the remote mirror's branches, tags, and commits from this repository every time someone pushes to it.
+ .form-group.has-feedback
+ = rm_form.label :url, "Git repository URL", class: "label-light"
+ = rm_form.text_field :url, class: "form-control", placeholder: 'https://username:password@gitlab.company.com/group/project.git'
+
+ = render "projects/mirrors/instructions"
+
+ .form-group
+ = rm_form.check_box :only_protected_branches, class: 'pull-left'
+ .prepend-left-20
+ = rm_form.label :only_protected_branches, class: 'label-light'
+ = link_to icon('question-circle'), help_page_path('user/project/protected_branches')
+
+ = f.submit 'Save changes', class: 'btn btn-create', name: 'update_remote_mirror'
diff --git a/app/views/projects/mirrors/_show.html.haml b/app/views/projects/mirrors/_show.html.haml
new file mode 100644
index 00000000000..de77701a373
--- /dev/null
+++ b/app/views/projects/mirrors/_show.html.haml
@@ -0,0 +1,3 @@
+- if can?(current_user, :admin_remote_mirror, @project)
+ = render 'projects/mirrors/push'
+
diff --git a/app/views/projects/new.html.haml b/app/views/projects/new.html.haml
index d2b4ed5c0b4..35a09f06bfa 100644
--- a/app/views/projects/new.html.haml
+++ b/app/views/projects/new.html.haml
@@ -57,54 +57,11 @@
.tab-pane.import-project-pane.js-toggle-container{ id: 'import-project-pane', class: active_when(active_tab == 'import'), role: 'tabpanel' }
= form_for @project, html: { class: 'new_project' } do |f|
- if import_sources_enabled?
- .project-import.row
- .col-lg-12
- .form-group.import-btn-container.clearfix
- = f.label :visibility_level, class: 'label-light' do #the label here seems wrong
- Import project from
- .import-buttons
- - if gitlab_project_import_enabled?
- .import_gitlab_project.has-tooltip{ data: { container: 'body' } }
- = link_to new_import_gitlab_project_path, class: 'btn btn_import_gitlab_project project-submit' do
- = icon('gitlab', text: 'GitLab export')
- %div
- - if github_import_enabled?
- = link_to new_import_github_path, class: 'btn js-import-github' do
- = icon('github', text: 'GitHub')
- %div
- - if bitbucket_import_enabled?
- = link_to status_import_bitbucket_path, class: "btn import_bitbucket #{'how_to_import_link' unless bitbucket_import_configured?}" do
- = icon('bitbucket', text: 'Bitbucket')
- - unless bitbucket_import_configured?
- = render 'bitbucket_import_modal'
- %div
- - if gitlab_import_enabled?
- = link_to status_import_gitlab_path, class: "btn import_gitlab #{'how_to_import_link' unless gitlab_import_configured?}" do
- = icon('gitlab', text: 'GitLab.com')
- - unless gitlab_import_configured?
- = render 'gitlab_import_modal'
- %div
- - if google_code_import_enabled?
- = link_to new_import_google_code_path, class: 'btn import_google_code' do
- = icon('google', text: 'Google Code')
- %div
- - if fogbugz_import_enabled?
- = link_to new_import_fogbugz_path, class: 'btn import_fogbugz' do
- = icon('bug', text: 'Fogbugz')
- %div
- - if gitea_import_enabled?
- = link_to new_import_gitea_path, class: 'btn import_gitea' do
- = custom_icon('go_logo')
- Gitea
- %div
- - if git_import_enabled?
- %button.btn.js-toggle-button.js-import-git-toggle-button{ type: "button", data: { toggle_open_class: 'active' } }
- = icon('git', text: 'Repo by URL')
- .col-lg-12
- .js-toggle-content.toggle-import-form{ class: ('hide' if active_tab != 'import') }
- %hr
- = render "shared/import_form", f: f
- = render 'new_project_fields', f: f, project_name_id: "import-url-name"
+ = render 'import_project_pane', f: f, active_tab: active_tab
+ - else
+ .nothing-here-block
+ %h4 No import options available
+ %p Contact an administrator to enable options for importing your project.
.save-project-loader.d-none
.center
diff --git a/app/views/projects/pipelines/_info.html.haml b/app/views/projects/pipelines/_info.html.haml
index ca373e39d61..aa53fc3ea28 100644
--- a/app/views/projects/pipelines/_info.html.haml
+++ b/app/views/projects/pipelines/_info.html.haml
@@ -5,7 +5,7 @@
%h3.commit-title
= markdown(@commit.title, pipeline: :single_line)
- if @commit.description.present?
- %pre.commit-description
+ .commit-description<
= preserve(markdown(@commit.description, pipeline: :single_line))
.info-well
diff --git a/app/views/projects/pipelines/_with_tabs.html.haml b/app/views/projects/pipelines/_with_tabs.html.haml
index 88c9b6c45f2..118391aac64 100644
--- a/app/views/projects/pipelines/_with_tabs.html.haml
+++ b/app/views/projects/pipelines/_with_tabs.html.haml
@@ -1,5 +1,3 @@
-- failed_builds = @pipeline.statuses.latest.failed
-
.tabs-holder
%ul.pipelines-tabs.nav-links.no-top.no-bottom.mobile-separator.nav.nav-tabs
%li.js-pipeline-tab-link
@@ -9,11 +7,11 @@
= link_to builds_project_pipeline_path(@project, @pipeline), data: { target: '#js-tab-builds', action: 'builds', toggle: 'tab' }, class: 'builds-tab' do
= _("Jobs")
%span.badge.badge-pill.js-builds-counter= pipeline.total_size
- - if failed_builds.present?
+ - if @pipeline.failed_builds.present?
%li.js-failures-tab-link
= link_to failures_project_pipeline_path(@project, @pipeline), data: { target: '#js-tab-failures', action: 'failures', toggle: 'tab' }, class: 'failures-tab' do
= _("Failed Jobs")
- %span.badge.badge-pill.js-failures-counter= failed_builds.count
+ %span.badge.badge-pill.js-failures-counter= @pipeline.failed_builds.count
.tab-content
#js-tab-pipeline.tab-pane
@@ -43,9 +41,10 @@
%th Coverage
%th
= render partial: "projects/stage/stage", collection: pipeline.legacy_stages, as: :stage
- - if failed_builds.present?
+
+ - if @pipeline.failed_builds.present?
#js-tab-failures.build-failures.tab-pane
- - failed_builds.each_with_index do |build, index|
+ - @pipeline.failed_builds.each_with_index do |build, index|
.build-state
%span.ci-status-icon-failed= custom_icon('icon_status_failed')
%span.stage
diff --git a/app/views/projects/pipelines/new.html.haml b/app/views/projects/pipelines/new.html.haml
index f9ac75c789a..d1e8e9d0d60 100644
--- a/app/views/projects/pipelines/new.html.haml
+++ b/app/views/projects/pipelines/new.html.haml
@@ -1,5 +1,6 @@
- breadcrumb_title "Pipelines"
- page_title = s_("Pipeline|Run Pipeline")
+- settings_link = link_to _('CI/CD settings'), project_settings_ci_cd_path(@project)
%h3.page-title
= s_("Pipeline|Run Pipeline")
@@ -8,17 +9,26 @@
= form_for @pipeline, as: :pipeline, url: project_pipelines_path(@project), html: { id: "new-pipeline-form", class: "js-new-pipeline-form js-requires-input" } do |f|
= form_errors(@pipeline)
.form-group.row
- = f.label :ref, s_('Pipeline|Run on'), class: 'col-form-label col-sm-2'
- .col-sm-10
+ .col-sm-12
+ = f.label :ref, s_('Pipeline|Create for')
= hidden_field_tag 'pipeline[ref]', params[:ref] || @project.default_branch
= dropdown_tag(params[:ref] || @project.default_branch,
options: { toggle_class: 'js-branch-select wide git-revision-dropdown-toggle',
filter: true, dropdown_class: "dropdown-menu-selectable git-revision-dropdown", placeholder: s_("Pipeline|Search branches"),
data: { selected: params[:ref] || @project.default_branch, field_name: 'pipeline[ref]' } })
.form-text.text-muted
- = s_("Pipeline|Existing branch name, tag")
+ = s_("Pipeline|Existing branch name or tag")
+
+ .col-sm-12.prepend-top-10.js-ci-variable-list-section
+ %label
+ = s_('Pipeline|Variables')
+ %ul.ci-variable-list
+ = render 'ci/variables/variable_row', form_field: 'pipeline', only_key_value: true
+ .form-text.text-muted
+ = (s_("Pipeline|Specify variable values to be used in this run. The values specified in %{settings_link} will be used by default.") % {settings_link: settings_link}).html_safe
+
.form-actions
- = f.submit s_('Pipeline|Run pipeline'), class: 'btn btn-success', tabindex: 3
+ = f.submit s_('Pipeline|Create pipeline'), class: 'btn btn-success js-variables-save-button', tabindex: 3
= link_to 'Cancel', project_pipelines_path(@project), class: 'btn btn-default pull-right'
-# haml-lint:disable InlineJavaScript
diff --git a/app/views/projects/registry/repositories/_tag.html.haml b/app/views/projects/registry/repositories/_tag.html.haml
index 7b5e31bc4b7..a4cde53e8c6 100644
--- a/app/views/projects/registry/repositories/_tag.html.haml
+++ b/app/views/projects/registry/repositories/_tag.html.haml
@@ -18,7 +18,7 @@
\-
%td
- if tag.created_at
- = time_ago_in_words(tag.created_at)
+ = time_ago_with_tooltip tag.created_at
- else
.light
\-
diff --git a/app/views/projects/runners/_group_runners.html.haml b/app/views/projects/runners/_group_runners.html.haml
new file mode 100644
index 00000000000..ec8011182de
--- /dev/null
+++ b/app/views/projects/runners/_group_runners.html.haml
@@ -0,0 +1,37 @@
+- link = link_to 'Runners API', help_page_path('api/runners.md')
+
+%h3
+ = _('Group Runners')
+
+.bs-callout.bs-callout-warning
+ = _('GitLab Group Runners can execute code for all the projects in this group.')
+ = _('They can be managed using the %{link}.').html_safe % { link: link }
+
+ - if @project.group
+ %hr
+ - if @project.group_runners_enabled?
+ = link_to toggle_group_runners_project_runners_path(@project), class: 'btn btn-close', method: :post do
+ = _('Disable group Runners')
+ - else
+ = link_to toggle_group_runners_project_runners_path(@project), class: 'btn btn-success btn-inverted', method: :post do
+ = _('Enable group Runners')
+ &nbsp;
+ = _('for this project')
+
+- if !@project.group
+ = _('This project does not belong to a group and can therefore not make use of group Runners.')
+
+- elsif @group_runners.empty?
+ = _('This group does not provide any group Runners yet.')
+
+ - if can?(current_user, :admin_pipeline, @project.group)
+ - group_link = link_to 'Group CI/CD settings', group_settings_ci_cd_path(@project.group)
+ = _('Group masters can register group runners in the %{link}').html_safe % { link: group_link }
+ - else
+ = _('Ask your group master to setup a group Runner.')
+
+- else
+ %h4.underlined-title
+ = _('Available group Runners : %{runners}').html_safe % { runners: @group_runners.count }
+ %ul.bordered-list
+ = render partial: 'projects/runners/runner', collection: @group_runners, as: :runner
diff --git a/app/views/projects/runners/_index.html.haml b/app/views/projects/runners/_index.html.haml
index 63dcf1fcc16..022687b831f 100644
--- a/app/views/projects/runners/_index.html.haml
+++ b/app/views/projects/runners/_index.html.haml
@@ -1,19 +1,4 @@
-.light.prepend-top-default
- %p
- A 'Runner' is a process which runs a job.
- You can setup as many Runners as you need.
- %br
- Runners can be placed on separate users, servers, and even on your local machine.
-
- %p Each Runner can be in one of the following states:
- %div
- %ul
- %li
- %span.badge.badge-success active
- \- Runner is active and can process any new jobs
- %li
- %span.badge.badge-danger paused
- \- Runner is paused and will not receive any new jobs
+= render 'shared/runners/runner_description'
%hr
@@ -23,3 +8,4 @@
= render 'projects/runners/specific_runners'
.col-sm-6
= render 'projects/runners/shared_runners'
+ = render 'projects/runners/group_runners'
diff --git a/app/views/projects/runners/_runner.html.haml b/app/views/projects/runners/_runner.html.haml
index 955300685b2..a23f5d6f0c3 100644
--- a/app/views/projects/runners/_runner.html.haml
+++ b/app/views/projects/runners/_runner.html.haml
@@ -3,10 +3,10 @@
= runner_status_icon(runner)
- if @project_runners.include?(runner)
- = link_to runner.short_sha, runner_path(runner), class: 'commit-sha'
+ = link_to runner.short_sha, project_runner_path(@project, runner), class: 'commit-sha'
- if runner.locked?
- = icon('lock', class: 'has-tooltip', title: 'Locked to current projects')
+ = icon('lock', class: 'has-tooltip', title: _('Locked to current projects'))
%small.edit-runner
= link_to edit_project_runner_path(@project, runner) do
@@ -18,18 +18,18 @@
.float-right
- if @project_runners.include?(runner)
- if runner.active?
- = link_to 'Pause', pause_project_runner_path(@project, runner), method: :post, class: 'btn btn-sm btn-danger', data: { confirm: "Are you sure?" }
+ = link_to _('Pause'), pause_project_runner_path(@project, runner), method: :post, class: 'btn btn-sm btn-danger', data: { confirm: _("Are you sure?") }
- else
- = link_to 'Resume', resume_project_runner_path(@project, runner), method: :post, class: 'btn btn-success btn-sm'
+ = link_to _('Resume'), resume_project_runner_path(@project, runner), method: :post, class: 'btn btn-success btn-sm'
- if runner.belongs_to_one_project?
- = link_to 'Remove Runner', runner_path(runner), data: { confirm: "Are you sure?" }, method: :delete, class: 'btn btn-danger btn-sm'
+ = link_to _('Remove Runner'), project_runner_path(@project, runner), data: { confirm: _("Are you sure?") }, method: :delete, class: 'btn btn-danger btn-sm'
- else
- runner_project = @project.runner_projects.find_by(runner_id: runner)
- = link_to 'Disable for this project', project_runner_project_path(@project, runner_project), data: { confirm: "Are you sure?" }, method: :delete, class: 'btn btn-danger btn-sm'
- - elsif runner.specific?
+ = link_to _('Disable for this project'), project_runner_project_path(@project, runner_project), data: { confirm: _("Are you sure?") }, method: :delete, class: 'btn btn-danger btn-sm'
+ - elsif !(runner.is_shared? || runner.group_type?) # We can simplify this to `runner.project_type?` when migrating #runner_type is complete
= form_for [@project.namespace.becomes(Namespace), @project, @project.runner_projects.new] do |f|
= f.hidden_field :runner_id, value: runner.id
- = f.submit 'Enable for this project', class: 'btn btn-sm'
+ = f.submit _('Enable for this project'), class: 'btn btn-sm'
.float-right
%small.light
\##{runner.id}
diff --git a/app/views/projects/runners/_shared_runners.html.haml b/app/views/projects/runners/_shared_runners.html.haml
index 4fd4ca355a8..969efdb2560 100644
--- a/app/views/projects/runners/_shared_runners.html.haml
+++ b/app/views/projects/runners/_shared_runners.html.haml
@@ -17,8 +17,8 @@
&nbsp; for this project
- if @shared_runners_count.zero?
- This GitLab server does not provide any shared Runners yet.
- Please use the specific Runners or ask your administrator to create one.
+ This GitLab instance does not provide any shared Runners yet. Instance
+ administrators can register shared Runners in the admin area.
- else
%h4.underlined-title Available shared Runners : #{@shared_runners_count}
%ul.bordered-list.available-shared-runners
diff --git a/app/views/projects/runners/edit.html.haml b/app/views/projects/runners/edit.html.haml
index 78dc4817ed7..56eb526bfd5 100644
--- a/app/views/projects/runners/edit.html.haml
+++ b/app/views/projects/runners/edit.html.haml
@@ -3,4 +3,4 @@
%h4 Runner ##{@runner.id}
%hr
- = render 'form', runner: @runner, runner_form_url: runner_path(@runner)
+ = render 'shared/runners/form', runner: @runner, runner_form_url: project_runner_path(@project, @runner)
diff --git a/app/views/projects/services/_index.html.haml b/app/views/projects/services/_index.html.haml
index 7a945ccc3de..acbab8b85c9 100644
--- a/app/views/projects/services/_index.html.haml
+++ b/app/views/projects/services/_index.html.haml
@@ -27,5 +27,4 @@
= service.description
%td.light
- if service.updated_at.present?
- = time_ago_in_words service.updated_at
- ago
+ = time_ago_with_tooltip service.updated_at
diff --git a/app/views/projects/settings/repository/show.html.haml b/app/views/projects/settings/repository/show.html.haml
index f57590a908f..5dda2ec28b4 100644
--- a/app/views/projects/settings/repository/show.html.haml
+++ b/app/views/projects/settings/repository/show.html.haml
@@ -2,6 +2,8 @@
- page_title "Repository"
- @content_class = "limit-container-width" unless fluid_layout
+= render "projects/mirrors/show"
+
-# Protected branches & tags use a lot of nested partials.
-# The shared parts of the views can be found in the `shared` directory.
-# Those are used throughout the actual views. These `shared` views are then
diff --git a/app/views/projects/triggers/_trigger.html.haml b/app/views/projects/triggers/_trigger.html.haml
index f6e966b1f84..ed9f2c32f4b 100644
--- a/app/views/projects/triggers/_trigger.html.haml
+++ b/app/views/projects/triggers/_trigger.html.haml
@@ -25,7 +25,7 @@
%td
- if trigger.last_used
- #{time_ago_in_words(trigger.last_used)} ago
+ = time_ago_with_tooltip trigger.last_used
- else
Never
diff --git a/app/views/shared/_mini_pipeline_graph.html.haml b/app/views/shared/_mini_pipeline_graph.html.haml
index 901a177323b..ac2164a4a71 100644
--- a/app/views/shared/_mini_pipeline_graph.html.haml
+++ b/app/views/shared/_mini_pipeline_graph.html.haml
@@ -6,12 +6,13 @@
- status_klass = "ci-status-icon ci-status-icon-#{detailed_status.group}"
.stage-container.dropdown{ class: klass }
- %button.mini-pipeline-graph-dropdown-toggle.has-tooltip.js-builds-dropdown-button{ class: "ci-status-icon-#{detailed_status.group}", type: 'button', data: { toggle: 'dropdown', title: "#{stage.name}: #{detailed_status.label}", placement: 'top', "stage-endpoint" => stage_project_pipeline_path(pipeline.project, pipeline, stage: stage.name) } }
+ %button.mini-pipeline-graph-dropdown-toggle.has-tooltip.js-builds-dropdown-button{ class: "ci-status-icon-#{detailed_status.group}", type: 'button', data: { toggle: 'dropdown', title: "#{stage.name}: #{detailed_status.label}", placement: 'top', "stage-endpoint" => stage_ajax_project_pipeline_path(pipeline.project, pipeline, stage: stage.name) } }
= sprite_icon(icon_status)
= icon('caret-down')
%ul.dropdown-menu.mini-pipeline-graph-dropdown-menu.js-builds-dropdown-container
%li.js-builds-dropdown-list.scrollable-menu
+ %ul
%li.js-builds-dropdown-loading.hidden
.text-center
diff --git a/app/views/shared/_remote_mirror_update_button.html.haml b/app/views/shared/_remote_mirror_update_button.html.haml
new file mode 100644
index 00000000000..34de1c0695f
--- /dev/null
+++ b/app/views/shared/_remote_mirror_update_button.html.haml
@@ -0,0 +1,13 @@
+- if @project.has_remote_mirror?
+ .append-bottom-default
+ - if remote_mirror.update_in_progress?
+ %span.btn.disabled
+ = icon("refresh spin")
+ Updating&hellip;
+ - else
+ = link_to update_now_project_mirror_path(@project, sync_remote: true), method: :post, class: "btn" do
+ = icon("refresh")
+ Update Now
+ - if @remote_mirror.last_successful_update_at
+ %p.inline.prepend-left-10
+ Successfully updated #{time_ago_with_tooltip(@remote_mirror.last_successful_update_at)}.
diff --git a/app/views/shared/boards/components/_board.html.haml b/app/views/shared/boards/components/_board.html.haml
index 9bde80f4b51..8006bc3222c 100644
--- a/app/views/shared/boards/components/_board.html.haml
+++ b/app/views/shared/boards/components/_board.html.haml
@@ -15,8 +15,9 @@
":title" => '(list.label ? list.label.description : "")',
data: { container: "body", placement: "bottom" },
class: "badge color-label title board-title-text",
- ":style" => "{ backgroundColor: (list.label && list.label.color ? list.label.color : null), color: (list.label && list.label.color ? list.label.text_color : \"#2e2e2e\") }" }
+ ":style" => "{ backgroundColor: (list.label && list.label.color ? list.label.color : null), color: (list.label && list.label.text_color ? list.label.text_color : \"#2e2e2e\") }" }
{{ list.title }}
+
- if can?(current_user, :admin_list, current_board_parent)
%board-delete{ "inline-template" => true,
":list" => "list",
diff --git a/app/views/projects/runners/_form.html.haml b/app/views/shared/runners/_form.html.haml
index e8bc7705a50..e958f760cf3 100644
--- a/app/views/projects/runners/_form.html.haml
+++ b/app/views/shared/runners/_form.html.haml
@@ -18,12 +18,13 @@
.form-check
= f.check_box :run_untagged
%span.light Indicates whether this runner can pick jobs without tags
- .form-group.row
- = label :locked, 'Lock to current projects', class: 'col-form-label col-sm-2'
- .col-sm-10
- .form-check
- = f.check_box :locked
- %span.light When a runner is locked, it cannot be assigned to other projects
+ - unless runner.group_type?
+ .form-group.row
+ = label :locked, 'Lock to current projects', class: 'col-form-label col-sm-2'
+ .col-sm-10
+ .form-check
+ = f.check_box :locked
+ %span.light When a runner is locked, it cannot be assigned to other projects
.form-group.row
= label_tag :token, class: 'col-form-label col-sm-2' do
Token
diff --git a/app/views/shared/runners/_runner_description.html.haml b/app/views/shared/runners/_runner_description.html.haml
new file mode 100644
index 00000000000..da5c032add5
--- /dev/null
+++ b/app/views/shared/runners/_runner_description.html.haml
@@ -0,0 +1,16 @@
+.light.prepend-top-default
+ %p
+ = _("A 'Runner' is a process which runs a job. You can setup as many Runners as you need.")
+ %br
+ = _('Runners can be placed on separate users, servers, and even on your local machine.')
+
+ %p
+ = _('Each Runner can be in one of the following states:')
+ %div
+ %ul
+ %li
+ %span.badge.badge-success active
+ = _('- Runner is active and can process any new jobs')
+ %li
+ %span.badge.badge-danger paused
+ = _('- Runner is paused and will not receive any new jobs')
diff --git a/app/views/projects/runners/show.html.haml b/app/views/shared/runners/show.html.haml
index db7b7eb693d..3f6c5c9adc6 100644
--- a/app/views/projects/runners/show.html.haml
+++ b/app/views/shared/runners/show.html.haml
@@ -6,6 +6,9 @@
- if @runner.shared?
%span.runner-state.runner-state-shared
Shared
+ - elsif @runner.group_type?
+ %span.runner-state.runner-state-shared
+ Group
- else
%span.runner-state.runner-state-specific
Specific
@@ -25,9 +28,10 @@
%tr
%td Can run untagged jobs
%td= @runner.run_untagged? ? 'Yes' : 'No'
- %tr
- %td Locked to this project
- %td= @runner.locked? ? 'Yes' : 'No'
+ - unless @runner.group_type?
+ %tr
+ %td Locked to this project
+ %td= @runner.locked? ? 'Yes' : 'No'
%tr
%td Tags
%td
diff --git a/app/views/sherlock/transactions/_general.html.haml b/app/views/sherlock/transactions/_general.html.haml
index 25745b13e98..7ec8dde8421 100644
--- a/app/views/sherlock/transactions/_general.html.haml
+++ b/app/views/sherlock/transactions/_general.html.haml
@@ -35,5 +35,4 @@
%span.light
#{t('sherlock.finished_at')}:
%strong
- = time_ago_in_words(@transaction.finished_at)
- = t('sherlock.ago')
+ = time_ago_with_tooltip @transaction.finished_at
diff --git a/app/views/sherlock/transactions/index.html.haml b/app/views/sherlock/transactions/index.html.haml
index 2e9d4e2e3e1..4d9df01ae31 100644
--- a/app/views/sherlock/transactions/index.html.haml
+++ b/app/views/sherlock/transactions/index.html.haml
@@ -35,8 +35,7 @@
= t('sherlock.seconds')
%td= trans.queries.length
%td
- = time_ago_in_words(trans.finished_at)
- = t('sherlock.ago')
+ = time_ago_with_tooltip trans.finished_at
%td
= link_to(sherlock_transaction_path(trans), class: 'btn btn-sm') do
= t('sherlock.view')
diff --git a/app/views/users/show.html.haml b/app/views/users/show.html.haml
index 30abe9b7494..b2ec7166832 100644
--- a/app/views/users/show.html.haml
+++ b/app/views/users/show.html.haml
@@ -12,7 +12,7 @@
.cover-block.user-cover-block.top-area
.cover-controls
- if @user == current_user
- = link_to profile_path, class: 'btn btn-gray has-tooltip', title: 'Edit profile', 'aria-label': 'Edit profile' do
+ = link_to profile_path, class: 'btn btn-default has-tooltip', title: 'Edit profile', 'aria-label': 'Edit profile' do
= icon('pencil')
- elsif current_user
- if @user.abuse_report
@@ -20,13 +20,13 @@
data: { toggle: 'tooltip', placement: 'bottom', container: 'body' } }
= icon('exclamation-circle')
- else
- = link_to new_abuse_report_path(user_id: @user.id, ref_url: request.referrer), class: 'btn btn-gray',
+ = link_to new_abuse_report_path(user_id: @user.id, ref_url: request.referrer), class: 'btn',
title: 'Report abuse', data: { toggle: 'tooltip', placement: 'bottom', container: 'body' } do
= icon('exclamation-circle')
- = link_to user_path(@user, rss_url_options), class: 'btn btn-gray has-tooltip', title: 'Subscribe', 'aria-label': 'Subscribe' do
+ = link_to user_path(@user, rss_url_options), class: 'btn btn-default has-tooltip', title: 'Subscribe', 'aria-label': 'Subscribe' do
= icon('rss')
- if current_user && current_user.admin?
- = link_to [:admin, @user], class: 'btn btn-gray', title: 'View user in admin area',
+ = link_to [:admin, @user], class: 'btn btn-default', title: 'View user in admin area',
data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do
= icon('users')
@@ -35,7 +35,7 @@
= link_to avatar_icon_for_user(@user, 400), target: '_blank', rel: 'noopener noreferrer' do
= image_tag avatar_icon_for_user(@user, 90), class: "avatar s90", alt: ''
- .user-info
+ .user-info.prepend-left-default.append-right-default
.cover-title
= @user.name
diff --git a/app/views/users/terms/index.html.haml b/app/views/users/terms/index.html.haml
new file mode 100644
index 00000000000..c5406696bdd
--- /dev/null
+++ b/app/views/users/terms/index.html.haml
@@ -0,0 +1,13 @@
+- redirect_params = { redirect: @redirect } if @redirect
+
+.panel-content.rendered-terms
+ = markdown_field(@term, :terms)
+.row-content-block.footer-block.clearfix
+ - if can?(current_user, :accept_terms, @term)
+ .pull-right
+ = button_to accept_term_path(@term, redirect_params), class: 'btn btn-success prepend-left-8' do
+ = _('Accept terms')
+ - if can?(current_user, :decline_terms, @term)
+ .pull-right
+ = button_to decline_term_path(@term, redirect_params), class: 'btn btn-default prepend-left-8' do
+ = _('Decline and sign out')
diff --git a/app/workers/admin_email_worker.rb b/app/workers/admin_email_worker.rb
index bec0a003a1c..044e470141e 100644
--- a/app/workers/admin_email_worker.rb
+++ b/app/workers/admin_email_worker.rb
@@ -3,6 +3,12 @@ class AdminEmailWorker
include CronjobQueue
def perform
+ send_repository_check_mail if Gitlab::CurrentSettings.repository_checks_enabled
+ end
+
+ private
+
+ def send_repository_check_mail
repository_check_failed_count = Project.where(last_repository_check_failed: true).count
return if repository_check_failed_count.zero?
diff --git a/app/workers/all_queues.yml b/app/workers/all_queues.yml
index c469aea7052..b6433eb3eff 100644
--- a/app/workers/all_queues.yml
+++ b/app/workers/all_queues.yml
@@ -52,6 +52,7 @@
- pipeline_creation:create_pipeline
- pipeline_creation:run_pipeline_schedule
- pipeline_background:archive_trace
+- pipeline_background:ci_build_trace_chunk_flush
- pipeline_default:build_coverage
- pipeline_default:build_trace_sections
- pipeline_default:pipeline_metrics
@@ -106,9 +107,11 @@
- rebase
- repository_fork
- repository_import
+- repository_remove_remote
- storage_migrator
- system_hook_push
- update_merge_requests
- update_user_activity
- upload_checksum
- web_hook
+- repository_update_remote_mirror
diff --git a/app/workers/ci/build_trace_chunk_flush_worker.rb b/app/workers/ci/build_trace_chunk_flush_worker.rb
new file mode 100644
index 00000000000..218d6688bd9
--- /dev/null
+++ b/app/workers/ci/build_trace_chunk_flush_worker.rb
@@ -0,0 +1,12 @@
+module Ci
+ class BuildTraceChunkFlushWorker
+ include ApplicationWorker
+ include PipelineBackgroundQueue
+
+ def perform(build_trace_chunk_id)
+ ::Ci::BuildTraceChunk.find_by(id: build_trace_chunk_id).try do |build_trace_chunk|
+ build_trace_chunk.use_database!
+ end
+ end
+ end
+end
diff --git a/app/workers/gitlab/github_import/advance_stage_worker.rb b/app/workers/gitlab/github_import/advance_stage_worker.rb
index f7f498af840..8d708e15a66 100644
--- a/app/workers/gitlab/github_import/advance_stage_worker.rb
+++ b/app/workers/gitlab/github_import/advance_stage_worker.rb
@@ -63,11 +63,10 @@ module Gitlab
end
def find_project(id)
- # We only care about the import JID so we can refresh it. We also only
- # want the project if it hasn't been marked as failed yet. It's possible
- # the import gets marked as stuck when jobs of the current stage failed
- # somehow.
- Project.select(:import_jid).import_started.find_by(id: id)
+ # TODO: Only select the JID
+ # This is due to the fact that the JID could be present in either the project record or
+ # its associated import_state record
+ Project.import_started.find_by(id: id)
end
end
end
diff --git a/app/workers/gitlab/github_import/refresh_import_jid_worker.rb b/app/workers/gitlab/github_import/refresh_import_jid_worker.rb
index 7108b531bc2..68d2c5c4331 100644
--- a/app/workers/gitlab/github_import/refresh_import_jid_worker.rb
+++ b/app/workers/gitlab/github_import/refresh_import_jid_worker.rb
@@ -31,7 +31,10 @@ module Gitlab
end
def find_project(id)
- Project.select(:import_jid).import_started.find_by(id: id)
+ # TODO: Only select the JID
+ # This is due to the fact that the JID could be present in either the project record or
+ # its associated import_state record
+ Project.import_started.find_by(id: id)
end
end
end
diff --git a/app/workers/new_note_worker.rb b/app/workers/new_note_worker.rb
index b925741934a..67c54fbf10e 100644
--- a/app/workers/new_note_worker.rb
+++ b/app/workers/new_note_worker.rb
@@ -5,7 +5,7 @@ class NewNoteWorker
# old `NewNoteWorker` jobs (can remove later)
def perform(note_id, _params = {})
if note = Note.find_by(id: note_id)
- NotificationService.new.new_note(note) if note.can_create_notification?
+ NotificationService.new.new_note(note)
Notes::PostProcessService.new(note).execute
else
Rails.logger.error("NewNoteWorker: couldn't find note with ID=#{note_id}, skipping job")
diff --git a/app/workers/repository_check/batch_worker.rb b/app/workers/repository_check/batch_worker.rb
index 76688cf51c1..72f0a9b0619 100644
--- a/app/workers/repository_check/batch_worker.rb
+++ b/app/workers/repository_check/batch_worker.rb
@@ -4,8 +4,11 @@ module RepositoryCheck
include CronjobQueue
RUN_TIME = 3600
+ BATCH_SIZE = 10_000
def perform
+ return unless Gitlab::CurrentSettings.repository_checks_enabled
+
start = Time.now
# This loop will break after a little more than one hour ('a little
@@ -15,7 +18,6 @@ module RepositoryCheck
# check, only one (or two) will be checked at a time.
project_ids.each do |project_id|
break if Time.now - start >= RUN_TIME
- break unless current_settings.repository_checks_enabled
next unless try_obtain_lease(project_id)
@@ -31,12 +33,20 @@ module RepositoryCheck
# getting ID's from Postgres is not terribly slow, and because no user
# has to sit and wait for this query to finish.
def project_ids
- limit = 10_000
- never_checked_projects = Project.where('last_repository_check_at IS NULL AND created_at < ?', 24.hours.ago)
- .limit(limit).pluck(:id)
- old_check_projects = Project.where('last_repository_check_at < ?', 1.month.ago)
- .reorder('last_repository_check_at ASC').limit(limit).pluck(:id)
- never_checked_projects + old_check_projects
+ never_checked_project_ids(BATCH_SIZE) + old_checked_project_ids(BATCH_SIZE)
+ end
+
+ def never_checked_project_ids(batch_size)
+ Project.where(last_repository_check_at: nil)
+ .where('created_at < ?', 24.hours.ago)
+ .limit(batch_size).pluck(:id)
+ end
+
+ def old_checked_project_ids(batch_size)
+ Project.where.not(last_repository_check_at: nil)
+ .where('last_repository_check_at < ?', 1.month.ago)
+ .reorder(last_repository_check_at: :asc)
+ .limit(batch_size).pluck(:id)
end
def try_obtain_lease(id)
@@ -47,16 +57,5 @@ module RepositoryCheck
timeout: 24.hours
).try_obtain
end
-
- def current_settings
- # No caching of the settings! If we cache them and an admin disables
- # this feature, an active RepositoryCheckWorker would keep going for up
- # to 1 hour after the feature was disabled.
- if Rails.env.test?
- Gitlab::CurrentSettings.fake_application_settings
- else
- ApplicationSetting.current
- end
- end
end
end
diff --git a/app/workers/repository_check/single_repository_worker.rb b/app/workers/repository_check/single_repository_worker.rb
index 116bc185b38..3cffb8b14e4 100644
--- a/app/workers/repository_check/single_repository_worker.rb
+++ b/app/workers/repository_check/single_repository_worker.rb
@@ -5,27 +5,34 @@ module RepositoryCheck
def perform(project_id)
project = Project.find(project_id)
+ healthy = project_healthy?(project)
+
+ update_repository_check_status(project, healthy)
+ end
+
+ private
+
+ def update_repository_check_status(project, healthy)
project.update_columns(
- last_repository_check_failed: !check(project),
+ last_repository_check_failed: !healthy,
last_repository_check_at: Time.now
)
end
- private
+ def project_healthy?(project)
+ repo_healthy?(project) && wiki_repo_healthy?(project)
+ end
- def check(project)
- if has_pushes?(project) && !git_fsck(project.repository)
- false
- elsif project.wiki_enabled?
- # Historically some projects never had their wiki repos initialized;
- # this happens on project creation now. Let's initialize an empty repo
- # if it is not already there.
- project.create_wiki
+ def repo_healthy?(project)
+ return true unless has_changes?(project)
- git_fsck(project.wiki.repository)
- else
- true
- end
+ git_fsck(project.repository)
+ end
+
+ def wiki_repo_healthy?(project)
+ return true unless has_wiki_changes?(project)
+
+ git_fsck(project.wiki.repository)
end
def git_fsck(repository)
@@ -39,8 +46,19 @@ module RepositoryCheck
false
end
- def has_pushes?(project)
+ def has_changes?(project)
Project.with_push.exists?(project.id)
end
+
+ def has_wiki_changes?(project)
+ return false unless project.wiki_enabled?
+
+ # Historically some projects never had their wiki repos initialized;
+ # this happens on project creation now. Let's initialize an empty repo
+ # if it is not already there.
+ return false unless project.create_wiki
+
+ has_changes?(project)
+ end
end
end
diff --git a/app/workers/repository_remove_remote_worker.rb b/app/workers/repository_remove_remote_worker.rb
new file mode 100644
index 00000000000..1c19b604b77
--- /dev/null
+++ b/app/workers/repository_remove_remote_worker.rb
@@ -0,0 +1,35 @@
+class RepositoryRemoveRemoteWorker
+ include ApplicationWorker
+ include ExclusiveLeaseGuard
+
+ LEASE_TIMEOUT = 1.hour
+
+ attr_reader :project, :remote_name
+
+ def perform(project_id, remote_name)
+ @remote_name = remote_name
+ @project = Project.find_by_id(project_id)
+
+ return unless @project
+
+ logger.info("Removing remote #{remote_name} from project #{project.id}")
+
+ try_obtain_lease do
+ remove_remote = @project.repository.remove_remote(remote_name)
+
+ if remove_remote
+ logger.info("Remote #{remote_name} was successfully removed from project #{project.id}")
+ else
+ logger.error("Could not remove remote #{remote_name} from project #{project.id}")
+ end
+ end
+ end
+
+ def lease_timeout
+ LEASE_TIMEOUT
+ end
+
+ def lease_key
+ "remove_remote_#{project.id}_#{remote_name}"
+ end
+end
diff --git a/app/workers/repository_update_remote_mirror_worker.rb b/app/workers/repository_update_remote_mirror_worker.rb
new file mode 100644
index 00000000000..bb963979e88
--- /dev/null
+++ b/app/workers/repository_update_remote_mirror_worker.rb
@@ -0,0 +1,49 @@
+class RepositoryUpdateRemoteMirrorWorker
+ UpdateAlreadyInProgressError = Class.new(StandardError)
+ UpdateError = Class.new(StandardError)
+
+ include ApplicationWorker
+ include Gitlab::ShellAdapter
+
+ sidekiq_options retry: 3, dead: false
+
+ sidekiq_retry_in { |count| 30 * count }
+
+ sidekiq_retries_exhausted do |msg, _|
+ Sidekiq.logger.warn "Failed #{msg['class']} with #{msg['args']}: #{msg['error_message']}"
+ end
+
+ def perform(remote_mirror_id, scheduled_time)
+ remote_mirror = RemoteMirror.find(remote_mirror_id)
+ return if remote_mirror.updated_since?(scheduled_time)
+
+ raise UpdateAlreadyInProgressError if remote_mirror.update_in_progress?
+
+ remote_mirror.update_start
+
+ project = remote_mirror.project
+ current_user = project.creator
+ result = Projects::UpdateRemoteMirrorService.new(project, current_user).execute(remote_mirror)
+ raise UpdateError, result[:message] if result[:status] == :error
+
+ remote_mirror.update_finish
+ rescue UpdateAlreadyInProgressError
+ raise
+ rescue UpdateError => ex
+ fail_remote_mirror(remote_mirror, ex.message)
+ raise
+ rescue => ex
+ return unless remote_mirror
+
+ fail_remote_mirror(remote_mirror, ex.message)
+ raise UpdateError, "#{ex.class}: #{ex.message}"
+ end
+
+ private
+
+ def fail_remote_mirror(remote_mirror, message)
+ remote_mirror.mark_as_failed(message)
+
+ Rails.logger.error(message)
+ end
+end
diff --git a/app/workers/stuck_import_jobs_worker.rb b/app/workers/stuck_import_jobs_worker.rb
index fbb14efc525..6fdd7592e74 100644
--- a/app/workers/stuck_import_jobs_worker.rb
+++ b/app/workers/stuck_import_jobs_worker.rb
@@ -22,7 +22,8 @@ class StuckImportJobsWorker
end
def mark_projects_with_jid_as_failed!
- jids_and_ids = enqueued_projects_with_jid.pluck(:import_jid, :id).to_h
+ # TODO: Rollback this change to use SQL through #pluck
+ jids_and_ids = enqueued_projects_with_jid.map { |project| [project.import_jid, project.id] }.to_h
# Find the jobs that aren't currently running or that exceeded the threshold.
completed_jids = Gitlab::SidekiqStatus.completed_jids(jids_and_ids.keys)
@@ -42,15 +43,15 @@ class StuckImportJobsWorker
end
def enqueued_projects
- Project.with_import_status(:scheduled, :started)
+ Project.joins_import_state.where("(import_state.status = 'scheduled' OR import_state.status = 'started') OR (projects.import_status = 'scheduled' OR projects.import_status = 'started')")
end
def enqueued_projects_with_jid
- enqueued_projects.where.not(import_jid: nil)
+ enqueued_projects.where.not("import_state.jid IS NULL AND projects.import_jid IS NULL")
end
def enqueued_projects_without_jid
- enqueued_projects.where(import_jid: nil)
+ enqueued_projects.where("import_state.jid IS NULL AND projects.import_jid IS NULL")
end
def error_message