diff options
author | rubenmoya <rmoyarodriguez@gmail.com> | 2019-01-05 09:40:05 +0100 |
---|---|---|
committer | rubenmoya <rmoyarodriguez@gmail.com> | 2019-01-05 09:40:05 +0100 |
commit | cf5a9d2993c2998e6394560f5c4fe2fef3f35b1c (patch) | |
tree | dbd6f1c6a9c7878122f485300795d5b4b5b621e8 /app/assets | |
parent | 2269061e7151718d750bef4bbf1348dae8ac8a4a (diff) | |
parent | d432d674148601555c4ba693bb7c282ac9fe3d4a (diff) | |
download | gitlab-ce-cf5a9d2993c2998e6394560f5c4fe2fef3f35b1c.tar.gz |
Merge branch 'master' into 54311-fix-board-add-label
Diffstat (limited to 'app/assets')
45 files changed, 489 insertions, 215 deletions
diff --git a/app/assets/javascripts/blob_edit/blob_bundle.js b/app/assets/javascripts/blob_edit/blob_bundle.js index b07f951346e..5f64175362d 100644 --- a/app/assets/javascripts/blob_edit/blob_bundle.js +++ b/app/assets/javascripts/blob_edit/blob_bundle.js @@ -16,6 +16,7 @@ export default () => { const filePath = editBlobForm.data('blobFilename'); const currentAction = $('.js-file-title').data('currentAction'); const projectId = editBlobForm.data('project-id'); + const isMarkdown = editBlobForm.data('is-markdown'); const commitButton = $('.js-commit-button'); const cancelLink = $('.btn.btn-cancel'); @@ -27,7 +28,13 @@ export default () => { window.onbeforeunload = null; }); - new EditBlob(`${urlRoot}${assetsPath}`, filePath, currentAction, projectId); + new EditBlob({ + assetsPath: `${urlRoot}${assetsPath}`, + filePath, + currentAction, + projectId, + isMarkdown, + }); new NewCommitForm(editBlobForm); // returning here blocks page navigation diff --git a/app/assets/javascripts/blob_edit/edit_blob.js b/app/assets/javascripts/blob_edit/edit_blob.js index 6e19548eed2..011898a5e7a 100644 --- a/app/assets/javascripts/blob_edit/edit_blob.js +++ b/app/assets/javascripts/blob_edit/edit_blob.js @@ -6,22 +6,31 @@ import createFlash from '~/flash'; import { __ } from '~/locale'; import TemplateSelectorMediator from '../blob/file_template_mediator'; import getModeByFileExtension from '~/lib/utils/ace_utils'; +import { addEditorMarkdownListeners } from '~/lib/utils/text_markdown'; export default class EditBlob { - constructor(assetsPath, aceMode, currentAction, projectId) { - this.configureAceEditor(aceMode, assetsPath); + // The options object has: + // assetsPath, filePath, currentAction, projectId, isMarkdown + constructor(options) { + this.options = options; + this.configureAceEditor(); this.initModePanesAndLinks(); this.initSoftWrap(); - this.initFileSelectors(currentAction, projectId); + this.initFileSelectors(); } - configureAceEditor(filePath, assetsPath) { + configureAceEditor() { + const { filePath, assetsPath, isMarkdown } = this.options; ace.config.set('modePath', `${assetsPath}/ace`); ace.config.loadModule('ace/ext/searchbox'); ace.config.loadModule('ace/ext/modelist'); this.editor = ace.edit('editor'); + if (isMarkdown) { + addEditorMarkdownListeners(this.editor); + } + // This prevents warnings re: automatic scrolling being logged this.editor.$blockScrolling = Infinity; @@ -32,7 +41,8 @@ export default class EditBlob { } } - initFileSelectors(currentAction, projectId) { + initFileSelectors() { + const { currentAction, projectId } = this.options; this.fileTemplateMediator = new TemplateSelectorMediator({ currentAction, editor: this.editor, diff --git a/app/assets/javascripts/boards/components/issue_due_date.vue b/app/assets/javascripts/boards/components/issue_due_date.vue index e038198e6f0..9c4c6632976 100644 --- a/app/assets/javascripts/boards/components/issue_due_date.vue +++ b/app/assets/javascripts/boards/components/issue_due_date.vue @@ -3,7 +3,12 @@ import dateFormat from 'dateformat'; import { GlTooltip } from '@gitlab/ui'; import Icon from '~/vue_shared/components/icon.vue'; import { __ } from '~/locale'; -import { getDayDifference, getTimeago, dateInWords } from '~/lib/utils/datetime_utility'; +import { + getDayDifference, + getTimeago, + dateInWords, + parsePikadayDate, +} from '~/lib/utils/datetime_utility'; export default { components: { @@ -54,7 +59,7 @@ export default { return standardDateFormat; }, issueDueDate() { - return new Date(this.date); + return parsePikadayDate(this.date); }, timeDifference() { const today = new Date(); diff --git a/app/assets/javascripts/boards/components/modal/empty_state.vue b/app/assets/javascripts/boards/components/modal/empty_state.vue index 08408eb0b52..defd857b92c 100644 --- a/app/assets/javascripts/boards/components/modal/empty_state.vue +++ b/app/assets/javascripts/boards/components/modal/empty_state.vue @@ -45,7 +45,7 @@ export default { <section class="empty-state"> <div class="row"> <div class="col-12 col-md-6 order-md-last"> - <aside class="svg-content"><img :src="emptyStateSvg" /></aside> + <aside class="svg-content d-none d-md-block"><img :src="emptyStateSvg" /></aside> </div> <div class="col-12 col-md-6 order-md-first"> <div class="text-content"> diff --git a/app/assets/javascripts/clusters/clusters_bundle.js b/app/assets/javascripts/clusters/clusters_bundle.js index cf70a48f076..aff32d95db1 100644 --- a/app/assets/javascripts/clusters/clusters_bundle.js +++ b/app/assets/javascripts/clusters/clusters_bundle.js @@ -1,6 +1,6 @@ import Visibility from 'visibilityjs'; import Vue from 'vue'; -import initDismissableCallout from '~/dismissable_callout'; +import PersistentUserCallout from '../persistent_user_callout'; import { s__, sprintf } from '../locale'; import Flash from '../flash'; import Poll from '../lib/utils/poll'; @@ -67,7 +67,7 @@ export default class Clusters { this.showTokenButton = document.querySelector('.js-show-cluster-token'); this.tokenField = document.querySelector('.js-cluster-token'); - initDismissableCallout('.js-cluster-security-warning'); + Clusters.initDismissableCallout(); initSettingsPanels(); setupToggleButtons(document.querySelector('.js-cluster-enable-toggle-area')); this.initApplications(clusterType); @@ -108,6 +108,12 @@ export default class Clusters { }); } + static initDismissableCallout() { + const callout = document.querySelector('.js-cluster-security-warning'); + + if (callout) new PersistentUserCallout(callout); // eslint-disable-line no-new + } + addListeners() { if (this.showTokenButton) this.showTokenButton.addEventListener('click', this.showToken); eventHub.$on('installApplication', this.installApplication); diff --git a/app/assets/javascripts/diffs/components/compare_versions_dropdown.vue b/app/assets/javascripts/diffs/components/compare_versions_dropdown.vue index 8da02ed0b7c..b9b1ee02697 100644 --- a/app/assets/javascripts/diffs/components/compare_versions_dropdown.vue +++ b/app/assets/javascripts/diffs/components/compare_versions_dropdown.vue @@ -129,7 +129,7 @@ export default { </strong> </div> <div> - <small class="commit-sha"> {{ version.truncated_commit_sha }} </small> + <small class="commit-sha"> {{ version.short_commit_sha }} </small> </div> <div> <small> diff --git a/app/assets/javascripts/diffs/components/diff_content.vue b/app/assets/javascripts/diffs/components/diff_content.vue index 42d09e44768..ba6dcd63880 100644 --- a/app/assets/javascripts/diffs/components/diff_content.vue +++ b/app/assets/javascripts/diffs/components/diff_content.vue @@ -45,6 +45,9 @@ export default { isTextFile() { return this.diffFile.viewer.name === 'text'; }, + errorMessage() { + return this.diffFile.viewer.error; + }, diffFileCommentForm() { return this.getCommentFormForDiffFile(this.diffFile.file_hash); }, @@ -75,7 +78,7 @@ export default { <template> <div class="diff-content"> - <div class="diff-viewer"> + <div v-if="!errorMessage" class="diff-viewer"> <template v-if="isTextFile"> <empty-file-viewer v-if="diffFile.empty" /> <inline-diff-view @@ -129,5 +132,8 @@ export default { </div> </diff-viewer> </div> + <div v-else class="diff-viewer"> + <div class="nothing-here-block" v-html="errorMessage"></div> + </div> </div> </template> diff --git a/app/assets/javascripts/dismissable_callout.js b/app/assets/javascripts/dismissable_callout.js deleted file mode 100644 index 5185b019376..00000000000 --- a/app/assets/javascripts/dismissable_callout.js +++ /dev/null @@ -1,27 +0,0 @@ -import $ from 'jquery'; -import axios from '~/lib/utils/axios_utils'; -import { __ } from '~/locale'; -import Flash from '~/flash'; - -export default function initDismissableCallout(alertSelector) { - const alertEl = document.querySelector(alertSelector); - 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/gfm_auto_complete.js b/app/assets/javascripts/gfm_auto_complete.js index c14eb936930..8178821be3d 100644 --- a/app/assets/javascripts/gfm_auto_complete.js +++ b/app/assets/javascripts/gfm_auto_complete.js @@ -256,7 +256,7 @@ class GfmAutoComplete { displayTpl(value) { let tmpl = GfmAutoComplete.Loading.template; if (value.title != null) { - tmpl = GfmAutoComplete.Milestones.template; + tmpl = GfmAutoComplete.Milestones.templateFunction(value.title); } return tmpl; }, @@ -323,7 +323,7 @@ class GfmAutoComplete { searchKey: 'search', data: GfmAutoComplete.defaultLoadingData, displayTpl(value) { - let tmpl = GfmAutoComplete.Labels.template; + let tmpl = GfmAutoComplete.Labels.templateFunction(value.color, value.title); if (GfmAutoComplete.isLoading(value)) { tmpl = GfmAutoComplete.Loading.template; } @@ -588,9 +588,11 @@ GfmAutoComplete.Members = { }, }; GfmAutoComplete.Labels = { - template: - // eslint-disable-next-line no-template-curly-in-string - '<li><span class="dropdown-label-box" style="background: ${color}"></span> ${title}</li>', + templateFunction(color, title) { + return `<li><span class="dropdown-label-box" style="background: ${_.escape( + color, + )}"></span> ${_.escape(title)}</li>`; + }, }; // Issues, MergeRequests and Snippets GfmAutoComplete.Issues = { @@ -600,8 +602,9 @@ GfmAutoComplete.Issues = { }; // Milestones GfmAutoComplete.Milestones = { - // eslint-disable-next-line no-template-curly-in-string - template: '<li>${title}</li>', + templateFunction(title) { + return `<li>${_.escape(title)}</li>`; + }, }; GfmAutoComplete.Loading = { template: diff --git a/app/assets/javascripts/lib/utils/text_markdown.js b/app/assets/javascripts/lib/utils/text_markdown.js index 1254ec798a6..84a617acb42 100644 --- a/app/assets/javascripts/lib/utils/text_markdown.js +++ b/app/assets/javascripts/lib/utils/text_markdown.js @@ -8,6 +8,10 @@ function selectedText(text, textarea) { return text.substring(textarea.selectionStart, textarea.selectionEnd); } +function addBlockTags(blockTag, selected) { + return `${blockTag}\n${selected}\n${blockTag}`; +} + function lineBefore(text, textarea) { var split; split = text @@ -24,19 +28,45 @@ function lineAfter(text, textarea) { .split('\n')[0]; } +function editorBlockTagText(text, blockTag, selected, editor) { + const lines = text.split('\n'); + const selectionRange = editor.getSelectionRange(); + const shouldRemoveBlock = + lines[selectionRange.start.row - 1] === blockTag && + lines[selectionRange.end.row + 1] === blockTag; + + if (shouldRemoveBlock) { + if (blockTag !== null) { + // ace is globally defined + // eslint-disable-next-line no-undef + const { Range } = ace.require('ace/range'); + const lastLine = lines[selectionRange.end.row + 1]; + const rangeWithBlockTags = new Range( + lines[selectionRange.start.row - 1], + 0, + selectionRange.end.row + 1, + lastLine.length, + ); + editor.getSelection().setSelectionRange(rangeWithBlockTags); + } + return selected; + } + return addBlockTags(blockTag, selected); +} + function blockTagText(text, textArea, blockTag, selected) { - const before = lineBefore(text, textArea); - const after = lineAfter(text, textArea); - if (before === blockTag && after === blockTag) { + const shouldRemoveBlock = + lineBefore(text, textArea) === blockTag && lineAfter(text, textArea) === blockTag; + + if (shouldRemoveBlock) { // To remove the block tag we have to select the line before & after if (blockTag != null) { textArea.selectionStart = textArea.selectionStart - (blockTag.length + 1); textArea.selectionEnd = textArea.selectionEnd + (blockTag.length + 1); } return selected; - } else { - return blockTag + '\n' + selected + '\n' + blockTag; } + return addBlockTags(blockTag, selected); } function moveCursor({ @@ -46,33 +76,48 @@ function moveCursor({ positionBetweenTags, removedLastNewLine, select, + editor, + editorSelectionStart, + editorSelectionEnd, }) { var pos; - if (!textArea.setSelectionRange) { + if (textArea && !textArea.setSelectionRange) { return; } if (select && select.length > 0) { - // calculate the part of the text to be selected - const startPosition = textArea.selectionStart - (tag.length - tag.indexOf(select)); - const endPosition = startPosition + select.length; - return textArea.setSelectionRange(startPosition, endPosition); - } - if (textArea.selectionStart === textArea.selectionEnd) { - if (positionBetweenTags) { - pos = textArea.selectionStart - tag.length; - } else { - pos = textArea.selectionStart; + if (textArea) { + // calculate the part of the text to be selected + const startPosition = textArea.selectionStart - (tag.length - tag.indexOf(select)); + const endPosition = startPosition + select.length; + return textArea.setSelectionRange(startPosition, endPosition); + } else if (editor) { + editor.navigateLeft(tag.length - tag.indexOf(select)); + editor.getSelection().selectAWord(); + return; } + } + if (textArea) { + if (textArea.selectionStart === textArea.selectionEnd) { + if (positionBetweenTags) { + pos = textArea.selectionStart - tag.length; + } else { + pos = textArea.selectionStart; + } - if (removedLastNewLine) { - pos -= 1; - } + if (removedLastNewLine) { + pos -= 1; + } - if (cursorOffset) { - pos -= cursorOffset; - } + if (cursorOffset) { + pos -= cursorOffset; + } - return textArea.setSelectionRange(pos, pos); + return textArea.setSelectionRange(pos, pos); + } + } else if (editor && editorSelectionStart.row === editorSelectionEnd.row) { + if (positionBetweenTags) { + editor.navigateLeft(tag.length); + } } } @@ -85,6 +130,7 @@ export function insertMarkdownText({ selected = '', wrap, select, + editor, }) { var textToInsert, selectedSplit, @@ -92,11 +138,20 @@ export function insertMarkdownText({ removedLastNewLine, removedFirstNewLine, currentLineEmpty, - lastNewLine; + lastNewLine, + editorSelectionStart, + editorSelectionEnd; removedLastNewLine = false; removedFirstNewLine = false; currentLineEmpty = false; + if (editor) { + const selectionRange = editor.getSelectionRange(); + + editorSelectionStart = selectionRange.start; + editorSelectionEnd = selectionRange.end; + } + // check for link pattern and selected text is an URL // if so fill in the url part instead of the text part of the pattern. if (tag === LINK_TAG_PATTERN) { @@ -119,14 +174,27 @@ export function insertMarkdownText({ } // Remove the last newline - if (textArea.selectionEnd - textArea.selectionStart > selected.replace(/\n$/, '').length) { - removedLastNewLine = true; - selected = selected.replace(/\n$/, ''); + if (textArea) { + if (textArea.selectionEnd - textArea.selectionStart > selected.replace(/\n$/, '').length) { + removedLastNewLine = true; + selected = selected.replace(/\n$/, ''); + } + } else if (editor) { + if (editorSelectionStart.row !== editorSelectionEnd.row) { + removedLastNewLine = true; + selected = selected.replace(/\n$/, ''); + } } selectedSplit = selected.split('\n'); - if (!wrap) { + if (editor && !wrap) { + lastNewLine = editor.getValue().split('\n')[editorSelectionStart.row]; + + if (/^\s*$/.test(lastNewLine)) { + currentLineEmpty = true; + } + } else if (textArea && !wrap) { lastNewLine = textArea.value.substr(0, textArea.selectionStart).lastIndexOf('\n'); // Check whether the current line is empty or consists only of spaces(=handle as empty) @@ -135,13 +203,19 @@ export function insertMarkdownText({ } } - startChar = !wrap && !currentLineEmpty && textArea.selectionStart > 0 ? '\n' : ''; + const isBeginning = + (textArea && textArea.selectionStart === 0) || + (editor && editorSelectionStart.column === 0 && editorSelectionStart.row === 0); + + startChar = !wrap && !currentLineEmpty && !isBeginning ? '\n' : ''; const textPlaceholder = '{text}'; if (selectedSplit.length > 1 && (!wrap || (blockTag != null && blockTag !== ''))) { if (blockTag != null && blockTag !== '') { - textToInsert = blockTagText(text, textArea, blockTag, selected); + textToInsert = editor + ? editorBlockTagText(text, blockTag, selected, editor) + : blockTagText(text, textArea, blockTag, selected); } else { textToInsert = selectedSplit .map(function(val) { @@ -170,7 +244,11 @@ export function insertMarkdownText({ textToInsert += '\n'; } - insertText(textArea, textToInsert); + if (editor) { + editor.insert(textToInsert); + } else { + insertText(textArea, textToInsert); + } return moveCursor({ textArea, tag: tag.replace(textPlaceholder, selected), @@ -178,6 +256,9 @@ export function insertMarkdownText({ positionBetweenTags: wrap && selected.length === 0, removedLastNewLine, select, + editor, + editorSelectionStart, + editorSelectionEnd, }); } @@ -217,6 +298,25 @@ export function addMarkdownListeners(form) { }); } +export function addEditorMarkdownListeners(editor) { + $('.js-md') + .off('click') + .on('click', function(e) { + const { mdTag, mdBlock, mdPrepend, mdSelect } = $(e.currentTarget).data(); + + insertMarkdownText({ + tag: mdTag, + blockTag: mdBlock, + wrap: !mdPrepend, + select: mdSelect, + selected: editor.getSelectedText(), + text: editor.getValue(), + editor, + }); + editor.focus(); + }); +} + export function removeMarkdownListeners(form) { return $('.js-md', form).off('click'); } diff --git a/app/assets/javascripts/notes/components/discussion_filter.vue b/app/assets/javascripts/notes/components/discussion_filter.vue index 86c114a761a..f5c410211b6 100644 --- a/app/assets/javascripts/notes/components/discussion_filter.vue +++ b/app/assets/javascripts/notes/components/discussion_filter.vue @@ -2,7 +2,11 @@ import $ from 'jquery'; import { mapGetters, mapActions } from 'vuex'; import Icon from '~/vue_shared/components/icon.vue'; -import { DISCUSSION_FILTERS_DEFAULT_VALUE, HISTORY_ONLY_FILTER_VALUE } from '../constants'; +import { + DISCUSSION_FILTERS_DEFAULT_VALUE, + HISTORY_ONLY_FILTER_VALUE, + DISCUSSION_TAB_LABEL, +} from '../constants'; export default { components: { @@ -23,6 +27,7 @@ export default { return { currentValue: this.selectedValue, defaultValue: DISCUSSION_FILTERS_DEFAULT_VALUE, + displayFilters: true, }; }, computed: { @@ -32,6 +37,14 @@ export default { return this.filters.find(filter => filter.value === this.currentValue); }, }, + created() { + if (window.mrTabs) { + const { eventHub, currentTab } = window.mrTabs; + + eventHub.$on('MergeRequestTabChange', this.toggleFilters); + this.toggleFilters(currentTab); + } + }, mounted() { this.toggleCommentsForm(); }, @@ -51,12 +64,15 @@ export default { toggleCommentsForm() { this.setCommentsDisabled(this.currentValue === HISTORY_ONLY_FILTER_VALUE); }, + toggleFilters(tab) { + this.displayFilters = tab === DISCUSSION_TAB_LABEL; + }, }, }; </script> <template> - <div class="discussion-filter-container d-inline-block align-bottom"> + <div v-if="displayFilters" class="discussion-filter-container d-inline-block align-bottom"> <button id="discussion-filter-dropdown" ref="dropdownToggle" diff --git a/app/assets/javascripts/notes/components/note_form.vue b/app/assets/javascripts/notes/components/note_form.vue index 9b7f3d3588d..e78596f8b52 100644 --- a/app/assets/javascripts/notes/components/note_form.vue +++ b/app/assets/javascripts/notes/components/note_form.vue @@ -226,7 +226,7 @@ export default { <button :disabled="isDisabled" type="button" - class="js-vue-issue-save btn btn-success js-comment-button" + class="js-vue-issue-save btn btn-success js-comment-button qa-reply-comment-button" @click="handleUpdate();" > {{ saveButtonTitle }} diff --git a/app/assets/javascripts/notes/components/toggle_replies_widget.vue b/app/assets/javascripts/notes/components/toggle_replies_widget.vue index 72a8ff28466..f1b0b12bdce 100644 --- a/app/assets/javascripts/notes/components/toggle_replies_widget.vue +++ b/app/assets/javascripts/notes/components/toggle_replies_widget.vue @@ -57,7 +57,7 @@ export default { tooltip-placement="bottom" /> </div> - <button class="btn btn-link js-replies-text" type="button" @click="toggle"> + <button class="btn btn-link js-replies-text qa-expand-replies" type="button" @click="toggle"> {{ replies.length }} {{ n__('reply', 'replies', replies.length) }} </button> {{ __('Last reply by') }} @@ -66,7 +66,11 @@ export default { </a> <time-ago-tooltip :time="lastReply.created_at" tooltip-placement="bottom" /> </template> - <span v-else class="collapse-replies-btn js-collapse-replies" @click="toggle"> + <span + v-else + class="collapse-replies-btn js-collapse-replies qa-collapse-replies" + @click="toggle" + > <icon name="chevron-down" /> {{ s__('Notes|Collapse replies') }} </span> </li> diff --git a/app/assets/javascripts/notes/constants.js b/app/assets/javascripts/notes/constants.js index 3147dc64c27..78d365fe94b 100644 --- a/app/assets/javascripts/notes/constants.js +++ b/app/assets/javascripts/notes/constants.js @@ -17,6 +17,7 @@ export const RESOLVE_NOTE_METHOD_NAME = 'post'; export const DESCRIPTION_TYPE = 'changed the description'; export const HISTORY_ONLY_FILTER_VALUE = 2; export const DISCUSSION_FILTERS_DEFAULT_VALUE = 0; +export const DISCUSSION_TAB_LABEL = 'show'; export const NOTEABLE_TYPE_MAPPING = { Issue: ISSUE_NOTEABLE_TYPE, diff --git a/app/assets/javascripts/pages/groups/clusters/index/index.js b/app/assets/javascripts/pages/groups/clusters/index/index.js index 845a5f7042c..21efc4f6d00 100644 --- a/app/assets/javascripts/pages/groups/clusters/index/index.js +++ b/app/assets/javascripts/pages/groups/clusters/index/index.js @@ -1,5 +1,7 @@ -import initDismissableCallout from '~/dismissable_callout'; +import PersistentUserCallout from '~/persistent_user_callout'; document.addEventListener('DOMContentLoaded', () => { - initDismissableCallout('.gcp-signup-offer'); + const callout = document.querySelector('.gcp-signup-offer'); + + if (callout) new PersistentUserCallout(callout); // eslint-disable-line no-new }); diff --git a/app/assets/javascripts/pages/groups/index.js b/app/assets/javascripts/pages/groups/index.js index bf80d8b8193..a63a0dbc6b1 100644 --- a/app/assets/javascripts/pages/groups/index.js +++ b/app/assets/javascripts/pages/groups/index.js @@ -1,6 +1,12 @@ -import initDismissableCallout from '~/dismissable_callout'; +import PersistentUserCallout from '~/persistent_user_callout'; import initGkeDropdowns from '~/projects/gke_cluster_dropdowns'; +function initGcpSignupCallout() { + const callout = document.querySelector('.gcp-signup-offer'); + + if (callout) new PersistentUserCallout(callout); // eslint-disable-line no-new +} + document.addEventListener('DOMContentLoaded', () => { const { page } = document.body.dataset; const newClusterViews = [ @@ -10,7 +16,7 @@ document.addEventListener('DOMContentLoaded', () => { ]; if (newClusterViews.indexOf(page) > -1) { - initDismissableCallout('.gcp-signup-offer'); + initGcpSignupCallout(); initGkeDropdowns(); } }); diff --git a/app/assets/javascripts/pages/projects/clusters/index/index.js b/app/assets/javascripts/pages/projects/clusters/index/index.js index 845a5f7042c..21efc4f6d00 100644 --- a/app/assets/javascripts/pages/projects/clusters/index/index.js +++ b/app/assets/javascripts/pages/projects/clusters/index/index.js @@ -1,5 +1,7 @@ -import initDismissableCallout from '~/dismissable_callout'; +import PersistentUserCallout from '~/persistent_user_callout'; document.addEventListener('DOMContentLoaded', () => { - initDismissableCallout('.gcp-signup-offer'); + const callout = document.querySelector('.gcp-signup-offer'); + + if (callout) new PersistentUserCallout(callout); // eslint-disable-line no-new }); diff --git a/app/assets/javascripts/pages/projects/index.js b/app/assets/javascripts/pages/projects/index.js index 5659e13981a..b0345b4e50d 100644 --- a/app/assets/javascripts/pages/projects/index.js +++ b/app/assets/javascripts/pages/projects/index.js @@ -1,5 +1,5 @@ -import initDismissableCallout from '~/dismissable_callout'; import initGkeDropdowns from '~/projects/gke_cluster_dropdowns'; +import PersistentUserCallout from '../../persistent_user_callout'; import Project from './project'; import ShortcutsNavigation from '../../behaviors/shortcuts/shortcuts_navigation'; @@ -12,7 +12,9 @@ document.addEventListener('DOMContentLoaded', () => { ]; if (newClusterViews.indexOf(page) > -1) { - initDismissableCallout('.gcp-signup-offer'); + const callout = document.querySelector('.gcp-signup-offer'); + if (callout) new PersistentUserCallout(callout); // eslint-disable-line no-new + initGkeDropdowns(); } diff --git a/app/assets/javascripts/pages/users/user_overview_block.js b/app/assets/javascripts/pages/users/user_overview_block.js index eec2b5ca8e5..e9ecec717d6 100644 --- a/app/assets/javascripts/pages/users/user_overview_block.js +++ b/app/assets/javascripts/pages/users/user_overview_block.js @@ -29,18 +29,21 @@ export default class UserOverviewBlock { render(data) { const { html, count } = data; - const contentList = document.querySelector(`${this.container} .overview-content-list`); + const containerEl = document.querySelector(this.container); + const contentList = containerEl.querySelector('.overview-content-list'); contentList.innerHTML += html; - const loadingEl = document.querySelector(`${this.container} .loading`); + const loadingEl = containerEl.querySelector('.loading'); if (count && count > 0) { - document.querySelector(`${this.container} .js-view-all`).classList.remove('hide'); + containerEl.querySelector('.js-view-all').classList.remove('hide'); } else { - document - .querySelector(`${this.container} .nothing-here-block`) - .classList.add('text-left', 'p-0'); + const nothingHereBlock = containerEl.querySelector('.nothing-here-block'); + + if (nothingHereBlock) { + nothingHereBlock.classList.add('text-left', 'p-0'); + } } loadingEl.classList.add('hide'); diff --git a/app/assets/javascripts/persistent_user_callout.js b/app/assets/javascripts/persistent_user_callout.js new file mode 100644 index 00000000000..1e34e74a152 --- /dev/null +++ b/app/assets/javascripts/persistent_user_callout.js @@ -0,0 +1,34 @@ +import axios from './lib/utils/axios_utils'; +import { __ } from './locale'; +import Flash from './flash'; + +export default class PersistentUserCallout { + constructor(container) { + const { dismissEndpoint, featureId } = container.dataset; + this.container = container; + this.dismissEndpoint = dismissEndpoint; + this.featureId = featureId; + + this.init(); + } + + init() { + const closeButton = this.container.querySelector('.js-close'); + closeButton.addEventListener('click', event => this.dismiss(event)); + } + + dismiss(event) { + event.preventDefault(); + + axios + .post(this.dismissEndpoint, { + feature_name: this.featureId, + }) + .then(() => { + this.container.remove(); + }) + .catch(() => { + Flash(__('An error occurred while dismissing the alert. Refresh the page and try again.')); + }); + } +} diff --git a/app/assets/javascripts/pipelines/components/pipeline_url.vue b/app/assets/javascripts/pipelines/components/pipeline_url.vue index 30a5bbf92ce..7d8863dff29 100644 --- a/app/assets/javascripts/pipelines/components/pipeline_url.vue +++ b/app/assets/javascripts/pipelines/components/pipeline_url.vue @@ -65,7 +65,7 @@ export default { v-if="pipeline.flags.latest" v-gl-tooltip class="js-pipeline-url-latest badge badge-success" - title="__('Latest pipeline for this branch')" + :title="__('Latest pipeline for this branch')" > latest </span> @@ -100,7 +100,7 @@ export default { <span v-if="pipeline.flags.merge_request" v-gl-tooltip - title="__('This pipeline is run in a merge request context')" + :title="__('This pipeline is run in a merge request context')" class="js-pipeline-url-mergerequest badge badge-info" > merge request diff --git a/app/assets/javascripts/set_status_modal/emoji_menu_in_modal.js b/app/assets/javascripts/set_status_modal/emoji_menu_in_modal.js index 14a89ef9293..3a8631a196f 100644 --- a/app/assets/javascripts/set_status_modal/emoji_menu_in_modal.js +++ b/app/assets/javascripts/set_status_modal/emoji_menu_in_modal.js @@ -12,9 +12,8 @@ class EmojiMenuInModal extends AwardsHandler { this.bindEvents(); } - postEmoji($emojiButton, awardUrl, selectedEmoji, callback) { + postEmoji($emojiButton, awardUrl, selectedEmoji) { this.selectEmojiCallback(selectedEmoji, this.emoji.glEmojiTag(selectedEmoji)); - callback(); } } diff --git a/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue b/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue index d8a75388e84..b7f12076958 100644 --- a/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue +++ b/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue @@ -106,6 +106,9 @@ export default { (!this.mr.isNothingToMergeState && !this.mr.isMergedState) ); }, + shouldRenderCollaborationStatus() { + return this.mr.allowCollaboration && this.mr.isOpen; + }, shouldRenderMergedPipeline() { return this.mr.state === 'merged' && !_.isEmpty(this.mr.mergePipeline); }, @@ -315,7 +318,7 @@ export default { <div class="mr-widget-section"> <component :is="componentName" :mr="mr" :service="service" /> - <section v-if="mr.allowCollaboration" class="mr-info-list mr-links"> + <section v-if="shouldRenderCollaborationStatus" class="mr-info-list mr-links"> {{ s__('mrWidget|Allows commits from members who can merge to the target branch') }} </section> diff --git a/app/assets/javascripts/vue_shared/components/callout.vue b/app/assets/javascripts/vue_shared/components/callout.vue index ddbb14ae812..56bafebf4ce 100644 --- a/app/assets/javascripts/vue_shared/components/callout.vue +++ b/app/assets/javascripts/vue_shared/components/callout.vue @@ -11,13 +11,14 @@ export default { }, message: { type: String, - required: true, + required: false, + default: '', }, }, }; </script> <template> <div :class="`bs-callout bs-callout-${category}`" role="alert" aria-live="assertive"> - {{ message }} + {{ message }} <slot></slot> </div> </template> diff --git a/app/assets/javascripts/vue_shared/components/gl_modal_vuex.vue b/app/assets/javascripts/vue_shared/components/gl_modal_vuex.vue new file mode 100644 index 00000000000..df6fadf10cd --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/gl_modal_vuex.vue @@ -0,0 +1,69 @@ +<script> +import { mapState, mapActions } from 'vuex'; +import { GlModal } from '@gitlab/ui'; + +/** + * This component keeps the GlModal's visibility in sync with the given vuex module. + */ +export default { + components: { + GlModal, + }, + props: { + modalId: { + type: String, + required: true, + }, + modalModule: { + type: String, + required: true, + }, + }, + computed: { + ...mapState({ + isVisible(state) { + return state[this.modalModule].isVisible; + }, + }), + attrs() { + const { modalId, modalModule, ...attrs } = this.$attrs; + + return attrs; + }, + }, + watch: { + isVisible(val) { + return val ? this.bsShow() : this.bsHide(); + }, + }, + methods: { + ...mapActions({ + syncShow(dispatch) { + return dispatch(`${this.modalModule}/show`); + }, + syncHide(dispatch) { + return dispatch(`${this.modalModule}/hide`); + }, + }), + bsShow() { + this.$root.$emit('bv::show::modal', this.modalId); + }, + bsHide() { + // $root.$emit is a workaround because other b-modal approaches don't work yet with gl-modal + this.$root.$emit('bv::hide::modal', this.modalId); + }, + }, +}; +</script> + +<template> + <gl-modal + v-bind="attrs" + :modal-id="modalId" + v-on="$listeners" + @shown="syncShow" + @hidden="syncHide" + > + <slot></slot> + </gl-modal> +</template> diff --git a/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_image.vue b/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_image.vue index e833a8e0483..95f4395ac13 100644 --- a/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_image.vue +++ b/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_image.vue @@ -67,6 +67,7 @@ export default { // In both cases we should render the defaultAvatarUrl sanitizedSource() { let baseSrc = this.imgSrc === '' || this.imgSrc === null ? defaultAvatarUrl : this.imgSrc; + // Only adds the width to the URL if its not a base64 data image if (!baseSrc.startsWith('data:') && !baseSrc.includes('?')) baseSrc += `?width=${this.size}`; return baseSrc; }, diff --git a/app/assets/javascripts/vuex_shared/modules/modal/actions.js b/app/assets/javascripts/vuex_shared/modules/modal/actions.js new file mode 100644 index 00000000000..552237e05c5 --- /dev/null +++ b/app/assets/javascripts/vuex_shared/modules/modal/actions.js @@ -0,0 +1,17 @@ +import * as types from './mutation_types'; + +export const open = ({ commit }, data) => { + commit(types.OPEN, data); +}; + +export const close = ({ commit }) => { + commit(types.CLOSE); +}; + +export const show = ({ commit }) => { + commit(types.SHOW); +}; + +export const hide = ({ commit }) => { + commit(types.HIDE); +}; diff --git a/app/assets/javascripts/vuex_shared/modules/modal/index.js b/app/assets/javascripts/vuex_shared/modules/modal/index.js new file mode 100644 index 00000000000..c349d875c24 --- /dev/null +++ b/app/assets/javascripts/vuex_shared/modules/modal/index.js @@ -0,0 +1,10 @@ +import state from './state'; +import mutations from './mutations'; +import * as actions from './actions'; + +export default () => ({ + namespaced: true, + state: state(), + mutations, + actions, +}); diff --git a/app/assets/javascripts/vuex_shared/modules/modal/mutation_types.js b/app/assets/javascripts/vuex_shared/modules/modal/mutation_types.js new file mode 100644 index 00000000000..f8259736009 --- /dev/null +++ b/app/assets/javascripts/vuex_shared/modules/modal/mutation_types.js @@ -0,0 +1,4 @@ +export const HIDE = 'HIDE'; +export const SHOW = 'SHOW'; +export const OPEN = 'OPEN'; +export const CLOSE = 'CLOSE'; diff --git a/app/assets/javascripts/vuex_shared/modules/modal/mutations.js b/app/assets/javascripts/vuex_shared/modules/modal/mutations.js new file mode 100644 index 00000000000..9e96ae8b5a9 --- /dev/null +++ b/app/assets/javascripts/vuex_shared/modules/modal/mutations.js @@ -0,0 +1,18 @@ +import * as types from './mutation_types'; + +export default { + [types.SHOW](state) { + state.isVisible = true; + }, + [types.HIDE](state) { + state.isVisible = false; + }, + [types.OPEN](state, data) { + state.data = data; + state.isVisible = true; + }, + [types.CLOSE](state) { + state.data = null; + state.isVisible = false; + }, +}; diff --git a/app/assets/javascripts/vuex_shared/modules/modal/state.js b/app/assets/javascripts/vuex_shared/modules/modal/state.js new file mode 100644 index 00000000000..5d0955aa9b0 --- /dev/null +++ b/app/assets/javascripts/vuex_shared/modules/modal/state.js @@ -0,0 +1,4 @@ +export default () => ({ + isVisible: false, + data: null, +}); diff --git a/app/assets/stylesheets/bootstrap_migration.scss b/app/assets/stylesheets/bootstrap_migration.scss index f0671e36130..587127bb059 100644 --- a/app/assets/stylesheets/bootstrap_migration.scss +++ b/app/assets/stylesheets/bootstrap_migration.scss @@ -70,6 +70,17 @@ h6, margin-bottom: 10px; } +/* Our adjustments to hx & .hx above add unnecessary margins to modal-title + and page-title in modals, so we set them to 0 in order to have properly + formatted modal headers. */ +.modal-header { + .modal-title, + .page-title { + margin-top: 0; + margin-bottom: 0; + } +} + h5, .h5 { font-size: $gl-font-size; @@ -134,7 +145,8 @@ table { pointer-events: none; } -.popover { +.popover, +.popover-header { font-size: 14px; } @@ -142,7 +154,9 @@ table { @include media-breakpoint-up($breakpoint) { $infix: breakpoint-infix($breakpoint, $grid-breakpoints); - .d#{$infix}-table-header-group { display: table-header-group !important; } + .d#{$infix}-table-header-group { + display: table-header-group !important; + } } } diff --git a/app/assets/stylesheets/framework.scss b/app/assets/stylesheets/framework.scss index 834e7ffce81..62d471bc30c 100644 --- a/app/assets/stylesheets/framework.scss +++ b/app/assets/stylesheets/framework.scss @@ -31,7 +31,6 @@ @import 'framework/logo'; @import 'framework/markdown_area'; @import 'framework/media_object'; -@import 'framework/mobile'; @import 'framework/modal'; @import 'framework/pagination'; @import 'framework/panels'; diff --git a/app/assets/stylesheets/framework/animations.scss b/app/assets/stylesheets/framework/animations.scss index 549a8730301..43d4044033f 100644 --- a/app/assets/stylesheets/framework/animations.scss +++ b/app/assets/stylesheets/framework/animations.scss @@ -260,3 +260,25 @@ $skeleton-line-widths: ( .slide-down-leave-to { transform: translateY(-30%); } + +@keyframes spin { + 0% { transform: rotate(0deg);} + 100% { transform: rotate(360deg);} +} + +/** COMMON ANIMATION CLASSES **/ +.transform-origin-center { @include webkit-prefix(transform-origin, 50% 50%); } +.animate-n-spin { @include webkit-prefix(animation-name, spin); } +.animate-c-infinite { @include webkit-prefix(animation-iteration-count, infinite); } +.animate-t-linear { @include webkit-prefix(animation-timing-function, linear); } +.animate-d-1 { @include webkit-prefix(animation-duration, 1s); } +.animate-d-2 { @include webkit-prefix(animation-duration, 2s); } + +/** COMPOSITE ANIMATION CLASSES **/ +.gl-spinner { + @include webkit-prefix(animation-name, spin); + @include webkit-prefix(animation-iteration-count, infinite); + @include webkit-prefix(animation-timing-function, linear); + @include webkit-prefix(animation-duration, 1s); + transform-origin: 50% 50%; +} diff --git a/app/assets/stylesheets/framework/header.scss b/app/assets/stylesheets/framework/header.scss index 7d283dcfb71..5574873fa22 100644 --- a/app/assets/stylesheets/framework/header.scss +++ b/app/assets/stylesheets/framework/header.scss @@ -597,3 +597,11 @@ @include emoji-menu-toggle-button; } } + +.nav-links > li > a { + .badge.badge-pill { + @include media-breakpoint-down(xs) { display: none; } + } + + @include media-breakpoint-down(xs) { margin-right: 3px; } +} diff --git a/app/assets/stylesheets/framework/issue_box.scss b/app/assets/stylesheets/framework/issue_box.scss index a66604e56ff..e51f230a680 100644 --- a/app/assets/stylesheets/framework/issue_box.scss +++ b/app/assets/stylesheets/framework/issue_box.scss @@ -45,9 +45,4 @@ &.status-box-upcoming { background: $gl-text-color-secondary; } - - &.status-box-milestone { - color: $gl-text-color; - background: $gray-darker; - } } diff --git a/app/assets/stylesheets/framework/layout.scss b/app/assets/stylesheets/framework/layout.scss index 9218df9b40f..97cb9d90ff0 100644 --- a/app/assets/stylesheets/framework/layout.scss +++ b/app/assets/stylesheets/framework/layout.scss @@ -40,6 +40,14 @@ body { .content { margin: 0; + + @include media-breakpoint-down(xs) { margin-top: 20px; } + } + + @include media-breakpoint-down(xs) { + .container .title { + padding-left: 15px !important; + } } } diff --git a/app/assets/stylesheets/framework/markdown_area.scss b/app/assets/stylesheets/framework/markdown_area.scss index 5609a2086e6..ce46d760d7b 100644 --- a/app/assets/stylesheets/framework/markdown_area.scss +++ b/app/assets/stylesheets/framework/markdown_area.scss @@ -173,7 +173,7 @@ svg { width: 14px; height: 14px; - margin-top: 3px; + vertical-align: middle; fill: $gl-text-color-secondary; } @@ -307,4 +307,8 @@ overflow: hidden; text-overflow: ellipsis; } + + .referenced-users { + margin-right: 0; + } } diff --git a/app/assets/stylesheets/framework/mobile.scss b/app/assets/stylesheets/framework/mobile.scss deleted file mode 100644 index 3bb046d0e51..00000000000 --- a/app/assets/stylesheets/framework/mobile.scss +++ /dev/null @@ -1,88 +0,0 @@ -/** Common mobile (screen XS, SM) styles **/ -@include media-breakpoint-down(xs) { - .container .content { - margin-top: 20px; - } - - .nav-links > li > a { - padding: 10px; - font-size: 12px; - margin-right: 3px; - - .badge.badge-pill { - display: none; - } - } - - .referenced-users { - margin-right: 0; - } - - .issues-details-filters:not(.filtered-search-block), - .dash-projects-filters, - .check-all-holder { - display: none; - } - - .rss-btn { - display: none; - } - - .project-home-links { - display: none; - } - - .project-home-panel { - padding-left: 0 !important; - - .project-repo-buttons, - .git-clone-holder { - display: none; - } - } - - .group-buttons { - display: none; - } - - .container .title { - padding-left: 15px !important; - } - - .nav-links, - .nav-links { - li a { - font-size: 14px; - padding: 19px 10px; - } - } - - .activity-filter-block { - display: none; - } - - .projects-search-form { - .btn { - display: none; - } - } -} - -@include media-breakpoint-down(sm) { - .issues-filters { - .milestone-filter { - display: none; - } - } - - .page-title { - .note-created-ago, - .new-issue-link { - display: none; - } - } - - aside:not(.right-sidebar) { - display: none; - } -} diff --git a/app/assets/stylesheets/framework/modal.scss b/app/assets/stylesheets/framework/modal.scss index 95291b4a9ad..46d40ea7aa5 100644 --- a/app/assets/stylesheets/framework/modal.scss +++ b/app/assets/stylesheets/framework/modal.scss @@ -29,10 +29,6 @@ padding-right: 28px; } } - - .page-title { - margin-top: 0; - } } .modal-body { diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss index 343c09b4a3e..d92d81b2cb5 100644 --- a/app/assets/stylesheets/framework/variables.scss +++ b/app/assets/stylesheets/framework/variables.scss @@ -32,6 +32,15 @@ $gray-dark: darken($gray-light, $darken-dark-factor); $gray-darker: #eee; $gray-darkest: #c4c4c4; +$black: #000; +$black-transparent: rgba(0, 0, 0, 0.3); +$almost-black: #242424; + +$t-gray-a-02: rgba($black, 0.02); +$t-gray-a-04: rgba($black, 0.04); +$t-gray-a-06: rgba($black, 0.06); +$t-gray-a-08: rgba($black, 0.08); + $gl-gray-100: #dddddd; $gl-gray-200: #cccccc; $gl-gray-350: #aaaaaa; @@ -170,11 +179,6 @@ $theme-light-red-500: #c24b38; $theme-light-red-600: #b03927; $theme-light-red-700: #a62e21; -$black: #000; -$black-transparent: rgba(0, 0, 0, 0.3); -$shadow-color: rgba($black, 0.1); -$almost-black: #242424; - $border-white-light: darken($white-light, $darken-border-factor); $border-white-normal: darken($white-normal, $darken-border-factor); @@ -187,6 +191,7 @@ $border-gray-dark: darken($white-normal, $darken-border-factor); * UI elements */ $border-color: #e5e5e5; +$shadow-color: $t-gray-a-08; $well-expand-item: #e8f2f7; $well-inner-border: #eef0f2; $well-light-border: #f1f1f1; @@ -198,7 +203,6 @@ $well-light-text-color: #5b6169; $gl-font-size: 14px; $gl-font-size-xs: 11px; $gl-font-size-small: 12px; -$gl-font-size-medium: 20px; $gl-font-size-large: 16px; $gl-font-weight-normal: 400; $gl-font-weight-bold: 600; diff --git a/app/assets/stylesheets/pages/editor.scss b/app/assets/stylesheets/pages/editor.scss index f46ff360496..5a988b184b6 100644 --- a/app/assets/stylesheets/pages/editor.scss +++ b/app/assets/stylesheets/pages/editor.scss @@ -128,6 +128,10 @@ width: 100%; } } + + @media(max-width: map-get($grid-breakpoints, md)-1) { + clear: both; + } } .editor-ref { diff --git a/app/assets/stylesheets/pages/milestone.scss b/app/assets/stylesheets/pages/milestone.scss index 1e92582d6d9..94bf32945fc 100644 --- a/app/assets/stylesheets/pages/milestone.scss +++ b/app/assets/stylesheets/pages/milestone.scss @@ -1,3 +1,5 @@ +$status-box-line-height: 26px; + .issues-sortable-list .str-truncated { max-width: 90%; } @@ -38,6 +40,7 @@ font-size: $tooltip-font-size; margin-top: 0; margin-right: $gl-padding-4; + line-height: $status-box-line-height; @include media-breakpoint-down(xs) { line-height: unset; diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss index fdd17af35fb..7a47e0a2836 100644 --- a/app/assets/stylesheets/pages/pipelines.scss +++ b/app/assets/stylesheets/pages/pipelines.scss @@ -978,7 +978,6 @@ button.mini-pipeline-graph-dropdown-toggle { * Top arrow in the dropdown in the mini pipeline graph */ .mini-pipeline-graph-dropdown-menu { - z-index: 200; &::before, &::after { diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss index 0ce0db038a7..004c49dd226 100644 --- a/app/assets/stylesheets/pages/projects.scss +++ b/app/assets/stylesheets/pages/projects.scss @@ -973,7 +973,7 @@ pre.light-well { padding: $gl-padding 0; @include media-breakpoint-up(lg) { - padding: $gl-padding-24 0; + padding: $gl-padding 0; } &.no-description { @@ -990,7 +990,7 @@ pre.light-well { } h2 { - font-size: $gl-font-size-medium; + font-size: $gl-font-size-large; font-weight: $gl-font-weight-bold; margin-bottom: 0; @@ -1049,7 +1049,7 @@ pre.light-well { } .controls { - margin-top: $gl-padding; + margin-top: $gl-padding-8; @include media-breakpoint-down(md) { margin-top: 0; |