diff options
author | Luke Bennett <lukeeeebennettplus@gmail.com> | 2018-05-28 14:27:06 +0300 |
---|---|---|
committer | Luke Bennett <lukeeeebennettplus@gmail.com> | 2018-05-28 14:27:06 +0300 |
commit | a9583bc9562512bd046a7f2c32fd267f32a5dac2 (patch) | |
tree | af91d0688d49850746de400afdacb5389c0cfb16 /app/assets | |
parent | a3e472e0a7a6b6ec5654edd20b947ba660ed2dc3 (diff) | |
parent | 265b1fafe64ae9fe8a3e92d83c1678b47533ba86 (diff) |
Merge remote-tracking branch 'origin/master' into 39549-label-list-page-redesign-with-draggable-labels
Diffstat (limited to 'app/assets')
379 files changed, 13324 insertions, 7740 deletions
diff --git a/app/assets/javascripts/api.js b/app/assets/javascripts/api.js index 8ad3d18b302..ce1069276ab 100644 --- a/app/assets/javascripts/api.js +++ b/app/assets/javascripts/api.js @@ -21,8 +21,11 @@ const Api = { issuableTemplatePath: '/:namespace_path/:project_path/templates/:type/:key', usersPath: '/api/:version/users.json', commitPath: '/api/:version/projects/:id/repository/commits', + commitPipelinesPath: '/:project_id/commit/:sha/pipelines', branchSinglePath: '/api/:version/projects/:id/repository/branches/:branch', createBranchPath: '/api/:version/projects/:id/repository/branches', + pipelinesPath: '/api/:version/projects/:id/pipelines', + pipelineJobsPath: '/api/:version/projects/:id/pipelines/:pipeline_id/jobs', group(groupId, callback) { const url = Api.buildUrl(Api.groupPath).replace(':id', groupId); @@ -164,6 +167,19 @@ const Api = { }); }, + commitPipelines(projectId, sha) { + const encodedProjectId = projectId + .split('/') + .map(fragment => encodeURIComponent(fragment)) + .join('/'); + + const url = Api.buildUrl(Api.commitPipelinesPath) + .replace(':project_id', encodedProjectId) + .replace(':sha', encodeURIComponent(sha)); + + return axios.get(url); + }, + branchSingle(id, branch) { const url = Api.buildUrl(Api.branchSinglePath) .replace(':id', encodeURIComponent(id)) @@ -222,6 +238,20 @@ const Api = { }); }, + pipelines(projectPath, params = {}) { + const url = Api.buildUrl(this.pipelinesPath).replace(':id', encodeURIComponent(projectPath)); + + return axios.get(url, { params }); + }, + + pipelineJobs(projectPath, pipelineId, params = {}) { + const url = Api.buildUrl(this.pipelineJobsPath) + .replace(':id', encodeURIComponent(projectPath)) + .replace(':pipeline_id', pipelineId); + + return axios.get(url, { params }); + }, + buildUrl(url) { let urlRoot = ''; if (gon.relative_url_root != null) { diff --git a/app/assets/javascripts/awards_handler.js b/app/assets/javascripts/awards_handler.js index 976d32abe9b..eb0f06efab4 100644 --- a/app/assets/javascripts/awards_handler.js +++ b/app/assets/javascripts/awards_handler.js @@ -345,7 +345,7 @@ class AwardsHandler { counter.text(counterNumber - 1); this.removeYouFromUserList($emojiButton); } else if (emoji === 'thumbsup' || emoji === 'thumbsdown') { - $emojiButton.tooltip('destroy'); + $emojiButton.tooltip('dispose'); counter.text('0'); this.removeYouFromUserList($emojiButton); if ($emojiButton.parents('.note').length) { @@ -358,7 +358,7 @@ class AwardsHandler { } removeEmoji($emojiButton) { - $emojiButton.tooltip('destroy'); + $emojiButton.tooltip('dispose'); $emojiButton.remove(); const $votesBlock = this.getVotesBlock(); if ($votesBlock.find('.js-emoji-btn').length === 0) { @@ -392,7 +392,7 @@ class AwardsHandler { .removeAttr('data-title') .removeAttr('data-original-title') .attr('title', this.toSentence(authors)) - .tooltip('fixTitle'); + .tooltip('_fixTitle'); } addYouToUserList(votesBlock, emoji) { @@ -405,7 +405,7 @@ class AwardsHandler { users.unshift('You'); return awardBlock .attr('title', this.toSentence(users)) - .tooltip('fixTitle'); + .tooltip('_fixTitle'); } createAwardButtonForVotesBlock(votesBlock, emojiName) { diff --git a/app/assets/javascripts/badges/components/badge.vue b/app/assets/javascripts/badges/components/badge.vue index 6e6cb31e3ac..d0f60e1d4cb 100644 --- a/app/assets/javascripts/badges/components/badge.vue +++ b/app/assets/javascripts/badges/components/badge.vue @@ -89,7 +89,7 @@ export default { v-show="hasError" class="btn-group" > - <div class="btn btn-default btn-xs disabled"> + <div class="btn btn-default btn-sm disabled"> <icon class="prepend-left-8 append-right-8" name="doc_image" @@ -98,7 +98,7 @@ export default { /> </div> <div - class="btn btn-default btn-xs disabled" + class="btn btn-default btn-sm disabled" > <span class="prepend-left-8 append-right-8">{{ s__('Badges|No badge image') }}</span> </div> @@ -106,7 +106,7 @@ export default { <button v-show="hasError" - class="btn btn-transparent btn-xs text-primary" + class="btn btn-transparent btn-sm text-primary" type="button" v-tooltip :title="s__('Badges|Reload badge image')" diff --git a/app/assets/javascripts/badges/components/badge_list.vue b/app/assets/javascripts/badges/components/badge_list.vue index ca7197e1e0f..268968b63b3 100644 --- a/app/assets/javascripts/badges/components/badge_list.vue +++ b/app/assets/javascripts/badges/components/badge_list.vue @@ -23,8 +23,8 @@ export default { </script> <template> - <div class="panel panel-default"> - <div class="panel-heading"> + <div class="card"> + <div class="card-header"> {{ s__('Badges|Your badges') }} <span v-show="!isLoading" @@ -33,19 +33,19 @@ export default { </div> <loading-icon v-show="isLoading" - class="panel-body" + class="card-body" size="2" /> <div v-if="hasNoBadges" - class="panel-body" + class="card-body" > <span v-if="isGroupBadge">{{ s__('Badges|This group has no badges') }}</span> <span v-else>{{ s__('Badges|This project has no badges') }}</span> </div> <div v-else - class="panel-body" + class="card-body" > <badge-list-row v-for="badge in badges" diff --git a/app/assets/javascripts/behaviors/copy_to_clipboard.js b/app/assets/javascripts/behaviors/copy_to_clipboard.js index e2a73a1797c..75834ba351d 100644 --- a/app/assets/javascripts/behaviors/copy_to_clipboard.js +++ b/app/assets/javascripts/behaviors/copy_to_clipboard.js @@ -8,10 +8,10 @@ function showTooltip(target, title) { if (!$target.data('hideTooltip')) { $target .attr('title', title) - .tooltip('fixTitle') + .tooltip('_fixTitle') .tooltip('show') .attr('title', originalTitle) - .tooltip('fixTitle'); + .tooltip('_fixTitle'); } } 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/behaviors/quick_submit.js b/app/assets/javascripts/behaviors/quick_submit.js index 3ec932bdb73..b6e2781773c 100644 --- a/app/assets/javascripts/behaviors/quick_submit.js +++ b/app/assets/javascripts/behaviors/quick_submit.js @@ -69,7 +69,7 @@ $(document).on('keyup.quick_submit', '.js-quick-submit input[type=submit], .js-q $this.tooltip({ container: 'body', html: 'true', - placement: 'auto top', + placement: 'top', title, trigger: 'manual', }); diff --git a/app/assets/javascripts/behaviors/requires_input.js b/app/assets/javascripts/behaviors/requires_input.js index ffff4ddb71a..a8b6dbf0948 100644 --- a/app/assets/javascripts/behaviors/requires_input.js +++ b/app/assets/javascripts/behaviors/requires_input.js @@ -42,9 +42,9 @@ $.fn.requiresInput = function requiresInput() { function hideOrShowHelpBlock(form) { const selected = $('.js-select-namespace option:selected'); if (selected.length && selected.data('optionsParent') === 'groups') { - form.find('.help-block').hide(); + form.find('.form-text.text-muted').hide(); } else if (selected.length) { - form.find('.help-block').show(); + form.find('.form-text.text-muted').show(); } } diff --git a/app/assets/javascripts/blob/balsamiq/balsamiq_viewer.js b/app/assets/javascripts/blob/balsamiq/balsamiq_viewer.js index c17877a276d..766039404ce 100644 --- a/app/assets/javascripts/blob/balsamiq/balsamiq_viewer.js +++ b/app/assets/javascripts/blob/balsamiq/balsamiq_viewer.js @@ -2,9 +2,9 @@ import sqljs from 'sql.js'; import { template as _template } from 'underscore'; const PREVIEW_TEMPLATE = _template(` - <div class="panel panel-default"> - <div class="panel-heading"><%- name %></div> - <div class="panel-body"> + <div class="card"> + <div class="card-header"><%- name %></div> + <div class="card-body"> <img class="img-thumbnail" src="data:image/png;base64,<%- image %>"/> </div> </div> diff --git a/app/assets/javascripts/blob/sketch/index.js b/app/assets/javascripts/blob/sketch/index.js index 0799991aa40..13318c58006 100644 --- a/app/assets/javascripts/blob/sketch/index.js +++ b/app/assets/javascripts/blob/sketch/index.js @@ -44,7 +44,7 @@ export default class SketchLoader { previewLink.href = previewUrl; previewLink.target = '_blank'; previewImage.src = previewUrl; - previewImage.className = 'img-responsive'; + previewImage.className = 'img-fluid'; previewLink.appendChild(previewImage); this.container.appendChild(previewLink); diff --git a/app/assets/javascripts/blob/viewer/index.js b/app/assets/javascripts/blob/viewer/index.js index 137e1f5a099..f61c0be9230 100644 --- a/app/assets/javascripts/blob/viewer/index.js +++ b/app/assets/javascripts/blob/viewer/index.js @@ -116,7 +116,7 @@ export default class BlobViewer { this.copySourceBtn.classList.add('disabled'); } - $(this.copySourceBtn).tooltip('fixTitle'); + $(this.copySourceBtn).tooltip('_fixTitle'); } switchToViewer(name) { diff --git a/app/assets/javascripts/boards/components/board_card.vue b/app/assets/javascripts/boards/components/board_card.vue index 84885ca9306..33e3369b971 100644 --- a/app/assets/javascripts/boards/components/board_card.vue +++ b/app/assets/javascripts/boards/components/board_card.vue @@ -77,7 +77,7 @@ export default { <template> <li - class="card" + class="board-card" :class="{ 'user-can-drag': !disabled && issue.id, 'is-disabled': disabled || !issue.id, diff --git a/app/assets/javascripts/boards/components/board_new_issue.vue b/app/assets/javascripts/boards/components/board_new_issue.vue index 8d84c1735b8..e8dfd95f7ae 100644 --- a/app/assets/javascripts/boards/components/board_new_issue.vue +++ b/app/assets/javascripts/boards/components/board_new_issue.vue @@ -92,7 +92,7 @@ export default { <template> <div class="board-new-issue-form"> - <div class="card"> + <div class="board-card"> <form @submit="submit($event)"> <div class="flash-container" @@ -122,7 +122,7 @@ export default { /> <div class="clearfix prepend-top-10"> <button - class="btn btn-success pull-left" + class="btn btn-success float-left" type="submit" :disabled="disabled" ref="submit-button" @@ -130,7 +130,7 @@ export default { Submit issue </button> <button - class="btn btn-default pull-right" + class="btn btn-default float-right" type="button" @click="cancel" > diff --git a/app/assets/javascripts/boards/components/issue_card_inner.js b/app/assets/javascripts/boards/components/issue_card_inner.js index 84fe9b1288a..dcc07810d01 100644 --- a/app/assets/javascripts/boards/components/issue_card_inner.js +++ b/app/assets/javascripts/boards/components/issue_card_inner.js @@ -135,8 +135,8 @@ gl.issueBoards.IssueCardInner = Vue.extend({ }, template: ` <div> - <div class="card-header"> - <h4 class="card-title"> + <div class="board-card-header"> + <h4 class="board-card-title"> <i class="fa fa-eye-slash confidential-icon" v-if="issue.confidential" @@ -147,13 +147,13 @@ gl.issueBoards.IssueCardInner = Vue.extend({ :href="issue.path" :title="issue.title">{{ issue.title }}</a> <span - class="card-number" + class="board-card-number" v-if="issueId" > {{ issue.referencePath }} </span> </h4> - <div class="card-assignee"> + <div class="board-card-assignee"> <user-avatar-link v-for="(assignee, index) in issue.assignees" :key="assignee.id" @@ -175,11 +175,11 @@ gl.issueBoards.IssueCardInner = Vue.extend({ </div> </div> <div - class="card-footer" + class="board-card-footer" v-if="showLabelFooter" > <button - class="label color-label has-tooltip" + class="badge color-label has-tooltip" v-for="label in issue.labels" type="button" v-if="showLabel(label)" diff --git a/app/assets/javascripts/boards/components/modal/empty_state.js b/app/assets/javascripts/boards/components/modal/empty_state.js index 9e37f95cdd6..eb8a66975ee 100644 --- a/app/assets/javascripts/boards/components/modal/empty_state.js +++ b/app/assets/javascripts/boards/components/modal/empty_state.js @@ -41,10 +41,10 @@ gl.issueBoards.ModalEmptyState = Vue.extend({ template: ` <section class="empty-state"> <div class="row"> - <div class="col-xs-12 col-sm-6 col-sm-push-6"> + <div class="col-xs-12 col-sm-6 order-sm-last"> <aside class="svg-content"><img :src="emptyStateSvg"/></aside> </div> - <div class="col-xs-12 col-sm-6 col-sm-pull-6"> + <div class="col-xs-12 col-sm-6 order-sm-first"> <div class="text-content"> <h4>{{ contents.title }}</h4> <p v-html="contents.content"></p> diff --git a/app/assets/javascripts/boards/components/modal/footer.js b/app/assets/javascripts/boards/components/modal/footer.js index 9735e0ddacc..11bb3e98334 100644 --- a/app/assets/javascripts/boards/components/modal/footer.js +++ b/app/assets/javascripts/boards/components/modal/footer.js @@ -58,7 +58,7 @@ gl.issueBoards.ModalFooter = Vue.extend({ template: ` <footer class="form-actions add-issues-footer"> - <div class="pull-left"> + <div class="float-left"> <button class="btn btn-success" type="button" @@ -72,7 +72,7 @@ gl.issueBoards.ModalFooter = Vue.extend({ <lists-dropdown></lists-dropdown> </div> <button - class="btn btn-default pull-right" + class="btn btn-default float-right" type="button" @click="toggleModal(false)"> Cancel diff --git a/app/assets/javascripts/boards/components/modal/list.js b/app/assets/javascripts/boards/components/modal/list.js index 6b04a6c7a6c..6c662432037 100644 --- a/app/assets/javascripts/boards/components/modal/list.js +++ b/app/assets/javascripts/boards/components/modal/list.js @@ -133,9 +133,9 @@ gl.issueBoards.ModalList = Vue.extend({ <div v-for="issue in group" v-if="showIssue(issue)" - class="card-parent"> + class="board-card-parent"> <div - class="card" + class="board-card" :class="{ 'is-active': issue.selected }" @click="toggleIssue($event, issue)"> <issue-card-inner diff --git a/app/assets/javascripts/boards/components/modal/tabs.js b/app/assets/javascripts/boards/components/modal/tabs.js index b6465a88e5e..9d331de8e22 100644 --- a/app/assets/javascripts/boards/components/modal/tabs.js +++ b/app/assets/javascripts/boards/components/modal/tabs.js @@ -24,7 +24,7 @@ gl.issueBoards.ModalTabs = Vue.extend({ role="button" @click.prevent="changeTab('all')"> Open issues - <span class="badge"> + <span class="badge badge-pill"> {{ issuesCount }} </span> </a> @@ -35,7 +35,7 @@ gl.issueBoards.ModalTabs = Vue.extend({ role="button" @click.prevent="changeTab('selected')"> Selected issues - <span class="badge"> + <span class="badge badge-pill"> {{ selectedCount }} </span> </a> diff --git a/app/assets/javascripts/boards/index.js b/app/assets/javascripts/boards/index.js index a6f8681cfac..29ab13b8e0b 100644 --- a/app/assets/javascripts/boards/index.js +++ b/app/assets/javascripts/boards/index.js @@ -214,7 +214,7 @@ export default () => { if (this.disabled) { $tooltip.tooltip(); } else { - $tooltip.tooltip('destroy'); + $tooltip.tooltip('dispose'); } }); }, diff --git a/app/assets/javascripts/boards/models/list.js b/app/assets/javascripts/boards/models/list.js index e210d69895e..7144f4190e7 100644 --- a/app/assets/javascripts/boards/models/list.js +++ b/app/assets/javascripts/boards/models/list.js @@ -113,6 +113,8 @@ class List { issue.id = data.id; issue.iid = data.iid; issue.project = data.project; + issue.path = data.real_path; + issue.referencePath = data.reference_path; if (this.issuesSize > 1) { const moveBeforeId = this.issues[1].id; diff --git a/app/assets/javascripts/ci_variable_list/ci_variable_list.js b/app/assets/javascripts/ci_variable_list/ci_variable_list.js index e177a3bfdc7..47efb3a8cee 100644 --- a/app/assets/javascripts/ci_variable_list/ci_variable_list.js +++ b/app/assets/javascripts/ci_variable_list/ci_variable_list.js @@ -141,6 +141,11 @@ export default class VariableList { $rowClone.find(entry.selector).val(entry.default); }); + // Close any dropdowns + $rowClone.find('.dropdown-menu.show').each((index, $dropdown) => { + $dropdown.classList.remove('show'); + }); + this.initRow($rowClone); $row.after($rowClone); 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/application_row.vue b/app/assets/javascripts/clusters/components/application_row.vue index c2a35341eb2..fae580c091b 100644 --- a/app/assets/javascripts/clusters/components/application_row.vue +++ b/app/assets/javascripts/clusters/components/application_row.vue @@ -179,7 +179,7 @@ role="row" > <div - class="alert alert-danger alert-block append-bottom-0 table-section section-100" + class="alert alert-danger alert-block append-bottom-0" role="gridcell" > <div> diff --git a/app/assets/javascripts/clusters/components/applications.vue b/app/assets/javascripts/clusters/components/applications.vue index 9c12b89240c..bb5fcea648d 100644 --- a/app/assets/javascripts/clusters/components/applications.vue +++ b/app/assets/javascripts/clusters/components/applications.vue @@ -191,11 +191,11 @@ export default { :value="ingressExternalIp" readonly /> - <span class="input-group-btn"> + <span class="input-group-append"> <clipboard-button :text="ingressExternalIp" :title="s__('ClusterIntegration|Copy Ingress IP Address to clipboard')" - class="js-clipboard-btn" + class="input-group-text js-clipboard-btn" /> </span> </div> 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/commons/bootstrap.js b/app/assets/javascripts/commons/bootstrap.js index db96da4ccba..50e2949ab55 100644 --- a/app/assets/javascripts/commons/bootstrap.js +++ b/app/assets/javascripts/commons/bootstrap.js @@ -1,15 +1,7 @@ import $ from 'jquery'; // bootstrap jQuery plugins -import 'bootstrap-sass/assets/javascripts/bootstrap/affix'; -import 'bootstrap-sass/assets/javascripts/bootstrap/alert'; -import 'bootstrap-sass/assets/javascripts/bootstrap/button'; -import 'bootstrap-sass/assets/javascripts/bootstrap/dropdown'; -import 'bootstrap-sass/assets/javascripts/bootstrap/modal'; -import 'bootstrap-sass/assets/javascripts/bootstrap/tab'; -import 'bootstrap-sass/assets/javascripts/bootstrap/transition'; -import 'bootstrap-sass/assets/javascripts/bootstrap/tooltip'; -import 'bootstrap-sass/assets/javascripts/bootstrap/popover'; +import 'bootstrap'; // custom jQuery functions $.fn.extend({ 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 260c91cac24..ffe15f02f2e 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', 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,10 +75,10 @@ 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'); + $dropdown.tooltip('_fixTitle'); } }); }); diff --git a/app/assets/javascripts/create_merge_request_dropdown.js b/app/assets/javascripts/create_merge_request_dropdown.js index fb1fc9cd32e..09d490106df 100644 --- a/app/assets/javascripts/create_merge_request_dropdown.js +++ b/app/assets/javascripts/create_merge_request_dropdown.js @@ -61,8 +61,8 @@ export default class CreateMergeRequestDropdown { } available() { - this.availableButton.classList.remove('hide'); - this.unavailableButton.classList.add('hide'); + this.availableButton.classList.remove('hidden'); + this.unavailableButton.classList.add('hidden'); } bindEvents() { @@ -84,20 +84,21 @@ export default class CreateMergeRequestDropdown { if (data.can_create_branch) { this.available(); this.enable(); + this.updateBranchName(data.suggested_branch_name); if (!this.droplabInitialized) { this.droplabInitialized = true; this.initDroplab(); this.bindEvents(); } - } else if (data.has_related_branch) { + } else { this.hide(); } }) .catch(() => { this.unavailable(); this.disable(); - Flash('Failed to check if a new branch can be created.'); + Flash(__('Failed to check related branches.')); }); } @@ -231,7 +232,7 @@ export default class CreateMergeRequestDropdown { } hide() { - this.wrapperEl.classList.add('hide'); + this.wrapperEl.classList.add('hidden'); } init() { @@ -405,8 +406,13 @@ export default class CreateMergeRequestDropdown { } unavailable() { - this.availableButton.classList.add('hide'); - this.unavailableButton.classList.remove('hide'); + this.availableButton.classList.add('hidden'); + this.unavailableButton.classList.remove('hidden'); + } + + updateBranchName(suggestedBranchName) { + this.branchInput.value = suggestedBranchName; + this.updateCreatePaths('branch', suggestedBranchName); } updateInputState(target, ref, result) { @@ -414,8 +420,6 @@ export default class CreateMergeRequestDropdown { // ref - string - what a user typed. // result - string - what has been found on backend. - const pathReplacement = `$1${ref}`; - // If a found branch equals exact the same text a user typed, // that means a new branch cannot be created as it already exists. if (ref === result) { @@ -426,18 +430,12 @@ export default class CreateMergeRequestDropdown { this.refIsValid = true; this.refInput.dataset.value = ref; this.showAvailableMessage('ref'); - this.createBranchPath = this.createBranchPath.replace(this.regexps.ref.createBranchPath, - pathReplacement); - this.createMrPath = this.createMrPath.replace(this.regexps.ref.createMrPath, - pathReplacement); + this.updateCreatePaths(target, ref); } } else if (target === 'branch') { this.branchIsValid = true; this.showAvailableMessage('branch'); - this.createBranchPath = this.createBranchPath.replace(this.regexps.branch.createBranchPath, - pathReplacement); - this.createMrPath = this.createMrPath.replace(this.regexps.branch.createMrPath, - pathReplacement); + this.updateCreatePaths(target, ref); } else { this.refIsValid = false; this.refInput.dataset.value = ref; @@ -457,4 +455,15 @@ export default class CreateMergeRequestDropdown { this.disableCreateAction(); } } + + // target - 'branch' or 'ref' + // ref - string - the new value to use as branch or ref + updateCreatePaths(target, ref) { + const pathReplacement = `$1${ref}`; + + this.createBranchPath = this.createBranchPath.replace(this.regexps[target].createBranchPath, + pathReplacement); + this.createMrPath = this.createMrPath.replace(this.regexps[target].createMrPath, + pathReplacement); + } } diff --git a/app/assets/javascripts/cycle_analytics/components/limit_warning_component.vue b/app/assets/javascripts/cycle_analytics/components/limit_warning_component.vue index 32ae0cc1476..5be17081b58 100644 --- a/app/assets/javascripts/cycle_analytics/components/limit_warning_component.vue +++ b/app/assets/javascripts/cycle_analytics/components/limit_warning_component.vue @@ -16,7 +16,7 @@ <template> <span v-if="count === 50" - class="events-info pull-right" + class="events-info float-right" > <i class="fa fa-warning" diff --git a/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js b/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js index 87f8854f940..1c43fc3cdc7 100644 --- a/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js +++ b/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js @@ -82,7 +82,6 @@ export default () => { this.service .fetchCycleAnalyticsData(fetchOptions) - .then(resp => resp.json()) .then((response) => { this.store.setCycleAnalyticsData(response); this.selectDefaultStage(); @@ -116,7 +115,6 @@ export default () => { stage, startDate: this.startDate, }) - .then(resp => resp.json()) .then((response) => { this.isEmptyStage = !response.events.length; this.store.setStageEvents(response.events, stage); diff --git a/app/assets/javascripts/cycle_analytics/cycle_analytics_service.js b/app/assets/javascripts/cycle_analytics/cycle_analytics_service.js index f496c38208d..4cf416c50e5 100644 --- a/app/assets/javascripts/cycle_analytics/cycle_analytics_service.js +++ b/app/assets/javascripts/cycle_analytics/cycle_analytics_service.js @@ -1,16 +1,20 @@ -import Vue from 'vue'; -import VueResource from 'vue-resource'; - -Vue.use(VueResource); +import axios from '~/lib/utils/axios_utils'; export default class CycleAnalyticsService { constructor(options) { - this.requestPath = options.requestPath; - this.cycleAnalytics = Vue.resource(this.requestPath); + this.axios = axios.create({ + baseURL: options.requestPath, + }); } fetchCycleAnalyticsData(options = { startDate: 30 }) { - return this.cycleAnalytics.get({ cycle_analytics: { start_date: options.startDate } }); + return this.axios + .get('', { + params: { + 'cycle_analytics[start_date]': options.startDate, + }, + }) + .then(x => x.data); } fetchStageData(options) { @@ -19,12 +23,12 @@ export default class CycleAnalyticsService { startDate, } = options; - return Vue.http.get(`${this.requestPath}/events/${stage.name}.json`, { - params: { - cycle_analytics: { - start_date: startDate, + return this.axios + .get(`events/${stage.name}.json`, { + params: { + 'cycle_analytics[start_date]': startDate, }, - }, - }); + }) + .then(x => x.data); } } diff --git a/app/assets/javascripts/deploy_keys/components/action_btn.vue b/app/assets/javascripts/deploy_keys/components/action_btn.vue index b839b9f286f..67dda0e29cb 100644 --- a/app/assets/javascripts/deploy_keys/components/action_btn.vue +++ b/app/assets/javascripts/deploy_keys/components/action_btn.vue @@ -1,55 +1,50 @@ <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-default', - }, + type: { + type: String, + required: true, }, - data() { - return { - isLoading: false, - }; + btnCssClass: { + type: String, + required: false, + default: 'btn-default', }, - 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 c6091efd62f..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="pull-left append-right-10 hidden-xs"> - <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 822b0323156..3b146c7389a 100644 --- a/app/assets/javascripts/deploy_keys/components/keys_panel.vue +++ b/app/assets/javascripts/deploy_keys/components/keys_panel.vue @@ -1,62 +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="well-list" - v-if="keys.length" - > - <li + <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..9dc3b21f6f6 100644 --- a/app/assets/javascripts/deploy_keys/service/index.js +++ b/app/assets/javascripts/deploy_keys/service/index.js @@ -1,34 +1,24 @@ -import Vue from 'vue'; -import VueResource from 'vue-resource'; - -Vue.use(VueResource); +import axios from '~/lib/utils/axios_utils'; export default class DeployKeysService { constructor(endpoint) { - this.endpoint = endpoint; - - this.resource = Vue.resource(`${this.endpoint}{/id}`, {}, { - enable: { - method: 'PUT', - url: `${this.endpoint}{/id}/enable`, - }, - disable: { - method: 'PUT', - url: `${this.endpoint}{/id}/disable`, - }, + this.axios = axios.create({ + baseURL: endpoint, }); } getKeys() { - return this.resource.get() - .then(response => response.json()); + return this.axios.get() + .then(response => response.data); } enableKey(id) { - return this.resource.enable({ id }, {}); + return this.axios.put(`${id}/enable`) + .then(response => response.data); } disableKey(id) { - return this.resource.disable({ id }, {}); + return this.axios.put(`${id}/disable`) + .then(response => response.data); } } 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/diff_notes/components/diff_note_avatars.js b/app/assets/javascripts/diff_notes/components/diff_note_avatars.js index 180a6bd67e7..fe9b0795609 100644 --- a/app/assets/javascripts/diff_notes/components/diff_note_avatars.js +++ b/app/assets/javascripts/diff_notes/components/diff_note_avatars.js @@ -79,7 +79,7 @@ const DiffNoteAvatars = Vue.extend({ storeState: { handler() { this.$nextTick(() => { - $('.has-tooltip', this.$el).tooltip('fixTitle'); + $('.has-tooltip', this.$el).tooltip('_fixTitle'); // We need to add/remove a class to an element that is outside the Vue instance this.addNoCommentClass(); @@ -138,7 +138,7 @@ const DiffNoteAvatars = Vue.extend({ this.$nextTick(() => { this.setDiscussionVisible(); - $('.has-tooltip', this.$el).tooltip('fixTitle'); + $('.has-tooltip', this.$el).tooltip('_fixTitle'); $('.has-tooltip', this.$el).tooltip('hide'); }); }, diff --git a/app/assets/javascripts/diff_notes/components/resolve_btn.js b/app/assets/javascripts/diff_notes/components/resolve_btn.js index df4c72ba0ed..8d66417abac 100644 --- a/app/assets/javascripts/diff_notes/components/resolve_btn.js +++ b/app/assets/javascripts/diff_notes/components/resolve_btn.js @@ -61,7 +61,7 @@ const ResolveBtn = Vue.extend({ this.$nextTick(() => { $(this.$refs.button) .tooltip('hide') - .tooltip('fixTitle'); + .tooltip('_fixTitle'); }); }, resolve: function () { diff --git a/app/assets/javascripts/due_date_select.js b/app/assets/javascripts/due_date_select.js index 842a4255f08..4164149dd06 100644 --- a/app/assets/javascripts/due_date_select.js +++ b/app/assets/javascripts/due_date_select.js @@ -2,7 +2,9 @@ import $ from 'jquery'; import Pikaday from 'pikaday'; +import { __ } from '~/locale'; import axios from './lib/utils/axios_utils'; +import { timeFor } from './lib/utils/datetime_utility'; import { parsePikadayDate, pikadayToString } from './lib/utils/datefix'; class DueDateSelect { @@ -14,6 +16,7 @@ class DueDateSelect { this.$dropdownParent = $dropdownParent; this.$datePicker = $dropdownParent.find('.js-due-date-calendar'); this.$block = $block; + this.$sidebarCollapsedValue = $block.find('.sidebar-collapsed-icon'); this.$selectbox = $dropdown.closest('.selectbox'); this.$value = $block.find('.value'); this.$valueContent = $block.find('.value-content'); @@ -128,7 +131,8 @@ class DueDateSelect { submitSelectedDate(isDropdown) { const selectedDateValue = this.datePayload[this.abilityName].due_date; - const displayedDateStyle = this.displayedDate !== 'No due date' ? 'bold' : 'no-value'; + const hasDueDate = this.displayedDate !== 'No due date'; + const displayedDateStyle = hasDueDate ? 'bold' : 'no-value'; this.$loading.removeClass('hidden').fadeIn(); @@ -145,10 +149,13 @@ class DueDateSelect { return axios.put(this.issueUpdateURL, this.datePayload) .then(() => { + const tooltipText = hasDueDate ? `${__('Due date')}<br />${selectedDateValue} (${timeFor(selectedDateValue)})` : __('Due date'); if (isDropdown) { this.$dropdown.trigger('loaded.gl.dropdown'); this.$dropdown.dropdown('toggle'); } + this.$sidebarCollapsedValue.attr('data-original-title', tooltipText); + return this.$loading.fadeOut(); }); } 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 16bd2f5feb3..0b3fef9fcca 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" @@ -75,7 +74,7 @@ </span> </button> - <ul class="dropdown-menu dropdown-menu-align-right"> + <ul class="dropdown-menu dropdown-menu-right"> <li v-for="(action, i) in actions" :key="i"> @@ -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_item.vue b/app/assets/javascripts/environments/components/environment_item.vue index 79326ca3487..23aaab2c441 100644 --- a/app/assets/javascripts/environments/components/environment_item.vue +++ b/app/assets/javascripts/environments/components/environment_item.vue @@ -486,14 +486,14 @@ {{ model.folderName }} </span> - <span class="badge"> + <span class="badge badge-pill"> {{ model.size }} </span> </span> </div> <div - class="table-section section-10 deployment-column hidden-xs hidden-sm" + class="table-section section-10 deployment-column d-none d-sm-none d-md-block" role="gridcell" > <span v-if="shouldRenderDeploymentID"> @@ -513,7 +513,7 @@ </div> <div - class="table-section section-15 hidden-xs hidden-sm" + class="table-section section-15 d-none d-sm-none d-md-block" role="gridcell" > <a diff --git a/app/assets/javascripts/environments/components/environment_monitoring.vue b/app/assets/javascripts/environments/components/environment_monitoring.vue index 081537cf218..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'; @@ -26,17 +28,16 @@ <template> <a v-tooltip - class="btn monitoring-url hidden-xs hidden-sm" + class="btn monitoring-url d-none d-sm-none d-md-block" data-container="body" rel="noopener noreferrer nofollow" :href="monitoringUrl" :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 605a88e997e..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; @@ -43,7 +40,7 @@ <template> <button type="button" - class="btn hidden-xs hidden-sm" + class="btn d-none d-sm-none d-md-block" @click="onClick" :disabled="isLoading" > diff --git a/app/assets/javascripts/environments/components/environment_stop.vue b/app/assets/javascripts/environments/components/environment_stop.vue index dda7429a726..7055f208451 100644 --- a/app/assets/javascripts/environments/components/environment_stop.vue +++ b/app/assets/javascripts/environments/components/environment_stop.vue @@ -43,7 +43,7 @@ if (confirm('Are you sure you want to stop this environment?')) { this.isLoading = true; - $(this.$el).tooltip('destroy'); + $(this.$el).tooltip('dispose'); eventHub.$emit('postAction', this.stopUrl); } @@ -55,7 +55,7 @@ <button v-tooltip type="button" - class="btn stop-env-link hidden-xs hidden-sm" + class="btn stop-env-link d-none d-sm-none d-md-block" data-container="body" @click="onClick" :disabled="isLoading" diff --git a/app/assets/javascripts/environments/components/environment_terminal_button.vue b/app/assets/javascripts/environments/components/environment_terminal_button.vue index 407d5333c0e..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'; @@ -35,12 +30,15 @@ <template> <a v-tooltip - class="btn terminal-button hidden-xs hidden-sm" + class="btn terminal-button d-none d-sm-none d-md-block" data-container="body" :title="title" :aria-label="title" :href="terminalPath" - v-html="terminalIconSvg" > + <icon + name="terminal" + :size="12" + /> </a> </template> diff --git a/app/assets/javascripts/environments/components/environments_app.vue b/app/assets/javascripts/environments/components/environments_app.vue index c0be72f7401..3da762446c9 100644 --- a/app/assets/javascripts/environments/components/environments_app.vue +++ b/app/assets/javascripts/environments/components/environments_app.vue @@ -68,8 +68,7 @@ this.store.updateEnvironmentProp(folder, 'isLoadingFolderContent', showLoader); this.service.getFolderContent(folder.folder_path) - .then(resp => resp.json()) - .then(response => this.store.setfolderContent(folder, response.environments)) + .then(response => this.store.setfolderContent(folder, response.data.environments)) .then(() => this.store.updateEnvironmentProp(folder, 'isLoadingFolderContent', false)) .catch(() => { Flash(s__('Environments|An error occurred while fetching the environments.')); diff --git a/app/assets/javascripts/environments/mixins/environments_mixin.js b/app/assets/javascripts/environments/mixins/environments_mixin.js index 34d18d55120..a7a79dbca70 100644 --- a/app/assets/javascripts/environments/mixins/environments_mixin.js +++ b/app/assets/javascripts/environments/mixins/environments_mixin.js @@ -6,7 +6,6 @@ import Visibility from 'visibilityjs'; import Poll from '../../lib/utils/poll'; import { getParameterByName, - parseQueryStringIntoObject, } from '../../lib/utils/common_utils'; import { s__ } from '../../locale'; import Flash from '../../flash'; @@ -46,17 +45,14 @@ export default { methods: { saveData(resp) { - const headers = resp.headers; - return resp.json().then((response) => { - this.isLoading = false; - - if (_.isEqual(parseQueryStringIntoObject(resp.url.split('?')[1]), this.requestData)) { - this.store.storeAvailableCount(response.available_count); - this.store.storeStoppedCount(response.stopped_count); - this.store.storeEnvironments(response.environments); - this.store.setPagination(headers); - } - }); + this.isLoading = false; + + if (_.isEqual(resp.config.params, this.requestData)) { + this.store.storeAvailableCount(resp.data.available_count); + this.store.storeStoppedCount(resp.data.stopped_count); + this.store.storeEnvironments(resp.data.environments); + this.store.setPagination(resp.headers); + } }, /** @@ -70,7 +66,7 @@ export default { updateContent(parameters) { this.updateInternalState(parameters); // fetch new data - return this.service.get(this.requestData) + return this.service.fetchEnvironments(this.requestData) .then(response => this.successCallback(response)) .then(() => { // restart polling @@ -105,7 +101,7 @@ export default { fetchEnvironments() { this.isLoading = true; - return this.service.get(this.requestData) + return this.service.fetchEnvironments(this.requestData) .then(this.successCallback) .catch(this.errorCallback); }, @@ -141,7 +137,7 @@ export default { this.poll = new Poll({ resource: this.service, - method: 'get', + method: 'fetchEnvironments', data: this.requestData, successCallback: this.successCallback, errorCallback: this.errorCallback, diff --git a/app/assets/javascripts/environments/services/environments_service.js b/app/assets/javascripts/environments/services/environments_service.js index 03ab74b3338..3b121551aca 100644 --- a/app/assets/javascripts/environments/services/environments_service.js +++ b/app/assets/javascripts/environments/services/environments_service.js @@ -1,25 +1,22 @@ -/* eslint-disable class-methods-use-this */ -import Vue from 'vue'; -import VueResource from 'vue-resource'; - -Vue.use(VueResource); +import axios from '~/lib/utils/axios_utils'; export default class EnvironmentsService { constructor(endpoint) { - this.environments = Vue.resource(endpoint); + this.environmentsEndpoint = endpoint; this.folderResults = 3; } - get(options = {}) { + fetchEnvironments(options = {}) { const { scope, page } = options; - return this.environments.get({ scope, page }); + return axios.get(this.environmentsEndpoint, { params: { scope, page } }); } + // eslint-disable-next-line class-methods-use-this postAction(endpoint) { - return Vue.http.post(endpoint, {}, { emulateJSON: true }); + return axios.post(endpoint, {}, { emulateJSON: true }); } getFolderContent(folderUrl) { - return Vue.http.get(`${folderUrl}.json?per_page=${this.folderResults}`); + return axios.get(`${folderUrl}.json?per_page=${this.folderResults}`); } } diff --git a/app/assets/javascripts/feature_highlight/feature_highlight.js b/app/assets/javascripts/feature_highlight/feature_highlight.js index 2d5bae9a9c4..2f27c9351bc 100644 --- a/app/assets/javascripts/feature_highlight/feature_highlight.js +++ b/app/assets/javascripts/feature_highlight/feature_highlight.js @@ -24,7 +24,7 @@ export function setupFeatureHighlightPopover(id, debounceTimeout = 300) { template: ` <div class="popover feature-highlight-popover" role="tooltip"> <div class="arrow"></div> - <div class="popover-content"></div> + <div class="popover-body"></div> </div> `, }) 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/gl_dropdown.js b/app/assets/javascripts/gl_dropdown.js index fa48d7d1915..746a06b7c4f 100644 --- a/app/assets/javascripts/gl_dropdown.js +++ b/app/assets/javascripts/gl_dropdown.js @@ -374,7 +374,7 @@ GitLabDropdown = (function() { $relatedTarget = $(e.relatedTarget); $dropdownMenu = $relatedTarget.closest('.dropdown-menu'); if ($dropdownMenu.length === 0) { - return _this.dropdown.removeClass('open'); + return _this.dropdown.removeClass('show'); } } }; @@ -801,7 +801,7 @@ GitLabDropdown = (function() { if (this.options.filterable) { const initialScrollTop = $(window).scrollTop(); - if (this.dropdown.is('.open')) { + if (this.dropdown.is('.show') && !this.filterInput.is(':focus')) { this.filterInput.focus(); } diff --git a/app/assets/javascripts/gl_field_error.js b/app/assets/javascripts/gl_field_error.js index 972b2252acb..87c6e37b9fb 100644 --- a/app/assets/javascripts/gl_field_error.js +++ b/app/assets/javascripts/gl_field_error.js @@ -62,7 +62,7 @@ export default class GlFieldError { this.inputDomElement = this.inputElement.get(0); this.form = formErrors; this.errorMessage = this.inputElement.attr('title') || 'This field is required.'; - this.fieldErrorElement = $(`<p class='${errorMessageClass} hide'>${this.errorMessage}</p>`); + this.fieldErrorElement = $(`<p class='${errorMessageClass} hidden'>${this.errorMessage}</p>`); this.state = { valid: false, @@ -146,8 +146,8 @@ export default class GlFieldError { renderInvalid() { this.inputElement.addClass(inputErrorClass); - this.scopedSiblings.hide(); - return this.fieldErrorElement.show(); + this.scopedSiblings.addClass('hidden'); + return this.fieldErrorElement.removeClass('hidden'); } renderClear() { @@ -157,7 +157,7 @@ export default class GlFieldError { this.accessCurrentValue(trimmedInput); } this.inputElement.removeClass(inputErrorClass); - this.scopedSiblings.hide(); - this.fieldErrorElement.hide(); + this.scopedSiblings.addClass('hidden'); + this.fieldErrorElement.addClass('hidden'); } } 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/groups/components/group_item.vue b/app/assets/javascripts/groups/components/group_item.vue index 764b130fdb8..7f64a9bd741 100644 --- a/app/assets/javascripts/groups/components/group_item.vue +++ b/app/assets/javascripts/groups/components/group_item.vue @@ -99,7 +99,7 @@ export default { /> </div> <div - class="avatar-container prepend-top-8 prepend-left-5 s24 hidden-xs" + class="avatar-container prepend-top-8 prepend-left-5 s24 d-none d-sm-block" :class="{ 'content-loading': group.isChildrenLoading }" > <a 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 037e3efb4ce..1cec84706fc 100644 --- a/app/assets/javascripts/ide/components/changed_file_icon.vue +++ b/app/assets/javascripts/ide/components/changed_file_icon.vue @@ -1,31 +1,88 @@ <script> -import icon from '~/vue_shared/components/icon.vue'; +import tooltip from '~/vue_shared/directives/tooltip'; +import Icon from '~/vue_shared/components/icon.vue'; +import { pluralize } from '~/lib/utils/text_utility'; +import { __, sprintf } from '~/locale'; export default { components: { - icon, + Icon, + }, + directives: { + tooltip, }, props: { file: { type: Object, required: true, }, + showTooltip: { + type: Boolean, + required: false, + default: false, + }, + showStagedIcon: { + type: Boolean, + required: false, + default: false, + }, + forceModifiedIcon: { + type: Boolean, + required: false, + default: false, + }, }, computed: { changedIcon() { - return this.file.tempFile ? 'file-addition' : 'file-modified'; + const suffix = this.file.staged && !this.showStagedIcon ? '-solid' : ''; + return this.file.tempFile && !this.forceModifiedIcon + ? `file-addition${suffix}` + : `file-modified${suffix}`; + }, + stagedIcon() { + return `${this.changedIcon}-solid`; }, changedIconClass() { - return `multi-${this.changedIcon}`; + return `multi-${this.changedIcon} pull-left`; + }, + tooltipTitle() { + if (!this.showTooltip) return undefined; + + const type = this.file.tempFile ? 'addition' : 'modification'; + + if (this.file.changed && !this.file.staged) { + return sprintf(__('Unstaged %{type}'), { + type, + }); + } else if (!this.file.changed && this.file.staged) { + return sprintf(__('Staged %{type}'), { + type, + }); + } else if (this.file.changed && this.file.staged) { + return sprintf(__('Unstaged and staged %{type}'), { + type: pluralize(type), + }); + } + + return undefined; }, }, }; </script> <template> - <icon - :name="changedIcon" - :size="12" - :css-classes="`ide-file-changed-icon ${changedIconClass}`" - /> + <span + v-tooltip + :title="tooltipTitle" + data-container="body" + data-placement="right" + class="ide-file-changed-icon" + > + <icon + v-if="file.changed || file.tempFile || file.staged" + :name="changedIcon" + :size="12" + :css-classes="changedIconClass" + /> + </span> </template> diff --git a/app/assets/javascripts/ide/components/commit_sidebar/actions.vue b/app/assets/javascripts/ide/components/commit_sidebar/actions.vue index 45321df191c..b4f3778d946 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, mapGetters } from 'vuex'; import { sprintf, __ } from '~/locale'; import * as consts from '../../stores/modules/commit/constants'; import RadioGroup from './radio_group.vue'; @@ -9,7 +9,8 @@ export default { RadioGroup, }, computed: { - ...mapState(['currentBranchId']), + ...mapState(['currentBranchId', 'changedFiles', 'stagedFiles']), + ...mapGetters(['currentProject']), commitToCurrentBranchText() { return sprintf( __('Commit to %{branchName} branch'), @@ -17,6 +18,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, @@ -41,9 +53,11 @@ export default { :show-input="true" /> <radio-group + v-if="currentProject.merge_requests_enabled" :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 new file mode 100644 index 00000000000..d0a60d647e5 --- /dev/null +++ b/app/assets/javascripts/ide/components/commit_sidebar/empty_state.vue @@ -0,0 +1,36 @@ +<script> +import { mapState } from 'vuex'; + +export default { + computed: { + ...mapState(['lastCommitMsg', 'noChangesStateSvgPath']), + }, +}; +</script> + +<template> + <div + v-if="!lastCommitMsg" + class="multi-file-commit-panel-section ide-commit-empty-state js-empty-state" + > + <div + class="ide-commit-empty-state-container" + > + <div class="svg-content svg-80"> + <img :src="noChangesStateSvgPath" /> + </div> + <div class="append-right-default prepend-left-default"> + <div + class="text-content text-center" + > + <h4> + {{ __('No changes') }} + </h4> + <p> + {{ __('Edit files in the editor and commit changes here') }} + </p> + </div> + </div> + </div> + </div> +</template> 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..81961fe3c57 --- /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 } 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; + }, + enterTransition() { + this.$nextTick(() => { + const elHeight = this.isCompact + ? this.$refs.compactEl && this.$refs.compactEl.offsetHeight + : this.$refs.formEl && this.$refs.formEl.offsetHeight; + + this.componentHeight = elHeight; + }); + }, + 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 453208f3f19..c3ac18bfb83 100644 --- a/app/assets/javascripts/ide/components/commit_sidebar/list.vue +++ b/app/assets/javascripts/ide/components/commit_sidebar/list.vue @@ -1,66 +1,128 @@ <script> - import { mapState } from 'vuex'; - import icon from '~/vue_shared/components/icon.vue'; - import listItem from './list_item.vue'; - import listCollapsed from './list_collapsed.vue'; +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'; - export default { - components: { - icon, - listItem, - listCollapsed, +export default { + components: { + Icon, + ListItem, + }, + directives: { + tooltip, + }, + props: { + title: { + type: String, + required: true, }, - props: { - title: { - type: String, - required: true, - }, - fileList: { - type: Array, - required: true, - }, + fileList: { + type: Array, + required: true, }, - computed: { - ...mapState([ - 'currentProjectId', - 'currentBranchId', - 'rightPanelCollapsed', - ]), - isCommitInfoShown() { - return this.rightPanelCollapsed || this.fileList.length; - }, + iconName: { + type: String, + required: true, }, - methods: { - toggleCollapsed() { - this.$emit('toggleCollapsed'); - }, + action: { + type: String, + required: true, }, - }; + actionBtnText: { + type: String, + required: true, + }, + itemActionComponent: { + type: String, + required: true, + }, + stagedList: { + type: Boolean, + required: false, + default: false, + }, + }, + data() { + return { + showActionButton: false, + }; + }, + computed: { + titleText() { + return sprintf(__('%{title} changes'), { + title: this.title, + }); + }, + }, + methods: { + ...mapActions(['stageAllChanges', 'unstageAllChanges']), + actionBtnClicked() { + this[this.action](); + }, + setShowActionButton(show) { + this.showActionButton = show; + }, + }, +}; </script> <template> <div - :class="{ - 'multi-file-commit-list': isCommitInfoShown - }" + class="ide-commit-list-container" > - <list-collapsed - v-if="rightPanelCollapsed" - /> - <template v-else> - <ul - v-if="fileList.length" - class="list-unstyled append-bottom-0" + <header + class="multi-file-commit-panel-header" + @mouseenter="setShowActionButton(true)" + @mouseleave="setShowActionButton(false)" + > + <div + class="multi-file-commit-panel-header-title" > - <li - v-for="file in fileList" - :key="file.key" + <icon + v-once + :name="iconName" + :size="18" + /> + {{ titleText }} + <span + v-show="!showActionButton" + class="ide-commit-file-count" > - <list-item - :file="file" - /> - </li> - </ul> - </template> + {{ fileList.length }} + </span> + <button + v-show="showActionButton" + type="button" + class="btn btn-blank btn-link ide-staged-action-btn" + @click="actionBtnClicked" + > + {{ actionBtnText }} + </button> + </div> + </header> + <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" + > + {{ __('No changes') }} + </p> </div> </template> diff --git a/app/assets/javascripts/ide/components/commit_sidebar/list_collapsed.vue b/app/assets/javascripts/ide/components/commit_sidebar/list_collapsed.vue index 15918ac9631..2254271c679 100644 --- a/app/assets/javascripts/ide/components/commit_sidebar/list_collapsed.vue +++ b/app/assets/javascripts/ide/components/commit_sidebar/list_collapsed.vue @@ -1,35 +1,110 @@ <script> - import { mapGetters } from 'vuex'; - import icon from '~/vue_shared/components/icon.vue'; +import Icon from '~/vue_shared/components/icon.vue'; +import tooltip from '~/vue_shared/directives/tooltip'; +import { sprintf, n__, __ } from '~/locale'; - export default { - components: { - icon, +export default { + components: { + Icon, + }, + directives: { + tooltip, + }, + props: { + files: { + type: Array, + required: true, }, - computed: { - ...mapGetters([ - 'addedFiles', - 'modifiedFiles', - ]), + iconName: { + type: String, + required: true, }, - }; + title: { + type: String, + required: true, + }, + }, + computed: { + addedFilesLength() { + return this.files.filter(f => f.tempFile).length; + }, + modifiedFilesLength() { + return this.files.filter(f => !f.tempFile).length; + }, + addedFilesIconClass() { + return this.addedFilesLength ? 'multi-file-addition' : ''; + }, + modifiedFilesClass() { + return this.modifiedFilesLength ? 'multi-file-modified' : ''; + }, + additionsTooltip() { + return sprintf(n__('1 %{type} addition', '%d %{type} additions', this.addedFilesLength), { + type: this.title.toLowerCase(), + }); + }, + modifiedTooltip() { + return sprintf( + n__('1 %{type} modification', '%d %{type} modifications', this.modifiedFilesLength), + { type: this.title.toLowerCase() }, + ); + }, + titleTooltip() { + return sprintf(__('%{title} changes'), { title: this.title }); + }, + additionIconName() { + return this.title.toLowerCase() === 'staged' ? 'file-addition-solid' : 'file-addition'; + }, + modifiedIconName() { + return this.title.toLowerCase() === 'staged' ? 'file-modified-solid' : 'file-modified'; + }, + }, +}; </script> <template> <div class="multi-file-commit-list-collapsed text-center" > - <icon - name="file-addition" - :size="18" - css-classes="multi-file-addition append-bottom-10" - /> - {{ addedFiles.length }} - <icon - name="file-modified" - :size="18" - css-classes="multi-file-modified prepend-top-10 append-bottom-10" - /> - {{ modifiedFiles.length }} + <div + v-tooltip + :title="titleTooltip" + data-container="body" + data-placement="left" + class="append-bottom-15" + > + <icon + v-once + :name="iconName" + :size="18" + /> + </div> + <div + v-tooltip + :title="additionsTooltip" + data-container="body" + data-placement="left" + class="append-bottom-10" + > + <icon + :name="additionIconName" + :size="18" + :css-classes="addedFilesIconClass" + /> + </div> + {{ addedFilesLength }} + <div + v-tooltip + :title="modifiedTooltip" + data-container="body" + data-placement="left" + class="prepend-top-10 append-bottom-10" + > + <icon + :name="modifiedIconName" + :size="18" + :css-classes="modifiedFilesClass" + /> + </div> + {{ modifiedFilesLength }} </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 560cdd941cd..03f3e4de83c 100644 --- a/app/assets/javascripts/ide/components/commit_sidebar/list_item.vue +++ b/app/assets/javascripts/ide/components/commit_sidebar/list_item.vue @@ -1,34 +1,70 @@ <script> 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: { Icon, + StageButton, + UnstageButton, }, props: { file: { type: Object, required: true, }, + actionComponent: { + type: String, + required: true, + }, + keyPrefix: { + type: String, + required: false, + default: '', + }, + stagedList: { + type: Boolean, + required: false, + default: false, + }, }, computed: { iconName() { - return this.file.tempFile ? 'file-addition' : 'file-modified'; + const prefix = this.stagedList ? '-solid' : ''; + return this.file.tempFile ? `file-addition${prefix}` : `file-modified${prefix}`; }, iconClass() { return `multi-file-${this.file.tempFile ? 'addition' : 'modified'} append-right-8`; }, }, methods: { - ...mapActions(['discardFileChanges', 'updateViewer', 'openPendingTab']), - openFileInEditor(file) { - return this.openPendingTab(file).then(changeViewer => { + ...mapActions([ + 'discardFileChanges', + 'updateViewer', + 'openPendingTab', + 'unstageChange', + 'stageChange', + ]), + openFileInEditor() { + return this.openPendingTab({ + file: this.file, + keyPrefix: this.keyPrefix.toLowerCase(), + }).then(changeViewer => { if (changeViewer) { - this.updateViewer('diff'); + this.updateViewer(viewerTypes.diff); } }); }, + fileAction() { + if (this.file.staged) { + this.unstageChange(this.file.path); + } else { + this.stageChange(this.file.path); + } + }, }, }; </script> @@ -38,7 +74,9 @@ export default { <button type="button" class="multi-file-commit-list-path" - @click="openFileInEditor(file)"> + @dblclick="fileAction" + @click="openFileInEditor" + > <span class="multi-file-commit-list-file-path"> <icon :name="iconName" @@ -47,12 +85,9 @@ export default { />{{ file.path }} </span> </button> - <button - type="button" - class="btn btn-blank multi-file-discard-btn" - @click="discardFileChanges(file.path)" - > - Discard - </button> + <component + :is="actionComponent" + :path="file.path" + /> </div> </template> 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/stage_button.vue b/app/assets/javascripts/ide/components/commit_sidebar/stage_button.vue new file mode 100644 index 00000000000..52dce8412ab --- /dev/null +++ b/app/assets/javascripts/ide/components/commit_sidebar/stage_button.vue @@ -0,0 +1,59 @@ +<script> +import { mapActions } from 'vuex'; +import Icon from '~/vue_shared/components/icon.vue'; +import tooltip from '~/vue_shared/directives/tooltip'; + +export default { + components: { + Icon, + }, + directives: { + tooltip, + }, + props: { + path: { + type: String, + required: true, + }, + }, + methods: { + ...mapActions(['stageChange', 'discardFileChanges']), + }, +}; +</script> + +<template> + <div + v-once + class="multi-file-discard-btn" + > + <button + v-tooltip + type="button" + class="btn btn-blank append-right-5" + :aria-label="__('Stage changes')" + :title="__('Stage changes')" + data-container="body" + @click.stop="stageChange(path)" + > + <icon + name="mobile-issue-close" + :size="12" + /> + </button> + <button + v-tooltip + type="button" + class="btn btn-blank" + :aria-label="__('Discard changes')" + :title="__('Discard changes')" + data-container="body" + @click.stop="discardFileChanges(path)" + > + <icon + name="remove" + :size="12" + /> + </button> + </div> +</template> 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/commit_sidebar/unstage_button.vue b/app/assets/javascripts/ide/components/commit_sidebar/unstage_button.vue new file mode 100644 index 00000000000..123d60da47e --- /dev/null +++ b/app/assets/javascripts/ide/components/commit_sidebar/unstage_button.vue @@ -0,0 +1,45 @@ +<script> +import { mapActions } from 'vuex'; +import Icon from '~/vue_shared/components/icon.vue'; +import tooltip from '~/vue_shared/directives/tooltip'; + +export default { + components: { + Icon, + }, + directives: { + tooltip, + }, + props: { + path: { + type: String, + required: true, + }, + }, + methods: { + ...mapActions(['unstageChange']), + }, +}; +</script> + +<template> + <div + v-once + class="multi-file-discard-btn" + > + <button + v-tooltip + type="button" + class="btn btn-blank" + :aria-label="__('Unstage changes')" + :title="__('Unstage changes')" + data-container="body" + @click="unstageChange(path)" + > + <icon + name="history" + :size="12" + /> + </button> + </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/file_finder/index.vue b/app/assets/javascripts/ide/components/file_finder/index.vue new file mode 100644 index 00000000000..cabb3f59b17 --- /dev/null +++ b/app/assets/javascripts/ide/components/file_finder/index.vue @@ -0,0 +1,243 @@ +<script> +import { mapActions, mapGetters, mapState } from 'vuex'; +import fuzzaldrinPlus from 'fuzzaldrin-plus'; +import VirtualList from 'vue-virtual-scroll-list'; +import Item from './item.vue'; +import router from '../../ide_router'; +import { + MAX_FILE_FINDER_RESULTS, + FILE_FINDER_ROW_HEIGHT, + FILE_FINDER_EMPTY_ROW_HEIGHT, +} from '../../constants'; +import { + UP_KEY_CODE, + DOWN_KEY_CODE, + ENTER_KEY_CODE, + ESC_KEY_CODE, +} from '../../../lib/utils/keycodes'; + +export default { + components: { + Item, + VirtualList, + }, + data() { + return { + focusedIndex: 0, + searchText: '', + mouseOver: false, + cancelMouseOver: false, + }; + }, + computed: { + ...mapGetters(['allBlobs']), + ...mapState(['fileFindVisible', 'loading']), + filteredBlobs() { + const searchText = this.searchText.trim(); + + if (searchText === '') { + return this.allBlobs.slice(0, MAX_FILE_FINDER_RESULTS); + } + + return fuzzaldrinPlus.filter(this.allBlobs, searchText, { + key: 'path', + maxResults: MAX_FILE_FINDER_RESULTS, + }); + }, + filteredBlobsLength() { + return this.filteredBlobs.length; + }, + listShowCount() { + return this.filteredBlobsLength ? Math.min(this.filteredBlobsLength, 5) : 1; + }, + listHeight() { + return this.filteredBlobsLength ? FILE_FINDER_ROW_HEIGHT : FILE_FINDER_EMPTY_ROW_HEIGHT; + }, + showClearInputButton() { + return this.searchText.trim() !== ''; + }, + }, + watch: { + fileFindVisible() { + this.$nextTick(() => { + if (!this.fileFindVisible) { + this.searchText = ''; + } else { + this.focusedIndex = 0; + + if (this.$refs.searchInput) { + this.$refs.searchInput.focus(); + } + } + }); + }, + searchText() { + this.focusedIndex = 0; + }, + focusedIndex() { + if (!this.mouseOver) { + this.$nextTick(() => { + const el = this.$refs.virtualScrollList.$el; + const scrollTop = this.focusedIndex * FILE_FINDER_ROW_HEIGHT; + const bottom = this.listShowCount * FILE_FINDER_ROW_HEIGHT; + + if (this.focusedIndex === 0) { + // if index is the first index, scroll straight to start + el.scrollTop = 0; + } else if (this.focusedIndex === this.filteredBlobsLength - 1) { + // if index is the last index, scroll to the end + el.scrollTop = this.filteredBlobsLength * FILE_FINDER_ROW_HEIGHT; + } else if (scrollTop >= bottom + el.scrollTop) { + // if element is off the bottom of the scroll list, scroll down one item + el.scrollTop = scrollTop - bottom + FILE_FINDER_ROW_HEIGHT; + } else if (scrollTop < el.scrollTop) { + // if element is off the top of the scroll list, scroll up one item + el.scrollTop = scrollTop; + } + }); + } + }, + }, + methods: { + ...mapActions(['toggleFileFinder']), + clearSearchInput() { + this.searchText = ''; + + this.$nextTick(() => { + this.$refs.searchInput.focus(); + }); + }, + onKeydown(e) { + switch (e.keyCode) { + case UP_KEY_CODE: + e.preventDefault(); + this.mouseOver = false; + this.cancelMouseOver = true; + if (this.focusedIndex > 0) { + this.focusedIndex -= 1; + } else { + this.focusedIndex = this.filteredBlobsLength - 1; + } + break; + case DOWN_KEY_CODE: + e.preventDefault(); + this.mouseOver = false; + this.cancelMouseOver = true; + if (this.focusedIndex < this.filteredBlobsLength - 1) { + this.focusedIndex += 1; + } else { + this.focusedIndex = 0; + } + break; + default: + break; + } + }, + onKeyup(e) { + switch (e.keyCode) { + case ENTER_KEY_CODE: + this.openFile(this.filteredBlobs[this.focusedIndex]); + break; + case ESC_KEY_CODE: + this.toggleFileFinder(false); + break; + default: + break; + } + }, + openFile(file) { + this.toggleFileFinder(false); + router.push(`/project${file.url}`); + }, + onMouseOver(index) { + if (!this.cancelMouseOver) { + this.mouseOver = true; + this.focusedIndex = index; + } + }, + onMouseMove(index) { + this.cancelMouseOver = false; + this.onMouseOver(index); + }, + }, +}; +</script> + +<template> + <div + class="ide-file-finder-overlay" + @mousedown.self="toggleFileFinder(false)" + > + <div + class="dropdown-menu diff-file-changes ide-file-finder show" + > + <div class="dropdown-input"> + <input + type="search" + class="dropdown-input-field" + :placeholder="__('Search files')" + autocomplete="off" + v-model="searchText" + ref="searchInput" + @keydown="onKeydown($event)" + @keyup="onKeyup($event)" + /> + <i + aria-hidden="true" + class="fa fa-search dropdown-input-search" + :class="{ + hidden: showClearInputButton + }" + ></i> + <i + role="button" + :aria-label="__('Clear search input')" + class="fa fa-times dropdown-input-clear" + :class="{ + show: showClearInputButton + }" + @click="clearSearchInput" + ></i> + </div> + <div> + <virtual-list + :size="listHeight" + :remain="listShowCount" + wtag="ul" + ref="virtualScrollList" + > + <template v-if="filteredBlobsLength"> + <li + v-for="(file, index) in filteredBlobs" + :key="file.key" + > + <item + class="disable-hover" + :file="file" + :search-text="searchText" + :focused="index === focusedIndex" + :index="index" + @click="openFile" + @mouseover="onMouseOver" + @mousemove="onMouseMove" + /> + </li> + </template> + <li + v-else + class="dropdown-menu-empty-item" + > + <div class="append-right-default prepend-left-default prepend-top-8 append-bottom-8"> + <template v-if="loading"> + {{ __('Loading...') }} + </template> + <template v-else> + {{ __('No files found.') }} + </template> + </div> + </li> + </virtual-list> + </div> + </div> + </div> +</template> diff --git a/app/assets/javascripts/ide/components/file_finder/item.vue b/app/assets/javascripts/ide/components/file_finder/item.vue new file mode 100644 index 00000000000..d4427420207 --- /dev/null +++ b/app/assets/javascripts/ide/components/file_finder/item.vue @@ -0,0 +1,113 @@ +<script> +import fuzzaldrinPlus from 'fuzzaldrin-plus'; +import FileIcon from '../../../vue_shared/components/file_icon.vue'; +import ChangedFileIcon from '../changed_file_icon.vue'; + +const MAX_PATH_LENGTH = 60; + +export default { + components: { + ChangedFileIcon, + FileIcon, + }, + props: { + file: { + type: Object, + required: true, + }, + focused: { + type: Boolean, + required: true, + }, + searchText: { + type: String, + required: true, + }, + index: { + type: Number, + required: true, + }, + }, + computed: { + pathWithEllipsis() { + const path = this.file.path; + + return path.length < MAX_PATH_LENGTH + ? path + : `...${path.substr(path.length - MAX_PATH_LENGTH)}`; + }, + nameSearchTextOccurences() { + return fuzzaldrinPlus.match(this.file.name, this.searchText); + }, + pathSearchTextOccurences() { + return fuzzaldrinPlus.match(this.pathWithEllipsis, this.searchText); + }, + }, + methods: { + clickRow() { + this.$emit('click', this.file); + }, + mouseOverRow() { + this.$emit('mouseover', this.index); + }, + mouseMove() { + this.$emit('mousemove', this.index); + }, + }, +}; +</script> + +<template> + <button + type="button" + class="diff-changed-file" + :class="{ + 'is-focused': focused, + }" + @click.prevent="clickRow" + @mouseover="mouseOverRow" + @mousemove="mouseMove" + > + <file-icon + :file-name="file.name" + :size="16" + css-classes="diff-file-changed-icon append-right-8" + /> + <span class="diff-changed-file-content append-right-8"> + <strong + class="diff-changed-file-name" + > + <span + v-for="(char, index) in file.name.split('')" + :key="index + char" + :class="{ + highlighted: nameSearchTextOccurences.indexOf(index) >= 0, + }" + v-text="char" + > + </span> + </strong> + <span + class="diff-changed-file-path prepend-top-5" + > + <span + v-for="(char, index) in pathWithEllipsis.split('')" + :key="index + char" + :class="{ + highlighted: pathSearchTextOccurences.indexOf(index) >= 0, + }" + v-text="char" + > + </span> + </span> + </span> + <span + v-if="file.changed || file.tempFile" + class="diff-changed-stats" + > + <changed-file-icon + :file="file" + /> + </span> + </button> +</template> diff --git a/app/assets/javascripts/ide/components/ide.vue b/app/assets/javascripts/ide/components/ide.vue index 1c237c0ec97..2c184ea726c 100644 --- a/app/assets/javascripts/ide/components/ide.vue +++ b/app/assets/javascripts/ide/components/ide.vue @@ -1,35 +1,31 @@ <script> -import { mapState, mapGetters } from 'vuex'; -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 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; export default { components: { - ideSidebar, - ideContextbar, - repoTabs, - ideStatusBar, - repoEditor, - }, - props: { - emptyStateSvgPath: { - type: String, - required: true, - }, - noChangesStateSvgPath: { - type: String, - required: true, - }, - committedStateSvgPath: { - type: String, - required: true, - }, + IdeSidebar, + RepoTabs, + IdeStatusBar, + RepoEditor, + FindFile, }, computed: { - ...mapState(['changedFiles', 'openFiles', 'viewer', 'currentMergeRequestId']), + ...mapState([ + 'changedFiles', + 'openFiles', + 'viewer', + 'currentMergeRequestId', + 'fileFindVisible', + 'emptyStateSvgPath', + ]), ...mapGetters(['activeFile', 'hasChanges']), }, mounted() { @@ -42,67 +38,91 @@ export default { }); return returnValue; }; + + Mousetrap.bind(['t', 'command+p', 'ctrl+p'], e => { + if (e.preventDefault) { + e.preventDefault(); + } + + 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')) || + el.classList.contains('inputarea') + ) { + return true; + } else if (combo === 'command+p' || combo === 'ctrl+p') { + return false; + } + + return originalStopCallback(e, el, combo); + }, }, }; </script> <template> - <div - class="ide-view" - > - <ide-sidebar /> + <article class="ide"> <div - class="multi-file-edit-pane" + class="ide-view" > - <template - v-if="activeFile" + <find-file + v-show="fileFindVisible" + /> + <ide-sidebar /> + <div + class="multi-file-edit-pane" > - <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 - > - <div - v-once - class="ide-empty-state" + <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" + /> + </template> + <template + v-else > - <div class="row js-empty-state"> - <div class="col-xs-12"> - <div class="svg-content svg-250"> - <img :src="emptyStateSvgPath" /> + <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-xs-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> + Select a file from the left sidebar to begin editing. + Afterwards, you'll be able 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" - /> - </div> + <ide-status-bar :file="activeFile"/> + </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 79a83b47994..00000000000 --- a/app/assets/javascripts/ide/components/ide_context_bar.vue +++ /dev/null @@ -1,84 +0,0 @@ -<script> -import { mapActions, mapGetters, mapState } from 'vuex'; -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, - }, - }, - computed: { - ...mapState(['changedFiles', 'rightPanelCollapsed']), - ...mapGetters(['currentIcon']), - }, - methods: { - ...mapActions(['setPanelCollapsedStatus']), - }, -}; -</script> - -<template> - <resizable-panel - :collapsible="true" - :initial-width="340" - side="right" - > - <div - class="multi-file-commit-panel-section" - > - <header - class="multi-file-commit-panel-header" - :class="{ - 'is-collapsed': rightPanelCollapsed, - }" - > - <div - class="multi-file-commit-panel-header-title" - v-if="!rightPanelCollapsed" - > - <div - v-if="changedFiles.length" - > - <icon - name="list-bulleted" - :size="18" - /> - Staged - </div> - </div> - <button - type="button" - class="btn btn-transparent multi-file-commit-panel-collapse-btn" - @click.stop="setPanelCollapsedStatus({ - side: 'right', - collapsed: !rightPanelCollapsed, - })" - > - <icon - :name="currentIcon" - :size="18" - /> - </button> - </header> - <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_file_buttons.vue b/app/assets/javascripts/ide/components/ide_file_buttons.vue index a6c6f46a144..30b00abf6ed 100644 --- a/app/assets/javascripts/ide/components/ide_file_buttons.vue +++ b/app/assets/javascripts/ide/components/ide_file_buttons.vue @@ -32,14 +32,14 @@ export default { <template> <div v-if="showButtons" - class="pull-right ide-btn-group" + class="float-right ide-btn-group" > <a v-tooltip v-if="!file.binary" :href="file.blamePath" :title="__('Blame')" - class="btn btn-xs btn-transparent blame" + class="btn btn-sm btn-transparent blame" > <icon name="blame" @@ -50,7 +50,7 @@ export default { v-tooltip :href="file.commitsPath" :title="__('History')" - class="btn btn-xs btn-transparent history" + class="btn btn-sm btn-transparent history" > <icon name="history" @@ -61,7 +61,7 @@ export default { v-tooltip :href="file.permalink" :title="__('Permalink')" - class="btn btn-xs btn-transparent permalink" + class="btn btn-sm btn-transparent permalink" > <icon name="link" @@ -72,7 +72,7 @@ export default { v-tooltip :href="file.rawPath" target="_blank" - class="btn btn-xs btn-transparent prepend-left-10 raw" + class="btn btn-sm btn-transparent prepend-left-10 raw" rel="noopener noreferrer" :title="rawDownloadButtonLabel"> <icon 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 152a5f632ad..6f60cfbf184 100644 --- a/app/assets/javascripts/ide/components/ide_status_bar.vue +++ b/app/assets/javascripts/ide/components/ide_status_bar.vue @@ -1,11 +1,16 @@ <script> +import { mapActions, mapState, 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 CiIcon from '../../vue_shared/components/ci_icon.vue'; +import userAvatarImage from '../../vue_shared/components/user_avatar/user_avatar_image.vue'; export default { components: { icon, + userAvatarImage, + CiIcon, }, directives: { tooltip, @@ -14,47 +19,126 @@ export default { props: { file: { type: Object, - required: true, + required: false, + default: null, + }, + }, + data() { + return { + lastCommitFormatedAge: null, + }; + }, + computed: { + ...mapState(['currentBranchId', 'currentProjectId']), + ...mapGetters(['currentProject', 'lastCommit']), + }, + watch: { + lastCommit() { + if (!this.isPollingInitialized) { + this.initPipelinePolling(); + } + }, + }, + mounted() { + this.startTimer(); + }, + beforeDestroy() { + if (this.intervalId) { + clearInterval(this.intervalId); + } + if (this.isPollingInitialized) { + this.stopPipelinePolling(); + } + }, + methods: { + ...mapActions(['pipelinePoll', 'stopPipelinePolling']), + startTimer() { + this.intervalId = setInterval(() => { + this.commitAgeUpdate(); + }, 1000); + }, + initPipelinePolling() { + this.pipelinePoll(); + this.isPollingInitialized = true; + }, + 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 class="ref-name"> + <footer class="ide-status-bar"> + <div + class="ide-status-branch" + v-if="lastCommit && lastCommitFormatedAge" + > + <span + class="ide-status-pipeline" + v-if="lastCommit.pipeline && lastCommit.pipeline.details" + > + <ci-icon + :status="lastCommit.pipeline.details.status" + v-tooltip + :title="lastCommit.pipeline.details.status.text" + /> + Pipeline + <a + class="monospace" + :href="lastCommit.pipeline.details.status.details_path">#{{ lastCommit.pipeline.id }}</a> + {{ lastCommit.pipeline.details.status.text }} + for + </span> + <icon - name="branch" - :size="12" + name="commit" /> - {{ file.branchId }} - </div> - <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> + <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 769e9b79cad..f0b29702497 100644 --- a/app/assets/javascripts/ide/components/new_dropdown/index.vue +++ b/app/assets/javascripts/ide/components/new_dropdown/index.vue @@ -1,49 +1,55 @@ <script> - import { mapActions } from 'vuex'; - import icon from '~/vue_shared/components/icon.vue'; - import newModal from './modal.vue'; - import upload from './upload.vue'; +import { mapActions } from 'vuex'; +import icon from '~/vue_shared/components/icon.vue'; +import newModal from './modal.vue'; +import upload from './upload.vue'; - export default { - components: { - icon, - newModal, - upload, +export default { + components: { + icon, + newModal, + upload, + }, + props: { + branch: { + type: String, + required: true, }, - props: { - branch: { - type: String, - required: true, - }, - path: { - type: String, - required: true, - }, + path: { + type: String, + required: false, + default: '', }, - data() { - return { - openModal: false, - modalType: '', - dropdownOpen: false, - }; + }, + data() { + return { + openModal: false, + modalType: '', + dropdownOpen: false, + }; + }, + watch: { + dropdownOpen() { + this.$nextTick(() => { + this.$refs.dropdownMenu.scrollIntoView(); + }); }, - methods: { - ...mapActions([ - 'createTempEntry', - ]), - createNewItem(type) { - this.modalType = type; - this.openModal = true; - this.dropdownOpen = false; - }, - hideModal() { - this.openModal = false; - }, - openDropdown() { - this.dropdownOpen = !this.dropdownOpen; - }, + }, + methods: { + ...mapActions(['createTempEntry']), + createNewItem(type) { + this.modalType = type; + this.openModal = true; + this.dropdownOpen = false; }, - }; + hideModal() { + this.openModal = false; + }, + openDropdown() { + this.dropdownOpen = !this.dropdownOpen; + }, + }, +}; </script> <template> @@ -51,7 +57,7 @@ <div class="dropdown" :class="{ - open: dropdownOpen, + show: dropdownOpen, }" > <button @@ -63,15 +69,18 @@ <icon name="plus" :size="12" - css-classes="pull-left" + css-classes="float-left" /> <icon name="arrow-down" :size="12" - css-classes="pull-left" + css-classes="float-left" /> </button> - <ul class="dropdown-menu dropdown-menu-right"> + <ul + class="dropdown-menu dropdown-menu-right" + ref="dropdownMenu" + > <li> <a href="#" diff --git a/app/assets/javascripts/ide/components/new_dropdown/modal.vue b/app/assets/javascripts/ide/components/new_dropdown/modal.vue index 4b5a50785b6..d83a90f71e1 100644 --- a/app/assets/javascripts/ide/components/new_dropdown/modal.vue +++ b/app/assets/javascripts/ide/components/new_dropdown/modal.vue @@ -40,13 +40,6 @@ export default { return __('Create file'); }, - formLabelName() { - if (this.type === 'tree') { - return __('Directory name'); - } - - return __('File name'); - }, }, mounted() { this.$refs.fieldName.focus(); @@ -77,13 +70,13 @@ export default { @submit="createEntryInStore" > <form - class="form-horizontal" slot="body" @submit.prevent="createEntryInStore" + class="form-group row append-bottom-0" > <fieldset class="form-group append-bottom-0"> - <label class="label-light col-sm-3"> - {{ formLabelName }} + <label class="label-light col-form-label col-sm-3 ide-new-modal-label"> + {{ __('Name') }} </label> <div class="col-sm-9"> <input diff --git a/app/assets/javascripts/ide/components/repo_commit_section.vue b/app/assets/javascripts/ide/components/repo_commit_section.vue index 14673754503..c5092d8e04d 100644 --- a/app/assets/javascripts/ide/components/repo_commit_section.vue +++ b/app/assets/javascripts/ide/components/repo_commit_section.vue @@ -1,64 +1,63 @@ <script> import { mapState, mapActions, mapGetters } from 'vuex'; import tooltip from '~/vue_shared/directives/tooltip'; -import icon from '~/vue_shared/components/icon.vue'; +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 CommitMessageField from './commit_sidebar/message_field.vue'; +import CommitFilesList from './commit_sidebar/list.vue'; +import EmptyState from './commit_sidebar/empty_state.vue'; import * as consts from '../stores/modules/commit/constants'; -import Actions from './commit_sidebar/actions.vue'; +import { activityBarViews } from '../constants'; export default { components: { DeprecatedModal, - icon, - commitFilesList, - Actions, - LoadingButton, - CommitMessageField, + Icon, + CommitFilesList, + EmptyState, }, directives: { tooltip, }, - props: { - noChangesStateSvgPath: { - type: String, - required: true, - }, - committedStateSvgPath: { - type: String, - required: true, - }, - }, computed: { ...mapState([ - 'currentProjectId', - 'currentBranchId', + 'changedFiles', + 'stagedFiles', 'rightPanelCollapsed', 'lastCommitMsg', - 'changedFiles', + 'unusedSeal', ]), ...mapState('commit', ['commitMessage', 'submitCommitLoading']), - ...mapGetters('commit', ['commitButtonDisabled', 'discardDraftButtonDisabled', 'branchName']), - statusSvg() { - return this.lastCommitMsg ? this.committedStateSvgPath : this.noChangesStateSvgPath; + ...mapGetters(['lastOpenedFile', 'hasChanges', 'someUncommitedChanges']), + ...mapGetters('commit', ['commitButtonDisabled', 'discardDraftButtonDisabled']), + showStageUnstageArea() { + return !!(this.someUncommitedChanges || this.lastCommitMsg || !this.unusedSeal); }, }, - methods: { - ...mapActions(['setPanelCollapsedStatus']), - ...mapActions('commit', [ - 'updateCommitMessage', - 'discardDraft', - 'commitChanges', - 'updateCommitAction', - ]), - toggleCollapsed() { - this.setPanelCollapsedStatus({ - side: 'right', - collapsed: !this.rightPanelCollapsed, - }); + watch: { + hasChanges() { + if (!this.hasChanges) { + this.updateActivityBarView(activityBarViews.edit); + } }, + }, + mounted() { + if (this.lastOpenedFile) { + this.openPendingTab({ + file: this.lastOpenedFile, + }) + .then(changeViewer => { + if (changeViewer) { + this.updateViewer('diff'); + } + }) + .catch(e => { + throw e; + }); + } + }, + methods: { + ...mapActions(['openPendingTab', 'updateViewer', 'updateActivityBarView']), + ...mapActions('commit', ['commitChanges', 'updateCommitAction']), forceCreateNewBranch() { return this.updateCommitAction(consts.COMMIT_TO_NEW_BRANCH).then(() => this.commitChanges()); }, @@ -69,9 +68,6 @@ export default { <template> <div class="multi-file-commit-panel-section" - :class="{ - 'multi-file-commit-empty-state-container': !changedFiles.length - }" > <deprecated-modal id="ide-create-branch-modal" @@ -85,76 +81,30 @@ export default { Would you like to create a new branch?`) }} </template> </deprecated-modal> - <commit-files-list - title="Staged" - :file-list="changedFiles" - :collapsed="rightPanelCollapsed" - @toggleCollapsed="toggleCollapsed" - /> <template - v-if="changedFiles.length" + v-if="showStageUnstageArea" > - <form - class="form-horizontal 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 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> - </div> - </form> + <commit-files-list + class="is-first" + icon-name="unstaged" + :title="__('Unstaged')" + :file-list="changedFiles" + action="stageAllChanges" + :action-btn-text="__('Stage all')" + item-action-component="stage-button" + /> + <commit-files-list + icon-name="staged" + :title="__('Staged')" + :file-list="stagedFiles" + action="unstageAllChanges" + :action-btn-text="__('Unstage all')" + item-action-component="unstage-button" + :staged-list="true" + /> </template> - <div - v-else-if="!rightPanelCollapsed" - class="row js-empty-state" - > - <div class="col-xs-10 col-xs-offset-1"> - <div class="svg-content svg-80"> - <img :src="statusSvg" /> - </div> - </div> - <div class="col-xs-10 col-xs-offset-1"> - <div - class="text-content text-center" - v-if="!lastCommitMsg" - > - <h4> - {{ __('No changes') }} - </h4> - <p> - {{ __('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> + <empty-state + 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 711bafa17a9..a281ecb95c8 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']), + ...mapState(['rightPanelCollapsed', 'viewer', 'panelResizing', 'currentActivityView']), + ...mapGetters([ + 'currentMergeRequest', + 'getStagedFile', + 'isEditModeActive', + 'isCommitModeActive', + 'isReviewModeActive', + ]), shouldHideEditor() { return this.file && this.file.binary && !this.file.content; }, @@ -36,10 +43,29 @@ export default { }, }, watch: { - file(oldVal, newVal) { + file(newVal, oldVal) { + if (oldVal.pending) { + this.removePendingTab(oldVal); + } + // Compare key to allow for files opened in review mode to be cached differently - if (newVal.key !== this.file.key) { + if (oldVal.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 +103,7 @@ export default { 'setFileViewMode', 'setFileEOL', 'updateViewer', - 'updateDelayViewerUpdated', + 'removePendingTab', ]), initMonaco() { if (this.shouldHideEditor) return; @@ -89,14 +115,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 +126,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(); @@ -120,9 +138,14 @@ export default { setupEditor() { if (!this.file || !this.editor.instance) return; - this.model = this.editor.createModel(this.file); + const head = this.getStagedFile(this.file.path); + + this.model = this.editor.createModel( + this.file, + this.file.staged && this.file.key.indexOf('unstaged-') === 0 ? head : null, + ); - if (this.viewer === 'mrdiff') { + if (this.viewer === viewerTypes.mr && this.file.mrChange) { this.editor.attachMergeRequestModel(this.model); } else { this.editor.attachModel(this.model); @@ -163,6 +186,7 @@ export default { }); }, }, + viewerTypes, }; </script> @@ -171,16 +195,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 pull-left" - v-if="!shouldHideEditor"> + class="nav-links float-left" + 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> @@ -207,6 +232,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 3b5068d4910..442697e1c80 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}`); }, }, }; @@ -75,11 +122,11 @@ export default { <div class="file" :class="fileClass" + @click="clickFile" + role="button" > <div class="file-name" - @click="clickFile" - role="button" > <span class="ide-file-name str-truncated" @@ -97,21 +144,40 @@ export default { :file="file" /> </span> - <span class="pull-right"> + <span class="float-right ide-file-icon-holder"> <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-else-if="showChangedFileIcon" :file="file" - v-if="file.changed || file.tempFile" + :show-tooltip="true" + :show-staged-icon="true" + :force-modified-icon="true" + class="pull-right" /> </span> <new-dropdown - v-if="isTree" + v-if="isTree && !disableActionDropdown" :project-id="file.projectId" :branch="file.branchId" :path="file.path" - class="pull-right prepend-left-8" + class="float-right prepend-left-8" /> </div> </div> diff --git a/app/assets/javascripts/ide/components/repo_loading_file.vue b/app/assets/javascripts/ide/components/repo_loading_file.vue index 79af8c0b0c7..3e47da88050 100644 --- a/app/assets/javascripts/ide/components/repo_loading_file.vue +++ b/app/assets/javascripts/ide/components/repo_loading_file.vue @@ -25,13 +25,13 @@ /> </td> <template v-if="!leftPanelCollapsed"> - <td class="hidden-sm hidden-xs"> + <td class="d-none d-sm-none d-md-block"> <skeleton-loading-container :small="true" /> </td> - <td class="hidden-xs"> + <td class="d-none d-sm-block"> <skeleton-loading-container class="animation-container-right" :small="true" diff --git a/app/assets/javascripts/ide/components/repo_tab.vue b/app/assets/javascripts/ide/components/repo_tab.vue index 304a73ed1ad..fb26b973236 100644 --- a/app/assets/javascripts/ide/components/repo_tab.vue +++ b/app/assets/javascripts/ide/components/repo_tab.vue @@ -26,13 +26,18 @@ export default { }, computed: { closeLabel() { - if (this.tab.changed || this.tab.tempFile) { + if (this.fileHasChanged) { return `${this.tab.name} changed`; } return `Close ${this.tab.name}`; }, showChangedIcon() { - return this.tab.changed ? !this.tabMouseOver : false; + if (this.tab.pending) return true; + + return this.fileHasChanged ? !this.tabMouseOver : false; + }, + fileHasChanged() { + return this.tab.changed || this.tab.tempFile || this.tab.staged; }, }, @@ -42,18 +47,18 @@ export default { this.updateDelayViewerUpdated(true); if (tab.pending) { - this.openPendingTab(tab); + this.openPendingTab({ file: tab, keyPrefix: tab.staged ? 'staged' : 'unstaged' }); } else { this.$router.push(`/project${tab.url}`); } }, mouseOverTab() { - if (this.tab.changed) { + if (this.fileHasChanged) { this.tabMouseOver = true; } }, mouseOutTab() { - if (this.tab.changed) { + if (this.fileHasChanged) { this.tabMouseOver = false; } }, @@ -63,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" @@ -81,24 +103,8 @@ export default { <changed-file-icon v-else :file="tab" + :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 b60d042e0be..83fe22f40a4 100644 --- a/app/assets/javascripts/ide/constants.js +++ b/app/assets/javascripts/ide/constants.js @@ -1,3 +1,22 @@ // Fuzzy file finder +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; + +// 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..a21cec4e8d8 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); @@ -40,7 +41,7 @@ const router = new VueRouter({ component: EmptyRouterComponent, children: [ { - path: ':targetmode(edit|tree|blob)/:branch/*', + path: ':targetmode(edit|tree|blob)/*', component: EmptyRouterComponent, }, { @@ -62,21 +63,27 @@ router.beforeEach((to, from, next) => { .then(() => { const fullProjectId = `${to.params.namespace}/${to.params.project}`; - if (to.params.branch) { + const baseSplit = to.params[0].split('/-/'); + const branchId = baseSplit[0].slice(-1) === '/' ? baseSplit[0].slice(0, -1) : baseSplit[0]; + + if (branchId) { + const basePath = baseSplit.length > 1 ? baseSplit[1] : ''; + + store.dispatch('setCurrentBranchId', branchId); + store.dispatch('getBranchData', { projectId: fullProjectId, - branchId: to.params.branch, + branchId, }); store .dispatch('getFiles', { projectId: fullProjectId, - branchId: to.params.branch, + branchId, }) .then(() => { - if (to.params[0]) { - const path = - to.params[0].slice(-1) === '/' ? to.params[0].slice(0, -1) : to.params[0]; + if (basePath) { + const path = basePath.slice(-1) === '/' ? basePath.slice(0, -1) : basePath; const treeEntryKey = Object.keys(store.state.entries).find( key => key === path && !store.state.entries[key].pending, ); @@ -99,14 +106,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/common/model.js b/app/assets/javascripts/ide/lib/common/model.js index e47adae99ed..b1e43a1e38c 100644 --- a/app/assets/javascripts/ide/lib/common/model.js +++ b/app/assets/javascripts/ide/lib/common/model.js @@ -3,22 +3,23 @@ import Disposable from './disposable'; import eventHub from '../../eventhub'; export default class Model { - constructor(monaco, file) { + constructor(monaco, file, head = null) { this.monaco = monaco; this.disposable = new Disposable(); this.file = file; + this.head = head; this.content = file.content !== '' ? file.content : file.raw; this.disposable.add( (this.originalModel = this.monaco.editor.createModel( - this.file.raw, + head ? head.content : this.file.raw, undefined, - new this.monaco.Uri(null, null, `original/${this.file.key}`), + new this.monaco.Uri(null, null, `original/${this.path}`), )), (this.model = this.monaco.editor.createModel( this.content, undefined, - new this.monaco.Uri(null, null, this.file.key), + new this.monaco.Uri(null, null, this.path), )), ); if (this.file.mrChange) { @@ -26,18 +27,20 @@ export default class Model { (this.baseModel = this.monaco.editor.createModel( this.file.baseRaw, undefined, - new this.monaco.Uri(null, null, `target/${this.file.path}`), + new this.monaco.Uri(null, null, `target/${this.path}`), )), ); } - this.events = new Map(); + this.events = new Set(); this.updateContent = this.updateContent.bind(this); + this.updateNewContent = this.updateNewContent.bind(this); this.dispose = this.dispose.bind(this); eventHub.$on(`editor.update.model.dispose.${this.file.key}`, this.dispose); - eventHub.$on(`editor.update.model.content.${this.file.path}`, this.updateContent); + eventHub.$on(`editor.update.model.content.${this.file.key}`, this.updateContent); + eventHub.$on(`editor.update.model.new.content.${this.file.key}`, this.updateNewContent); } get url() { @@ -73,22 +76,36 @@ export default class Model { } onChange(cb) { - this.events.set( - this.path, - this.disposable.add(this.model.onDidChangeContent(e => cb(this, e))), - ); + this.events.add(this.disposable.add(this.model.onDidChangeContent(e => cb(this, e)))); + } + + onDispose(cb) { + this.events.add(cb); } - updateContent(content) { + updateContent({ content, changed }) { this.getOriginalModel().setValue(content); + + if (!changed) { + this.getModel().setValue(content); + } + } + + updateNewContent(content) { this.getModel().setValue(content); } dispose() { this.disposable.dispose(); + + this.events.forEach(cb => { + if (typeof cb === 'function') cb(); + }); + this.events.clear(); eventHub.$off(`editor.update.model.dispose.${this.file.key}`, this.dispose); - eventHub.$off(`editor.update.model.content.${this.file.path}`, this.updateContent); + eventHub.$off(`editor.update.model.content.${this.file.key}`, this.updateContent); + eventHub.$off(`editor.update.model.new.content.${this.file.key}`, this.updateNewContent); } } diff --git a/app/assets/javascripts/ide/lib/common/model_manager.js b/app/assets/javascripts/ide/lib/common/model_manager.js index 0e7b563b5d6..7f643969480 100644 --- a/app/assets/javascripts/ide/lib/common/model_manager.js +++ b/app/assets/javascripts/ide/lib/common/model_manager.js @@ -17,12 +17,12 @@ export default class ModelManager { return this.models.get(key); } - addModel(file) { + addModel(file, head = null) { if (this.hasCachedModel(file.key)) { return this.getModel(file.key); } - const model = new Model(this.monaco, file); + const model = new Model(this.monaco, file, head); this.models.set(model.path, model); this.disposable.add(model); diff --git a/app/assets/javascripts/ide/lib/decorations/controller.js b/app/assets/javascripts/ide/lib/decorations/controller.js index 42904774747..13d477bb2cf 100644 --- a/app/assets/javascripts/ide/lib/decorations/controller.js +++ b/app/assets/javascripts/ide/lib/decorations/controller.js @@ -38,6 +38,15 @@ export default class DecorationsController { ); } + hasDecorations(model) { + return this.decorations.has(model.url); + } + + removeDecorations(model) { + this.decorations.delete(model.url); + this.editorDecorations.delete(model.url); + } + dispose() { this.decorations.clear(); this.editorDecorations.clear(); diff --git a/app/assets/javascripts/ide/lib/diff/controller.js b/app/assets/javascripts/ide/lib/diff/controller.js index b136545ad11..f579424cf33 100644 --- a/app/assets/javascripts/ide/lib/diff/controller.js +++ b/app/assets/javascripts/ide/lib/diff/controller.js @@ -3,7 +3,7 @@ import { throttle } from 'underscore'; import DirtyDiffWorker from './diff_worker'; import Disposable from '../common/disposable'; -export const getDiffChangeType = (change) => { +export const getDiffChangeType = change => { if (change.modified) { return 'modified'; } else if (change.added) { @@ -16,12 +16,7 @@ export const getDiffChangeType = (change) => { }; export const getDecorator = change => ({ - range: new monaco.Range( - change.lineNumber, - 1, - change.endLineNumber, - 1, - ), + range: new monaco.Range(change.lineNumber, 1, change.endLineNumber, 1), options: { isWholeLine: true, linesDecorationsClassName: `dirty-diff dirty-diff-${getDiffChangeType(change)}`, @@ -31,6 +26,7 @@ export const getDecorator = change => ({ export default class DirtyDiffController { constructor(modelManager, decorationsController) { this.disposable = new Disposable(); + this.models = new Map(); this.editorSimpleWorker = null; this.modelManager = modelManager; this.decorationsController = decorationsController; @@ -42,7 +38,15 @@ export default class DirtyDiffController { } attachModel(model) { + if (this.models.has(model.url)) return; + model.onChange(() => this.throttledComputeDiff(model)); + model.onDispose(() => { + this.decorationsController.removeDecorations(model); + this.models.delete(model.url); + }); + + this.models.set(model.url, model); } computeDiff(model) { @@ -54,7 +58,11 @@ export default class DirtyDiffController { } reDecorate(model) { - this.decorationsController.decorate(model); + if (this.decorationsController.hasDecorations(model)) { + this.decorationsController.decorate(model); + } else { + this.computeDiff(model); + } } decorate({ data }) { @@ -65,6 +73,7 @@ export default class DirtyDiffController { dispose() { this.disposable.dispose(); + this.models.clear(); this.dirtyDiffWorker.removeEventListener('message', this.decorate); this.dirtyDiffWorker.terminate(); diff --git a/app/assets/javascripts/ide/lib/editor.js b/app/assets/javascripts/ide/lib/editor.js index 001737d6ee8..9c3bb9cc17d 100644 --- a/app/assets/javascripts/ide/lib/editor.js +++ b/app/assets/javascripts/ide/lib/editor.js @@ -1,10 +1,12 @@ import _ from 'underscore'; +import store from '../stores'; import DecorationsController from './decorations/controller'; import DirtyDiffController from './diff/controller'; import Disposable from './common/disposable'; import ModelManager from './common/model_manager'; import editorOptions, { defaultEditorOptions } from './editor_options'; import gitlabTheme from './themes/gl_theme'; +import keymap from './keymap.json'; export const clearDomElement = el => { if (!el || !el.firstChild) return; @@ -53,32 +55,36 @@ export default class Editor { )), ); + this.addCommands(); + window.addEventListener('resize', this.debouncedUpdate, false); } } - 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, })), ); + this.addCommands(); + window.addEventListener('resize', this.debouncedUpdate, false); } } - createModel(file) { - return this.modelManager.addModel(file); + createModel(file, head = null) { + return this.modelManager.addModel(file, head); } attachModel(model) { @@ -189,4 +195,31 @@ export default class Editor { static renderSideBySide(domElement) { return domElement.offsetWidth >= 700; } + + addCommands() { + const getKeyCode = key => { + const monacoKeyMod = key.indexOf('KEY_') === 0; + + return monacoKeyMod ? this.monaco.KeyCode[key] : this.monaco.KeyMod[key]; + }; + + keymap.forEach(command => { + const keybindings = command.bindings.map(binding => { + const keys = binding.split('+'); + + // eslint-disable-next-line no-bitwise + return keys.length > 1 ? getKeyCode(keys[0]) | getKeyCode(keys[1]) : getKeyCode(keys[0]); + }); + + this.instance.addAction({ + id: command.id, + label: command.label, + keybindings, + run() { + store.dispatch(command.action.name, command.action.params); + return null; + }, + }); + }); + } } diff --git a/app/assets/javascripts/ide/lib/keymap.json b/app/assets/javascripts/ide/lib/keymap.json new file mode 100644 index 00000000000..131abfebbed --- /dev/null +++ b/app/assets/javascripts/ide/lib/keymap.json @@ -0,0 +1,11 @@ +[ + { + "id": "file-finder", + "label": "File finder", + "bindings": ["CtrlCmd+KEY_P"], + "action": { + "name": "toggleFileFinder", + "params": true + } + } +] diff --git a/app/assets/javascripts/ide/services/index.js b/app/assets/javascripts/ide/services/index.js index a12e637616a..e8b51f2b516 100644 --- a/app/assets/javascripts/ide/services/index.js +++ b/app/assets/javascripts/ide/services/index.js @@ -75,4 +75,8 @@ export default { }, }); }, + lastCommitPipelines({ getters }) { + const commitSha = getters.lastCommit.id; + return Api.commitPipelines(getters.currentProject.path_with_namespace, commitSha); + }, }; diff --git a/app/assets/javascripts/ide/stores/actions.js b/app/assets/javascripts/ide/stores/actions.js index c6ba679d99c..1a98b42761e 100644 --- a/app/assets/javascripts/ide/stores/actions.js +++ b/app/assets/javascripts/ide/stores/actions.js @@ -1,3 +1,4 @@ +import $ from 'jquery'; import Vue from 'vue'; import { visitUrl } from '~/lib/utils/url_utility'; import flash from '~/flash'; @@ -32,6 +33,19 @@ export const setPanelCollapsedStatus = ({ commit }, { side, collapsed }) => { } }; +export const toggleRightPanelCollapsed = ({ dispatch, state }, e = undefined) => { + if (e) { + $(e.currentTarget) + .tooltip('hide') + .blur(); + } + + dispatch('setPanelCollapsedStatus', { + side: 'right', + collapsed: !state.rightPanelCollapsed, + }); +}; + export const setResizingStatus = ({ commit }, resizing) => { commit(types.SET_RESIZING_STATUS, resizing); }; @@ -60,7 +74,7 @@ export const createTempEntry = ( } worker.addEventListener('message', ({ data }) => { - const { file } = data; + const { file, parentPath } = data; worker.terminate(); @@ -76,6 +90,10 @@ export const createTempEntry = ( dispatch('setFileActive', file.path); } + if (parentPath && !state.entries[parentPath].opened) { + commit(types.TOGGLE_TREE_OPEN, parentPath); + } + resolve(file); }); @@ -104,6 +122,16 @@ export const scrollToTab = () => { }); }; +export const stageAllChanges = ({ state, commit }) => { + commit(types.SET_LAST_COMMIT_MSG, ''); + + state.changedFiles.forEach(file => commit(types.STAGE_CHANGE, file.path)); +}; + +export const unstageAllChanges = ({ state, commit }) => { + state.stagedFiles.forEach(file => commit(types.UNSTAGE_CHANGE, file.path)); +}; + export const updateViewer = ({ commit }, viewer) => { commit(types.UPDATE_VIEWER, viewer); }; @@ -112,7 +140,39 @@ 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 }); + + if (file.parentPath) { + dispatch('updateTempFlagForEntry', { file: state.entries[file.parentPath], tempFile }); + } +}; + +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'; export * from './actions/merge_request'; + +// prevent babel-plugin-rewire from generating an invalid default during karma tests +export default () => {}; diff --git a/app/assets/javascripts/ide/stores/actions/file.js b/app/assets/javascripts/ide/stores/actions/file.js index 1a17320a1ea..13aea91d8ba 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,10 +24,12 @@ export const closeFile = ({ commit, state, dispatch }, file) => { const nextFileToOpen = state.openFiles[nextIndexToOpen]; if (nextFileToOpen.pending) { - dispatch('updateViewer', 'diff'); - dispatch('openPendingTab', nextFileToOpen); + 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) { @@ -60,7 +63,9 @@ export const getFileData = ({ state, commit, dispatch }, { path, makeFileActive const file = state.entries[path]; commit(types.TOGGLE_LOADING, { entry: file }); return service - .getFileData(file.url) + .getFileData( + `${gon.relative_url_root ? gon.relative_url_root : ''}${file.url.replace('/-/', '/')}`, + ) .then(res => { const pageTitle = decodeURI(normalizeHeaders(res.headers)['PAGE-TITLE']); setPageTitle(pageTitle); @@ -114,7 +119,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 }); @@ -125,6 +130,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 }) => { @@ -153,7 +160,7 @@ export const setFileViewMode = ({ state, commit }, { file, viewMode }) => { commit(types.SET_FILE_VIEWMODE, { file, viewMode }); }; -export const discardFileChanges = ({ state, commit }, path) => { +export const discardFileChanges = ({ dispatch, state, commit, getters }, path) => { const file = state.entries[path]; commit(types.DISCARD_FILE_CHANGES, path); @@ -161,17 +168,41 @@ export const discardFileChanges = ({ state, commit }, path) => { if (file.tempFile && file.opened) { commit(types.TOGGLE_FILE_OPEN, path); + } else if (getters.activeFile && file.path === getters.activeFile.path) { + dispatch('updateDelayViewerUpdated', true) + .then(() => { + router.push(`/project${file.url}`); + }) + .catch(e => { + throw e; + }); } - eventHub.$emit(`editor.update.model.content.${file.path}`, file.raw); + eventHub.$emit(`editor.update.model.new.content.${file.key}`, file.content); + eventHub.$emit(`editor.update.model.dispose.unstaged-${file.key}`, file.content); }; -export const openPendingTab = ({ commit, getters, dispatch, state }, file) => { - if (getters.activeFile && getters.activeFile.path === file.path && state.viewer === 'diff') { - return false; +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); } +}; + +export const unstageChange = ({ commit }, path) => { + commit(types.UNSTAGE_CHANGE, path); +}; + +export const openPendingTab = ({ commit, getters, dispatch, state }, { file, keyPrefix }) => { + if (getters.activeFile && getters.activeFile.key === `${keyPrefix}-${file.key}`) return false; + + state.openFiles.forEach(f => eventHub.$emit(`editor.update.model.dispose.${f.key}`)); - commit(types.ADD_PENDING_TAB, { file }); + commit(types.ADD_PENDING_TAB, { file, keyPrefix }); dispatch('scrollToTab'); diff --git a/app/assets/javascripts/ide/stores/actions/project.js b/app/assets/javascripts/ide/stores/actions/project.js index 4eb23b2ee0f..cece9154c82 100644 --- a/app/assets/javascripts/ide/stores/actions/project.js +++ b/app/assets/javascripts/ide/stores/actions/project.js @@ -1,6 +1,11 @@ +import Visibility from 'visibilityjs'; import flash from '~/flash'; +import { __ } from '~/locale'; import service from '../../services'; import * as types from '../mutation_types'; +import Poll from '../../../lib/utils/poll'; + +let eTagPoll; export const getProjectData = ( { commit, state, dispatch }, @@ -21,7 +26,7 @@ export const getProjectData = ( }) .catch(() => { flash( - 'Error loading project data. Please try again.', + __('Error loading project data. Please try again.'), 'alert', document, null, @@ -55,12 +60,11 @@ export const getBranchData = ( branch: data, }); commit(types.SET_BRANCH_WORKING_REFERENCE, { projectId, branchId, reference: id }); - commit(types.SET_CURRENT_BRANCH, branchId); resolve(data); }) .catch(() => { flash( - 'Error loading branch data. Please try again.', + __('Error loading branch data. Please try again.'), 'alert', document, null, @@ -73,3 +77,75 @@ 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); + }); + +export const pollSuccessCallBack = ({ commit, state, dispatch }, { data }) => { + if (data.pipelines && data.pipelines.length) { + const lastCommitHash = + state.projects[state.currentProjectId].branches[state.currentBranchId].commit.id; + const lastCommitPipeline = data.pipelines.find( + pipeline => pipeline.commit.id === lastCommitHash, + ); + commit(types.SET_LAST_COMMIT_PIPELINE, { + projectId: state.currentProjectId, + branchId: state.currentBranchId, + pipeline: lastCommitPipeline || {}, + }); + } + + return data; +}; + +export const pipelinePoll = ({ getters, dispatch }) => { + eTagPoll = new Poll({ + resource: service, + method: 'lastCommitPipelines', + data: { + getters, + }, + successCallback: ({ data }) => dispatch('pollSuccessCallBack', { data }), + errorCallback: () => { + flash( + __('Something went wrong while fetching the latest pipeline status.'), + 'alert', + document, + null, + false, + true, + ); + }, + }); + + if (!Visibility.hidden()) { + eTagPoll.makeRequest(); + } + + Visibility.change(() => { + if (!Visibility.hidden()) { + eTagPoll.restart(); + } else { + eTagPoll.stop(); + } + }); +}; + +export const stopPipelinePolling = () => { + eTagPoll.stop(); +}; + +export const restartPipelinePolling = () => { + eTagPoll.restart(); +}; diff --git a/app/assets/javascripts/ide/stores/getters.js b/app/assets/javascripts/ide/stores/getters.js index a77cdbc13c8..b239a605371 100644 --- a/app/assets/javascripts/ide/stores/getters.js +++ b/app/assets/javascripts/ide/stores/getters.js @@ -1,3 +1,6 @@ +import { getChangesCountForFiles, filePathMatches } from './utils'; +import { activityBarViews } from '../constants'; + export const activeFile = state => state.openFiles.find(file => file.active) || null; export const addedFiles = state => state.changedFiles.filter(f => f.tempFile); @@ -28,10 +31,61 @@ export const currentMergeRequest = state => { return null; }; -// eslint-disable-next-line no-confusing-arrow -export const currentIcon = state => - state.rightPanelCollapsed ? 'angle-double-left' : 'angle-double-right'; +export const currentProject = state => state.projects[state.currentProjectId]; + +export const currentTree = state => + state.trees[`${state.currentProjectId}/${state.currentBranchId}`]; -export const hasChanges = state => !!state.changedFiles.length; +export const hasChanges = state => !!state.changedFiles.length || !!state.stagedFiles.length; export const hasMergeRequest = state => !!state.currentMergeRequestId; + +export const allBlobs = state => + Object.keys(state.entries) + .reduce((acc, key) => { + const entry = state.entries[key]; + + if (entry.type === 'blob') { + acc.push(entry); + } + + return acc; + }, []) + .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/index.js b/app/assets/javascripts/ide/stores/index.js index 7c82ce7976b..699710055e3 100644 --- a/app/assets/javascripts/ide/stores/index.js +++ b/app/assets/javascripts/ide/stores/index.js @@ -5,6 +5,7 @@ import * as actions from './actions'; import * as getters from './getters'; import mutations from './mutations'; import commitModule from './modules/commit'; +import pipelines from './modules/pipelines'; Vue.use(Vuex); @@ -15,5 +16,6 @@ export default new Vuex.Store({ getters, modules: { commit: commitModule, + pipelines, }, }); diff --git a/app/assets/javascripts/ide/stores/modules/commit/actions.js b/app/assets/javascripts/ide/stores/modules/commit/actions.js index 367c45f7e2d..cd25c3060f2 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 = { @@ -98,23 +99,14 @@ export const updateFilesAfterCommit = ( { root: true }, ); - rootState.changedFiles.forEach(entry => { - commit( - rootTypes.SET_LAST_COMMIT_DATA, - { - entry, - lastCommit, - }, - { root: true }, - ); - - eventHub.$emit(`editor.update.model.content.${entry.path}`, entry.content); + rootState.stagedFiles.forEach(file => { + const changedFile = rootState.changedFiles.find(f => f.path === file.path); commit( - rootTypes.SET_FILE_RAW_DATA, + rootTypes.UPDATE_FILE_AFTER_COMMIT, { - file: entry, - raw: entry.content, + file, + lastCommit, }, { root: true }, ); @@ -122,23 +114,22 @@ export const updateFilesAfterCommit = ( commit( rootTypes.TOGGLE_FILE_CHANGED, { - file: entry, + file, changed: false, }, { root: true }, ); - }); - commit(rootTypes.REMOVE_ALL_CHANGES_FILES, null, { root: true }); + dispatch('updateTempFlagForEntry', { file, tempFile: false }, { root: true }); - if (state.commitAction === consts.COMMIT_TO_NEW_BRANCH) { - router.push( - `/project/${rootState.currentProjectId}/blob/${branch}/${rootGetters.activeFile.path}`, - ); - } + eventHub.$emit(`editor.update.model.content.${file.key}`, { + content: file.content, + changed: !!changedFile, + }); + }); }; -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'); @@ -184,8 +175,52 @@ export const commitChanges = ({ commit, state, getters, dispatch, rootState }) = { root: true }, ); } + + commit(rootTypes.CLEAR_STAGED_CHANGES, null, { root: true }); + + setTimeout(() => { + commit(rootTypes.SET_LAST_COMMIT_MSG, '', { root: true }); + }, 5000); }) - .then(() => dispatch('updateCommitAction', consts.COMMIT_TO_CURRENT_BRANCH)); + .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( + 'refreshLastCommitData', + { + projectId: rootState.currentProjectId, + branchId: rootState.currentBranchId, + }, + { root: true }, + ), + ); }) .catch(err => { let errMsg = __('Error committing changes. Please try again.'); @@ -198,3 +233,6 @@ export const commitChanges = ({ commit, state, getters, dispatch, rootState }) = commit(types.UPDATE_LOADING, false); }); }; + +// prevent babel-plugin-rewire from generating an invalid default during karma tests +export default () => {}; diff --git a/app/assets/javascripts/ide/stores/modules/commit/getters.js b/app/assets/javascripts/ide/stores/modules/commit/getters.js index f7cdd6adb0c..d01060201f2 100644 --- a/app/assets/javascripts/ide/stores/modules/commit/getters.js +++ b/app/assets/javascripts/ide/stores/modules/commit/getters.js @@ -1,12 +1,17 @@ import * as consts from './constants'; -export const discardDraftButtonDisabled = state => state.commitMessage === '' || state.submitCommitLoading; +const BRANCH_SUFFIX_COUNT = 5; + +export const discardDraftButtonDisabled = state => + state.commitMessage === '' || state.submitCommitLoading; export const commitButtonDisabled = (state, getters, rootState) => - getters.discardDraftButtonDisabled || !rootState.changedFiles.length; + getters.discardDraftButtonDisabled || !rootState.stagedFiles.length; export const newBranchName = (state, _, rootState) => - `${gon.current_username}-${rootState.currentBranchId}-patch-${`${new Date().getTime()}`.substr(-5)}`; + `${gon.current_username}-${rootState.currentBranchId}-patch-${`${new Date().getTime()}`.substr( + -BRANCH_SUFFIX_COUNT, + )}`; export const branchName = (state, getters, rootState) => { if ( @@ -22,3 +27,6 @@ export const branchName = (state, getters, rootState) => { return rootState.currentBranchId; }; + +// prevent babel-plugin-rewire from generating an invalid default during karma tests +export default () => {}; diff --git a/app/assets/javascripts/ide/stores/modules/pipelines/actions.js b/app/assets/javascripts/ide/stores/modules/pipelines/actions.js new file mode 100644 index 00000000000..07f7b201f2e --- /dev/null +++ b/app/assets/javascripts/ide/stores/modules/pipelines/actions.js @@ -0,0 +1,49 @@ +import { __ } from '../../../../locale'; +import Api from '../../../../api'; +import flash from '../../../../flash'; +import * as types from './mutation_types'; + +export const requestLatestPipeline = ({ commit }) => commit(types.REQUEST_LATEST_PIPELINE); +export const receiveLatestPipelineError = ({ commit }) => { + flash(__('There was an error loading latest pipeline')); + commit(types.RECEIVE_LASTEST_PIPELINE_ERROR); +}; +export const receiveLatestPipelineSuccess = ({ commit }, pipeline) => + commit(types.RECEIVE_LASTEST_PIPELINE_SUCCESS, pipeline); + +export const fetchLatestPipeline = ({ dispatch, rootState }, sha) => { + dispatch('requestLatestPipeline'); + + return Api.pipelines(rootState.currentProjectId, { sha, per_page: '1' }) + .then(({ data }) => { + dispatch('receiveLatestPipelineSuccess', data.pop()); + }) + .catch(() => dispatch('receiveLatestPipelineError')); +}; + +export const requestJobs = ({ commit }) => commit(types.REQUEST_JOBS); +export const receiveJobsError = ({ commit }) => { + flash(__('There was an error loading jobs')); + commit(types.RECEIVE_JOBS_ERROR); +}; +export const receiveJobsSuccess = ({ commit }, data) => commit(types.RECEIVE_JOBS_SUCCESS, data); + +export const fetchJobs = ({ dispatch, state, rootState }, page = '1') => { + dispatch('requestJobs'); + + Api.pipelineJobs(rootState.currentProjectId, state.latestPipeline.id, { + page, + }) + .then(({ data, headers }) => { + const nextPage = headers && headers['x-next-page']; + + dispatch('receiveJobsSuccess', data); + + if (nextPage) { + dispatch('fetchJobs', nextPage); + } + }) + .catch(() => dispatch('receiveJobsError')); +}; + +export default () => {}; diff --git a/app/assets/javascripts/ide/stores/modules/pipelines/getters.js b/app/assets/javascripts/ide/stores/modules/pipelines/getters.js new file mode 100644 index 00000000000..d6c91f5b64d --- /dev/null +++ b/app/assets/javascripts/ide/stores/modules/pipelines/getters.js @@ -0,0 +1,7 @@ +export const hasLatestPipeline = state => !state.isLoadingPipeline && !!state.latestPipeline; + +export const failedJobs = state => + state.stages.reduce( + (acc, stage) => acc.concat(stage.jobs.filter(job => job.status === 'failed')), + [], + ); diff --git a/app/assets/javascripts/ide/stores/modules/pipelines/index.js b/app/assets/javascripts/ide/stores/modules/pipelines/index.js new file mode 100644 index 00000000000..b44c3141b81 --- /dev/null +++ b/app/assets/javascripts/ide/stores/modules/pipelines/index.js @@ -0,0 +1,12 @@ +import state from './state'; +import * as actions from './actions'; +import mutations from './mutations'; +import * as getters from './getters'; + +export default { + namespaced: true, + state: state(), + actions, + mutations, + getters, +}; diff --git a/app/assets/javascripts/ide/stores/modules/pipelines/mutation_types.js b/app/assets/javascripts/ide/stores/modules/pipelines/mutation_types.js new file mode 100644 index 00000000000..6b5701670a6 --- /dev/null +++ b/app/assets/javascripts/ide/stores/modules/pipelines/mutation_types.js @@ -0,0 +1,7 @@ +export const REQUEST_LATEST_PIPELINE = 'REQUEST_LATEST_PIPELINE'; +export const RECEIVE_LASTEST_PIPELINE_ERROR = 'RECEIVE_LASTEST_PIPELINE_ERROR'; +export const RECEIVE_LASTEST_PIPELINE_SUCCESS = 'RECEIVE_LASTEST_PIPELINE_SUCCESS'; + +export const REQUEST_JOBS = 'REQUEST_JOBS'; +export const RECEIVE_JOBS_ERROR = 'RECEIVE_JOBS_ERROR'; +export const RECEIVE_JOBS_SUCCESS = 'RECEIVE_JOBS_SUCCESS'; diff --git a/app/assets/javascripts/ide/stores/modules/pipelines/mutations.js b/app/assets/javascripts/ide/stores/modules/pipelines/mutations.js new file mode 100644 index 00000000000..2b16e57b386 --- /dev/null +++ b/app/assets/javascripts/ide/stores/modules/pipelines/mutations.js @@ -0,0 +1,53 @@ +/* eslint-disable no-param-reassign */ +import * as types from './mutation_types'; + +export default { + [types.REQUEST_LATEST_PIPELINE](state) { + state.isLoadingPipeline = true; + }, + [types.RECEIVE_LASTEST_PIPELINE_ERROR](state) { + state.isLoadingPipeline = false; + }, + [types.RECEIVE_LASTEST_PIPELINE_SUCCESS](state, pipeline) { + state.isLoadingPipeline = false; + + if (pipeline) { + state.latestPipeline = { + id: pipeline.id, + status: pipeline.status, + }; + } + }, + [types.REQUEST_JOBS](state) { + state.isLoadingJobs = true; + }, + [types.RECEIVE_JOBS_ERROR](state) { + state.isLoadingJobs = false; + }, + [types.RECEIVE_JOBS_SUCCESS](state, jobs) { + state.isLoadingJobs = false; + + state.stages = jobs.reduce((acc, job) => { + let stage = acc.find(s => s.title === job.stage); + + if (!stage) { + stage = { + title: job.stage, + jobs: [], + }; + + acc.push(stage); + } + + stage.jobs = stage.jobs.concat({ + id: job.id, + name: job.name, + status: job.status, + stage: job.stage, + duration: job.duration, + }); + + return acc; + }, state.stages); + }, +}; diff --git a/app/assets/javascripts/ide/stores/modules/pipelines/state.js b/app/assets/javascripts/ide/stores/modules/pipelines/state.js new file mode 100644 index 00000000000..6f22542aaea --- /dev/null +++ b/app/assets/javascripts/ide/stores/modules/pipelines/state.js @@ -0,0 +1,6 @@ +export default () => ({ + isLoadingPipeline: false, + isLoadingJobs: false, + latestPipeline: null, + stages: [], +}); diff --git a/app/assets/javascripts/ide/stores/mutation_types.js b/app/assets/javascripts/ide/stores/mutation_types.js index e3f504e5ab0..0a3f8d031c4 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,8 +20,10 @@ 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'; +export const SET_LAST_COMMIT_PIPELINE = 'SET_LAST_COMMIT_PIPELINE'; // Tree mutation types export const SET_DIRECTORY_DATA = 'SET_DIRECTORY_DATA'; @@ -51,5 +54,15 @@ export const SET_FILE_MERGE_REQUEST_CHANGE = 'SET_FILE_MERGE_REQUEST_CHANGE'; export const UPDATE_VIEWER = 'UPDATE_VIEWER'; export const UPDATE_DELAY_VIEWER_CHANGE = 'UPDATE_DELAY_VIEWER_CHANGE'; +export const CLEAR_STAGED_CHANGES = 'CLEAR_STAGED_CHANGES'; +export const STAGE_CHANGE = 'STAGE_CHANGE'; +export const UNSTAGE_CHANGE = 'UNSTAGE_CHANGE'; + +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 5e5eb831662..a257e2ef025 100644 --- a/app/assets/javascripts/ide/stores/mutations.js +++ b/app/assets/javascripts/ide/stores/mutations.js @@ -4,6 +4,7 @@ import mergeRequestMutation from './mutations/merge_request'; import fileMutations from './mutations/file'; import treeMutations from './mutations/tree'; import branchMutations from './mutations/branch'; +import { sortTree } from './utils'; export default { [types.SET_INITIAL_DATA](state, data) { @@ -49,6 +50,11 @@ export default { lastCommitMsg, }); }, + [types.CLEAR_STAGED_CHANGES](state) { + Object.assign(state, { + stagedFiles: [], + }); + }, [types.SET_ENTRIES](state, entries) { Object.assign(state, { entries, @@ -68,7 +74,7 @@ export default { f => foundEntry.tree.find(e => e.path === f.path) === undefined, ); Object.assign(foundEntry, { - tree: foundEntry.tree.concat(tree), + tree: sortTree(foundEntry.tree.concat(tree)), }); } @@ -81,10 +87,16 @@ export default { if (!foundEntry) { Object.assign(state.trees[`${projectId}/${branchId}`], { - tree: state.trees[`${projectId}/${branchId}`].tree.concat(data.treeList), + tree: sortTree(state.trees[`${projectId}/${branchId}`].tree.concat(data.treeList)), }); } }, + [types.UPDATE_TEMP_FLAG](state, { path, tempFile }) { + Object.assign(state.entries[path], { + tempFile, + changed: tempFile, + }); + }, [types.UPDATE_VIEWER](state, viewer) { Object.assign(state, { viewer, @@ -95,6 +107,47 @@ 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, + }); + }, + [types.UPDATE_FILE_AFTER_COMMIT](state, { file, lastCommit }) { + const changedFile = state.changedFiles.find(f => f.path === file.path); + + Object.assign(state.entries[file.path], { + raw: file.content, + changed: !!changedFile, + staged: false, + lastCommit: Object.assign(state.entries[file.path].lastCommit, { + id: lastCommit.commit.id, + url: lastCommit.commit_path, + message: lastCommit.commit.message, + author: lastCommit.commit.author_name, + updatedAt: lastCommit.commit.authored_date, + }), + }); + }, + [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..f17ec4da308 100644 --- a/app/assets/javascripts/ide/stores/mutations/branch.js +++ b/app/assets/javascripts/ide/stores/mutations/branch.js @@ -14,6 +14,10 @@ export default { treeId: `${projectPath}/${branchName}`, active: true, workingReference: '', + commit: { + ...branch.commit, + pipeline: {}, + }, }, }, }); @@ -23,4 +27,14 @@ export default { workingReference: reference, }); }, + [types.SET_BRANCH_COMMIT](state, { projectId, branchId, commit }) { + Object.assign(state.projects[projectId].branches[branchId], { + commit, + }); + }, + [types.SET_LAST_COMMIT_PIPELINE](state, { projectId, branchId, pipeline }) { + Object.assign(state.projects[projectId].branches[branchId].commit, { + pipeline, + }); + }, }; diff --git a/app/assets/javascripts/ide/stores/mutations/file.js b/app/assets/javascripts/ide/stores/mutations/file.js index eeb14b5490c..13f123b6630 100644 --- a/app/assets/javascripts/ide/stores/mutations/file.js +++ b/app/assets/javascripts/ide/stores/mutations/file.js @@ -1,9 +1,11 @@ +/* eslint-disable no-param-reassign */ import * as types from '../mutation_types'; export default { [types.SET_FILE_ACTIVE](state, { path, active }) { Object.assign(state.entries[path], { active, + lastOpenedAt: new Date().getTime(), }); if (active && !state.entries[path].pending) { @@ -57,7 +59,9 @@ export default { }); }, [types.UPDATE_FILE_CONTENT](state, { path, content }) { - const changed = content !== state.entries[path].raw; + const stagedFile = state.stagedFiles.find(f => f.path === path); + const rawContent = stagedFile ? stagedFile.content : state.entries[path].raw; + const changed = content !== rawContent; Object.assign(state.entries[path], { content, @@ -91,8 +95,10 @@ export default { }); }, [types.DISCARD_FILE_CHANGES](state, path) { + const stagedFile = state.stagedFiles.find(f => f.path === path); + Object.assign(state.entries[path], { - content: state.entries[path].raw, + content: stagedFile ? stagedFile.content : state.entries[path].raw, changed: false, }); }, @@ -106,38 +112,82 @@ export default { changedFiles: state.changedFiles.filter(f => f.path !== path), }); }, + [types.STAGE_CHANGE](state, path) { + const stagedFile = state.stagedFiles.find(f => f.path === path); + + Object.assign(state, { + changedFiles: state.changedFiles.filter(f => f.path !== path), + entries: Object.assign(state.entries, { + [path]: Object.assign(state.entries[path], { + staged: true, + changed: false, + }), + }), + }); + + if (stagedFile) { + Object.assign(stagedFile, { + ...state.entries[path], + }); + } else { + Object.assign(state, { + stagedFiles: state.stagedFiles.concat({ + ...state.entries[path], + }), + }); + } + }, + [types.UNSTAGE_CHANGE](state, path) { + const changedFile = state.changedFiles.find(f => f.path === path); + const stagedFile = state.stagedFiles.find(f => f.path === path); + + if (!changedFile && stagedFile) { + Object.assign(state.entries[path], { + ...stagedFile, + key: state.entries[path].key, + active: state.entries[path].active, + opened: state.entries[path].opened, + changed: true, + }); + + Object.assign(state, { + changedFiles: state.changedFiles.concat(state.entries[path]), + }); + } + + Object.assign(state, { + stagedFiles: state.stagedFiles.filter(f => f.path !== path), + entries: Object.assign(state.entries, { + [path]: Object.assign(state.entries[path], { + staged: false, + }), + }), + }); + }, [types.TOGGLE_FILE_CHANGED](state, { file, changed }) { Object.assign(state.entries[file.path], { changed, }); }, [types.ADD_PENDING_TAB](state, { file, keyPrefix = 'pending' }) { - const pendingTab = state.openFiles.find(f => f.path === file.path && f.pending); - let openFiles = state.openFiles.map(f => - Object.assign(f, { active: f.path === file.path, opened: false }), + 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, + }), ); - - 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, - active: true, - pending: true, - opened: true, - key: `${keyPrefix}-${f.key}`, - }); - } - - return acc.concat(f); - }, []); - } - - Object.assign(state, { openFiles }); + 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 e5cc8814000..e7411f16a4f 100644 --- a/app/assets/javascripts/ide/stores/state.js +++ b/app/assets/javascripts/ide/stores/state.js @@ -1,8 +1,11 @@ +import { activityBarViews, viewerTypes } from '../constants'; + export default () => ({ currentProjectId: '', currentBranchId: '', currentMergeRequestId: '', changedFiles: [], + stagedFiles: [], endpoints: {}, lastCommitMsg: '', lastCommitPath: '', @@ -15,6 +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 05a019de54f..e0b9766fbee 100644 --- a/app/assets/javascripts/ide/stores/utils.js +++ b/app/assets/javascripts/ide/stores/utils.js @@ -15,6 +15,7 @@ export const dataStructure = () => ({ opened: false, active: false, changed: false, + staged: false, lastCommitPath: '', lastCommit: { id: '', @@ -41,6 +42,9 @@ export const dataStructure = () => ({ viewMode: 'edit', previewMode: null, size: 0, + parentPath: null, + lastOpenedAt: 0, + mrChange: null, }); export const decorateData = entity => { @@ -63,6 +67,7 @@ export const decorateData = entity => { previewMode, file_lock, html, + parentPath = '', } = entity; return { @@ -86,6 +91,7 @@ export const decorateData = entity => { previewMode, file_lock, html, + parentPath, }; }; @@ -101,7 +107,7 @@ export const setPageTitle = title => { export const createCommitPayload = (branch, newBranch, state, rootState) => ({ branch, commit_message: state.commitMessage, - actions: rootState.changedFiles.map(f => ({ + actions: rootState.stagedFiles.map(f => ({ action: f.tempFile ? 'create' : 'update', file_path: f.path, content: f.content, @@ -119,8 +125,8 @@ const sortTreesByTypeAndName = (a, b) => { } else if (a.type === 'blob' && b.type === 'tree') { return 1; } - if (a.name.toLowerCase() < b.name.toLowerCase()) return -1; - if (a.name.toLowerCase() > b.name.toLowerCase()) return 1; + if (a.name < b.name) return -1; + if (a.name > b.name) return 1; return 0; }; @@ -132,3 +138,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/ide/stores/workers/files_decorator_worker.js b/app/assets/javascripts/ide/stores/workers/files_decorator_worker.js index a1673276900..0a1c253c637 100644 --- a/app/assets/javascripts/ide/stores/workers/files_decorator_worker.js +++ b/app/assets/javascripts/ide/stores/workers/files_decorator_worker.js @@ -6,6 +6,7 @@ self.addEventListener('message', e => { const treeList = []; let file; + let parentPath; const entries = data.reduce((acc, path) => { const pathSplit = path.split('/'); const blobName = pathSplit.pop().trim(); @@ -17,18 +18,21 @@ self.addEventListener('message', e => { const foundEntry = acc[folderPath]; if (!foundEntry) { + parentPath = parentFolder ? parentFolder.path : null; + const tree = decorateData({ projectId, branchId, id: folderPath, name: folderName, path: folderPath, - url: `/${projectId}/tree/${branchId}/${folderPath}/`, + url: `/${projectId}/tree/${branchId}/-/${folderPath}/`, type: 'tree', parentTreeUrl: parentFolder ? parentFolder.url : `/${projectId}/tree/${branchId}/`, tempFile, changed: tempFile, opened: tempFile, + parentPath, }); Object.assign(acc, { @@ -52,13 +56,15 @@ self.addEventListener('message', e => { if (blobName !== '') { const fileFolder = acc[pathSplit.join('/')]; + parentPath = fileFolder ? fileFolder.path : null; + file = decorateData({ projectId, branchId, id: path, name: blobName, path, - url: `/${projectId}/blob/${branchId}/${path}`, + url: `/${projectId}/blob/${branchId}/-/${path}`, type: 'blob', parentTreeUrl: fileFolder ? fileFolder.url : `/${projectId}/blob/${branchId}`, tempFile, @@ -66,6 +72,7 @@ self.addEventListener('message', e => { content, base64, previewMode: viewerInformationForPath(blobName), + parentPath, }); Object.assign(acc, { @@ -86,5 +93,6 @@ self.addEventListener('message', e => { entries, treeList: sortTree(treeList), file, + parentPath, }); }); diff --git a/app/assets/javascripts/issuable_context.js b/app/assets/javascripts/issuable_context.js index 7470d634b99..f3d722409b0 100644 --- a/app/assets/javascripts/issuable_context.js +++ b/app/assets/javascripts/issuable_context.js @@ -30,10 +30,10 @@ export default class IssuableContext { const $selectbox = $block.find('.selectbox'); if ($selectbox.is(':visible')) { $selectbox.hide(); - $block.find('.value').show(); + $block.find('.value:not(.dont-hide)').show(); } else { $selectbox.show(); - $block.find('.value').hide(); + $block.find('.value:not(.dont-hide)').hide(); } if ($selectbox.is(':visible')) { 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/issue_show/components/edit_actions.vue b/app/assets/javascripts/issue_show/components/edit_actions.vue index a539506bce2..7ef5e679881 100644 --- a/app/assets/javascripts/issue_show/components/edit_actions.vue +++ b/app/assets/javascripts/issue_show/components/edit_actions.vue @@ -51,7 +51,7 @@ <template> <div class="prepend-top-default append-bottom-default clearfix"> <button - class="btn btn-save pull-left" + class="btn btn-save float-left" :class="{ disabled: formState.updateLoading || !isSubmitEnabled }" type="submit" :disabled="formState.updateLoading || !isSubmitEnabled" @@ -64,14 +64,14 @@ </i> </button> <button - class="btn btn-default pull-right" + class="btn btn-default float-right" type="button" @click="closeForm"> Cancel </button> <button v-if="shouldShowDeleteButton" - class="btn btn-danger pull-right append-right-default" + class="btn btn-danger float-right append-right-default" :class="{ disabled: deleteLoading }" type="button" :disabled="deleteLoading" diff --git a/app/assets/javascripts/issue_show/components/form.vue b/app/assets/javascripts/issue_show/components/form.vue index 779705e19ac..ab8bd34762f 100644 --- a/app/assets/javascripts/issue_show/components/form.vue +++ b/app/assets/javascripts/issue_show/components/form.vue @@ -84,7 +84,7 @@ <div :class="{ 'col-sm-8 col-lg-9': hasIssuableTemplates, - 'col-xs-12': !hasIssuableTemplates, + 'col-12': !hasIssuableTemplates, }" > <title-field diff --git a/app/assets/javascripts/job.js b/app/assets/javascripts/job.js index ace45e9dd29..c67bd7fb0c6 100644 --- a/app/assets/javascripts/job.js +++ b/app/assets/javascripts/job.js @@ -1,5 +1,6 @@ import $ from 'jquery'; import _ from 'underscore'; +import StickyFill from 'stickyfilljs'; import axios from './lib/utils/axios_utils'; import { visitUrl } from './lib/utils/url_utility'; import bp from './breakpoints'; @@ -82,17 +83,11 @@ export default class Job { /** If the browser does not support position sticky, it returns the position as static. If the browser does support sticky, then we allow the browser to handle it, if not - then we default back to Bootstraps affix + then we use a polyfill **/ if (this.$topBar.css('position') !== 'static') return; - const offsetTop = this.$buildTrace.offset().top; - - this.$topBar.affix({ - offset: { - top: offsetTop, - }, - }); + StickyFill.add(this.$topBar); } // eslint-disable-next-line class-methods-use-this diff --git a/app/assets/javascripts/jobs/components/header.vue b/app/assets/javascripts/jobs/components/header.vue index 357bc9aab17..c1044f4cd42 100644 --- a/app/assets/javascripts/jobs/components/header.vue +++ b/app/assets/javascripts/jobs/components/header.vue @@ -1,82 +1,94 @@ <script> - import ciHeader from '../../vue_shared/components/header_ci_component.vue'; - import loadingIcon from '../../vue_shared/components/loading_icon.vue'; +import ciHeader from '../../vue_shared/components/header_ci_component.vue'; +import loadingIcon from '../../vue_shared/components/loading_icon.vue'; +import callout from '../../vue_shared/components/callout.vue'; - export default { - name: 'JobHeaderSection', - components: { - ciHeader, - loadingIcon, +export default { + name: 'JobHeaderSection', + components: { + ciHeader, + loadingIcon, + callout, + }, + props: { + job: { + type: Object, + required: true, }, - props: { - job: { - type: Object, - required: true, - }, - isLoading: { - type: Boolean, - required: true, - }, + isLoading: { + type: Boolean, + required: true, }, - data() { - return { - actions: this.getActions(), - }; + }, + data() { + return { + actions: this.getActions(), + }; + }, + computed: { + status() { + return this.job && this.job.status; }, - computed: { - status() { - return this.job && this.job.status; - }, - shouldRenderContent() { - return !this.isLoading && Object.keys(this.job).length; - }, - /** - * When job has not started the key will be `false` - * When job started the key will be a string with a date. - */ - jobStarted() { - return !this.job.started === false; - }, + shouldRenderContent() { + return !this.isLoading && Object.keys(this.job).length; }, - watch: { - job() { - this.actions = this.getActions(); - }, + shouldRenderReason() { + return !!(this.job.status && this.job.callout_message); }, - methods: { - getActions() { - const actions = []; + /** + * When job has not started the key will be `false` + * When job started the key will be a string with a date. + */ + jobStarted() { + return !this.job.started === false; + }, + }, + watch: { + job() { + this.actions = this.getActions(); + }, + }, + methods: { + getActions() { + const actions = []; - if (this.job.new_issue_path) { - actions.push({ - label: 'New issue', - path: this.job.new_issue_path, - cssClass: 'js-new-issue btn btn-new btn-inverted visible-md-block visible-lg-block', - type: 'link', - }); - } - return actions; - }, + if (this.job.new_issue_path) { + actions.push({ + label: 'New issue', + path: this.job.new_issue_path, + cssClass: 'js-new-issue btn btn-new btn-inverted d-none d-md-block d-lg-block d-xl-block', + type: 'link', + }); + } + return actions; }, - }; + }, +}; </script> <template> - <div class="js-build-header build-header top-area"> - <ci-header - v-if="shouldRenderContent" - :status="status" - item-name="Job" - :item-id="job.id" - :time="job.created_at" - :user="job.user" - :actions="actions" - :has-sidebar-button="true" - :should-render-triggered-label="jobStarted" - /> - <loading-icon - v-if="isLoading" - size="2" - class="prepend-top-default append-bottom-default" + <header> + <div class="js-build-header build-header top-area"> + <ci-header + v-if="shouldRenderContent" + :status="status" + item-name="Job" + :item-id="job.id" + :time="job.created_at" + :user="job.user" + :actions="actions" + :has-sidebar-button="true" + :should-render-triggered-label="jobStarted" + /> + <loading-icon + v-if="isLoading" + size="2" + class="prepend-top-default append-bottom-default" + /> + </div> + + <callout + v-if="shouldRenderReason" + :message="job.callout_message" /> - </div> + </header> </template> diff --git a/app/assets/javascripts/jobs/components/sidebar_detail_row.vue b/app/assets/javascripts/jobs/components/sidebar_detail_row.vue index dfe87d89a39..83560a8ff0e 100644 --- a/app/assets/javascripts/jobs/components/sidebar_detail_row.vue +++ b/app/assets/javascripts/jobs/components/sidebar_detail_row.vue @@ -39,7 +39,7 @@ <span v-if="hasHelpURL" - class="help-button pull-right" + class="help-button float-right" > <a :href="helpUrl" diff --git a/app/assets/javascripts/jobs/components/sidebar_details_block.vue b/app/assets/javascripts/jobs/components/sidebar_details_block.vue index af47056d98f..82fec7e936b 100644 --- a/app/assets/javascripts/jobs/components/sidebar_details_block.vue +++ b/app/assets/javascripts/jobs/components/sidebar_details_block.vue @@ -1,80 +1,119 @@ <script> - import detailRow from './sidebar_detail_row.vue'; - import loadingIcon from '../../vue_shared/components/loading_icon.vue'; - import timeagoMixin from '../../vue_shared/mixins/timeago'; - import { timeIntervalInWords } from '../../lib/utils/datetime_utility'; +import detailRow from './sidebar_detail_row.vue'; +import loadingIcon from '../../vue_shared/components/loading_icon.vue'; +import timeagoMixin from '../../vue_shared/mixins/timeago'; +import { timeIntervalInWords } from '../../lib/utils/datetime_utility'; - export default { - name: 'SidebarDetailsBlock', - components: { - detailRow, - loadingIcon, +export default { + name: 'SidebarDetailsBlock', + components: { + detailRow, + loadingIcon, + }, + mixins: [timeagoMixin], + props: { + job: { + type: Object, + required: true, }, - mixins: [ - timeagoMixin, - ], - props: { - job: { - type: Object, - required: true, - }, - isLoading: { - type: Boolean, - required: true, - }, - runnerHelpUrl: { - type: String, - required: false, - default: '', - }, + isLoading: { + type: Boolean, + required: true, }, - computed: { - shouldRenderContent() { - return !this.isLoading && Object.keys(this.job).length > 0; - }, - coverage() { - return `${this.job.coverage}%`; - }, - duration() { - return timeIntervalInWords(this.job.duration); - }, - queued() { - return timeIntervalInWords(this.job.queued); - }, - runnerId() { - return `#${this.job.runner.id}`; - }, - hasTimeout() { - return this.job.metadata != null && this.job.metadata.timeout_human_readable !== null; - }, - timeout() { - if (this.job.metadata == null) { - return ''; - } + canUserRetry: { + type: Boolean, + required: false, + default: false, + }, + runnerHelpUrl: { + type: String, + required: false, + default: '', + }, + }, + computed: { + shouldRenderContent() { + return !this.isLoading && Object.keys(this.job).length > 0; + }, + coverage() { + return `${this.job.coverage}%`; + }, + duration() { + return timeIntervalInWords(this.job.duration); + }, + queued() { + return timeIntervalInWords(this.job.queued); + }, + runnerId() { + return `${this.job.runner.description} (#${this.job.runner.id})`; + }, + retryButtonClass() { + let className = 'js-retry-button pull-right btn btn-retry d-none d-md-block d-lg-block d-xl-block'; + className += + this.job.status && this.job.recoverable + ? ' btn-primary' + : ' btn-inverted-secondary'; + return className; + }, + hasTimeout() { + return this.job.metadata != null && this.job.metadata.timeout_human_readable !== null; + }, + timeout() { + if (this.job.metadata == null) { + return ''; + } - let t = this.job.metadata.timeout_human_readable; - if (this.job.metadata.timeout_source !== '') { - t += ` (from ${this.job.metadata.timeout_source})`; - } + let t = this.job.metadata.timeout_human_readable; + if (this.job.metadata.timeout_source !== '') { + t += ` (from ${this.job.metadata.timeout_source})`; + } - return t; - }, - renderBlock() { - return this.job.merge_request || - this.job.duration || - this.job.finished_data || - this.job.erased_at || - this.job.queued || - this.job.runner || - this.job.coverage || - this.job.tags.length || - this.job.cancel_path; - }, + return t; }, - }; + renderBlock() { + return ( + this.job.merge_request || + this.job.duration || + this.job.finished_data || + this.job.erased_at || + this.job.queued || + this.job.runner || + this.job.coverage || + this.job.tags.length || + this.job.cancel_path + ); + }, + }, +}; </script> <template> <div> + <div class="block"> + <strong class="inline prepend-top-8"> + {{ job.name }} + </strong> + <a + v-if="canUserRetry" + :class="retryButtonClass" + :href="job.retry_path" + data-method="post" + rel="nofollow" + > + {{ __('Retry') }} + </a> + <button + type="button" + :aria-label="__('Toggle Sidebar')" + class="btn btn-blank gutter-toggle pull-right + d-block d-sm-block d-md-none js-sidebar-build-toggle" + > + <i + aria-hidden="true" + data-hidden="true" + class="fa fa-angle-double-right" + ></i> + </button> + </div> <template v-if="shouldRenderContent"> <div class="block retry-link" @@ -85,16 +124,16 @@ class="js-new-issue btn btn-new btn-inverted" :href="job.new_issue_path" > - New issue + {{ __('New issue') }} </a> <a - v-if="job.retry_path" + v-if="canUserRetry" class="js-retry-job btn btn-inverted-secondary" :href="job.retry_path" data-method="post" rel="nofollow" > - Retry + {{ __('Retry') }} </a> </div> <div :class="{block : renderBlock }"> @@ -103,7 +142,7 @@ v-if="job.merge_request" > <span class="build-light-text"> - Merge Request: + {{ __('Merge Request:') }} </span> <a :href="job.merge_request.path"> !{{ job.merge_request.iid }} @@ -158,7 +197,7 @@ v-if="job.tags.length" > <span class="build-light-text"> - Tags: + {{ __('Tags:') }} </span> <span v-for="(tag, i) in job.tags" @@ -178,7 +217,7 @@ data-method="post" rel="nofollow" > - Cancel + {{ __('Cancel') }} </a> </div> </div> diff --git a/app/assets/javascripts/jobs/job_details_bundle.js b/app/assets/javascripts/jobs/job_details_bundle.js index 656676ead91..f2939ad4dbe 100644 --- a/app/assets/javascripts/jobs/job_details_bundle.js +++ b/app/assets/javascripts/jobs/job_details_bundle.js @@ -35,9 +35,11 @@ export default () => { }); // Sidebar information block + const detailsBlockElement = document.getElementById('js-details-block-vue'); + const detailsBlockDataset = detailsBlockElement.dataset; // eslint-disable-next-line new Vue({ - el: '#js-details-block-vue', + el: detailsBlockElement, components: { detailsBlock, }, @@ -50,6 +52,7 @@ export default () => { return createElement('details-block', { props: { isLoading: this.mediator.state.isLoading, + canUserRetry: !!('canUserRetry' in detailsBlockDataset), job: this.mediator.store.state.job, runnerHelpUrl: dataset.runnerHelpUrl, }, diff --git a/app/assets/javascripts/label_manager.js b/app/assets/javascripts/label_manager.js index 8013df53f25..a8a314abd6b 100644 --- a/app/assets/javascripts/label_manager.js +++ b/app/assets/javascripts/label_manager.js @@ -36,7 +36,7 @@ export default class LabelManager { const $label = $(`#${$btn.data('domId')}`); const action = $btn.parents('.js-prioritized-labels').length ? 'remove' : 'add'; const $tooltip = $(`#${$btn.find('.has-tooltip:visible').attr('aria-describedby')}`); - $tooltip.tooltip('destroy'); + $tooltip.tooltip('dispose'); _this.toggleLabelPriority($label, action); _this.toggleEmptyState($label, $btn, action); } diff --git a/app/assets/javascripts/labels_select.js b/app/assets/javascripts/labels_select.js index d0050abb8e9..eafdaf4a672 100644 --- a/app/assets/javascripts/labels_select.js +++ b/app/assets/javascripts/labels_select.js @@ -83,7 +83,7 @@ export default class LabelsSelect { $dropdown.trigger('loading.gl.dropdown'); axios.put(issueUpdateURL, data) .then(({ data }) => { - var labelCount, template, labelTooltipTitle, labelTitles; + var labelCount, template, labelTooltipTitle, labelTitles, formattedLabels; $loading.fadeOut(); $dropdown.trigger('loaded.gl.dropdown'); $selectbox.hide(); @@ -115,13 +115,12 @@ export default class LabelsSelect { labelTooltipTitle = labelTitles.join(', '); } else { - labelTooltipTitle = ''; - $sidebarLabelTooltip.tooltip('destroy'); + labelTooltipTitle = __('Labels'); } $sidebarLabelTooltip .attr('title', labelTooltipTitle) - .tooltip('fixTitle'); + .tooltip('_fixTitle'); $('.has-tooltip', $value).tooltip({ container: 'body' diff --git a/app/assets/javascripts/lib/utils/common_utils.js b/app/assets/javascripts/lib/utils/common_utils.js index 9ff2042475b..8b5445d012b 100644 --- a/app/assets/javascripts/lib/utils/common_utils.js +++ b/app/assets/javascripts/lib/utils/common_utils.js @@ -51,7 +51,7 @@ export const rstrip = (val) => { return val; }; -export const updateTooltipTitle = ($tooltipEl, newTitle) => $tooltipEl.attr('title', newTitle).tooltip('fixTitle'); +export const updateTooltipTitle = ($tooltipEl, newTitle) => $tooltipEl.attr('title', newTitle).tooltip('_fixTitle'); export const disableButtonIfEmptyField = (fieldSelector, buttonSelector, eventName = 'input') => { const field = $(fieldSelector); diff --git a/app/assets/javascripts/lib/utils/datetime_utility.js b/app/assets/javascripts/lib/utils/datetime_utility.js index c3d94d63c13..0ff23bbb061 100644 --- a/app/assets/javascripts/lib/utils/datetime_utility.js +++ b/app/assets/javascripts/lib/utils/datetime_utility.js @@ -2,10 +2,7 @@ import $ from 'jquery'; import timeago from 'timeago.js'; import dateFormat from 'vendor/date.format'; import { pluralize } from './text_utility'; -import { - languageCode, - s__, -} from '../../locale'; +import { languageCode, s__ } from '../../locale'; window.timeago = timeago; window.dateFormat = dateFormat; @@ -17,11 +14,37 @@ window.dateFormat = dateFormat; * * @param {Boolean} abbreviated */ -const getMonthNames = (abbreviated) => { +const getMonthNames = abbreviated => { if (abbreviated) { - return [s__('Jan'), s__('Feb'), s__('Mar'), s__('Apr'), s__('May'), s__('Jun'), s__('Jul'), s__('Aug'), s__('Sep'), s__('Oct'), s__('Nov'), s__('Dec')]; + return [ + s__('Jan'), + s__('Feb'), + s__('Mar'), + s__('Apr'), + s__('May'), + s__('Jun'), + s__('Jul'), + s__('Aug'), + s__('Sep'), + s__('Oct'), + s__('Nov'), + s__('Dec'), + ]; } - return [s__('January'), s__('February'), s__('March'), s__('April'), s__('May'), s__('June'), s__('July'), s__('August'), s__('September'), s__('October'), s__('November'), s__('December')]; + return [ + s__('January'), + s__('February'), + s__('March'), + s__('April'), + s__('May'), + s__('June'), + s__('July'), + s__('August'), + s__('September'), + s__('October'), + s__('November'), + s__('December'), + ]; }; /** @@ -29,7 +52,8 @@ const getMonthNames = (abbreviated) => { * @param {date} date * @returns {String} */ -export const getDayName = date => ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'][date.getDay()]; +export const getDayName = date => + ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'][date.getDay()]; /** * @example @@ -55,7 +79,7 @@ export function getTimeago() { if (!timeagoInstance) { const localeRemaining = function getLocaleRemaining(number, index) { return [ - [s__('Timeago|less than a minute ago'), s__('Timeago|in a while')], + [s__('Timeago|less than a minute ago'), s__('Timeago|right now')], [s__('Timeago|less than a minute ago'), s__('Timeago|%s seconds remaining')], [s__('Timeago|about a minute ago'), s__('Timeago|1 minute remaining')], [s__('Timeago|%s minutes ago'), s__('Timeago|%s minutes remaining')], @@ -73,7 +97,7 @@ export function getTimeago() { }; const locale = function getLocale(number, index) { return [ - [s__('Timeago|less than a minute ago'), s__('Timeago|in a while')], + [s__('Timeago|less than a minute ago'), s__('Timeago|right now')], [s__('Timeago|less than a minute ago'), s__('Timeago|in %s seconds')], [s__('Timeago|about a minute ago'), s__('Timeago|in 1 minute')], [s__('Timeago|%s minutes ago'), s__('Timeago|in %s minutes')], @@ -102,7 +126,7 @@ export function getTimeago() { * For the given element, renders a timeago instance. * @param {jQuery} $els */ -export const renderTimeago = ($els) => { +export const renderTimeago = $els => { const timeagoEls = $els || document.querySelectorAll('.js-timeago-render'); // timeago.js sets timeouts internally for each timeago value to be updated in real time @@ -119,7 +143,7 @@ export const localTimeAgo = ($timeagoEls, setTimeago = true) => { if (setTimeago) { // Recreate with custom template $(el).tooltip({ - template: '<div class="tooltip local-timeago" role="tooltip"><div class="tooltip-arrow"></div><div class="tooltip-inner"></div></div>', + template: '<div class="tooltip local-timeago" role="tooltip"><div class="arrow"></div><div class="tooltip-inner"></div></div>', }); } @@ -141,7 +165,9 @@ export const timeFor = (time, expiredLabel) => { if (new Date(time) < new Date()) { return expiredLabel || s__('Timeago|Past due'); } - return getTimeago().format(time, `${timeagoLanguageCode}-remaining`).trim(); + return getTimeago() + .format(time, `${timeagoLanguageCode}-remaining`) + .trim(); }; export const getDayDifference = (a, b) => { @@ -161,7 +187,7 @@ export const getDayDifference = (a, b) => { export function timeIntervalInWords(intervalInSeconds) { const secondsInteger = parseInt(intervalInSeconds, 10); const minutes = Math.floor(secondsInteger / 60); - const seconds = secondsInteger - (minutes * 60); + const seconds = secondsInteger - minutes * 60; let text = ''; if (minutes >= 1) { @@ -178,8 +204,34 @@ export function dateInWords(date, abbreviated = false, hideYear = false) { const month = date.getMonth(); const year = date.getFullYear(); - const monthNames = [s__('January'), s__('February'), s__('March'), s__('April'), s__('May'), s__('June'), s__('July'), s__('August'), s__('September'), s__('October'), s__('November'), s__('December')]; - const monthNamesAbbr = [s__('Jan'), s__('Feb'), s__('Mar'), s__('Apr'), s__('May'), s__('Jun'), s__('Jul'), s__('Aug'), s__('Sep'), s__('Oct'), s__('Nov'), s__('Dec')]; + const monthNames = [ + s__('January'), + s__('February'), + s__('March'), + s__('April'), + s__('May'), + s__('June'), + s__('July'), + s__('August'), + s__('September'), + s__('October'), + s__('November'), + s__('December'), + ]; + const monthNamesAbbr = [ + s__('Jan'), + s__('Feb'), + s__('Mar'), + s__('Apr'), + s__('May'), + s__('Jun'), + s__('Jul'), + s__('Aug'), + s__('Sep'), + s__('Oct'), + s__('Nov'), + s__('Dec'), + ]; const monthName = abbreviated ? monthNamesAbbr[month] : monthNames[month]; @@ -210,7 +262,7 @@ export const monthInWords = (date, abbreviated = false) => { * * @param {Date} date */ -export const totalDaysInMonth = (date) => { +export const totalDaysInMonth = date => { if (!date) { return 0; } @@ -223,12 +275,20 @@ export const totalDaysInMonth = (date) => { * * @param {Date} date */ -export const getSundays = (date) => { +export const getSundays = date => { if (!date) { return []; } - const daysToSunday = ['Saturday', 'Friday', 'Thursday', 'Wednesday', 'Tuesday', 'Monday', 'Sunday']; + const daysToSunday = [ + 'Saturday', + 'Friday', + 'Thursday', + 'Wednesday', + 'Tuesday', + 'Monday', + 'Sunday', + ]; const month = date.getMonth(); const year = date.getFullYear(); diff --git a/app/assets/javascripts/lib/utils/keycodes.js b/app/assets/javascripts/lib/utils/keycodes.js new file mode 100644 index 00000000000..5e0f9b612a2 --- /dev/null +++ b/app/assets/javascripts/lib/utils/keycodes.js @@ -0,0 +1,4 @@ +export const UP_KEY_CODE = 38; +export const DOWN_KEY_CODE = 40; +export const ENTER_KEY_CODE = 13; +export const ESC_KEY_CODE = 27; 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 2c80baba10b..9803bebfd10 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; @@ -46,16 +46,20 @@ document.addEventListener('beforeunload', () => { // Unbind scroll events $(document).off('scroll'); // Close any open tooltips - $('.has-tooltip, [data-toggle="tooltip"]').tooltip('destroy'); + $('.has-tooltip, [data-toggle="tooltip"]').tooltip('dispose'); // Close any open popover - $('[data-toggle="popover"]').popover('destroy'); + $('[data-toggle="popover"]').popover('dispose'); }); 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('destroy') + $(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 @@ -136,9 +141,9 @@ document.addEventListener('DOMContentLoaded', () => { }); // Initialize tooltips - $.fn.tooltip.Constructor.DEFAULTS.trigger = 'hover'; $body.tooltip({ selector: '.has-tooltip, [data-toggle="tooltip"]', + trigger: 'hover', placement(tip, el) { return $(el).data('placement') || 'bottom'; }, @@ -155,7 +160,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); @@ -189,7 +196,7 @@ document.addEventListener('DOMContentLoaded', () => { $container.remove(); }); - $('.navbar-toggle').on('click', () => { + $('.navbar-toggler').on('click', () => { $('.header-content').toggleClass('menu-expanded'); gl.lazyLoader.loadCheck(); }); @@ -204,9 +211,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'); @@ -247,9 +260,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/merge_conflicts/merge_conflict_store.js b/app/assets/javascripts/merge_conflicts/merge_conflict_store.js index db1d09eb2f2..70f185e3656 100644 --- a/app/assets/javascripts/merge_conflicts/merge_conflict_store.js +++ b/app/assets/javascripts/merge_conflicts/merge_conflict_store.js @@ -351,7 +351,7 @@ import Cookies from 'js-cookie'; }, getCommitButtonText() { - const initial = 'Commit conflict resolution'; + const initial = 'Commit to source branch'; const inProgress = 'Committing...'; return this.state ? this.state.isSubmitting ? inProgress : initial : initial; diff --git a/app/assets/javascripts/merge_request_tabs.js b/app/assets/javascripts/merge_request_tabs.js index 3f84f4b9499..3548c07aea8 100644 --- a/app/assets/javascripts/merge_request_tabs.js +++ b/app/assets/javascripts/merge_request_tabs.js @@ -362,7 +362,7 @@ export default class MergeRequestTabs { // // status - Boolean, true to show, false to hide toggleLoading(status) { - $('.mr-loading-status .loading').toggle(status); + $('.mr-loading-status .loading').toggleClass('hidden', status); } diffViewType() { diff --git a/app/assets/javascripts/milestone_select.js b/app/assets/javascripts/milestone_select.js index d0a2b27b0e6..f8b3d3061f0 100644 --- a/app/assets/javascripts/milestone_select.js +++ b/app/assets/javascripts/milestone_select.js @@ -4,6 +4,7 @@ import $ from 'jquery'; import _ from 'underscore'; +import { __ } from '~/locale'; import axios from './lib/utils/axios_utils'; import { timeFor } from './lib/utils/datetime_utility'; import ModalStore from './boards/stores/modal_store'; @@ -11,7 +12,8 @@ import ModalStore from './boards/stores/modal_store'; export default class MilestoneSelect { constructor(currentProject, els, options = {}) { if (currentProject !== null) { - this.currentProject = typeof currentProject === 'string' ? JSON.parse(currentProject) : currentProject; + this.currentProject = + typeof currentProject === 'string' ? JSON.parse(currentProject) : currentProject; } this.init(els, options); @@ -25,7 +27,10 @@ export default class MilestoneSelect { } $els.each((i, dropdown) => { - let collapsedSidebarLabelTemplate, milestoneLinkNoneTemplate, milestoneLinkTemplate, selectedMilestone, selectedMilestoneDefault; + let milestoneLinkNoneTemplate, + milestoneLinkTemplate, + selectedMilestone, + selectedMilestoneDefault; const $dropdown = $(dropdown); const projectId = $dropdown.data('projectId'); const milestonesUrl = $dropdown.data('milestones'); @@ -45,46 +50,47 @@ export default class MilestoneSelect { const $sidebarCollapsedValue = $block.find('.sidebar-collapsed-icon'); const $value = $block.find('.value'); const $loading = $block.find('.block-loading').fadeOut(); - selectedMilestoneDefault = (showAny ? '' : null); - selectedMilestoneDefault = (showNo && defaultNo ? 'No Milestone' : selectedMilestoneDefault); + selectedMilestoneDefault = showAny ? '' : null; + selectedMilestoneDefault = showNo && defaultNo ? 'No Milestone' : selectedMilestoneDefault; selectedMilestone = $dropdown.data('selected') || selectedMilestoneDefault; if (issueUpdateURL) { - milestoneLinkTemplate = _.template('<a href="/<%- full_path %>/milestones/<%- iid %>" class="bold has-tooltip" data-container="body" title="<%- remaining %>"><%- title %></a>'); + milestoneLinkTemplate = _.template( + '<a href="/<%- full_path %>/milestones/<%- iid %>" class="bold has-tooltip" data-container="body" title="<%- remaining %>"><%- title %></a>', + ); milestoneLinkNoneTemplate = '<span class="no-value">None</span>'; - collapsedSidebarLabelTemplate = _.template('<span class="has-tooltip" data-container="body" title="<%- name %><br /><%- remaining %>" data-placement="left" data-html="true"> <%- title %> </span>'); } return $dropdown.glDropdown({ showMenuAbove: showMenuAbove, - data: (term, callback) => axios.get(milestonesUrl) - .then(({ data }) => { + data: (term, callback) => + axios.get(milestonesUrl).then(({ data }) => { const extraOptions = []; if (showAny) { extraOptions.push({ - id: 0, - name: '', - title: 'Any Milestone' + id: null, + name: null, + title: 'Any Milestone', }); } if (showNo) { extraOptions.push({ id: -1, name: 'No Milestone', - title: 'No Milestone' + title: 'No Milestone', }); } if (showUpcoming) { extraOptions.push({ id: -2, name: '#upcoming', - title: 'Upcoming' + title: 'Upcoming', }); } if (showStarted) { extraOptions.push({ id: -3, name: '#started', - title: 'Started' + title: 'Started', }); } if (extraOptions.length) { @@ -106,7 +112,7 @@ export default class MilestoneSelect { `, filterable: true, search: { - fields: ['title'] + fields: ['title'], }, selectable: true, toggleLabel: (selected, el, e) => { @@ -119,7 +125,7 @@ export default class MilestoneSelect { defaultLabel: defaultLabel, fieldName: $dropdown.data('fieldName'), text: milestone => _.escape(milestone.title), - id: (milestone) => { + id: milestone => { if (!useId && !$dropdown.is('.js-issuable-form-dropdown')) { return milestone.name; } else { @@ -131,7 +137,7 @@ export default class MilestoneSelect { // display:block overrides the hide-collapse rule return $value.css('display', ''); }, - opened: (e) => { + opened: e => { const $el = $(e.currentTarget); if ($dropdown.hasClass('js-issue-board-sidebar') || options.handleClick) { selectedMilestone = $dropdown[0].dataset.selected || selectedMilestoneDefault; @@ -140,7 +146,7 @@ export default class MilestoneSelect { $(`[data-milestone-id="${_.escape(selectedMilestone)}"] > a`, $el).addClass('is-active'); }, vue: $dropdown.hasClass('js-issue-board-sidebar'), - clicked: (clickEvent) => { + clicked: clickEvent => { const { $el, e } = clickEvent; let selected = clickEvent.selectedObj; @@ -155,11 +161,14 @@ export default class MilestoneSelect { const page = $('body').attr('data-page'); const isIssueIndex = page === 'projects:issues:index'; - const isMRIndex = (page === page && page === 'projects:merge_requests:index'); - const isSelecting = (selected.name !== selectedMilestone); + const isMRIndex = page === page && page === 'projects:merge_requests:index'; + const isSelecting = selected.name !== selectedMilestone; selectedMilestone = isSelecting ? selected.name : selectedMilestoneDefault; - if ($dropdown.hasClass('js-filter-bulk-update') || $dropdown.hasClass('js-issuable-form-dropdown')) { + if ( + $dropdown.hasClass('js-filter-bulk-update') || + $dropdown.hasClass('js-issuable-form-dropdown') + ) { e.preventDefault(); return; } @@ -177,10 +186,13 @@ export default class MilestoneSelect { return $dropdown.closest('form').submit(); } else if ($dropdown.hasClass('js-issue-board-sidebar')) { if (selected.id !== -1 && isSelecting) { - gl.issueBoards.boardStoreIssueSet('milestone', new ListMilestone({ - id: selected.id, - title: selected.name - })); + gl.issueBoards.boardStoreIssueSet( + 'milestone', + new ListMilestone({ + id: selected.id, + title: selected.name, + }), + ); } else { gl.issueBoards.boardStoreIssueDelete('milestone'); } @@ -188,7 +200,8 @@ export default class MilestoneSelect { $dropdown.trigger('loading.gl.dropdown'); $loading.removeClass('hidden').fadeIn(); - gl.issueBoards.BoardsStore.detail.issue.update($dropdown.attr('data-issue-update')) + gl.issueBoards.BoardsStore.detail.issue + .update($dropdown.attr('data-issue-update')) .then(() => { $dropdown.trigger('loaded.gl.dropdown'); $loading.fadeOut(); @@ -203,7 +216,8 @@ export default class MilestoneSelect { data[abilityName].milestone_id = selected != null ? selected : null; $loading.removeClass('hidden').fadeIn(); $dropdown.trigger('loading.gl.dropdown'); - return axios.put(issueUpdateURL, data) + return axios + .put(issueUpdateURL, data) .then(({ data }) => { $dropdown.trigger('loaded.gl.dropdown'); $loading.fadeOut(); @@ -214,17 +228,26 @@ export default class MilestoneSelect { data.milestone.remaining = timeFor(data.milestone.due_date); data.milestone.name = data.milestone.title; $value.html(milestoneLinkTemplate(data.milestone)); - return $sidebarCollapsedValue.find('span').html(collapsedSidebarLabelTemplate(data.milestone)); + return $sidebarCollapsedValue + .attr( + 'data-original-title', + `${data.milestone.name}<br />${data.milestone.remaining}`, + ) + .find('span') + .text(data.milestone.title); } else { $value.html(milestoneLinkNoneTemplate); - return $sidebarCollapsedValue.find('span').text('No'); + return $sidebarCollapsedValue + .attr('data-original-title', __('Milestone')) + .find('span') + .text(__('None')); } }) .catch(() => { $loading.fadeOut(); }); } - } + }, }); }); } 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/components/graph_group.vue b/app/assets/javascripts/monitoring/components/graph_group.vue index a6dbe42a8f0..241627f9790 100644 --- a/app/assets/javascripts/monitoring/components/graph_group.vue +++ b/app/assets/javascripts/monitoring/components/graph_group.vue @@ -17,12 +17,12 @@ export default { <template> <div v-if="showPanels" - class="panel panel-default prometheus-panel" + class="card prometheus-panel" > - <div class="panel-heading"> + <div class="card-header"> <h4>{{ name }}</h4> </div> - <div class="panel-body prometheus-graph-group"> + <div class="card-body prometheus-graph-group"> <slot></slot> </div> </div> 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.js b/app/assets/javascripts/notes.js index 2121907dff0..b2c1a26bbae 100644 --- a/app/assets/javascripts/notes.js +++ b/app/assets/javascripts/notes.js @@ -1231,8 +1231,8 @@ export default class Notes { const isForced = forceShow === true || forceShow === false; const showNow = forceShow === true || (!isCurrentlyShown && !isForced); - targetRow.toggle(showNow); - notesContent.toggle(showNow); + targetRow.toggleClass('hide', !showNow); + notesContent.toggleClass('hide', !showNow); } if (addForm) { @@ -1427,7 +1427,7 @@ export default class Notes { const { discussion_html } = data; const lines = $(discussion_html).find('.line_holder'); lines.addClass('fade-in'); - $container.find('tbody').prepend(lines); + $container.find('.diff-content > table > tbody').prepend(lines); const fileHolder = $container.find('.file-holder'); $container.find('.line-holder-placeholder').remove(); syntaxHighlight(fileHolder); @@ -1675,7 +1675,7 @@ export default class Notes { <div class="note-header"> <div class="note-header-info"> <a href="/${_.escape(currentUsername)}"> - <span class="hidden-xs">${_.escape( + <span class="d-none d-sm-block">${_.escape( currentUsername, )}</span> <span class="note-headline-light">${_.escape( @@ -1694,7 +1694,7 @@ export default class Notes { </li>`, ); - $tempNote.find('.hidden-xs').text(_.escape(currentUserFullname)); + $tempNote.find('.d-none.d-sm-block').text(_.escape(currentUserFullname)); $tempNote .find('.note-headline-light') .text(`@${_.escape(currentUsername)}`); diff --git a/app/assets/javascripts/notes/components/comment_form.vue b/app/assets/javascripts/notes/components/comment_form.vue index 396a675b4ac..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; }, @@ -325,7 +321,7 @@ Please check your network connection and try again.`; <li class="timeline-entry"> <div class="timeline-entry-inner"> <div class="flash-container error-alert timeline-content"></div> - <div class="timeline-icon hidden-xs hidden-sm"> + <div class="timeline-icon d-none d-sm-none d-md-block"> <user-avatar-link v-if="author" :link-href="author.path" @@ -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" @@ -373,7 +369,7 @@ js-gfm-input js-autosize markdown-area js-vue-textarea" </markdown-field> <div class="note-form-actions"> <div - class="pull-left btn-group + class="float-left btn-group append-right-10 comment-type-dropdown js-comment-type-dropdown droplab-dropdown"> <button @click.prevent="handleSave()" @@ -387,6 +383,7 @@ append-right-10 comment-type-dropdown js-comment-type-dropdown droplab-dropdown" name="button" type="button" class="btn comment-btn note-type-toggle js-note-new-discussion dropdown-toggle" + data-display="static" data-toggle="dropdown" aria-label="Open comment type dropdown"> <i diff --git a/app/assets/javascripts/notes/components/discussion_counter.vue b/app/assets/javascripts/notes/components/discussion_counter.vue index d492d1cd001..cbe4774a360 100644 --- a/app/assets/javascripts/notes/components/discussion_counter.vue +++ b/app/assets/javascripts/notes/components/discussion_counter.vue @@ -86,7 +86,7 @@ export default { v-html="resolveSvg" ></span> </span> - <span class=".line-resolve-text"> + <span class="line-resolve-text"> {{ resolvedDiscussionCount }}/{{ discussionCount }} {{ countText }} resolved </span> </div> diff --git a/app/assets/javascripts/notes/components/note_edited_text.vue b/app/assets/javascripts/notes/components/note_edited_text.vue index 4ddca918495..2dc39d1a186 100644 --- a/app/assets/javascripts/notes/components/note_edited_text.vue +++ b/app/assets/javascripts/notes/components/note_edited_text.vue @@ -32,17 +32,17 @@ export default { <template> <div :class="className"> {{ actionText }} - <time-ago-tooltip - :time="editedAt" - tooltip-placement="bottom" - /> <template v-if="editedBy"> - by + {{ s__('ByAuthor|by') }} <a :href="editedBy.path" class="js-vue-author author_link"> {{ editedBy.name }} </a> </template> + <time-ago-tooltip + :time="editedAt" + tooltip-placement="bottom" + /> </div> </template> diff --git a/app/assets/javascripts/notes/components/note_header.vue b/app/assets/javascripts/notes/components/note_header.vue index c3d1ef1fcc6..a4081957207 100644 --- a/app/assets/javascripts/notes/components/note_header.vue +++ b/app/assets/javascripts/notes/components/note_header.vue @@ -62,6 +62,21 @@ export default { <template> <div class="note-header-info"> + <div + v-if="includeToggle" + class="discussion-actions"> + <button + @click="handleToggle" + class="note-action-button discussion-toggle-button js-vue-toggle-button" + type="button"> + <i + :class="toggleChevronClass" + class="fa" + aria-hidden="true"> + </i> + {{ __('Toggle discussion') }} + </button> + </div> <a :href="author.path"> <span class="note-header-author-name">{{ author.name }}</span> <span class="note-headline-light"> @@ -78,10 +93,13 @@ export default { v-html="actionTextHtml" class="system-note-message"> </span> + <span class="system-note-separator"> + · + </span> <a :href="noteTimestampLink" @click="updateTargetNoteHash" - class="note-timestamp"> + class="note-timestamp system-note-separator"> <time-ago-tooltip :time="createdAt" tooltip-placement="bottom" @@ -95,20 +113,5 @@ export default { </i> </span> </span> - <div - v-if="includeToggle" - class="discussion-actions"> - <button - @click="handleToggle" - class="note-action-button discussion-toggle-button js-vue-toggle-button" - type="button"> - <i - :class="toggleChevronClass" - class="fa" - aria-hidden="true"> - </i> - Toggle discussion - </button> - </div> </div> </template> diff --git a/app/assets/javascripts/notes/components/noteable_discussion.vue b/app/assets/javascripts/notes/components/noteable_discussion.vue index e0f883a8e08..7f5aacaa3a2 100644 --- a/app/assets/javascripts/notes/components/noteable_discussion.vue +++ b/app/assets/javascripts/notes/components/noteable_discussion.vue @@ -100,7 +100,7 @@ export default { : 'div'; }, wrapperClass() { - return this.isDiffDiscussion ? '' : 'panel panel-default'; + return this.isDiffDiscussion ? '' : 'card'; }, }, mounted() { @@ -263,7 +263,7 @@ Please check your network connection and try again.`; class="discussion-reply-holder"> <template v-if="!isReplying && canReply"> <div - class="btn-group-justified discussion-with-resolve-btn" + class="btn-group d-flex discussion-with-resolve-btn" role="group"> <div class="btn-group" diff --git a/app/assets/javascripts/notes/stores/actions.js b/app/assets/javascripts/notes/stores/actions.js index 244a6980b5a..98ce070288e 100644 --- a/app/assets/javascripts/notes/stores/actions.js +++ b/app/assets/javascripts/notes/stores/actions.js @@ -315,3 +315,6 @@ export const scrollToNoteIfNeeded = (context, el) => { scrollToElement(el); } }; + +// prevent babel-plugin-rewire from generating an invalid default during karma tests +export default () => {}; diff --git a/app/assets/javascripts/notes/stores/getters.js b/app/assets/javascripts/notes/stores/getters.js index f89591a54d6..787be6f4c99 100644 --- a/app/assets/javascripts/notes/stores/getters.js +++ b/app/assets/javascripts/notes/stores/getters.js @@ -68,3 +68,6 @@ export const resolvedDiscussionCount = (state, getters) => { return Object.keys(resolvedMap).length; }; + +// prevent babel-plugin-rewire from generating an invalid default during karma tests +export default () => {}; diff --git a/app/assets/javascripts/notifications_form.js b/app/assets/javascripts/notifications_form.js index 9e6cf67dff0..00e27ca0e70 100644 --- a/app/assets/javascripts/notifications_form.js +++ b/app/assets/javascripts/notifications_form.js @@ -15,7 +15,7 @@ export default class NotificationsForm { toggleCheckbox(e) { const $checkbox = $(e.currentTarget); - const $parent = $checkbox.closest('.checkbox'); + const $parent = $checkbox.closest('.form-check'); this.saveEvent($checkbox, $parent); } diff --git a/app/assets/javascripts/pages/admin/admin.js b/app/assets/javascripts/pages/admin/admin.js index 91f154b7ecd..ff4d6ab15f9 100644 --- a/app/assets/javascripts/pages/admin/admin.js +++ b/app/assets/javascripts/pages/admin/admin.js @@ -25,7 +25,7 @@ export default function adminInit() { $('body').on('click', '.js-toggle-colors-link', (e) => { e.preventDefault(); - $('.js-toggle-colors-container').toggle(); + $('.js-toggle-colors-container').toggleClass('hide'); }); $('.log-tabs a').on('click', function logTabsClick(e) { diff --git a/app/assets/javascripts/pages/admin/users/components/delete_user_modal.vue b/app/assets/javascripts/pages/admin/users/components/delete_user_modal.vue index 0e3ac636661..9ce176744ba 100644 --- a/app/assets/javascripts/pages/admin/users/components/delete_user_modal.vue +++ b/app/assets/javascripts/pages/admin/users/components/delete_user_modal.vue @@ -52,16 +52,15 @@ text() { const keepContributionsText = s__(`AdminArea| You are about to permanently delete the user %{username}. - This will delete all of the issues, merge requests, and groups linked to them. + Issues, merge requests, and groups linked to them will be transferred to a system-wide "Ghost-user". To avoid data loss, consider using the %{strong_start}block user%{strong_end} feature instead. Once you %{strong_start}Delete user%{strong_end}, it cannot be undone or recovered.`); const deleteContributionsText = s__(`AdminArea| You are about to permanently delete the user %{username}. - Issues, merge requests, and groups linked to them will be transferred to a system-wide "Ghost-user". + This will delete all of the issues, merge requests, and groups linked to them. To avoid data loss, consider using the %{strong_start}block user%{strong_end} feature instead. Once you %{strong_start}Delete user%{strong_end}, it cannot be undone or recovered.`); - return sprintf(this.deleteContributions ? deleteContributionsText : keepContributionsText, { username: `<strong>${_.escape(this.username)}</strong>`, 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/ldap/omniauth_callbacks/index.js b/app/assets/javascripts/pages/ldap/omniauth_callbacks/index.js new file mode 100644 index 00000000000..edd7e38471b --- /dev/null +++ b/app/assets/javascripts/pages/ldap/omniauth_callbacks/index.js @@ -0,0 +1,3 @@ +import initU2F from '../../../shared/sessions/u2f'; + +document.addEventListener('DOMContentLoaded', initU2F); diff --git a/app/assets/javascripts/pages/profiles/two_factor_auths/index.js b/app/assets/javascripts/pages/profiles/two_factor_auths/index.js index fbdef329ab2..8e8f47c21d8 100644 --- a/app/assets/javascripts/pages/profiles/two_factor_auths/index.js +++ b/app/assets/javascripts/pages/profiles/two_factor_auths/index.js @@ -5,7 +5,7 @@ document.addEventListener('DOMContentLoaded', () => { const twoFactorNode = document.querySelector('.js-two-factor-auth'); const skippable = twoFactorNode.dataset.twoFactorSkippable === 'true'; if (skippable) { - const button = `<a class="btn btn-xs btn-warning pull-right" data-method="patch" href="${twoFactorNode.dataset.two_factor_skip_url}">Configure it later</a>`; + const button = `<a class="btn btn-sm btn-warning float-right" data-method="patch" href="${twoFactorNode.dataset.two_factor_skip_url}">Configure it later</a>`; const flashAlert = document.querySelector('.flash-alert .container-fluid'); if (flashAlert) flashAlert.insertAdjacentHTML('beforeend', button); } 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/graphs/show/stat_graph_contributors_graph.js b/app/assets/javascripts/pages/projects/graphs/show/stat_graph_contributors_graph.js index a99ce0f1c36..5316d3e9f3c 100644 --- a/app/assets/javascripts/pages/projects/graphs/show/stat_graph_contributors_graph.js +++ b/app/assets/javascripts/pages/projects/graphs/show/stat_graph_contributors_graph.js @@ -1,4 +1,4 @@ -/* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, max-len, no-restricted-syntax, vars-on-top, no-use-before-define, no-param-reassign, new-cap, no-underscore-dangle, wrap-iife, comma-dangle, no-return-assign, prefer-arrow-callback, quotes, prefer-template, newline-per-chained-call, no-else-return, no-shadow */ +/* eslint-disable func-names, space-before-function-paren, prefer-rest-params, max-len, no-restricted-syntax, vars-on-top, no-use-before-define, no-param-reassign, new-cap, no-underscore-dangle, wrap-iife, comma-dangle, no-return-assign, prefer-arrow-callback, quotes, prefer-template, newline-per-chained-call, no-else-return, no-shadow */ import $ from 'jquery'; import _ from 'underscore'; @@ -13,17 +13,17 @@ import { dateTickFormat } from '~/lib/utils/tick_formats'; const d3 = { extent, max, select, scaleTime, scaleLinear, axisLeft, axisBottom, area, brushX, timeParse }; -const extend = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; }; const hasProp = {}.hasOwnProperty; +const extend = function(child, parent) { for (const key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; }; export const ContributorsGraph = (function() { function ContributorsGraph() {} ContributorsGraph.prototype.MARGIN = { top: 20, - right: 20, + right: 10, bottom: 30, - left: 50 + left: 40 }; ContributorsGraph.prototype.x_domain = null; @@ -32,6 +32,12 @@ export const ContributorsGraph = (function() { ContributorsGraph.prototype.dates = []; + ContributorsGraph.prototype.determine_width = function(baseWidth, $parentElement) { + const parentPaddingWidth = parseFloat($parentElement.css('padding-left')) + parseFloat($parentElement.css('padding-right')); + const marginWidth = this.MARGIN.left + this.MARGIN.right; + return baseWidth - parentPaddingWidth - marginWidth; + }; + ContributorsGraph.set_x_domain = function(data) { return ContributorsGraph.prototype.x_domain = data; }; @@ -105,11 +111,10 @@ export const ContributorsMasterGraph = (function(superClass) { function ContributorsMasterGraph(data1) { const $parentElement = $('#contributors-master'); - const parentPadding = parseFloat($parentElement.css('padding-left')) + parseFloat($parentElement.css('padding-right')); this.data = data1; this.update_content = this.update_content.bind(this); - this.width = $('.content').width() - parentPadding - (this.MARGIN.left + this.MARGIN.right); + this.width = this.determine_width($('.js-graphs-show').width(), $parentElement); this.height = 200; this.x = null; this.y = null; @@ -122,8 +127,7 @@ export const ContributorsMasterGraph = (function(superClass) { } ContributorsMasterGraph.prototype.process_dates = function(data) { - var dates; - dates = this.get_dates(data); + const dates = this.get_dates(data); this.parse_dates(data); return ContributorsGraph.set_dates(dates); }; @@ -133,8 +137,7 @@ export const ContributorsMasterGraph = (function(superClass) { }; ContributorsMasterGraph.prototype.parse_dates = function(data) { - var parseDate; - parseDate = d3.timeParse("%Y-%m-%d"); + const parseDate = d3.timeParse("%Y-%m-%d"); return data.forEach(function(d) { return d.date = parseDate(d.date); }); @@ -152,7 +155,14 @@ export const ContributorsMasterGraph = (function(superClass) { }; ContributorsMasterGraph.prototype.create_svg = function() { - return this.svg = d3.select("#contributors-master").append("svg").attr("width", this.width + this.MARGIN.left + this.MARGIN.right).attr("height", this.height + this.MARGIN.top + this.MARGIN.bottom).attr("class", "tint-box").append("g").attr("transform", "translate(" + this.MARGIN.left + "," + this.MARGIN.top + ")"); + this.svg = d3.select("#contributors-master") + .append("svg") + .attr("width", this.width + this.MARGIN.left + this.MARGIN.right) + .attr("height", this.height + this.MARGIN.top + this.MARGIN.bottom) + .attr("class", "tint-box") + .append("g") + .attr("transform", "translate(" + this.MARGIN.left + "," + this.MARGIN.top + ")"); + return this.svg; }; ContributorsMasterGraph.prototype.create_area = function(x, y) { @@ -218,12 +228,14 @@ export const ContributorsAuthorGraph = (function(superClass) { extend(ContributorsAuthorGraph, superClass); function ContributorsAuthorGraph(data1) { + const $parentElements = $('.person'); + this.data = data1; // Don't split graph size in half for mobile devices. - if ($(window).width() < 768) { - this.width = $('.content').width() - 80; + if ($(window).width() < 790) { + this.width = this.determine_width($('.js-graphs-show').width(), $parentElements); } else { - this.width = ($('.content').width() / 2) - 100; + this.width = this.determine_width($('.js-graphs-show').width() / 2, $parentElements); } this.height = 200; this.x = null; @@ -249,8 +261,7 @@ export const ContributorsAuthorGraph = (function(superClass) { ContributorsAuthorGraph.prototype.create_area = function(x, y) { return this.area = d3.area().x(function(d) { - var parseDate; - parseDate = d3.timeParse("%Y-%m-%d"); + const parseDate = d3.timeParse("%Y-%m-%d"); return x(parseDate(d)); }).y0(this.height).y1((function(_this) { return function(d) { @@ -264,9 +275,16 @@ export const ContributorsAuthorGraph = (function(superClass) { }; ContributorsAuthorGraph.prototype.create_svg = function() { - var persons = document.querySelectorAll('.person'); + const persons = document.querySelectorAll('.person'); this.list_item = persons[persons.length - 1]; - return this.svg = d3.select(this.list_item).append("svg").attr("width", this.width + this.MARGIN.left + this.MARGIN.right).attr("height", this.height + this.MARGIN.top + this.MARGIN.bottom).attr("class", "spark").append("g").attr("transform", "translate(" + this.MARGIN.left + "," + this.MARGIN.top + ")"); + this.svg = d3.select(this.list_item) + .append("svg") + .attr("width", this.width + this.MARGIN.left + this.MARGIN.right) + .attr("height", this.height + this.MARGIN.top + this.MARGIN.bottom) + .attr("class", "spark") + .append("g") + .attr("transform", "translate(" + this.MARGIN.left + "," + this.MARGIN.top + ")"); + return this.svg; }; ContributorsAuthorGraph.prototype.draw_path = function(data) { 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/pages/projects/shared/permissions/components/project_setting_row.vue b/app/assets/javascripts/pages/projects/shared/permissions/components/project_setting_row.vue index 25a88f846eb..17b91479ea5 100644 --- a/app/assets/javascripts/pages/projects/shared/permissions/components/project_setting_row.vue +++ b/app/assets/javascripts/pages/projects/shared/permissions/components/project_setting_row.vue @@ -42,7 +42,7 @@ </label> <span v-if="helpText" - class="help-block" + class="form-text text-muted" > {{ helpText }} </span> diff --git a/app/assets/javascripts/pages/projects/wikis/components/delete_wiki_modal.vue b/app/assets/javascripts/pages/projects/wikis/components/delete_wiki_modal.vue new file mode 100644 index 00000000000..df21e2f8771 --- /dev/null +++ b/app/assets/javascripts/pages/projects/wikis/components/delete_wiki_modal.vue @@ -0,0 +1,77 @@ +<script> +import _ from 'underscore'; +import GlModal from '~/vue_shared/components/gl_modal.vue'; +import { s__, sprintf } from '~/locale'; + +export default { + components: { + GlModal, + }, + props: { + deleteWikiUrl: { + type: String, + required: true, + default: '', + }, + pageTitle: { + type: String, + required: true, + default: '', + }, + csrfToken: { + type: String, + required: true, + default: '', + }, + }, + computed: { + message() { + return s__('WikiPageConfirmDelete|Are you sure you want to delete this page?'); + }, + title() { + return sprintf( + s__('WikiPageConfirmDelete|Delete page %{pageTitle}?'), + { + pageTitle: _.escape(this.pageTitle), + }, + false, + ); + }, + }, + methods: { + onSubmit() { + this.$refs.form.submit(); + }, + }, +}; +</script> + +<template> + <gl-modal + id="delete-wiki-modal" + :header-title-text="title" + footer-primary-button-variant="danger" + :footer-primary-button-text="s__('WikiPageConfirmDelete|Delete page')" + @submit="onSubmit" + > + {{ message }} + <form + ref="form" + :action="deleteWikiUrl" + method="post" + class="form-horizontal js-requires-input" + > + <input + ref="method" + type="hidden" + name="_method" + value="delete" + /> + <input + type="hidden" + name="authenticity_token" + :value="csrfToken" + /> + </form> + </gl-modal> +</template> diff --git a/app/assets/javascripts/pages/projects/wikis/index.js b/app/assets/javascripts/pages/projects/wikis/index.js index ec01c66ffda..0295653cb29 100644 --- a/app/assets/javascripts/pages/projects/wikis/index.js +++ b/app/assets/javascripts/pages/projects/wikis/index.js @@ -1,12 +1,40 @@ import $ from 'jquery'; +import Vue from 'vue'; +import Translate from '~/vue_shared/translate'; +import csrf from '~/lib/utils/csrf'; import Wikis from './wikis'; import ShortcutsWiki from '../../../shortcuts_wiki'; import ZenMode from '../../../zen_mode'; import GLForm from '../../../gl_form'; +import deleteWikiModal from './components/delete_wiki_modal.vue'; document.addEventListener('DOMContentLoaded', () => { new Wikis(); // eslint-disable-line no-new new ShortcutsWiki(); // eslint-disable-line no-new new ZenMode(); // eslint-disable-line no-new new GLForm($('.wiki-form'), true); // eslint-disable-line no-new + + const deleteWikiButton = document.getElementById('delete-wiki-button'); + + if (deleteWikiButton) { + Vue.use(Translate); + + const { deleteWikiUrl, pageTitle } = deleteWikiButton.dataset; + const deleteWikiModalEl = document.getElementById('delete-wiki-modal'); + const deleteModal = new Vue({ // eslint-disable-line + el: deleteWikiModalEl, + data: { + deleteWikiUrl: '', + }, + render(createElement) { + return createElement(deleteWikiModal, { + props: { + pageTitle, + deleteWikiUrl, + csrfToken: csrf.token, + }, + }); + }, + }); + } }); diff --git a/app/assets/javascripts/pages/users/activity_calendar.js b/app/assets/javascripts/pages/users/activity_calendar.js index 8ce938c958b..50d042fef29 100644 --- a/app/assets/javascripts/pages/users/activity_calendar.js +++ b/app/assets/javascripts/pages/users/activity_calendar.js @@ -19,7 +19,7 @@ function getSystemDate(systemUtcOffsetSeconds) { const date = new Date(); const localUtcOffsetMinutes = 0 - date.getTimezoneOffset(); const systemUtcOffsetMinutes = systemUtcOffsetSeconds / 60; - date.setMinutes((date.getMinutes() - localUtcOffsetMinutes) + systemUtcOffsetMinutes); + date.setMinutes(date.getMinutes() - localUtcOffsetMinutes + systemUtcOffsetMinutes); return date; } @@ -35,18 +35,36 @@ function formatTooltipText({ date, count }) { return `${contribText}<br />${dateDayName} ${dateText}`; } -const initColorKey = () => d3.scaleLinear().range(['#acd5f2', '#254e77']).domain([0, 3]); +const initColorKey = () => + d3 + .scaleLinear() + .range(['#acd5f2', '#254e77']) + .domain([0, 3]); export default class ActivityCalendar { - constructor(container, timestamps, calendarActivitiesPath, utcOffset = 0) { + constructor(container, timestamps, calendarActivitiesPath, utcOffset = 0, firstDayOfWeek = 0) { this.calendarActivitiesPath = calendarActivitiesPath; this.clickDay = this.clickDay.bind(this); this.currentSelectedDate = ''; this.daySpace = 1; this.daySize = 15; - this.daySizeWithSpace = this.daySize + (this.daySpace * 2); - this.monthNames = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']; + this.daySizeWithSpace = this.daySize + this.daySpace * 2; + this.monthNames = [ + 'Jan', + 'Feb', + 'Mar', + 'Apr', + 'May', + 'Jun', + 'Jul', + 'Aug', + 'Sep', + 'Oct', + 'Nov', + 'Dec', + ]; this.months = []; + this.firstDayOfWeek = firstDayOfWeek; // Loop through the timestamps to create a group of objects // The group of objects will be grouped based on the day of the week they are @@ -70,7 +88,7 @@ export default class ActivityCalendar { // Create a new group array if this is the first day of the week // or if is first object - if ((day === 0 && i !== 0) || i === 0) { + if ((day === this.firstDayOfWeek && i !== 0) || i === 0) { this.timestampsTmp.push([]); group += 1; } @@ -109,21 +127,30 @@ export default class ActivityCalendar { } renderSvg(container, group) { - const width = ((group + 1) * this.daySizeWithSpace) + this.getExtraWidthPadding(group); - return d3.select(container) + const width = (group + 1) * this.daySizeWithSpace + this.getExtraWidthPadding(group); + return d3 + .select(container) .append('svg') - .attr('width', width) - .attr('height', 167) - .attr('class', 'contrib-calendar'); + .attr('width', width) + .attr('height', 167) + .attr('class', 'contrib-calendar'); + } + + dayYPos(day) { + return this.daySizeWithSpace * ((day + 7 - this.firstDayOfWeek) % 7); } renderDays() { - this.svg.selectAll('g').data(this.timestampsTmp).enter().append('g') + this.svg + .selectAll('g') + .data(this.timestampsTmp) + .enter() + .append('g') .attr('transform', (group, i) => { _.each(group, (stamp, a) => { if (a === 0 && stamp.day === 0) { const month = stamp.date.getMonth(); - const x = (this.daySizeWithSpace * i) + 1 + this.daySizeWithSpace; + const x = this.daySizeWithSpace * i + 1 + this.daySizeWithSpace; const lastMonth = _.last(this.months); if ( lastMonth == null || @@ -133,86 +160,113 @@ export default class ActivityCalendar { } } }); - return `translate(${(this.daySizeWithSpace * i) + 1 + this.daySizeWithSpace}, 18)`; + return `translate(${this.daySizeWithSpace * i + 1 + this.daySizeWithSpace}, 18)`; }) .selectAll('rect') - .data(stamp => stamp) - .enter() - .append('rect') - .attr('x', '0') - .attr('y', stamp => this.daySizeWithSpace * stamp.day) - .attr('width', this.daySize) - .attr('height', this.daySize) - .attr('fill', stamp => ( - stamp.count !== 0 ? this.color(Math.min(stamp.count, 40)) : '#ededed' - )) - .attr('title', stamp => formatTooltipText(stamp)) - .attr('class', 'user-contrib-cell js-tooltip') - .attr('data-container', 'body') - .on('click', this.clickDay); + .data(stamp => stamp) + .enter() + .append('rect') + .attr('x', '0') + .attr('y', stamp => this.dayYPos(stamp.day)) + .attr('width', this.daySize) + .attr('height', this.daySize) + .attr( + 'fill', + stamp => (stamp.count !== 0 ? this.color(Math.min(stamp.count, 40)) : '#ededed'), + ) + .attr('title', stamp => formatTooltipText(stamp)) + .attr('class', 'user-contrib-cell js-tooltip') + .attr('data-container', 'body') + .on('click', this.clickDay); } renderDayTitles() { const days = [ { text: 'M', - y: 29 + (this.daySizeWithSpace * 1), - }, { + y: 29 + this.dayYPos(1), + }, + { text: 'W', - y: 29 + (this.daySizeWithSpace * 3), - }, { + y: 29 + this.dayYPos(3), + }, + { text: 'F', - y: 29 + (this.daySizeWithSpace * 5), + y: 29 + this.dayYPos(5), }, ]; - this.svg.append('g') + this.svg + .append('g') .selectAll('text') - .data(days) - .enter() - .append('text') - .attr('text-anchor', 'middle') - .attr('x', 8) - .attr('y', day => day.y) - .text(day => day.text) - .attr('class', 'user-contrib-text'); + .data(days) + .enter() + .append('text') + .attr('text-anchor', 'middle') + .attr('x', 8) + .attr('y', day => day.y) + .text(day => day.text) + .attr('class', 'user-contrib-text'); } renderMonths() { - this.svg.append('g') + this.svg + .append('g') .attr('direction', 'ltr') .selectAll('text') - .data(this.months) - .enter() - .append('text') - .attr('x', date => date.x) - .attr('y', 10) - .attr('class', 'user-contrib-text') - .text(date => this.monthNames[date.month]); + .data(this.months) + .enter() + .append('text') + .attr('x', date => date.x) + .attr('y', 10) + .attr('class', 'user-contrib-text') + .text(date => this.monthNames[date.month]); } renderKey() { - const keyValues = ['no contributions', '1-9 contributions', '10-19 contributions', '20-29 contributions', '30+ contributions']; - const keyColors = ['#ededed', this.colorKey(0), this.colorKey(1), this.colorKey(2), this.colorKey(3)]; + const keyValues = [ + 'no contributions', + '1-9 contributions', + '10-19 contributions', + '20-29 contributions', + '30+ contributions', + ]; + const keyColors = [ + '#ededed', + this.colorKey(0), + this.colorKey(1), + this.colorKey(2), + this.colorKey(3), + ]; - this.svg.append('g') - .attr('transform', `translate(18, ${(this.daySizeWithSpace * 8) + 16})`) + this.svg + .append('g') + .attr('transform', `translate(18, ${this.daySizeWithSpace * 8 + 16})`) .selectAll('rect') - .data(keyColors) - .enter() - .append('rect') - .attr('width', this.daySize) - .attr('height', this.daySize) - .attr('x', (color, i) => this.daySizeWithSpace * i) - .attr('y', 0) - .attr('fill', color => color) - .attr('class', 'js-tooltip') - .attr('title', (color, i) => keyValues[i]) - .attr('data-container', 'body'); + .data(keyColors) + .enter() + .append('rect') + .attr('width', this.daySize) + .attr('height', this.daySize) + .attr('x', (color, i) => this.daySizeWithSpace * i) + .attr('y', 0) + .attr('fill', color => color) + .attr('class', 'js-tooltip') + .attr('title', (color, i) => keyValues[i]) + .attr('data-container', 'body'); } initColor() { - const colorRange = ['#ededed', this.colorKey(0), this.colorKey(1), this.colorKey(2), this.colorKey(3)]; - return d3.scaleThreshold().domain([0, 10, 20, 30]).range(colorRange); + const colorRange = [ + '#ededed', + this.colorKey(0), + this.colorKey(1), + this.colorKey(2), + this.colorKey(3), + ]; + return d3 + .scaleThreshold() + .domain([0, 10, 20, 30]) + .range(colorRange); } clickDay(stamp) { @@ -227,14 +281,15 @@ export default class ActivityCalendar { $('.user-calendar-activities').html(LOADING_HTML); - axios.get(this.calendarActivitiesPath, { - params: { - date, - }, - responseType: 'text', - }) - .then(({ data }) => $('.user-calendar-activities').html(data)) - .catch(() => flash(__('An error occurred while retrieving calendar activity'))); + axios + .get(this.calendarActivitiesPath, { + params: { + date, + }, + responseType: 'text', + }) + .then(({ data }) => $('.user-calendar-activities').html(data)) + .catch(() => flash(__('An error occurred while retrieving calendar activity'))); } else { this.currentSelectedDate = ''; $('.user-calendar-activities').html(''); diff --git a/app/assets/javascripts/pages/users/user_tabs.js b/app/assets/javascripts/pages/users/user_tabs.js index 124bc2ba710..ca375007ec5 100644 --- a/app/assets/javascripts/pages/users/user_tabs.js +++ b/app/assets/javascripts/pages/users/user_tabs.js @@ -181,7 +181,7 @@ export default class UserTabs { toggleLoading(status) { return this.$parentEl.find('.loading-status .loading') - .toggle(status); + .toggleClass('hidden', status); } setCurrentAction(source) { @@ -195,6 +195,6 @@ export default class UserTabs { } getCurrentAction() { - return this.$parentEl.find('.nav-links .active a').data('action'); + return this.$parentEl.find('.nav-links a.active').data('action'); } } diff --git a/app/assets/javascripts/performance_bar/components/performance_bar_app.vue b/app/assets/javascripts/performance_bar/components/performance_bar_app.vue index 2fd1715ee79..8ffaa52d9e8 100644 --- a/app/assets/javascripts/performance_bar/components/performance_bar_app.vue +++ b/app/assets/javascripts/performance_bar/components/performance_bar_app.vue @@ -5,7 +5,6 @@ import PerformanceBarService from '../services/performance_bar_service'; import detailedMetric from './detailed_metric.vue'; import requestSelector from './request_selector.vue'; import simpleMetric from './simple_metric.vue'; -import upstreamPerformanceBar from './upstream_performance_bar.vue'; import Flash from '../../flash'; @@ -14,7 +13,6 @@ export default { detailedMetric, requestSelector, simpleMetric, - upstreamPerformanceBar, }, props: { store: { @@ -128,9 +126,6 @@ export default { {{ currentRequest.details.host.hostname }} </span> </div> - <upstream-performance-bar - v-if="initialRequest && currentRequest.details" - /> <detailed-metric v-for="metric in $options.detailedMetrics" :key="metric.metric" diff --git a/app/assets/javascripts/performance_bar/components/request_selector.vue b/app/assets/javascripts/performance_bar/components/request_selector.vue index 3ed07a4a47d..dd9578a6c7f 100644 --- a/app/assets/javascripts/performance_bar/components/request_selector.vue +++ b/app/assets/javascripts/performance_bar/components/request_selector.vue @@ -37,7 +37,7 @@ export default { <template> <div id="peek-request-selector" - class="pull-right" + class="float-right" > <select v-model="currentRequestId"> <option diff --git a/app/assets/javascripts/performance_bar/components/upstream_performance_bar.vue b/app/assets/javascripts/performance_bar/components/upstream_performance_bar.vue deleted file mode 100644 index 2b5915f381f..00000000000 --- a/app/assets/javascripts/performance_bar/components/upstream_performance_bar.vue +++ /dev/null @@ -1,20 +0,0 @@ -<script> -export default { - mounted() { - const upstreamPerformanceBar = document - .getElementById('peek-view-performance-bar') - .cloneNode(true); - - upstreamPerformanceBar.classList.remove('hidden'); - - this.$refs.wrapper.appendChild(upstreamPerformanceBar); - }, -}; -</script> -<template> - <div - id="peek-view-performance-bar-vue" - class="view" - ref="wrapper" - ></div> -</template> diff --git a/app/assets/javascripts/performance_bar/index.js b/app/assets/javascripts/performance_bar/index.js index a0ddf36a672..4a98aed7679 100644 --- a/app/assets/javascripts/performance_bar/index.js +++ b/app/assets/javascripts/performance_bar/index.js @@ -1,5 +1,3 @@ -import 'vendor/peek.performance_bar'; - import Vue from 'vue'; import performanceBarApp from './components/performance_bar_app.vue'; import PerformanceBarStore from './stores/performance_bar_store'; diff --git a/app/assets/javascripts/pipelines/components/async_button.vue b/app/assets/javascripts/pipelines/components/async_button.vue deleted file mode 100644 index 0cdffbde05b..00000000000 --- a/app/assets/javascripts/pipelines/components/async_button.vue +++ /dev/null @@ -1,95 +0,0 @@ -<script> - /* eslint-disable no-alert */ - - import eventHub from '../event_hub'; - import loadingIcon from '../../vue_shared/components/loading_icon.vue'; - import icon from '../../vue_shared/components/icon.vue'; - import tooltip from '../../vue_shared/directives/tooltip'; - - export default { - directives: { - tooltip, - }, - components: { - loadingIcon, - icon, - }, - props: { - endpoint: { - type: String, - required: true, - }, - title: { - type: String, - required: true, - }, - icon: { - type: String, - required: true, - }, - cssClass: { - type: String, - required: true, - }, - pipelineId: { - type: Number, - required: true, - }, - type: { - type: String, - required: true, - }, - }, - data() { - return { - isLoading: false, - }; - }, - computed: { - buttonClass() { - return `btn ${this.cssClass}`; - }, - }, - created() { - // We're using eventHub to listen to the modal here instead of - // using props because it would would make the parent components - // much more complex to keep track of the loading state of each button - eventHub.$on('postAction', this.setLoading); - }, - beforeDestroy() { - eventHub.$off('postAction', this.setLoading); - }, - methods: { - onClick() { - eventHub.$emit('openConfirmationModal', { - pipelineId: this.pipelineId, - endpoint: this.endpoint, - type: this.type, - }); - }, - setLoading(endpoint) { - if (endpoint === this.endpoint) { - this.isLoading = true; - } - }, - }, - }; -</script> - -<template> - <button - v-tooltip - type="button" - @click="onClick" - :class="buttonClass" - :title="title" - :aria-label="title" - data-container="body" - data-placement="top" - :disabled="isLoading"> - <icon - :name="icon" - /> - <loading-icon v-if="isLoading" /> - </button> -</template> diff --git a/app/assets/javascripts/pipelines/components/blank_state.vue b/app/assets/javascripts/pipelines/components/blank_state.vue index 8d3d6223d7b..f3219b8291c 100644 --- a/app/assets/javascripts/pipelines/components/blank_state.vue +++ b/app/assets/javascripts/pipelines/components/blank_state.vue @@ -17,13 +17,13 @@ <template> <div class="row empty-state"> - <div class="col-xs-12"> + <div class="col-12"> <div class="svg-content"> <img :src="svgPath" /> </div> </div> - <div class="col-xs-12 text-center"> + <div class="col-12 text-center"> <div class="text-content"> <h4>{{ message }}</h4> </div> diff --git a/app/assets/javascripts/pipelines/components/empty_state.vue b/app/assets/javascripts/pipelines/components/empty_state.vue index 10ac8c08bed..50c27bed9fd 100644 --- a/app/assets/javascripts/pipelines/components/empty_state.vue +++ b/app/assets/javascripts/pipelines/components/empty_state.vue @@ -19,13 +19,13 @@ </script> <template> <div class="row empty-state js-empty-state"> - <div class="col-xs-12"> + <div class="col-12"> <div class="svg-content svg-250"> <img :src="emptyStateSvgPath" /> </div> </div> - <div class="col-xs-12"> + <div class="col-12"> <div class="text-content"> <template v-if="canSetCi"> @@ -34,7 +34,7 @@ </h4> <p> - {{ s__(`Pipelines|Continous Integration can help + {{ s__(`Pipelines|Continuous Integration can help catch bugs by running your tests automatically, while Continuous Deployment can help you deliver code to your product environment.`) }} diff --git a/app/assets/javascripts/pipelines/components/graph/action_component.vue b/app/assets/javascripts/pipelines/components/graph/action_component.vue index e99d949801f..82b4ce083fb 100644 --- a/app/assets/javascripts/pipelines/components/graph/action_component.vue +++ b/app/assets/javascripts/pipelines/components/graph/action_component.vue @@ -1,11 +1,21 @@ <script> import $ from 'jquery'; -import tooltip from '../../../vue_shared/directives/tooltip'; -import Icon from '../../../vue_shared/components/icon.vue'; -import { dasherize } from '../../../lib/utils/text_utility'; -import eventHub from '../../event_hub'; +import axios from '~/lib/utils/axios_utils'; +import { dasherize } from '~/lib/utils/text_utility'; +import { __ } from '~/locale'; +import createFlash from '~/flash'; +import tooltip from '~/vue_shared/directives/tooltip'; +import Icon from '~/vue_shared/components/icon.vue'; + /** - * Renders either a cancel, retry or play icon pointing to the given path. + * Renders either a cancel, retry or play icon button and handles the post request + * + * Used in: + * - mr widget mini pipeline graph: `mr_widget_pipeline.vue` + * - pipelines table + * - pipelines table in merge request page + * - pipelines table in commit page + * - pipelines detail page in big graph */ export default { components: { @@ -32,26 +42,41 @@ export default { required: true, }, - buttonDisabled: { - type: String, - required: false, - default: null, - }, }, + data() { + return { + isDisabled: false, + }; + }, + computed: { cssClass() { const actionIconDash = dasherize(this.actionIcon); return `${actionIconDash} js-icon-${actionIconDash}`; }, - isDisabled() { - return this.buttonDisabled === this.link; - }, }, - methods: { + /** + * The request should not be handled here. + * However due to this component being used in several + * different apps it avoids repetition & complexity. + * + */ onClickAction() { $(this.$el).tooltip('hide'); - eventHub.$emit('graphAction', this.link); + + this.isDisabled = true; + + axios.post(`${this.link}.json`) + .then(() => { + this.isDisabled = false; + this.$emit('pipelineActionRequestComplete'); + }) + .catch(() => { + this.isDisabled = false; + + createFlash(__('An error occurred while making the request.')); + }); }, }, }; @@ -62,11 +87,12 @@ export default { @click="onClickAction" v-tooltip :title="tooltipText" - class="btn btn-blank btn-transparent ci-action-icon-container ci-action-icon-wrapper" + class="js-ci-action btn btn-blank +btn-transparent ci-action-icon-container ci-action-icon-wrapper" :class="cssClass" data-container="body" :disabled="isDisabled" > - <icon :name="actionIcon" /> + <icon :name="actionIcon"/> </button> </template> diff --git a/app/assets/javascripts/pipelines/components/graph/dropdown_action_component.vue b/app/assets/javascripts/pipelines/components/graph/dropdown_action_component.vue deleted file mode 100644 index 7c4fd65e36f..00000000000 --- a/app/assets/javascripts/pipelines/components/graph/dropdown_action_component.vue +++ /dev/null @@ -1,53 +0,0 @@ -<script> - import icon from '../../../vue_shared/components/icon.vue'; - import tooltip from '../../../vue_shared/directives/tooltip'; - - /** - * Renders either a cancel, retry or play icon pointing to the given path. - * TODO: Remove UJS from here and use an async request instead. - */ - export default { - components: { - icon, - }, - - directives: { - tooltip, - }, - props: { - tooltipText: { - type: String, - required: true, - }, - - link: { - type: String, - required: true, - }, - - actionMethod: { - type: String, - required: true, - }, - - actionIcon: { - type: String, - required: true, - }, - }, - }; -</script> -<template> - <a - v-tooltip - :data-method="actionMethod" - :title="tooltipText" - :href="link" - rel="nofollow" - class="ci-action-icon-wrapper js-ci-status-icon" - data-container="body" - aria-label="Job's action" - > - <icon :name="actionIcon" /> - </a> -</template> 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 be213c2ee78..7bfe11ab8cd 100644 --- a/app/assets/javascripts/pipelines/components/graph/dropdown_job_component.vue +++ b/app/assets/javascripts/pipelines/components/graph/dropdown_job_component.vue @@ -1,87 +1,93 @@ <script> - import $ from 'jquery'; - import jobNameComponent from './job_name_component.vue'; - import jobComponent from './job_component.vue'; - import tooltip from '../../../vue_shared/directives/tooltip'; +import $ from 'jquery'; +import JobNameComponent from './job_name_component.vue'; +import JobComponent from './job_component.vue'; +import tooltip from '../../../vue_shared/directives/tooltip'; - /** - * Renders the dropdown for the pipeline graph. - * - * The following object should be provided as `job`: - * - * { - * "id": 4256, - * "name": "test", - * "status": { - * "icon": "icon_status_success", - * "text": "passed", - * "label": "passed", - * "group": "success", - * "details_path": "/root/ci-mock/builds/4256", - * "action": { - * "icon": "retry", - * "title": "Retry", - * "path": "/root/ci-mock/builds/4256/retry", - * "method": "post" - * } - * } - * } - */ - export default { - directives: { - tooltip, - }, +/** + * Renders the dropdown for the pipeline graph. + * + * The following object should be provided as `job`: + * + * { + * "id": 4256, + * "name": "test", + * "status": { + * "icon": "icon_status_success", + * "text": "passed", + * "label": "passed", + * "group": "success", + * "details_path": "/root/ci-mock/builds/4256", + * "action": { + * "icon": "retry", + * "title": "Retry", + * "path": "/root/ci-mock/builds/4256/retry", + * "method": "post" + * } + * } + * } + */ +export default { + directives: { + tooltip, + }, - components: { - jobComponent, - jobNameComponent, - }, + components: { + JobComponent, + JobNameComponent, + }, - props: { - job: { - type: Object, - required: true, - }, + props: { + job: { + type: Object, + required: true, }, + }, - computed: { - tooltipText() { - return `${this.job.name} - ${this.job.status.label}`; - }, + computed: { + tooltipText() { + return `${this.job.name} - ${this.job.status.label}`; }, + }, - mounted() { - this.stopDropdownClickPropagation(); - }, + mounted() { + this.stopDropdownClickPropagation(); + }, - methods: { - /** - * 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. + methods: { + /** + * When the user right clicks or cmd/ctrl + click in the job name or the action icon + * the dropdown should not be closed 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-grouped-pipeline-dropdown a.mini-pipeline-graph-dropdown-item')) - .on('click', (e) => { - e.stopPropagation(); - }); - }, + stopDropdownClickPropagation() { + $( + '.js-grouped-pipeline-dropdown button, .js-grouped-pipeline-dropdown a.mini-pipeline-graph-dropdown-item', + this.$el, + ).on('click', e => { + e.stopPropagation(); + }); + }, + + pipelineActionRequestComplete() { + this.$emit('pipelineActionRequestComplete'); }, - }; + }, +}; </script> <template> - <div class="ci-job-dropdown-container"> + <div class="ci-job-dropdown-container dropdown"> <button v-tooltip type="button" data-toggle="dropdown" data-container="body" class="dropdown-menu-toggle build-content" - :title="tooltipText"> + :title="tooltipText" + > <job-name-component :name="job.name" @@ -98,11 +104,12 @@ <ul> <li v-for="(item, i) in job.jobs" - :key="i"> + :key="i" + > <job-component :job="item" - :is-dropdown="true" css-class-job-name="mini-pipeline-graph-dropdown-item" + @pipelineActionRequestComplete="pipelineActionRequestComplete" /> </li> </ul> diff --git a/app/assets/javascripts/pipelines/components/graph/graph_component.vue b/app/assets/javascripts/pipelines/components/graph/graph_component.vue index ac9ce7e47d6..4ec67f6c01b 100644 --- a/app/assets/javascripts/pipelines/components/graph/graph_component.vue +++ b/app/assets/javascripts/pipelines/components/graph/graph_component.vue @@ -7,7 +7,6 @@ export default { StageColumnComponent, LoadingIcon, }, - props: { isLoading: { type: Boolean, @@ -17,11 +16,6 @@ export default { type: Object, required: true, }, - actionDisabled: { - type: String, - required: false, - default: null, - }, }, computed: { @@ -52,6 +46,10 @@ export default { return className; }, + + refreshPipelineGraph() { + this.$emit('refreshPipelineGraph'); + }, }, }; </script> @@ -75,7 +73,7 @@ export default { :key="stage.name" :stage-connector-class="stageConnectorClass(index, stage)" :is-first-column="isFirstColumn(index)" - :action-disabled="actionDisabled" + @refreshPipelineGraph="refreshPipelineGraph" /> </ul> </div> diff --git a/app/assets/javascripts/pipelines/components/graph/job_component.vue b/app/assets/javascripts/pipelines/components/graph/job_component.vue index c6e5ae6df41..dc16d395bcb 100644 --- a/app/assets/javascripts/pipelines/components/graph/job_component.vue +++ b/app/assets/javascripts/pipelines/components/graph/job_component.vue @@ -1,6 +1,5 @@ <script> import ActionComponent from './action_component.vue'; -import DropdownActionComponent from './dropdown_action_component.vue'; import JobNameComponent from './job_name_component.vue'; import tooltip from '../../../vue_shared/directives/tooltip'; @@ -32,10 +31,8 @@ import tooltip from '../../../vue_shared/directives/tooltip'; export default { components: { ActionComponent, - DropdownActionComponent, JobNameComponent, }, - directives: { tooltip, }, @@ -44,26 +41,12 @@ export default { type: Object, required: true, }, - cssClassJobName: { type: String, required: false, default: '', }, - - isDropdown: { - type: Boolean, - required: false, - default: false, - }, - - actionDisabled: { - type: String, - required: false, - default: null, - }, }, - computed: { status() { return this.job && this.job.status ? this.job.status : {}; @@ -96,6 +79,11 @@ export default { return this.job.status && this.job.status.action && this.job.status.action.path; }, }, + methods: { + pipelineActionRequestComplete() { + this.$emit('pipelineActionRequestComplete'); + }, + }, }; </script> <template> @@ -108,6 +96,7 @@ export default { :class="cssClassJobName" data-container="body" data-html="true" + data-boundary="viewport" class="js-pipeline-graph-job-link" > @@ -120,7 +109,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" @@ -134,19 +123,11 @@ export default { </div> <action-component - v-if="hasAction && !isDropdown" - :tooltip-text="status.action.title" - :link="status.action.path" - :action-icon="status.action.icon" - :button-disabled="actionDisabled" - /> - - <dropdown-action-component - v-if="hasAction && isDropdown" + v-if="hasAction" :tooltip-text="status.action.title" :link="status.action.path" :action-icon="status.action.icon" - :action-method="status.action.method" + @pipelineActionRequestComplete="pipelineActionRequestComplete" /> </div> </template> diff --git a/app/assets/javascripts/pipelines/components/graph/stage_column_component.vue b/app/assets/javascripts/pipelines/components/graph/stage_column_component.vue index f6e6569e15b..f32368947e8 100644 --- a/app/assets/javascripts/pipelines/components/graph/stage_column_component.vue +++ b/app/assets/javascripts/pipelines/components/graph/stage_column_component.vue @@ -29,11 +29,6 @@ export default { required: false, default: '', }, - actionDisabled: { - type: String, - required: false, - default: null, - }, }, methods: { @@ -48,6 +43,10 @@ export default { buildConnnectorClass(index) { return index === 0 && !this.isFirstColumn ? 'left-connector' : ''; }, + + pipelineActionRequestComplete() { + this.$emit('refreshPipelineGraph'); + }, }, }; </script> @@ -74,12 +73,13 @@ export default { v-if="job.size === 1" :job="job" css-class-job-name="build-content" - :action-disabled="actionDisabled" + @pipelineActionRequestComplete="pipelineActionRequestComplete" /> <dropdown-job-component v-if="job.size > 1" :job="job" + @pipelineActionRequestComplete="pipelineActionRequestComplete" /> </li> diff --git a/app/assets/javascripts/pipelines/components/pipeline_url.vue b/app/assets/javascripts/pipelines/components/pipeline_url.vue index ceb4d9ca604..4d965733f95 100644 --- a/app/assets/javascripts/pipelines/components/pipeline_url.vue +++ b/app/assets/javascripts/pipelines/components/pipeline_url.vue @@ -46,7 +46,7 @@ }; </script> <template> - <div class="table-section section-15 hidden-xs hidden-sm pipeline-tags"> + <div class="table-section section-15 d-none d-sm-none d-md-block pipeline-tags"> <a :href="pipeline.path" class="js-pipeline-url-link"> @@ -69,35 +69,35 @@ <span v-if="pipeline.flags.latest" v-tooltip - class="js-pipeline-url-latest label label-success" + class="js-pipeline-url-latest badge badge-success" title="Latest pipeline for this branch"> latest </span> <span v-if="pipeline.flags.yaml_errors" v-tooltip - class="js-pipeline-url-yaml label label-danger" + class="js-pipeline-url-yaml badge badge-danger" :title="pipeline.yaml_errors"> yaml invalid </span> <span v-if="pipeline.flags.failure_reason" v-tooltip - class="js-pipeline-url-failure label label-danger" + class="js-pipeline-url-failure badge badge-danger" :title="pipeline.failure_reason"> error </span> <a v-if="pipeline.flags.auto_devops" tabindex="0" - class="js-pipeline-url-autodevops label label-info autodevops-badge" + class="js-pipeline-url-autodevops badge badge-info autodevops-badge" v-popover="popoverOptions" role="button"> Auto DevOps </a> <span v-if="pipeline.flags.stuck" - class="js-pipeline-url-stuck label label-warning"> + class="js-pipeline-url-stuck badge badge-warning"> stuck </span> </div> diff --git a/app/assets/javascripts/pipelines/components/pipelines_actions.vue b/app/assets/javascripts/pipelines/components/pipelines_actions.vue index 3297af7bde4..e9bc3cf14ca 100644 --- a/app/assets/javascripts/pipelines/components/pipelines_actions.vue +++ b/app/assets/javascripts/pipelines/components/pipelines_actions.vue @@ -63,7 +63,7 @@ <loading-icon v-if="isLoading" /> </button> - <ul class="dropdown-menu dropdown-menu-align-right"> + <ul class="dropdown-menu dropdown-menu-right"> <li v-for="(action, i) in actions" :key="i" diff --git a/app/assets/javascripts/pipelines/components/pipelines_artifacts.vue b/app/assets/javascripts/pipelines/components/pipelines_artifacts.vue index 1b9e0f917a4..31fcc9dd412 100644 --- a/app/assets/javascripts/pipelines/components/pipelines_artifacts.vue +++ b/app/assets/javascripts/pipelines/components/pipelines_artifacts.vue @@ -37,7 +37,7 @@ > </i> </button> - <ul class="dropdown-menu dropdown-menu-align-right"> + <ul class="dropdown-menu dropdown-menu-right"> <li v-for="(artifact, i) in artifacts" :key="i"> diff --git a/app/assets/javascripts/pipelines/components/pipelines_table.vue b/app/assets/javascripts/pipelines/components/pipelines_table.vue index 714aed1333e..4318abe97e0 100644 --- a/app/assets/javascripts/pipelines/components/pipelines_table.vue +++ b/app/assets/javascripts/pipelines/components/pipelines_table.vue @@ -1,7 +1,7 @@ <script> - import DeprecatedModal from '~/vue_shared/components/deprecated_modal.vue'; + import Modal from '~/vue_shared/components/gl_modal.vue'; import { s__, sprintf } from '~/locale'; - import pipelinesTableRowComponent from './pipelines_table_row.vue'; + import PipelinesTableRowComponent from './pipelines_table_row.vue'; import eventHub from '../event_hub'; /** @@ -11,8 +11,8 @@ */ export default { components: { - pipelinesTableRowComponent, - DeprecatedModal, + PipelinesTableRowComponent, + Modal, }, props: { pipelines: { @@ -37,30 +37,19 @@ return { pipelineId: '', endpoint: '', - type: '', + cancelingPipeline: null, }; }, computed: { modalTitle() { - return this.type === 'stop' ? - sprintf(s__('Pipeline|Stop pipeline #%{pipelineId}?'), { - pipelineId: `'${this.pipelineId}'`, - }, false) : - sprintf(s__('Pipeline|Retry pipeline #%{pipelineId}?'), { - pipelineId: `'${this.pipelineId}'`, - }, false); + return sprintf(s__('Pipeline|Stop pipeline #%{pipelineId}?'), { + pipelineId: `${this.pipelineId}`, + }, false); }, modalText() { - return this.type === 'stop' ? - sprintf(s__('Pipeline|You’re about to stop pipeline %{pipelineId}.'), { - pipelineId: `<strong>#${this.pipelineId}</strong>`, - }, false) : - sprintf(s__('Pipeline|You’re about to retry pipeline %{pipelineId}.'), { - pipelineId: `<strong>#${this.pipelineId}</strong>`, - }, false); - }, - primaryButtonLabel() { - return this.type === 'stop' ? s__('Pipeline|Stop pipeline') : s__('Pipeline|Retry pipeline'); + return sprintf(s__('Pipeline|You’re about to stop pipeline %{pipelineId}.'), { + pipelineId: `<strong>#${this.pipelineId}</strong>`, + }, false); }, }, created() { @@ -73,10 +62,10 @@ setModalData(data) { this.pipelineId = data.pipelineId; this.endpoint = data.endpoint; - this.type = data.type; }, onSubmit() { eventHub.$emit('postAction', this.endpoint); + this.cancelingPipeline = this.pipelineId; }, }, }; @@ -119,21 +108,18 @@ :update-graph-dropdown="updateGraphDropdown" :auto-devops-help-path="autoDevopsHelpPath" :view-type="viewType" + :canceling-pipeline="cancelingPipeline" /> - <deprecated-modal + + <modal id="confirmation-modal" - :title="modalTitle" - :text="modalText" - kind="danger" - :primary-button-label="primaryButtonLabel" + :header-title-text="modalTitle" + footer-primary-button-variant="danger" + :footer-primary-button-text="s__('Pipeline|Stop pipeline')" @submit="onSubmit" > - <template - slot="body" - slot-scope="props" - > - <p v-html="props.text"></p> - </template> - </deprecated-modal> + <span v-html="modalText"></span> + </modal> + </div> </template> diff --git a/app/assets/javascripts/pipelines/components/pipelines_table_row.vue b/app/assets/javascripts/pipelines/components/pipelines_table_row.vue index 4cbd67e0372..a3c17479e6f 100644 --- a/app/assets/javascripts/pipelines/components/pipelines_table_row.vue +++ b/app/assets/javascripts/pipelines/components/pipelines_table_row.vue @@ -1,13 +1,15 @@ <script> - /* eslint-disable no-param-reassign */ - import asyncButtonComponent from './async_button.vue'; - import pipelinesActionsComponent from './pipelines_actions.vue'; - import pipelinesArtifactsComponent from './pipelines_artifacts.vue'; - import ciBadge from '../../vue_shared/components/ci_badge_link.vue'; - import pipelineStage from './stage.vue'; - import pipelineUrl from './pipeline_url.vue'; - import pipelinesTimeago from './time_ago.vue'; - import commitComponent from '../../vue_shared/components/commit.vue'; + import eventHub from '../event_hub'; + import PipelinesActionsComponent from './pipelines_actions.vue'; + import PipelinesArtifactsComponent from './pipelines_artifacts.vue'; + import CiBadge from '../../vue_shared/components/ci_badge_link.vue'; + import PipelineStage from './stage.vue'; + import PipelineUrl from './pipeline_url.vue'; + import PipelinesTimeago from './time_ago.vue'; + import CommitComponent from '../../vue_shared/components/commit.vue'; + import LoadingButton from '../../vue_shared/components/loading_button.vue'; + import Icon from '../../vue_shared/components/icon.vue'; + import { PIPELINES_TABLE } from '../constants'; /** * Pipeline table row. @@ -16,14 +18,15 @@ */ export default { components: { - asyncButtonComponent, - pipelinesActionsComponent, - pipelinesArtifactsComponent, - commitComponent, - pipelineStage, - pipelineUrl, - ciBadge, - pipelinesTimeago, + PipelinesActionsComponent, + PipelinesArtifactsComponent, + CommitComponent, + PipelineStage, + PipelineUrl, + CiBadge, + PipelinesTimeago, + LoadingButton, + Icon, }, props: { pipeline: { @@ -43,6 +46,17 @@ type: String, required: true, }, + cancelingPipeline: { + type: String, + required: false, + default: null, + }, + }, + pipelinesTable: PIPELINES_TABLE, + data() { + return { + isRetrying: false, + }; }, computed: { /** @@ -119,8 +133,10 @@ if (this.pipeline.ref) { return Object.keys(this.pipeline.ref).reduce((accumulator, prop) => { if (prop === 'path') { + // eslint-disable-next-line no-param-reassign accumulator.ref_url = this.pipeline.ref[prop]; } else { + // eslint-disable-next-line no-param-reassign accumulator[prop] = this.pipeline.ref[prop]; } return accumulator; @@ -215,6 +231,23 @@ isChildView() { return this.viewType === 'child'; }, + + isCancelling() { + return this.cancelingPipeline === this.pipeline.id; + }, + }, + + methods: { + handleCancelClick() { + eventHub.$emit('openConfirmationModal', { + pipelineId: this.pipeline.id, + endpoint: this.pipeline.cancel_path, + }); + }, + handleRetryClick() { + this.isRetrying = true; + eventHub.$emit('retryPipeline', this.pipeline.retry_path); + }, }, }; </script> @@ -272,6 +305,7 @@ v-for="(stage, index) in pipeline.details.stages" :key="index"> <pipeline-stage + :type="$options.pipelinesTable" :stage="stage" :update-dropdown="updateGraphDropdown" /> @@ -287,7 +321,8 @@ <div v-if="displayPipelineActions" - class="table-section section-20 table-button-footer pipeline-actions"> + class="table-section section-20 table-button-footer pipeline-actions" + > <div class="btn-group table-action-buttons"> <pipelines-actions-component v-if="pipeline.details.manual_actions.length" @@ -296,33 +331,31 @@ <pipelines-artifacts-component v-if="pipeline.details.artifacts.length" - class="hidden-xs hidden-sm" + class="d-none d-sm-none d-md-block" :artifacts="pipeline.details.artifacts" /> - <async-button-component + <loading-button v-if="pipeline.flags.retryable" - :endpoint="pipeline.retry_path" - css-class="js-pipelines-retry-button btn-default btn-retry" - title="Retry" - icon="repeat" - :pipeline-id="pipeline.id" - data-toggle="modal" - data-target="#confirmation-modal" - type="retry" - /> + @click="handleRetryClick" + container-class="js-pipelines-retry-button btn btn-default btn-retry" + :loading="isRetrying" + :disabled="isRetrying" + > + <icon name="repeat" /> + </loading-button> - <async-button-component + <loading-button v-if="pipeline.flags.cancelable" - :endpoint="pipeline.cancel_path" - css-class="js-pipelines-cancel-button btn-remove" - title="Stop" - icon="close" - :pipeline-id="pipeline.id" + @click="handleCancelClick" data-toggle="modal" data-target="#confirmation-modal" - type="stop" - /> + container-class="js-pipelines-cancel-button btn btn-remove" + :loading="isCancelling" + :disabled="isCancelling" + > + <icon name="close" /> + </loading-button> </div> </div> </div> diff --git a/app/assets/javascripts/pipelines/components/stage.vue b/app/assets/javascripts/pipelines/components/stage.vue index 32cf3dba3c3..f9769815796 100644 --- a/app/assets/javascripts/pipelines/components/stage.vue +++ b/app/assets/javascripts/pipelines/components/stage.vue @@ -1,135 +1,157 @@ <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'; +import { PIPELINES_TABLE } from '../constants'; + +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, - }, + type: { + type: String, + required: false, + default: '', + }, + }, + + 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'); + }, + + pipelineActionRequestComplete() { + if (this.type === PIPELINES_TABLE) { + // warn the table to update + eventHub.$emit('refreshPipelinesTable'); + } else { + // close the dropdown in mr widget + $(this.$refs.dropdown).dropdown('toggle'); + } }, - }; + }, +}; </script> <template> @@ -146,6 +168,7 @@ id="stageDropdown" aria-haspopup="true" aria-expanded="false" + ref="dropdown" > <span @@ -168,7 +191,6 @@ > <li - :class="dropdownClass" class="js-builds-dropdown-list scrollable-menu" > @@ -176,8 +198,17 @@ <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" + @pipelineActionRequestComplete="pipelineActionRequestComplete" + /> + </li> </ul> </li> </ul> diff --git a/app/assets/javascripts/pipelines/components/time_ago.vue b/app/assets/javascripts/pipelines/components/time_ago.vue index cd54d26c9d3..79dbdca4010 100644 --- a/app/assets/javascripts/pipelines/components/time_ago.vue +++ b/app/assets/javascripts/pipelines/components/time_ago.vue @@ -75,7 +75,7 @@ </p> <p - class="finished-at hidden-xs hidden-sm" + class="finished-at d-none d-sm-none d-md-block" v-if="hasFinishedTime" > diff --git a/app/assets/javascripts/pipelines/constants.js b/app/assets/javascripts/pipelines/constants.js index b384c7500e7..eaa11a84cb9 100644 --- a/app/assets/javascripts/pipelines/constants.js +++ b/app/assets/javascripts/pipelines/constants.js @@ -1,2 +1,2 @@ -// eslint-disable-next-line import/prefer-default-export export const CANCEL_REQUEST = 'CANCEL_REQUEST'; +export const PIPELINES_TABLE = 'PIPELINES_TABLE'; diff --git a/app/assets/javascripts/pipelines/mixins/pipelines.js b/app/assets/javascripts/pipelines/mixins/pipelines.js index 6d87f75ae8e..30b1eee186d 100644 --- a/app/assets/javascripts/pipelines/mixins/pipelines.js +++ b/app/assets/javascripts/pipelines/mixins/pipelines.js @@ -53,11 +53,15 @@ export default { }); eventHub.$on('postAction', this.postAction); + eventHub.$on('retryPipeline', this.postAction); eventHub.$on('clickedDropdown', this.updateTable); + eventHub.$on('refreshPipelinesTable', this.fetchPipelines); }, beforeDestroy() { eventHub.$off('postAction', this.postAction); + eventHub.$off('retryPipeline', this.postAction); eventHub.$off('clickedDropdown', this.updateTable); + eventHub.$off('refreshPipelinesTable', this.fetchPipelines); }, destroyed() { this.poll.stop(); diff --git a/app/assets/javascripts/pipelines/pipeline_details_bundle.js b/app/assets/javascripts/pipelines/pipeline_details_bundle.js index 900eb7855f4..b49a16a87e6 100644 --- a/app/assets/javascripts/pipelines/pipeline_details_bundle.js +++ b/app/assets/javascripts/pipelines/pipeline_details_bundle.js @@ -25,28 +25,14 @@ export default () => { data() { return { mediator, - actionDisabled: null, }; }, - created() { - eventHub.$on('graphAction', this.postAction); - }, - beforeDestroy() { - eventHub.$off('graphAction', this.postAction); - }, methods: { - postAction(action) { - this.actionDisabled = action; - - this.mediator.service.postAction(action) - .then(() => { - this.mediator.refreshPipeline(); - this.actionDisabled = null; - }) - .catch(() => { - this.actionDisabled = null; - Flash(__('An error occurred while making the request.')); - }); + requestRefreshPipelineGraph() { + // When an action is clicked + // (wether in the dropdown or in the main nodes, we refresh the big graph) + this.mediator.refreshPipeline() + .catch(() => Flash(__('An error occurred while making the request.'))); }, }, render(createElement) { @@ -54,7 +40,9 @@ export default () => { props: { isLoading: this.mediator.state.isLoading, pipeline: this.mediator.store.state.pipeline, - actionDisabled: this.actionDisabled, + }, + on: { + refreshPipelineGraph: this.requestRefreshPipelineGraph, }, }); }, @@ -79,7 +67,8 @@ export default () => { }, methods: { postAction(action) { - this.mediator.service.postAction(action.path) + this.mediator.service + .postAction(action.path) .then(() => this.mediator.refreshPipeline()) .catch(() => Flash(__('An error occurred while making the request.'))); }, diff --git a/app/assets/javascripts/profile/account/components/update_username.vue b/app/assets/javascripts/profile/account/components/update_username.vue index e5de3f69b01..a7a2a7235fd 100644 --- a/app/assets/javascripts/profile/account/components/update_username.vue +++ b/app/assets/javascripts/profile/account/components/update_username.vue @@ -86,7 +86,11 @@ Please update your Git repository remotes as soon as possible.`), <div class="form-group"> <label :for="$options.inputId">{{ s__('Profiles|Path') }}</label> <div class="input-group"> - <div class="input-group-addon">{{ rootUrl }}</div> + <div class="input-group-prepend"> + <div class="input-group-text"> + {{ rootUrl }} + </div> + </div> <input :id="$options.inputId" class="form-control" diff --git a/app/assets/javascripts/project_fork.js b/app/assets/javascripts/project_fork.js index 6fedd94a6a9..f5cd1c3cc3e 100644 --- a/app/assets/javascripts/project_fork.js +++ b/app/assets/javascripts/project_fork.js @@ -4,6 +4,6 @@ export default () => { $('.js-fork-thumbnail').on('click', function forkThumbnailClicked() { if ($(this).hasClass('disabled')) return false; - return $('.js-fork-content').toggle(); + return $('.js-fork-content').toggleClass('hidden'); }); }; diff --git a/app/assets/javascripts/project_label_subscription.js b/app/assets/javascripts/project_label_subscription.js index 72f03b02131..a5e68d1b0a0 100644 --- a/app/assets/javascripts/project_label_subscription.js +++ b/app/assets/javascripts/project_label_subscription.js @@ -39,7 +39,9 @@ export default class ProjectLabelSubscription { const $button = $(button); const originalTitle = $button.attr('data-original-title'); - if (originalTitle) ProjectLabelSubscription.setNewTitle($button, originalTitle, newStatus, newAction); + if (originalTitle) { + ProjectLabelSubscription.setNewTitle($button, originalTitle, newStatus, newAction); + } return button; }); @@ -51,6 +53,6 @@ export default class ProjectLabelSubscription { const actionRegexp = new RegExp(newStatusVerb, 'i'); const newTitle = originalTitle.replace(actionRegexp, newAction); - $button.tooltip('hide').attr('data-original-title', newTitle).tooltip('fixTitle'); + $button.tooltip('hide').attr('data-original-title', newTitle).tooltip('_fixTitle'); } } diff --git a/app/assets/javascripts/project_visibility.js b/app/assets/javascripts/project_visibility.js index 7c95c71e239..a52ac768e57 100644 --- a/app/assets/javascripts/project_visibility.js +++ b/app/assets/javascripts/project_visibility.js @@ -7,7 +7,7 @@ function setVisibilityOptions(namespaceSelector) { const selectedNamespace = namespaceSelector.options[namespaceSelector.selectedIndex]; const { name, visibility, visibilityLevel, showPath, editPath } = selectedNamespace.dataset; - document.querySelectorAll('.visibility-level-setting .radio').forEach((option) => { + document.querySelectorAll('.visibility-level-setting .form-check').forEach((option) => { const optionInput = option.querySelector('input[type=radio]'); const optionValue = optionInput ? optionInput.value : 0; const optionTitle = option.querySelector('.option-title'); diff --git a/app/assets/javascripts/projects/project_new.js b/app/assets/javascripts/projects/project_new.js index 93603dfc14d..888b1d6ce33 100644 --- a/app/assets/javascripts/projects/project_new.js +++ b/app/assets/javascripts/projects/project_new.js @@ -66,8 +66,8 @@ const bindEvents = () => { .on('click', (e) => { e.preventDefault(); }) .popover({ title: $pushNewProjectTipTrigger.data('title'), - placement: 'auto bottom', - html: 'true', + placement: 'bottom', + html: true, content: $('.push-new-project-tip-template').html(), }) .on('shown.bs.popover', () => { 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/components/search.vue b/app/assets/javascripts/projects_dropdown/components/search.vue index 0c46ed184be..7fcd62dcdb8 100644 --- a/app/assets/javascripts/projects_dropdown/components/search.vue +++ b/app/assets/javascripts/projects_dropdown/components/search.vue @@ -46,7 +46,7 @@ <template> <div - class="search-input-container hidden-xs" + class="search-input-container d-none d-sm-block" > <input type="search" 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/prometheus_metrics/prometheus_metrics.js b/app/assets/javascripts/prometheus_metrics/prometheus_metrics.js index 0a60f4845b2..1a75fdd75db 100644 --- a/app/assets/javascripts/prometheus_metrics/prometheus_metrics.js +++ b/app/assets/javascripts/prometheus_metrics/prometheus_metrics.js @@ -31,7 +31,7 @@ export default class PrometheusMetrics { /* eslint-disable class-methods-use-this */ handlePanelToggle(e) { const $toggleBtn = $(e.currentTarget); - const $currentPanelBody = $toggleBtn.closest('.panel').find('.panel-body'); + const $currentPanelBody = $toggleBtn.closest('.card').find('.card-body'); $currentPanelBody.toggleClass('hidden'); if ($toggleBtn.hasClass('fa-caret-down')) { $toggleBtn.removeClass('fa-caret-down').addClass('fa-caret-right'); diff --git a/app/assets/javascripts/registry/components/collapsible_container.vue b/app/assets/javascripts/registry/components/collapsible_container.vue index a03180e80e6..2fc3778820b 100644 --- a/app/assets/javascripts/registry/components/collapsible_container.vue +++ b/app/assets/javascripts/registry/components/collapsible_container.vue @@ -28,11 +28,6 @@ isOpen: false, }; }, - computed: { - clipboardText() { - return `docker pull ${this.repo.location}`; - }, - }, methods: { ...mapActions([ 'fetchRepos', @@ -84,12 +79,12 @@ <clipboard-button v-if="repo.location" - :text="clipboardText" + :text="repo.location" :title="repo.location" css-class="btn-default btn-transparent btn-clipboard" /> - <div class="controls hidden-xs pull-right"> + <div class="controls d-none d-sm-block float-right"> <button v-if="repo.canDelete" type="button" diff --git a/app/assets/javascripts/registry/components/table_registry.vue b/app/assets/javascripts/registry/components/table_registry.vue index ee4eb3581f3..e4a4b3bb129 100644 --- a/app/assets/javascripts/registry/components/table_registry.vue +++ b/app/assets/javascripts/registry/components/table_registry.vue @@ -56,10 +56,6 @@ .catch(() => this.showError(errorMessagesTypes.FETCH_REGISTRY)); }, - clipboardText(text) { - return `docker pull ${text}`; - }, - showError(message) { Flash(errorMessages[message]); }, @@ -89,7 +85,7 @@ <clipboard-button v-if="item.location" :title="item.location" - :text="clipboardText(item.location)" + :text="item.location" css-class="btn-default btn-transparent btn-clipboard" /> </td> @@ -111,14 +107,20 @@ </td> <td> - {{ timeFormated(item.createdAt) }} + <span + v-tooltip + :title="tooltipTitle(item.createdAt)" + data-placement="bottom" + > + {{ timeFormated(item.createdAt) }} + </span> </td> <td class="content"> <button v-if="item.canDelete" type="button" - class="js-delete-registry btn btn-danger hidden-xs pull-right" + class="js-delete-registry btn btn-danger d-none d-sm-block float-right" :title="s__('ContainerRegistry|Remove tag')" :aria-label="s__('ContainerRegistry|Remove tag')" data-container="body" diff --git a/app/assets/javascripts/registry/stores/actions.js b/app/assets/javascripts/registry/stores/actions.js index 795b39bb3dc..593a43c7cc1 100644 --- a/app/assets/javascripts/registry/stores/actions.js +++ b/app/assets/javascripts/registry/stores/actions.js @@ -35,3 +35,6 @@ export const deleteRegistry = ({ commit }, image) => Vue.http.delete(image.destr export const setMainEndpoint = ({ commit }, data) => commit(types.SET_MAIN_ENDPOINT, data); export const toggleLoading = ({ commit }) => commit(types.TOGGLE_MAIN_LOADING); + +// prevent babel-plugin-rewire from generating an invalid default during karma tests +export default () => {}; diff --git a/app/assets/javascripts/registry/stores/getters.js b/app/assets/javascripts/registry/stores/getters.js index 588f479c492..f4923512578 100644 --- a/app/assets/javascripts/registry/stores/getters.js +++ b/app/assets/javascripts/registry/stores/getters.js @@ -1,2 +1,5 @@ export const isLoading = state => state.isLoading; export const repos = state => state.repos; + +// prevent babel-plugin-rewire from generating an invalid default during karma tests +export default () => {}; diff --git a/app/assets/javascripts/right_sidebar.js b/app/assets/javascripts/right_sidebar.js index 2088a49590a..2afcf4626b8 100644 --- a/app/assets/javascripts/right_sidebar.js +++ b/app/assets/javascripts/right_sidebar.js @@ -5,6 +5,7 @@ import _ from 'underscore'; import Cookies from 'js-cookie'; import flash from './flash'; import axios from './lib/utils/axios_utils'; +import { __ } from './locale'; function Sidebar(currentUser) { this.toggleTodo = this.toggleTodo.bind(this); @@ -41,12 +42,14 @@ Sidebar.prototype.addEventListeners = function() { }; Sidebar.prototype.sidebarToggleClicked = function (e, triggered) { - var $allGutterToggleIcons, $this, $thisIcon; + var $allGutterToggleIcons, $this, isExpanded, tooltipLabel; e.preventDefault(); $this = $(this); - $thisIcon = $this.find('i'); + isExpanded = $this.find('i').hasClass('fa-angle-double-right'); + tooltipLabel = isExpanded ? __('Expand sidebar') : __('Collapse sidebar'); $allGutterToggleIcons = $('.js-sidebar-toggle i'); - if ($thisIcon.hasClass('fa-angle-double-right')) { + + if (isExpanded) { $allGutterToggleIcons.removeClass('fa-angle-double-right').addClass('fa-angle-double-left'); $('aside.right-sidebar').removeClass('right-sidebar-expanded').addClass('right-sidebar-collapsed'); $('.layout-page').removeClass('right-sidebar-expanded').addClass('right-sidebar-collapsed'); @@ -57,6 +60,9 @@ Sidebar.prototype.sidebarToggleClicked = function (e, triggered) { if (gl.lazyLoader) gl.lazyLoader.loadCheck(); } + + $this.attr('data-original-title', tooltipLabel); + if (!triggered) { Cookies.set("collapsed_gutter", $('.right-sidebar').hasClass('right-sidebar-collapsed')); } @@ -102,7 +108,7 @@ Sidebar.prototype.todoUpdateDone = function(data) { .attr('title', $el.data(`${attrPrefix}Text`)); if ($el.hasClass('has-tooltip')) { - $el.tooltip('fixTitle'); + $el.tooltip('_fixTitle'); } if ($el.data(`${attrPrefix}Icon`)) { 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/shortcuts_navigation.js b/app/assets/javascripts/shortcuts_navigation.js index a4d10850471..78f7353eb0d 100644 --- a/app/assets/javascripts/shortcuts_navigation.js +++ b/app/assets/javascripts/shortcuts_navigation.js @@ -7,7 +7,7 @@ export default class ShortcutsNavigation extends Shortcuts { super(); Mousetrap.bind('g p', () => findAndFollowLink('.shortcuts-project')); - Mousetrap.bind('g e', () => findAndFollowLink('.shortcuts-project-activity')); + Mousetrap.bind('g v', () => findAndFollowLink('.shortcuts-project-activity')); Mousetrap.bind('g f', () => findAndFollowLink('.shortcuts-tree')); Mousetrap.bind('g c', () => findAndFollowLink('.shortcuts-commits')); Mousetrap.bind('g j', () => findAndFollowLink('.shortcuts-builds')); @@ -16,9 +16,10 @@ export default class ShortcutsNavigation extends Shortcuts { Mousetrap.bind('g i', () => findAndFollowLink('.shortcuts-issues')); Mousetrap.bind('g b', () => findAndFollowLink('.shortcuts-issue-boards')); Mousetrap.bind('g m', () => findAndFollowLink('.shortcuts-merge_requests')); - Mousetrap.bind('g t', () => findAndFollowLink('.shortcuts-todos')); Mousetrap.bind('g w', () => findAndFollowLink('.shortcuts-wiki')); Mousetrap.bind('g s', () => findAndFollowLink('.shortcuts-snippets')); + Mousetrap.bind('g k', () => findAndFollowLink('.shortcuts-kubernetes')); + Mousetrap.bind('g e', () => findAndFollowLink('.shortcuts-environments')); Mousetrap.bind('i', () => findAndFollowLink('.shortcuts-new-issue')); this.enabledHelp.push('.hidden-shortcut.project'); diff --git a/app/assets/javascripts/sidebar/components/assignees/assignee_title.vue b/app/assets/javascripts/sidebar/components/assignees/assignee_title.vue index 5eeb2a41bae..284a258d3c9 100644 --- a/app/assets/javascripts/sidebar/components/assignees/assignee_title.vue +++ b/app/assets/javascripts/sidebar/components/assignees/assignee_title.vue @@ -41,7 +41,7 @@ export default { </i> <a v-if="editable" - class="js-sidebar-dropdown-toggle edit-link pull-right" + class="js-sidebar-dropdown-toggle edit-link float-right" href="#" > {{ __('Edit') }} @@ -49,7 +49,7 @@ export default { <a v-if="showToggle" aria-label="Toggle sidebar" - class="gutter-toggle pull-right js-sidebar-toggle" + class="gutter-toggle float-right js-sidebar-toggle" href="#" role="button" > diff --git a/app/assets/javascripts/sidebar/components/assignees/assignees.vue b/app/assets/javascripts/sidebar/components/assignees/assignees.vue index 1e7f46454bf..5a374d84796 100644 --- a/app/assets/javascripts/sidebar/components/assignees/assignees.vue +++ b/app/assets/javascripts/sidebar/components/assignees/assignees.vue @@ -1,6 +1,12 @@ <script> +import { __ } from '~/locale'; +import tooltip from '~/vue_shared/directives/tooltip'; + export default { name: 'Assignees', + directives: { + tooltip, + }, props: { rootPath: { type: String, @@ -14,6 +20,11 @@ export default { type: Boolean, required: true, }, + issuableType: { + type: String, + require: true, + default: 'issue', + }, }, data() { return { @@ -62,6 +73,12 @@ export default { names.push(`+ ${this.users.length - maxRender} more`); } + if (!this.users.length) { + const emptyTooltipLabel = this.issuableType === 'issue' ? + __('Assignee(s)') : __('Assignee'); + names.push(emptyTooltipLabel); + } + return names.join(', '); }, sidebarAvatarCounter() { @@ -109,9 +126,11 @@ export default { <div> <div class="sidebar-collapsed-icon sidebar-collapsed-user" - :class="{ 'multiple-users': hasMoreThanOneAssignee, 'has-tooltip': hasAssignees }" + :class="{ 'multiple-users': hasMoreThanOneAssignee }" + v-tooltip data-container="body" data-placement="left" + data-boundary="viewport" :title="collapsedTooltipTitle" > <i diff --git a/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.vue b/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.vue index 3c6b9c27814..b04a2eff798 100644 --- a/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.vue +++ b/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.vue @@ -1,9 +1,9 @@ <script> -import Flash from '../../../flash'; +import Flash from '~/flash'; +import eventHub from '~/sidebar/event_hub'; +import Store from '~/sidebar/stores/sidebar_store'; import AssigneeTitle from './assignee_title.vue'; import Assignees from './assignees.vue'; -import Store from '../../stores/sidebar_store'; -import eventHub from '../../event_hub'; export default { name: 'SidebarAssignees', @@ -25,6 +25,11 @@ export default { required: false, default: false, }, + issuableType: { + type: String, + require: true, + default: 'issue', + }, }, data() { return { @@ -90,6 +95,7 @@ export default { :users="store.assignees" :editable="store.editable" @assign-self="assignSelf" + :issuable-type="issuableType" /> </div> </template> diff --git a/app/assets/javascripts/sidebar/components/confidential/confidential_issue_sidebar.vue b/app/assets/javascripts/sidebar/components/confidential/confidential_issue_sidebar.vue index ceb02309959..3f6e2f05396 100644 --- a/app/assets/javascripts/sidebar/components/confidential/confidential_issue_sidebar.vue +++ b/app/assets/javascripts/sidebar/components/confidential/confidential_issue_sidebar.vue @@ -1,15 +1,19 @@ <script> -import Flash from '../../../flash'; +import { __ } from '~/locale'; +import Flash from '~/flash'; +import tooltip from '~/vue_shared/directives/tooltip'; +import Icon from '~/vue_shared/components/icon.vue'; +import eventHub from '~/sidebar/event_hub'; import editForm from './edit_form.vue'; -import Icon from '../../../vue_shared/components/icon.vue'; -import { __ } from '../../../locale'; -import eventHub from '../../event_hub'; export default { components: { editForm, Icon, }, + directives: { + tooltip, + }, props: { isConfidential: { required: true, @@ -33,6 +37,9 @@ export default { confidentialityIcon() { return this.isConfidential ? 'eye-slash' : 'eye'; }, + tooltipLabel() { + return this.isConfidential ? __('Confidential') : __('Not confidential'); + }, }, created() { eventHub.$on('closeConfidentialityForm', this.toggleForm); @@ -65,6 +72,11 @@ export default { <div class="sidebar-collapsed-icon" @click="toggleForm" + v-tooltip + data-container="body" + data-placement="left" + data-boundary="viewport" + :title="tooltipLabel" > <icon :name="confidentialityIcon" @@ -75,7 +87,7 @@ export default { {{ __('Confidentiality') }} <a v-if="isEditable" - class="pull-right confidential-edit" + class="float-right confidential-edit" href="#" @click.prevent="toggleForm" > diff --git a/app/assets/javascripts/sidebar/components/confidential/edit_form.vue b/app/assets/javascripts/sidebar/components/confidential/edit_form.vue index 3783f71a848..4165aa19acf 100644 --- a/app/assets/javascripts/sidebar/components/confidential/edit_form.vue +++ b/app/assets/javascripts/sidebar/components/confidential/edit_form.vue @@ -32,7 +32,7 @@ export default { </script> <template> - <div class="dropdown open"> + <div class="dropdown show"> <div class="dropdown-menu sidebar-item-warning-message"> <div> <p diff --git a/app/assets/javascripts/sidebar/components/lock/edit_form.vue b/app/assets/javascripts/sidebar/components/lock/edit_form.vue index e1e4715826a..d392977e5e2 100644 --- a/app/assets/javascripts/sidebar/components/lock/edit_form.vue +++ b/app/assets/javascripts/sidebar/components/lock/edit_form.vue @@ -41,7 +41,7 @@ export default { </script> <template> - <div class="dropdown open"> + <div class="dropdown show"> <div class="dropdown-menu sidebar-item-warning-message"> <p class="text" diff --git a/app/assets/javascripts/sidebar/components/lock/lock_issue_sidebar.vue b/app/assets/javascripts/sidebar/components/lock/lock_issue_sidebar.vue index e4893451af3..fb69c741dcd 100644 --- a/app/assets/javascripts/sidebar/components/lock/lock_issue_sidebar.vue +++ b/app/assets/javascripts/sidebar/components/lock/lock_issue_sidebar.vue @@ -1,15 +1,22 @@ <script> +import { __ } from '~/locale'; import Flash from '~/flash'; +import tooltip from '~/vue_shared/directives/tooltip'; +import issuableMixin from '~/vue_shared/mixins/issuable'; +import Icon from '~/vue_shared/components/icon.vue'; +import eventHub from '~/sidebar/event_hub'; import editForm from './edit_form.vue'; -import issuableMixin from '../../../vue_shared/mixins/issuable'; -import Icon from '../../../vue_shared/components/icon.vue'; -import eventHub from '../../event_hub'; export default { components: { editForm, Icon, }, + + directives: { + tooltip, + }, + mixins: [issuableMixin], props: { @@ -44,6 +51,10 @@ export default { isLockDialogOpen() { return this.mediator.store.isLockDialogOpen; }, + + tooltipLabel() { + return this.isLocked ? __('Locked') : __('Unlocked'); + }, }, created() { @@ -85,6 +96,11 @@ export default { <div class="sidebar-collapsed-icon" @click="toggleForm" + v-tooltip + data-container="body" + data-placement="left" + data-boundary="viewport" + :title="tooltipLabel" > <icon :name="lockIcon" @@ -97,7 +113,7 @@ export default { {{ sprintf(__('Lock %{issuableDisplayName}'), { issuableDisplayName: issuableDisplayName }) }} <button v-if="isEditable" - class="pull-right lock-edit" + class="float-right lock-edit" type="button" @click.prevent="toggleForm" > diff --git a/app/assets/javascripts/sidebar/components/participants/participants.vue b/app/assets/javascripts/sidebar/components/participants/participants.vue index 006a6d2905d..0a945fc7fd5 100644 --- a/app/assets/javascripts/sidebar/components/participants/participants.vue +++ b/app/assets/javascripts/sidebar/components/participants/participants.vue @@ -1,9 +1,13 @@ <script> - import { __, n__, sprintf } from '../../../locale'; - import loadingIcon from '../../../vue_shared/components/loading_icon.vue'; - import userAvatarImage from '../../../vue_shared/components/user_avatar/user_avatar_image.vue'; + import { __, n__, sprintf } from '~/locale'; + import tooltip from '~/vue_shared/directives/tooltip'; + import loadingIcon from '~/vue_shared/components/loading_icon.vue'; + import userAvatarImage from '~/vue_shared/components/user_avatar/user_avatar_image.vue'; export default { + directives: { + tooltip, + }, components: { loadingIcon, userAvatarImage, @@ -66,13 +70,24 @@ toggleMoreParticipants() { this.isShowingMoreParticipants = !this.isShowingMoreParticipants; }, + onClickCollapsedIcon() { + this.$emit('toggleSidebar'); + }, }, }; </script> <template> <div> - <div class="sidebar-collapsed-icon"> + <div + class="sidebar-collapsed-icon" + v-tooltip + data-container="body" + data-placement="left" + data-boundary="viewport" + :title="participantLabel" + @click="onClickCollapsedIcon" + > <i class="fa fa-users" aria-hidden="true" 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 d69d100a26c..6745c1aafff 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,12 +73,16 @@ <template> <div> - <div class="sidebar-collapsed-icon"> + <div + class="sidebar-collapsed-icon" + @click="onClickCollapsedIcon" + > <span v-tooltip :title="notificationTooltip" data-container="body" data-placement="left" + data-boundary="viewport" > <icon :name="notificationIcon" @@ -71,12 +92,12 @@ /> </span> </div> - <span class="issuable-header-text hide-collapsed pull-left"> + <span class="issuable-header-text hide-collapsed float-left"> {{ __('Notifications') }} </span> <toggle-button ref="toggleButton" - class="pull-right hide-collapsed js-issuable-subscribe-button" + class="float-right hide-collapsed js-issuable-subscribe-button" :is-loading="showLoadingState" :value="subscribed" @change="toggleSubscription" diff --git a/app/assets/javascripts/sidebar/components/time_tracking/collapsed_state.vue b/app/assets/javascripts/sidebar/components/time_tracking/collapsed_state.vue index 3b86f1145d1..209af1ce152 100644 --- a/app/assets/javascripts/sidebar/components/time_tracking/collapsed_state.vue +++ b/app/assets/javascripts/sidebar/components/time_tracking/collapsed_state.vue @@ -1,12 +1,17 @@ <script> - import icon from '../../../vue_shared/components/icon.vue'; - import { abbreviateTime } from '../../../lib/utils/pretty_time'; + import { __, sprintf } from '~/locale'; + import { abbreviateTime } from '~/lib/utils/pretty_time'; + import icon from '~/vue_shared/components/icon.vue'; + import tooltip from '~/vue_shared/directives/tooltip'; export default { name: 'TimeTrackingCollapsedState', components: { icon, }, + directives: { + tooltip, + }, props: { showComparisonState: { type: Boolean, @@ -79,6 +84,21 @@ return ''; }, + timeTrackedTooltipText() { + let title; + if (this.showComparisonState) { + title = __('Time remaining'); + } else if (this.showEstimateOnlyState) { + title = __('Estimated'); + } else if (this.showSpentOnlyState) { + title = __('Time spent'); + } + + return sprintf('%{title}: %{text}', ({ title, text: this.text })); + }, + tooltipText() { + return this.showNoTimeTrackingState ? __('Time tracking') : this.timeTrackedTooltipText; + }, }, methods: { abbreviateTime(timeStr) { @@ -89,7 +109,14 @@ </script> <template> - <div class="sidebar-collapsed-icon"> + <div + class="sidebar-collapsed-icon" + v-tooltip + data-container="body" + data-placement="left" + data-boundary="viewport" + :title="tooltipText" + > <icon name="timer" /> <div class="time-tracking-collapsed-summary"> <div :class="divClass"> diff --git a/app/assets/javascripts/sidebar/components/time_tracking/comparison_pane.vue b/app/assets/javascripts/sidebar/components/time_tracking/comparison_pane.vue index 82c4562f9a9..6f79310b1cc 100644 --- a/app/assets/javascripts/sidebar/components/time_tracking/comparison_pane.vue +++ b/app/assets/javascripts/sidebar/components/time_tracking/comparison_pane.vue @@ -71,7 +71,7 @@ export default { </div> </div> <div class="compare-display-container"> - <div class="compare-display pull-left"> + <div class="compare-display float-left"> <span class="compare-label"> {{ s__('TimeTracking|Spent') }} </span> @@ -79,7 +79,7 @@ export default { {{ timeSpentHumanReadable }} </span> </div> - <div class="compare-display estimated pull-right"> + <div class="compare-display estimated float-right"> <span class="compare-label"> {{ s__('TimeTrackingEstimated|Est') }} </span> 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 71dca498b3d..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, }, @@ -116,7 +116,7 @@ export default { <div class="title hide-collapsed"> {{ __('Time tracking') }} <div - class="help-button pull-right" + class="help-button float-right" v-if="!showHelpState" @click="toggleHelpState(true)" > @@ -127,7 +127,7 @@ export default { </i> </div> <div - class="close-help-button pull-right" + class="close-help-button float-right" v-if="showHelpState" @click="toggleHelpState(false)" > diff --git a/app/assets/javascripts/sidebar/lib/sidebar_move_issue.js b/app/assets/javascripts/sidebar/lib/sidebar_move_issue.js index 1eadebc7004..b267422cd97 100644 --- a/app/assets/javascripts/sidebar/lib/sidebar_move_issue.js +++ b/app/assets/javascripts/sidebar/lib/sidebar_move_issue.js @@ -1,4 +1,5 @@ import $ from 'jquery'; +import _ from 'underscore'; function isValidProjectId(id) { return id > 0; @@ -43,7 +44,7 @@ class SidebarMoveIssue { renderRow: project => ` <li> <a href="#" class="js-move-issue-dropdown-item"> - ${project.name_with_namespace} + ${_.escape(project.name_with_namespace)} </a> </li> `, diff --git a/app/assets/javascripts/sidebar/mount_sidebar.js b/app/assets/javascripts/sidebar/mount_sidebar.js index 9f5d852260e..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'; @@ -27,6 +27,7 @@ function mountAssigneesComponent(mediator) { mediator, field: el.dataset.field, signedIn: el.hasAttribute('data-signed-in'), + issuableType: gl.utils.isInIssuePage() ? 'issue' : 'merge_request', }, }), }); diff --git a/app/assets/javascripts/user_callout.js b/app/assets/javascripts/user_callout.js index 97d5cf96bcb..96dfff77859 100644 --- a/app/assets/javascripts/user_callout.js +++ b/app/assets/javascripts/user_callout.js @@ -15,7 +15,7 @@ export default class UserCallout { init() { if (!this.isCalloutDismissed || this.isCalloutDismissed === 'false') { - $('.js-close-callout').on('click', e => this.dismissCallout(e)); + this.userCalloutBody.find('.js-close-callout').on('click', e => this.dismissCallout(e)); } } @@ -23,12 +23,15 @@ export default class UserCallout { const $currentTarget = $(e.currentTarget); if (this.options.setCalloutPerProject) { - Cookies.set(this.cookieName, 'true', { expires: 365, path: this.userCalloutBody.data('projectPath') }); + Cookies.set(this.cookieName, 'true', { + expires: 365, + path: this.userCalloutBody.data('projectPath'), + }); } else { Cookies.set(this.cookieName, 'true', { expires: 365 }); } - if ($currentTarget.hasClass('close')) { + if ($currentTarget.hasClass('close') || $currentTarget.hasClass('js-close')) { this.userCalloutBody.remove(); } } diff --git a/app/assets/javascripts/users_select.js b/app/assets/javascripts/users_select.js index 520a0b3f424..cd954f75613 100644 --- a/app/assets/javascripts/users_select.js +++ b/app/assets/javascripts/users_select.js @@ -5,6 +5,7 @@ import $ from 'jquery'; import _ from 'underscore'; import axios from './lib/utils/axios_utils'; +import { __ } from './locale'; import ModalStore from './boards/stores/modal_store'; // TODO: remove eventHub hack after code splitting refactor @@ -182,7 +183,7 @@ function UsersSelect(currentUser, els, options = {}) { return axios.put(issueURL, data) .then(({ data }) => { - var user; + var user, tooltipTitle; $dropdown.trigger('loaded.gl.dropdown'); $loading.fadeOut(); if (data.assignee) { @@ -191,15 +192,17 @@ function UsersSelect(currentUser, els, options = {}) { username: data.assignee.username, avatar: data.assignee.avatar_url }; + tooltipTitle = _.escape(user.name); } else { user = { name: 'Unassigned', username: '', avatar: '' }; + tooltipTitle = __('Assignee'); } $value.html(assigneeTemplate(user)); - $collapsedSidebar.attr('title', _.escape(user.name)).tooltip('fixTitle'); + $collapsedSidebar.attr('title', tooltipTitle).tooltip('_fixTitle'); return $collapsedSidebar.html(collapsedAssigneeTemplate(user)); }); }; diff --git a/app/assets/javascripts/vue_merge_request_widget/components/deployment.vue b/app/assets/javascripts/vue_merge_request_widget/components/deployment.vue index 7bef2e97349..1608858da22 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/deployment.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/deployment.vue @@ -109,12 +109,12 @@ export default { rel="noopener noreferrer nofollow" class="deploy-link js-deploy-url" > + {{ deployment.external_url_formatted }} <i class="fa fa-external-link" aria-hidden="true" > </i> - {{ deployment.external_url_formatted }} </a> </template> <span @@ -127,7 +127,7 @@ export default { </span> <loading-button v-if="deployment.stop_url" - container-class="btn btn-default btn-xs prepend-left-default" + container-class="btn btn-default btn-sm prepend-left-default" label="Stop environment" :loading="isStopping" @click="stopEnvironment" diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_author.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_author.vue index cb6e9858736..8338fde61c7 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_author.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_author.vue @@ -2,7 +2,7 @@ import tooltip from '../../vue_shared/directives/tooltip'; export default { - name: 'MRWidgetAuthor', + name: 'MrWidgetAuthor', directives: { tooltip, }, diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_author_time.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_author_time.vue index 8f1fd809a81..644e4b7d81a 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_author_time.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_author_time.vue @@ -1,10 +1,10 @@ <script> - import mrWidgetAuthor from './mr_widget_author.vue'; + import MrWidgetAuthor from './mr_widget_author.vue'; export default { name: 'MRWidgetAuthorTime', components: { - mrWidgetAuthor, + MrWidgetAuthor, }, props: { actionText: { diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.vue index 18ee4c62bf1..51a0fda6555 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.vue @@ -131,7 +131,7 @@ export default { aria-hidden="true"> </i> </button> - <ul class="dropdown-menu dropdown-menu-align-right"> + <ul class="dropdown-menu dropdown-menu-right"> <li> <a class="js-download-email-patches" diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_maintainer_edit.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_maintainer_edit.vue deleted file mode 100644 index f0298f732ea..00000000000 --- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_maintainer_edit.vue +++ /dev/null @@ -1,20 +0,0 @@ -<script> - export default { - name: 'MRWidgetMaintainerEdit', - props: { - maintainerEditAllowed: { - type: Boolean, - default: false, - required: false, - }, - }, - }; -</script> - -<template> - <section class="mr-info-list mr-links"> - <p v-if="maintainerEditAllowed"> - {{ s__("mrWidget|Allows edits from maintainers") }} - </p> - </section> -</template> diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_auto_merge_failed.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_auto_merge_failed.vue index ebaf2b972eb..b404a592234 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_auto_merge_failed.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_auto_merge_failed.vue @@ -42,7 +42,7 @@ @click="refreshWidget" :disabled="isRefreshing" type="button" - class="btn btn-xs btn-default" + class="btn btn-sm btn-default" > <loading-icon v-if="isRefreshing" diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_conflicts.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_conflicts.vue index dad4b0fe49d..5c1500ab801 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_conflicts.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_conflicts.vue @@ -43,13 +43,13 @@ To merge this request, first rebase locally.`) }} <a v-if="mr.canMerge && mr.conflictResolutionPath" :href="mr.conflictResolutionPath" - class="js-resolve-conflicts-button btn btn-default btn-xs" + class="js-resolve-conflicts-button btn btn-default btn-sm" > {{ s__("mrWidget|Resolve conflicts") }} </a> <button v-if="mr.canMerge" - class="js-merge-locally-button btn btn-default btn-xs" + class="js-merge-locally-button btn btn-default btn-sm" data-toggle="modal" data-target="#modal_merge_info" > diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_failed_to_merge.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_failed_to_merge.vue index 602b68ea572..05fecd4de35 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_failed_to_merge.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_failed_to_merge.vue @@ -1,66 +1,70 @@ <script> - import { n__ } from '~/locale'; - import statusIcon from '../mr_widget_status_icon.vue'; - import eventHub from '../../event_hub'; +import { n__ } from '~/locale'; +import statusIcon from '../mr_widget_status_icon.vue'; +import eventHub from '../../event_hub'; - export default { - name: 'MRWidgetFailedToMerge', +export default { + name: 'MRWidgetFailedToMerge', - components: { - statusIcon, - }, + components: { + statusIcon, + }, - props: { - mr: { - type: Object, - required: true, - default: () => ({}), - }, + props: { + mr: { + type: Object, + required: true, + default: () => ({}), }, + }, - data() { - return { - timer: 10, - isRefreshing: false, - }; - }, + data() { + return { + timer: 10, + isRefreshing: false, + intervalId: null, + }; + }, - computed: { - timerText() { - return n__( - 'Refreshing in a second to show the updated status...', - 'Refreshing in %d seconds to show the updated status...', - this.timer, - ); - }, + computed: { + timerText() { + return n__( + 'Refreshing in a second to show the updated status...', + 'Refreshing in %d seconds to show the updated status...', + this.timer, + ); }, + }, - mounted() { - setInterval(() => { - this.updateTimer(); - }, 1000); - }, + mounted() { + this.intervalId = setInterval(this.updateTimer, 1000); + }, - created() { - eventHub.$emit('DisablePolling'); - }, + created() { + eventHub.$emit('DisablePolling'); + }, - methods: { - refresh() { - this.isRefreshing = true; - eventHub.$emit('MRWidgetUpdateRequested'); - eventHub.$emit('EnablePolling'); - }, - updateTimer() { - this.timer = this.timer - 1; + beforeDestroy() { + if (this.intervalId) { + clearInterval(this.intervalId); + } + }, - if (this.timer === 0) { - this.refresh(); - } - }, + methods: { + refresh() { + this.isRefreshing = true; + eventHub.$emit('MRWidgetUpdateRequested'); + eventHub.$emit('EnablePolling'); }, + updateTimer() { + this.timer = this.timer - 1; - }; + if (this.timer === 0) { + this.refresh(); + } + }, + }, +}; </script> <template> <div class="mr-widget-body media"> @@ -94,7 +98,7 @@ </span> <button @click="refresh" - class="btn btn-default btn-xs js-refresh-button" + class="btn btn-default btn-sm js-refresh-button" type="button" > {{ s__("mrWidget|Refresh now") }} diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merge_when_pipeline_succeeds.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merge_when_pipeline_succeeds.vue index 7ff7fc7988a..e8352c362d6 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merge_when_pipeline_succeeds.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merge_when_pipeline_succeeds.vue @@ -1,13 +1,13 @@ <script> import Flash from '../../../flash'; import statusIcon from '../mr_widget_status_icon.vue'; - import mrWidgetAuthor from '../../components/mr_widget_author.vue'; + import MrWidgetAuthor from '../../components/mr_widget_author.vue'; import eventHub from '../../event_hub'; export default { name: 'MRWidgetMergeWhenPipelineSucceeds', components: { - mrWidgetAuthor, + MrWidgetAuthor, statusIcon, }, props: { @@ -94,7 +94,7 @@ :disabled="isCancellingAutoMerge" role="button" href="#" - class="btn btn-xs btn-default js-cancel-auto-merge"> + class="btn btn-sm btn-default js-cancel-auto-merge"> <i v-if="isCancellingAutoMerge" class="fa fa-spinner fa-spin" @@ -129,7 +129,7 @@ :disabled="isRemovingSourceBranch" @click.prevent="removeSourceBranch" role="button" - class="btn btn-xs btn-default js-remove-source-branch" + class="btn btn-sm btn-default js-remove-source-branch" href="#" > <i 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 c1618bc6ea0..8c7e0664559 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: { @@ -116,7 +118,7 @@ <a v-if="mr.canRevertInCurrentMR" v-tooltip - class="btn btn-close btn-xs" + class="btn btn-close btn-sm" href="#modal-revert-commit" data-toggle="modal" data-container="body" @@ -127,7 +129,7 @@ <a v-else-if="mr.revertInForkPath" v-tooltip - class="btn btn-close btn-xs" + class="btn btn-close btn-sm" data-method="post" :href="mr.revertInForkPath" :title="revertTitle" @@ -137,7 +139,7 @@ <a v-if="mr.canCherryPickInCurrentMR" v-tooltip - class="btn btn-default btn-xs" + class="btn btn-default btn-sm" href="#modal-cherry-pick-commit" data-toggle="modal" data-container="body" @@ -148,7 +150,7 @@ <a v-else-if="mr.cherryPickInForkPath" v-tooltip - class="btn btn-default btn-xs" + class="btn btn-default btn-sm" data-method="post" :href="mr.cherryPickInForkPath" :title="cherryPickTitle" @@ -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") }} @@ -175,7 +189,7 @@ @click="removeSourceBranch" :disabled="isMakingRequest" type="button" - class="btn btn-xs btn-default js-remove-branch-button" + class="btn btn-sm btn-default js-remove-branch-button" > {{ s__("mrWidget|Remove Source Branch") }} </button> diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_squash_before_merge.js b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_squash_before_merge.vue index bf8628d18a6..926a3172412 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_squash_before_merge.js +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_squash_before_merge.vue @@ -10,6 +10,6 @@ In EE, the configuration extends this object to add a functioning squash-before- button. */ -export default { - template: '', -}; +<script> + export default {}; +</script> diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/nothing_to_merge.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/nothing_to_merge.vue index 3d9161f6926..086dbabe77e 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/nothing_to_merge.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/nothing_to_merge.vue @@ -18,10 +18,10 @@ export default { <template> <div class="mr-widget-body mr-widget-empty-state"> <div class="row"> - <div class="artwork col-sm-5 col-sm-push-7 col-xs-12 text-center"> + <div class="artwork col-md-5 order-md-last col-12 text-center"> <span v-html="emptyStateSVG"></span> </div> - <div class="text col-sm-7 col-sm-pull-5 col-xs-12"> + <div class="text col-md-7 order-md-first col-12"> <span> Merge requests are a place to propose changes you have made to a project and discuss those changes with others. diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue index 0264625a526..1d1c8ebc179 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue @@ -312,7 +312,7 @@ export default { v-else @click="toggleCommitMessageEditor" :disabled="isMergeButtonDisabled" - class="js-modify-commit-message-button btn btn-default btn-xs" + class="js-modify-commit-message-button btn btn-default btn-sm" type="button"> Modify commit message </button> @@ -329,7 +329,7 @@ export default { class="prepend-top-default commit-message-editor"> <div class="form-group clearfix"> <label - class="control-label" + class="col-form-label" for="commit-message"> Commit message </label> diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/unresolved_discussions.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/unresolved_discussions.vue index a1f7e696795..8ea3f22ecc2 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/unresolved_discussions.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/unresolved_discussions.vue @@ -28,7 +28,7 @@ export default { <a v-if="mr.createIssueToResolveDiscussionsPath" :href="mr.createIssueToResolveDiscussionsPath" - class="btn btn-default btn-xs js-create-issue" + class="btn btn-default btn-sm js-create-issue" > {{ s__("mrWidget|Create an issue to resolve them later") }} </a> 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 44e1a616a19..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-xs 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..15097fa2a3f 100644 --- a/app/assets/javascripts/vue_merge_request_widget/dependencies.js +++ b/app/assets/javascripts/vue_merge_request_widget/dependencies.js @@ -15,13 +15,12 @@ export { default as WidgetHeader } from './components/mr_widget_header.vue'; export { default as WidgetMergeHelp } from './components/mr_widget_merge_help.vue'; export { default as WidgetPipeline } from './components/mr_widget_pipeline.vue'; export { default as Deployment } from './components/deployment.vue'; -export { default as WidgetMaintainerEdit } from './components/mr_widget_maintainer_edit.vue'; export { default as WidgetRelatedLinks } from './components/mr_widget_related_links.vue'; 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'; @@ -41,8 +40,8 @@ export { default as MRWidgetService } from './services/mr_widget_service'; export { default as eventHub } from './event_hub'; export { default as getStateKey } from './stores/get_state_key'; export { default as stateMaps } from './stores/state_maps'; -export { default as SquashBeforeMerge } from './components/states/mr_widget_squash_before_merge'; +export { default as SquashBeforeMerge } from './components/states/mr_widget_squash_before_merge.vue'; export { default as notify } from '../lib/utils/notify'; export { default as SourceBranchRemovalStatus } from './components/source_branch_removal_status.vue'; -export { default as mrWidgetOptions } from './mr_widget_options'; +export { default as mrWidgetOptions } from './mr_widget_options.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.vue index 0be5d9e5a55..f69fe03fcb3 100644 --- a/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.js +++ b/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue @@ -1,18 +1,19 @@ +<script> + import Project from '~/pages/projects/project'; import SmartInterval from '~/smart_interval'; -import Flash from '../flash'; +import createFlash from '../flash'; import { WidgetHeader, WidgetMergeHelp, WidgetPipeline, Deployment, - WidgetMaintainerEdit, WidgetRelatedLinks, MergedState, ClosedState, MergingState, RebaseState, - WipState, + WorkInProgressState, ArchivedState, ConflictsState, NothingToMergeState, @@ -40,10 +41,39 @@ import { setFavicon } from '../lib/utils/common_utils'; export default { el: '#js-vue-mr-widget', name: 'MRWidget', + components: { + 'mr-widget-header': WidgetHeader, + 'mr-widget-merge-help': WidgetMergeHelp, + 'mr-widget-pipeline': WidgetPipeline, + Deployment, + 'mr-widget-related-links': WidgetRelatedLinks, + 'mr-widget-merged': MergedState, + 'mr-widget-closed': ClosedState, + 'mr-widget-merging': MergingState, + 'mr-widget-failed-to-merge': FailedToMerge, + 'mr-widget-wip': WorkInProgressState, + 'mr-widget-archived': ArchivedState, + 'mr-widget-conflicts': ConflictsState, + 'mr-widget-nothing-to-merge': NothingToMergeState, + 'mr-widget-not-allowed': NotAllowedState, + 'mr-widget-missing-branch': MissingBranchState, + 'mr-widget-ready-to-merge': ReadyToMergeState, + 'sha-mismatch': ShaMismatchState, + 'mr-widget-squash-before-merge': SquashBeforeMerge, + 'mr-widget-checking': CheckingState, + 'mr-widget-unresolved-discussions': UnresolvedDiscussionsState, + 'mr-widget-pipeline-blocked': PipelineBlockedState, + 'mr-widget-pipeline-failed': PipelineFailedState, + 'mr-widget-merge-when-pipeline-succeeds': MergeWhenPipelineSucceedsState, + 'mr-widget-auto-merge-failed': AutoMergeFailed, + 'mr-widget-rebase': RebaseState, + SourceBranchRemovalStatus, + }, props: { mrData: { type: Object, required: false, + default: null, }, }, data() { @@ -72,6 +102,13 @@ export default { (!this.mr.isNothingToMergeState && !this.mr.isMergedState); }, }, + created() { + this.initPolling(); + this.bindEventHubListeners(); + }, + mounted() { + this.handleMounted(); + }, methods: { createService(store) { const endpoints = { @@ -99,7 +136,7 @@ export default { cb.call(null, data); } }) - .catch(() => new Flash('Something went wrong. Please try again.')); + .catch(() => createFlash('Something went wrong. Please try again.')); }, initPolling() { this.pollingInterval = new SmartInterval({ @@ -134,7 +171,7 @@ export default { } }) .catch(() => { - new Flash('Something went wrong while fetching the environments for this merge request. Please try again.'); // eslint-disable-line + createFlash('Something went wrong while fetching the environments for this merge request. Please try again.'); // eslint-disable-line }); }, fetchActionsContent() { @@ -147,7 +184,7 @@ export default { Project.initRefSwitcher(); } }) - .catch(() => new Flash('Something went wrong. Please try again.')); + .catch(() => createFlash('Something went wrong. Please try again.')); }, handleNotification(data) { if (data.ci_status === this.mr.ciStatus) return; @@ -202,76 +239,53 @@ export default { this.initDeploymentsPolling(); }, }, - created() { - this.initPolling(); - this.bindEventHubListeners(); - }, - mounted() { - this.handleMounted(); - }, - components: { - 'mr-widget-header': WidgetHeader, - 'mr-widget-merge-help': WidgetMergeHelp, - 'mr-widget-pipeline': WidgetPipeline, - Deployment, - 'mr-widget-maintainer-edit': WidgetMaintainerEdit, - 'mr-widget-related-links': WidgetRelatedLinks, - 'mr-widget-merged': MergedState, - 'mr-widget-closed': ClosedState, - 'mr-widget-merging': MergingState, - 'mr-widget-failed-to-merge': FailedToMerge, - 'mr-widget-wip': WipState, - 'mr-widget-archived': ArchivedState, - 'mr-widget-conflicts': ConflictsState, - 'mr-widget-nothing-to-merge': NothingToMergeState, - 'mr-widget-not-allowed': NotAllowedState, - 'mr-widget-missing-branch': MissingBranchState, - 'mr-widget-ready-to-merge': ReadyToMergeState, - 'mr-widget-sha-mismatch': ShaMismatchState, - 'mr-widget-squash-before-merge': SquashBeforeMerge, - 'mr-widget-checking': CheckingState, - 'mr-widget-unresolved-discussions': UnresolvedDiscussionsState, - 'mr-widget-pipeline-blocked': PipelineBlockedState, - 'mr-widget-pipeline-failed': PipelineFailedState, - 'mr-widget-merge-when-pipeline-succeeds': MergeWhenPipelineSucceedsState, - 'mr-widget-auto-merge-failed': AutoMergeFailed, - 'mr-widget-rebase': RebaseState, - SourceBranchRemovalStatus, - }, - template: ` - <div class="mr-state-widget prepend-top-default"> - <mr-widget-header :mr="mr" /> - <mr-widget-pipeline - v-if="shouldRenderPipelines" - :pipeline="mr.pipeline" - :ci-status="mr.ciStatus" - :has-ci="mr.hasCI" - /> - <deployment - v-for="deployment in mr.deployments" - :key="deployment.id" - :deployment="deployment" +}; +</script> +<template> + <div class="mr-state-widget prepend-top-default"> + <mr-widget-header + :mr="mr" + /> + <mr-widget-pipeline + v-if="shouldRenderPipelines" + :pipeline="mr.pipeline" + :ci-status="mr.ciStatus" + :has-ci="mr.hasCI" + /> + <deployment + v-for="deployment in mr.deployments" + :key="deployment.id" + :deployment="deployment" + /> + <div class="mr-widget-section"> + <component + :is="componentName" + :mr="mr" + :service="service" + /> + + <section + v-if="mr.maintainerEditAllowed" + class="mr-info-list mr-links" + > + {{ s__("mrWidget|Allows edits from maintainers") }} + </section> + + <mr-widget-related-links + v-if="shouldRenderRelatedLinks" + :state="mr.state" + :related-links="mr.relatedLinks" + /> + + <source-branch-removal-status + v-if="shouldRenderSourceBranchRemovalStatus" /> - <div class="mr-widget-section"> - <component - :is="componentName" - :mr="mr" - :service="service" /> - <mr-widget-maintainer-edit - :maintainerEditAllowed="mr.maintainerEditAllowed" /> - <mr-widget-related-links - v-if="shouldRenderRelatedLinks" - :state="mr.state" - :related-links="mr.relatedLinks" /> - <source-branch-removal-status - v-if="shouldRenderSourceBranchRemovalStatus" - /> - </div> - <div - class="mr-widget-footer" - v-if="shouldRenderMergeHelp"> - <mr-widget-merge-help /> - </div> </div> - `, -}; + <div + class="mr-widget-footer" + v-if="shouldRenderMergeHelp" + > + <mr-widget-merge-help /> + </div> + </div> +</template> 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/callout.vue b/app/assets/javascripts/vue_shared/components/callout.vue new file mode 100644 index 00000000000..ccf802c456c --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/callout.vue @@ -0,0 +1,27 @@ +<script> +const calloutVariants = ['danger', 'success', 'info', 'warning']; + +export default { + props: { + category: { + type: String, + required: false, + default: calloutVariants[0], + validator: value => calloutVariants.includes(value), + }, + message: { + type: String, + required: true, + }, + }, +}; +</script> +<template> + <div + :class="`bs-callout bs-callout-${category}`" + role="alert" + aria-live="assertive" + > + {{ message }} + </div> +</template> diff --git a/app/assets/javascripts/vue_shared/components/ci_badge_link.vue b/app/assets/javascripts/vue_shared/components/ci_badge_link.vue index 5324d5dc797..0d64efcbf68 100644 --- a/app/assets/javascripts/vue_shared/components/ci_badge_link.vue +++ b/app/assets/javascripts/vue_shared/components/ci_badge_link.vue @@ -1,52 +1,52 @@ <script> - import ciIcon from './ci_icon.vue'; - import tooltip from '../directives/tooltip'; - /** - * Renders CI Badge link with CI icon and status text based on - * API response shared between all places where it is used. - * - * Receives status object containing: - * status: { - * details_path: "/gitlab-org/gitlab-ce/pipelines/8150156" // url - * group:"running" // used for CSS class - * icon: "icon_status_running" // used to render the icon - * label:"running" // used for potential tooltip - * text:"running" // text rendered - * } - * - * Used in: - * - Pipelines table - first column - * - Jobs table - first column - * - Pipeline show view - header - * - Job show view - header - * - MR widget - */ +import CiIcon from './ci_icon.vue'; +import tooltip from '../directives/tooltip'; +/** + * Renders CI Badge link with CI icon and status text based on + * API response shared between all places where it is used. + * + * Receives status object containing: + * status: { + * details_path: "/gitlab-org/gitlab-ce/pipelines/8150156" // url + * group:"running" // used for CSS class + * icon: "icon_status_running" // used to render the icon + * label:"running" // used for potential tooltip + * text:"running" // text rendered + * } + * + * Used in: + * - Pipelines table - first column + * - Jobs table - first column + * - Pipeline show view - header + * - Job show view - header + * - MR widget + */ - export default { - components: { - ciIcon, +export default { + components: { + CiIcon, + }, + directives: { + tooltip, + }, + props: { + status: { + type: Object, + required: true, }, - directives: { - tooltip, + showText: { + type: Boolean, + required: false, + default: true, }, - props: { - status: { - type: Object, - required: true, - }, - showText: { - type: Boolean, - required: false, - default: true, - }, + }, + computed: { + cssClass() { + const className = this.status.group; + return className ? `ci-status ci-${className}` : 'ci-status'; }, - computed: { - cssClass() { - const className = this.status.group; - return className ? `ci-status ci-${className}` : 'ci-status'; - }, - }, - }; + }, +}; </script> <template> <a diff --git a/app/assets/javascripts/vue_shared/components/ci_icon.vue b/app/assets/javascripts/vue_shared/components/ci_icon.vue index 8fea746f4de..fcab8f571dd 100644 --- a/app/assets/javascripts/vue_shared/components/ci_icon.vue +++ b/app/assets/javascripts/vue_shared/components/ci_icon.vue @@ -1,45 +1,44 @@ <script> - import icon from '../../vue_shared/components/icon.vue'; +import Icon from '../../vue_shared/components/icon.vue'; - /** - * Renders CI icon based on API response shared between all places where it is used. - * - * Receives status object containing: - * status: { - * details_path: "/gitlab-org/gitlab-ce/pipelines/8150156" // url - * group:"running" // used for CSS class - * icon: "icon_status_running" // used to render the icon - * label:"running" // used for potential tooltip - * text:"running" // text rendered - * } - * - * Used in: - * - Pipelines table Badge - * - Pipelines table mini graph - * - Pipeline graph - * - Pipeline show view badge - * - Jobs table - * - Jobs show view header - * - Jobs show view sidebar - */ - export default { - components: { - icon, +/** + * Renders CI icon based on API response shared between all places where it is used. + * + * Receives status object containing: + * status: { + * details_path: "/gitlab-org/gitlab-ce/pipelines/8150156" // url + * group:"running" // used for CSS class + * icon: "icon_status_running" // used to render the icon + * label:"running" // used for potential tooltip + * text:"running" // text rendered + * } + * + * Used in: + * - Pipelines table Badge + * - Pipelines table mini graph + * - Pipeline graph + * - Pipeline show view badge + * - Jobs table + * - Jobs show view header + * - Jobs show view sidebar + */ +export default { + components: { + Icon, + }, + props: { + status: { + type: Object, + required: true, }, - props: { - status: { - type: Object, - required: true, - }, + }, + computed: { + cssClass() { + const status = this.status.group; + return `ci-status-icon ci-status-icon-${status} js-ci-status-icon-${status}`; }, - - computed: { - cssClass() { - const status = this.status.group; - return `ci-status-icon ci-status-icon-${status} js-ci-status-icon-${status}`; - }, - }, - }; + }, +}; </script> <template> <span :class="cssClass"> diff --git a/app/assets/javascripts/vue_shared/components/clipboard_button.vue b/app/assets/javascripts/vue_shared/components/clipboard_button.vue index cab126a7eca..cb2cc3901ad 100644 --- a/app/assets/javascripts/vue_shared/components/clipboard_button.vue +++ b/app/assets/javascripts/vue_shared/components/clipboard_button.vue @@ -1,40 +1,50 @@ <script> - /** - * Falls back to the code used in `copy_to_clipboard.js` - */ - import tooltip from '../directives/tooltip'; +/** + * Falls back to the code used in `copy_to_clipboard.js` + * + * Renders a button with a clipboard icon that copies the content of `data-clipboard-text` + * when clicked. + * + * @example + * <clipboard-button + * title="Copy to clipbard" + * text="Content to be copied" + * css-class="btn-transparent" + * /> + */ +import tooltip from '../directives/tooltip'; - export default { - name: 'ClipboardButton', - directives: { - tooltip, +export default { + name: 'ClipboardButton', + directives: { + tooltip, + }, + props: { + text: { + type: String, + required: true, }, - props: { - text: { - type: String, - required: true, - }, - title: { - type: String, - required: true, - }, - tooltipPlacement: { - type: String, - required: false, - default: 'top', - }, - tooltipContainer: { - type: [String, Boolean], - required: false, - default: false, - }, - cssClass: { - type: String, - required: false, - default: 'btn-default', - }, + title: { + type: String, + required: true, }, - }; + tooltipPlacement: { + type: String, + required: false, + default: 'top', + }, + tooltipContainer: { + type: [String, Boolean], + required: false, + default: false, + }, + cssClass: { + type: String, + required: false, + default: 'btn-default', + }, + }, +}; </script> <template> diff --git a/app/assets/javascripts/vue_shared/components/commit.vue b/app/assets/javascripts/vue_shared/components/commit.vue index b8875d04488..8f250a6c989 100644 --- a/app/assets/javascripts/vue_shared/components/commit.vue +++ b/app/assets/javascripts/vue_shared/components/commit.vue @@ -1,119 +1,111 @@ <script> - import commitIconSvg from 'icons/_icon_commit.svg'; - import userAvatarLink from './user_avatar/user_avatar_link.vue'; - import tooltip from '../directives/tooltip'; - import icon from '../../vue_shared/components/icon.vue'; +import UserAvatarLink from './user_avatar/user_avatar_link.vue'; +import tooltip from '../directives/tooltip'; +import Icon from '../../vue_shared/components/icon.vue'; - export default { - directives: { - tooltip, +export default { + directives: { + tooltip, + }, + components: { + UserAvatarLink, + Icon, + }, + props: { + /** + * Indicates the existance of a tag. + * Used to render the correct icon, if true will render `fa-tag` icon, + * if false will render a svg sprite fork icon + */ + tag: { + type: Boolean, + required: false, + default: false, }, - components: { - userAvatarLink, - icon, + /** + * If provided is used to render the branch name and url. + * Should contain the following properties: + * name + * ref_url + */ + commitRef: { + type: Object, + required: false, + default: () => ({}), + }, + /** + * Used to link to the commit sha. + */ + commitUrl: { + type: String, + required: false, + default: '', }, - props: { - /** - * Indicates the existance of a tag. - * Used to render the correct icon, if true will render `fa-tag` icon, - * if false will render a svg sprite fork icon - */ - tag: { - type: Boolean, - required: false, - default: false, - }, - /** - * If provided is used to render the branch name and url. - * Should contain the following properties: - * name - * ref_url - */ - commitRef: { - type: Object, - required: false, - default: () => ({}), - }, - /** - * Used to link to the commit sha. - */ - commitUrl: { - type: String, - required: false, - default: '', - }, - /** - * Used to show the commit short sha that links to the commit url. - */ - shortSha: { - type: String, - required: false, - default: '', - }, - /** - * If provided shows the commit tile. - */ - title: { - type: String, - required: false, - default: '', - }, - /** - * If provided renders information about the author of the commit. - * When provided should include: - * `avatar_url` to render the avatar icon - * `web_url` to link to user profile - * `username` to render alt and title tags - */ - author: { - type: Object, - required: false, - default: () => ({}), - }, - showBranch: { - type: Boolean, - required: false, - default: true, - }, + /** + * Used to show the commit short sha that links to the commit url. + */ + shortSha: { + type: String, + required: false, + default: '', + }, + /** + * If provided shows the commit tile. + */ + title: { + type: String, + required: false, + default: '', + }, + /** + * If provided renders information about the author of the commit. + * When provided should include: + * `avatar_url` to render the avatar icon + * `web_url` to link to user profile + * `username` to render alt and title tags + */ + author: { + type: Object, + required: false, + default: () => ({}), + }, + showBranch: { + type: Boolean, + required: false, + default: true, }, - computed: { - /** - * Used to verify if all the properties needed to render the commit - * ref section were provided. - * - * @returns {Boolean} - */ - hasCommitRef() { - return this.commitRef && this.commitRef.name && this.commitRef.ref_url; - }, - /** - * Used to verify if all the properties needed to render the commit - * author section were provided. - * - * @returns {Boolean} - */ - hasAuthor() { - return this.author && - this.author.avatar_url && - this.author.path && - this.author.username; - }, - /** - * If information about the author is provided will return a string - * to be rendered as the alt attribute of the img tag. - * - * @returns {String} - */ - userImageAltDescription() { - return this.author && - this.author.username ? `${this.author.username}'s avatar` : null; - }, + }, + computed: { + /** + * Used to verify if all the properties needed to render the commit + * ref section were provided. + * + * @returns {Boolean} + */ + hasCommitRef() { + return this.commitRef && this.commitRef.name && this.commitRef.ref_url; }, - created() { - this.commitIconSvg = commitIconSvg; + /** + * Used to verify if all the properties needed to render the commit + * author section were provided. + * + * @returns {Boolean} + */ + hasAuthor() { + return this.author && this.author.avatar_url && this.author.path && this.author.username; }, - }; + /** + * If information about the author is provided will return a string + * to be rendered as the alt attribute of the img tag. + * + * @returns {String} + */ + userImageAltDescription() { + return this.author && this.author.username ? `${this.author.username}'s avatar` : null; + }, + }, +}; </script> <template> <div class="branch-commit"> @@ -141,11 +133,10 @@ {{ commitRef.name }} </a> </template> - <div - v-html="commitIconSvg" + <icon + name="commit" class="commit-icon js-commit-icon" - > - </div> + /> <a class="commit-sha" diff --git a/app/assets/javascripts/vue_shared/components/content_viewer/viewers/download_viewer.vue b/app/assets/javascripts/vue_shared/components/content_viewer/viewers/download_viewer.vue index 395a71acccf..7b5367ac19b 100644 --- a/app/assets/javascripts/vue_shared/components/content_viewer/viewers/download_viewer.vue +++ b/app/assets/javascripts/vue_shared/components/content_viewer/viewers/download_viewer.vue @@ -42,7 +42,7 @@ export default { target="_blank"> <icon name="download" - css-classes="pull-left append-right-8" + css-classes="float-left append-right-8" :size="16" /> {{ __('Download') }} diff --git a/app/assets/javascripts/vue_shared/components/deprecated_modal.vue b/app/assets/javascripts/vue_shared/components/deprecated_modal.vue index dcf1489b37c..424af5a0293 100644 --- a/app/assets/javascripts/vue_shared/components/deprecated_modal.vue +++ b/app/assets/javascripts/vue_shared/components/deprecated_modal.vue @@ -87,7 +87,7 @@ <div :id="id" class="modal" - :class="id ? '' : 'show'" + :class="id ? '' : 'd-block'" role="dialog" tabindex="-1" > @@ -99,12 +99,12 @@ <div class="modal-content"> <div class="modal-header"> <slot name="header"> - <h4 class="modal-title pull-left"> + <h4 class="modal-title float-left"> {{ title }} </h4> <button type="button" - class="close pull-right" + class="close float-right" @click="emitCancel($event)" data-dismiss="modal" aria-label="Close" @@ -166,7 +166,7 @@ </div> <div v-if="!id" - class="modal-backdrop fade in" + class="modal-backdrop fade show" > </div> </div> diff --git a/app/assets/javascripts/vue_shared/components/expand_button.vue b/app/assets/javascripts/vue_shared/components/expand_button.vue index c943c8d98a4..9295be3e2b2 100644 --- a/app/assets/javascripts/vue_shared/components/expand_button.vue +++ b/app/assets/javascripts/vue_shared/components/expand_button.vue @@ -1,33 +1,33 @@ <script> - import { __ } from '~/locale'; - /** - * Port of detail_behavior expand button. - * - * @example - * <expand-button> - * <template slot="expanded"> - * Text goes here. - * </template> - * </expand-button> - */ - export default { - name: 'ExpandButton', - data() { - return { - isCollapsed: true, - }; +import { __ } from '~/locale'; +/** + * Port of detail_behavior expand button. + * + * @example + * <expand-button> + * <template slot="expanded"> + * Text goes here. + * </template> + * </expand-button> + */ +export default { + name: 'ExpandButton', + data() { + return { + isCollapsed: true, + }; + }, + computed: { + ariaLabel() { + return __('Click to expand text'); }, - computed: { - ariaLabel() { - return __('Click to expand text'); - }, + }, + methods: { + onClick() { + this.isCollapsed = !this.isCollapsed; }, - methods: { - onClick() { - this.isCollapsed = !this.isCollapsed; - }, - }, - }; + }, +}; </script> <template> <span> diff --git a/app/assets/javascripts/vue_shared/components/file_icon.vue b/app/assets/javascripts/vue_shared/components/file_icon.vue index ee1c3498748..be2755452e2 100644 --- a/app/assets/javascripts/vue_shared/components/file_icon.vue +++ b/app/assets/javascripts/vue_shared/components/file_icon.vue @@ -1,9 +1,9 @@ <script> - import getIconForFile from './file_icon/file_icon_map'; - import loadingIcon from '../../vue_shared/components/loading_icon.vue'; - import icon from '../../vue_shared/components/icon.vue'; +import getIconForFile from './file_icon/file_icon_map'; +import loadingIcon from '../../vue_shared/components/loading_icon.vue'; +import icon from '../../vue_shared/components/icon.vue'; - /* This is a re-usable vue component for rendering a svg sprite +/* This is a re-usable vue component for rendering a svg sprite icon Sample configuration: @@ -15,60 +15,60 @@ /> */ - export default { - components: { - loadingIcon, - icon, +export default { + components: { + loadingIcon, + icon, + }, + props: { + fileName: { + type: String, + required: true, }, - props: { - fileName: { - type: String, - required: true, - }, - folder: { - type: Boolean, - required: false, - default: false, - }, + folder: { + type: Boolean, + required: false, + default: false, + }, - opened: { - type: Boolean, - required: false, - default: false, - }, + opened: { + type: Boolean, + required: false, + default: false, + }, - loading: { - type: Boolean, - required: false, - default: false, - }, + loading: { + type: Boolean, + required: false, + default: false, + }, - size: { - type: Number, - required: false, - default: 16, - }, + size: { + type: Number, + required: false, + default: 16, + }, - cssClasses: { - type: String, - required: false, - default: '', - }, + cssClasses: { + type: String, + required: false, + default: '', + }, + }, + computed: { + spriteHref() { + const iconName = getIconForFile(this.fileName) || 'file'; + return `${gon.sprite_file_icons}#${iconName}`; + }, + folderIconName() { + return this.opened ? 'folder-open' : 'folder'; }, - computed: { - spriteHref() { - const iconName = getIconForFile(this.fileName) || 'file'; - return `${gon.sprite_file_icons}#${iconName}`; - }, - folderIconName() { - return this.opened ? 'folder-open' : 'folder'; - }, - iconSizeClass() { - return this.size ? `s${this.size}` : ''; - }, + iconSizeClass() { + return this.size ? `s${this.size}` : ''; }, - }; + }, +}; </script> <template> <span> @@ -82,6 +82,7 @@ v-if="!loading && folder" :name="folderIconName" :size="size" + css-classes="folder-icon" /> <loading-icon v-if="loading" diff --git a/app/assets/javascripts/vue_shared/components/gl_modal.vue b/app/assets/javascripts/vue_shared/components/gl_modal.vue index f28e5e2715d..d5d5a7d3798 100644 --- a/app/assets/javascripts/vue_shared/components/gl_modal.vue +++ b/app/assets/javascripts/vue_shared/components/gl_modal.vue @@ -53,6 +53,11 @@ export default { <div class="modal-content"> <div class="modal-header"> <slot name="header"> + <h4 class="modal-title"> + <slot name="title"> + {{ headerTitleText }} + </slot> + </h4> <button type="button" class="close js-modal-close-action" @@ -62,11 +67,6 @@ export default { > <span aria-hidden="true">×</span> </button> - <h4 class="modal-title"> - <slot name="title"> - {{ headerTitleText }} - </slot> - </h4> </slot> </div> diff --git a/app/assets/javascripts/vue_shared/components/header_ci_component.vue b/app/assets/javascripts/vue_shared/components/header_ci_component.vue index a0cd0cbd200..ca17fa06a00 100644 --- a/app/assets/javascripts/vue_shared/components/header_ci_component.vue +++ b/app/assets/javascripts/vue_shared/components/header_ci_component.vue @@ -1,78 +1,78 @@ <script> - import ciIconBadge from './ci_badge_link.vue'; - import loadingIcon from './loading_icon.vue'; - import timeagoTooltip from './time_ago_tooltip.vue'; - import tooltip from '../directives/tooltip'; - import userAvatarImage from './user_avatar/user_avatar_image.vue'; +import CiIconBadge from './ci_badge_link.vue'; +import LoadingIcon from './loading_icon.vue'; +import TimeagoTooltip from './time_ago_tooltip.vue'; +import tooltip from '../directives/tooltip'; +import UserAvatarImage from './user_avatar/user_avatar_image.vue'; - /** - * Renders header component for job and pipeline page based on UI mockups - * - * Used in: - * - job show page - * - pipeline show page - */ - export default { - components: { - ciIconBadge, - loadingIcon, - timeagoTooltip, - userAvatarImage, +/** + * Renders header component for job and pipeline page based on UI mockups + * + * Used in: + * - job show page + * - pipeline show page + */ +export default { + components: { + CiIconBadge, + LoadingIcon, + TimeagoTooltip, + UserAvatarImage, + }, + directives: { + tooltip, + }, + props: { + status: { + type: Object, + required: true, }, - directives: { - tooltip, + itemName: { + type: String, + required: true, }, - props: { - status: { - type: Object, - required: true, - }, - itemName: { - type: String, - required: true, - }, - itemId: { - type: Number, - required: true, - }, - time: { - type: String, - required: true, - }, - user: { - type: Object, - required: false, - default: () => ({}), - }, - actions: { - type: Array, - required: false, - default: () => [], - }, - hasSidebarButton: { - type: Boolean, - required: false, - default: false, - }, - shouldRenderTriggeredLabel: { - type: Boolean, - required: false, - default: true, - }, + itemId: { + type: Number, + required: true, }, + time: { + type: String, + required: true, + }, + user: { + type: Object, + required: false, + default: () => ({}), + }, + actions: { + type: Array, + required: false, + default: () => [], + }, + hasSidebarButton: { + type: Boolean, + required: false, + default: false, + }, + shouldRenderTriggeredLabel: { + type: Boolean, + required: false, + default: true, + }, + }, - computed: { - userAvatarAltText() { - return `${this.user.name}'s avatar`; - }, + computed: { + userAvatarAltText() { + return `${this.user.name}'s avatar`; }, + }, - methods: { - onClickAction(action) { - this.$emit('actionClicked', action); - }, + methods: { + onClickAction(action) { + this.$emit('actionClicked', action); }, - }; + }, +}; </script> <template> @@ -163,8 +163,8 @@ <button v-if="hasSidebarButton" type="button" - class="btn btn-default visible-xs-block -visible-sm-block sidebar-toggle-btn js-sidebar-build-toggle js-sidebar-build-toggle-header" + class="btn btn-default d-block d-sm-none d-md-none +sidebar-toggle-btn js-sidebar-build-toggle js-sidebar-build-toggle-header" aria-label="Toggle Sidebar" id="toggleSidebar" > diff --git a/app/assets/javascripts/vue_shared/components/icon.vue b/app/assets/javascripts/vue_shared/components/icon.vue index 6a2e05000e1..c42c4a1fbe7 100644 --- a/app/assets/javascripts/vue_shared/components/icon.vue +++ b/app/assets/javascripts/vue_shared/components/icon.vue @@ -1,85 +1,88 @@ <script> +/* This is a re-usable vue component for rendering a svg sprite + icon - /* This is a re-usable vue component for rendering a svg sprite - icon + Sample configuration: - Sample configuration: + <icon + name="retry" + :size="32" + css-classes="top" + /> - <icon - name="retry" - :size="32" - css-classes="top" - /> +*/ +// only allow classes in images.scss e.g. s12 +const validSizes = [8, 12, 16, 18, 24, 32, 48, 72]; - */ - // only allow classes in images.scss e.g. s12 - const validSizes = [8, 12, 16, 18, 24, 32, 48, 72]; - - export default { - props: { - name: { - type: String, - required: true, - }, +export default { + props: { + name: { + type: String, + required: true, + }, - size: { - type: Number, - required: false, - default: 16, - validator(value) { - return validSizes.includes(value); - }, + size: { + type: Number, + required: false, + default: 16, + validator(value) { + return validSizes.includes(value); }, + }, - cssClasses: { - type: String, - required: false, - default: '', - }, + cssClasses: { + type: String, + required: false, + default: '', + }, - width: { - type: Number, - required: false, - default: null, - }, + width: { + type: Number, + required: false, + default: null, + }, - height: { - type: Number, - required: false, - default: null, - }, + height: { + type: Number, + required: false, + default: null, + }, - y: { - type: Number, - required: false, - default: null, - }, + y: { + type: Number, + required: false, + default: null, + }, - x: { - type: Number, - required: false, - default: null, - }, + x: { + type: Number, + required: false, + default: null, }, + }, - computed: { - spriteHref() { - return `${gon.sprite_icons}#${this.name}`; - }, - iconSizeClass() { - return this.size ? `s${this.size}` : ''; - }, + computed: { + spriteHref() { + return `${gon.sprite_icons}#${this.name}`; + }, + iconTestClass() { + return `ic-${this.name}`; + }, + iconSizeClass() { + return this.size ? `s${this.size}` : ''; }, - }; + }, +}; </script> <template> <svg - :class="[iconSizeClass, cssClasses]" + :class="[iconSizeClass, iconTestClass, cssClasses]" :width="width" :height="height" :x="x" - :y="y"> + :y="y" + > <use v-bind="{ 'xlink:href':spriteHref }" /> </svg> </template> diff --git a/app/assets/javascripts/vue_shared/components/identicon.vue b/app/assets/javascripts/vue_shared/components/identicon.vue index 0a30f467b08..23010f40f26 100644 --- a/app/assets/javascripts/vue_shared/components/identicon.vue +++ b/app/assets/javascripts/vue_shared/components/identicon.vue @@ -17,7 +17,7 @@ export default { }, computed: { /** - * This method is based on app/helpers/application_helper.rb#project_identicon + * This method is based on app/helpers/avatars_helper.rb#project_identicon */ identiconStyles() { const allowedColors = [ diff --git a/app/assets/javascripts/vue_shared/components/loading_button.vue b/app/assets/javascripts/vue_shared/components/loading_button.vue index e832d94d32f..88c13a1f340 100644 --- a/app/assets/javascripts/vue_shared/components/loading_button.vue +++ b/app/assets/javascripts/vue_shared/components/loading_button.vue @@ -70,12 +70,14 @@ /> </transition> <transition name="fade"> - <span - v-if="label" - class="js-loading-button-label" - > - {{ label }} - </span> + <slot> + <span + v-if="label" + class="js-loading-button-label" + > + {{ label }} + </span> + </slot> </transition> </button> </template> diff --git a/app/assets/javascripts/vue_shared/components/markdown/toolbar.vue b/app/assets/javascripts/vue_shared/components/markdown/toolbar.vue index c0ee88bbf72..d63318f3da6 100644 --- a/app/assets/javascripts/vue_shared/components/markdown/toolbar.vue +++ b/app/assets/javascripts/vue_shared/components/markdown/toolbar.vue @@ -111,7 +111,7 @@ Attach a file </button> <button - class="btn btn-default btn-xs hide button-cancel-uploading-files" + class="btn btn-default btn-sm hide button-cancel-uploading-files" type="button" > Cancel diff --git a/app/assets/javascripts/vue_shared/components/navigation_tabs.vue b/app/assets/javascripts/vue_shared/components/navigation_tabs.vue index b33a0101dbf..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"> @@ -67,7 +67,7 @@ <span v-if="shouldRenderBadge(tab.count)" - class="badge" + class="badge badge-pill" > {{ tab.count }} </span> diff --git a/app/assets/javascripts/vue_shared/components/notes/placeholder_note.vue b/app/assets/javascripts/vue_shared/components/notes/placeholder_note.vue index 50b1508691b..eccba61a8c0 100644 --- a/app/assets/javascripts/vue_shared/components/notes/placeholder_note.vue +++ b/app/assets/javascripts/vue_shared/components/notes/placeholder_note.vue @@ -54,7 +54,7 @@ <div class="note-header"> <div class="note-header-info"> <a :href="getUserData.path"> - <span class="hidden-xs">{{ getUserData.name }}</span> + <span class="d-none d-sm-block">{{ getUserData.name }}</span> <span class="note-headline-light">@{{ getUserData.username }}</span> </a> </div> diff --git a/app/assets/javascripts/vue_shared/components/sidebar/date_picker.vue b/app/assets/javascripts/vue_shared/components/sidebar/date_picker.vue index 3fcacd156c5..71ec34f2c7a 100644 --- a/app/assets/javascripts/vue_shared/components/sidebar/date_picker.vue +++ b/app/assets/javascripts/vue_shared/components/sidebar/date_picker.vue @@ -116,7 +116,7 @@ v-if="isLoading" :inline="true" /> - <div class="pull-right"> + <div class="float-right"> <button v-if="editable && !editing" type="button" diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select/base.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/base.vue index 5ede53d8d01..70b46a9c2bb 100644 --- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select/base.vue +++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/base.vue @@ -1,4 +1,5 @@ <script> +import $ from 'jquery'; import { __ } from '~/locale'; import LabelsSelect from '~/labels_select'; import LoadingIcon from '../../loading_icon.vue'; @@ -98,11 +99,18 @@ export default { this.labelsDropdown = new LabelsSelect(this.$refs.dropdownButton, { handleClick: this.handleClick, }); + $(this.$refs.dropdown).on('hidden.gl.dropdown', this.handleDropdownHidden); }, methods: { handleClick(label) { this.$emit('onLabelClick', label); }, + handleCollapsedValueClick() { + this.$emit('toggleCollapse'); + }, + handleDropdownHidden() { + this.$emit('onDropdownClose'); + }, }, }; </script> @@ -112,6 +120,7 @@ export default { <dropdown-value-collapsed v-if="showCreate" :labels="context.labels" + @onValueClick="handleCollapsedValueClick" /> <dropdown-title :can-edit="canEdit" @@ -133,7 +142,10 @@ export default { :name="hiddenInputName" :label="label" /> - <div class="dropdown"> + <div + class="dropdown" + ref="dropdown" + > <dropdown-button :ability-name="abilityName" :field-name="hiddenInputName" diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_create_label.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_create_label.vue index 34a07f33a23..3c400afdc1d 100644 --- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_create_label.vue +++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_create_label.vue @@ -77,13 +77,13 @@ export default { <div class="clearfix"> <button type="button" - class="btn btn-primary pull-left js-new-label-btn disabled" + class="btn btn-primary float-left js-new-label-btn disabled" > {{ __('Create') }} </button> <button type="button" - class="btn btn-default pull-right js-cancel-label-btn" + class="btn btn-default float-right js-cancel-label-btn" > {{ __('Cancel') }} </button> diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_title.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_title.vue index 7da82e90e29..9ac32ff13c6 100644 --- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_title.vue +++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_title.vue @@ -21,7 +21,7 @@ export default { </i> <button type="button" - class="edit-link btn btn-blank pull-right js-sidebar-dropdown-toggle" + class="edit-link btn btn-blank float-right js-sidebar-dropdown-toggle" > {{ __('Edit') }} </button> diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed.vue index 5cf728fe050..68fa2ab8d01 100644 --- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed.vue +++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed.vue @@ -26,6 +26,11 @@ export default { return labelsString; }, }, + methods: { + handleClick() { + this.$emit('onValueClick'); + }, + }, }; </script> @@ -36,6 +41,7 @@ export default { data-placement="left" data-container="body" :title="labelsList" + @click="handleClick" > <i aria-hidden="true" diff --git a/app/assets/javascripts/vue_shared/components/sidebar/toggle_sidebar.vue b/app/assets/javascripts/vue_shared/components/sidebar/toggle_sidebar.vue index 8211d425b1f..de6f8c32e74 100644 --- a/app/assets/javascripts/vue_shared/components/sidebar/toggle_sidebar.vue +++ b/app/assets/javascripts/vue_shared/components/sidebar/toggle_sidebar.vue @@ -1,18 +1,29 @@ <script> - export default { - name: 'ToggleSidebar', - props: { - collapsed: { - type: Boolean, - required: true, - }, +import { __ } from '~/locale'; +import tooltip from '~/vue_shared/directives/tooltip'; + +export default { + name: 'ToggleSidebar', + directives: { + tooltip, + }, + props: { + collapsed: { + type: Boolean, + required: true, + }, + }, + computed: { + tooltipLabel() { + return this.collapsed ? __('Expand sidebar') : __('Collapse sidebar'); }, - methods: { - toggle() { - this.$emit('toggle'); - }, + }, + methods: { + toggle() { + this.$emit('toggle'); }, - }; + }, +}; </script> <template> @@ -20,6 +31,10 @@ type="button" class="btn btn-blank gutter-toggle btn-sidebar-action" @click="toggle" + v-tooltip + data-container="body" + data-placement="left" + :title="tooltipLabel" > <i aria-label="toggle collapse" diff --git a/app/assets/javascripts/vue_shared/components/time_ago_tooltip.vue b/app/assets/javascripts/vue_shared/components/time_ago_tooltip.vue index bec4e7c99b6..368eeb6c453 100644 --- a/app/assets/javascripts/vue_shared/components/time_ago_tooltip.vue +++ b/app/assets/javascripts/vue_shared/components/time_ago_tooltip.vue @@ -40,7 +40,7 @@ export default { :class="cssClass" :title="tooltipTitle(time)" :data-placement="tooltipPlacement" - data-container="body"> - {{ timeFormated(time) }} + data-container="body" + v-text="timeFormated(time)"> </time> </template> diff --git a/app/assets/javascripts/vue_shared/directives/popover.js b/app/assets/javascripts/vue_shared/directives/popover.js index eb35294906b..c913bc34c68 100644 --- a/app/assets/javascripts/vue_shared/directives/popover.js +++ b/app/assets/javascripts/vue_shared/directives/popover.js @@ -17,6 +17,6 @@ export default { }, unbind(el) { - $(el).popover('destroy'); + $(el).popover('dispose'); }, }; diff --git a/app/assets/javascripts/vue_shared/directives/tooltip.js b/app/assets/javascripts/vue_shared/directives/tooltip.js index b7f7e9fec15..155e6b6698a 100644 --- a/app/assets/javascripts/vue_shared/directives/tooltip.js +++ b/app/assets/javascripts/vue_shared/directives/tooltip.js @@ -6,10 +6,10 @@ export default { }, componentUpdated(el) { - $(el).tooltip('fixTitle'); + $(el).tooltip('_fixTitle'); }, unbind(el) { - $(el).tooltip('destroy'); + $(el).tooltip('dispose'); }, }; diff --git a/app/assets/javascripts/vue_shared/models/label.js b/app/assets/javascripts/vue_shared/models/label.js index 70b9efe0c68..d29c7fe973a 100644 --- a/app/assets/javascripts/vue_shared/models/label.js +++ b/app/assets/javascripts/vue_shared/models/label.js @@ -1,4 +1,4 @@ -class ListLabel { +export default class ListLabel { constructor(obj) { this.id = obj.id; this.title = obj.title; diff --git a/app/assets/stylesheets/bootstrap_migration.scss b/app/assets/stylesheets/bootstrap_migration.scss new file mode 100644 index 00000000000..3b7ee5c73e6 --- /dev/null +++ b/app/assets/stylesheets/bootstrap_migration.scss @@ -0,0 +1,149 @@ +/* + * Scss to help with bootstrap 3 to 4 migration + */ + +$text-color: $gl-text-color; + +$brand-primary: $gl-primary; +$brand-success: $gl-success; +$brand-info: $gl-info; +$brand-warning: $gl-warning; +$brand-danger: $gl-danger; + +$border-radius-base: 3px !default; + +$modal-body-bg: $white-light; +$input-border: $border-color; +$input-border-focus: $focus-border-color; + +$padding-base-vertical: $gl-vert-padding; +$padding-base-horizontal: $gl-padding; + +html { + // Override default font size used in bs4 + font-size: 14px; +} + +button, +html [type="button"], +[type="reset"], +[type="submit"] { + // Override bootstrap reboot + -webkit-appearance: inherit; +} + +[role="button"] { + cursor: pointer; +} + +a { + color: $gl-link-color; +} + +.form-group.row .col-form-label { + // Bootstrap 4 aligns labels to the left + // for horizontal forms + @include media-breakpoint-up(md) { + text-align: right; + } +} + +table { + // Remove any table border lines + border-spacing: 0; +} + +.tooltip { + // Fix bootstrap4 bug whereby tooltips flicker when they are hovered over their borders + pointer-events: none; +} + +.popover { + font-size: 14px; +} + +@each $breakpoint in map-keys($grid-breakpoints) { + @include media-breakpoint-up($breakpoint) { + $infix: breakpoint-infix($breakpoint, $grid-breakpoints); + + .d#{$infix}-table-header-group { display: table-header-group !important; } + } +} + +.text-secondary { + // Override Bootstrap's light secondary color + // We have to use !important because bootstrap has that set as well + color: $gl-text-color-secondary !important; +} + +// Polyfill deprecated selectors + +.hidden { + display: none !important; + visibility: hidden !important; +} + +.hide { + display: none; +} + +.dropdown-toggle::after { + // Remove bootstrap's dropdown caret + display: none; +} + +.badge { + padding: 4px 5px; + font-size: 12px; + font-style: normal; + font-weight: $gl-font-weight-normal; + display: inline-block; + + &.badge-gray { + background-color: $label-gray-bg; + color: $gl-text-color; + text-shadow: none; + } + + &.badge-inverse { + background-color: $label-inverse-bg; + } +} + +.divider { + @extend .dropdown-divider; +} + +.info-well { + background: $theme-gray-50; + color: $gl-text-color; + border: 1px solid $border-color; + border-radius: 4px; + margin-bottom: 16px; + + .well-segment { + padding: 16px; + + &:not(:last-of-type) { + border-bottom: 1px solid $well-inner-border; + } + } +} + +.card { + &.card-without-border { + @extend .border-0; + } + + &.card-without-margin { + margin: 0; + } + + &.bg-light { + @extend .border-0; + } +} + +.nav-tabs .nav-link { + border: 0; +} 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 2fccfa4011c..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 'framework/tw_bootstrap_variables'; -@import 'framework/tw_bootstrap'; -@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/avatar.scss b/app/assets/stylesheets/framework/avatar.scss index 077d0424093..c1ec11e434a 100644 --- a/app/assets/stylesheets/framework/avatar.scss +++ b/app/assets/stylesheets/framework/avatar.scss @@ -31,7 +31,7 @@ .avatar { @extend .avatar-circle; - @include transition-property(none); + transition-property: none; width: 40px; height: 40px; diff --git a/app/assets/stylesheets/framework/badges.scss b/app/assets/stylesheets/framework/badges.scss index 6bbe32df772..57df9b969c3 100644 --- a/app/assets/stylesheets/framework/badges.scss +++ b/app/assets/stylesheets/framework/badges.scss @@ -1,4 +1,4 @@ -.badge { +.badge.badge-pill { font-weight: $gl-font-weight-normal; background-color: $badge-bg; color: $badge-color; diff --git a/app/assets/stylesheets/framework/banner.scss b/app/assets/stylesheets/framework/banner.scss index 02f3896d591..71bbab2065d 100644 --- a/app/assets/stylesheets/framework/banner.scss +++ b/app/assets/stylesheets/framework/banner.scss @@ -23,7 +23,7 @@ border-bottom: 1px solid $border-color; } - @media (max-width: $screen-xs-max) { + @include media-breakpoint-down(xs) { justify-content: center; flex-direction: column; align-items: center; diff --git a/app/assets/stylesheets/framework/blank.scss b/app/assets/stylesheets/framework/blank.scss index 9982a5779af..91dbb2a6365 100644 --- a/app/assets/stylesheets/framework/blank.scss +++ b/app/assets/stylesheets/framework/blank.scss @@ -37,7 +37,7 @@ flex: 0 0 100%; margin-bottom: 15px; - @media (min-width: $screen-sm-min) { + @include media-breakpoint-up(sm) { flex: 0 0 49%; &:nth-child(odd) { @@ -67,7 +67,7 @@ border: 1px solid $border-color; border-radius: $border-radius-default; - @media (min-width: $screen-sm-min) { + @include media-breakpoint-up(sm) { display: flex; align-items: center; padding: 50px 30px; @@ -89,12 +89,12 @@ } .blank-state-body { - @media (max-width: $screen-xs-max) { + @include media-breakpoint-down(xs) { text-align: center; margin-top: 20px; } - @media (min-width: $screen-sm-min) { + @include media-breakpoint-up(sm) { padding-left: 20px; } } diff --git a/app/assets/stylesheets/framework/blocks.scss b/app/assets/stylesheets/framework/blocks.scss index c5c7afe25be..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; } @@ -151,7 +151,7 @@ display: inline-block; margin-left: 5px; font-size: 18px; - color: $gray; + color: color("gray"); } p { @@ -195,7 +195,7 @@ } } - @media (max-width: $screen-xs-max) { + @include media-breakpoint-down(xs) { text-align: center; .avatar { @@ -315,7 +315,7 @@ } } - @media (max-width: $screen-sm-min) { + @include media-breakpoint-down(sm) { flex-direction: column; .inner-content { @@ -342,7 +342,7 @@ .btn { margin: $btn-side-margin 5px; - @media(max-width: $screen-xs-max) { + @include media-breakpoint-down(xs) { width: 100%; } } diff --git a/app/assets/stylesheets/framework/broadcast_messages.scss b/app/assets/stylesheets/framework/broadcast_messages.scss index 9b54fb94cdc..d3e7d751e63 100644 --- a/app/assets/stylesheets/framework/broadcast_messages.scss +++ b/app/assets/stylesheets/framework/broadcast_messages.scss @@ -19,3 +19,9 @@ @extend .broadcast-message; margin-bottom: 20px; } + +.toggle-colors { + input { + min-height: 34px; + } +} diff --git a/app/assets/stylesheets/framework/buttons.scss b/app/assets/stylesheets/framework/buttons.scss index f4f5926e198..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 { @@ -312,7 +304,8 @@ padding: 0 5px; } -.input-group-btn { +.input-group-prepend, +.input-group-append { .btn { @include btn-middle; @@ -401,7 +394,7 @@ } } -@media (max-width: $screen-xs-max) { +@include media-breakpoint-down(xs) { .btn-wide-on-xs { width: 100%; } diff --git a/app/assets/stylesheets/framework/calendar.scss b/app/assets/stylesheets/framework/calendar.scss index c165ec0b94b..0b9dff64b0b 100644 --- a/app/assets/stylesheets/framework/calendar.scss +++ b/app/assets/stylesheets/framework/calendar.scss @@ -4,7 +4,7 @@ border-top: 0; direction: rtl; - @media (min-width: $screen-sm-min) and (max-width: $screen-md-max) { + @media (min-width: map-get($grid-breakpoints, sm)) and (max-width: map-get($grid-breakpoints, sm)) { overflow-x: auto; } } diff --git a/app/assets/stylesheets/framework/ci_variable_list.scss b/app/assets/stylesheets/framework/ci_variable_list.scss index 5fe835dd8f9..7207e5119ce 100644 --- a/app/assets/stylesheets/framework/ci_variable_list.scss +++ b/app/assets/stylesheets/framework/ci_variable_list.scss @@ -10,14 +10,14 @@ display: flex; align-items: flex-start; - @media (max-width: $screen-xs-max) { + @include media-breakpoint-down(xs) { align-items: flex-end; } &:not(:last-child) { margin-bottom: $gl-btn-padding; - @media (max-width: $screen-xs-max) { + @include media-breakpoint-down(xs) { margin-bottom: 3 * $gl-btn-padding; } } @@ -26,7 +26,7 @@ .ci-variable-body-item:last-child { margin-right: $ci-variable-remove-button-width; - @media (max-width: $screen-xs-max) { + @include media-breakpoint-down(xs) { margin-right: 0; } } @@ -35,7 +35,7 @@ display: none; } - @media (max-width: $screen-xs-max) { + @include media-breakpoint-down(xs) { .ci-variable-row-body { margin-right: $ci-variable-remove-button-width; } @@ -48,7 +48,7 @@ align-items: flex-start; width: 100%; - @media (max-width: $screen-xs-max) { + @include media-breakpoint-down(xs) { display: block; } } @@ -59,7 +59,7 @@ &:not(:last-child) { margin-right: $gl-btn-padding; - @media (max-width: $screen-xs-max) { + @include media-breakpoint-down(xs) { margin-right: 0; margin-bottom: $gl-btn-padding; } diff --git a/app/assets/stylesheets/framework/common.scss b/app/assets/stylesheets/framework/common.scss index 0db1b78ac9f..a8d13249ecb 100644 --- a/app/assets/stylesheets/framework/common.scss +++ b/app/assets/stylesheets/framework/common.scss @@ -70,7 +70,7 @@ pre { padding: 0; } - &.well-pre { + &.card.card-body-pre { border: 1px solid $well-pre-bg; background: $gray-light; border-radius: 0; @@ -218,7 +218,7 @@ li.note { } .git_error_tips { - @extend .col-md-6; + @extend .col-lg-6; text-align: left; margin-top: 40px; @@ -308,7 +308,7 @@ img.emoji { .btn-sign-in { text-shadow: none; - @media (min-width: $screen-sm-min) { + @include media-breakpoint-up(sm) { margin-top: 8px; } } @@ -327,7 +327,7 @@ img.emoji { } } -.well { +.card.card-body { margin-bottom: $gl-padding; hr { @@ -336,7 +336,7 @@ img.emoji { } .search_box { - @extend .well; + @extend .card.card-body; text-align: center; } @@ -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; } @@ -474,6 +475,7 @@ img.emoji { .append-right-20 { margin-right: 20px; } .append-bottom-0 { margin-bottom: 0; } .append-bottom-5 { margin-bottom: 5px; } +.append-bottom-8 { margin-bottom: $grid-size; } .append-bottom-10 { margin-bottom: 10px; } .append-bottom-15 { margin-bottom: 15px; } .append-bottom-20 { margin-bottom: 20px; } diff --git a/app/assets/stylesheets/framework/contextual_sidebar.scss b/app/assets/stylesheets/framework/contextual_sidebar.scss index e2d97d0298f..1a415e1b852 100644 --- a/app/assets/stylesheets/framework/contextual_sidebar.scss +++ b/app/assets/stylesheets/framework/contextual_sidebar.scss @@ -1,11 +1,11 @@ .page-with-contextual-sidebar { transition: padding-left $sidebar-transition-duration; - @media (min-width: $screen-md-min) { + @include media-breakpoint-up(md) { padding-left: $contextual-sidebar-collapsed-width; } - @media (min-width: $screen-lg-min) { + @include media-breakpoint-up(lg) { padding-left: $contextual-sidebar-width; } @@ -16,7 +16,7 @@ } .page-with-icon-sidebar { - @media (min-width: $screen-sm-min) { + @include media-breakpoint-up(sm) { padding-left: $contextual-sidebar-collapsed-width; } } @@ -75,7 +75,7 @@ transform: translate3d(0, 0, 0); &:not(.sidebar-collapsed-desktop) { - @media (min-width: $screen-sm-min) and (max-width: $screen-md-max) { + @media (min-width: map-get($grid-breakpoints, sm)) and (max-width: map-get($grid-breakpoints, sm)) { box-shadow: inset -2px 0 0 $border-color, 2px 1px 3px $dropdown-shadow-color; } @@ -88,7 +88,7 @@ overflow-x: hidden; } - .badge:not(.fly-out-badge), + .badge.badge-pill:not(.fly-out-badge), .sidebar-context-title, .nav-item-name { display: none; @@ -142,7 +142,7 @@ } } - @media (max-width: $screen-xs-max) { + @include media-breakpoint-down(xs) { left: (-$contextual-sidebar-width); } @@ -166,7 +166,7 @@ width: 100%; overflow: auto; - @media (min-width: $screen-sm-min) { + @include media-breakpoint-up(sm) { overflow: hidden; } } @@ -207,7 +207,7 @@ > li { > a { - @media (min-width: $screen-sm-min) { + @include media-breakpoint-up(sm) { margin-right: 2px; } @@ -222,7 +222,7 @@ } .sidebar-sub-level-items { - @media (min-width: $screen-sm-min) { + @include media-breakpoint-up(sm) { position: fixed; top: 0; left: 0; @@ -277,7 +277,7 @@ } } - .badge { + .badge.badge-pill { background-color: $inactive-badge-background; color: $gl-text-color-secondary; } @@ -291,7 +291,7 @@ padding-left: 11px; } - .badge { + .badge.badge-pill { font-weight: $gl-font-weight-bold; } @@ -339,7 +339,7 @@ } .toggle-sidebar-button { - @media (max-width: $screen-xs-max) { + @include media-breakpoint-down(xs) { display: none; } } @@ -420,7 +420,7 @@ color: $gl-text-color-secondary; } - @media (max-width: $screen-xs-max) { + @include media-breakpoint-down(xs) { display: flex; align-items: center; @@ -429,7 +429,7 @@ } } - @media (max-width: $screen-xs-max) { + @include media-breakpoint-down(xs) { + .breadcrumbs-links { padding-left: $gl-padding; border-left: 1px solid $gl-text-color-quaternary; @@ -437,7 +437,7 @@ } } -@media (max-width: $screen-xs-max) { +@include media-breakpoint-down(xs) { .close-nav-button { display: flex; } diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss index cc5fac6816d..1570b1f2eaa 100644 --- a/app/assets/stylesheets/framework/dropdowns.scss +++ b/app/assets/stylesheets/framework/dropdowns.scss @@ -24,7 +24,7 @@ display: none; } -.open { +.show.dropdown { .dropdown-menu, .dropdown-menu-nav { @include set-visible; @@ -32,7 +32,7 @@ max-height: $dropdown-max-height; overflow-y: auto; - @media (max-width: $screen-xs-max) { + @include media-breakpoint-down(xs) { width: 100%; } } @@ -43,7 +43,7 @@ border-color: $gray-darkest; } - [data-toggle="dropdown"] { + [data-toggle='dropdown'] { outline: 0; } } @@ -172,7 +172,11 @@ color: $brand-danger; } - &:hover, + &.disable-hover { + text-decoration: none; + } + + &:not(.disable-hover):hover, &:active, &:focus, &.is-focused { @@ -180,7 +184,7 @@ text-decoration: none; - .badge { + .badge.badge-pill { background-color: darken($dropdown-link-hover-bg, 5%); } } @@ -206,11 +210,10 @@ .dropdown-menu, .dropdown-menu-nav { - @include set-invisible; + display: none; position: absolute; width: auto; top: 100%; - left: 0; z-index: 300; min-width: 240px; max-width: 500px; @@ -324,7 +327,7 @@ color: $gl-text-color-secondary; } - .badge + span:not(.badge) { + .badge.badge-pill + span:not(.badge.badge-pill) { // Expects up to 3 digits on the badge margin-right: 40px; } @@ -388,7 +391,7 @@ transform: translateY(0); } -.comment-type-dropdown.open .dropdown-menu { +.comment-type-dropdown.show .dropdown-menu { display: block; } @@ -469,16 +472,11 @@ .dropdown-select { width: $dropdown-width; - @media (max-width: $screen-sm-min) { + @include media-breakpoint-down(sm) { width: 100%; } } -.dropdown-menu-align-right { - left: auto; - right: 0; -} - .dropdown-menu-selectable { li { a, @@ -508,17 +506,16 @@ } &.is-indeterminate::before { - content: "\f068"; + content: '\f068'; } &.is-active::before { - content: "\f00c"; + content: '\f00c'; } } } } - .dropdown-title { position: relative; padding: 2px 25px 10px; @@ -724,7 +721,6 @@ } } - .dropdown-menu-due-date { .dropdown-content { max-height: 230px; @@ -781,7 +777,7 @@ } } -@media (max-width: $screen-xs-max) { +@include media-breakpoint-down(xs) { .navbar-gitlab { li.header-projects, li.header-more, @@ -836,7 +832,7 @@ header.header-content .dropdown-menu.projects-dropdown-menu { width: 70%; } - @media (max-width: $screen-xs-max) { + @include media-breakpoint-down(xs) { flex-direction: column; width: 100%; height: auto; @@ -854,9 +850,13 @@ header.header-content .dropdown-menu.projects-dropdown-menu { } .projects-list-frequent-container, - .projects-list-search-container, { + .projects-list-search-container { padding: 8px 0; overflow-y: auto; + + li.section-empty.section-failure { + color: $callout-danger-color; + } } .section-header, @@ -867,13 +867,6 @@ header.header-content .dropdown-menu.projects-dropdown-menu { font-size: $gl-font-size; } - .projects-list-frequent-container, - .projects-list-search-container { - li.section-empty.section-failure { - color: $callout-danger-color; - } - } - .search-input-container { position: relative; padding: 4px $gl-padding; @@ -895,7 +888,7 @@ header.header-content .dropdown-menu.projects-dropdown-menu { height: 284px; } - @media (max-width: $screen-xs-max) { + @include media-breakpoint-down(xs) { .projects-list-frequent-container { width: auto; height: auto; @@ -905,8 +898,7 @@ header.header-content .dropdown-menu.projects-dropdown-menu { } .projects-list-item-container { - .project-item-avatar-container - .project-item-metadata-container { + .project-item-avatar-container .project-item-metadata-container { float: left; } @@ -937,7 +929,7 @@ header.header-content .dropdown-menu.projects-dropdown-menu { color: $gl-text-color-secondary; } - @media (max-width: $screen-xs-max) { + @include media-breakpoint-down(xs) { .project-item-metadata-container { float: none; } 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/feature_highlight.scss b/app/assets/stylesheets/framework/feature_highlight.scss index 4f26cd015e4..cad915bc86f 100644 --- a/app/assets/stylesheets/framework/feature_highlight.scss +++ b/app/assets/stylesheets/framework/feature_highlight.scss @@ -79,7 +79,7 @@ border-right-color: $dropdown-border-color; } - .popover-content { + .popover-body { padding: 0; } } diff --git a/app/assets/stylesheets/framework/files.scss b/app/assets/stylesheets/framework/files.scss index d835d49d8b2..f77ec4b6a2c 100644 --- a/app/assets/stylesheets/framework/files.scss +++ b/app/assets/stylesheets/framework/files.scss @@ -75,7 +75,7 @@ padding: 8px $gl-padding; border-bottom: 1px solid $border-color; - @media (max-width: $screen-xs-max) { + @include media-breakpoint-down(xs) { text-align: left; } @@ -125,7 +125,7 @@ &.wiki { padding: $gl-padding; - @media (min-width: $screen-md-min) { + @include media-breakpoint-up(md) { padding: $gl-padding * 2; } } @@ -364,7 +364,7 @@ span.idiff { } } - @media (max-width: $screen-xs-max) { + @include media-breakpoint-down(xs) { display: block; .file-actions { diff --git a/app/assets/stylesheets/framework/filters.scss b/app/assets/stylesheets/framework/filters.scss index 621a4adc0cb..0ee5748952a 100644 --- a/app/assets/stylesheets/framework/filters.scss +++ b/app/assets/stylesheets/framework/filters.scss @@ -9,19 +9,19 @@ float: right; margin-right: 0; - @media (max-width: $screen-xs-max) { + @include media-breakpoint-down(xs) { float: none; } } } .filters-section { - @media (max-width: $screen-xs-max) { + @include media-breakpoint-down(xs) { display: inline-block; } } -@media (min-width: $screen-sm-min) { +@include media-breakpoint-up(sm) { .filter-item:not(:last-child) { margin-right: 6px; } @@ -37,7 +37,7 @@ } } -@media (max-width: $screen-xs-max) { +@include media-breakpoint-down(xs) { .filter-item { display: block; margin: 0 0 10px; @@ -53,7 +53,7 @@ display: -webkit-flex; display: flex; - @media (max-width: $screen-xs-min) { + @include media-breakpoint-down(xs) { -webkit-flex-direction: column; flex-direction: column; } @@ -193,7 +193,7 @@ border: 1px solid $border-color; background-color: $white-light; - @media (max-width: $screen-xs-min) { + @include media-breakpoint-down(xs) { -webkit-flex: 1 1 auto; flex: 1 1 auto; margin-bottom: 10px; @@ -264,7 +264,7 @@ max-width: 280px; overflow: auto; - @media (max-width: $screen-xs-min) { + @include media-breakpoint-down(xs) { width: auto; left: 0; right: 0; @@ -315,7 +315,7 @@ .filtered-search-history-dropdown { width: 40%; - @media (max-width: $screen-xs-min) { + @include media-breakpoint-down(xs) { left: 0; right: 0; max-width: none; @@ -353,7 +353,7 @@ } } -@media (max-width: $screen-xs-max) { +@include media-breakpoint-down(xs) { .issues-details-filters { padding: 0 0 10px; background-color: $white-light; @@ -361,7 +361,7 @@ } } -@media (max-width: $screen-xs) { +@include media-breakpoint-down(sm) { .filter-dropdown-container { .dropdown-toggle, .dropdown, diff --git a/app/assets/stylesheets/framework/flash.scss b/app/assets/stylesheets/framework/flash.scss index cb2f71b0033..52c3f18a682 100644 --- a/app/assets/stylesheets/framework/flash.scss +++ b/app/assets/stylesheets/framework/flash.scss @@ -8,19 +8,19 @@ .flash-notice { @extend .alert; - @extend .alert-info; + background-color: $blue-500; margin: 0; } .flash-warning { @extend .alert; - @extend .alert-warning; + background-color: $orange-500; margin: 0; } .flash-alert { @extend .alert; - @extend .alert-danger; + background-color: $red-500; margin: 0; .flash-text, @@ -42,14 +42,16 @@ .flash-success { @extend .alert; - @extend .alert-success; + background-color: $green-500; margin: 0; } .flash-notice, .flash-alert, - .flash-success { + .flash-success, + .flash-warning { border-radius: $border-radius-default; + color: $white-light; .container-fluid, .container-fluid.container-limited { @@ -72,7 +74,7 @@ } } -@media (max-width: $screen-sm-max) { +@include media-breakpoint-down(sm) { ul.notes { .flash-container.timeline-content { margin-left: 0; diff --git a/app/assets/stylesheets/framework/forms.scss b/app/assets/stylesheets/framework/forms.scss index 2c30311b1c1..c76ea532912 100644 --- a/app/assets/stylesheets/framework/forms.scss +++ b/app/assets/stylesheets/framework/forms.scss @@ -36,8 +36,8 @@ label { } } -.control-label { - @extend .col-sm-2; +.form-control-label { + @extend .col-md-2; } .inline-input-group { @@ -48,21 +48,21 @@ label { width: 150px; } -@media (min-width: $screen-sm-min) { +@include media-breakpoint-up(sm) { .custom-form-control { width: 150px; } } /* Medium devices (desktops, 992px and up) */ -@media (min-width: $screen-md-min) { +@include media-breakpoint-up(md) { .custom-form-control { width: 170px; } } /* Large devices (large desktops, 1200px and up) */ -@media (min-width: $screen-lg-min) { +@include media-breakpoint-up(lg) { .custom-form-control { width: 200px; } @@ -72,7 +72,7 @@ label { margin-left: 0; margin-right: 0; - .control-label { + .form-control-label { font-weight: $gl-font-weight-bold; padding-top: 4px; } @@ -83,7 +83,8 @@ label { font-family: $monospace_font; } - .input-group-btn .btn { + .input-group-prepend .btn, + .input-group-append .btn { padding: 3px $gl-btn-padding; background-color: $gray-light; border: 1px solid $border-color; @@ -102,10 +103,10 @@ label { } } - @media(max-width: $screen-xs-max) { + @include media-breakpoint-down(xs) { padding: 0 $gl-padding; - .control-label, + .form-control-label, .text-block { padding-left: 0; } @@ -120,6 +121,14 @@ label { @include box-shadow(none); border-radius: 2px; padding: $gl-vert-padding $gl-input-padding; + + &.input-short { + width: $input-short-width; + + @include media-breakpoint-up(md) { + width: $input-short-md-width; + } + } } .select-wrapper { @@ -155,8 +164,8 @@ label { margin-top: 35px; } -.form-group .control-label, -.form-group .control-label-full-width { +.form-group .form-control-label, +.form-group .form-control-label-full-width { font-weight: $gl-font-weight-normal; } @@ -170,17 +179,19 @@ label { max-width: 180px; } - .input-group-addon { + .input-group-prepend, + .input-group-append { background-color: $input-group-addon-bg; } - .input-group-addon:not(:first-child):not(:last-child) { + .input-group-prepend:not(:first-child):not(:last-child), + .input-group-append:not(:first-child):not(:last-child) { border-left: 0; border-right: 0; } } -.help-block { +.form-text.text-muted { margin-bottom: 0; margin-top: #{$grid-size / 2}; } @@ -221,7 +232,7 @@ label { } } -@media(max-width: $screen-xs-max) { +@include media-breakpoint-down(xs) { .remember-me { .remember-me-checkbox { margin-top: 0; diff --git a/app/assets/stylesheets/framework/gitlab_theme.scss b/app/assets/stylesheets/framework/gitlab_theme.scss index 05cb0196ced..d6ae8cbb416 100644 --- a/app/assets/stylesheets/framework/gitlab_theme.scss +++ b/app/assets/stylesheets/framework/gitlab_theme.scss @@ -21,7 +21,7 @@ } .container-fluid { - .navbar-toggle { + .navbar-toggler { border-left: 1px solid lighten($color-700, 10%); } } @@ -63,7 +63,7 @@ &:hover, &:focus { - @media (min-width: $screen-sm-min) { + @include media-breakpoint-up(sm) { background-color: rgba($color-200, 0.2); } @@ -165,37 +165,18 @@ } } - .sidebar-top-level-items > li.active .badge { + .sidebar-top-level-items > li.active .badge.badge-pill { color: $color-800; } - .nav-links li.active a { - border-bottom-color: $color-500; + .nav-links li a.active { + border-bottom: 2px solid $color-500; - .badge { + .badge.badge-pill { font-weight: $gl-font-weight-bold; } } - // 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 { @@ -289,8 +277,8 @@ body { } .container-fluid { - .navbar-toggle, - .navbar-toggle:hover { + .navbar-toggler, + .navbar-toggler:hover { color: $theme-gray-700; border-left: 1px solid $theme-gray-200; } @@ -340,12 +328,8 @@ body { } } - .sidebar-top-level-items > li.active .badge { + .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/header.scss b/app/assets/stylesheets/framework/header.scss index 0136af76a13..2085e5646ef 100644 --- a/app/assets/stylesheets/framework/header.scss +++ b/app/assets/stylesheets/framework/header.scss @@ -37,6 +37,7 @@ } .header-content { + width: 100%; display: -webkit-flex; display: flex; justify-content: space-between; @@ -91,7 +92,7 @@ border-radius: $border-radius-default; .tanuki-logo { - @media (min-width: $screen-sm-min) { + @include media-breakpoint-up(sm) { margin-right: 8px; } } @@ -110,7 +111,7 @@ } &.menu-expanded { - @media (max-width: $screen-xs-max) { + @include media-breakpoint-down(xs) { .title-container { display: none; } @@ -133,13 +134,13 @@ border-top: 0; padding: 0; - @media (max-width: $screen-xs-max) { + @include media-breakpoint-down(xs) { flex: 1 1 auto; } .nav { - > li:not(.hidden-xs) a { - @media (max-width: $screen-xs-max) { + > li:not(.d-none) a { + @include media-breakpoint-down(xs) { margin-left: 0; min-width: 100%; } @@ -156,7 +157,7 @@ } } - .navbar-toggle { + .navbar-toggler { right: -10px; border-radius: 0; min-width: 45px; @@ -181,14 +182,14 @@ } .navbar-nav { - @media (max-width: $screen-xs-max) { + @include media-breakpoint-down(xs) { display: -webkit-flex; display: flex; padding-right: 10px; } li { - .badge { + .badge.badge-pill { box-shadow: none; font-weight: $gl-font-weight-bold; } @@ -197,7 +198,7 @@ .nav > li { &.header-user { - @media (max-width: $screen-xs-max) { + @include media-breakpoint-down(xs) { padding-left: 10px; } } @@ -208,7 +209,7 @@ padding: 6px 8px; height: 32px; - @media (max-width: $screen-xs-max) { + @include media-breakpoint-down(xs) { padding: 0; } @@ -324,8 +325,8 @@ fill: currentColor; } -.header-user .dropdown-menu-nav, -.header-new .dropdown-menu-nav { +.header-user .dropdown-menu, +.header-new .dropdown-menu { margin-top: $dropdown-vertical-offset; } @@ -378,7 +379,7 @@ margin-bottom: 0; line-height: 16px; - @media (max-width: $screen-xs-max) { + @include media-breakpoint-down(xs) { flex-wrap: wrap; } @@ -410,7 +411,7 @@ .breadcrumb-item-text { text-decoration: inherit; - @media (max-width: $screen-xs-max) { + @include media-breakpoint-down(xs) { @include str-truncated(128px); } } @@ -452,7 +453,7 @@ .navbar-nav { li { - .badge { + .badge.badge-pill { position: inherit; font-weight: $gl-font-weight-normal; margin-left: -6px; @@ -478,7 +479,7 @@ } } -@media (max-width: $screen-xs-max) { +@include media-breakpoint-down(xs) { .navbar-gitlab .container-fluid { font-size: 18px; @@ -493,7 +494,7 @@ margin-left: -8px; margin-right: -10px; - .nav > li:not(.hidden-xs) { + .nav > li:not(.d-none) { display: table-cell !important; width: 25%; @@ -514,7 +515,7 @@ } .header-user { - .dropdown-menu-nav { + .dropdown-menu { width: auto; min-width: 160px; margin-top: 4px; 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/issue_box.scss b/app/assets/stylesheets/framework/issue_box.scss index d8c57a0e2d9..1d247671761 100644 --- a/app/assets/stylesheets/framework/issue_box.scss +++ b/app/assets/stylesheets/framework/issue_box.scss @@ -11,7 +11,7 @@ padding: 5px 11px; margin-top: 4px; /* Small devices (tablets, 768px and up) */ - @media (min-width: $screen-sm-min) { + @include media-breakpoint-up(sm) { padding: 0 $gl-btn-padding; margin-top: 5px; } diff --git a/app/assets/stylesheets/framework/layout.scss b/app/assets/stylesheets/framework/layout.scss index d107422e517..0536c39cee7 100644 --- a/app/assets/stylesheets/framework/layout.scss +++ b/app/assets/stylesheets/framework/layout.scss @@ -15,7 +15,7 @@ body { background-color: $white-light !important; } - &.card-content { + &.board-card-content { background-color: $gray-darker; .content-wrapper { @@ -74,7 +74,7 @@ body { } /* Center alert text and alert action links on smaller screens */ - @media (max-width: $screen-sm-max) { + @include media-breakpoint-down(sm) { .alert { text-align: center; } diff --git a/app/assets/stylesheets/framework/lists.scss b/app/assets/stylesheets/framework/lists.scss index f1a8a46dda4..17f4958d535 100644 --- a/app/assets/stylesheets/framework/lists.scss +++ b/app/assets/stylesheets/framework/lists.scss @@ -2,7 +2,7 @@ * Well styled list * */ -.well-list { +.card-body-list { position: relative; margin: 0; padding: 0; @@ -72,7 +72,7 @@ } } - .well-title { + .card.card-body-title { font-size: $list-font-size; line-height: 18px; } @@ -159,7 +159,7 @@ ul.content-list { &:last-child { margin-right: 0; - @media(max-width: $screen-xs-max) { + @include media-breakpoint-down(xs) { margin: 0 auto; } } @@ -173,7 +173,7 @@ ul.content-list { .member-controls { float: none; - @media (min-width: $screen-sm-min) { + @include media-breakpoint-up(sm) { float: right; } } @@ -228,7 +228,7 @@ ul.content-list { } } - .label-default { + .badge-secondary { color: $gl-text-color-secondary; } @@ -237,7 +237,7 @@ ul.content-list { } } -.panel > .content-list > li { +.card > .content-list > li { padding: $gl-padding-top $gl-padding; } @@ -279,251 +279,14 @@ ul.indent-list { padding: 10px 0 0 30px; } - // Specific styles for tree list @keyframes spin-avatar { from { transform: rotate(0deg); } to { transform: rotate(360deg); } } -.groups-list-tree-container { - .has-no-search-results { - text-align: center; - padding: $gl-padding; - font-style: italic; - color: $well-light-text-color; - } - - > .group-list-tree > .group-row.has-children:first-child { - border-top: 0; - } -} - -.group-list-tree { - .avatar-container.content-loading { - position: relative; - - > a, - > a .avatar { - height: 100%; - border-radius: 50%; - } - - > a { - padding: 2px; - - .avatar { - border: 2px solid $white-normal; - - &.identicon { - line-height: 15px; - } - } - } - - &::after { - content: ""; - position: absolute; - height: 100%; - width: 100%; - background-color: transparent; - border: 2px outset $kdb-border; - border-radius: 50%; - animation: spin-avatar 3s infinite linear; - } - } - - .folder-toggle-wrap { - float: left; - line-height: $list-text-height; - font-size: 0; - - span { - font-size: $gl-font-size; - } - } - - .folder-caret, - .item-type-icon { - display: inline-block; - } - - .folder-caret { - width: 15px; - - svg { - margin-bottom: 2px; - } - } - - .item-type-icon { - margin-top: 2px; - width: 20px; - } - - > .group-row:not(.has-children) { - .folder-caret { - opacity: 0; - } - } - - .content-list li:last-child { - padding-bottom: 0; - } - - .group-list-tree { - margin-bottom: 0; - margin-left: 30px; - position: relative; - - &::before { - content: ''; - display: block; - width: 0; - position: absolute; - top: 5px; - bottom: 0; - left: -16px; - border-left: 2px solid $border-white-normal; - } - - .group-row { - position: relative; - - &::before { - content: ""; - display: block; - width: 10px; - height: 0; - border-top: 2px solid $border-white-normal; - position: absolute; - top: 30px; - left: -16px; - } - - &:last-child::before { - background: $white-light; - height: auto; - top: 30px; - bottom: 0; - } - - &.being-removed { - opacity: 0.5; - } - } - } - - .group-row { - padding: 0; - - &.has-children { - border-top: 0; - } - - &:first-child { - border-top: 1px solid $white-normal; - } - - &:last-of-type { - .group-row-contents:not(:hover) { - border-bottom: 1px solid transparent; - } - } - } - - .group-row-contents { - padding: 10px 10px 8px; - border-top: solid 1px transparent; - border-bottom: solid 1px $white-normal; - - &:hover { - border-color: $row-hover-border; - background-color: $row-hover; - cursor: pointer; - } - - .avatar-container > a { - width: 100%; - text-decoration: none; - } - - &.has-more-items { - display: block; - padding: 20px 10px; - } - - .stats { - position: relative; - line-height: 46px; - - > span { - display: inline-flex; - align-items: center; - height: 16px; - min-width: 30px; - } - - > span:last-child { - margin-right: 0; - } - - .stat-value { - margin: 2px 0 0 5px; - } - } - - .controls { - margin-left: 5px; - - > .btn { - margin-right: $btn-xs-side-margin; - } - } - } - - .project-row-contents .stats { - line-height: inherit; - - > span:first-child { - margin-left: 25px; - } - - .item-visibility { - margin-right: 0; - } - - .last-updated { - position: absolute; - right: 12px; - min-width: 250px; - text-align: right; - color: $gl-text-color-secondary; - } - } -} - .namespace-title { .tooltip-inner { max-width: 350px; } } - -ul.group-list-tree { - li.group-row { - > .group-row-contents .title { - line-height: $list-text-height; - } - - &.has-description > .group-row-contents .title { - line-height: inherit; - } - } -} - -.js-groups-list-holder { - .groups-list-loading { - font-size: 34px; - text-align: center; - } -} diff --git a/app/assets/stylesheets/framework/markdown_area.scss b/app/assets/stylesheets/framework/markdown_area.scss index 938f5f49c09..b893151e4fe 100644 --- a/app/assets/stylesheets/framework/markdown_area.scss +++ b/app/assets/stylesheets/framework/markdown_area.scss @@ -74,7 +74,7 @@ } .md-header-tab { - @media (max-width: $screen-xs-max) { + @include media-breakpoint-down(xs) { flex: 1; width: 100%; border-bottom: 1px solid $border-color; @@ -90,7 +90,7 @@ &.active { display: block; - @media (max-width: $screen-xs-max) { + @include media-breakpoint-down(xs) { flex: none; display: flex; justify-content: center; @@ -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; @@ -182,7 +192,7 @@ margin-left: $gl-padding; margin-right: -5px; - @media (max-width: $screen-xs-max) { + @include media-breakpoint-down(xs) { margin-left: 0; margin-right: 0; } @@ -258,7 +268,7 @@ } } -@media (max-width: $screen-xs-max) { +@include media-breakpoint-down(xs) { .atwho-view-ul { width: 350px; } diff --git a/app/assets/stylesheets/framework/mixins.scss b/app/assets/stylesheets/framework/mixins.scss index e12b5aab381..0ea0b65b95f 100644 --- a/app/assets/stylesheets/framework/mixins.scss +++ b/app/assets/stylesheets/framework/mixins.scss @@ -17,6 +17,16 @@ */ @mixin markdown-table { width: auto; + display: inline-block; + overflow-x: auto; + border-left: 0; + border-right: 0; + border-bottom: 0; + + @supports(width: fit-content) { + display: block; + width: fit-content; + } } /* @@ -200,3 +210,15 @@ margin-left: -$size; } } + +/* + * Mixin that fixes wrapping issues with long strings (e.g. URLs) + * + * Note: the width needs to be set for it to work in Firefox + */ +@mixin overflow-break-word { + overflow-wrap: break-word; + word-wrap: break-word; + word-break: break-word; + max-width: 100%; +} diff --git a/app/assets/stylesheets/framework/mobile.scss b/app/assets/stylesheets/framework/mobile.scss index 8604e753c18..6244fb86fea 100644 --- a/app/assets/stylesheets/framework/mobile.scss +++ b/app/assets/stylesheets/framework/mobile.scss @@ -1,5 +1,5 @@ /** Common mobile (screen XS, SM) styles **/ -@media (max-width: $screen-xs-max) { +@include media-breakpoint-down(xs) { .container .content { margin-top: 20px; } @@ -14,7 +14,7 @@ font-size: 12px; margin-right: 3px; - .badge { + .badge.badge-pill { display: none; } } @@ -40,10 +40,6 @@ .project-home-panel { padding-left: 0 !important; - .project-avatar { - display: block; - } - .project-repo-buttons, .git-clone-holder { display: none; @@ -90,7 +86,7 @@ } } -@media (max-width: $screen-sm-max) { +@include media-breakpoint-down(sm) { .issues-filters { .milestone-filter { display: none; diff --git a/app/assets/stylesheets/framework/modal.scss b/app/assets/stylesheets/framework/modal.scss index eb789cc64b0..667661d8b5c 100644 --- a/app/assets/stylesheets/framework/modal.scss +++ b/app/assets/stylesheets/framework/modal.scss @@ -46,7 +46,7 @@ margin-left: $grid-size; } - @media (max-width: $screen-xs-max) { + @include media-breakpoint-down(xs) { flex-direction: column; .btn + .btn { @@ -55,7 +55,7 @@ } } - @media (min-width: $screen-sm-min) { + @include media-breakpoint-up(sm) { .btn:first-of-type { margin-left: auto; } @@ -74,7 +74,7 @@ body.modal-open { } } -@media (min-width: $screen-lg-min) { +@include media-breakpoint-up(lg) { .modal-full { width: 98%; } @@ -84,7 +84,7 @@ body.modal-open { background-color: $black-transparent; z-index: 2100; - @media (min-width: $screen-md-min) { + @include media-breakpoint-up(md) { .modal-dialog { margin: 30px auto; } diff --git a/app/assets/stylesheets/framework/page_header.scss b/app/assets/stylesheets/framework/page_header.scss index 0c879f40930..660e3dcac8d 100644 --- a/app/assets/stylesheets/framework/page_header.scss +++ b/app/assets/stylesheets/framework/page_header.scss @@ -3,7 +3,7 @@ padding: 10px 0; margin-bottom: 0; - @media (min-width: $screen-sm-min) { + @include media-breakpoint-up(sm) { display: flex; align-items: center; @@ -19,7 +19,7 @@ margin-right: 3px; } - @media (max-width: $screen-xs-max) { + @include media-breakpoint-down(xs) { .btn { width: 100%; margin-top: 10px; @@ -35,7 +35,7 @@ @extend .avatar-inline; margin-left: 0; - @media (min-width: $screen-sm-min) { + @include media-breakpoint-up(sm) { margin-left: 4px; } } diff --git a/app/assets/stylesheets/framework/pagination.scss b/app/assets/stylesheets/framework/pagination.scss index c3ec9db0f07..d3e013590b6 100644 --- a/app/assets/stylesheets/framework/pagination.scss +++ b/app/assets/stylesheets/framework/pagination.scss @@ -6,6 +6,7 @@ .pagination { padding: 0; + margin: 20px 0; a { cursor: pointer; @@ -30,7 +31,7 @@ } } -.panel > .gl-pagination { +.card > .gl-pagination { margin: 0; } @@ -44,7 +45,7 @@ display: none; } - .page { + .page-item { display: none; &.active { @@ -57,13 +58,13 @@ /** * Small screen pagination */ -@media (max-width: $screen-xs-min) { +@include media-breakpoint-down(xs) { .gl-pagination { .pagination li a { padding: 6px 10px; } - .page { + .page-item { display: none; &.active { @@ -76,9 +77,9 @@ /** * Medium screen pagination */ -@media (min-width: $screen-xs-min) and (max-width: $screen-md-max) { +@media (min-width: map-get($grid-breakpoints, xs)) and (max-width: map-get($grid-breakpoints, sm)) { .gl-pagination { - .page { + .page-item { display: none; &.active, diff --git a/app/assets/stylesheets/framework/panels.scss b/app/assets/stylesheets/framework/panels.scss index e8d69e62194..a8e28104a94 100644 --- a/app/assets/stylesheets/framework/panels.scss +++ b/app/assets/stylesheets/framework/panels.scss @@ -1,14 +1,14 @@ -.panel { +.card { margin-bottom: $gl-padding; } -.panel-slim { - @extend .panel; +.card-slim { + @extend .card; margin-bottom: $gl-vert-padding; } -.panel-heading { +.card-header { padding: $gl-vert-padding $gl-padding; line-height: 36px; @@ -21,7 +21,7 @@ line-height: 20px; } - .badge { + .badge.badge-pill { margin-top: -2px; margin-left: 5px; } @@ -41,12 +41,13 @@ } } -.panel-empty-heading { +.card-empty-heading { border-bottom: 0; } -.panel-body { +.card-body { padding: $gl-padding; + background-color: $white-light; .form-actions { margin: -$gl-padding; @@ -54,7 +55,7 @@ } } -.panel-title { +.card-title { font-size: inherit; line-height: inherit; } diff --git a/app/assets/stylesheets/framework/responsive_tables.scss b/app/assets/stylesheets/framework/responsive_tables.scss index 34fccf6f0a4..764bebd82c6 100644 --- a/app/assets/stylesheets/framework/responsive_tables.scss +++ b/app/assets/stylesheets/framework/responsive_tables.scss @@ -6,7 +6,7 @@ .gl-responsive-table-row-layout { width: 100%; - @media (min-width: $screen-md-min) { + @include media-breakpoint-up(md) { display: flex; align-items: center; @@ -21,7 +21,7 @@ margin-top: 10px; border: 1px solid $border-color; - @media (min-width: $screen-md-min) { + @include media-breakpoint-up(md) { margin: 0; padding: $gl-padding 0; border: 0; @@ -44,13 +44,13 @@ &.section-#{$width} { flex: 0 0 #{$width + '%'}; - @media (min-width: $screen-md-min) { + @include media-breakpoint-up(md) { max-width: #{$width + '%'}; } } } - @media (max-width: $screen-sm-max) { + @include media-breakpoint-down(sm) { display: flex; align-self: stretch; padding: 10px; @@ -65,7 +65,7 @@ &.section-wrap { white-space: normal; - @media (max-width: $screen-sm-max) { + @include media-breakpoint-down(sm) { flex-wrap: wrap; } } @@ -76,11 +76,11 @@ } .table-button-footer { - @media (min-width: $screen-md-min) { + @include media-breakpoint-up(md) { text-align: right; } - @media (max-width: $screen-sm-max) { + @include media-breakpoint-down(sm) { display: block; align-self: stretch; min-height: 0; @@ -122,7 +122,7 @@ .table-row-header { font-size: 13px; - @media (max-width: $screen-sm-max) { + @include media-breakpoint-down(sm) { display: none; } } @@ -132,13 +132,13 @@ color: $gl-text-color-secondary; text-align: left; - @media (min-width: $screen-md-min) { + @include media-breakpoint-up(md) { display: none; } } .table-mobile-content { - @media (max-width: $screen-sm-max) { + @include media-breakpoint-down(sm) { @include flex-max-width(60); text-align: right; } @@ -154,7 +154,7 @@ overflow: hidden; text-overflow: ellipsis; - @media (min-width: $screen-md-min) { + @include media-breakpoint-up(md) { flex: 0 0 90%; } diff --git a/app/assets/stylesheets/framework/secondary_navigation_elements.scss b/app/assets/stylesheets/framework/secondary_navigation_elements.scss index 17c31d6b184..c3c64adf3da 100644 --- a/app/assets/stylesheets/framework/secondary_navigation_elements.scss +++ b/app/assets/stylesheets/framework/secondary_navigation_elements.scss @@ -27,18 +27,18 @@ color: $black; border-bottom: 2px solid $gray-darkest; - .badge { + .badge.badge-pill { color: $black; } } - } - - &.active a { - color: $black; - font-weight: $gl-font-weight-bold; - .badge { + &.active { color: $black; + font-weight: $gl-font-weight-bold; + + .badge.badge-pill { + color: $black; + } } } } @@ -56,7 +56,7 @@ white-space: normal; /* Small devices (phones, tablets, 768px and lower) */ - @media (max-width: $screen-xs-max) { + @include media-breakpoint-down(xs) { width: 100%; } } @@ -80,7 +80,7 @@ } /* Small devices (phones, tablets, 768px and lower) */ - @media (max-width: $screen-xs-max) { + @include media-breakpoint-down(xs) { width: 100%; &.mobile-separator { @@ -124,21 +124,13 @@ position: relative; /* Medium devices (desktops, 992px and up) */ - @media (min-width: $screen-md-min) { width: 200px; } + @include media-breakpoint-up(md) { width: 200px; } /* Large devices (large desktops, 1200px and up) */ - @media (min-width: $screen-lg-min) { width: 250px; } - - &.input-short { - /* Medium devices (desktops, 992px and up) */ - @media (min-width: $screen-md-min) { width: 170px; } - - /* Large devices (large desktops, 1200px and up) */ - @media (min-width: $screen-lg-min) { width: 210px; } - } + @include media-breakpoint-up(lg) { width: 250px; } } - @media (max-width: $screen-xs-max) { + @include media-breakpoint-down(xs) { padding-bottom: 0; width: 100%; @@ -164,10 +156,6 @@ } } - .input-short { - width: 100%; - } - .icon-label { display: inline-block; } @@ -184,7 +172,7 @@ .nav-controls { width: auto; - @media (max-width: $screen-xs-max) { + @include media-breakpoint-down(xs) { width: 100%; } } @@ -204,7 +192,7 @@ width: 100%; } - @media (max-width: $screen-xs-max) { + @include media-breakpoint-down(xs) { flex-flow: row wrap; .nav-controls { @@ -241,8 +229,6 @@ } .scrolling-tabs-container { - position: relative; - .merge-request-tabs-container & { overflow: hidden; } @@ -272,8 +258,6 @@ } .inner-page-scroll-tabs { - position: relative; - .fade-right { @include fade(left, $white-light); right: 0; @@ -368,7 +352,7 @@ max-width: 300px; width: auto; - @media(max-width: $screen-xs-max) { + @include media-breakpoint-down(xs) { max-width: 250px; } } diff --git a/app/assets/stylesheets/framework/sidebar.scss b/app/assets/stylesheets/framework/sidebar.scss index 798f248dad4..8c716400913 100644 --- a/app/assets/stylesheets/framework/sidebar.scss +++ b/app/assets/stylesheets/framework/sidebar.scss @@ -16,7 +16,7 @@ .nav-header-btn { padding: 10px $gl-sidebar-padding; color: inherit; - transition-duration: .3s; + transition-duration: 0.3s; position: absolute; top: 0; cursor: pointer; @@ -31,7 +31,7 @@ .right-sidebar-collapsed { padding-right: 0; - @media (min-width: $screen-sm-min) { + @include media-breakpoint-up(sm) { &:not(.wiki-sidebar):not(.build-sidebar):not(.issuable-bulk-update-sidebar) .content-wrapper { padding-right: $gutter_collapsed_width; } @@ -65,13 +65,13 @@ display: inline-flex; } - @media (min-width: $screen-sm-min) and (max-width: $screen-sm-max) { + @include media-breakpoint-only(sm) { &:not(.wiki-sidebar):not(.build-sidebar):not(.issuable-bulk-update-sidebar) .content-wrapper { padding-right: $gutter_collapsed_width; } } - @media (min-width: $screen-md-min) { + @include media-breakpoint-up(md) { .content-wrapper { padding-right: $gutter_width; } @@ -137,6 +137,12 @@ } } +.issuable-sidebar .labels { + .value.dont-hide ~ .selectbox { + padding-top: $gl-padding-8; + } +} + .pikaday-container { .pika-single { margin-top: 2px; @@ -151,4 +157,3 @@ .sidebar-collapsed-icon .sidebar-collapsed-value { font-size: 12px; } - diff --git a/app/assets/stylesheets/framework/snippets.scss b/app/assets/stylesheets/framework/snippets.scss index 606d4675f19..f80e9b1014b 100644 --- a/app/assets/stylesheets/framework/snippets.scss +++ b/app/assets/stylesheets/framework/snippets.scss @@ -40,7 +40,7 @@ } .snippet-actions { - @media (min-width: $screen-sm-min) { + @include media-breakpoint-up(sm) { float: right; } } @@ -67,7 +67,8 @@ padding: 8px 40px; } - .embed-toggle { + .embed-toggle, + .snippet-clipboard-btn { height: 35px; } } diff --git a/app/assets/stylesheets/framework/tables.scss b/app/assets/stylesheets/framework/tables.scss index 5bde96caf42..a10bd1544c5 100644 --- a/app/assets/stylesheets/framework/tables.scss +++ b/app/assets/stylesheets/framework/tables.scss @@ -48,7 +48,7 @@ table { } .responsive-table { - @media (max-width: $screen-sm-max) { + @include media-breakpoint-down(sm) { th { width: 100%; } diff --git a/app/assets/stylesheets/framework/tabs.scss b/app/assets/stylesheets/framework/tabs.scss index c8ba14b7066..6b60149fbbb 100644 --- a/app/assets/stylesheets/framework/tabs.scss +++ b/app/assets/stylesheets/framework/tabs.scss @@ -1,6 +1,7 @@ .gitlab-tabs { background: $gray-light; border: 1px solid $border-color; + flex-wrap: nowrap; li { width: 50%; diff --git a/app/assets/stylesheets/framework/terms.scss b/app/assets/stylesheets/framework/terms.scss new file mode 100644 index 00000000000..744fd0ff796 --- /dev/null +++ b/app/assets/stylesheets/framework/terms.scss @@ -0,0 +1,64 @@ +.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; + line-height: $line-height-base; + + .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; + + .navbar-nav { + margin: 0; + } + } + + .nav li { + float: none; + } + } + + .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/timeline.scss b/app/assets/stylesheets/framework/timeline.scss index 373f35e71d8..75c11590547 100644 --- a/app/assets/stylesheets/framework/timeline.scss +++ b/app/assets/stylesheets/framework/timeline.scss @@ -4,7 +4,7 @@ padding: 0; &::before { - @include notes-media('max', $screen-xs-min) { + @include notes-media('max', map-get($grid-breakpoints, xs)) { background: none; } } @@ -34,7 +34,7 @@ .timeline-entry-inner { position: relative; - @include notes-media('max', $screen-xs-min) { + @include notes-media('max', map-get($grid-breakpoints, xs)) { .timeline-icon { display: none; } diff --git a/app/assets/stylesheets/framework/toggle.scss b/app/assets/stylesheets/framework/toggle.scss index 0cd83df218f..d5cc78a6680 100644 --- a/app/assets/stylesheets/framework/toggle.scss +++ b/app/assets/stylesheets/framework/toggle.scss @@ -121,7 +121,7 @@ cursor: not-allowed; } - @media (max-width: $screen-xs-min) { + @include media-breakpoint-down(xs) { width: 50px; &::before, diff --git a/app/assets/stylesheets/framework/tw_bootstrap.scss b/app/assets/stylesheets/framework/tw_bootstrap.scss deleted file mode 100644 index 1c6e2bf3074..00000000000 --- a/app/assets/stylesheets/framework/tw_bootstrap.scss +++ /dev/null @@ -1,201 +0,0 @@ -/* - * Twitter bootstrap with GitLab customizations/additions - * - */ - -// Core variables and mixins -@import "bootstrap/variables"; -@import "bootstrap/mixins"; - -// Reset -@import "bootstrap/normalize"; -@import "bootstrap/print"; - -// Core CSS -@import "bootstrap/scaffolding"; -@import "bootstrap/type"; -@import "bootstrap/code"; -@import "bootstrap/grid"; -@import "bootstrap/tables"; -@import "bootstrap/forms"; -@import "bootstrap/buttons"; - -// Components -@import "bootstrap/component-animations"; -// @import "bootstrap/dropdowns"; -@import "bootstrap/button-groups"; -@import "bootstrap/input-groups"; -@import "bootstrap/navs"; -@import "bootstrap/navbar"; -@import "bootstrap/breadcrumbs"; -@import "bootstrap/pagination"; -@import "bootstrap/pager"; -@import "bootstrap/labels"; -@import "bootstrap/badges"; -@import "bootstrap/alerts"; -@import "bootstrap/progress-bars"; -@import "bootstrap/list-group"; -@import "bootstrap/wells"; -@import "bootstrap/close"; -@import "bootstrap/panels"; - -// Components w/ JavaScript -@import "bootstrap/modals"; -@import "bootstrap/tooltip"; -@import "bootstrap/popovers"; - -// Utility classes -.clearfix { - @include clearfix(); -} - -.center-block { - @include center-block(); -} - -.pull-right { - float: right !important; -} - -.pull-left { - float: left !important; -} - -.hide { - display: none; -} - -.show { - display: block !important; -} - -.invisible { - visibility: hidden; -} - -.text-hide { - @include text-hide(); -} - -.hidden { - display: none !important; - visibility: hidden !important; -} - -.affix { - position: fixed; -} - -/* - * Fix <summary> elements on firefox - * See https://github.com/necolas/normalize.css/issues/640 - * and https://github.com/twbs/bootstrap/issues/21060 - * - */ -summary { - display: list-item; -} - -@import "bootstrap/responsive-utilities"; - -// Labels -.label { - padding: 4px 5px; - font-size: 12px; - font-style: normal; - font-weight: $gl-font-weight-normal; - display: inline-block; - - &.label-gray { - background-color: $label-gray-bg; - color: $gl-text-color; - text-shadow: none; - } - - &.label-inverse { - background-color: $label-inverse-bg; - } -} - - -/** - * fix to keep tooltips position in top navigation bar - * - */ -.navbar .nav > li { - position: relative; - white-space: nowrap; -} - -/** - * Add some extra stuff to panels - * - */ - -.panel { - box-shadow: none; - - .panel-body { - form, - pre { - margin: 0; - } - - .form-actions { - margin: -15px; - margin-top: 18px; - } - } - - .panel-footer { - .pagination { - margin: 0; - } - - .btn { - min-width: 124px; - } - - .btn-clipboard { - min-width: 0; - } - } - - &.panel-small { - .panel-heading { - padding: 6px 15px; - font-size: 13px; - font-weight: $gl-font-weight-normal; - - a { - color: $panel-heading-link-color; - } - } - } - - &.panel-without-border { - border: 0; - } - - &.panel-without-margin { - margin: 0; - } -} - -.panel-succes .panel-heading, -.panel-info .panel-heading, -.panel-danger .panel-heading, -.panel-warning .panel-heading, -.panel-primary .panel-heading, -.alert { - a:not(.btn) { - @extend .alert-link; - color: $white-light; - text-decoration: underline; - } -} - -// Prevent datetimes on tooltips to break into two lines -.local-timeago { - white-space: nowrap; -} diff --git a/app/assets/stylesheets/framework/tw_bootstrap_variables.scss b/app/assets/stylesheets/framework/tw_bootstrap_variables.scss deleted file mode 100644 index d04e555769b..00000000000 --- a/app/assets/stylesheets/framework/tw_bootstrap_variables.scss +++ /dev/null @@ -1,199 +0,0 @@ -// Override Bootstrap variables here (defaults from bootstrap-sass v3.3.3): -// For all variables see https://github.com/twbs/bootstrap-sass/blob/master/templates/project/_bootstrap-variables.sass -// -// Variables -// -------------------------------------------------- - - -//== Colors -// -//## Gray and brand colors for use across Bootstrap. - -// $gray-base: #000 -// $gray-darker: lighten($gray-base, 13.5%) // #222 -// $gray-dark: lighten($gray-base, 20%) // #333 -// $gray: lighten($gray-base, 33.5%) // #555 -// $gray-light: lighten($gray-base, 46.7%) // #777 -// $gray-lighter: lighten($gray-base, 93.5%) // #eee - -$brand-primary: $gl-primary; -$brand-success: $gl-success; -$brand-info: $gl-info; -$brand-warning: $gl-warning; -$brand-danger: $gl-danger; - -$border-radius-base: 3px !default; -$border-radius-large: 3px !default; -$border-radius-small: 3px !default; - - -//== Scaffolding -// -$text-color: $gl-text-color; -$link-color: $gl-link-color; -$link-hover-color: $gl-link-hover-color; - - -//== Typography -// -//## Font, line-height, and color for body text, headings, and more. - -$font-family-sans-serif: $regular_font; -$font-family-monospace: $monospace_font; -$font-size-base: $gl-font-size; - - -//== Components -// -//## Define common padding and border radius sizes and more. Values based on 14px text and 1.428 line-height (~20px to start). - -$padding-base-vertical: $gl-vert-padding; -$padding-base-horizontal: $gl-padding; -$component-active-color: $white-light; -$component-active-bg: $brand-info; - -//== Forms -// -//## - -$input-color: $text-color; -$input-border: $border-color; -$input-border-focus: $focus-border-color; -$legend-color: $text-color; - - -//== Pagination -// -//## - -$pagination-color: $gl-text-color; -$pagination-bg: $white-light; -$pagination-border: $border-color; - -$pagination-hover-color: $gl-text-color; -$pagination-hover-bg: $row-hover; -$pagination-hover-border: $border-color; - -$pagination-active-color: $white-light; -$pagination-active-bg: $gl-link-color; -$pagination-active-border: $gl-link-color; - -$pagination-disabled-color: #cdcdcd; -$pagination-disabled-bg: $gray-light; -$pagination-disabled-border: $border-color; - - -//== Form states and alerts -// -//## Define colors for form feedback states and, by default, alerts. - -$state-success-text: $white-light; -$state-success-bg: $brand-success; -$state-success-border: $brand-success; - -$state-info-text: $white-light; -$state-info-bg: $brand-info; -$state-info-border: $brand-info; - -$state-warning-text: $white-light; -$state-warning-bg: $brand-warning; -$state-warning-border: $brand-warning; - -$state-danger-text: $white-light; -$state-danger-bg: $brand-danger; -$state-danger-border: $brand-danger; - - -//== Alerts -// -//## Define alert colors, border radius, and padding. - -$alert-border-radius: 0; - - -//== Panels -// -//## - -$panel-border-radius: 2px; -$panel-default-text: $text-color; -$panel-default-border: $border-color; -$panel-default-heading-bg: $gray-light; -$panel-footer-bg: $gray-light; -$panel-inner-border: $border-color; - -$badge-bg: $badge-bg; -$badge-color: $badge-color; - -//== Wells -// -//## - -$well-bg: $gray-light; -$well-border: #eee; - -//== Code -// -//## - -$code-color: $red-600; -$code-bg: lighten($red-100, 2%); - -$kbd-color: $white-light; -$kbd-bg: #333; - -//== Buttons -// -//## -$btn-default-color: $gl-text-color; -$btn-default-bg: $white-light; -$btn-default-border: #e7e9ed; - -//== Nav -// -//## -$nav-link-padding: 13px $gl-padding; - -//== Code -// -//## -$pre-bg: $gray-light !default; -$pre-color: $gl-text-color !default; -$pre-border-color: $border-color; - -$table-bg-accent: $gray-light; - -$zindex-popover: 900; - -//== Modals -// -//## - -//** Padding applied to the modal body -$modal-inner-padding: $gl-padding; - -//** Padding applied to the modal title -$modal-title-padding: $gl-padding; -//** Modal title line-height -// $modal-title-line-height: $line-height-base - -//** Background color of modal content area -$modal-content-bg: $gray-light; -$modal-body-bg: $white-light; -//** Modal content border color -// $modal-content-border-color: rgba(0,0,0,.2) -//** Modal content border color **for IE8** -// $modal-content-fallback-border-color: #999 - -//** Modal backdrop background color -// $modal-backdrop-bg: #000 -//** Modal backdrop opacity -// $modal-backdrop-opacity: .5 -//** Modal header border color -// $modal-header-border-color: #e5e5e5 -//** Modal footer border color -// $modal-footer-border-color: $modal-header-border-color - -$modal-lg: 860px; -$modal-md: 540px; -// $modal-sm: 300px diff --git a/app/assets/stylesheets/framework/typography.scss b/app/assets/stylesheets/framework/typography.scss index 9e1371648ed..ed0bfbbe08b 100644 --- a/app/assets/stylesheets/framework/typography.scss +++ b/app/assets/stylesheets/framework/typography.scss @@ -114,7 +114,7 @@ font-size: 0.95em; } - blockquote { + .blockquote { color: $gl-grayish-blue; font-size: inherit; padding: 8px 24px; @@ -122,12 +122,12 @@ border-left: 3px solid $white-dark; } - blockquote:dir(rtl) { + .blockquote:dir(rtl) { border-left: 0; border-right: 3px solid $white-dark; } - blockquote p { + .blockquote p { color: $gl-grayish-blue !important; font-size: inherit; line-height: 1.5; @@ -321,6 +321,16 @@ h6 { /** CODE **/ pre { font-family: $monospace_font; + display: block; + padding: $gl-padding-8; + margin: 0 0 $gl-padding-8; + font-size: 13px; + word-break: break-all; + word-wrap: break-word; + color: $gl-text-color; + background-color: $gray-light; + border: 1px solid $border-color; + border-radius: $border-radius-small; } code { @@ -391,7 +401,7 @@ h4 { } .text-right-lg { - @media (min-width: $screen-lg-min) { + @include media-breakpoint-up(lg) { text-align: right; } } diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss index 37223175199..89b61530ddb 100644 --- a/app/assets/stylesheets/framework/variables.scss +++ b/app/assets/stylesheets/framework/variables.scss @@ -142,6 +142,11 @@ $border-gray-normal-dashed: darken($gray-normal, $darken-border-dashed-factor); $border-gray-dark: darken($white-normal, $darken-border-factor); /* + * Override Bootstrap 4 variables + */ +$secondary: $gray-light; + +/* * UI elements */ $border-color: #e5e5e5; @@ -212,6 +217,7 @@ $tooltip-font-size: 12px; /* * Padding */ +$gl-padding-24: 24px; $gl-padding: 16px; $gl-padding-8: 8px; $gl-padding-4: 4px; @@ -229,6 +235,7 @@ $row-hover: $blue-50; $row-hover-border: $blue-200; $progress-color: #c0392b; $header-height: 40px; +$ide-statusbar-height: 25px; $fixed-layout-width: 1280px; $limited-layout-width: 990px; $limited-layout-width-sm: 790px; @@ -247,6 +254,7 @@ $btn-sm-side-margin: 7px; $btn-xs-side-margin: 5px; $issue-status-expired: $orange-500; $issuable-sidebar-color: $gl-text-color-secondary; +$sidebar-block-hover-color: #ebebeb; $group-path-color: #999; $namespace-kind-color: #aaa; $panel-heading-link-color: #777; @@ -262,6 +270,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 @@ -302,6 +311,11 @@ $gl-warning: $orange-500; $gl-danger: $red-500; $gl-btn-active-background: rgba(0, 0, 0, 0.16); $gl-btn-active-gradient: inset 0 2px 3px $gl-btn-active-background; +// Bootstrap override states +$success: $gl-success; +$info: $gl-info; +$warning: $gl-warning; +$danger: $gl-danger; /* * Commit Diff Colors @@ -331,11 +345,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 @@ -373,6 +386,8 @@ $dropdown-hover-color: $blue-400; $link-active-background: rgba(0, 0, 0, 0.04); $link-hover-background: rgba(0, 0, 0, 0.06); $inactive-badge-background: rgba(0, 0, 0, 0.08); +$sidebar-toggle-height: 60px; +$sidebar-milestone-toggle-bottom-margin: 10px; /* * Buttons @@ -461,11 +476,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 @@ -551,6 +564,8 @@ $input-danger-border: $red-400; $input-group-addon-bg: #f7f8fa; $gl-field-focus-shadow: rgba(0, 0, 0, 0.075); $gl-field-focus-shadow-error: rgba($red-500, 0.6); +$input-short-width: 200px; +$input-short-md-width: 280px; /* * Help @@ -686,6 +701,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/framework/wells.scss b/app/assets/stylesheets/framework/wells.scss index 2f3a80daa90..514fac82b1e 100644 --- a/app/assets/stylesheets/framework/wells.scss +++ b/app/assets/stylesheets/framework/wells.scss @@ -5,7 +5,7 @@ border-radius: $border-radius-default; margin-bottom: $gl-padding; - .well-segment { + .card.card-body-segment { padding: $gl-padding; &:not(:last-of-type) { @@ -19,6 +19,7 @@ .fork-svg { margin-right: 4px; + vertical-align: bottom; } } @@ -58,7 +59,7 @@ } } - .label.label-gray { + .label-gray { background-color: $well-expand-item; } @@ -107,7 +108,7 @@ } } -.well-centered { +.card.card-body-centered { h1 { font-weight: $gl-font-weight-normal; text-align: center; diff --git a/app/assets/stylesheets/mailers/highlighted_diff_email.scss b/app/assets/stylesheets/mailers/highlighted_diff_email.scss index 658ac26fca9..b5eda79e5ed 100644 --- a/app/assets/stylesheets/mailers/highlighted_diff_email.scss +++ b/app/assets/stylesheets/mailers/highlighted_diff_email.scss @@ -138,7 +138,7 @@ pre { margin: 0; } -blockquote { +.blockquote { color: $gl-grayish-blue; padding: 0 0 0 15px; margin: 0; diff --git a/app/assets/stylesheets/pages/boards.scss b/app/assets/stylesheets/pages/boards.scss index 318d3ddaece..1c3d312f7ac 100644 --- a/app/assets/stylesheets/pages/boards.scss +++ b/app/assets/stylesheets/pages/boards.scss @@ -1,5 +1,3 @@ -@import './issues/issue_count_badge'; - [v-cloak] { display: none; } @@ -60,7 +58,7 @@ .boards-app { position: relative; - @media (min-width: $screen-sm-min) { + @include media-breakpoint-up(sm) { transition: width $sidebar-transition-duration; width: 100%; @@ -83,11 +81,11 @@ white-space: nowrap; min-height: 200px; - @media (min-width: $screen-sm-min) and (max-width: $screen-sm-max) { + @include media-breakpoint-only(sm) { height: calc(100vh - #{$issue-board-list-difference-sm}); } - @media (min-width: $screen-md-min) { + @include media-breakpoint-up(md) { height: calc(100vh - #{$issue-board-list-difference-md}); } @@ -96,13 +94,13 @@ 100vh - #{$issue-board-list-difference-xs} - #{$performance-bar-height} ); - @media (min-width: $screen-sm-min) and (max-width: $screen-sm-max) { + @include media-breakpoint-only(sm) { height: calc( 100vh - #{$issue-board-list-difference-sm} - #{$performance-bar-height} ); } - @media (min-width: $screen-md-min) { + @include media-breakpoint-up(md) { height: calc( 100vh - #{$issue-board-list-difference-md} - #{$performance-bar-height} ); @@ -119,7 +117,7 @@ white-space: normal; vertical-align: top; - @media (min-width: $screen-sm-min) { + @include media-breakpoint-up(sm) { width: 400px; } @@ -276,7 +274,7 @@ font-size: (26px / $issue-boards-font-size) * 1em; } -.card { +.board-card { position: relative; padding: 11px 10px 11px $gl-padding; background: $white-light; @@ -284,12 +282,15 @@ box-shadow: 0 1px 2px $issue-boards-card-shadow; list-style: none; + // as a fallback, hide overflow content so that dragging and dropping still works + overflow: hidden; + &:not(:last-child) { margin-bottom: 5px; } &.is-active, - &.is-active .card-assignee:hover a { + &.is-active .board-card-assignee:hover a { background-color: $row-hover; &:first-child:not(:only-child) { @@ -297,7 +298,7 @@ } } - .label { + .badge { border: 0; outline: 0; } @@ -309,23 +310,23 @@ } } -.card-title { +.board-card-title { + @include overflow-break-word(); margin: 0 30px 0 0; font-size: 1em; line-height: inherit; a { color: $gl-text-color; - word-wrap: break-word; margin-right: 2px; } } -.card-header { +.board-card-header { display: flex; min-height: 20px; - .card-assignee { + .board-card-assignee { display: flex; justify-content: flex-end; position: absolute; @@ -396,16 +397,16 @@ } } -.card-footer { +.board-card-footer { margin: 0 0 5px; - .label { + .badge { margin-top: 5px; margin-right: 6px; } } -.card-number { +.board-card-number { font-size: 12px; color: $gl-text-color-secondary; } @@ -461,6 +462,7 @@ } .issuable-header-text { + @include overflow-break-word(); padding-right: 35px; > strong { @@ -562,11 +564,11 @@ .add-issues-list-column { width: 100%; - @media (min-width: $screen-sm-min) { + @include media-breakpoint-up(sm) { width: 50%; } - @media (min-width: $screen-md-min) { + @include media-breakpoint-up(md) { width: (100% / 3); } } @@ -581,11 +583,11 @@ margin-right: -$gl-vert-padding; overflow-y: scroll; - .card-parent { + .board-card-parent { padding: 0 5px 5px; } - .card { + .board-card { border: 1px solid $border-gray-dark; box-shadow: 0 1px 2px rgba($issue-boards-card-shadow, 0.3); cursor: pointer; @@ -635,7 +637,7 @@ display: none; margin-right: 10px; - @media (min-width: $screen-sm-min) { + @include media-breakpoint-up(sm) { display: block; } } @@ -643,7 +645,7 @@ .dropdown-menu-toggle { width: 100px; - @media (min-width: $screen-md-min) { + @include media-breakpoint-up(md) { width: 140px; } } diff --git a/app/assets/stylesheets/pages/builds.scss b/app/assets/stylesheets/pages/builds.scss index 7a6352e45f1..9ee02ca1d83 100644 --- a/app/assets/stylesheets/pages/builds.scss +++ b/app/assets/stylesheets/pages/builds.scss @@ -1,39 +1,56 @@ @keyframes fade-out-status { - 0%, 50% { opacity: 1; } - 100% { opacity: 0; } + 0%, + 50% { + opacity: 1; + } + + 100% { + opacity: 0; + } } @keyframes blinking-dots { 0% { background-color: rgba($white-light, 1); box-shadow: 12px 0 0 0 rgba($white-light, 0.2), - 24px 0 0 0 rgba($white-light, 0.2); + 24px 0 0 0 rgba($white-light, 0.2); } 25% { background-color: rgba($white-light, 0.4); box-shadow: 12px 0 0 0 rgba($white-light, 2), - 24px 0 0 0 rgba($white-light, 0.2); + 24px 0 0 0 rgba($white-light, 0.2); } 75% { background-color: rgba($white-light, 0.4); box-shadow: 12px 0 0 0 rgba($white-light, 0.2), - 24px 0 0 0 rgba($white-light, 1); + 24px 0 0 0 rgba($white-light, 1); } 100% { background-color: rgba($white-light, 1); box-shadow: 12px 0 0 0 rgba($white-light, 0.2), - 24px 0 0 0 rgba($white-light, 0.2); + 24px 0 0 0 rgba($white-light, 0.2); } } @keyframes blinking-scroll-button { - 0% { opacity: 0.2; } - 25% { opacity: 0.5; } - 50% { opacity: 0.7; } - 100% { opacity: 1; } + 0% { + opacity: 0.2; + } + + 25% { + opacity: 0.5; + } + + 50% { + opacity: 0.7; + } + + 100% { + opacity: 1; + } } .build-page { @@ -125,12 +142,12 @@ .btn-scroll.animate { .first-triangle { animation: blinking-scroll-button 1s ease infinite; - animation-delay: .3s; + animation-delay: 0.3s; } .second-triangle { animation: blinking-scroll-button 1s ease infinite; - animation-delay: .2s; + animation-delay: 0.2s; } .third-triangle { @@ -198,7 +215,7 @@ } .header-action-buttons { - @media (max-width: $screen-xs-max) { + @include media-breakpoint-down(xs) { .sidebar-toggle-btn { margin-top: 0; margin-left: 10px; @@ -260,10 +277,6 @@ &.coverage { padding: 0 16px 11px; } - - .btn-group-justified { - margin-top: 5px; - } } .block-last { @@ -288,7 +301,7 @@ background-color: $white-light; } - .label { + .badge.badge-pill { margin-left: 2px; } @@ -303,7 +316,7 @@ } } - @media (max-width: $screen-sm-max) { + @include media-breakpoint-down(sm) { display: block; .btn { 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 e9384d41e00..a4ca82de90e 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,14 +20,10 @@ margin: 0; color: $gl-text-color; } - - .commit-description { - margin-top: 15px; - } } .commit-hash-full { - @media (max-width: $screen-sm-max) { + @include media-breakpoint-down(sm) { width: 80px; white-space: nowrap; overflow: hidden; @@ -70,7 +65,7 @@ } .branch-info .commit-icon { - margin-right: 3px; + margin-right: 8px; svg { top: 3px; @@ -178,7 +173,7 @@ .commit-detail { display: flex; justify-content: space-between; - align-items: center; + align-items: start; flex-grow: 1; } @@ -188,7 +183,7 @@ } .commit-actions { - @media (min-width: $screen-sm-min) { + @include media-breakpoint-up(sm) { .fa-spinner { font-size: 12px; } @@ -268,20 +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; - - 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; @@ -336,8 +327,12 @@ } &.invalid { - @include status-color($gray-dark, $gray, $gray-darkest); + @include status-color($gray-dark, color("gray"), $gray-darkest); border-color: $gray-darkest; + + &:not(span):hover { + color: color("gray"); + } } } diff --git a/app/assets/stylesheets/pages/convdev_index.scss b/app/assets/stylesheets/pages/convdev_index.scss index fb1899284fd..bd338326154 100644 --- a/app/assets/stylesheets/pages/convdev_index.scss +++ b/app/assets/stylesheets/pages/convdev_index.scss @@ -53,19 +53,19 @@ $space-between-cards: 8px; padding: $space-between-cards / 2; position: relative; - @media (min-width: $screen-xs-min) { + @include media-breakpoint-up(xs) { width: percentage(1 / 4); } - @media (min-width: $screen-sm-min) { + @include media-breakpoint-up(sm) { width: percentage(1 / 5); } - @media (min-width: $screen-md-min) { + @include media-breakpoint-up(md) { width: percentage(1 / 6); } - @media (min-width: $screen-lg-min) { + @include media-breakpoint-up(lg) { width: percentage(1 / 10); } } @@ -82,7 +82,7 @@ $space-between-cards: 8px; .convdev-card-low { border-top-color: $color-low-score; - .card-score-big { + .board-card-score-big { background-color: $red-50; } } @@ -90,7 +90,7 @@ $space-between-cards: 8px; .convdev-card-average { border-top-color: $color-average-score; - .card-score-big { + .board-card-score-big { background-color: $orange-50; } } @@ -98,7 +98,7 @@ $space-between-cards: 8px; .convdev-card-high { border-top-color: $color-high-score; - .card-score-big { + .board-card-score-big { background-color: $green-50; } } @@ -112,14 +112,14 @@ $space-between-cards: 8px; margin: 0 0 2px; } - .text-light { + .light-text { font-size: 13px; line-height: 1.25; color: $gl-text-color-secondary; } } -.card-scores { +.board-card-scores { display: flex; justify-content: space-around; align-items: center; @@ -127,22 +127,22 @@ $space-between-cards: 8px; line-height: 1; } -.card-score { +.board-card-score { color: $gl-text-color-secondary; - .card-score-name { + .board-card-score-name { font-size: 13px; margin-top: 4px; } } -.card-score-value { +.board-card-score-value { font-size: 16px; color: $gl-text-color; font-weight: $gl-font-weight-normal; } -.card-score-big { +.board-card-score-big { border-top: 2px solid $border-color; border-bottom: 1px solid $border-color; font-size: 22px; @@ -150,7 +150,7 @@ $space-between-cards: 8px; font-weight: $gl-font-weight-normal; } -.card-buttons { +.board-card-buttons { display: flex; > * { diff --git a/app/assets/stylesheets/pages/cycle_analytics.scss b/app/assets/stylesheets/pages/cycle_analytics.scss index cfef6476d4d..a22c666a525 100644 --- a/app/assets/stylesheets/pages/cycle_analytics.scss +++ b/app/assets/stylesheets/pages/cycle_analytics.scss @@ -15,7 +15,7 @@ max-width: 480px; padding: 0 $gl-padding; - @media (max-width: $screen-sm-min) { + @include media-breakpoint-down(sm) { margin: 0 auto; } } @@ -75,13 +75,13 @@ } } - .panel { + .card { .content-block { padding: 24px 0; border-bottom: 0; position: relative; - @media (max-width: $screen-xs-max) { + @include media-breakpoint-down(xs) { padding: 6px 0 24px; } } @@ -89,7 +89,7 @@ .column { text-align: center; - @media (max-width: $screen-xs-max) { + @include media-breakpoint-down(xs) { padding: 15px 0; } @@ -106,7 +106,7 @@ } &:last-child { - @media (max-width: $screen-xs-max) { + @include media-breakpoint-down(xs) { text-align: center; } } @@ -213,7 +213,7 @@ .stage-panel { min-width: 968px; - .panel-heading { + .card-header { padding: 0; background-color: transparent; } @@ -266,7 +266,9 @@ &.issue-title, &.commit-title, &.merge-merquest-title { - @include text-overflow(); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; max-width: 100%; display: block; diff --git a/app/assets/stylesheets/pages/detail_page.scss b/app/assets/stylesheets/pages/detail_page.scss index 2f2c04206e2..7b36bcb3c7d 100644 --- a/app/assets/stylesheets/pages/detail_page.scss +++ b/app/assets/stylesheets/pages/detail_page.scss @@ -14,7 +14,7 @@ white-space: nowrap; } - @media (max-width: $screen-xs-max) { + @include media-breakpoint-down(xs) { display: block; } } @@ -25,7 +25,7 @@ display: flex; flex-grow: 1; - @media (min-width: $screen-sm-min) { + @include media-breakpoint-up(sm) { padding-left: 0; padding-right: 0; } @@ -36,7 +36,7 @@ flex-shrink: 0; flex: 0 0 auto; - @media (max-width: $screen-xs-max) { + @include media-breakpoint-down(xs) { width: 100%; margin-top: 10px; diff --git a/app/assets/stylesheets/pages/diff.scss b/app/assets/stylesheets/pages/diff.scss index 11052be40a8..f06c9dcdf8c 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; @@ -80,13 +86,13 @@ &.left-side-selected { td.line_content.parallel.right-side { - @include user-select(none); + user-select: none; } } &.right-side-selected { td.line_content.parallel.left-side { - @include user-select(none); + user-select: none; } } } @@ -103,7 +109,7 @@ .old_line, .new_line { - @include user-select(none); + user-select: none; margin: 0; border: 0; padding: 0 5px; @@ -586,14 +592,14 @@ } .commit-stat-summary { - @media (min-width: $screen-sm-min) { + @include media-breakpoint-up(sm) { margin-left: -$gl-padding; padding-left: $gl-padding; background-color: $white-light; } } - @media (min-width: $screen-sm-min) { + @include media-breakpoint-up(sm) { position: -webkit-sticky; position: sticky; top: 24px; @@ -604,10 +610,6 @@ top: 76px; } - &:not(.is-stuck) .diff-stats-additions-deletions-collapsed { - display: none; - } - &.is-stuck { padding-top: 0; padding-bottom: 0; @@ -616,13 +618,21 @@ .diff-stats-additions-deletions-expanded, .inline-parallel-buttons { - display: none; + display: none !important; + } + } + } + + @include media-breakpoint-up(lg) { + &.is-stuck { + .diff-stats-additions-deletions-collapsed { + display: block !important; } } } } -@media (min-width: $screen-sm-min) { +@include media-breakpoint-up(sm) { .with-performance-bar { .diff-files-changed.diff-files-changed-merge-request { top: 76px + $performance-bar-height; @@ -635,7 +645,7 @@ width: 100%; z-index: 150; - @media (min-width: $screen-sm-min) { + @include media-breakpoint-up(sm) { left: $gl-padding; } @@ -766,9 +776,9 @@ } } -.frame .badge, -.image-diff-avatar-link .badge, -.notes > .badge { +.frame .badge.badge-pill, +.image-diff-avatar-link .badge.badge-pill, +.notes > .badge.badge-pill { position: absolute; background-color: $blue-400; color: $white-light; @@ -782,7 +792,7 @@ } } -.frame .badge, +.frame .badge.badge-pill, .frame .image-comment-badge { // Center align badges on the frame transform: translate(-50%, -50%); @@ -805,14 +815,14 @@ .image-diff-avatar-link { position: relative; - .badge, + .badge.badge-pill, .image-comment-badge { top: 25px; right: 8px; } } -.notes > .badge { +.notes > .badge.badge-pill { display: none; left: -13px; } @@ -834,7 +844,7 @@ display: none; } - .notes > .badge { + .notes > .badge.badge-pill { display: block; } } diff --git a/app/assets/stylesheets/pages/editor.scss b/app/assets/stylesheets/pages/editor.scss index 8ecda50602d..437621299e0 100644 --- a/app/assets/stylesheets/pages/editor.scss +++ b/app/assets/stylesheets/pages/editor.scss @@ -37,7 +37,7 @@ padding-top: 7px; padding-bottom: 7px; - .pull-right { + .float-right { height: 20px; } } @@ -63,11 +63,11 @@ max-width: 450px; float: left; - @media(max-width: $screen-md-max) { + @media(max-width: map-get($grid-breakpoints, lg)-1) { width: 280px; } - @media(max-width: $screen-sm-max) { + @media(max-width: map-get($grid-breakpoints, md)-1) { width: 180px; } } @@ -111,10 +111,10 @@ } -@media(max-width: $screen-xs-max){ +@include media-breakpoint-down(xs) { .file-editor { .file-title { - .pull-right { + .float-right { height: auto; } } @@ -153,7 +153,7 @@ vertical-align: top; display: inline-block; - @media(max-width: $screen-sm-max) { + @media(max-width: map-get($grid-breakpoints, md)-1) { display: block; margin: 19px 0 12px; } @@ -166,7 +166,7 @@ padding: 0 0 0 14px; border-left: 1px solid $border-color; - @media(max-width: $screen-sm-max) { + @media(max-width: map-get($grid-breakpoints, md)-1) { display: block; width: 100%; margin: 5px 0; @@ -181,7 +181,7 @@ margin-top: 6px; line-height: 21px; - @media(max-width: $screen-sm-max) { + @media(max-width: map-get($grid-breakpoints, md)-1) { display: block; margin: 5px 0; } @@ -193,7 +193,7 @@ vertical-align: top; margin: 5px 0 0 8px; - @media(max-width: $screen-sm-max) { + @media(max-width: map-get($grid-breakpoints, md)-1) { display: block; width: 100%; margin: 0 0 16px; @@ -209,7 +209,7 @@ font-family: $regular_font; margin-top: -5px; - @media(max-width: $screen-sm-max) { + @media(max-width: map-get($grid-breakpoints, md)-1) { display: block; width: 100%; margin: 5px 0; @@ -223,7 +223,7 @@ width: 250px; vertical-align: top; - @media(max-width: $screen-sm-max) { + @media(max-width: map-get($grid-breakpoints, md)-1) { display: block; width: 100%; margin: 5px 0; @@ -237,7 +237,7 @@ display: inline-block; margin: 7px 0 0 10px; - @media(max-width: $screen-sm-max) { + @media(max-width: map-get($grid-breakpoints, md)-1) { display: block; width: 100%; margin: 20px 0; diff --git a/app/assets/stylesheets/pages/environments.scss b/app/assets/stylesheets/pages/environments.scss index 3a300086fa3..cd0d67613c3 100644 --- a/app/assets/stylesheets/pages/environments.scss +++ b/app/assets/stylesheets/pages/environments.scss @@ -1,4 +1,4 @@ -@media (max-width: $screen-md-max) { +@include media-breakpoint-down(md) { .deployments-container { width: 100%; overflow: auto; @@ -138,7 +138,7 @@ border-left: 0; border-right: 0; - @media (min-width: $screen-sm-max) { + @media (min-width: map-get($grid-breakpoints, md)-1) { border-top: 0; } } @@ -251,7 +251,7 @@ font-size: 16px; } - @media (max-width: $screen-sm-max) { + @include media-breakpoint-down(sm) { min-width: 100%; } } @@ -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; } } @@ -401,7 +432,7 @@ } } - @media (max-width: $screen-sm-max) { + @include media-breakpoint-down(sm) { .label-axis-text, .text-metric-usage, .legend-axis-text { diff --git a/app/assets/stylesheets/pages/events.scss b/app/assets/stylesheets/pages/events.scss index d9267f5cdf3..f79586b68b9 100644 --- a/app/assets/stylesheets/pages/events.scss +++ b/app/assets/stylesheets/pages/events.scss @@ -70,7 +70,7 @@ .md { font-size: $gl-font-size; - .label { + .badge.badge-pill { color: $gl-text-color; } @@ -173,7 +173,7 @@ } } -@media (max-width: $screen-xs-max) { +@include media-breakpoint-down(xs) { .event-item { padding-left: 0; diff --git a/app/assets/stylesheets/pages/groups.scss b/app/assets/stylesheets/pages/groups.scss index 6ee8b33bd39..c2b42e02eee 100644 --- a/app/assets/stylesheets/pages/groups.scss +++ b/app/assets/stylesheets/pages/groups.scss @@ -2,7 +2,7 @@ @include str-truncated(90%); } -.dashboard .side .panel .panel-heading .input-group { +.dashboard .side .card .card-header .input-group { .form-control { height: 42px; @@ -18,6 +18,10 @@ .group-row { @include basic-list-stats; + + .description p { + margin-bottom: 0; + } } .ldap-group-links { @@ -36,7 +40,7 @@ flex: 1; } - .dropdown-menu-align-right { + .dropdown-menu-right { margin-top: 0; } @@ -98,7 +102,7 @@ } } - @media (max-width: $screen-sm-max) { + @include media-breakpoint-down(sm) { &, .dropdown, .dropdown .dropdown-toggle, @@ -145,14 +149,14 @@ padding: 50px 100px; overflow: hidden; - @media (max-width: $screen-sm-max) { + @include media-breakpoint-down(sm) { padding: 50px 0; } svg { float: right; - @media (max-width: $screen-sm-max) { + @include media-breakpoint-down(sm) { float: none; display: block; width: 250px; @@ -167,7 +171,7 @@ width: 460px; margin-top: 120px; - @media (max-width: $screen-sm-max) { + @include media-breakpoint-down(sm) { float: none; margin-top: 60px; width: auto; @@ -201,7 +205,7 @@ max-width: 480px; padding: 0 $gl-padding; - @media (max-width: $screen-sm-min) { + @include media-breakpoint-down(sm) { margin: 0 auto; } } @@ -237,3 +241,231 @@ overflow-y: unset; } } + +.groups-list-tree-container { + .has-no-search-results { + text-align: center; + padding: $gl-padding; + font-style: italic; + color: $well-light-text-color; + } + + > .group-list-tree > .group-row.has-children:first-child { + border-top: 0; + } +} + +.group-list-tree { + .avatar-container.content-loading { + position: relative; + + > a, + > a .avatar { + height: 100%; + border-radius: 50%; + } + + > a { + padding: 2px; + + .avatar { + border: 2px solid $white-normal; + + &.identicon { + line-height: 15px; + } + } + } + + &::after { + content: ""; + position: absolute; + height: 100%; + width: 100%; + background-color: transparent; + border: 2px outset $kdb-border; + border-radius: 50%; + animation: spin-avatar 3s infinite linear; + } + } + + .folder-toggle-wrap { + float: left; + line-height: $list-text-height; + font-size: 0; + + span { + font-size: $gl-font-size; + } + } + + .folder-caret, + .item-type-icon { + display: inline-block; + } + + .folder-caret { + width: 15px; + + svg { + margin-bottom: 2px; + } + } + + .item-type-icon { + margin-top: 2px; + width: 20px; + } + + > .group-row:not(.has-children) { + .folder-caret { + opacity: 0; + } + } + + .content-list li:last-child { + padding-bottom: 0; + } + + .group-list-tree { + margin-bottom: 0; + margin-left: 30px; + position: relative; + + &::before { + content: ''; + display: block; + width: 0; + position: absolute; + top: 5px; + bottom: 0; + left: -16px; + border-left: 2px solid $border-white-normal; + } + + .group-row { + position: relative; + + &::before { + content: ""; + display: block; + width: 10px; + height: 0; + border-top: 2px solid $border-white-normal; + position: absolute; + top: 30px; + left: -16px; + } + + &:last-child::before { + background: $white-light; + height: auto; + top: 30px; + bottom: 0; + } + + &.being-removed { + opacity: 0.5; + } + } + } + + .group-row { + padding: 0; + + &.has-children { + border-top: 0; + } + + &:first-child { + border-top: 1px solid $white-normal; + } + } + + .group-row-contents { + padding: $gl-padding-top; + + &:hover { + border-color: $row-hover-border; + background-color: $row-hover; + cursor: pointer; + } + + .avatar-container > a { + width: 100%; + text-decoration: none; + } + + &.has-more-items { + display: block; + padding: 20px 10px; + } + + .stats { + position: relative; + line-height: 46px; + + > span { + display: inline-flex; + align-items: center; + height: 16px; + min-width: 30px; + } + + > span:last-child { + margin-right: 0; + } + + .stat-value { + margin: 2px 0 0 5px; + } + } + + .controls { + margin-left: 5px; + + > .btn { + margin-right: $btn-xs-side-margin; + } + } + } + + .project-row-contents .stats { + line-height: inherit; + + > span:first-child { + margin-left: 25px; + } + + .item-visibility { + margin-right: 0; + } + + .last-updated { + position: absolute; + right: 12px; + min-width: 250px; + text-align: right; + color: $gl-text-color-secondary; + } + } +} + +ul.group-list-tree { + li.group-row { + > .group-row-contents .title { + line-height: $list-text-height; + } + + &.has-description > .group-row-contents .title { + line-height: inherit; + } + } +} + +.js-groups-list-holder { + .groups-list-loading { + font-size: 34px; + text-align: center; + } +} diff --git a/app/assets/stylesheets/pages/help.scss b/app/assets/stylesheets/pages/help.scss index 9cc9e11bcd1..0350fe5752e 100644 --- a/app/assets/stylesheets/pages/help.scss +++ b/app/assets/stylesheets/pages/help.scss @@ -28,8 +28,8 @@ } .key { - @extend .label; - @extend .label-inverse; + @extend .badge.badge-pill; + background-color: $label-inverse-bg; font: 11px Consolas, "Liberation Mono", Menlo, Courier, monospace; padding: 3px 5px; } diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss index 2c0ed976301..4aea9740735 100644 --- a/app/assets/stylesheets/pages/issuable.scss +++ b/app/assets/stylesheets/pages/issuable.scss @@ -187,14 +187,31 @@ padding-left: 10px; &:hover { - color: $gray-darkest; + color: $gl-text-color; + } + + &:hover, + &:focus { + text-decoration: none; } } &.assignee { - .author_link:hover { - .author { - text-decoration: underline; + .author_link { + display: block; + padding-left: 42px; + position: relative; + + &:hover { + .author { + text-decoration: underline; + } + } + + .avatar { + left: 0; + position: absolute; + top: 0; } } } @@ -356,7 +373,7 @@ /* Extra small devices (phones, less than 768px) */ display: none; /* Small devices (tablets, 768px and up) */ - @media (min-width: $screen-sm-min) { + @include media-breakpoint-up(sm) { display: block; } @@ -368,6 +385,14 @@ padding: 15px 0 0; border-bottom: 0; overflow: hidden; + + &:hover { + background-color: $sidebar-block-hover-color; + } + + &.issuable-sidebar-header { + padding-top: 0; + } } .participants { @@ -380,8 +405,17 @@ .gutter-toggle { width: 100%; + height: $sidebar-toggle-height; margin-left: 0; - padding-left: 25px; + padding-left: 0; + border-bottom: 1px solid $border-gray-dark; + } + + a.gutter-toggle { + display: flex; + justify-content: center; + flex-direction: column; + text-align: center; } .sidebar-collapsed-icon { @@ -428,10 +462,10 @@ .btn-clipboard { border: 0; + background: transparent; color: $issuable-sidebar-color; &:hover { - background: transparent; color: $gl-text-color; } } @@ -622,7 +656,7 @@ } .issuable-form-padding-top { - @media (min-width: $screen-sm-min) { + @include media-breakpoint-up(sm) { padding-top: 7px; } } @@ -636,7 +670,7 @@ padding-left: 9px; padding-right: 9px; - @media (min-width: $screen-sm-min) { + @include media-breakpoint-up(sm) { display: inline-block; height: auto; align-self: center; @@ -644,7 +678,7 @@ } .issuable-gutter-toggle { - @media (max-width: $screen-sm-max) { + @include media-breakpoint-down(sm) { margin-left: $btn-side-margin; } } @@ -662,7 +696,7 @@ width: 100%; } - @media (max-width: $screen-sm-max) { + @include media-breakpoint-down(sm) { margin-bottom: $gl-padding; } } @@ -703,7 +737,7 @@ } } - @media(max-width: $screen-xs-max) { + @include media-breakpoint-down(xs) { .issuable-meta { .controls li { margin-right: 0; @@ -738,7 +772,7 @@ } } - @media(max-width: $screen-md-max) { + @media(max-width: map-get($grid-breakpoints, lg)-1) { .task-status, .issuable-due-date, .project-ref-path { diff --git a/app/assets/stylesheets/pages/issues.scss b/app/assets/stylesheets/pages/issues.scss index b9390450477..19fb99bfa93 100644 --- a/app/assets/stylesheets/pages/issues.scss +++ b/app/assets/stylesheets/pages/issues.scss @@ -1,5 +1,3 @@ -@import "./issues/issue_count_badge"; - .issues-list { .issue { padding: 10px 0 10px $gl-padding; @@ -143,7 +141,7 @@ ul.related-merge-requests > li { } } -@media (max-width: $screen-xs-max) { +@include media-breakpoint-down(xs) { .detail-page-header { .issuable-meta { line-height: 18px; @@ -205,7 +203,7 @@ ul.related-merge-requests > li { } } - .btn-group:not(.hide) { + .btn-group:not(.hidden) { display: flex; } @@ -247,7 +245,7 @@ ul.related-merge-requests > li { display: block; } -@media (min-width: $screen-sm-min) { +@include media-breakpoint-up(sm) { .emoji-block .row { display: flex; @@ -258,8 +256,8 @@ ul.related-merge-requests > li { } .create-mr-dropdown-wrap { - .btn-group:not(.hide) { - display: inline-block; + .btn-group:not(.hidden) { + display: inline-flex; } } } diff --git a/app/assets/stylesheets/pages/labels.scss b/app/assets/stylesheets/pages/labels.scss index 97985dc69c2..c25e6ce23bb 100644 --- a/app/assets/stylesheets/pages/labels.scss +++ b/app/assets/stylesheets/pages/labels.scss @@ -61,6 +61,7 @@ padding: 0 $grid-size; line-height: 16px; border-radius: $label-border-radius; + color: $white-light; } .dropdown-labels-error { @@ -217,6 +218,10 @@ display: inline-flex; vertical-align: top; + &:hover .color-label { + text-decoration: underline; + } + .label { vertical-align: inherit; font-size: $label-font-size; diff --git a/app/assets/stylesheets/pages/login.scss b/app/assets/stylesheets/pages/login.scss index 97303d02666..c1b1d2e028d 100644 --- a/app/assets/stylesheets/pages/login.scss +++ b/app/assets/stylesheets/pages/login.scss @@ -191,9 +191,9 @@ } } -@media (max-width: $screen-xs-max) { +@include media-breakpoint-down(xs) { .login-page { - .col-sm-5.pull-right { + .col-md-5.float-right { float: none !important; margin-bottom: 45px; } @@ -243,7 +243,7 @@ .navless-container { padding: 65px 15px; // height of footer + bottom padding of email confirmation link - @media (max-width: $screen-xs-max) { + @include media-breakpoint-down(xs) { padding: 0 15px 65px; } } diff --git a/app/assets/stylesheets/pages/members.scss b/app/assets/stylesheets/pages/members.scss index 3422829de58..de2b5701e2d 100644 --- a/app/assets/stylesheets/pages/members.scss +++ b/app/assets/stylesheets/pages/members.scss @@ -9,9 +9,13 @@ } } +.member-sort-dropdown { + margin-left: $gl-padding-8; +} + .member { .list-item-name { - @media (min-width: $screen-sm-min) { + @include media-breakpoint-up(sm) { float: left; width: 50%; } @@ -22,12 +26,12 @@ } .controls { - @media (min-width: $screen-sm-min) { + @include media-breakpoint-up(sm) { display: -webkit-flex; display: flex; } - .dropdown-menu.dropdown-menu-align-right { + .dropdown-menu.dropdown-menu-right { margin-top: -2px; } } @@ -35,7 +39,7 @@ .form-horizontal { margin-top: 20px; - @media (min-width: $screen-sm-min) { + @include media-breakpoint-up(sm) { display: -webkit-flex; display: flex; margin-top: 3px; @@ -45,20 +49,20 @@ .btn-remove { width: 100%; - @media (min-width: $screen-sm-min) { + @include media-breakpoint-up(sm) { width: auto; } } &.existing-title { - @media (min-width: $screen-sm-min) { + @include media-breakpoint-up(sm) { float: left; } } } .member-form-control { - @media (max-width: $screen-xs-max) { + @include media-breakpoint-down(xs) { padding-bottom: 5px; margin-left: 0; margin-right: 0; @@ -73,7 +77,7 @@ .member-search-form { position: relative; - @media (min-width: $screen-sm-min) { + @include media-breakpoint-up(sm) { float: right; } @@ -86,7 +90,7 @@ width: 100%; } - @media (min-width: $screen-sm-min) { + @include media-breakpoint-up(sm) { margin-top: 0; width: 155px; } @@ -96,19 +100,9 @@ width: 100%; padding-right: 35px; - @media (min-width: $screen-sm-min) { + @include media-breakpoint-up(sm) { width: 250px; } - - &.input-short { - @media (min-width: $screen-md-min) { - width: 170px; - } - - @media (min-width: $screen-lg-min) { - width: 210px; - } - } } } @@ -124,7 +118,7 @@ border: 0; outline: 0; - @media (min-width: $screen-sm-min) { + @include media-breakpoint-up(sm) { right: 160px; } } @@ -135,7 +129,7 @@ align-items: center; justify-content: center; - @media (max-width: $screen-sm-min) { + @include media-breakpoint-down(sm) { display: block; .flex-project-title { @@ -151,7 +145,7 @@ text-overflow: ellipsis; } - .badge { + .badge.badge-pill { height: 17px; line-height: 16px; margin-right: 5px; @@ -166,14 +160,14 @@ } } -.panel { - .panel-heading { - .badge { +.card { + .card-header { + .badge.badge-pill { margin-top: 0; } - @media (max-width: $screen-sm-min) { - .badge { + @include media-breakpoint-down(sm) { + .badge.badge-pill { margin-right: 0; margin-left: 0; } @@ -217,7 +211,7 @@ margin-right: 0; } - @media (max-width: $screen-xs-max) { + @include media-breakpoint-down(xs) { display: block; .controls > .btn { @@ -245,7 +239,7 @@ } } -.panel-mobile { +.card-mobile { .content-list.members-list li { display: block; diff --git a/app/assets/stylesheets/pages/merge_conflicts.scss b/app/assets/stylesheets/pages/merge_conflicts.scss index 04bde64c752..e76525fdbf6 100644 --- a/app/assets/stylesheets/pages/merge_conflicts.scss +++ b/app/assets/stylesheets/pages/merge_conflicts.scss @@ -286,6 +286,14 @@ $colors: ( } .resolve-conflicts-form { - padding-top: $gl-padding; + h4 { + margin-top: 0; + } + + .resolve-info { + @media(max-width: map-get($grid-breakpoints, lg)-1) { + margin-bottom: $gl-padding; + } + } } } diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss index 66db4917178..9eceb3e9a33 100644 --- a/app/assets/stylesheets/pages/merge_requests.scss +++ b/app/assets/stylesheets/pages/merge_requests.scss @@ -131,7 +131,7 @@ color: $gl-text-color; display: flex; - @media (max-width: $screen-xs-max) { + @include media-breakpoint-down(xs) { flex-wrap: wrap; } } @@ -156,10 +156,6 @@ .dropdown-menu { z-index: 300; } - - .ci-action-icon-wrapper { - line-height: 16px; - } } .mini-pipeline-graph-dropdown-toggle { @@ -286,7 +282,7 @@ display: inline-block; } - @media (max-width: $screen-xs-max) { + @include media-breakpoint-down(xs) { p { font-size: 13px; } @@ -498,18 +494,18 @@ } } -.panel-new-merge-request { - .panel-heading { +.card-new-merge-request { + .card-header { padding: 5px 10px; font-weight: $gl-font-weight-bold; line-height: 25px; } - .panel-body { + .card-body { padding: 10px 5px; } - .panel-footer { + .card-footer { padding: 0; .btn { @@ -523,7 +519,7 @@ } .item-title { - @media (min-width: $screen-sm-min) { + @include media-breakpoint-up(sm) { width: 45%; } } @@ -554,7 +550,7 @@ margin-bottom: 0; } - @media (min-width: $screen-xs-min) { + @include media-breakpoint-up(xs) { float: left; width: 50%; margin-bottom: 0; @@ -652,7 +648,7 @@ background-color: $white-light; border-bottom: 1px solid $border-color; - @media (min-width: $screen-sm-min) { + @include media-breakpoint-up(sm) { position: sticky; position: -webkit-sticky; } @@ -661,7 +657,7 @@ left: 0; transition: right .15s; - @media (max-width: $screen-xs-max) { + @include media-breakpoint-down(xs) { right: 0; } @@ -704,7 +700,7 @@ display: flex; justify-content: space-between; - @media (max-width: $screen-xs-max) { + @include media-breakpoint-down(xs) { flex-direction: column-reverse; } } @@ -740,7 +736,7 @@ display: flex; flex-wrap: wrap; - @media (min-width: $screen-xs) { + @include media-breakpoint-up(xs) { flex-wrap: nowrap; white-space: nowrap; } @@ -757,7 +753,7 @@ min-width: 100px; max-width: 150px; - @media (min-width: $screen-xs) { + @include media-breakpoint-up(xs) { min-width: 0; max-width: 100%; } diff --git a/app/assets/stylesheets/pages/milestone.scss b/app/assets/stylesheets/pages/milestone.scss index 3af8d80daab..dba83e56d72 100644 --- a/app/assets/stylesheets/pages/milestone.scss +++ b/app/assets/stylesheets/pages/milestone.scss @@ -31,7 +31,7 @@ } } - .panel-heading { + .card-header { line-height: $line-height-base; padding: 14px 16px; display: -webkit-flex; @@ -53,10 +53,6 @@ } .milestone-sidebar { - .gutter-toggle { - margin-bottom: 10px; - } - .milestone-progress { .title { padding-top: 5px; @@ -102,7 +98,17 @@ margin-right: 0; } + .right-sidebar-expanded & { + .gutter-toggle { + margin-bottom: $sidebar-milestone-toggle-bottom-margin; + } + } + .right-sidebar-collapsed & { + .milestone-progress { + padding-top: 0; + } + .reference { border-top: 1px solid $border-gray-normal; } @@ -139,7 +145,7 @@ padding: 20px 0; } -@media (max-width: $screen-xs-max) { +@include media-breakpoint-down(xs) { .milestone-actions { @include clearfix(); padding-top: $gl-vert-padding; @@ -175,7 +181,7 @@ width: 100%; } - @media (min-width: $screen-xs-min) { + @include media-breakpoint-up(xs) { .milestone-buttons .verbose { display: inline; } @@ -223,7 +229,7 @@ } } -@media (max-width: $screen-xs-max) { +@include media-breakpoint-down(xs) { .milestone-banner-text, .milestone-banner-link { display: inline; diff --git a/app/assets/stylesheets/pages/note_form.scss b/app/assets/stylesheets/pages/note_form.scss index 4a528bc2bb1..3b037d066dc 100644 --- a/app/assets/stylesheets/pages/note_form.scss +++ b/app/assets/stylesheets/pages/note_form.scss @@ -51,7 +51,7 @@ } .note-image-attach { - @extend .col-md-4; + @extend .col-lg-4; margin-left: 45px; float: none; } @@ -93,7 +93,7 @@ -webkit-flex-flow: row wrap; width: 100%; - .pull-right { + .float-right { // Flexbox quirk to make sure right-aligned items stay right-aligned. margin-left: auto; } @@ -185,12 +185,12 @@ } .notes.notes-form > li.timeline-entry { - @include notes-media('max', $screen-sm-max) { + @include notes-media('max', map-get($grid-breakpoints, md) - 1) { padding: 0; } .timeline-content { - @include notes-media('max', $screen-sm-max) { + @include notes-media('max', map-get($grid-breakpoints, md) - 1) { margin: 0; } } @@ -326,7 +326,7 @@ outline: 0; } - @media (min-width: $screen-md-min) { + @include media-breakpoint-up(md) { float: left; margin-right: $gl-padding; @@ -350,13 +350,13 @@ line-height: 16px; margin-top: 2px; - @media (min-width: $screen-md-min) { + @include media-breakpoint-up(md) { float: left; } } .note-form-actions { - @media (max-width: $screen-xs-max) { + @include media-breakpoint-down(xs) { .btn { float: none; width: 100%; @@ -375,7 +375,7 @@ left: 127px; top: 2px; - @media (max-width: $screen-xs-max) { + @include media-breakpoint-down(xs) { position: relative; top: 0; left: 0; @@ -410,7 +410,7 @@ width: 298px; } - @media (max-width: $screen-xs-max) { + @include media-breakpoint-down(xs) { display: flex; width: 100%; margin-bottom: 10px; @@ -432,7 +432,7 @@ .uploading-container { float: right; - @media (max-width: $screen-xs-max) { + @include media-breakpoint-down(xs) { float: left; margin-top: 5px; } @@ -444,7 +444,7 @@ } .uploading-error-message { - @media (max-width: $screen-xs-max) { + @include media-breakpoint-down(xs) { &::after { content: "\a"; white-space: pre; diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss index 81e98f358a8..299eda53140 100644 --- a/app/assets/stylesheets/pages/notes.scss +++ b/app/assets/stylesheets/pages/notes.scss @@ -22,7 +22,7 @@ ul.notes { .discussion-body { padding-top: 8px; - .panel { + .card { margin-bottom: 0; } } @@ -42,7 +42,7 @@ ul.notes { position: relative; border-bottom: 0; - @include notes-media('min', $screen-sm-min) { + @include notes-media('min', map-get($grid-breakpoints, sm)) { padding-left: $note-icon-gutter-width; } @@ -66,7 +66,7 @@ ul.notes { } .timeline-icon { - @include notes-media('min', $screen-sm-min) { + @include notes-media('min', map-get($grid-breakpoints, sm)) { margin-left: -$note-icon-gutter-width; } } @@ -74,7 +74,7 @@ ul.notes { .timeline-content { margin-left: $note-icon-gutter-width; - @include notes-media('min', $screen-sm-min) { + @include notes-media('min', map-get($grid-breakpoints, sm)) { margin-left: 0; } } @@ -154,7 +154,7 @@ ul.notes { .note-header { - @include notes-media('max', $screen-xs-min) { + @include notes-media('max', map-get($grid-breakpoints, xs)) { .inline { display: block; } @@ -217,7 +217,7 @@ ul.notes { .timeline-icon { float: left; - @include notes-media('min', $screen-sm-min) { + @include notes-media('min', map-get($grid-breakpoints, sm)) { margin-left: 0; width: auto; } @@ -231,7 +231,7 @@ ul.notes { } .timeline-content { - @include notes-media('min', $screen-sm-min) { + @include notes-media('min', map-get($grid-breakpoints, sm)) { margin-left: 30px; } } @@ -407,10 +407,6 @@ ul.notes { .note-header { display: flex; justify-content: space-between; - - @include notes-media('max', $screen-xs-max) { - flex-flow: row wrap; - } } .note-header-info { @@ -427,7 +423,7 @@ ul.notes { } .note-header-author-name { - @include notes-media('max', $screen-xs-max) { + @include notes-media('max', map-get($grid-breakpoints, sm) - 1) { display: none; } } @@ -435,7 +431,7 @@ ul.notes { .note-headline-light { display: inline; - @include notes-media('max', $screen-xs-min) { + @include notes-media('max', map-get($grid-breakpoints, xs)) { display: block; } } @@ -459,6 +455,10 @@ ul.notes { white-space: normal; } + .system-note-separator { + color: $gl-text-color-disabled; + } + a:hover { text-decoration: underline; } @@ -473,11 +473,6 @@ ul.notes { margin-left: 10px; color: $gray-darkest; - @include notes-media('max', $screen-md-max) { - float: none; - margin-left: 0; - } - .btn-group > .discussion-next-btn { margin-left: -1px; } @@ -491,7 +486,7 @@ ul.notes { margin-left: 10px; color: $gray-darkest; - @include notes-media('max', $screen-xs-max) { + @include notes-media('max', map-get($grid-breakpoints, sm) - 1) { float: none; margin-left: 0; } @@ -639,6 +634,10 @@ ul.notes { margin-left: -55px; position: absolute; z-index: 10; + + .new & { + margin-top: -10px; + } } .discussion-body, @@ -673,7 +672,7 @@ ul.notes { } .line-resolve-all-container { - @include notes-media('min', $screen-sm-min) { + @include notes-media('min', map-get($grid-breakpoints, sm)) { margin-right: 0; padding-left: $gl-padding; } @@ -772,7 +771,3 @@ ul.notes { height: auto; } } - -.line-resolve-text { - vertical-align: middle; -} diff --git a/app/assets/stylesheets/pages/notifications.scss b/app/assets/stylesheets/pages/notifications.scss index bdf07a99daf..e98cb444f0a 100644 --- a/app/assets/stylesheets/pages/notifications.scss +++ b/app/assets/stylesheets/pages/notifications.scss @@ -2,7 +2,7 @@ line-height: 34px; .dropdown-menu { - @extend .dropdown-menu-align-right; + @extend .dropdown-menu-right; } } diff --git a/app/assets/stylesheets/pages/pipeline_schedules.scss b/app/assets/stylesheets/pages/pipeline_schedules.scss index bc7fa8a26d9..86e70955389 100644 --- a/app/assets/stylesheets/pages/pipeline_schedules.scss +++ b/app/assets/stylesheets/pages/pipeline_schedules.scss @@ -69,7 +69,7 @@ .cron-preset-radio-input { display: inline-block; - @media (max-width: $screen-md-max) { + @include media-breakpoint-down(md) { display: block; margin: 0 0 5px 5px; } diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss index 855ebf7d86d..f85f66b9c0b 100644 --- a/app/assets/stylesheets/pages/pipelines.scss +++ b/app/assets/stylesheets/pages/pipelines.scss @@ -1,3 +1,30 @@ +@mixin flat-connector-before($length: 44px) { + &::before { + content: ''; + position: absolute; + top: 48%; + left: -$length; + border-top: 2px solid $border-color; + width: $length; + height: 1px; + } +} + +@mixin build-content($border-radius: 30px) { + display: inline-block; + padding: 8px 10px 9px; + width: 100%; + border: 1px solid $border-color; + border-radius: $border-radius; + background-color: $white-light; + + &:hover { + background-color: $stage-hover-bg; + border: 1px solid $dropdown-toggle-active-border-color; + color: $gl-text-color; + } +} + .pipelines { .stage { max-width: 90px; @@ -16,14 +43,13 @@ margin: 0; white-space: normal; - @media (max-width: $screen-sm-max) { + @include media-breakpoint-down(sm) { justify-content: flex-end; } } .ci-table { - - .label { + .badge { margin-bottom: 3px; } @@ -67,13 +93,9 @@ } } - .btn-group { - &.open { - .btn-default { - background-color: $white-normal; - border-color: $border-white-normal; - } - } + .btn-group.open .btn-default { + background-color: $white-normal; + border-color: $border-white-normal; } .btn .text-center { @@ -87,7 +109,7 @@ } } -@media (max-width: $screen-md-max) { +@include media-breakpoint-down(md) { .content-list { &.builds-content-list { width: 100%; @@ -123,7 +145,6 @@ } .branch-commit { - .ref-name { font-weight: $gl-font-weight-bold; max-width: 100px; @@ -156,14 +177,14 @@ color: $gl-link-color; } - .label { + .badge { margin-right: 4px; } .label-container { font-size: 0; - .label { + .badge { margin-top: 5px; } } @@ -232,7 +253,7 @@ .stage-cell { &.table-section { - @media (min-width: $screen-md-min) { + @include media-breakpoint-up(md) { min-width: 160px; /* Hack alert: Without this the mini graph pipeline won't work properly*/ margin-right: -4px; } @@ -364,15 +385,7 @@ margin-left: 44px; .left-connector { - &::before { - content: ''; - position: absolute; - top: 48%; - left: -44px; - border-top: 2px solid $border-color; - width: 44px; - height: 1px; - } + @include flat-connector-before; } } } @@ -388,22 +401,16 @@ &:last-child { .build { // Remove right connecting horizontal line from first build in last stage - &:first-child { - &::after { - border: 0; - } + &:first-child::after { + border: 0; } // Remove right curved connectors from all builds in last stage - &:not(:first-child) { - &::after { - border: 0; - } + &:not(:first-child)::after { + border: 0; } // Remove opposite curve - .curve { - &::before { - display: none; - } + .curve::before { + display: none; } } } @@ -411,16 +418,12 @@ &:first-child { .build { // Remove left curved connectors from all builds in first stage - &:not(:first-child) { - &::before { - border: 0; - } + &:not(:first-child)::before { + border: 0; } // Remove opposite curve - .curve { - &::after { - display: none; - } + .curve::after { + display: none; } } } @@ -468,48 +471,19 @@ margin-bottom: 10px; white-space: normal; + .ci-job-dropdown-container { + // override dropdown.scss + .dropdown-menu li button { + padding: 0; + text-align: center; + } + } + // ensure .build-content has hover style when action-icon is hovered .ci-job-dropdown-container:hover .build-content { @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; @@ -526,12 +500,7 @@ } .build-content { - display: inline-block; - padding: 8px 10px 9px; - width: 100%; - border: 1px solid $border-color; - border-radius: 30px; - background-color: $white-light; + @include build-content(); } a.build-content:hover, @@ -540,7 +509,6 @@ border: 1px solid $dropdown-toggle-active-border-color; } - // Connect first build in each stage with right horizontal line &:first-child { &::after { @@ -594,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 @@ -633,8 +638,7 @@ } } -// Dropdown button in mini pipeline graph -button.mini-pipeline-graph-dropdown-toggle { +@mixin mini-pipeline-item() { border-radius: 100px; background-color: $white-light; border-width: 1px; @@ -647,30 +651,6 @@ button.mini-pipeline-graph-dropdown-toggle { position: relative; vertical-align: middle; - > .fa.fa-caret-down { - position: absolute; - left: 20px; - top: 5px; - display: inline-block; - visibility: hidden; - opacity: 0; - color: inherit; - font-size: 12px; - transition: visibility 0.1s, opacity 0.1s linear; - } - - &:active, - &:focus, - &:hover { - outline: none; - width: 35px; - - .fa.fa-caret-down { - visibility: visible; - opacity: 1; - } - } - // Dropdown button animation in mini pipeline graph &.ci-status-icon-success { @include mini-pipeline-graph-color($green-100, $green-500, $green-600); @@ -702,93 +682,106 @@ button.mini-pipeline-graph-dropdown-toggle { } } -// dropdown content for big and mini pipeline +// Dropdown button in mini pipeline graph +button.mini-pipeline-graph-dropdown-toggle { + @include mini-pipeline-item(); + + > .fa.fa-caret-down { + position: absolute; + left: 20px; + top: 5px; + display: inline-block; + visibility: hidden; + opacity: 0; + color: inherit; + font-size: 12px; + transition: visibility 0.1s, opacity 0.1s linear; + } + + &:active, + &:focus, + &:hover { + outline: none; + width: 35px; + + .fa.fa-caret-down { + visibility: visible; + opacity: 1; + } + } +} + +/** + 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 @@ -800,6 +793,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; @@ -822,7 +820,7 @@ button.mini-pipeline-graph-dropdown-toggle { display: block; } - @media (max-width: $screen-xs-max) { + @include media-breakpoint-down(xs) { max-width: 60%; } } @@ -909,7 +907,7 @@ button.mini-pipeline-graph-dropdown-toggle { transform: translate(-50%, 0); border-width: 0 5px 6px; - @media (max-width: $screen-sm-max) { + @include media-breakpoint-down(sm) { left: 100%; margin-left: -12px; } @@ -931,7 +929,7 @@ button.mini-pipeline-graph-dropdown-toggle { &.dropdown-menu { transform: translate(-80%, 0); - @media(min-width: $screen-md-min) { + @media(min-width: map-get($grid-breakpoints, md)) { transform: translate(-50%, 0); right: auto; left: 50%; diff --git a/app/assets/stylesheets/pages/profile.scss b/app/assets/stylesheets/pages/profile.scss index b199f9876d3..06078f1d12e 100644 --- a/app/assets/stylesheets/pages/profile.scss +++ b/app/assets/stylesheets/pages/profile.scss @@ -5,7 +5,7 @@ } .avatar-image { - @media (min-width: $screen-sm-min) { + @include media-breakpoint-up(sm) { float: left; margin-bottom: 0; } @@ -119,7 +119,7 @@ .key-list-item { .key-list-item-info { - @media (min-width: $screen-sm-min) { + @include media-breakpoint-up(sm) { float: left; } } @@ -188,7 +188,7 @@ .modal-dialog { width: 380px; - @media (max-width: $screen-xs-max) { + @include media-breakpoint-down(xs) { width: auto; } @@ -242,7 +242,7 @@ left: 0; } - @media (max-width: $screen-xs-max) { + @include media-breakpoint-down(xs) { .cover-block { padding-top: 20px; } @@ -352,7 +352,7 @@ table.u2f-registrations { } } - @media(max-width: $screen-xs-max) { + @include media-breakpoint-down(xs) { text-align: center; .bordered-box { @@ -414,7 +414,7 @@ table.u2f-registrations { } &.unverified { - @include status-color($gray-dark, $gray, $common-gray-dark); + @include status-color($gray-dark, color("gray"), $common-gray-dark); } } } diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss index d7d343b088a..22964163e95 100644 --- a/app/assets/stylesheets/pages/projects.scss +++ b/app/assets/stylesheets/pages/projects.scss @@ -9,7 +9,7 @@ .new_project, .edit-project, .import-project { - .help-block { + .form-text.text-muted { margin-bottom: 10px; } @@ -34,7 +34,7 @@ } } - @media (max-width: $screen-xs-max) { + @include media-breakpoint-down(xs) { .input-group > div { &:last-child { margin-bottom: 0; @@ -46,7 +46,8 @@ } } - .input-group-addon { + .input-group-prepend, + .input-group-append { overflow: hidden; text-overflow: ellipsis; line-height: unset; @@ -82,7 +83,7 @@ border: 1px solid $border-color; padding: 10px 32px; - @media (max-width: $screen-xs-min) { + @include media-breakpoint-down(xs) { padding: 10px 20px; } } @@ -134,7 +135,7 @@ max-width: 400px; } - @media (max-width: $screen-xs-min) { + @include media-breakpoint-down(xs) { padding-left: 20px; } } @@ -144,7 +145,7 @@ padding-top: 24px; padding-bottom: 24px; - @media (min-width: $screen-sm-min) { + @include media-breakpoint-up(sm) { border-bottom: 1px solid $border-color; } @@ -205,7 +206,6 @@ .project-repo-buttons, .group-buttons { .btn { - @include btn-gray; padding: 3px 10px; &:last-child { @@ -231,11 +231,11 @@ } .notification-dropdown .dropdown-menu { - @extend .dropdown-menu-align-right; + @extend .dropdown-menu-right; } .download-button { - @media (max-width: $screen-md-max) { + @include media-breakpoint-down(md) { margin-left: 0; } } @@ -294,7 +294,7 @@ } .count { - @include btn-gray; + @include btn-white; display: inline-block; background: $white-light; border-radius: 2px; @@ -354,30 +354,48 @@ min-width: 200px; } -.deploy-key-content { - @media (min-width: $screen-sm-min) { - 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 { - @media (min-width: $screen-sm-min) { - 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; + } } } @@ -427,11 +445,11 @@ a.deploy-project-label { height: 200px; width: calc((100% / 2) - #{$gl-padding * 2}); - @media (min-width: $screen-md-min) { + @include media-breakpoint-up(md) { width: calc((100% / 4) - #{$gl-padding * 2}); } - @media (min-width: $screen-lg-min) { + @include media-breakpoint-up(lg) { width: calc((100% / 5) - #{$gl-padding * 2}); } @@ -520,11 +538,12 @@ a.deploy-project-label { .template-input-group { position: relative; - @media (min-width: $screen-sm-min) { + @include media-breakpoint-up(sm) { display: flex; } - .input-group-addon { + .input-group-prepend, + .input-group-append { flex: 1; text-align: left; padding-left: ($gl-padding * 3); @@ -584,12 +603,12 @@ a.deploy-project-label { margin: 0 auto 4px; font-size: 24px; - @media (min-width: $screen-xs-max) { + @media (min-width: map-get($grid-breakpoints, sm)-1) { top: 0; } } - @media (max-width: $screen-xs-max) { + @include media-breakpoint-down(xs) { .btn-template-icon { display: inline-block; height: 14px; @@ -608,31 +627,31 @@ a.deploy-project-label { .create-project-options { display: flex; - @media (max-width: $screen-xs-max) { + @include media-breakpoint-down(xs) { display: block; } .first-column { - @media (min-width: $screen-xs-min) { + @include media-breakpoint-up(xs) { max-width: 50%; padding-right: 30px; } - @media (max-width: $screen-xs-max) { + @include media-breakpoint-down(xs) { max-width: 100%; width: 100%; } } .second-column { - @media (min-width: $screen-xs-min) { + @include media-breakpoint-up(xs) { width: 50%; flex: 1; padding-left: 30px; position: relative; } - @media (max-width: $screen-xs-max) { + @include media-breakpoint-down(xs) { max-width: 100%; width: 100%; padding-left: 0; @@ -640,7 +659,7 @@ a.deploy-project-label { } // Mobile - @media (max-width: $screen-xs-max) { + @include media-breakpoint-down(xs) { padding-top: 30px; } @@ -660,7 +679,7 @@ a.deploy-project-label { line-height: 20px; // Mobile - @media (max-width: $screen-xs-max) { + @include media-breakpoint-down(xs) { left: 50%; top: 0; transform: translateX(-50%); @@ -680,7 +699,7 @@ a.deploy-project-label { top: 0; // Mobile - @media (max-width: $screen-xs-max) { + @include media-breakpoint-down(xs) { top: 10px; left: 10px; right: 10px; @@ -718,7 +737,7 @@ a.deploy-project-label { vertical-align: top; margin-top: 0; - @media (min-width: $screen-lg-min) { + @include media-breakpoint-up(lg) { float: right; } } @@ -849,19 +868,14 @@ pre.light-well { } } -.panel .projects-list li { +.card .projects-list li { padding: 10px 15px; margin: 0; } -.commits-search-form { - .input-short { - min-width: 200px; - } -} - .git-clone-holder { width: 380px; + height: 28px; .btn-clipboard { border: 1px solid $border-color; @@ -877,11 +891,11 @@ pre.light-well { .form-control { @extend .monospace; - background: $white-light; + background-color: $white-light; + border-color: $border-color; font-size: 14px; margin-left: -1px; cursor: auto; - width: 101%; } } @@ -910,7 +924,8 @@ pre.light-well { } .project-tip-command { - > .input-group-btn:first-child { + > .input-group-prepend:first-child, + > .input-group-append:first-child { width: auto; } } @@ -961,7 +976,7 @@ pre.light-well { .dropdown-menu-projects { width: 300px; - @media (min-width: $screen-sm-min) { + @include media-breakpoint-up(sm) { width: 500px; } @@ -975,7 +990,7 @@ pre.light-well { .inline-input-group { width: 100%; - @media (min-width: $screen-sm-min) { + @include media-breakpoint-up(sm) { width: 300px; } } @@ -986,8 +1001,8 @@ pre.light-well { text-align: center; margin-top: -20px; - @media (min-width: $screen-sm-min) { - margin-top: 0; + @include media-breakpoint-up(sm) { + margin: 0 $gl-padding-8; width: auto; } } @@ -1025,7 +1040,7 @@ pre.light-well { } &.form-group { - @media (min-width: $screen-sm-min) { + @include media-breakpoint-up(sm) { margin-bottom: 0; } } @@ -1053,12 +1068,12 @@ pre.light-well { .project-feature { padding-top: 10px; - @media (min-width: $screen-sm-min) { + @include media-breakpoint-up(sm) { padding-left: 45px; } &.nested { - @media (min-width: $screen-sm-min) { + @include media-breakpoint-up(sm) { padding-left: 90px; } } diff --git a/app/assets/stylesheets/pages/repo.scss b/app/assets/stylesheets/pages/repo.scss index f58f2579050..17d7087bd85 100644 --- a/app/assets/stylesheets/pages/repo.scss +++ b/app/assets/stylesheets/pages/repo.scss @@ -17,11 +17,12 @@ } .ide-view { + position: relative; display: flex; height: calc(100vh - #{$header-height}); 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 { @@ -37,12 +38,15 @@ .ide-file-list { flex: 1; + padding-left: $gl-padding; + padding-right: $gl-padding; + padding-bottom: $grid-size; .file { cursor: pointer; &.file-open { - background: $link-active-background; + background: $white-normal; } &.file-active { @@ -54,6 +58,7 @@ white-space: nowrap; text-overflow: ellipsis; max-width: inherit; + line-height: 22px; svg { vertical-align: middle; @@ -66,29 +71,34 @@ } } + .ide-file-icon-holder { + display: flex; + align-items: center; + } + .ide-file-changed-icon { margin-left: auto; + + > svg { + display: block; + } } .ide-new-btn { display: none; - margin-bottom: -4px; - margin-right: -8px; } &:hover, &:focus { - background: $link-active-background; + background: $white-normal; .ide-new-btn { display: block; } } - &.folder { - svg { - fill: $gl-text-color-secondary; - } + .folder-icon { + fill: $gl-text-color-secondary; } } @@ -102,24 +112,16 @@ } } -.file-name, -.file-col-commit-message { +.file-name { display: flex; overflow: visible; - padding: 6px 12px; + align-items: center; + width: 100%; } .multi-file-loading-container { margin-top: 10px; padding: 10px; - - .animation-container { - background: $gray-light; - - div { - background: $gray-light; - } - } } .multi-file-table-col-commit-message { @@ -146,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; @@ -239,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; @@ -297,20 +297,27 @@ .margin-view-overlays .delete-sign { opacity: 0.4; } - - .cursors-layer { - display: none; - } } } .multi-file-editor-holder { height: 100%; + min-height: 0; } .preview-container { - height: 100%; - overflow: auto; + flex-grow: 1; + position: relative; + + .md-previewer { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + overflow: auto; + padding: $gl-padding; + } .file-container { background-color: $gray-darker; @@ -350,10 +357,6 @@ color: $diff-image-info-color; } } - - .md-previewer { - padding: $gl-padding; - } } .ide-mode-tabs { @@ -371,20 +374,46 @@ .ide-btn-group { padding: $gl-padding-4 $gl-vert-padding; + line-height: 24px; } .ide-status-bar { border-top: 1px solid $white-dark; - padding: $gl-bar-padding $gl-padding; + padding: 2px $gl-padding-8 0; background: $white-light; display: flex; justify-content: space-between; + height: $ide-statusbar-height; + + position: absolute; + bottom: 0; + left: 0; + width: 100%; + + font-size: 12px; + line-height: 22px; + + * { + font-size: inherit; + } + + > div + div { + padding-left: $gl-padding; + } svg { - vertical-align: middle; + vertical-align: sub; } } +.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%; @@ -420,27 +449,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; - height: 100%; - - .context-header { - width: auto; - margin-right: 0; - } + flex: 1; } .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 { @@ -448,68 +485,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; } } @@ -521,9 +500,13 @@ overflow: auto; } -.multi-file-commit-empty-state-container { - align-items: center; - justify-content: center; +.ide-commit-empty-state { + padding: 0 $gl-padding; +} + +.ide-commit-empty-state-container { + margin-top: auto; + margin-bottom: auto; } .multi-file-commit-panel-header { @@ -531,86 +514,79 @@ align-items: center; margin-bottom: 0; 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; - } - } + padding: 12px 0; } .multi-file-commit-panel-header-title { display: flex; flex: 1; - padding: 0 $gl-btn-padding; + align-items: center; svg { margin-right: $gl-btn-padding; + color: $theme-gray-700; } } .multi-file-commit-panel-collapse-btn { border-left: 1px solid $white-dark; + margin-left: auto; } .multi-file-commit-list { flex: 1; overflow: auto; - padding: $gl-padding 0; + padding: $grid-size 0; + margin-left: -$grid-size; + margin-right: -$grid-size; min-height: 60px; + + .multi-file-commit-list-item { + margin-left: 0; + margin-right: 0; + } + + &.help-block { + margin-left: 0; + right: 0; + } } .multi-file-commit-list-item { - display: flex; - padding: 0; - align-items: center; - .multi-file-discard-btn { display: none; + margin-top: -2px; 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; + display: flex; } } } -.multi-file-addition { - fill: $green-500; +.multi-file-addition, +.multi-file-addition-solid { + color: $green-500; } -.multi-file-modified { - fill: $orange-500; +.multi-file-modified, +.multi-file-modified-solid { + color: $orange-500; } .multi-file-commit-list-collapsed { display: flex; flex-direction: column; + padding: $gl-padding 0; - > svg { + svg { + display: block; margin-left: auto; margin-right: auto; + color: $theme-gray-700; } .file-status-icon { @@ -620,25 +596,39 @@ } } +.multi-file-commit-list-item, +.ide-file-list .file { + display: flex; + align-items: center; + margin-left: -$grid-size; + margin-right: -$grid-size; + padding: $grid-size / 2 $grid-size; + border-radius: $border-radius-default; + text-align: left; + + &:hover, + &:focus { + background: $white-normal; + } +} + .multi-file-commit-list-path { - padding: $grid-size / 2; - padding-left: $gl-padding; + padding: 0; background: none; border: 0; text-align: left; width: 100%; - min-width: 0; + + &:hover, + &:focus { + outline: 0; + } svg { min-width: 16px; vertical-align: middle; display: inline-block; } - - &:hover, - &:focus { - outline: 0; - } } .multi-file-commit-list-file-path { @@ -654,12 +644,30 @@ } .multi-file-commit-form { - padding: $gl-padding; - border-top: 1px solid $white-dark; + position: relative; + background-color: $white-light; + border-left: 1px solid $white-dark; + transition: all 0.3s ease; + + > form, + > .commit-form-compact { + padding: $gl-padding 0; + margin-left: $gl-padding; + margin-right: $gl-padding; + border-top: 1px solid $white-dark; + } .btn { font-size: $gl-font-size; } + + .multi-file-commit-panel-success-message { + top: 0; + } +} + +.multi-file-commit-panel-bottom { + position: relative; } .dirty-diff { @@ -795,7 +803,7 @@ position: absolute; top: 0; bottom: 0; - width: 3px; + width: 1px; background-color: $white-dark; &.dragright { @@ -807,12 +815,46 @@ } } +.ide-commit-list-container { + display: flex; + flex: 1; + flex-direction: column; + min-height: 140px; + margin-left: $gl-padding; + margin-right: $gl-padding; + + &.is-first { + border-bottom: 1px solid $white-dark; + } +} + +.ide-staged-action-btn { + margin-left: auto; + 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; + } + } } - .help-block { + .form-text.text-muted { margin-top: 0; line-height: 0; } @@ -822,17 +864,78 @@ 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 { + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; + z-index: 100; +} + +.ide-file-finder { + top: 10px; + left: 50%; + transform: translateX(-50%); + + .highlighted { + color: $blue-500; + font-weight: $gl-font-weight-bold; + } } .ide-commit-message-field { @@ -905,3 +1008,120 @@ background: transparent; resize: none; } + +.ide-tree-header { + display: flex; + align-items: center; + margin-bottom: 8px; + padding: 12px 0; + 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: 0; + left: 0; + right: 0; + 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 57b995adb64..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; - } - - .help-block { - 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/assets/stylesheets/pages/runners.scss b/app/assets/stylesheets/pages/runners.scss index 5fb97b13470..2734faec558 100644 --- a/app/assets/stylesheets/pages/runners.scss +++ b/app/assets/stylesheets/pages/runners.scss @@ -51,7 +51,7 @@ } } -@media (max-width: $screen-md-max) { +@include media-breakpoint-down(md) { .runners-content { width: 100%; overflow: auto; diff --git a/app/assets/stylesheets/pages/search.scss b/app/assets/stylesheets/pages/search.scss index dbde0720993..a35c4ff7c80 100644 --- a/app/assets/stylesheets/pages/search.scss +++ b/app/assets/stylesheets/pages/search.scss @@ -47,6 +47,7 @@ input[type="checkbox"]:hover { } .location-badge { + white-space: nowrap; height: 32px; font-size: 12px; margin: -4px 4px -4px -4px; @@ -166,7 +167,7 @@ input[type="checkbox"]:hover { } .search-holder { - @media (min-width: $screen-sm-min) { + @include media-breakpoint-up(sm) { display: -webkit-flex; display: flex; } @@ -178,7 +179,7 @@ input[type="checkbox"]:hover { position: relative; margin-right: 0; - @media (min-width: $screen-sm-min) { + @include media-breakpoint-up(sm) { margin-right: 5px; } } @@ -202,7 +203,7 @@ input[type="checkbox"]:hover { width: 100%; margin-top: 5px; - @media (min-width: $screen-sm-min) { + @include media-breakpoint-up(sm) { width: auto; margin-top: 0; margin-left: 5px; @@ -210,7 +211,7 @@ input[type="checkbox"]:hover { } .dropdown { - @media (min-width: $screen-sm-min) { + @include media-breakpoint-up(sm) { margin-left: 5px; margin-right: 5px; } @@ -220,7 +221,7 @@ input[type="checkbox"]:hover { width: 100%; margin-top: 5px; - @media (min-width: $screen-sm-min) { + @include media-breakpoint-up(sm) { width: 180px; margin-top: 0; } diff --git a/app/assets/stylesheets/pages/settings.scss b/app/assets/stylesheets/pages/settings.scss index c410049bc0b..2b3773eebad 100644 --- a/app/assets/stylesheets/pages/settings.scss +++ b/app/assets/stylesheets/pages/settings.scss @@ -70,7 +70,7 @@ animation: none; } - @media(max-width: $screen-sm-max) { + @media(max-width: map-get($grid-breakpoints, md)-1) { padding-right: 20px; } @@ -98,8 +98,8 @@ } .bs-callout, - .checkbox:first-child, - .help-block { + .form-check:first-child, + .form-text.text-muted { margin-top: 0; } @@ -131,12 +131,12 @@ color: $gl-danger; } -.service-settings .control-label { +.service-settings .form-control-label { padding-top: 0; } .integration-settings-form { - .well { + .card.card-body { padding: $gl-padding / 2; box-shadow: none; } @@ -158,7 +158,7 @@ } .visibility-level-setting { - .radio { + .form-check { margin-bottom: 10px; i.fa { @@ -174,7 +174,7 @@ .option-description, .option-disabled-reason { - margin-left: 29px; + margin-left: 45px; color: $project-option-descr-color; } @@ -199,22 +199,22 @@ } .prometheus-metrics-monitoring { - .panel { - .panel-toggle { + .card { + .card-toggle { width: 14px; } - .badge { + .badge.badge-pill { font-size: 12px; line-height: 12px; } - .panel-heading .badge-count { + .card-header .label-count { color: $white-light; background: $common-gray-dark; } - .panel-body { + .card-body { padding: 0; } @@ -249,7 +249,7 @@ li { padding: $gl-padding; - .badge { + .badge.badge-pill { margin-left: 5px; background: $badge-bg; } diff --git a/app/assets/stylesheets/pages/stat_graph.scss b/app/assets/stylesheets/pages/stat_graph.scss index 8e2c42c1bd3..3f6f5f06075 100644 --- a/app/assets/stylesheets/pages/stat_graph.scss +++ b/app/assets/stylesheets/pages/stat_graph.scss @@ -14,7 +14,10 @@ } #contributors-master { - @include make-md-column(12); + @include media-breakpoint-up(md) { + @include make-col-ready(); + @include make-col(12); + } svg { width: 100%; @@ -33,10 +36,14 @@ } .person { - @include make-md-column(6); + @include media-breakpoint-up(md) { + @include make-col-ready(); + @include make-col(6); + } + margin-top: 10px; - @media (max-width: $screen-xs-max) { + @include media-breakpoint-down(xs) { width: 100%; } diff --git a/app/assets/stylesheets/pages/status.scss b/app/assets/stylesheets/pages/status.scss index ade5ddd147b..620297e589d 100644 --- a/app/assets/stylesheets/pages/status.scss +++ b/app/assets/stylesheets/pages/status.scss @@ -58,7 +58,7 @@ } } -.visible-xs-inline { +.d-block.d-sm-none-inline { .ci-status-link { position: relative; top: 2px; diff --git a/app/assets/stylesheets/pages/todos.scss b/app/assets/stylesheets/pages/todos.scss index 4b9824fab0c..e5d7dd13915 100644 --- a/app/assets/stylesheets/pages/todos.scss +++ b/app/assets/stylesheets/pages/todos.scss @@ -126,7 +126,7 @@ color: $gl-grayish-blue; font-size: $gl-font-size; - .label { + .badge.badge-pill { color: $gl-text-color; } @@ -162,7 +162,7 @@ } } -@media (max-width: $screen-sm-max) { +@include media-breakpoint-down(sm) { .todos-filters { .dropdown-menu-toggle { width: 130px; @@ -174,7 +174,7 @@ } } -@media (max-width: $screen-xs-max) { +@include media-breakpoint-down(xs) { .todo { .avatar { display: none; @@ -214,7 +214,7 @@ margin-left: auto; margin-right: auto; - @media (min-width: $screen-sm-min) { + @include media-breakpoint-up(sm) { -webkit-flex-direction: row; flex-direction: row; padding-top: 80px; @@ -233,7 +233,7 @@ margin-left: auto; margin-right: auto; - @media (min-width: $screen-sm-min) { + @include media-breakpoint-up(sm) { width: 300px; margin-right: 0; -webkit-order: 2; @@ -244,7 +244,7 @@ .todos-all-done { padding-top: 20px; - @media (min-width: $screen-sm-min) { + @include media-breakpoint-up(sm) { padding-top: 50px; } diff --git a/app/assets/stylesheets/pages/tree.scss b/app/assets/stylesheets/pages/tree.scss index e0ee7e9aa3d..efd26cb1f81 100644 --- a/app/assets/stylesheets/pages/tree.scss +++ b/app/assets/stylesheets/pages/tree.scss @@ -7,7 +7,7 @@ color: $gl-text-color-secondary; } - @media (min-width: $screen-sm-min) { + @include media-breakpoint-up(sm) { display: flex; .tree-ref-container { @@ -41,15 +41,10 @@ position: relative; } } - - .add-to-tree-dropdown { - position: absolute; - left: 18px; - } } } - @media (max-width: $screen-xs-max) { + @include media-breakpoint-down(xs) { .repo-breadcrumb { margin-top: 10px; position: relative; @@ -121,7 +116,7 @@ margin-left: 5px; } - @media (min-width: $screen-md-min) and (max-width: $screen-md-max) { + @include media-breakpoint-only(md) { @include str-truncated(450px); } diff --git a/app/assets/stylesheets/pages/wiki.scss b/app/assets/stylesheets/pages/wiki.scss index 9a0ec936979..800f5c68e39 100644 --- a/app/assets/stylesheets/pages/wiki.scss +++ b/app/assets/stylesheets/pages/wiki.scss @@ -65,7 +65,7 @@ display: block; } - @media (min-width: $screen-sm-min) { + @include media-breakpoint-up(sm) { &.has-sidebar-toggle { padding-right: 40px; } @@ -81,7 +81,7 @@ } } - @media (min-width: $screen-md-min) { + @include media-breakpoint-up(md) { &.has-sidebar-toggle { padding-right: 0; } @@ -180,11 +180,6 @@ ul.wiki-pages-list.content-list { } } -.wiki-holder { - overflow-x: auto; - overflow-y: hidden; -} - .wiki { table { @include markdown-table; diff --git a/app/assets/stylesheets/performance_bar.scss b/app/assets/stylesheets/performance_bar.scss index 45ae94abaff..06ef58531d7 100644 --- a/app/assets/stylesheets/performance_bar.scss +++ b/app/assets/stylesheets/performance_bar.scss @@ -1,5 +1,4 @@ @import 'framework/variables'; -@import 'peek/views/performance_bar'; @import 'peek/views/rblineprof'; #js-peek { diff --git a/app/assets/stylesheets/print.scss b/app/assets/stylesheets/print.scss index b07a5ae22cd..90ccd4abd90 100644 --- a/app/assets/stylesheets/print.scss +++ b/app/assets/stylesheets/print.scss @@ -36,7 +36,9 @@ ul.notes-form, .gutter-toggle, .issuable-details .content-block-small, .edit-link, -.note-action-button { +.note-action-button, +.right-sidebar, +.flash-container { display: none !important; } @@ -53,3 +55,7 @@ pre { .right-sidebar { top: 0; } + +a[href]::after { + content: none !important; +} |