diff options
author | Dennis Tang <dtang@gitlab.com> | 2018-06-19 18:27:39 +0300 |
---|---|---|
committer | Dennis Tang <dtang@gitlab.com> | 2018-06-19 18:27:39 +0300 |
commit | dc31dac219a2b7c4ceb390005d8b48dec8904a5b (patch) | |
tree | d3d4ffe58d1db802c941722bf209b243f6d90435 /app | |
parent | 3e5899ad9839d3ef8a920da8b6948ba4d0fa2c06 (diff) | |
parent | 9092e96abc165ccd76d5b54fcd507c8c989b99ec (diff) |
Merge remote-tracking branch 'origin/master' into 43446-new-cluster-page-tabs
Diffstat (limited to 'app')
760 files changed, 7887 insertions, 4138 deletions
diff --git a/app/assets/images/ci_favicons/dev/favicon_status_canceled.ico b/app/assets/images/ci_favicons/dev/favicon_status_canceled.ico Binary files differdeleted file mode 100644 index 4af3582b60d..00000000000 --- a/app/assets/images/ci_favicons/dev/favicon_status_canceled.ico +++ /dev/null diff --git a/app/assets/images/ci_favicons/dev/favicon_status_created.ico b/app/assets/images/ci_favicons/dev/favicon_status_created.ico Binary files differdeleted file mode 100644 index 13639da2e8a..00000000000 --- a/app/assets/images/ci_favicons/dev/favicon_status_created.ico +++ /dev/null diff --git a/app/assets/images/ci_favicons/dev/favicon_status_failed.ico b/app/assets/images/ci_favicons/dev/favicon_status_failed.ico Binary files differdeleted file mode 100644 index 5f0e711b104..00000000000 --- a/app/assets/images/ci_favicons/dev/favicon_status_failed.ico +++ /dev/null diff --git a/app/assets/images/ci_favicons/dev/favicon_status_manual.ico b/app/assets/images/ci_favicons/dev/favicon_status_manual.ico Binary files differdeleted file mode 100644 index 8b1168a1267..00000000000 --- a/app/assets/images/ci_favicons/dev/favicon_status_manual.ico +++ /dev/null diff --git a/app/assets/images/ci_favicons/dev/favicon_status_not_found.ico b/app/assets/images/ci_favicons/dev/favicon_status_not_found.ico Binary files differdeleted file mode 100644 index ed19b69e1c5..00000000000 --- a/app/assets/images/ci_favicons/dev/favicon_status_not_found.ico +++ /dev/null diff --git a/app/assets/images/ci_favicons/dev/favicon_status_pending.ico b/app/assets/images/ci_favicons/dev/favicon_status_pending.ico Binary files differdeleted file mode 100644 index 5dfefd4cc5a..00000000000 --- a/app/assets/images/ci_favicons/dev/favicon_status_pending.ico +++ /dev/null diff --git a/app/assets/images/ci_favicons/dev/favicon_status_running.ico b/app/assets/images/ci_favicons/dev/favicon_status_running.ico Binary files differdeleted file mode 100644 index a41539c0e3e..00000000000 --- a/app/assets/images/ci_favicons/dev/favicon_status_running.ico +++ /dev/null diff --git a/app/assets/images/ci_favicons/dev/favicon_status_skipped.ico b/app/assets/images/ci_favicons/dev/favicon_status_skipped.ico Binary files differdeleted file mode 100644 index 2c1ae552b93..00000000000 --- a/app/assets/images/ci_favicons/dev/favicon_status_skipped.ico +++ /dev/null diff --git a/app/assets/images/ci_favicons/dev/favicon_status_success.ico b/app/assets/images/ci_favicons/dev/favicon_status_success.ico Binary files differdeleted file mode 100644 index 70f0ca61eca..00000000000 --- a/app/assets/images/ci_favicons/dev/favicon_status_success.ico +++ /dev/null diff --git a/app/assets/images/ci_favicons/dev/favicon_status_warning.ico b/app/assets/images/ci_favicons/dev/favicon_status_warning.ico Binary files differdeleted file mode 100644 index db289e03eb1..00000000000 --- a/app/assets/images/ci_favicons/dev/favicon_status_warning.ico +++ /dev/null diff --git a/app/assets/images/ci_favicons/favicon_status_canceled.ico b/app/assets/images/ci_favicons/favicon_status_canceled.ico Binary files differdeleted file mode 100644 index 23adcffff50..00000000000 --- a/app/assets/images/ci_favicons/favicon_status_canceled.ico +++ /dev/null diff --git a/app/assets/images/ci_favicons/favicon_status_canceled.png b/app/assets/images/ci_favicons/favicon_status_canceled.png Binary files differnew file mode 100644 index 00000000000..8adaa9c600b --- /dev/null +++ b/app/assets/images/ci_favicons/favicon_status_canceled.png diff --git a/app/assets/images/ci_favicons/favicon_status_created.ico b/app/assets/images/ci_favicons/favicon_status_created.ico Binary files differdeleted file mode 100644 index f9d93b390d8..00000000000 --- a/app/assets/images/ci_favicons/favicon_status_created.ico +++ /dev/null diff --git a/app/assets/images/ci_favicons/favicon_status_created.png b/app/assets/images/ci_favicons/favicon_status_created.png Binary files differnew file mode 100644 index 00000000000..ca788dd0034 --- /dev/null +++ b/app/assets/images/ci_favicons/favicon_status_created.png diff --git a/app/assets/images/ci_favicons/favicon_status_failed.ico b/app/assets/images/ci_favicons/favicon_status_failed.ico Binary files differdeleted file mode 100644 index 28a22ebf724..00000000000 --- a/app/assets/images/ci_favicons/favicon_status_failed.ico +++ /dev/null diff --git a/app/assets/images/ci_favicons/favicon_status_failed.png b/app/assets/images/ci_favicons/favicon_status_failed.png Binary files differnew file mode 100644 index 00000000000..93f1e2772fd --- /dev/null +++ b/app/assets/images/ci_favicons/favicon_status_failed.png diff --git a/app/assets/images/ci_favicons/favicon_status_manual.ico b/app/assets/images/ci_favicons/favicon_status_manual.ico Binary files differdeleted file mode 100644 index dbbf1abf30c..00000000000 --- a/app/assets/images/ci_favicons/favicon_status_manual.ico +++ /dev/null diff --git a/app/assets/images/ci_favicons/favicon_status_manual.png b/app/assets/images/ci_favicons/favicon_status_manual.png Binary files differnew file mode 100644 index 00000000000..c926062c806 --- /dev/null +++ b/app/assets/images/ci_favicons/favicon_status_manual.png diff --git a/app/assets/images/ci_favicons/favicon_status_not_found.ico b/app/assets/images/ci_favicons/favicon_status_not_found.ico Binary files differdeleted file mode 100644 index 49b9b232dd1..00000000000 --- a/app/assets/images/ci_favicons/favicon_status_not_found.ico +++ /dev/null diff --git a/app/assets/images/ci_favicons/favicon_status_not_found.png b/app/assets/images/ci_favicons/favicon_status_not_found.png Binary files differnew file mode 100644 index 00000000000..df3049315a9 --- /dev/null +++ b/app/assets/images/ci_favicons/favicon_status_not_found.png diff --git a/app/assets/images/ci_favicons/favicon_status_pending.ico b/app/assets/images/ci_favicons/favicon_status_pending.ico Binary files differdeleted file mode 100644 index 05962f3f148..00000000000 --- a/app/assets/images/ci_favicons/favicon_status_pending.ico +++ /dev/null diff --git a/app/assets/images/ci_favicons/favicon_status_pending.png b/app/assets/images/ci_favicons/favicon_status_pending.png Binary files differnew file mode 100644 index 00000000000..f7d67d4a230 --- /dev/null +++ b/app/assets/images/ci_favicons/favicon_status_pending.png diff --git a/app/assets/images/ci_favicons/favicon_status_running.ico b/app/assets/images/ci_favicons/favicon_status_running.ico Binary files differdeleted file mode 100644 index 7fa3d4d48d4..00000000000 --- a/app/assets/images/ci_favicons/favicon_status_running.ico +++ /dev/null diff --git a/app/assets/images/ci_favicons/favicon_status_running.png b/app/assets/images/ci_favicons/favicon_status_running.png Binary files differnew file mode 100644 index 00000000000..ff4167c4b20 --- /dev/null +++ b/app/assets/images/ci_favicons/favicon_status_running.png diff --git a/app/assets/images/ci_favicons/favicon_status_skipped.ico b/app/assets/images/ci_favicons/favicon_status_skipped.ico Binary files differdeleted file mode 100644 index b0c26b62068..00000000000 --- a/app/assets/images/ci_favicons/favicon_status_skipped.ico +++ /dev/null diff --git a/app/assets/images/ci_favicons/favicon_status_skipped.png b/app/assets/images/ci_favicons/favicon_status_skipped.png Binary files differnew file mode 100644 index 00000000000..a9c36464b69 --- /dev/null +++ b/app/assets/images/ci_favicons/favicon_status_skipped.png diff --git a/app/assets/images/ci_favicons/favicon_status_success.ico b/app/assets/images/ci_favicons/favicon_status_success.ico Binary files differdeleted file mode 100644 index b150960b5be..00000000000 --- a/app/assets/images/ci_favicons/favicon_status_success.ico +++ /dev/null diff --git a/app/assets/images/ci_favicons/favicon_status_success.png b/app/assets/images/ci_favicons/favicon_status_success.png Binary files differnew file mode 100644 index 00000000000..bcc30c73f5f --- /dev/null +++ b/app/assets/images/ci_favicons/favicon_status_success.png diff --git a/app/assets/images/ci_favicons/favicon_status_warning.ico b/app/assets/images/ci_favicons/favicon_status_warning.ico Binary files differdeleted file mode 100644 index 7e71d71684d..00000000000 --- a/app/assets/images/ci_favicons/favicon_status_warning.ico +++ /dev/null diff --git a/app/assets/images/ci_favicons/favicon_status_warning.png b/app/assets/images/ci_favicons/favicon_status_warning.png Binary files differnew file mode 100644 index 00000000000..6db3b0280f5 --- /dev/null +++ b/app/assets/images/ci_favicons/favicon_status_warning.png diff --git a/app/assets/images/favicon-blue.ico b/app/assets/images/favicon-blue.ico Binary files differdeleted file mode 100644 index 156fcf07588..00000000000 --- a/app/assets/images/favicon-blue.ico +++ /dev/null diff --git a/app/assets/images/favicon-blue.png b/app/assets/images/favicon-blue.png Binary files differnew file mode 100644 index 00000000000..2229fe79462 --- /dev/null +++ b/app/assets/images/favicon-blue.png diff --git a/app/assets/images/favicon-yellow.ico b/app/assets/images/favicon-yellow.ico Binary files differdeleted file mode 100644 index b650f277fb6..00000000000 --- a/app/assets/images/favicon-yellow.ico +++ /dev/null diff --git a/app/assets/images/favicon-yellow.png b/app/assets/images/favicon-yellow.png Binary files differnew file mode 100644 index 00000000000..2d5289818b4 --- /dev/null +++ b/app/assets/images/favicon-yellow.png diff --git a/app/assets/images/favicon.ico b/app/assets/images/favicon.ico Binary files differdeleted file mode 100644 index 3479cbbb46f..00000000000 --- a/app/assets/images/favicon.ico +++ /dev/null diff --git a/app/assets/images/favicon.png b/app/assets/images/favicon.png Binary files differnew file mode 100644 index 00000000000..845e0ec34a5 --- /dev/null +++ b/app/assets/images/favicon.png diff --git a/app/assets/javascripts/badges/components/badge.vue b/app/assets/javascripts/badges/components/badge.vue index d0f60e1d4cb..b4bfaee1d85 100644 --- a/app/assets/javascripts/badges/components/badge.vue +++ b/app/assets/javascripts/badges/components/badge.vue @@ -72,11 +72,11 @@ export default { rel="noopener noreferrer" > <img - class="project-badge" :src="imageUrlWithRetries" + class="project-badge" + aria-hidden="true" @load="onLoad" @error="onError" - aria-hidden="true" /> </a> @@ -91,9 +91,9 @@ export default { > <div class="btn btn-default btn-sm disabled"> <icon + :size="16" class="prepend-left-8 append-right-8" name="doc_image" - :size="16" aria-hidden="true" /> </div> @@ -105,16 +105,16 @@ export default { </div> <button + v-tooltip v-show="hasError" + :title="s__('Badges|Reload badge image')" class="btn btn-transparent btn-sm text-primary" type="button" - v-tooltip - :title="s__('Badges|Reload badge image')" @click="reloadImage" > <icon - name="retry" :size="16" + name="retry" /> </button> </div> diff --git a/app/assets/javascripts/badges/components/badge_form.vue b/app/assets/javascripts/badges/components/badge_form.vue index 5975cb9669e..7a13f74c570 100644 --- a/app/assets/javascripts/badges/components/badge_form.vue +++ b/app/assets/javascripts/badges/components/badge_form.vue @@ -153,10 +153,10 @@ export default { <label for="badge-link-url">{{ s__('Badges|Link') }}</label> <input id="badge-link-url" - type="text" - class="form-control" v-model="linkUrl" :placeholder="$options.badgeLinkUrlPlaceholder" + type="text" + class="form-control" @input="debouncedPreview" /> <span @@ -169,10 +169,10 @@ export default { <label for="badge-image-url">{{ s__('Badges|Badge image URL') }}</label> <input id="badge-image-url" - type="text" - class="form-control" v-model="imageUrl" :placeholder="$options.badgeImageUrlPlaceholder" + type="text" + class="form-control" @input="debouncedPreview" /> <span @@ -184,8 +184,8 @@ export default { <div class="form-group"> <label for="badge-preview">{{ s__('Badges|Badge image preview') }}</label> <badge - id="badge-preview" v-show="renderedBadge && !isRendering" + id="badge-preview" :image-url="renderedImageUrl" :link-url="renderedLinkUrl" /> @@ -202,16 +202,16 @@ export default { <div class="row-content-block"> <loading-button - type="submit" - container-class="btn btn-success" :disabled="!canSubmit" :loading="isSaving" :label="submitButtonLabel" + type="submit" + container-class="btn btn-success" /> <button + v-if="isEditing" class="btn btn-cancel" type="button" - v-if="isEditing" @click="onCancel" >{{ __('Cancel') }}</button> </div> diff --git a/app/assets/javascripts/badges/components/badge_list_row.vue b/app/assets/javascripts/badges/components/badge_list_row.vue index af062bdf8c6..98aa00af0d7 100644 --- a/app/assets/javascripts/badges/components/badge_list_row.vue +++ b/app/assets/javascripts/badges/components/badge_list_row.vue @@ -41,9 +41,9 @@ export default { <template> <div class="gl-responsive-table-row-layout gl-responsive-table-row"> <badge - class="table-section section-30" :image-url="badge.renderedImageUrl" :link-url="badge.renderedLinkUrl" + class="table-section section-30" /> <span class="table-section section-50 str-truncated">{{ badge.linkUrl }}</span> <div class="table-section section-10"> @@ -54,29 +54,29 @@ export default { v-if="canEditBadge" class="table-action-buttons"> <button + :disabled="badge.isDeleting" class="btn btn-default append-right-8" type="button" - :disabled="badge.isDeleting" @click="editBadge(badge)" > <icon - name="pencil" :size="16" :aria-label="__('Edit')" + name="pencil" /> </button> <button + :disabled="badge.isDeleting" class="btn btn-danger" type="button" data-toggle="modal" data-target="#delete-badge-modal" - :disabled="badge.isDeleting" @click="updateBadgeInModal(badge)" > <icon - name="remove" :size="16" :aria-label="__('Delete')" + name="remove" /> </button> <loading-icon diff --git a/app/assets/javascripts/badges/components/badge_settings.vue b/app/assets/javascripts/badges/components/badge_settings.vue index 83f78394238..cc47e56dd1e 100644 --- a/app/assets/javascripts/badges/components/badge_settings.vue +++ b/app/assets/javascripts/badges/components/badge_settings.vue @@ -44,8 +44,8 @@ export default { <gl-modal id="delete-badge-modal" :header-title-text="s__('Badges|Delete badge?')" - footer-primary-button-variant="danger" :footer-primary-button-text="s__('Badges|Delete badge')" + footer-primary-button-variant="danger" @submit="onSubmitModal"> <div class="well"> <badge diff --git a/app/assets/javascripts/behaviors/markdown/copy_as_gfm.js b/app/assets/javascripts/behaviors/markdown/copy_as_gfm.js index 75cf90de0b5..1ea6dd909e9 100644 --- a/app/assets/javascripts/behaviors/markdown/copy_as_gfm.js +++ b/app/assets/javascripts/behaviors/markdown/copy_as_gfm.js @@ -119,7 +119,7 @@ const gfmRules = { return el.outerHTML; }, 'dl'(el, text) { - let lines = text.trim().split('\n'); + let lines = text.replace(/\n\n/g, '\n').trim().split('\n'); // Add two spaces to the front of subsequent list items lines, // or leave the line entirely blank. lines = lines.map((l) => { @@ -129,9 +129,13 @@ const gfmRules = { return ` ${line}`; }); - return `<dl>\n${lines.join('\n')}\n</dl>`; + return `<dl>\n${lines.join('\n')}\n</dl>\n`; }, - 'sub, dt, dd, kbd, q, samp, var, ruby, rt, rp, abbr, summary, details'(el, text) { + 'dt, dd, summary, details'(el, text) { + const tag = el.nodeName.toLowerCase(); + return `<${tag}>${text}</${tag}>\n`; + }, + 'sup, sub, kbd, q, samp, var, ruby, rt, rp, abbr'(el, text) { const tag = el.nodeName.toLowerCase(); return `<${tag}>${text}</${tag}>`; }, @@ -215,22 +219,22 @@ const gfmRules = { return text.replace(/^- /mg, '1. '); }, 'h1'(el, text) { - return `# ${text.trim()}`; + return `# ${text.trim()}\n`; }, 'h2'(el, text) { - return `## ${text.trim()}`; + return `## ${text.trim()}\n`; }, 'h3'(el, text) { - return `### ${text.trim()}`; + return `### ${text.trim()}\n`; }, 'h4'(el, text) { - return `#### ${text.trim()}`; + return `#### ${text.trim()}\n`; }, 'h5'(el, text) { - return `##### ${text.trim()}`; + return `##### ${text.trim()}\n`; }, 'h6'(el, text) { - return `###### ${text.trim()}`; + return `###### ${text.trim()}\n`; }, 'strong'(el, text) { return `**${text}**`; @@ -241,11 +245,13 @@ const gfmRules = { 'del'(el, text) { return `~~${text}~~`; }, - 'sup'(el, text) { - return `^${text}`; - }, 'hr'(el) { - return '-----'; + // extra leading \n is to ensure that there is a blank line between + // a list followed by an hr, otherwise this breaks old redcarpet rendering + return '\n-----\n'; + }, + 'p'(el, text) { + return `${text.trim()}\n`; }, 'table'(el) { const theadEl = el.querySelector('thead'); @@ -263,7 +269,9 @@ const gfmRules = { let before = ''; let after = ''; - switch (cell.style.textAlign) { + const alignment = cell.align || cell.style.textAlign; + + switch (alignment) { case 'center': before = ':'; after = ':'; diff --git a/app/assets/javascripts/blob/viewer/index.js b/app/assets/javascripts/blob/viewer/index.js index f61c0be9230..5485248cfaf 100644 --- a/app/assets/javascripts/blob/viewer/index.js +++ b/app/assets/javascripts/blob/viewer/index.js @@ -70,7 +70,7 @@ export default class BlobViewer { const initialViewer = this.$fileHolder[0].querySelector('.blob-viewer:not(.hidden)'); let initialViewerName = initialViewer.getAttribute('data-type'); - if (this.switcher && location.hash.indexOf('#L') === 0) { + if (this.switcher && window.location.hash.indexOf('#L') === 0) { initialViewerName = 'simple'; } diff --git a/app/assets/javascripts/boards/components/board.js b/app/assets/javascripts/boards/components/board.js index ac06d79fb60..7920e08e4d8 100644 --- a/app/assets/javascripts/boards/components/board.js +++ b/app/assets/javascripts/boards/components/board.js @@ -1,6 +1,5 @@ /* eslint-disable comma-dangle, space-before-function-paren, one-var */ -import $ from 'jquery'; import Sortable from 'sortablejs'; import Vue from 'vue'; import AccessorUtilities from '../../lib/utils/accessor'; @@ -14,17 +13,28 @@ window.gl = window.gl || {}; window.gl.issueBoards = window.gl.issueBoards || {}; gl.issueBoards.Board = Vue.extend({ - template: '#js-board-template', components: { boardList, 'board-delete': gl.issueBoards.BoardDelete, BoardBlankState, }, props: { - list: Object, - disabled: Boolean, - issueLinkBase: String, - rootPath: String, + list: { + type: Object, + default: () => ({}), + }, + disabled: { + type: Boolean, + required: true, + }, + issueLinkBase: { + type: String, + required: true, + }, + rootPath: { + type: String, + required: true, + }, boardId: { type: String, required: true, @@ -46,56 +56,8 @@ gl.issueBoards.Board = Vue.extend({ }); }, deep: true, - }, - detailIssue: { - handler () { - if (!Object.keys(this.detailIssue.issue).length) return; - - const issue = this.list.findIssue(this.detailIssue.issue.id); - - if (issue) { - const offsetLeft = this.$el.offsetLeft; - const boardsList = document.querySelectorAll('.boards-list')[0]; - const left = boardsList.scrollLeft - offsetLeft; - let right = (offsetLeft + this.$el.offsetWidth); - - if (window.innerWidth > 768 && boardsList.classList.contains('is-compact')) { - // -290 here because width of boardsList is animating so therefore - // getting the width here is incorrect - // 290 is the width of the sidebar - right -= (boardsList.offsetWidth - 290); - } else { - right -= boardsList.offsetWidth; - } - - if (right - boardsList.scrollLeft > 0) { - $(boardsList).animate({ - scrollLeft: right - }, this.sortableOptions.animation); - } else if (left > 0) { - $(boardsList).animate({ - scrollLeft: offsetLeft - }, this.sortableOptions.animation); - } - } - }, - deep: true } }, - methods: { - showNewIssueForm() { - this.$refs['board-list'].showIssueForm = !this.$refs['board-list'].showIssueForm; - }, - toggleExpanded(e) { - if (this.list.isExpandable && !e.target.classList.contains('js-no-trigger-collapse')) { - this.list.isExpanded = !this.list.isExpanded; - - if (AccessorUtilities.isLocalStorageAccessSafe()) { - localStorage.setItem(`boards.${this.boardId}.${this.list.type}.expanded`, this.list.isExpanded); - } - } - }, - }, mounted () { this.sortableOptions = gl.issueBoards.getBoardSortableDefaultOptions({ disabled: this.disabled, @@ -125,4 +87,19 @@ gl.issueBoards.Board = Vue.extend({ this.list.isExpanded = !isCollapsed; } }, + methods: { + showNewIssueForm() { + this.$refs['board-list'].showIssueForm = !this.$refs['board-list'].showIssueForm; + }, + toggleExpanded(e) { + if (this.list.isExpandable && !e.target.classList.contains('js-no-trigger-collapse')) { + this.list.isExpanded = !this.list.isExpanded; + + if (AccessorUtilities.isLocalStorageAccessSafe()) { + localStorage.setItem(`boards.${this.boardId}.${this.list.type}.expanded`, this.list.isExpanded); + } + } + }, + }, + template: '#js-board-template', }); diff --git a/app/assets/javascripts/boards/components/board_blank_state.vue b/app/assets/javascripts/boards/components/board_blank_state.vue index 2049eeb9c30..286529b4d13 100644 --- a/app/assets/javascripts/boards/components/board_blank_state.vue +++ b/app/assets/javascripts/boards/components/board_blank_state.vue @@ -72,8 +72,8 @@ export default { :key="index" > <span - class="label-color" - :style="{ backgroundColor: label.color }"> + :style="{ backgroundColor: label.color }" + class="label-color"> </span> {{ label.title }} </li> diff --git a/app/assets/javascripts/boards/components/board_card.vue b/app/assets/javascripts/boards/components/board_card.vue index 33e3369b971..b7d3574bc80 100644 --- a/app/assets/javascripts/boards/components/board_card.vue +++ b/app/assets/javascripts/boards/components/board_card.vue @@ -77,7 +77,6 @@ export default { <template> <li - class="board-card" :class="{ 'user-can-drag': !disabled && issue.id, 'is-disabled': disabled || !issue.id, @@ -85,6 +84,7 @@ export default { }" :index="index" :data-issue-id="issue.id" + class="board-card" @mousedown="mouseDown" @mousemove="mouseMove" @mouseup="showIssue($event)"> diff --git a/app/assets/javascripts/boards/components/board_delete.js b/app/assets/javascripts/boards/components/board_delete.js index 7be98825fda..4dd9aebeed9 100644 --- a/app/assets/javascripts/boards/components/board_delete.js +++ b/app/assets/javascripts/boards/components/board_delete.js @@ -8,13 +8,16 @@ window.gl.issueBoards = window.gl.issueBoards || {}; gl.issueBoards.BoardDelete = Vue.extend({ props: { - list: Object + list: { + type: Object, + default: () => ({}), + }, }, methods: { deleteBoard () { $(this.$el).tooltip('hide'); - if (confirm('Are you sure you want to delete this list?')) { + if (window.confirm('Are you sure you want to delete this list?')) { this.list.destroy(); } } diff --git a/app/assets/javascripts/boards/components/board_list.vue b/app/assets/javascripts/boards/components/board_list.vue index 84a7f277227..5c7565234d8 100644 --- a/app/assets/javascripts/boards/components/board_list.vue +++ b/app/assets/javascripts/boards/components/board_list.vue @@ -87,10 +87,46 @@ export default { mounted() { const options = gl.issueBoards.getBoardSortableDefaultOptions({ scroll: true, - group: 'issues', disabled: this.disabled, filter: '.board-list-count, .is-disabled', dataIdAttr: 'data-issue-id', + group: { + name: 'issues', + /** + * Dynamically determine between which containers + * items can be moved or copied as + * Assignee lists (EE feature) require this behavior + */ + pull: (to, from, dragEl, e) => { + // As per Sortable's docs, `to` should provide + // reference to exact sortable container on which + // we're trying to drag element, but either it is + // a library's bug or our markup structure is too complex + // that `to` never points to correct container + // See https://github.com/RubaXa/Sortable/issues/1037 + // + // So we use `e.target` which is always accurate about + // which element we're currently dragging our card upon + // So from there, we can get reference to actual container + // and thus the container type to enable Copy or Move + if (e.target) { + const containerEl = e.target.closest('.js-board-list') || e.target.querySelector('.js-board-list'); + const toBoardType = containerEl.dataset.boardType; + + if (toBoardType) { + const fromBoardType = this.list.type; + + if ((fromBoardType === 'assignee' && toBoardType === 'label') || + (fromBoardType === 'label' && toBoardType === 'assignee')) { + return 'clone'; + } + } + } + + return true; + }, + revertClone: true, + }, onStart: (e) => { const card = this.$refs.issue[e.oldIndex]; @@ -169,21 +205,22 @@ export default { <template> <div class="board-list-component"> <div + v-if="loading" class="board-list-loading text-center" - aria-label="Loading issues" - v-if="loading"> + aria-label="Loading issues"> <loading-icon /> </div> <board-new-issue + v-if="list.type !== 'closed' && showIssueForm" :group-id="groupId" - :list="list" - v-if="list.type !== 'closed' && showIssueForm"/> + :list="list"/> <ul - class="board-list" v-show="!loading" ref="list" :data-board="list.id" - :class="{ 'is-smaller': showIssueForm }"> + :data-board-type="list.type" + :class="{ 'is-smaller': showIssueForm }" + class="board-list js-board-list"> <board-card v-for="(issue, index) in issues" ref="issue" @@ -196,8 +233,8 @@ export default { :disabled="disabled" :key="issue.id" /> <li - class="board-list-count text-center" v-if="showCount" + class="board-list-count text-center" data-issue-id="-1"> <loading-icon v-show="list.loadingMore" diff --git a/app/assets/javascripts/boards/components/board_new_issue.vue b/app/assets/javascripts/boards/components/board_new_issue.vue index e8dfd95f7ae..ec23b1e7c11 100644 --- a/app/assets/javascripts/boards/components/board_new_issue.vue +++ b/app/assets/javascripts/boards/components/board_new_issue.vue @@ -49,11 +49,12 @@ export default { this.error = false; const labels = this.list.label ? [this.list.label] : []; + const assignees = this.list.assignee ? [this.list.assignee] : []; const issue = new ListIssue({ title: this.title, labels, subscribed: true, - assignees: [], + assignees, project_id: this.selectedProject.id, }); @@ -95,26 +96,26 @@ export default { <div class="board-card"> <form @submit="submit($event)"> <div - class="flash-container" v-if="error" + class="flash-container" > <div class="flash-alert"> An error occurred. Please try again. </div> </div> <label - class="label-light" :for="list.id + '-title'" + class="label-light" > Title </label> <input + ref="input" + v-model="title" + :id="list.id + '-title'" class="form-control" type="text" - v-model="title" - ref="input" autocomplete="off" - :id="list.id + '-title'" /> <project-select v-if="groupId" @@ -122,10 +123,10 @@ export default { /> <div class="clearfix prepend-top-10"> <button + ref="submit-button" + :disabled="disabled" class="btn btn-success float-left" type="submit" - :disabled="disabled" - ref="submit-button" > Submit issue </button> @@ -141,4 +142,3 @@ export default { </div> </div> </template> - diff --git a/app/assets/javascripts/boards/components/board_sidebar.js b/app/assets/javascripts/boards/components/board_sidebar.js index c4ee4f6c855..82fe6b0c5fb 100644 --- a/app/assets/javascripts/boards/components/board_sidebar.js +++ b/app/assets/javascripts/boards/components/board_sidebar.js @@ -21,8 +21,17 @@ window.gl = window.gl || {}; window.gl.issueBoards = window.gl.issueBoards || {}; gl.issueBoards.BoardSidebar = Vue.extend({ + components: { + assigneeTitle, + assignees, + removeBtn: gl.issueBoards.RemoveIssueBtn, + subscriptions, + }, props: { - currentUser: Object + currentUser: { + type: Object, + default: () => ({}), + }, }, data() { return { @@ -64,6 +73,26 @@ gl.issueBoards.BoardSidebar = Vue.extend({ deep: true }, }, + created () { + // Get events from glDropdown + eventHub.$on('sidebar.removeAssignee', this.removeAssignee); + eventHub.$on('sidebar.addAssignee', this.addAssignee); + eventHub.$on('sidebar.removeAllAssignees', this.removeAllAssignees); + eventHub.$on('sidebar.saveAssignees', this.saveAssignees); + }, + beforeDestroy() { + eventHub.$off('sidebar.removeAssignee', this.removeAssignee); + eventHub.$off('sidebar.addAssignee', this.addAssignee); + eventHub.$off('sidebar.removeAllAssignees', this.removeAllAssignees); + eventHub.$off('sidebar.saveAssignees', this.saveAssignees); + }, + mounted () { + new IssuableContext(this.currentUser); + new MilestoneSelect(); + new DueDateSelectors(); + new LabelsSelect(); + new Sidebar(); + }, methods: { closeSidebar () { this.detail.issue = {}; @@ -97,30 +126,4 @@ gl.issueBoards.BoardSidebar = Vue.extend({ }); }, }, - created () { - // Get events from glDropdown - eventHub.$on('sidebar.removeAssignee', this.removeAssignee); - eventHub.$on('sidebar.addAssignee', this.addAssignee); - eventHub.$on('sidebar.removeAllAssignees', this.removeAllAssignees); - eventHub.$on('sidebar.saveAssignees', this.saveAssignees); - }, - beforeDestroy() { - eventHub.$off('sidebar.removeAssignee', this.removeAssignee); - eventHub.$off('sidebar.addAssignee', this.addAssignee); - eventHub.$off('sidebar.removeAllAssignees', this.removeAllAssignees); - eventHub.$off('sidebar.saveAssignees', this.saveAssignees); - }, - mounted () { - new IssuableContext(this.currentUser); - new MilestoneSelect(); - new DueDateSelectors(); - new LabelsSelect(); - new Sidebar(); - }, - components: { - assigneeTitle, - assignees, - removeBtn: gl.issueBoards.RemoveIssueBtn, - subscriptions, - }, }); diff --git a/app/assets/javascripts/boards/components/issue_card_inner.js b/app/assets/javascripts/boards/components/issue_card_inner.js index dcc07810d01..f7d7b910e2f 100644 --- a/app/assets/javascripts/boards/components/issue_card_inner.js +++ b/app/assets/javascripts/boards/components/issue_card_inner.js @@ -9,6 +9,9 @@ window.gl = window.gl || {}; window.gl.issueBoards = window.gl.issueBoards || {}; gl.issueBoards.IssueCardInner = Vue.extend({ + components: { + UserAvatarLink, + }, props: { issue: { type: Object, @@ -35,6 +38,7 @@ gl.issueBoards.IssueCardInner = Vue.extend({ groupId: { type: Number, required: false, + default: null, }, }, data() { @@ -44,9 +48,6 @@ gl.issueBoards.IssueCardInner = Vue.extend({ maxCounter: 99, }; }, - components: { - UserAvatarLink, - }, computed: { numberOverLimit() { return this.issue.assignees.length - this.limitBeforeCounter; diff --git a/app/assets/javascripts/boards/components/modal/empty_state.js b/app/assets/javascripts/boards/components/modal/empty_state.js index 1e5f2383223..888bc9d7ef2 100644 --- a/app/assets/javascripts/boards/components/modal/empty_state.js +++ b/app/assets/javascripts/boards/components/modal/empty_state.js @@ -4,9 +4,6 @@ import modalMixin from '../../mixins/modal_mixins'; gl.issueBoards.ModalEmptyState = Vue.extend({ mixins: [modalMixin], - data() { - return ModalStore.store; - }, props: { newIssuePath: { type: String, @@ -17,6 +14,9 @@ gl.issueBoards.ModalEmptyState = Vue.extend({ required: true, }, }, + data() { + return ModalStore.store; + }, computed: { contents() { const obj = { diff --git a/app/assets/javascripts/boards/components/modal/footer.js b/app/assets/javascripts/boards/components/modal/footer.js index 11bb3e98334..2745ca219ad 100644 --- a/app/assets/javascripts/boards/components/modal/footer.js +++ b/app/assets/javascripts/boards/components/modal/footer.js @@ -7,6 +7,9 @@ import ModalStore from '../../stores/modal_store'; import modalMixin from '../../mixins/modal_mixins'; gl.issueBoards.ModalFooter = Vue.extend({ + components: { + 'lists-dropdown': gl.issueBoards.ModalFooterListsDropdown, + }, mixins: [modalMixin], data() { return { @@ -52,9 +55,6 @@ gl.issueBoards.ModalFooter = Vue.extend({ this.toggleModal(false); }, }, - components: { - 'lists-dropdown': gl.issueBoards.ModalFooterListsDropdown, - }, template: ` <footer class="form-actions add-issues-footer"> diff --git a/app/assets/javascripts/boards/components/modal/header.js b/app/assets/javascripts/boards/components/modal/header.js index 67c29ebca72..5e511bb8935 100644 --- a/app/assets/javascripts/boards/components/modal/header.js +++ b/app/assets/javascripts/boards/components/modal/header.js @@ -5,6 +5,10 @@ import ModalStore from '../../stores/modal_store'; import modalMixin from '../../mixins/modal_mixins'; gl.issueBoards.ModalHeader = Vue.extend({ + components: { + 'modal-tabs': gl.issueBoards.ModalTabs, + modalFilters, + }, mixins: [modalMixin], props: { projectId: { @@ -42,10 +46,6 @@ gl.issueBoards.ModalHeader = Vue.extend({ ModalStore.toggleAll(); }, }, - components: { - 'modal-tabs': gl.issueBoards.ModalTabs, - modalFilters, - }, template: ` <div> <header class="add-issues-header form-actions"> diff --git a/app/assets/javascripts/boards/components/modal/index.js b/app/assets/javascripts/boards/components/modal/index.js index 3083b3e4405..c8b2f45f177 100644 --- a/app/assets/javascripts/boards/components/modal/index.js +++ b/app/assets/javascripts/boards/components/modal/index.js @@ -10,6 +10,13 @@ import './empty_state'; import ModalStore from '../../stores/modal_store'; gl.issueBoards.IssuesModal = Vue.extend({ + components: { + 'modal-header': gl.issueBoards.ModalHeader, + 'modal-list': gl.issueBoards.ModalList, + 'modal-footer': gl.issueBoards.ModalFooter, + 'empty-state': gl.issueBoards.ModalEmptyState, + loadingIcon, + }, props: { newIssuePath: { type: String, @@ -43,6 +50,22 @@ gl.issueBoards.IssuesModal = Vue.extend({ data() { return ModalStore.store; }, + computed: { + showList() { + if (this.activeTab === 'selected') { + return this.selectedIssues.length > 0; + } + + return this.issuesCount > 0; + }, + showEmptyState() { + if (!this.loading && this.issuesCount === 0) { + return true; + } + + return this.activeTab === 'selected' && this.selectedIssues.length === 0; + }, + }, watch: { page() { this.loadIssues(); @@ -80,6 +103,9 @@ gl.issueBoards.IssuesModal = Vue.extend({ deep: true, }, }, + created() { + this.page = 1; + }, methods: { loadIssues(clearIssues = false) { if (!this.showAddIssuesModal) return false; @@ -112,32 +138,6 @@ gl.issueBoards.IssuesModal = Vue.extend({ }); }, }, - computed: { - showList() { - if (this.activeTab === 'selected') { - return this.selectedIssues.length > 0; - } - - return this.issuesCount > 0; - }, - showEmptyState() { - if (!this.loading && this.issuesCount === 0) { - return true; - } - - return this.activeTab === 'selected' && this.selectedIssues.length === 0; - }, - }, - created() { - this.page = 1; - }, - components: { - 'modal-header': gl.issueBoards.ModalHeader, - 'modal-list': gl.issueBoards.ModalList, - 'modal-footer': gl.issueBoards.ModalFooter, - 'empty-state': gl.issueBoards.ModalEmptyState, - loadingIcon, - }, template: ` <div class="add-issues-modal" diff --git a/app/assets/javascripts/boards/components/modal/list.js b/app/assets/javascripts/boards/components/modal/list.js index f86896d2178..11061c72a7b 100644 --- a/app/assets/javascripts/boards/components/modal/list.js +++ b/app/assets/javascripts/boards/components/modal/list.js @@ -3,6 +3,9 @@ import bp from '../../../breakpoints'; import ModalStore from '../../stores/modal_store'; gl.issueBoards.ModalList = Vue.extend({ + components: { + 'issue-card-inner': gl.issueBoards.IssueCardInner, + }, props: { issueLinkBase: { type: String, @@ -20,13 +23,6 @@ gl.issueBoards.ModalList = Vue.extend({ data() { return ModalStore.store; }, - watch: { - activeTab() { - if (this.activeTab === 'all') { - ModalStore.purgeUnselectedIssues(); - } - }, - }, computed: { loopIssues() { if (this.activeTab === 'all') { @@ -50,6 +46,25 @@ gl.issueBoards.ModalList = Vue.extend({ return groups; }, }, + watch: { + activeTab() { + if (this.activeTab === 'all') { + ModalStore.purgeUnselectedIssues(); + } + }, + }, + mounted() { + this.scrollHandlerWrapper = this.scrollHandler.bind(this); + this.setColumnCountWrapper = this.setColumnCount.bind(this); + this.setColumnCount(); + + this.$refs.list.addEventListener('scroll', this.scrollHandlerWrapper); + window.addEventListener('resize', this.setColumnCountWrapper); + }, + beforeDestroy() { + this.$refs.list.removeEventListener('scroll', this.scrollHandlerWrapper); + window.removeEventListener('resize', this.setColumnCountWrapper); + }, methods: { scrollHandler() { const currentPage = Math.floor(this.issues.length / this.perPage); @@ -96,21 +111,6 @@ gl.issueBoards.ModalList = Vue.extend({ } }, }, - mounted() { - this.scrollHandlerWrapper = this.scrollHandler.bind(this); - this.setColumnCountWrapper = this.setColumnCount.bind(this); - this.setColumnCount(); - - this.$refs.list.addEventListener('scroll', this.scrollHandlerWrapper); - window.addEventListener('resize', this.setColumnCountWrapper); - }, - beforeDestroy() { - this.$refs.list.removeEventListener('scroll', this.scrollHandlerWrapper); - window.removeEventListener('resize', this.setColumnCountWrapper); - }, - components: { - 'issue-card-inner': gl.issueBoards.IssueCardInner, - }, template: ` <section class="add-issues-list add-issues-list-columns" diff --git a/app/assets/javascripts/boards/components/new_list_dropdown.js b/app/assets/javascripts/boards/components/new_list_dropdown.js index 71f49319c36..6dcd4aaec43 100644 --- a/app/assets/javascripts/boards/components/new_list_dropdown.js +++ b/app/assets/javascripts/boards/components/new_list_dropdown.js @@ -56,6 +56,7 @@ gl.issueBoards.newListDropdownInit = () => { filterable: true, selectable: true, multiSelect: true, + containerSelector: '.js-tab-container-labels .dropdown-page-one .dropdown-content', clicked (options) { const { e } = options; const label = options.selectedObj; diff --git a/app/assets/javascripts/boards/index.js b/app/assets/javascripts/boards/index.js index 29ab13b8e0b..cdad8d238e3 100644 --- a/app/assets/javascripts/boards/index.js +++ b/app/assets/javascripts/boards/index.js @@ -7,6 +7,7 @@ import Vue from 'vue'; import Flash from '~/flash'; import { __ } from '~/locale'; import '~/vue_shared/models/label'; +import '~/vue_shared/models/assignee'; import FilteredSearchBoards from './filtered_search_boards'; import eventHub from './eventhub'; @@ -15,7 +16,6 @@ import './models/issue'; import './models/list'; import './models/milestone'; import './models/project'; -import './models/assignee'; import './stores/boards_store'; import ModalStore from './stores/modal_store'; import BoardService from './services/board_service'; diff --git a/app/assets/javascripts/boards/models/assignee.js b/app/assets/javascripts/boards/models/assignee.js deleted file mode 100644 index 05dd449e4fd..00000000000 --- a/app/assets/javascripts/boards/models/assignee.js +++ /dev/null @@ -1,12 +0,0 @@ -/* eslint-disable no-unused-vars */ - -class ListAssignee { - constructor(user, defaultAvatar) { - this.id = user.id; - this.name = user.name; - this.username = user.username; - this.avatar = user.avatar_url || defaultAvatar; - } -} - -window.ListAssignee = ListAssignee; diff --git a/app/assets/javascripts/boards/models/list.js b/app/assets/javascripts/boards/models/list.js index 7144f4190e7..1f0fe7f9e85 100644 --- a/app/assets/javascripts/boards/models/list.js +++ b/app/assets/javascripts/boards/models/list.js @@ -1,12 +1,14 @@ /* eslint-disable space-before-function-paren, no-underscore-dangle, class-methods-use-this, consistent-return, no-shadow, no-param-reassign, max-len, no-unused-vars */ /* global ListIssue */ -/* global ListLabel */ + +import ListLabel from '~/vue_shared/models/label'; +import ListAssignee from '~/vue_shared/models/assignee'; import queryData from '../utils/query_data'; const PER_PAGE = 20; class List { - constructor (obj, defaultAvatar) { + constructor(obj, defaultAvatar) { this.id = obj.id; this._uid = this.guid(); this.position = obj.position; @@ -24,6 +26,9 @@ class List { if (obj.label) { this.label = new ListLabel(obj.label); + } else if (obj.user) { + this.assignee = new ListAssignee(obj.user); + this.title = this.assignee.name; } if (this.type !== 'blank' && this.id) { @@ -34,14 +39,26 @@ class List { } guid() { - const s4 = () => Math.floor((1 + Math.random()) * 0x10000).toString(16).substring(1); + const s4 = () => + Math.floor((1 + Math.random()) * 0x10000) + .toString(16) + .substring(1); return `${s4()}${s4()}-${s4()}-${s4()}-${s4()}-${s4()}${s4()}${s4()}`; } - save () { - return gl.boardService.createList(this.label.id) + save() { + const entity = this.label || this.assignee; + let entityType = ''; + if (this.label) { + entityType = 'label_id'; + } else { + entityType = 'assignee_id'; + } + + return gl.boardService + .createList(entity.id, entityType) .then(res => res.data) - .then((data) => { + .then(data => { this.id = data.id; this.type = data.list_type; this.position = data.position; @@ -50,25 +67,23 @@ class List { }); } - destroy () { + destroy() { const index = gl.issueBoards.BoardsStore.state.lists.indexOf(this); gl.issueBoards.BoardsStore.state.lists.splice(index, 1); gl.issueBoards.BoardsStore.updateNewListDropdown(this.id); - gl.boardService.destroyList(this.id) - .catch(() => { - // TODO: handle request error - }); + gl.boardService.destroyList(this.id).catch(() => { + // TODO: handle request error + }); } - update () { - gl.boardService.updateList(this.id, this.position) - .catch(() => { - // TODO: handle request error - }); + update() { + gl.boardService.updateList(this.id, this.position).catch(() => { + // TODO: handle request error + }); } - nextPage () { + nextPage() { if (this.issuesSize > this.issues.length) { if (this.issues.length / PER_PAGE >= 1) { this.page += 1; @@ -78,7 +93,7 @@ class List { } } - getIssues (emptyIssues = true) { + getIssues(emptyIssues = true) { const data = queryData(gl.issueBoards.BoardsStore.filter.path, { page: this.page }); if (this.label && data.label_name) { @@ -89,7 +104,8 @@ class List { this.loading = true; } - return gl.boardService.getIssuesForList(this.id, data) + return gl.boardService + .getIssuesForList(this.id, data) .then(res => res.data) .then((data) => { this.loading = false; @@ -103,11 +119,12 @@ class List { }); } - newIssue (issue) { + newIssue(issue) { this.addIssue(issue, null, 0); this.issuesSize += 1; - return gl.boardService.newIssue(this.id, issue) + return gl.boardService + .newIssue(this.id, issue) .then(res => res.data) .then((data) => { issue.id = data.id; @@ -123,13 +140,13 @@ class List { }); } - createIssues (data) { - data.forEach((issueObj) => { + createIssues(data) { + data.forEach(issueObj => { this.addIssue(new ListIssue(issueObj, this.defaultAvatar)); }); } - addIssue (issue, listFrom, newIndex) { + addIssue(issue, listFrom, newIndex) { let moveBeforeId = null; let moveAfterId = null; @@ -152,6 +169,13 @@ class List { issue.addLabel(this.label); } + if (this.assignee) { + if (listFrom && listFrom.type === 'assignee') { + issue.removeAssignee(listFrom.assignee); + } + issue.addAssignee(this.assignee); + } + if (listFrom) { this.issuesSize += 1; @@ -160,29 +184,29 @@ class List { } } - moveIssue (issue, oldIndex, newIndex, moveBeforeId, moveAfterId) { + moveIssue(issue, oldIndex, newIndex, moveBeforeId, moveAfterId) { this.issues.splice(oldIndex, 1); this.issues.splice(newIndex, 0, issue); - gl.boardService.moveIssue(issue.id, null, null, moveBeforeId, moveAfterId) - .catch(() => { - // TODO: handle request error - }); + gl.boardService.moveIssue(issue.id, null, null, moveBeforeId, moveAfterId).catch(() => { + // TODO: handle request error + }); } updateIssueLabel(issue, listFrom, moveBeforeId, moveAfterId) { - gl.boardService.moveIssue(issue.id, listFrom.id, this.id, moveBeforeId, moveAfterId) + gl.boardService + .moveIssue(issue.id, listFrom.id, this.id, moveBeforeId, moveAfterId) .catch(() => { // TODO: handle request error }); } - findIssue (id) { + findIssue(id) { return this.issues.find(issue => issue.id === id); } - removeIssue (removeIssue) { - this.issues = this.issues.filter((issue) => { + removeIssue(removeIssue) { + this.issues = this.issues.filter(issue => { const matchesRemove = removeIssue.id === issue.id; if (matchesRemove) { diff --git a/app/assets/javascripts/boards/services/board_service.js b/app/assets/javascripts/boards/services/board_service.js index 7c90597f77c..029b0971f2c 100644 --- a/app/assets/javascripts/boards/services/board_service.js +++ b/app/assets/javascripts/boards/services/board_service.js @@ -30,11 +30,13 @@ export default class BoardService { return axios.post(this.listsEndpointGenerate, {}); } - createList(labelId) { + createList(entityId, entityType) { + const list = { + [entityType]: entityId, + }; + return axios.post(this.listsEndpoint, { - list: { - label_id: labelId, - }, + list, }); } diff --git a/app/assets/javascripts/boards/stores/boards_store.js b/app/assets/javascripts/boards/stores/boards_store.js index 20e78edf2a2..ffe86468b12 100644 --- a/app/assets/javascripts/boards/stores/boards_store.js +++ b/app/assets/javascripts/boards/stores/boards_store.js @@ -103,8 +103,15 @@ gl.issueBoards.BoardsStore = { const listLabels = issueLists.map(listIssue => listIssue.label); if (!issueTo) { - // Add to new lists issues if it doesn't already exist - listTo.addIssue(issue, listFrom, newIndex); + // Check if target list assignee is already present in this issue + if ((listTo.type === 'assignee' && listFrom.type === 'assignee') && + issue.findAssignee(listTo.assignee)) { + const targetIssue = listTo.findIssue(issue.id); + targetIssue.removeAssignee(listFrom.assignee); + } else { + // Add to new lists issues if it doesn't already exist + listTo.addIssue(issue, listFrom, newIndex); + } } else { listTo.updateIssueLabel(issue, listFrom); issueTo.removeLabel(listFrom.label); @@ -115,7 +122,11 @@ gl.issueBoards.BoardsStore = { list.removeIssue(issue); }); issue.removeLabels(listLabels); - } else { + } else if (listTo.type === 'backlog' && listFrom.type === 'assignee') { + issue.removeAssignee(listFrom.assignee); + listFrom.removeIssue(issue); + } else if ((listTo.type !== 'label' && listFrom.type === 'assignee') || + (listTo.type !== 'assignee' && listFrom.type === 'label')) { listFrom.removeIssue(issue); } }, @@ -126,13 +137,14 @@ gl.issueBoards.BoardsStore = { list.moveIssue(issue, oldIndex, newIndex, beforeId, afterId); }, findList (key, val, type = 'label') { - return this.state.lists.filter((list) => { - const byType = type ? list['type'] === type : true; + const filteredList = this.state.lists.filter((list) => { + const byType = type ? (list.type === type) || (list.type === 'assignee') : true; return list[key] === val && byType; - })[0]; + }); + return filteredList[0]; }, updateFiltersUrl () { - history.pushState(null, null, `?${this.filter.path}`); + window.history.pushState(null, null, `?${this.filter.path}`); } }; diff --git a/app/assets/javascripts/clusters/components/application_row.vue b/app/assets/javascripts/clusters/components/application_row.vue index 30567993322..ec52fdfdf32 100644 --- a/app/assets/javascripts/clusters/components/application_row.vue +++ b/app/assets/javascripts/clusters/components/application_row.vue @@ -125,8 +125,8 @@ <template> <div - class="gl-responsive-table-row gl-responsive-table-row-col-span" :class="rowJsClass" + class="gl-responsive-table-row gl-responsive-table-row-col-span" > <div class="gl-responsive-table-row-layout" @@ -155,8 +155,8 @@ <slot name="description"></slot> </div> <div - class="table-section table-button-footer section-align-top" :class="{ 'section-20': showManageButton, 'section-15': !showManageButton }" + class="table-section table-button-footer section-align-top" role="gridcell" > <div @@ -164,18 +164,18 @@ class="btn-group table-action-buttons" > <a - class="btn" :href="manageLink" + class="btn" > {{ manageButtonLabel }} </a> </div> <div class="btn-group table-action-buttons"> <loading-button - class="js-cluster-application-install-button" :loading="installButtonLoading" :disabled="installButtonDisabled" :label="installButtonLabel" + class="js-cluster-application-install-button" @click="installClicked" /> </div> @@ -187,7 +187,7 @@ role="row" > <div - class="alert alert-danger alert-block append-bottom-0" + class="alert alert-danger alert-block append-bottom-0 clusters-error-alert" role="gridcell" > <div> diff --git a/app/assets/javascripts/clusters/components/applications.vue b/app/assets/javascripts/clusters/components/applications.vue index 9d6be555a2c..8ee7279e544 100644 --- a/app/assets/javascripts/clusters/components/applications.vue +++ b/app/assets/javascripts/clusters/components/applications.vue @@ -152,11 +152,11 @@ export default { <application-row id="helm" :title="applications.helm.title" - title-link="https://docs.helm.sh/" :status="applications.helm.status" :status-reason="applications.helm.statusReason" :request-status="applications.helm.requestStatus" :request-reason="applications.helm.requestReason" + title-link="https://docs.helm.sh/" > <div slot="description"> {{ s__(`ClusterIntegration|Helm streamlines installing @@ -168,11 +168,11 @@ export default { <application-row :id="ingressId" :title="applications.ingress.title" - title-link="https://kubernetes.io/docs/concepts/services-networking/ingress/" :status="applications.ingress.status" :status-reason="applications.ingress.statusReason" :request-status="applications.ingress.requestStatus" :request-reason="applications.ingress.requestReason" + title-link="https://kubernetes.io/docs/concepts/services-networking/ingress/" > <div slot="description"> <p> @@ -191,10 +191,10 @@ export default { class="input-group" > <input - type="text" id="ingress-ip-address" - class="form-control js-ip-address" :value="ingressExternalIp" + type="text" + class="form-control js-ip-address" readonly /> <span class="input-group-append"> @@ -255,12 +255,12 @@ export default { <application-row id="prometheus" :title="applications.prometheus.title" - title-link="https://prometheus.io/docs/introduction/overview/" :manage-link="managePrometheusPath" :status="applications.prometheus.status" :status-reason="applications.prometheus.statusReason" :request-status="applications.prometheus.requestStatus" :request-reason="applications.prometheus.requestReason" + title-link="https://prometheus.io/docs/introduction/overview/" > <div slot="description" @@ -271,11 +271,11 @@ export default { <application-row id="runner" :title="applications.runner.title" - title-link="https://docs.gitlab.com/runner/" :status="applications.runner.status" :status-reason="applications.runner.statusReason" :request-status="applications.runner.requestStatus" :request-reason="applications.runner.requestReason" + title-link="https://docs.gitlab.com/runner/" > <div slot="description"> {{ s__(`ClusterIntegration|GitLab Runner connects to this @@ -287,12 +287,12 @@ export default { <application-row id="jupyter" :title="applications.jupyter.title" - title-link="https://jupyterhub.readthedocs.io/en/stable/" :status="applications.jupyter.status" :status-reason="applications.jupyter.statusReason" :request-status="applications.jupyter.requestStatus" :request-reason="applications.jupyter.requestReason" :install-application-request-params="{ hostname: applications.jupyter.hostname }" + title-link="https://jupyterhub.readthedocs.io/en/stable/" > <div slot="description"> <p> @@ -311,10 +311,10 @@ export default { <div class="input-group"> <input - type="text" - class="form-control js-hostname" v-model="applications.jupyter.hostname" :readonly="jupyterInstalled" + type="text" + class="form-control js-hostname" /> <span class="input-group-btn" diff --git a/app/assets/javascripts/clusters/stores/clusters_store.js b/app/assets/javascripts/clusters/stores/clusters_store.js index 3a4ac09f67c..d90db7b103c 100644 --- a/app/assets/javascripts/clusters/stores/clusters_store.js +++ b/app/assets/javascripts/clusters/stores/clusters_store.js @@ -95,7 +95,7 @@ export default class ClusterStore { this.state.applications.jupyter.hostname = serverAppEntry.hostname || (this.state.applications.ingress.externalIp - ? `jupyter.${this.state.applications.ingress.externalIp}.xip.io` + ? `jupyter.${this.state.applications.ingress.externalIp}.nip.io` : ''); } }); diff --git a/app/assets/javascripts/commit/pipelines/pipelines_table.vue b/app/assets/javascripts/commit/pipelines/pipelines_table.vue index 24d63b99a29..95c4be64d35 100644 --- a/app/assets/javascripts/commit/pipelines/pipelines_table.vue +++ b/app/assets/javascripts/commit/pipelines/pipelines_table.vue @@ -77,9 +77,9 @@ <div class="content-list pipelines"> <loading-icon + v-if="isLoading" :label="s__('Pipelines|Loading Pipelines')" size="3" - v-if="isLoading" class="prepend-top-20" /> @@ -91,8 +91,8 @@ /> <div - class="table-holder" v-else-if="shouldRenderTable" + class="table-holder" > <pipelines-table-component :pipelines="state.pipelines" diff --git a/app/assets/javascripts/commits.js b/app/assets/javascripts/commits.js index 7e2a3573f81..9a3ea7a55b6 100644 --- a/app/assets/javascripts/commits.js +++ b/app/assets/javascripts/commits.js @@ -45,7 +45,7 @@ export default class CommitsList { this.content.fadeTo('fast', 1.0); // Change url so if user reload a page - search results are saved - history.replaceState({ + window.history.replaceState({ page: commitsUrl, }, document.title, commitsUrl); }) diff --git a/app/assets/javascripts/create_merge_request_dropdown.js b/app/assets/javascripts/create_merge_request_dropdown.js index 09d490106df..108082799ef 100644 --- a/app/assets/javascripts/create_merge_request_dropdown.js +++ b/app/assets/javascripts/create_merge_request_dropdown.js @@ -66,8 +66,14 @@ export default class CreateMergeRequestDropdown { } bindEvents() { - this.createMergeRequestButton.addEventListener('click', this.onClickCreateMergeRequestButton.bind(this)); - this.createTargetButton.addEventListener('click', this.onClickCreateMergeRequestButton.bind(this)); + this.createMergeRequestButton.addEventListener( + 'click', + this.onClickCreateMergeRequestButton.bind(this), + ); + this.createTargetButton.addEventListener( + 'click', + this.onClickCreateMergeRequestButton.bind(this), + ); this.branchInput.addEventListener('keyup', this.onChangeInput.bind(this)); this.dropdownToggle.addEventListener('click', this.onClickSetFocusOnBranchNameInput.bind(this)); this.refInput.addEventListener('keyup', this.onChangeInput.bind(this)); @@ -77,7 +83,8 @@ export default class CreateMergeRequestDropdown { checkAbilityToCreateBranch() { this.setUnavailableButtonState(); - axios.get(this.canCreatePath) + axios + .get(this.canCreatePath) .then(({ data }) => { this.setUnavailableButtonState(false); @@ -105,7 +112,8 @@ export default class CreateMergeRequestDropdown { createBranch() { this.isCreatingBranch = true; - return axios.post(this.createBranchPath) + return axios + .post(this.createBranchPath) .then(({ data }) => { this.branchCreated = true; window.location.href = data.url; @@ -116,7 +124,8 @@ export default class CreateMergeRequestDropdown { createMergeRequest() { this.isCreatingMergeRequest = true; - return axios.post(this.createMrPath) + return axios + .post(this.createMrPath) .then(({ data }) => { this.mergeRequestCreated = true; window.location.href = data.url; @@ -195,7 +204,8 @@ export default class CreateMergeRequestDropdown { getRef(ref, target = 'all') { if (!ref) return false; - return axios.get(this.refsPath + ref) + return axios + .get(`${this.refsPath}${encodeURIComponent(ref)}`) .then(({ data }) => { const branches = data[Object.keys(data)[0]]; const tags = data[Object.keys(data)[1]]; @@ -204,7 +214,8 @@ export default class CreateMergeRequestDropdown { if (target === 'branch') { result = CreateMergeRequestDropdown.findByValue(branches, ref); } else { - result = CreateMergeRequestDropdown.findByValue(branches, ref, true) || + result = + CreateMergeRequestDropdown.findByValue(branches, ref, true) || CreateMergeRequestDropdown.findByValue(tags, ref, true); this.suggestedRef = result; } @@ -255,11 +266,13 @@ export default class CreateMergeRequestDropdown { } isBusy() { - return this.isCreatingMergeRequest || + return ( + this.isCreatingMergeRequest || this.mergeRequestCreated || this.isCreatingBranch || this.branchCreated || - this.isGettingRef; + this.isGettingRef + ); } onChangeInput(event) { @@ -271,7 +284,8 @@ export default class CreateMergeRequestDropdown { value = this.branchInput.value; } else if (event.target === this.refInput) { target = 'ref'; - value = event.target.value.slice(0, event.target.selectionStart) + + value = + event.target.value.slice(0, event.target.selectionStart) + event.target.value.slice(event.target.selectionEnd); } else { return false; @@ -396,7 +410,8 @@ export default class CreateMergeRequestDropdown { showNotAvailableMessage(target) { const { input, message } = this.getTargetData(target); - const text = target === 'branch' ? __('Branch is already taken') : __('Source is not available'); + const text = + target === 'branch' ? __('Branch is already taken') : __('Source is not available'); this.removeMessage(target); input.classList.add('gl-field-error-outline'); @@ -459,11 +474,15 @@ export default class CreateMergeRequestDropdown { // target - 'branch' or 'ref' // ref - string - the new value to use as branch or ref updateCreatePaths(target, ref) { - const pathReplacement = `$1${ref}`; + const pathReplacement = `$1${encodeURIComponent(ref)}`; - this.createBranchPath = this.createBranchPath.replace(this.regexps[target].createBranchPath, - pathReplacement); - this.createMrPath = this.createMrPath.replace(this.regexps[target].createMrPath, - pathReplacement); + 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/banner.vue b/app/assets/javascripts/cycle_analytics/components/banner.vue index 3204b8dd8e7..410d4873e55 100644 --- a/app/assets/javascripts/cycle_analytics/components/banner.vue +++ b/app/assets/javascripts/cycle_analytics/components/banner.vue @@ -23,9 +23,9 @@ <template> <div class="landing content-block"> <button + :aria-label="__('Dismiss Cycle Analytics introduction box')" class="js-ca-dismiss-button dismiss-button" type="button" - :aria-label="__('Dismiss Cycle Analytics introduction box')" @click="dismissOverviewDialog" > <i 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 5be17081b58..b626b187651 100644 --- a/app/assets/javascripts/cycle_analytics/components/limit_warning_component.vue +++ b/app/assets/javascripts/cycle_analytics/components/limit_warning_component.vue @@ -19,14 +19,14 @@ class="events-info float-right" > <i - class="fa fa-warning" v-tooltip - aria-hidden="true" :title="n__( 'Limited to showing %d event at most', 'Limited to showing %d events at most', 50 )" + class="fa fa-warning" + aria-hidden="true" data-placement="top" > </i> diff --git a/app/assets/javascripts/cycle_analytics/components/stage_component.vue b/app/assets/javascripts/cycle_analytics/components/stage_component.vue index 907638d798a..312fe75dde4 100644 --- a/app/assets/javascripts/cycle_analytics/components/stage_component.vue +++ b/app/assets/javascripts/cycle_analytics/components/stage_component.vue @@ -38,8 +38,8 @@ <user-avatar-image :img-src="issue.author.avatarUrl"/> <h5 class="item-title issue-title"> <a - class="issue-title" :href="issue.url" + class="issue-title" > {{ issue.title }} </a> diff --git a/app/assets/javascripts/cycle_analytics/components/stage_review_component.vue b/app/assets/javascripts/cycle_analytics/components/stage_review_component.vue index 34aa04083e6..d4735d030fc 100644 --- a/app/assets/javascripts/cycle_analytics/components/stage_review_component.vue +++ b/app/assets/javascripts/cycle_analytics/components/stage_review_component.vue @@ -74,12 +74,12 @@ </template> <template v-else> <span - class="merge-request-branch" v-if="mergeRequest.branch" + class="merge-request-branch" > <icon - name="fork" :size="16" + name="fork" /> <a :href="mergeRequest.branch.url"> {{ mergeRequest.branch.name }} diff --git a/app/assets/javascripts/cycle_analytics/components/stage_staging_component.vue b/app/assets/javascripts/cycle_analytics/components/stage_staging_component.vue index 92f2a95a66a..22637485c01 100644 --- a/app/assets/javascripts/cycle_analytics/components/stage_staging_component.vue +++ b/app/assets/javascripts/cycle_analytics/components/stage_staging_component.vue @@ -38,8 +38,8 @@ <ul class="stage-event-list"> <li v-for="(build, i) in items" - class="stage-event-item item-build-component" :key="i" + class="stage-event-item item-build-component" > <div class="item-details"> <!-- FIXME: Pass an alt attribute here for accessibility --> @@ -52,8 +52,8 @@ #{{ build.id }} </a> <icon - name="fork" :size="16" + name="fork" /> <a :href="build.branch.url" diff --git a/app/assets/javascripts/cycle_analytics/components/stage_test_component.vue b/app/assets/javascripts/cycle_analytics/components/stage_test_component.vue index b84bb6ed792..a0796f299e7 100644 --- a/app/assets/javascripts/cycle_analytics/components/stage_test_component.vue +++ b/app/assets/javascripts/cycle_analytics/components/stage_test_component.vue @@ -64,8 +64,8 @@ #{{ build.id }} </a> <icon - name="fork" :size="16" + name="fork" /> <a :href="build.branch.url" diff --git a/app/assets/javascripts/deploy_keys/components/action_btn.vue b/app/assets/javascripts/deploy_keys/components/action_btn.vue index 67dda0e29cb..7399fc97d45 100644 --- a/app/assets/javascripts/deploy_keys/components/action_btn.vue +++ b/app/assets/javascripts/deploy_keys/components/action_btn.vue @@ -40,9 +40,9 @@ export default { <template> <button - class="btn" :class="[{ disabled: isLoading }, btnCssClass]" :disabled="isLoading" + class="btn" @click="doAction"> <slot></slot> <loading-icon diff --git a/app/assets/javascripts/deploy_keys/components/app.vue b/app/assets/javascripts/deploy_keys/components/app.vue index c41fe55db63..d91e4809126 100644 --- a/app/assets/javascripts/deploy_keys/components/app.vue +++ b/app/assets/javascripts/deploy_keys/components/app.vue @@ -98,7 +98,7 @@ export default { }, disableKey(deployKey, callback) { // eslint-disable-next-line no-alert - if (confirm(s__('DeployKeys|You are going to remove this deploy key. Are you sure?'))) { + if (window.confirm(s__('DeployKeys|You are going to remove this deploy key. Are you sure?'))) { this.service .disableKey(deployKey.id) .then(this.fetchKeys) @@ -116,8 +116,8 @@ export default { <div class="append-bottom-default deploy-keys"> <loading-icon v-if="isLoading && !hasKeys" - size="2" :label="s__('DeployKeys|Loading deploy keys')" + size="2" /> <template v-else-if="hasKeys"> <div class="top-area scrolling-tabs-container inner-page-scroll-tabs"> @@ -138,16 +138,16 @@ export default { <navigation-tabs :tabs="tabs" - @onChangeTab="onChangeTab" scope="deployKeys" + @onChangeTab="onChangeTab" /> </div> <keys-panel - class="qa-project-deploy-keys" :project-id="projectId" :keys="keys[currentTab]" :store="store" :endpoint="endpoint" + class="qa-project-deploy-keys" /> </template> </div> diff --git a/app/assets/javascripts/deploy_keys/components/key.vue b/app/assets/javascripts/deploy_keys/components/key.vue index 6c2af7fa768..f66ca070445 100644 --- a/app/assets/javascripts/deploy_keys/components/key.vue +++ b/app/assets/javascripts/deploy_keys/components/key.vue @@ -135,9 +135,9 @@ export default { <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 + :title="projectTooltipTitle(firstProject)" + class="label deploy-project-label" > <span> {{ firstProject.project.full_name }} @@ -145,22 +145,22 @@ export default { <icon :name="firstProject.can_push ? 'lock-open' : 'lock'"/> </a> <a + v-tooltip v-if="isExpandable" + :title="restProjectsTooltip" class="label deploy-project-label" @click="toggleExpanded" - :title="restProjectsTooltip" - v-tooltip > <span>{{ restProjectsLabel }}</span> </a> <a - v-else-if="isExpanded" + v-tooltip v-for="deployKeysProject in restProjects" + v-else-if="isExpanded" :key="deployKeysProject.project.full_path" - class="label deploy-project-label" :href="deployKeysProject.project.full_path" :title="projectTooltipTitle(deployKeysProject)" - v-tooltip + class="label deploy-project-label" > <span> {{ deployKeysProject.project.full_name }} @@ -181,8 +181,8 @@ export default { </div> <div class="table-mobile-content text-secondary key-created-at"> <span - :title="tooltipTitle(deployKey.created_at)" - v-tooltip> + v-tooltip + :title="tooltipTitle(deployKey.created_at)"> <icon name="calendar"/> <span>{{ timeFormated(deployKey.created_at) }}</span> </span> @@ -198,34 +198,34 @@ export default { {{ __('Enable') }} </action-btn> <a + v-tooltip v-if="deployKey.can_edit" - class="btn btn-default text-secondary" :href="editDeployKeyPath" :title="__('Edit')" + class="btn btn-default text-secondary" data-container="body" - v-tooltip > <icon name="pencil"/> </a> <action-btn + v-tooltip v-if="isRemovable" :deploy-key="deployKey" + :title="__('Remove')" btn-css-class="btn-danger" type="remove" - :title="__('Remove')" data-container="body" - v-tooltip > <icon name="remove"/> </action-btn> <action-btn + v-tooltip v-else-if="isEnabled" :deploy-key="deployKey" + :title="__('Disable')" btn-css-class="btn-warning" type="disable" - :title="__('Disable')" data-container="body" - v-tooltip > <icon name="cancel"/> </action-btn> diff --git a/app/assets/javascripts/deploy_keys/components/keys_panel.vue b/app/assets/javascripts/deploy_keys/components/keys_panel.vue index 3b146c7389a..2f057ca29f6 100644 --- a/app/assets/javascripts/deploy_keys/components/keys_panel.vue +++ b/app/assets/javascripts/deploy_keys/components/keys_panel.vue @@ -59,8 +59,8 @@ export default { /> </template> <div - class="settings-message text-center" v-else + class="settings-message text-center" > {{ s__('DeployKeys|No deploy keys found. Create one with the form above.') }} </div> diff --git a/app/assets/javascripts/diff_notes/components/comment_resolve_btn.js b/app/assets/javascripts/diff_notes/components/comment_resolve_btn.js index d1260ff5373..ed24d1775f4 100644 --- a/app/assets/javascripts/diff_notes/components/comment_resolve_btn.js +++ b/app/assets/javascripts/diff_notes/components/comment_resolve_btn.js @@ -6,7 +6,10 @@ import Vue from 'vue'; const CommentAndResolveBtn = Vue.extend({ props: { - discussionId: String, + discussionId: { + type: String, + required: true, + }, }, data() { return { 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 fe9b0795609..40f7c2fe5f3 100644 --- a/app/assets/javascripts/diff_notes/components/diff_note_avatars.js +++ b/app/assets/javascripts/diff_notes/components/diff_note_avatars.js @@ -7,7 +7,15 @@ import Notes from '../../notes'; import userAvatarImage from '../../vue_shared/components/user_avatar/user_avatar_image.vue'; const DiffNoteAvatars = Vue.extend({ - props: ['discussionId'], + components: { + userAvatarImage, + }, + props: { + discussionId: { + type: String, + required: true, + }, + }, data() { return { isVisible: false, @@ -17,77 +25,6 @@ const DiffNoteAvatars = Vue.extend({ collapseIcon, }; }, - components: { - userAvatarImage, - }, - template: ` - <div class="diff-comment-avatar-holders" - :class="discussionClassName" - v-show="notesCount !== 0"> - <div v-if="!isVisible"> - <!-- FIXME: Pass an alt attribute here for accessibility --> - <user-avatar-image - v-for="note in notesSubset" - :key="note.id" - class="diff-comment-avatar js-diff-comment-avatar" - @click.native="clickedAvatar($event)" - :img-src="note.authorAvatar" - :tooltip-text="getTooltipText(note)" - :data-line-type="lineType" - :size="19" - data-html="true" - /> - <span v-if="notesCount > shownAvatars" - class="diff-comments-more-count has-tooltip js-diff-comment-avatar" - data-container="body" - data-placement="top" - ref="extraComments" - role="button" - :data-line-type="lineType" - :title="extraNotesTitle" - @click="clickedAvatar($event)">{{ moreText }}</span> - </div> - <button class="diff-notes-collapse js-diff-comment-avatar" - type="button" - aria-label="Show comments" - :data-line-type="lineType" - @click="clickedAvatar($event)" - v-if="isVisible" - v-html="collapseIcon"> - </button> - </div> - `, - mounted() { - this.$nextTick(() => { - this.addNoCommentClass(); - this.setDiscussionVisible(); - - this.lineType = $(this.$el).closest('.diff-line-num').hasClass('old_line') ? 'old' : 'new'; - }); - - $(document).on('toggle.comments', () => { - this.$nextTick(() => { - this.setDiscussionVisible(); - }); - }); - }, - beforeDestroy() { - this.addNoCommentClass(); - $(document).off('toggle.comments'); - }, - watch: { - storeState: { - handler() { - this.$nextTick(() => { - $('.has-tooltip', this.$el).tooltip('_fixTitle'); - - // We need to add/remove a class to an element that is outside the Vue instance - this.addNoCommentClass(); - }); - }, - deep: true, - }, - }, computed: { discussionClassName() { return `js-diff-avatars-${this.discussionId}`; @@ -128,6 +65,37 @@ const DiffNoteAvatars = Vue.extend({ return `${plusSign}${this.notesCount - this.shownAvatars}`; }, }, + watch: { + storeState: { + handler() { + this.$nextTick(() => { + $('.has-tooltip', this.$el).tooltip('_fixTitle'); + + // We need to add/remove a class to an element that is outside the Vue instance + this.addNoCommentClass(); + }); + }, + deep: true, + }, + }, + mounted() { + this.$nextTick(() => { + this.addNoCommentClass(); + this.setDiscussionVisible(); + + this.lineType = $(this.$el).closest('.diff-line-num').hasClass('old_line') ? 'old' : 'new'; + }); + + $(document).on('toggle.comments', () => { + this.$nextTick(() => { + this.setDiscussionVisible(); + }); + }); + }, + beforeDestroy() { + this.addNoCommentClass(); + $(document).off('toggle.comments'); + }, methods: { clickedAvatar(e) { Notes.instance.onAddDiffNote(e); @@ -164,6 +132,43 @@ const DiffNoteAvatars = Vue.extend({ return `${note.authorName}: ${note.noteTruncated}`; }, }, + template: ` + <div class="diff-comment-avatar-holders" + :class="discussionClassName" + v-show="notesCount !== 0"> + <div v-if="!isVisible"> + <!-- FIXME: Pass an alt attribute here for accessibility --> + <user-avatar-image + v-for="note in notesSubset" + :key="note.id" + class="diff-comment-avatar js-diff-comment-avatar" + @click.native="clickedAvatar($event)" + :img-src="note.authorAvatar" + :tooltip-text="getTooltipText(note)" + :data-line-type="lineType" + :size="19" + data-html="true" + /> + <span v-if="notesCount > shownAvatars" + class="diff-comments-more-count has-tooltip js-diff-comment-avatar" + data-container="body" + data-placement="top" + ref="extraComments" + role="button" + :data-line-type="lineType" + :title="extraNotesTitle" + @click="clickedAvatar($event)">{{ moreText }}</span> + </div> + <button class="diff-notes-collapse js-diff-comment-avatar" + type="button" + aria-label="Show comments" + :data-line-type="lineType" + @click="clickedAvatar($event)" + v-if="isVisible" + v-html="collapseIcon"> + </button> + </div> + `, }); Vue.component('diff-note-avatars', DiffNoteAvatars); diff --git a/app/assets/javascripts/diff_notes/components/jump_to_discussion.js b/app/assets/javascripts/diff_notes/components/jump_to_discussion.js index 8f9186dfb9a..2ce4b050763 100644 --- a/app/assets/javascripts/diff_notes/components/jump_to_discussion.js +++ b/app/assets/javascripts/diff_notes/components/jump_to_discussion.js @@ -10,7 +10,10 @@ import '../mixins/discussion'; const JumpToDiscussion = Vue.extend({ mixins: [DiscussionMixins], props: { - discussionId: String + discussionId: { + type: String, + required: true, + }, }, data: function () { return { @@ -52,6 +55,9 @@ const JumpToDiscussion = Vue.extend({ return lastId; } }, + created() { + this.discussion = this.discussions[this.discussionId]; + }, methods: { jumpToNextUnresolvedDiscussion: function () { let discussionsSelector; @@ -202,9 +208,6 @@ const JumpToDiscussion = Vue.extend({ }); } }, - created() { - this.discussion = this.discussions[this.discussionId]; - }, }); Vue.component('jump-to-discussion', JumpToDiscussion); diff --git a/app/assets/javascripts/diff_notes/components/resolve_btn.js b/app/assets/javascripts/diff_notes/components/resolve_btn.js index 8d66417abac..07f3be29090 100644 --- a/app/assets/javascripts/diff_notes/components/resolve_btn.js +++ b/app/assets/javascripts/diff_notes/components/resolve_btn.js @@ -8,14 +8,38 @@ import Flash from '../../flash'; const ResolveBtn = Vue.extend({ props: { - noteId: Number, - discussionId: String, - resolved: Boolean, - canResolve: Boolean, - resolvedBy: String, - authorName: String, - authorAvatar: String, - noteTruncated: String, + noteId: { + type: Number, + required: true, + }, + discussionId: { + type: String, + required: true, + }, + resolved: { + type: Boolean, + required: true, + }, + canResolve: { + type: Boolean, + required: true, + }, + resolvedBy: { + type: String, + required: true, + }, + authorName: { + type: String, + required: true, + }, + authorAvatar: { + type: String, + required: true, + }, + noteTruncated: { + type: String, + required: true, + }, }, data: function () { return { @@ -23,12 +47,6 @@ const ResolveBtn = Vue.extend({ loading: false }; }, - watch: { - 'discussions': { - handler: 'updateTooltip', - deep: true - } - }, computed: { discussion: function () { return this.discussions[this.discussionId]; @@ -56,6 +74,32 @@ const ResolveBtn = Vue.extend({ return this.note.resolved_by; }, }, + watch: { + 'discussions': { + handler: 'updateTooltip', + deep: true + } + }, + mounted: function () { + $(this.$refs.button).tooltip({ + container: 'body' + }); + }, + beforeDestroy: function () { + CommentsStore.delete(this.discussionId, this.noteId); + }, + created: function () { + CommentsStore.create({ + discussionId: this.discussionId, + noteId: this.noteId, + canResolve: this.canResolve, + resolved: this.resolved, + resolvedBy: this.resolvedBy, + authorName: this.authorName, + authorAvatar: this.authorAvatar, + noteTruncated: this.noteTruncated, + }); + }, methods: { updateTooltip: function () { this.$nextTick(() => { @@ -95,26 +139,6 @@ const ResolveBtn = Vue.extend({ .catch(() => new Flash('An error occurred when trying to resolve a comment. Please try again.')); } }, - mounted: function () { - $(this.$refs.button).tooltip({ - container: 'body' - }); - }, - beforeDestroy: function () { - CommentsStore.delete(this.discussionId, this.noteId); - }, - created: function () { - CommentsStore.create({ - discussionId: this.discussionId, - noteId: this.noteId, - canResolve: this.canResolve, - resolved: this.resolved, - resolvedBy: this.resolvedBy, - authorName: this.authorName, - authorAvatar: this.authorAvatar, - noteTruncated: this.noteTruncated, - }); - } }); Vue.component('resolve-btn', ResolveBtn); diff --git a/app/assets/javascripts/diff_notes/components/resolve_count.js b/app/assets/javascripts/diff_notes/components/resolve_count.js index fe7cf8f5fc1..9f613410e81 100644 --- a/app/assets/javascripts/diff_notes/components/resolve_count.js +++ b/app/assets/javascripts/diff_notes/components/resolve_count.js @@ -9,7 +9,10 @@ import '../mixins/discussion'; window.ResolveCount = Vue.extend({ mixins: [DiscussionMixins], props: { - loggedOut: Boolean + loggedOut: { + type: Boolean, + required: true, + }, }, data: function () { return { diff --git a/app/assets/javascripts/diff_notes/components/resolve_discussion_btn.js b/app/assets/javascripts/diff_notes/components/resolve_discussion_btn.js index 6a036e96171..210a00c5fc2 100644 --- a/app/assets/javascripts/diff_notes/components/resolve_discussion_btn.js +++ b/app/assets/javascripts/diff_notes/components/resolve_discussion_btn.js @@ -6,9 +6,18 @@ import Vue from 'vue'; const ResolveDiscussionBtn = Vue.extend({ props: { - discussionId: String, - mergeRequestId: Number, - canResolve: Boolean, + discussionId: { + type: String, + required: true, + }, + mergeRequestId: { + type: Number, + required: true, + }, + canResolve: { + type: Boolean, + required: true, + }, }, data: function() { return { @@ -45,16 +54,16 @@ const ResolveDiscussionBtn = Vue.extend({ } } }, + created: function () { + CommentsStore.createDiscussion(this.discussionId, this.canResolve); + + this.discussion = CommentsStore.state[this.discussionId]; + }, methods: { resolve: function () { ResolveService.toggleResolveForDiscussion(this.mergeRequestId, this.discussionId); } }, - created: function () { - CommentsStore.createDiscussion(this.discussionId, this.canResolve); - - this.discussion = CommentsStore.state[this.discussionId]; - } }); Vue.component('resolve-discussion-btn', ResolveDiscussionBtn); diff --git a/app/assets/javascripts/diff_notes/diff_notes_bundle.js b/app/assets/javascripts/diff_notes/diff_notes_bundle.js index e17daec6a92..d5161ab7df9 100644 --- a/app/assets/javascripts/diff_notes/diff_notes_bundle.js +++ b/app/assets/javascripts/diff_notes/diff_notes_bundle.js @@ -69,9 +69,10 @@ export default () => { gl.diffNotesCompileComponents(); - if (!hasVueMRDiscussionsCookie()) { + const resolveCountAppEl = document.querySelector('#resolve-count-app'); + if (!hasVueMRDiscussionsCookie() && resolveCountAppEl) { new Vue({ - el: '#resolve-count-app', + el: resolveCountAppEl, components: { 'resolve-count': ResolveCount }, diff --git a/app/assets/javascripts/environments/components/container.vue b/app/assets/javascripts/environments/components/container.vue index 6bd7c6b49cb..9aa224fa407 100644 --- a/app/assets/javascripts/environments/components/container.vue +++ b/app/assets/javascripts/environments/components/container.vue @@ -43,17 +43,17 @@ <div class="environments-container"> <loading-icon + v-if="isLoading" class="prepend-top-default" label="Loading environments" - v-if="isLoading" size="3" /> <slot name="emptyState"></slot> <div - class="table-holder" - v-if="!isLoading && environments.length > 0"> + v-if="!isLoading && environments.length > 0" + class="table-holder"> <environment-table :environments="environments" diff --git a/app/assets/javascripts/environments/components/environment_actions.vue b/app/assets/javascripts/environments/components/environment_actions.vue index 0b3fef9fcca..e3652fe739e 100644 --- a/app/assets/javascripts/environments/components/environment_actions.vue +++ b/app/assets/javascripts/environments/components/environment_actions.vue @@ -52,18 +52,18 @@ role="group"> <button v-tooltip + :title="title" + :aria-label="title" + :disabled="isLoading" type="button" class="dropdown btn btn-default dropdown-new js-dropdown-play-icon-container" data-container="body" data-toggle="dropdown" - :title="title" - :aria-label="title" - :disabled="isLoading" > <span> <icon - name="play" :size="12" + name="play" /> <i class="fa fa-caret-down" @@ -79,15 +79,15 @@ v-for="(action, i) in actions" :key="i"> <button + :class="{ disabled: isActionDisabled(action) }" + :disabled="isActionDisabled(action)" type="button" class="js-manual-action-link no-btn btn" @click="onClickAction(action.play_path)" - :class="{ disabled: isActionDisabled(action) }" - :disabled="isActionDisabled(action)" > <icon - name="play" :size="12" + name="play" /> <span> {{ action.name }} diff --git a/app/assets/javascripts/environments/components/environment_external_url.vue b/app/assets/javascripts/environments/components/environment_external_url.vue index ea6f1168c68..68195225d50 100644 --- a/app/assets/javascripts/environments/components/environment_external_url.vue +++ b/app/assets/javascripts/environments/components/environment_external_url.vue @@ -29,17 +29,17 @@ <template> <a v-tooltip + :title="title" + :aria-label="title" + :href="externalUrl" class="btn external-url" data-container="body" target="_blank" rel="noopener noreferrer nofollow" - :title="title" - :aria-label="title" - :href="externalUrl" > <icon - name="external-link" :size="12" + name="external-link" /> </a> </template> diff --git a/app/assets/javascripts/environments/components/environment_item.vue b/app/assets/javascripts/environments/components/environment_item.vue index 23aaab2c441..866e91057ec 100644 --- a/app/assets/javascripts/environments/components/environment_item.vue +++ b/app/assets/javascripts/environments/components/environment_item.vue @@ -427,11 +427,11 @@ </script> <template> <div - class="gl-responsive-table-row" :class="{ 'js-child-row environment-child-row': model.isChildren, 'folder-row': model.isFolder, }" + class="gl-responsive-table-row" role="row"> <div class="table-section section-10" @@ -446,19 +446,19 @@ </div> <a v-if="!model.isFolder" - class="environment-name flex-truncate-parent table-mobile-content" - :href="environmentPath"> + :href="environmentPath" + class="environment-name flex-truncate-parent table-mobile-content"> <span - class="flex-truncate-child" v-tooltip :title="model.name" + class="flex-truncate-child" >{{ model.name }}</span> </a> <span v-else class="folder-name" - @click="onClickFolder" - role="button"> + role="button" + @click="onClickFolder"> <span class="folder-icon"> <i @@ -503,11 +503,11 @@ <span v-if="!model.isFolder && deploymentHasUser"> by <user-avatar-link - class="js-deploy-user-container" :link-href="deploymentUser.web_url" :img-src="deploymentUser.avatar_url" :img-alt="userImageAltDescription" :tooltip-text="deploymentUser.username" + class="js-deploy-user-container" /> </span> </div> @@ -518,8 +518,8 @@ > <a v-if="shouldRenderBuildName" - class="build-link flex-truncate-parent" :href="buildPath" + class="build-link flex-truncate-parent" > <span class="flex-truncate-child">{{ buildName }}</span> </a> diff --git a/app/assets/javascripts/environments/components/environment_monitoring.vue b/app/assets/javascripts/environments/components/environment_monitoring.vue index 8df1b6317e3..947e8c901e9 100644 --- a/app/assets/javascripts/environments/components/environment_monitoring.vue +++ b/app/assets/javascripts/environments/components/environment_monitoring.vue @@ -28,16 +28,16 @@ <template> <a v-tooltip - 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" + class="btn monitoring-url d-none d-sm-none d-md-block" + data-container="body" + rel="noopener noreferrer nofollow" > <icon - name="chart" :size="12" + name="chart" /> </a> </template> diff --git a/app/assets/javascripts/environments/components/environment_rollback.vue b/app/assets/javascripts/environments/components/environment_rollback.vue index 7515d711c50..310835c5ea9 100644 --- a/app/assets/javascripts/environments/components/environment_rollback.vue +++ b/app/assets/javascripts/environments/components/environment_rollback.vue @@ -39,10 +39,10 @@ </script> <template> <button + :disabled="isLoading" type="button" class="btn d-none d-sm-none d-md-block" @click="onClick" - :disabled="isLoading" > <span v-if="isLastDeployment"> diff --git a/app/assets/javascripts/environments/components/environment_stop.vue b/app/assets/javascripts/environments/components/environment_stop.vue index 7055f208451..eba58bedd6d 100644 --- a/app/assets/javascripts/environments/components/environment_stop.vue +++ b/app/assets/javascripts/environments/components/environment_stop.vue @@ -40,7 +40,7 @@ methods: { onClick() { // eslint-disable-next-line no-alert - if (confirm('Are you sure you want to stop this environment?')) { + if (window.confirm('Are you sure you want to stop this environment?')) { this.isLoading = true; $(this.$el).tooltip('dispose'); @@ -54,13 +54,13 @@ <template> <button v-tooltip + :disabled="isLoading" + :title="title" + :aria-label="title" type="button" class="btn stop-env-link d-none d-sm-none d-md-block" data-container="body" @click="onClick" - :disabled="isLoading" - :title="title" - :aria-label="title" > <i class="fa fa-stop stop-env-icon" diff --git a/app/assets/javascripts/environments/components/environment_terminal_button.vue b/app/assets/javascripts/environments/components/environment_terminal_button.vue index 0dbbbb75e07..f8e3165f8cd 100644 --- a/app/assets/javascripts/environments/components/environment_terminal_button.vue +++ b/app/assets/javascripts/environments/components/environment_terminal_button.vue @@ -30,15 +30,15 @@ <template> <a v-tooltip - class="btn terminal-button d-none d-sm-none d-md-block" - data-container="body" :title="title" :aria-label="title" :href="terminalPath" + class="btn terminal-button d-none d-sm-none d-md-block" + data-container="body" > <icon - name="terminal" :size="12" + name="terminal" /> </a> </template> diff --git a/app/assets/javascripts/environments/components/environments_app.vue b/app/assets/javascripts/environments/components/environments_app.vue index 3da762446c9..b18f02343d6 100644 --- a/app/assets/javascripts/environments/components/environments_app.vue +++ b/app/assets/javascripts/environments/components/environments_app.vue @@ -93,8 +93,8 @@ <div class="top-area"> <tabs :tabs="tabs" - @onChangeTab="onChangeTab" scope="environments" + @onChangeTab="onChangeTab" /> <div @@ -119,8 +119,8 @@ @onChangePage="onChangePage" > <empty-state - slot="emptyState" v-if="!isLoading && state.environments.length === 0" + slot="emptyState" :new-path="newEnvironmentPath" :help-path="helpPagePath" :can-create-environment="canCreateEnvironment" diff --git a/app/assets/javascripts/environments/folder/environments_folder_view.vue b/app/assets/javascripts/environments/folder/environments_folder_view.vue index 5ef5e347387..5f72a39c5cb 100644 --- a/app/assets/javascripts/environments/folder/environments_folder_view.vue +++ b/app/assets/javascripts/environments/folder/environments_folder_view.vue @@ -39,8 +39,8 @@ <template> <div :class="cssContainerClass"> <div - class="top-area" v-if="!isLoading" + class="top-area" > <h4 class="js-folder-name environments-folder-name"> @@ -49,8 +49,8 @@ <tabs :tabs="tabs" - @onChangeTab="onChangeTab" scope="environments" + @onChangeTab="onChangeTab" /> </div> diff --git a/app/assets/javascripts/filtered_search/components/recent_searches_dropdown_content.vue b/app/assets/javascripts/filtered_search/components/recent_searches_dropdown_content.vue index 26618af9515..a8eb8d94be3 100644 --- a/app/assets/javascripts/filtered_search/components/recent_searches_dropdown_content.vue +++ b/app/assets/javascripts/filtered_search/components/recent_searches_dropdown_content.vue @@ -72,9 +72,9 @@ export default { @click="onItemActivated(item.text)"> <span> <span - class="filtered-search-history-dropdown-token" v-for="(token, index) in item.tokens" :key="`dropdown-token-${index}`" + class="filtered-search-history-dropdown-token" > <span class="name">{{ token.prefix }}</span> <span class="value">{{ token.suffix }}</span> diff --git a/app/assets/javascripts/gl_dropdown.js b/app/assets/javascripts/gl_dropdown.js index 746a06b7c4f..7fbba7e27cb 100644 --- a/app/assets/javascripts/gl_dropdown.js +++ b/app/assets/javascripts/gl_dropdown.js @@ -602,7 +602,11 @@ GitLabDropdown = (function() { var selector; selector = '.dropdown-content'; if (this.dropdown.find(".dropdown-toggle-page").length) { - selector = ".dropdown-page-one .dropdown-content"; + if (this.options.containerSelector) { + selector = this.options.containerSelector; + } else { + selector = '.dropdown-page-one .dropdown-content'; + } } return $(selector, this.dropdown).empty(); diff --git a/app/assets/javascripts/group_label_subscription.js b/app/assets/javascripts/group_label_subscription.js index 5648cb9a888..d33e3a37580 100644 --- a/app/assets/javascripts/group_label_subscription.js +++ b/app/assets/javascripts/group_label_subscription.js @@ -1,7 +1,12 @@ import $ from 'jquery'; +import { __ } from '~/locale'; import axios from './lib/utils/axios_utils'; import flash from './flash'; -import { __ } from './locale'; + +const tooltipTitles = { + group: __('Unsubscribe at group level'), + project: __('Unsubscribe at project level'), +}; export default class GroupLabelSubscription { constructor(container) { @@ -35,6 +40,7 @@ export default class GroupLabelSubscription { this.$unsubscribeButtons.attr('data-url', url); axios.post(url) + .then(() => GroupLabelSubscription.setNewTooltip($btn)) .then(() => this.toggleSubscriptionButtons()) .catch(() => flash(__('There was an error when subscribing to this label.'))); } @@ -44,4 +50,14 @@ export default class GroupLabelSubscription { this.$subscribeButtons.toggleClass('hidden'); this.$unsubscribeButtons.toggleClass('hidden'); } + + static setNewTooltip($button) { + if (!$button.hasClass('js-subscribe-button')) return; + + const type = $button.hasClass('js-group-level') ? 'group' : 'project'; + const newTitle = tooltipTitles[type]; + + $('.js-unsubscribe-button', $button.closest('.label-actions-list')) + .tooltip('hide').attr('title', newTitle).tooltip('_fixTitle'); + } } diff --git a/app/assets/javascripts/groups/components/app.vue b/app/assets/javascripts/groups/components/app.vue index 22eb7bd44c5..1def38bb336 100644 --- a/app/assets/javascripts/groups/components/app.vue +++ b/app/assets/javascripts/groups/components/app.vue @@ -216,10 +216,10 @@ export default { <template> <div> <loading-icon - class="loading-animation prepend-top-20" - size="2" v-if="isLoading" :label="s__('GroupsTree|Loading groups')" + class="loading-animation prepend-top-20" + size="2" /> <groups-component v-if="!isLoading" @@ -230,10 +230,10 @@ export default { /> <deprecated-modal v-show="showModal" - kind="warning" :primary-button-label="__('Leave')" :title="__('Are you sure?')" :text="groupLeaveConfirmationMessage" + kind="warning" @cancel="hideLeaveGroupModal" @submit="leaveGroup" /> diff --git a/app/assets/javascripts/groups/components/group_item.vue b/app/assets/javascripts/groups/components/group_item.vue index 7f64a9bd741..efbf2e3a295 100644 --- a/app/assets/javascripts/groups/components/group_item.vue +++ b/app/assets/javascripts/groups/components/group_item.vue @@ -71,14 +71,14 @@ export default { <template> <li - @click.stop="onClickRowGroup" :id="groupDomId" :class="rowClass" class="group-row" + @click.stop="onClickRowGroup" > <div - class="group-row-contents" - :class="{ 'project-row-contents': !isGroup }"> + :class="{ 'project-row-contents': !isGroup }" + class="group-row-contents"> <item-actions v-if="isGroup" :group="group" @@ -99,8 +99,8 @@ export default { /> </div> <div - class="avatar-container prepend-top-8 prepend-left-5 s24 d-none d-sm-block" :class="{ 'content-loading': group.isChildrenLoading }" + class="avatar-container prepend-top-8 prepend-left-5 s24 d-none d-sm-block" > <a :href="group.relativePath" @@ -108,14 +108,14 @@ export default { > <img v-if="hasAvatar" - class="avatar s24" :src="group.avatarUrl" + class="avatar s24" /> <identicon v-else - size-class="s24" :entity-id="group.id" :entity-name="group.name" + size-class="s24" /> </a> </div> diff --git a/app/assets/javascripts/groups/components/item_actions.vue b/app/assets/javascripts/groups/components/item_actions.vue index 87065b3d6e3..24eec4901ec 100644 --- a/app/assets/javascripts/groups/components/item_actions.vue +++ b/app/assets/javascripts/groups/components/item_actions.vue @@ -54,13 +54,13 @@ export default { <a v-tooltip v-if="group.canLeave" - @click.prevent="onLeaveGroup" :href="group.leavePath" :title="leaveBtnTitle" :aria-label="leaveBtnTitle" data-container="body" data-placement="bottom" - class="leave-group btn no-expand"> + class="leave-group btn no-expand" + @click.prevent="onLeaveGroup"> <icon name="leave"/> </a> </div> diff --git a/app/assets/javascripts/groups/components/item_stats.vue b/app/assets/javascripts/groups/components/item_stats.vue index 168b4e4af2c..87ab5480c15 100644 --- a/app/assets/javascripts/groups/components/item_stats.vue +++ b/app/assets/javascripts/groups/components/item_stats.vue @@ -45,44 +45,44 @@ <div class="stats"> <item-stats-value v-if="isGroup" - css-class="number-subgroups" - icon-name="folder" :title="__('Subgroups')" :value="item.subgroupCount" + css-class="number-subgroups" + icon-name="folder" /> <item-stats-value v-if="isGroup" - css-class="number-projects" - icon-name="bookmark" :title="__('Projects')" :value="item.projectCount" + css-class="number-projects" + icon-name="bookmark" /> <item-stats-value v-if="isGroup" - css-class="number-users" - icon-name="users" :title="__('Members')" :value="item.memberCount" + css-class="number-users" + icon-name="users" /> <item-stats-value v-if="isProject" + :value="item.starCount" css-class="project-stars" icon-name="star" - :value="item.starCount" /> <item-stats-value - css-class="item-visibility" - tooltip-placement="left" :icon-name="visibilityIcon" :title="visibilityTooltip" + css-class="item-visibility" + tooltip-placement="left" /> <div - class="last-updated" v-if="isProject" + class="last-updated" > <time-ago-tooltip - tooltip-placement="bottom" :time="item.updatedAt" + tooltip-placement="bottom" /> </div> </div> diff --git a/app/assets/javascripts/groups/components/item_stats_value.vue b/app/assets/javascripts/groups/components/item_stats_value.vue index 4d86ac8023c..ef9f2bca76c 100644 --- a/app/assets/javascripts/groups/components/item_stats_value.vue +++ b/app/assets/javascripts/groups/components/item_stats_value.vue @@ -52,10 +52,10 @@ <template> <span v-tooltip - data-container="body" :data-placement="tooltipPlacement" :class="cssClass" :title="title" + data-container="body" > <icon :name="iconName" /> <span diff --git a/app/assets/javascripts/ide/components/activity_bar.vue b/app/assets/javascripts/ide/components/activity_bar.vue index 05dbc1410de..62697e0ecc3 100644 --- a/app/assets/javascripts/ide/components/activity_bar.vue +++ b/app/assets/javascripts/ide/components/activity_bar.vue @@ -1,4 +1,5 @@ <script> +import $ from 'jquery'; import { mapActions, mapGetters, mapState } from 'vuex'; import Icon from '~/vue_shared/components/icon.vue'; import tooltip from '~/vue_shared/directives/tooltip'; @@ -20,6 +21,13 @@ export default { }, methods: { ...mapActions(['updateActivityBarView']), + changedActivityView(e, view) { + e.currentTarget.blur(); + + this.updateActivityBarView(view); + + $(e.currentTarget).tooltip('hide'); + }, }, activityBarViews, }; @@ -31,12 +39,12 @@ export default { <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')" + data-container="body" + data-placement="right" + class="ide-sidebar-link" > <icon :size="16" @@ -47,16 +55,16 @@ export default { <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')" + data-container="body" + data-placement="right" + type="button" + class="ide-sidebar-link js-ide-edit-mode" + @click.prevent="changedActivityView($event, $options.activityBarViews.edit)" > <icon name="code" @@ -66,16 +74,16 @@ export default { <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')" + data-container="body" + data-placement="right" + type="button" + class="ide-sidebar-link js-ide-review-mode" + @click.prevent="changedActivityView($event, $options.activityBarViews.review)" > <icon name="file-modified" @@ -85,16 +93,16 @@ export default { <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')" + data-container="body" + data-placement="right" + type="button" + class="ide-sidebar-link js-ide-commit-mode" + @click.prevent="changedActivityView($event, $options.activityBarViews.commit)" > <icon name="commit" diff --git a/app/assets/javascripts/ide/components/commit_sidebar/form.vue b/app/assets/javascripts/ide/components/commit_sidebar/form.vue index e2b42ab2642..955d9280728 100644 --- a/app/assets/javascripts/ide/components/commit_sidebar/form.vue +++ b/app/assets/javascripts/ide/components/commit_sidebar/form.vue @@ -91,7 +91,6 @@ export default { <template> <div - class="multi-file-commit-form" :class="{ 'is-compact': isCompact, 'is-full': !isCompact @@ -99,6 +98,7 @@ export default { :style="{ height: componentHeight ? `${componentHeight}px` : null, }" + class="multi-file-commit-form" > <transition name="commit-form-slide-up" @@ -108,12 +108,12 @@ export default { > <div v-if="isCompact" - class="commit-form-compact" ref="compactEl" + class="commit-form-compact" > <button - type="button" :disabled="!hasChanges" + type="button" class="btn btn-primary btn-sm btn-block" @click="toggleIsSmall" > @@ -126,8 +126,8 @@ export default { </div> <form v-if="!isCompact" - @submit.prevent.stop="commitChanges" ref="formEl" + @submit.prevent.stop="commitChanges" > <transition name="fade"> <success-message @@ -143,8 +143,8 @@ export default { <loading-button :loading="submitCommitLoading" :disabled="commitButtonDisabled" - container-class="btn btn-success btn-sm float-left" :label="__('Commit')" + container-class="btn btn-success btn-sm float-left" @click="commitChanges" /> <button diff --git a/app/assets/javascripts/ide/components/commit_sidebar/list.vue b/app/assets/javascripts/ide/components/commit_sidebar/list.vue index 1325fc993b2..d0fb0e3d99e 100644 --- a/app/assets/javascripts/ide/components/commit_sidebar/list.vue +++ b/app/assets/javascripts/ide/components/commit_sidebar/list.vue @@ -34,6 +34,10 @@ export default { type: String, required: true, }, + actionBtnIcon: { + type: String, + required: true, + }, itemActionComponent: { type: String, required: true, @@ -43,11 +47,15 @@ export default { required: false, default: false, }, - }, - data() { - return { - showActionButton: false, - }; + activeFileKey: { + type: String, + required: false, + default: null, + }, + keyPrefix: { + type: String, + required: true, + }, }, computed: { titleText() { @@ -55,15 +63,15 @@ export default { title: this.title, }); }, + filesLength() { + return this.fileList.length; + }, }, methods: { ...mapActions(['stageAllChanges', 'unstageAllChanges']), actionBtnClicked() { this[this.action](); }, - setShowActionButton(show) { - this.showActionButton = show; - }, }, }; </script> @@ -74,8 +82,6 @@ export default { > <header class="multi-file-commit-panel-header" - @mouseenter="setShowActionButton(true)" - @mouseleave="setShowActionButton(false)" > <div class="multi-file-commit-panel-header-title" @@ -86,24 +92,40 @@ export default { :size="18" /> {{ titleText }} - <span - v-show="!showActionButton" - class="ide-commit-file-count" - > - {{ fileList.length }} - </span> - <button - v-show="showActionButton" - type="button" - class="btn btn-blank btn-link ide-staged-action-btn" - @click="actionBtnClicked" - > - {{ actionBtnText }} - </button> + <div class="d-flex ml-auto"> + <button + v-tooltip + v-show="filesLength" + :class="{ + 'd-flex': filesLength + }" + :title="actionBtnText" + type="button" + class="btn btn-default ide-staged-action-btn p-0 order-1 align-items-center" + data-placement="bottom" + data-container="body" + data-boundary="viewport" + @click="actionBtnClicked" + > + <icon + :name="actionBtnIcon" + :size="12" + class="ml-auto mr-auto" + /> + </button> + <span + :class="{ + 'rounded-right': !filesLength + }" + class="ide-commit-file-count order-0 rounded-left text-center" + > + {{ filesLength }} + </span> + </div> </div> </header> <ul - v-if="fileList.length" + v-if="filesLength" class="multi-file-commit-list list-unstyled append-bottom-0" > <li @@ -113,8 +135,9 @@ export default { <list-item :file="file" :action-component="itemActionComponent" - :key-prefix="title" + :key-prefix="keyPrefix" :staged-list="stagedList" + :active-file-key="activeFileKey" /> </li> </ul> 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 2254271c679..d376a004e84 100644 --- a/app/assets/javascripts/ide/components/commit_sidebar/list_collapsed.vue +++ b/app/assets/javascripts/ide/components/commit_sidebar/list_collapsed.vue @@ -38,14 +38,17 @@ export default { return this.modifiedFilesLength ? 'multi-file-modified' : ''; }, additionsTooltip() { - return sprintf(n__('1 %{type} addition', '%d %{type} additions', this.addedFilesLength), { + return sprintf(n__('1 %{type} addition', '%{count} %{type} additions', this.addedFilesLength), { type: this.title.toLowerCase(), + count: this.addedFilesLength, }); }, modifiedTooltip() { return sprintf( - n__('1 %{type} modification', '%d %{type} modifications', this.modifiedFilesLength), - { type: this.title.toLowerCase() }, + n__('1 %{type} modification', '%{count} %{type} modifications', this.modifiedFilesLength), { + type: this.title.toLowerCase(), + count: this.modifiedFilesLength, + }, ); }, titleTooltip() { 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 03f3e4de83c..5cda7967130 100644 --- a/app/assets/javascripts/ide/components/commit_sidebar/list_item.vue +++ b/app/assets/javascripts/ide/components/commit_sidebar/list_item.vue @@ -1,5 +1,6 @@ <script> import { mapActions } from 'vuex'; +import tooltip from '~/vue_shared/directives/tooltip'; import Icon from '~/vue_shared/components/icon.vue'; import StageButton from './stage_button.vue'; import UnstageButton from './unstage_button.vue'; @@ -11,6 +12,9 @@ export default { StageButton, UnstageButton, }, + directives: { + tooltip, + }, props: { file: { type: Object, @@ -30,6 +34,11 @@ export default { required: false, default: false, }, + activeFileKey: { + type: String, + required: false, + default: null, + }, }, computed: { iconName() { @@ -39,6 +48,15 @@ export default { iconClass() { return `multi-file-${this.file.tempFile ? 'addition' : 'modified'} append-right-8`; }, + fullKey() { + return `${this.keyPrefix}-${this.file.key}`; + }, + isActive() { + return this.activeFileKey === this.fullKey; + }, + tooltipTitle() { + return this.file.path === this.file.name ? '' : this.file.path; + }, }, methods: { ...mapActions([ @@ -51,7 +69,7 @@ export default { openFileInEditor() { return this.openPendingTab({ file: this.file, - keyPrefix: this.keyPrefix.toLowerCase(), + keyPrefix: this.keyPrefix, }).then(changeViewer => { if (changeViewer) { this.updateViewer(viewerTypes.diff); @@ -70,24 +88,30 @@ export default { </script> <template> - <div class="multi-file-commit-list-item"> + <div class="multi-file-commit-list-item position-relative"> <button + v-tooltip + :title="tooltipTitle" + :class="{ + 'is-active': isActive + }" type="button" - class="multi-file-commit-list-path" + class="multi-file-commit-list-path w-100 border-0 ml-0 mr-0" @dblclick="fileAction" @click="openFileInEditor" > - <span class="multi-file-commit-list-file-path"> + <span class="multi-file-commit-list-file-path d-flex align-items-center"> <icon :name="iconName" :size="16" :css-classes="iconClass" - />{{ file.path }} + />{{ file.name }} </span> </button> <component :is="actionComponent" :path="file.path" + class="d-flex position-absolute" /> </div> </template> diff --git a/app/assets/javascripts/ide/components/commit_sidebar/message_field.vue b/app/assets/javascripts/ide/components/commit_sidebar/message_field.vue index 0ac0af2feaa..40496c80a46 100644 --- a/app/assets/javascripts/ide/components/commit_sidebar/message_field.vue +++ b/app/assets/javascripts/ide/components/commit_sidebar/message_field.vue @@ -66,10 +66,10 @@ export default { <template> <fieldset class="common-note-form ide-commit-message-field"> <div - class="md-area" :class="{ 'is-focused': isFocused }" + class="md-area" > <div v-once @@ -92,10 +92,10 @@ export default { <div class="ide-commit-message-textarea-container"> <div class="ide-commit-message-highlights-container"> <div - class="note-textarea highlights monospace" :style="{ transform: `translate3d(0, ${-scrollTop}px, 0)` }" + class="note-textarea highlights monospace" > <div v-for="(line, index) in allLines" @@ -113,15 +113,15 @@ export default { </div> </div> <textarea - class="note-textarea ide-commit-message-textarea" - name="commit-message" + ref="textarea" :placeholder="__('Write a commit message...')" :value="text" + class="note-textarea ide-commit-message-textarea" + name="commit-message" @scroll="handleScroll" @input="onInput" @focus="updateIsFocused(true)" @blur="updateIsFocused(false)" - ref="textarea" > </textarea> </div> 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 00f2312ae51..35ab3fd11df 100644 --- a/app/assets/javascripts/ide/components/commit_sidebar/radio_group.vue +++ b/app/assets/javascripts/ide/components/commit_sidebar/radio_group.vue @@ -58,12 +58,12 @@ export default { }" > <input - type="radio" - name="commit-action" :value="value" - @change="updateCommitAction($event.target.value)" :checked="commitAction === value" :disabled="disabled" + type="radio" + name="commit-action" + @change="updateCommitAction($event.target.value)" /> <span class="prepend-left-10"> <span @@ -80,9 +80,9 @@ export default { class="ide-commit-new-branch" > <input + :placeholder="newBranchName" type="text" class="form-control monospace" - :placeholder="newBranchName" @input="updateBranchName($event.target.value)" /> </div> diff --git a/app/assets/javascripts/ide/components/commit_sidebar/stage_button.vue b/app/assets/javascripts/ide/components/commit_sidebar/stage_button.vue index 52dce8412ab..7014b9f605e 100644 --- a/app/assets/javascripts/ide/components/commit_sidebar/stage_button.vue +++ b/app/assets/javascripts/ide/components/commit_sidebar/stage_button.vue @@ -25,35 +25,51 @@ export default { <template> <div v-once - class="multi-file-discard-btn" + class="multi-file-discard-btn dropdown" > <button v-tooltip - type="button" - class="btn btn-blank append-right-5" :aria-label="__('Stage changes')" :title="__('Stage changes')" + type="button" + class="btn btn-blank append-right-5 d-flex align-items-center" data-container="body" + data-boundary="viewport" + data-placement="bottom" @click.stop="stageChange(path)" > <icon - name="mobile-issue-close" :size="12" + name="mobile-issue-close" /> </button> <button v-tooltip + :title="__('More actions')" type="button" - class="btn btn-blank" - :aria-label="__('Discard changes')" - :title="__('Discard changes')" + class="btn btn-blank d-flex align-items-center" data-container="body" - @click.stop="discardFileChanges(path)" + data-boundary="viewport" + data-placement="bottom" + data-toggle="dropdown" + data-display="static" > <icon - name="remove" :size="12" + name="more" /> </button> + <div class="dropdown-menu dropdown-menu-right"> + <ul> + <li> + <button + type="button" + @click.stop="discardFileChanges(path)" + > + {{ __('Discard changes') }} + </button> + </li> + </ul> + </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 index 123d60da47e..9cec73ec00e 100644 --- a/app/assets/javascripts/ide/components/commit_sidebar/unstage_button.vue +++ b/app/assets/javascripts/ide/components/commit_sidebar/unstage_button.vue @@ -29,16 +29,18 @@ export default { > <button v-tooltip - type="button" - class="btn btn-blank" :aria-label="__('Unstage changes')" :title="__('Unstage changes')" + type="button" + class="btn btn-blank d-flex align-items-center" data-container="body" + data-boundary="viewport" + data-placement="bottom" @click="unstageChange(path)" > <icon - name="history" :size="12" + name="history" /> </button> </div> diff --git a/app/assets/javascripts/ide/components/editor_mode_dropdown.vue b/app/assets/javascripts/ide/components/editor_mode_dropdown.vue index b9af4d27145..95598c9aca6 100644 --- a/app/assets/javascripts/ide/components/editor_mode_dropdown.vue +++ b/app/assets/javascripts/ide/components/editor_mode_dropdown.vue @@ -44,11 +44,11 @@ export default { <ul> <li> <a - href="#" - @click.prevent="changeMode($options.viewerTypes.mr)" :class="{ 'is-active': viewer === $options.viewerTypes.mr, }" + href="#" + @click.prevent="changeMode($options.viewerTypes.mr)" > <strong class="dropdown-menu-inner-title"> {{ mergeReviewLine }} @@ -60,11 +60,11 @@ export default { </li> <li> <a - href="#" - @click.prevent="changeMode($options.viewerTypes.diff)" :class="{ 'is-active': viewer === $options.viewerTypes.diff, }" + href="#" + @click.prevent="changeMode($options.viewerTypes.diff)" > <strong class="dropdown-menu-inner-title">{{ __('Reviewing') }}</strong> <span class="dropdown-menu-inner-content"> diff --git a/app/assets/javascripts/ide/components/external_link.vue b/app/assets/javascripts/ide/components/external_link.vue index cf3316a8179..e24fe5bbccb 100644 --- a/app/assets/javascripts/ide/components/external_link.vue +++ b/app/assets/javascripts/ide/components/external_link.vue @@ -26,15 +26,15 @@ export default { > <a :href="file.permalink" - target="_blank" :title="s__('IDE|Open in file view')" + target="_blank" rel="noopener noreferrer" > <span class="vertical-align-middle">Open in file view</span> <icon + :size="16" name="external-link" css-classes="vertical-align-middle space-right" - :size="16" /> </a> </div> diff --git a/app/assets/javascripts/ide/components/file_finder/index.vue b/app/assets/javascripts/ide/components/file_finder/index.vue index cabb3f59b17..0ba33053717 100644 --- a/app/assets/javascripts/ide/components/file_finder/index.vue +++ b/app/assets/javascripts/ide/components/file_finder/index.vue @@ -173,38 +173,38 @@ export default { > <div class="dropdown-input"> <input + ref="searchInput" + :placeholder="__('Search files')" + v-model="searchText" 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 }" + aria-hidden="true" + class="fa fa-search dropdown-input-search" ></i> <i - role="button" :aria-label="__('Clear search input')" - class="fa fa-times dropdown-input-clear" :class="{ show: showClearInputButton }" + role="button" + class="fa fa-times dropdown-input-clear" @click="clearSearchInput" ></i> </div> <div> <virtual-list + ref="virtualScrollList" :size="listHeight" :remain="listShowCount" wtag="ul" - ref="virtualScrollList" > <template v-if="filteredBlobsLength"> <li @@ -212,11 +212,11 @@ export default { :key="file.key" > <item - class="disable-hover" :file="file" :search-text="searchText" :focused="index === focusedIndex" :index="index" + class="disable-hover" @click="openFile" @mouseover="onMouseOver" @mousemove="onMouseMove" diff --git a/app/assets/javascripts/ide/components/file_finder/item.vue b/app/assets/javascripts/ide/components/file_finder/item.vue index d4427420207..a4cf3edb981 100644 --- a/app/assets/javascripts/ide/components/file_finder/item.vue +++ b/app/assets/javascripts/ide/components/file_finder/item.vue @@ -59,11 +59,11 @@ export default { <template> <button - type="button" - class="diff-changed-file" :class="{ 'is-focused': focused, }" + type="button" + class="diff-changed-file" @click.prevent="clickRow" @mouseover="mouseOverRow" @mousemove="mouseMove" diff --git a/app/assets/javascripts/ide/components/ide.vue b/app/assets/javascripts/ide/components/ide.vue index f5f832521c5..f5f7f967a92 100644 --- a/app/assets/javascripts/ide/components/ide.vue +++ b/app/assets/javascripts/ide/components/ide.vue @@ -93,8 +93,8 @@ export default { :merge-request-id="currentMergeRequestId" /> <repo-editor - class="multi-file-edit-pane-content" :file="activeFile" + class="multi-file-edit-pane-content" /> </template> <template diff --git a/app/assets/javascripts/ide/components/ide_review.vue b/app/assets/javascripts/ide/components/ide_review.vue index 0c9ec3b00f0..f9978762c45 100644 --- a/app/assets/javascripts/ide/components/ide_review.vue +++ b/app/assets/javascripts/ide/components/ide_review.vue @@ -11,17 +11,20 @@ export default { }, computed: { ...mapGetters(['currentMergeRequest']), - ...mapState(['viewer']), + ...mapState(['viewer', 'currentMergeRequestId']), showLatestChangesText() { - return !this.currentMergeRequest || this.viewer === viewerTypes.diff; + return !this.currentMergeRequestId || this.viewer === viewerTypes.diff; }, showMergeRequestText() { - return this.currentMergeRequest && this.viewer === viewerTypes.mr; + return this.currentMergeRequestId && this.viewer === viewerTypes.mr; + }, + mergeRequestId() { + return `!${this.currentMergeRequest.iid}`; }, }, mounted() { this.$nextTick(() => { - this.updateViewer(this.currentMergeRequest ? viewerTypes.mr : viewerTypes.diff); + this.updateViewer(this.currentMergeRequestId ? viewerTypes.mr : viewerTypes.diff); }); }, methods: { @@ -33,8 +36,8 @@ export default { <template> <ide-tree-list :viewer-type="viewer" - header-class="ide-review-header" :disable-action-dropdown="true" + header-class="ide-review-header" > <template slot="header" @@ -54,7 +57,11 @@ export default { </template> <template v-else-if="showMergeRequestText"> {{ __('Merge request') }} - (<a :href="currentMergeRequest.web_url">!{{ currentMergeRequest.iid }}</a>) + (<a + v-if="currentMergeRequest" + :href="currentMergeRequest.web_url" + v-text="mergeRequestId" + ></a>) </template> </div> </template> diff --git a/app/assets/javascripts/ide/components/ide_side_bar.vue b/app/assets/javascripts/ide/components/ide_side_bar.vue index 3f980203911..21906674c4b 100644 --- a/app/assets/javascripts/ide/components/ide_side_bar.vue +++ b/app/assets/javascripts/ide/components/ide_side_bar.vue @@ -1,4 +1,5 @@ <script> +import $ from 'jquery'; import { mapState, mapGetters } from 'vuex'; import ProjectAvatarImage from '~/vue_shared/components/project_avatar/image.vue'; import Icon from '~/vue_shared/components/icon.vue'; @@ -13,6 +14,7 @@ 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 MergeRequestDropdown from './merge_requests/dropdown.vue'; import { activityBarViews } from '../constants'; export default { @@ -32,10 +34,12 @@ export default { CommitForm, IdeReview, SuccessMessage, + MergeRequestDropdown, }, data() { return { showTooltip: false, + showMergeRequestsDropdown: false, }; }, computed: { @@ -46,6 +50,7 @@ export default { 'changedFiles', 'stagedFiles', 'lastCommitMsg', + 'currentMergeRequestId', ]), ...mapGetters(['currentProject', 'someUncommitedChanges']), showSuccessMessage() { @@ -61,9 +66,39 @@ export default { watch: { currentBranchId() { this.$nextTick(() => { + if (!this.$refs.branchId) return; + this.showTooltip = this.$refs.branchId.scrollWidth > this.$refs.branchId.offsetWidth; }); }, + loading() { + this.$nextTick(() => { + this.addDropdownListeners(); + }); + }, + }, + mounted() { + this.addDropdownListeners(); + }, + beforeDestroy() { + $(this.$refs.mergeRequestDropdown) + .off('show.bs.dropdown') + .off('hide.bs.dropdown'); + }, + methods: { + addDropdownListeners() { + if (!this.$refs.mergeRequestDropdown) return; + + $(this.$refs.mergeRequestDropdown) + .on('show.bs.dropdown', () => { + this.toggleMergeRequestDropdown(); + }).on('hide.bs.dropdown', () => { + this.toggleMergeRequestDropdown(); + }); + }, + toggleMergeRequestDropdown() { + this.showMergeRequestsDropdown = !this.showMergeRequestsDropdown; + }, }, }; </script> @@ -80,53 +115,79 @@ export default { <div class="multi-file-commit-panel-inner"> <template v-if="loading"> <div - class="multi-file-loading-container" v-for="n in 3" :key="n" + class="multi-file-loading-container" > <skeleton-loading-container /> </div> </template> <template v-else> - <div class="context-header ide-context-header"> - <a - :href="currentProject.web_url" + <div + ref="mergeRequestDropdown" + class="context-header ide-context-header dropdown" + > + <button + type="button" + data-toggle="dropdown" > <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" + class="avatar-container project-avatar" /> </div> <identicon v-else - size-class="s40" :entity-id="currentProject.id" :entity-name="currentProject.name" + size-class="s40" /> <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 class="d-flex"> + <div + v-tooltip + v-if="currentBranchId" + ref="branchId" + :title="branchTooltipTitle" + class="sidebar-context-title ide-sidebar-branch-title" + > + <icon + name="branch" + css-classes="append-right-5" + />{{ currentBranchId }} + </div> + <div + v-if="currentMergeRequestId" + :class="{ + 'prepend-left-8': currentBranchId + }" + class="sidebar-context-title ide-sidebar-branch-title" + > + <icon + name="git-merge" + css-classes="append-right-5" + />!{{ currentMergeRequestId }} + </div> </div> </div> - </a> + <icon + class="ml-auto" + name="chevron-down" + /> + </button> + <merge-request-dropdown + :show="showMergeRequestsDropdown" + /> </div> <div class="multi-file-commit-panel-inner-scroll"> <component diff --git a/app/assets/javascripts/ide/components/ide_status_bar.vue b/app/assets/javascripts/ide/components/ide_status_bar.vue index 368a2995ed9..0582ad32e92 100644 --- a/app/assets/javascripts/ide/components/ide_status_bar.vue +++ b/app/assets/javascripts/ide/components/ide_status_bar.vue @@ -35,9 +35,7 @@ export default { }, watch: { lastCommit() { - if (!this.isPollingInitialized) { - this.initPipelinePolling(); - } + this.initPipelinePolling(); }, }, mounted() { @@ -47,9 +45,8 @@ export default { if (this.intervalId) { clearInterval(this.intervalId); } - if (this.isPollingInitialized) { - this.stopPipelinePolling(); - } + + this.stopPipelinePolling(); }, methods: { ...mapActions('pipelines', ['fetchLatestPipeline', 'stopPipelinePolling']), @@ -59,8 +56,9 @@ export default { }, 1000); }, initPipelinePolling() { - this.fetchLatestPipeline(); - this.isPollingInitialized = true; + if (this.lastCommit) { + this.fetchLatestPipeline(); + } }, commitAgeUpdate() { if (this.lastCommit) { @@ -77,22 +75,22 @@ export default { <template> <footer class="ide-status-bar"> <div - class="ide-status-branch" v-if="lastCommit && lastCommitFormatedAge" + class="ide-status-branch" > <span - class="ide-status-pipeline" v-if="latestPipeline && latestPipeline.details" + class="ide-status-pipeline" > <ci-icon - :status="latestPipeline.details.status" v-tooltip + :status="latestPipeline.details.status" :title="latestPipeline.details.status.text" /> Pipeline <a - class="monospace" - :href="latestPipeline.details.status.details_path">#{{ latestPipeline.id }}</a> + :href="latestPipeline.details.status.details_path" + class="monospace">#{{ latestPipeline.id }}</a> {{ latestPipeline.details.status.text }} for </span> @@ -102,18 +100,18 @@ export default { /> <a v-tooltip - class="commit-sha" :title="lastCommit.message" :href="getCommitPath(lastCommit.short_id)" + class="commit-sha" >{{ 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)" + data-placement="top" + data-container="body" > {{ lastCommitFormatedAge }} </time> @@ -131,8 +129,8 @@ export default { {{ file.eol }} </div> <div - class="ide-status-file" - v-if="file && !file.binary"> + v-if="file && !file.binary" + class="ide-status-file"> {{ file.editorRow }}:{{ file.editorColumn }} </div> <div diff --git a/app/assets/javascripts/ide/components/ide_tree_list.vue b/app/assets/javascripts/ide/components/ide_tree_list.vue index e64a09fcc90..0df99798d21 100644 --- a/app/assets/javascripts/ide/components/ide_tree_list.vue +++ b/app/assets/javascripts/ide/components/ide_tree_list.vue @@ -50,17 +50,17 @@ export default { > <template v-if="showLoading"> <div - class="multi-file-loading-container" v-for="n in 3" :key="n" + class="multi-file-loading-container" > <skeleton-loading-container /> </div> </template> <template v-else> <header - class="ide-tree-header" :class="headerClass" + class="ide-tree-header" > <slot name="header"></slot> </header> diff --git a/app/assets/javascripts/ide/components/jobs/detail.vue b/app/assets/javascripts/ide/components/jobs/detail.vue new file mode 100644 index 00000000000..f39ce545656 --- /dev/null +++ b/app/assets/javascripts/ide/components/jobs/detail.vue @@ -0,0 +1,137 @@ +<script> +import { mapActions, mapState } from 'vuex'; +import _ from 'underscore'; +import { __ } from '../../../locale'; +import tooltip from '../../../vue_shared/directives/tooltip'; +import Icon from '../../../vue_shared/components/icon.vue'; +import ScrollButton from './detail/scroll_button.vue'; +import JobDescription from './detail/description.vue'; + +const scrollPositions = { + top: 0, + bottom: 1, +}; + +export default { + directives: { + tooltip, + }, + components: { + Icon, + ScrollButton, + JobDescription, + }, + data() { + return { + scrollPos: scrollPositions.top, + }; + }, + computed: { + ...mapState('pipelines', ['detailJob']), + isScrolledToBottom() { + return this.scrollPos === scrollPositions.bottom; + }, + isScrolledToTop() { + return this.scrollPos === scrollPositions.top; + }, + jobOutput() { + return this.detailJob.output || __('No messages were logged'); + }, + }, + mounted() { + this.getTrace(); + }, + methods: { + ...mapActions('pipelines', ['fetchJobTrace', 'setDetailJob']), + scrollDown() { + if (this.$refs.buildTrace) { + this.$refs.buildTrace.scrollTo(0, this.$refs.buildTrace.scrollHeight); + } + }, + scrollUp() { + if (this.$refs.buildTrace) { + this.$refs.buildTrace.scrollTo(0, 0); + } + }, + scrollBuildLog: _.throttle(function buildLogScrollDebounce() { + const { scrollTop } = this.$refs.buildTrace; + const { offsetHeight, scrollHeight } = this.$refs.buildTrace; + + if (scrollTop + offsetHeight === scrollHeight) { + this.scrollPos = scrollPositions.bottom; + } else if (scrollTop === 0) { + this.scrollPos = scrollPositions.top; + } else { + this.scrollPos = ''; + } + }), + getTrace() { + return this.fetchJobTrace().then(() => this.scrollDown()); + }, + }, +}; +</script> + +<template> + <div class="ide-pipeline build-page d-flex flex-column flex-fill"> + <header class="ide-job-header d-flex align-items-center"> + <button + class="btn btn-default btn-sm d-flex" + @click="setDetailJob(null)" + > + <icon + name="chevron-left" + /> + {{ __('View jobs') }} + </button> + </header> + <div class="top-bar d-flex border-left-0"> + <job-description + :job="detailJob" + /> + <div class="controllers ml-auto"> + <a + v-tooltip + :title="__('Show complete raw log')" + :href="detailJob.rawPath" + data-placement="top" + data-container="body" + class="controllers-buttons" + target="_blank" + > + <i + aria-hidden="true" + class="fa fa-file-text-o" + ></i> + </a> + <scroll-button + :disabled="isScrolledToTop" + direction="up" + @click="scrollUp" + /> + <scroll-button + :disabled="isScrolledToBottom" + direction="down" + @click="scrollDown" + /> + </div> + </div> + <pre + ref="buildTrace" + class="build-trace mb-0 h-100" + @scroll="scrollBuildLog" + > + <code + v-show="!detailJob.isLoading" + class="bash" + v-html="jobOutput" + > + </code> + <div + v-show="detailJob.isLoading" + class="build-loader-animation" + > + </div> + </pre> + </div> +</template> diff --git a/app/assets/javascripts/ide/components/jobs/detail/description.vue b/app/assets/javascripts/ide/components/jobs/detail/description.vue new file mode 100644 index 00000000000..7e24974f7e5 --- /dev/null +++ b/app/assets/javascripts/ide/components/jobs/detail/description.vue @@ -0,0 +1,47 @@ +<script> +import Icon from '../../../../vue_shared/components/icon.vue'; +import CiIcon from '../../../../vue_shared/components/ci_icon.vue'; + +export default { + components: { + Icon, + CiIcon, + }, + props: { + job: { + type: Object, + required: true, + }, + }, + computed: { + jobId() { + return `#${this.job.id}`; + }, + }, +}; +</script> + +<template> + <div class="d-flex align-items-center"> + <ci-icon + :status="job.status" + :borderless="true" + :size="24" + class="d-flex" + /> + <span class="prepend-left-8"> + {{ job.name }} + <a + :href="job.path" + target="_blank" + class="ide-external-link" + > + {{ jobId }} + <icon + :size="12" + name="external-link" + /> + </a> + </span> + </div> +</template> diff --git a/app/assets/javascripts/ide/components/jobs/detail/scroll_button.vue b/app/assets/javascripts/ide/components/jobs/detail/scroll_button.vue new file mode 100644 index 00000000000..103a407987f --- /dev/null +++ b/app/assets/javascripts/ide/components/jobs/detail/scroll_button.vue @@ -0,0 +1,66 @@ +<script> +import { __ } from '../../../../locale'; +import Icon from '../../../../vue_shared/components/icon.vue'; +import tooltip from '../../../../vue_shared/directives/tooltip'; + +const directions = { + up: 'up', + down: 'down', +}; + +export default { + directives: { + tooltip, + }, + components: { + Icon, + }, + props: { + direction: { + type: String, + required: true, + validator(value) { + return Object.keys(directions).includes(value); + }, + }, + disabled: { + type: Boolean, + required: true, + }, + }, + computed: { + tooltipTitle() { + return this.direction === directions.up ? __('Scroll to top') : __('Scroll to bottom'); + }, + iconName() { + return `scroll_${this.direction}`; + }, + }, + methods: { + clickedScroll() { + this.$emit('click'); + }, + }, +}; +</script> + +<template> + <div + v-tooltip + :title="tooltipTitle" + class="controllers-buttons" + data-container="body" + data-placement="top" + > + <button + :disabled="disabled" + class="btn-scroll btn-transparent btn-blank" + type="button" + @click="clickedScroll" + > + <icon + :name="iconName" + /> + </button> + </div> +</template> diff --git a/app/assets/javascripts/ide/components/jobs/item.vue b/app/assets/javascripts/ide/components/jobs/item.vue index c33936021d4..7f4695a0451 100644 --- a/app/assets/javascripts/ide/components/jobs/item.vue +++ b/app/assets/javascripts/ide/components/jobs/item.vue @@ -1,11 +1,9 @@ <script> -import Icon from '../../../vue_shared/components/icon.vue'; -import CiIcon from '../../../vue_shared/components/ci_icon.vue'; +import JobDescription from './detail/description.vue'; export default { components: { - Icon, - CiIcon, + JobDescription, }, props: { job: { @@ -18,29 +16,29 @@ export default { return `#${this.job.id}`; }, }, + methods: { + clickViewLog() { + this.$emit('clickViewLog', this.job); + }, + }, }; </script> <template> <div class="ide-job-item"> - <ci-icon - :status="job.status" - :borderless="true" - :size="24" + <job-description + :job="job" + class="append-right-default" /> - <span class="prepend-left-8"> - {{ job.name }} - <a - :href="job.path" - target="_blank" - class="ide-external-link" + <div class="ml-auto align-self-center"> + <button + v-if="job.started" + type="button" + class="btn btn-default btn-sm" + @click="clickViewLog" > - {{ jobId }} - <icon - name="external-link" - :size="12" - /> - </a> - </span> + {{ __('View log') }} + </button> + </div> </div> </template> diff --git a/app/assets/javascripts/ide/components/jobs/list.vue b/app/assets/javascripts/ide/components/jobs/list.vue index bdd0364c9b9..3b16b860ecd 100644 --- a/app/assets/javascripts/ide/components/jobs/list.vue +++ b/app/assets/javascripts/ide/components/jobs/list.vue @@ -19,7 +19,7 @@ export default { }, }, methods: { - ...mapActions('pipelines', ['fetchJobs', 'toggleStageCollapsed']), + ...mapActions('pipelines', ['fetchJobs', 'toggleStageCollapsed', 'setDetailJob']), }, }; </script> @@ -38,6 +38,7 @@ export default { :stage="stage" @fetch="fetchJobs" @toggleCollapsed="toggleStageCollapsed" + @clickViewLog="setDetailJob" /> </template> </div> diff --git a/app/assets/javascripts/ide/components/jobs/stage.vue b/app/assets/javascripts/ide/components/jobs/stage.vue index 5b24bb1f5a7..15e881b7bc8 100644 --- a/app/assets/javascripts/ide/components/jobs/stage.vue +++ b/app/assets/javascripts/ide/components/jobs/stage.vue @@ -48,6 +48,9 @@ export default { toggleCollapsed() { this.$emit('toggleCollapsed', this.stage.id); }, + clickViewLog(job) { + this.$emit('clickViewLog', job); + }, }, }; </script> @@ -57,10 +60,10 @@ export default { class="ide-stage card prepend-top-default" > <div - class="card-header" :class="{ 'border-bottom-0': stage.isCollapsed }" + class="card-header" @click="toggleCollapsed" > <ci-icon @@ -69,10 +72,10 @@ export default { /> <strong v-tooltip="showTooltip" + ref="stageTitle" :title="showTooltip ? stage.name : null" data-container="body" class="prepend-left-8 ide-stage-title" - ref="stageTitle" > {{ stage.name }} </strong> @@ -90,8 +93,8 @@ export default { /> </div> <div - class="card-body" v-show="!stage.isCollapsed" + class="card-body" > <loading-icon v-if="showLoadingIcon" @@ -101,6 +104,7 @@ export default { v-for="job in stage.jobs" :key="job.id" :job="job" + @clickViewLog="clickViewLog" /> </template> </div> diff --git a/app/assets/javascripts/ide/components/merge_requests/dropdown.vue b/app/assets/javascripts/ide/components/merge_requests/dropdown.vue new file mode 100644 index 00000000000..4b9824bf04b --- /dev/null +++ b/app/assets/javascripts/ide/components/merge_requests/dropdown.vue @@ -0,0 +1,63 @@ +<script> +import { mapGetters } from 'vuex'; +import Tabs from '../../../vue_shared/components/tabs/tabs'; +import Tab from '../../../vue_shared/components/tabs/tab.vue'; +import List from './list.vue'; + +export default { + components: { + Tabs, + Tab, + List, + }, + props: { + show: { + type: Boolean, + required: true, + }, + }, + computed: { + ...mapGetters('mergeRequests', ['assignedData', 'createdData']), + createdMergeRequestLength() { + return this.createdData.mergeRequests.length; + }, + assignedMergeRequestLength() { + return this.assignedData.mergeRequests.length; + }, + }, +}; +</script> + +<template> + <div class="dropdown-menu ide-merge-requests-dropdown p-0"> + <tabs + v-if="show" + stop-propagation + > + <tab active> + <template slot="title"> + {{ __('Created by me') }} + <span class="badge badge-pill"> + {{ createdMergeRequestLength }} + </span> + </template> + <list + :empty-text="__('You have not created any merge requests')" + type="created" + /> + </tab> + <tab> + <template slot="title"> + {{ __('Assigned to me') }} + <span class="badge badge-pill"> + {{ assignedMergeRequestLength }} + </span> + </template> + <list + :empty-text="__('You do not have any assigned merge requests')" + type="assigned" + /> + </tab> + </tabs> + </div> +</template> diff --git a/app/assets/javascripts/ide/components/merge_requests/item.vue b/app/assets/javascripts/ide/components/merge_requests/item.vue new file mode 100644 index 00000000000..4e18376bd48 --- /dev/null +++ b/app/assets/javascripts/ide/components/merge_requests/item.vue @@ -0,0 +1,63 @@ +<script> +import Icon from '../../../vue_shared/components/icon.vue'; + +export default { + components: { + Icon, + }, + props: { + item: { + type: Object, + required: true, + }, + currentId: { + type: String, + required: true, + }, + currentProjectId: { + type: String, + required: true, + }, + }, + computed: { + isActive() { + return ( + this.item.iid === parseInt(this.currentId, 10) && + this.currentProjectId === this.item.projectPathWithNamespace + ); + }, + pathWithID() { + return `${this.item.projectPathWithNamespace}!${this.item.iid}`; + }, + }, + methods: { + clickItem() { + this.$emit('click', this.item); + }, + }, +}; +</script> + +<template> + <button + type="button" + class="btn-link d-flex align-items-center" + @click="clickItem" + > + <span class="d-flex append-right-default ide-merge-request-current-icon"> + <icon + v-if="isActive" + :size="18" + name="mobile-issue-close" + /> + </span> + <span> + <strong> + {{ item.title }} + </strong> + <span class="ide-merge-request-project-path d-block mt-1"> + {{ pathWithID }} + </span> + </span> + </button> +</template> diff --git a/app/assets/javascripts/ide/components/merge_requests/list.vue b/app/assets/javascripts/ide/components/merge_requests/list.vue new file mode 100644 index 00000000000..19d3e48ee10 --- /dev/null +++ b/app/assets/javascripts/ide/components/merge_requests/list.vue @@ -0,0 +1,132 @@ +<script> +import { mapActions, mapGetters, mapState } from 'vuex'; +import _ from 'underscore'; +import LoadingIcon from '../../../vue_shared/components/loading_icon.vue'; +import Item from './item.vue'; + +export default { + components: { + LoadingIcon, + Item, + }, + props: { + type: { + type: String, + required: true, + }, + emptyText: { + type: String, + required: true, + }, + }, + data() { + return { + search: '', + }; + }, + computed: { + ...mapGetters('mergeRequests', ['getData']), + ...mapState(['currentMergeRequestId', 'currentProjectId']), + data() { + return this.getData(this.type); + }, + isLoading() { + return this.data.isLoading; + }, + mergeRequests() { + return this.data.mergeRequests; + }, + hasMergeRequests() { + return this.mergeRequests.length !== 0; + }, + hasNoSearchResults() { + return this.search !== '' && !this.hasMergeRequests; + }, + }, + watch: { + isLoading: { + handler: 'focusSearch', + }, + }, + mounted() { + this.loadMergeRequests(); + }, + methods: { + ...mapActions('mergeRequests', ['fetchMergeRequests', 'openMergeRequest']), + loadMergeRequests() { + this.fetchMergeRequests({ type: this.type, search: this.search }); + }, + viewMergeRequest(item) { + this.openMergeRequest({ + projectPath: item.projectPathWithNamespace, + id: item.iid, + }); + }, + searchMergeRequests: _.debounce(function debounceSearch() { + this.loadMergeRequests(); + }, 250), + focusSearch() { + if (!this.isLoading) { + this.$nextTick(() => { + this.$refs.searchInput.focus(); + }); + } + }, + }, +}; +</script> + +<template> + <div> + <div class="dropdown-input mt-3 pb-3 mb-0 border-bottom"> + <input + ref="searchInput" + :placeholder="__('Search merge requests')" + v-model="search" + type="search" + class="dropdown-input-field" + @input="searchMergeRequests" + /> + <i + aria-hidden="true" + class="fa fa-search dropdown-input-search" + ></i> + </div> + <div class="dropdown-content ide-merge-requests-dropdown-content d-flex"> + <loading-icon + v-if="isLoading" + class="mt-3 mb-3 align-self-center ml-auto mr-auto" + size="2" + /> + <ul + v-else + class="mb-3 w-100" + > + <template v-if="hasMergeRequests"> + <li + v-for="item in mergeRequests" + :key="item.id" + > + <item + :item="item" + :current-id="currentMergeRequestId" + :current-project-id="currentProjectId" + @click="viewMergeRequest" + /> + </li> + </template> + <li + v-else + class="ide-merge-requests-empty d-flex align-items-center justify-content-center" + > + <template v-if="hasNoSearchResults"> + {{ __('No merge requests found') }} + </template> + <template v-else> + {{ emptyText }} + </template> + </li> + </ul> + </div> + </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 179a589d1ac..821be319cce 100644 --- a/app/assets/javascripts/ide/components/mr_file_icon.vue +++ b/app/assets/javascripts/ide/components/mr_file_icon.vue @@ -14,10 +14,10 @@ export default { <template> <icon - name="git-merge" v-tooltip :title="__('Part of merge request changes')" - css-classes="append-right-8" :size="12" + name="git-merge" + css-classes="append-right-8" /> </template> diff --git a/app/assets/javascripts/ide/components/new_dropdown/index.vue b/app/assets/javascripts/ide/components/new_dropdown/index.vue index f0b29702497..1e398d7e1aa 100644 --- a/app/assets/javascripts/ide/components/new_dropdown/index.vue +++ b/app/assets/javascripts/ide/components/new_dropdown/index.vue @@ -55,10 +55,10 @@ export default { <template> <div class="ide-new-btn"> <div - class="dropdown" :class="{ show: dropdownOpen, }" + class="dropdown" > <button type="button" @@ -67,19 +67,19 @@ export default { @click.stop="openDropdown()" > <icon - name="plus" :size="12" + name="plus" css-classes="float-left" /> <icon - name="arrow-down" :size="12" + name="arrow-down" css-classes="float-left" /> </button> <ul - class="dropdown-menu dropdown-menu-right" ref="dropdownMenu" + class="dropdown-menu dropdown-menu-right" > <li> <a diff --git a/app/assets/javascripts/ide/components/new_dropdown/modal.vue b/app/assets/javascripts/ide/components/new_dropdown/modal.vue index dd2800179ff..1e9668d5154 100644 --- a/app/assets/javascripts/ide/components/new_dropdown/modal.vue +++ b/app/assets/javascripts/ide/components/new_dropdown/modal.vue @@ -71,18 +71,18 @@ export default { > <form slot="body" - @submit.prevent="createEntryInStore" class="form-group row" + @submit.prevent="createEntryInStore" > <label class="label-light col-form-label col-sm-3"> {{ __('Name') }} </label> <div class="col-sm-9"> <input + ref="fieldName" + v-model="entryName" type="text" class="form-control" - v-model="entryName" - ref="fieldName" /> </div> </form> diff --git a/app/assets/javascripts/ide/components/new_dropdown/upload.vue b/app/assets/javascripts/ide/components/new_dropdown/upload.vue index c165af5ce52..1814924be39 100644 --- a/app/assets/javascripts/ide/components/new_dropdown/upload.vue +++ b/app/assets/javascripts/ide/components/new_dropdown/upload.vue @@ -67,9 +67,9 @@ </a> <input id="file-upload" + ref="fileUpload" type="file" class="hidden" - ref="fileUpload" /> </div> </template> diff --git a/app/assets/javascripts/ide/components/panes/right.vue b/app/assets/javascripts/ide/components/panes/right.vue index 703c4a70cfa..dedc2988618 100644 --- a/app/assets/javascripts/ide/components/panes/right.vue +++ b/app/assets/javascripts/ide/components/panes/right.vue @@ -4,6 +4,8 @@ import tooltip from '../../../vue_shared/directives/tooltip'; import Icon from '../../../vue_shared/components/icon.vue'; import { rightSidebarViews } from '../../constants'; import PipelinesList from '../pipelines/list.vue'; +import JobsDetail from '../jobs/detail.vue'; +import ResizablePanel from '../resizable_panel.vue'; export default { directives: { @@ -12,9 +14,17 @@ export default { components: { Icon, PipelinesList, + JobsDetail, + ResizablePanel, }, computed: { ...mapState(['rightPane']), + pipelinesActive() { + return ( + this.rightPane === rightSidebarViews.pipelines || + this.rightPane === rightSidebarViews.jobsDetail + ); + }, }, methods: { ...mapActions(['setRightPane']), @@ -32,24 +42,28 @@ export default { <div class="multi-file-commit-panel ide-right-sidebar" > - <div - class="multi-file-commit-panel-inner" + <resizable-panel v-if="rightPane" + :collapsible="false" + :initial-width="350" + :min-size="350" + class="multi-file-commit-panel-inner" + side="right" > <component :is="rightPane" /> - </div> + </resizable-panel> <nav class="ide-activity-bar"> <ul class="list-unstyled"> <li> <button v-tooltip - data-container="body" - data-placement="left" :title="__('Pipelines')" - class="ide-sidebar-link is-right" :class="{ - active: rightPane === $options.rightSidebarViews.pipelines + active: pipelinesActive }" + data-container="body" + data-placement="left" + class="ide-sidebar-link is-right" type="button" @click="clickTab($event, $options.rightSidebarViews.pipelines)" > diff --git a/app/assets/javascripts/ide/components/pipelines/list.vue b/app/assets/javascripts/ide/components/pipelines/list.vue index 06455fac439..5757dfdc925 100644 --- a/app/assets/javascripts/ide/components/pipelines/list.vue +++ b/app/assets/javascripts/ide/components/pipelines/list.vue @@ -75,8 +75,8 @@ export default { > #{{ latestPipeline.id }} <icon - name="external-link" :size="12" + name="external-link" /> </a> </span> @@ -94,7 +94,7 @@ export default { <p class="append-bottom-0"> {{ __('Found errors in your .gitlab-ci.yml:') }} </p> - <p class="append-bottom-0"> + <p class="append-bottom-0 break-word"> {{ latestPipeline.yamlError }} </p> <p diff --git a/app/assets/javascripts/ide/components/repo_commit_section.vue b/app/assets/javascripts/ide/components/repo_commit_section.vue index c5092d8e04d..c2c678ff0be 100644 --- a/app/assets/javascripts/ide/components/repo_commit_section.vue +++ b/app/assets/javascripts/ide/components/repo_commit_section.vue @@ -6,7 +6,7 @@ import DeprecatedModal from '~/vue_shared/components/deprecated_modal.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 { activityBarViews } from '../constants'; +import { activityBarViews, stageKeys } from '../constants'; export default { components: { @@ -27,11 +27,14 @@ export default { 'unusedSeal', ]), ...mapState('commit', ['commitMessage', 'submitCommitLoading']), - ...mapGetters(['lastOpenedFile', 'hasChanges', 'someUncommitedChanges']), + ...mapGetters(['lastOpenedFile', 'hasChanges', 'someUncommitedChanges', 'activeFile']), ...mapGetters('commit', ['commitButtonDisabled', 'discardDraftButtonDisabled']), showStageUnstageArea() { return !!(this.someUncommitedChanges || this.lastCommitMsg || !this.unusedSeal); }, + activeFileKey() { + return this.activeFile ? this.activeFile.key : null; + }, }, watch: { hasChanges() { @@ -44,6 +47,7 @@ export default { if (this.lastOpenedFile) { this.openPendingTab({ file: this.lastOpenedFile, + keyPrefix: this.lastOpenedFile.changed ? stageKeys.unstaged : stageKeys.staged, }) .then(changeViewer => { if (changeViewer) { @@ -62,6 +66,7 @@ export default { return this.updateCommitAction(consts.COMMIT_TO_NEW_BRANCH).then(() => this.commitChanges()); }, }, + stageKeys, }; </script> @@ -72,8 +77,8 @@ export default { <deprecated-modal id="ide-create-branch-modal" :primary-button-label="__('Create new branch')" - kind="success" :title="__('Branch has changed')" + kind="success" @submit="forceCreateNewBranch" > <template slot="body"> @@ -85,22 +90,28 @@ export default { v-if="showStageUnstageArea" > <commit-files-list - class="is-first" - icon-name="unstaged" :title="__('Unstaged')" + :key-prefix="$options.stageKeys.unstaged" :file-list="changedFiles" + :action-btn-text="__('Stage all changes')" + :active-file-key="activeFileKey" action="stageAllChanges" - :action-btn-text="__('Stage all')" + action-btn-icon="mobile-issue-close" item-action-component="stage-button" + class="is-first" + icon-name="unstaged" /> <commit-files-list - icon-name="staged" :title="__('Staged')" + :key-prefix="$options.stageKeys.staged" :file-list="stagedFiles" + :action-btn-text="__('Unstage all changes')" + :staged-list="true" + :active-file-key="activeFileKey" action="unstageAllChanges" - :action-btn-text="__('Unstage all')" + action-btn-icon="history" item-action-component="unstage-button" - :staged-list="true" + icon-name="staged" /> </template> <empty-state diff --git a/app/assets/javascripts/ide/components/repo_editor.vue b/app/assets/javascripts/ide/components/repo_editor.vue index 93453989c08..08ee12fd98f 100644 --- a/app/assets/javascripts/ide/components/repo_editor.vue +++ b/app/assets/javascripts/ide/components/repo_editor.vue @@ -1,16 +1,16 @@ <script> -/* global monaco */ import { mapState, mapGetters, mapActions } from 'vuex'; import flash from '~/flash'; import ContentViewer from '~/vue_shared/components/content_viewer/content_viewer.vue'; +import DiffViewer from '~/vue_shared/components/diff_viewer/diff_viewer.vue'; import { activityBarViews, viewerTypes } from '../constants'; -import monacoLoader from '../monaco_loader'; import Editor from '../lib/editor'; import ExternalLink from './external_link.vue'; export default { components: { ContentViewer, + DiffViewer, ExternalLink, }, props: { @@ -20,7 +20,13 @@ export default { }, }, computed: { - ...mapState(['rightPanelCollapsed', 'viewer', 'panelResizing', 'currentActivityView']), + ...mapState([ + 'rightPanelCollapsed', + 'viewer', + 'panelResizing', + 'currentActivityView', + 'rightPane', + ]), ...mapGetters([ 'currentMergeRequest', 'getStagedFile', @@ -31,9 +37,18 @@ export default { shouldHideEditor() { return this.file && this.file.binary && !this.file.content; }, + showContentViewer() { + return ( + (this.shouldHideEditor || this.file.viewMode === 'preview') && + (this.viewer !== viewerTypes.mr || !this.file.mrChange) + ); + }, + showDiffViewer() { + return this.shouldHideEditor && this.file.mrChange && this.viewer === viewerTypes.mr; + }, editTabCSS() { return { - active: this.file.viewMode === 'edit', + active: this.file.viewMode === 'editor', }; }, previewTabCSS() { @@ -50,12 +65,12 @@ export default { // Compare key to allow for files opened in review mode to be cached differently if (oldVal.key !== this.file.key) { - this.initMonaco(); + this.initEditor(); if (this.currentActivityView !== activityBarViews.edit) { this.setFileViewMode({ file: this.file, - viewMode: 'edit', + viewMode: 'editor', }); } } @@ -64,7 +79,7 @@ export default { if (this.currentActivityView !== activityBarViews.edit) { this.setFileViewMode({ file: this.file, - viewMode: 'edit', + viewMode: 'editor', }); } }, @@ -79,20 +94,18 @@ export default { this.editor.updateDimensions(); } }, + rightPane() { + this.editor.updateDimensions(); + }, }, beforeDestroy() { this.editor.dispose(); }, mounted() { - if (this.editor && monaco) { - this.initMonaco(); - } else { - monacoLoader(['vs/editor/editor.main'], () => { - this.editor = Editor.create(monaco); - - this.initMonaco(); - }); + if (!this.editor) { + this.editor = Editor.create(); } + this.initEditor(); }, methods: { ...mapActions([ @@ -105,7 +118,7 @@ export default { 'updateViewer', 'removePendingTab', ]), - initMonaco() { + initEditor() { if (this.shouldHideEditor) return; this.editor.clearEditor(); @@ -118,7 +131,7 @@ export default { this.createEditorInstance(); }) .catch(err => { - flash('Error setting up monaco. Please try again.', 'alert', document, null, false, true); + flash('Error setting up editor. Please try again.', 'alert', document, null, false, true); throw err; }); }, @@ -197,14 +210,14 @@ export default { > <div class="ide-mode-tabs clearfix" > <ul - class="nav-links float-left" v-if="!shouldHideEditor && isEditModeActive" + class="nav-links float-left" > <li :class="editTabCSS"> <a href="javascript:void(0);" role="button" - @click.prevent="setFileViewMode({ file, viewMode: 'edit' })"> + @click.prevent="setFileViewMode({ file, viewMode: 'editor' })"> <template v-if="viewer === $options.viewerTypes.edit"> {{ __('Edit') }} </template> @@ -229,19 +242,27 @@ export default { /> </div> <div - v-show="!shouldHideEditor && file.viewMode === 'edit'" + v-show="!shouldHideEditor && file.viewMode ==='editor'" ref="editor" - class="multi-file-editor-holder" :class="{ 'is-readonly': isCommitModeActive, }" + class="multi-file-editor-holder" > </div> <content-viewer - v-if="shouldHideEditor || file.viewMode === 'preview'" + v-if="showContentViewer" :content="file.content || file.raw" :path="file.rawPath || file.path" :file-size="file.size" :project-path="file.projectId"/> + <diff-viewer + v-if="showDiffViewer" + :diff-mode="file.mrChange.diffMode" + :new-path="file.mrChange.new_path" + :new-sha="currentMergeRequest.sha" + :old-path="file.mrChange.old_path" + :old-sha="currentMergeRequest.baseCommitSha" + :project-path="file.projectId"/> </div> </template> diff --git a/app/assets/javascripts/ide/components/repo_file.vue b/app/assets/javascripts/ide/components/repo_file.vue index f56aeced806..c34547fcc60 100644 --- a/app/assets/javascripts/ide/components/repo_file.vue +++ b/app/assets/javascripts/ide/components/repo_file.vue @@ -120,17 +120,17 @@ export default { <template> <div> <div - class="file" :class="fileClass" - @click="clickFile" + class="file" role="button" + @click="clickFile" > <div class="file-name" > <span - class="ide-file-name str-truncated" :style="levelIndentation" + class="ide-file-name str-truncated" > <file-icon :file-name="file.name" @@ -156,10 +156,10 @@ export default { <icon v-tooltip :title="folderChangesTooltip" + :size="12" data-container="body" data-placement="right" name="file-modified" - :size="12" css-classes="prepend-left-5 multi-file-modified" /> </span> diff --git a/app/assets/javascripts/ide/components/repo_file_status_icon.vue b/app/assets/javascripts/ide/components/repo_file_status_icon.vue index 97589e116c5..76a3333be50 100644 --- a/app/assets/javascripts/ide/components/repo_file_status_icon.vue +++ b/app/assets/javascripts/ide/components/repo_file_status_icon.vue @@ -26,8 +26,8 @@ export default { <template> <span - v-if="file.file_lock" v-tooltip + v-if="file.file_lock" :title="lockTooltip" data-container="body" > diff --git a/app/assets/javascripts/ide/components/repo_loading_file.vue b/app/assets/javascripts/ide/components/repo_loading_file.vue index 3e47da88050..7a5ede82253 100644 --- a/app/assets/javascripts/ide/components/repo_loading_file.vue +++ b/app/assets/javascripts/ide/components/repo_loading_file.vue @@ -33,8 +33,8 @@ <td class="d-none d-sm-block"> <skeleton-loading-container - class="animation-container-right" :small="true" + class="animation-container-right" /> </td> </template> diff --git a/app/assets/javascripts/ide/components/repo_tab.vue b/app/assets/javascripts/ide/components/repo_tab.vue index fb26b973236..1ad52c1bd83 100644 --- a/app/assets/javascripts/ide/components/repo_tab.vue +++ b/app/assets/javascripts/ide/components/repo_tab.vue @@ -76,8 +76,8 @@ export default { @mouseout="mouseOutTab" > <div - class="multi-file-tab" :title="tab.url" + class="multi-file-tab" > <file-icon :file-name="tab.name" @@ -89,16 +89,16 @@ export default { /> </div> <button + :aria-label="closeLabel" + :disabled="tab.pending" type="button" class="multi-file-tab-close" @click.stop.prevent="closeFile(tab)" - :aria-label="closeLabel" - :disabled="tab.pending" > <icon v-if="!showChangedIcon" - name="close" :size="12" + name="close" /> <changed-file-icon v-else diff --git a/app/assets/javascripts/ide/components/repo_tabs.vue b/app/assets/javascripts/ide/components/repo_tabs.vue index 99e51097e12..c12a63e26be 100644 --- a/app/assets/javascripts/ide/components/repo_tabs.vue +++ b/app/assets/javascripts/ide/components/repo_tabs.vue @@ -52,8 +52,8 @@ export default { <template> <div class="multi-file-tabs"> <ul - class="list-unstyled append-bottom-0" ref="tabsScroller" + class="list-unstyled append-bottom-0" > <repo-tab v-for="tab in files" diff --git a/app/assets/javascripts/ide/components/resizable_panel.vue b/app/assets/javascripts/ide/components/resizable_panel.vue index 5ea2a2f6825..7277fcb7617 100644 --- a/app/assets/javascripts/ide/components/resizable_panel.vue +++ b/app/assets/javascripts/ide/components/resizable_panel.vue @@ -63,11 +63,11 @@ export default { <template> <div - class="multi-file-commit-panel" :class="{ 'is-collapsed': collapsed && collapsible, }" :style="panelStyle" + class="multi-file-commit-panel" @click="toggleFullbarCollapsed" > <slot></slot> @@ -77,9 +77,9 @@ export default { :start-size="initialWidth" :min-size="minSize" :max-size="$options.maxSize" + :side="side === 'right' ? 'left' : 'right'" @resize-start="setResizingStatus(true)" @resize-end="setResizingStatus(false)" - :side="side === 'right' ? 'left' : 'right'" /> </div> </template> diff --git a/app/assets/javascripts/ide/constants.js b/app/assets/javascripts/ide/constants.js index 33cd20caf52..12e0c3aeef0 100644 --- a/app/assets/javascripts/ide/constants.js +++ b/app/assets/javascripts/ide/constants.js @@ -21,6 +21,19 @@ export const viewerTypes = { diff: 'diff', }; +export const diffModes = { + replaced: 'replaced', + new: 'new', + deleted: 'deleted', + renamed: 'renamed', +}; + export const rightSidebarViews = { pipelines: 'pipelines-list', + jobsDetail: 'jobs-detail', +}; + +export const stageKeys = { + unstaged: 'unstaged', + staged: 'staged', }; diff --git a/app/assets/javascripts/ide/lib/common/model.js b/app/assets/javascripts/ide/lib/common/model.js index e5149b1f3ad..78e6f632728 100644 --- a/app/assets/javascripts/ide/lib/common/model.js +++ b/app/assets/javascripts/ide/lib/common/model.js @@ -1,32 +1,32 @@ +import { editor as monacoEditor, Uri } from 'monaco-editor'; import Disposable from './disposable'; import eventHub from '../../eventhub'; export default class Model { - constructor(monaco, file, head = null) { - this.monaco = monaco; + constructor(file, head = null) { 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.originalModel = monacoEditor.createModel( head ? head.content : this.file.raw, undefined, - new this.monaco.Uri(null, null, `original/${this.path}`), + new Uri(false, false, `original/${this.path}`), )), - (this.model = this.monaco.editor.createModel( + (this.model = monacoEditor.createModel( this.content, undefined, - new this.monaco.Uri(null, null, this.path), + new Uri(false, false, this.path), )), ); if (this.file.mrChange) { this.disposable.add( - (this.baseModel = this.monaco.editor.createModel( + (this.baseModel = monacoEditor.createModel( this.file.baseRaw, undefined, - new this.monaco.Uri(null, null, `target/${this.path}`), + new Uri(false, false, `target/${this.path}`), )), ); } diff --git a/app/assets/javascripts/ide/lib/common/model_manager.js b/app/assets/javascripts/ide/lib/common/model_manager.js index 7f643969480..bd9b8fc3fcc 100644 --- a/app/assets/javascripts/ide/lib/common/model_manager.js +++ b/app/assets/javascripts/ide/lib/common/model_manager.js @@ -3,8 +3,7 @@ import Disposable from './disposable'; import Model from './model'; export default class ModelManager { - constructor(monaco) { - this.monaco = monaco; + constructor() { this.disposable = new Disposable(); this.models = new Map(); } @@ -22,7 +21,7 @@ export default class ModelManager { return this.getModel(file.key); } - const model = new Model(this.monaco, file, head); + const model = new Model(file, head); this.models.set(model.path, model); this.disposable.add(model); diff --git a/app/assets/javascripts/ide/lib/diff/controller.js b/app/assets/javascripts/ide/lib/diff/controller.js index f579424cf33..046e562ba2b 100644 --- a/app/assets/javascripts/ide/lib/diff/controller.js +++ b/app/assets/javascripts/ide/lib/diff/controller.js @@ -1,4 +1,4 @@ -/* global monaco */ +import { Range } from 'monaco-editor'; import { throttle } from 'underscore'; import DirtyDiffWorker from './diff_worker'; import Disposable from '../common/disposable'; @@ -16,7 +16,7 @@ export const getDiffChangeType = change => { }; export const getDecorator = change => ({ - range: new monaco.Range(change.lineNumber, 1, change.endLineNumber, 1), + range: new Range(change.lineNumber, 1, change.endLineNumber, 1), options: { isWholeLine: true, linesDecorationsClassName: `dirty-diff dirty-diff-${getDiffChangeType(change)}`, diff --git a/app/assets/javascripts/ide/lib/diff/diff_worker.js b/app/assets/javascripts/ide/lib/diff/diff_worker.js index e74c4046330..f09930e8158 100644 --- a/app/assets/javascripts/ide/lib/diff/diff_worker.js +++ b/app/assets/javascripts/ide/lib/diff/diff_worker.js @@ -1,8 +1,10 @@ import { computeDiff } from './diff'; +// eslint-disable-next-line no-restricted-globals self.addEventListener('message', (e) => { const data = e.data; + // eslint-disable-next-line no-restricted-globals self.postMessage({ path: data.path, changes: computeDiff(data.originalContent, data.newContent), diff --git a/app/assets/javascripts/ide/lib/editor.js b/app/assets/javascripts/ide/lib/editor.js index 9c3bb9cc17d..02038fcb534 100644 --- a/app/assets/javascripts/ide/lib/editor.js +++ b/app/assets/javascripts/ide/lib/editor.js @@ -1,4 +1,5 @@ import _ from 'underscore'; +import { editor as monacoEditor, KeyCode, KeyMod } from 'monaco-editor'; import store from '../stores'; import DecorationsController from './decorations/controller'; import DirtyDiffController from './diff/controller'; @@ -8,6 +9,11 @@ import editorOptions, { defaultEditorOptions } from './editor_options'; import gitlabTheme from './themes/gl_theme'; import keymap from './keymap.json'; +function setupMonacoTheme() { + monacoEditor.defineTheme(gitlabTheme.themeName, gitlabTheme.monacoTheme); + monacoEditor.setTheme('gitlab'); +} + export const clearDomElement = el => { if (!el || !el.firstChild) return; @@ -17,24 +23,22 @@ export const clearDomElement = el => { }; export default class Editor { - static create(monaco) { - if (this.editorInstance) return this.editorInstance; - - this.editorInstance = new Editor(monaco); - + static create() { + if (!this.editorInstance) { + this.editorInstance = new Editor(); + } return this.editorInstance; } - constructor(monaco) { - this.monaco = monaco; + constructor() { this.currentModel = null; this.instance = null; this.dirtyDiffController = null; this.disposable = new Disposable(); - this.modelManager = new ModelManager(this.monaco); + this.modelManager = new ModelManager(); this.decorationsController = new DecorationsController(this); - this.setupMonacoTheme(); + setupMonacoTheme(); this.debouncedUpdate = _.debounce(() => { this.updateDimensions(); @@ -46,7 +50,7 @@ export default class Editor { clearDomElement(domElement); this.disposable.add( - (this.instance = this.monaco.editor.create(domElement, { + (this.instance = monacoEditor.create(domElement, { ...defaultEditorOptions, })), (this.dirtyDiffController = new DirtyDiffController( @@ -66,7 +70,7 @@ export default class Editor { clearDomElement(domElement); this.disposable.add( - (this.instance = this.monaco.editor.createDiffEditor(domElement, { + (this.instance = monacoEditor.createDiffEditor(domElement, { ...defaultEditorOptions, quickSuggestions: false, occurrencesHighlight: false, @@ -122,17 +126,11 @@ export default class Editor { modified: model.getModel(), }); - this.monaco.editor.createDiffNavigator(this.instance, { + monacoEditor.createDiffNavigator(this.instance, { alwaysRevealFirst: true, }); } - setupMonacoTheme() { - this.monaco.editor.defineTheme(gitlabTheme.themeName, gitlabTheme.monacoTheme); - - this.monaco.editor.setTheme('gitlab'); - } - clearEditor() { if (this.instance) { this.instance.setModel(null); @@ -200,7 +198,7 @@ export default class Editor { const getKeyCode = key => { const monacoKeyMod = key.indexOf('KEY_') === 0; - return monacoKeyMod ? this.monaco.KeyCode[key] : this.monaco.KeyMod[key]; + return monacoKeyMod ? KeyCode[key] : KeyMod[key]; }; keymap.forEach(command => { diff --git a/app/assets/javascripts/ide/lib/editor_options.js b/app/assets/javascripts/ide/lib/editor_options.js index 9f895d49f2e..e35595ab1fd 100644 --- a/app/assets/javascripts/ide/lib/editor_options.js +++ b/app/assets/javascripts/ide/lib/editor_options.js @@ -12,5 +12,6 @@ export const defaultEditorOptions = { export default [ { readOnly: model => !!model.file.file_lock, + quickSuggestions: model => !(model.language === 'markdown'), }, ]; diff --git a/app/assets/javascripts/ide/monaco_loader.js b/app/assets/javascripts/ide/monaco_loader.js deleted file mode 100644 index 142a220097b..00000000000 --- a/app/assets/javascripts/ide/monaco_loader.js +++ /dev/null @@ -1,16 +0,0 @@ -import monacoContext from 'monaco-editor/dev/vs/loader'; - -monacoContext.require.config({ - paths: { - vs: `${__webpack_public_path__}monaco-editor/vs`, // eslint-disable-line camelcase - }, -}); - -// ignore CDN config and use local assets path for service worker which cannot be cross-domain -const relativeRootPath = (gon && gon.relative_url_root) || ''; -const monacoPath = `${relativeRootPath}/assets/webpack/monaco-editor/vs`; -window.MonacoEnvironment = { getWorkerUrl: () => `${monacoPath}/base/worker/workerMain.js` }; - -// eslint-disable-next-line no-underscore-dangle -window.__monaco_context__ = monacoContext; -export default monacoContext.require; diff --git a/app/assets/javascripts/ide/services/index.js b/app/assets/javascripts/ide/services/index.js index e8b51f2b516..da9de25302a 100644 --- a/app/assets/javascripts/ide/services/index.js +++ b/app/assets/javascripts/ide/services/index.js @@ -9,7 +9,7 @@ export default { return Vue.http.get(endpoint, { params: { format: 'json' } }); }, getFileData(endpoint) { - return Vue.http.get(endpoint, { params: { format: 'json' } }); + return Vue.http.get(endpoint, { params: { format: 'json', viewer: 'none' } }); }, getRawFileData(file) { if (file.tempFile) { diff --git a/app/assets/javascripts/ide/stores/actions/merge_request.js b/app/assets/javascripts/ide/stores/actions/merge_request.js index 5ec9bd661bb..edb20ff96fc 100644 --- a/app/assets/javascripts/ide/stores/actions/merge_request.js +++ b/app/assets/javascripts/ide/stores/actions/merge_request.js @@ -17,9 +17,7 @@ export const getMergeRequestData = ( mergeRequestId, mergeRequest: data, }); - if (!state.currentMergeRequestId) { - commit(types.SET_CURRENT_MERGE_REQUEST, mergeRequestId); - } + commit(types.SET_CURRENT_MERGE_REQUEST, mergeRequestId); resolve(data); }) .catch(() => { diff --git a/app/assets/javascripts/ide/stores/actions/project.js b/app/assets/javascripts/ide/stores/actions/project.js index 46af47d2f81..0b99bce4a8e 100644 --- a/app/assets/javascripts/ide/stores/actions/project.js +++ b/app/assets/javascripts/ide/stores/actions/project.js @@ -13,8 +13,7 @@ export const getProjectData = ({ commit, state }, { namespace, projectId, force .then(data => { commit(types.TOGGLE_LOADING, { entry: state }); commit(types.SET_PROJECT, { projectPath: `${namespace}/${projectId}`, project: data }); - if (!state.currentProjectId) - commit(types.SET_CURRENT_PROJECT, `${namespace}/${projectId}`); + commit(types.SET_CURRENT_PROJECT, `${namespace}/${projectId}`); resolve(data); }) .catch(() => { diff --git a/app/assets/javascripts/ide/stores/modules/commit/actions.js b/app/assets/javascripts/ide/stores/modules/commit/actions.js index 0a0db4033c8..7219abc4185 100644 --- a/app/assets/javascripts/ide/stores/modules/commit/actions.js +++ b/app/assets/javascripts/ide/stores/modules/commit/actions.js @@ -49,31 +49,6 @@ export const setLastCommitMessage = ({ rootState, commit }, data) => { commit(rootTypes.SET_LAST_COMMIT_MSG, commitMsg, { root: true }); }; -export const checkCommitStatus = ({ rootState }) => - service - .getBranchData(rootState.currentProjectId, rootState.currentBranchId) - .then(({ data }) => { - const { id } = data.commit; - const selectedBranch = - rootState.projects[rootState.currentProjectId].branches[rootState.currentBranchId]; - - if (selectedBranch.workingReference !== id) { - return true; - } - - return false; - }) - .catch(() => - flash( - __('Error checking branch data. Please try again.'), - 'alert', - document, - null, - false, - true, - ), - ); - export const updateFilesAfterCommit = ({ commit, dispatch, rootState }, { data }) => { const selectedProject = rootState.projects[rootState.currentProjectId]; const lastCommit = { @@ -128,24 +103,17 @@ export const updateFilesAfterCommit = ({ commit, dispatch, rootState }, { data } 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'); + const payload = createCommitPayload({ + branch: getters.branchName, + newBranch, + state, + rootState, + }); commit(types.UPDATE_LOADING, true); - return getCommitStatus - .then( - branchChanged => - new Promise(resolve => { - if (branchChanged) { - // show the modal with a Bootstrap call - $('#ide-create-branch-modal').modal('show'); - } else { - resolve(); - } - }), - ) - .then(() => service.commit(rootState.currentProjectId, payload)) + return service + .commit(rootState.currentProjectId, payload) .then(({ data }) => { commit(types.UPDATE_LOADING, false); @@ -220,12 +188,16 @@ export const commitChanges = ({ commit, state, getters, dispatch, rootState, roo ); }) .catch(err => { - let errMsg = __('Error committing changes. Please try again.'); - if (err.response.data && err.response.data.message) { - errMsg += ` (${stripHtml(err.response.data.message)})`; + if (err.response.status === 400) { + $('#ide-create-branch-modal').modal('show'); + } else { + let errMsg = __('Error committing changes. Please try again.'); + if (err.response.data && err.response.data.message) { + errMsg += ` (${stripHtml(err.response.data.message)})`; + } + flash(errMsg, 'alert', document, null, false, true); + window.dispatchEvent(new Event('resize')); } - flash(errMsg, 'alert', document, null, false, true); - window.dispatchEvent(new Event('resize')); commit(types.UPDATE_LOADING, false); }); diff --git a/app/assets/javascripts/ide/stores/modules/merge_requests/actions.js b/app/assets/javascripts/ide/stores/modules/merge_requests/actions.js index d3050183bd3..551dd322c9b 100644 --- a/app/assets/javascripts/ide/stores/modules/merge_requests/actions.js +++ b/app/assets/javascripts/ide/stores/modules/merge_requests/actions.js @@ -1,25 +1,48 @@ import { __ } from '../../../../locale'; import Api from '../../../../api'; import flash from '../../../../flash'; +import router from '../../../ide_router'; +import { scopes } from './constants'; import * as types from './mutation_types'; +import * as rootTypes from '../../mutation_types'; -export const requestMergeRequests = ({ commit }) => commit(types.REQUEST_MERGE_REQUESTS); -export const receiveMergeRequestsError = ({ commit }) => { +export const requestMergeRequests = ({ commit }, type) => + commit(types.REQUEST_MERGE_REQUESTS, type); +export const receiveMergeRequestsError = ({ commit }, type) => { flash(__('Error loading merge requests.')); - commit(types.RECEIVE_MERGE_REQUESTS_ERROR); + commit(types.RECEIVE_MERGE_REQUESTS_ERROR, type); }; -export const receiveMergeRequestsSuccess = ({ commit }, data) => - commit(types.RECEIVE_MERGE_REQUESTS_SUCCESS, data); +export const receiveMergeRequestsSuccess = ({ commit }, { type, data }) => + commit(types.RECEIVE_MERGE_REQUESTS_SUCCESS, { type, data }); -export const fetchMergeRequests = ({ dispatch, state: { scope, state } }, search = '') => { - dispatch('requestMergeRequests'); - dispatch('resetMergeRequests'); +export const fetchMergeRequests = ({ dispatch, state: { state } }, { type, search = '' }) => { + const scope = scopes[type]; + dispatch('requestMergeRequests', type); + dispatch('resetMergeRequests', type); Api.mergeRequests({ scope, state, search }) - .then(({ data }) => dispatch('receiveMergeRequestsSuccess', data)) - .catch(() => dispatch('receiveMergeRequestsError')); + .then(({ data }) => dispatch('receiveMergeRequestsSuccess', { type, data })) + .catch(() => dispatch('receiveMergeRequestsError', type)); }; -export const resetMergeRequests = ({ commit }) => commit(types.RESET_MERGE_REQUESTS); +export const resetMergeRequests = ({ commit }, type) => commit(types.RESET_MERGE_REQUESTS, type); + +export const openMergeRequest = ({ commit, dispatch }, { projectPath, id }) => { + commit(rootTypes.CLEAR_PROJECTS, null, { root: true }); + commit(rootTypes.SET_CURRENT_MERGE_REQUEST, `${id}`, { root: true }); + commit(rootTypes.RESET_OPEN_FILES, null, { root: true }); + dispatch('setCurrentBranchId', '', { root: true }); + dispatch('pipelines/stopPipelinePolling', null, { root: true }) + .then(() => { + dispatch('pipelines/resetLatestPipeline', null, { root: true }); + dispatch('pipelines/clearEtagPoll', null, { root: true }); + }) + .catch(e => { + throw e; + }); + dispatch('setRightPane', null, { root: true }); + + router.push(`/project/${projectPath}/merge_requests/${id}`); +}; export default () => {}; diff --git a/app/assets/javascripts/ide/stores/modules/merge_requests/constants.js b/app/assets/javascripts/ide/stores/modules/merge_requests/constants.js index 64b7763f257..a7085c7d04c 100644 --- a/app/assets/javascripts/ide/stores/modules/merge_requests/constants.js +++ b/app/assets/javascripts/ide/stores/modules/merge_requests/constants.js @@ -1,6 +1,6 @@ export const scopes = { - assignedToMe: 'assigned-to-me', - createdByMe: 'created-by-me', + assigned: 'assigned-to-me', + created: 'created-by-me', }; export const states = { diff --git a/app/assets/javascripts/ide/stores/modules/merge_requests/getters.js b/app/assets/javascripts/ide/stores/modules/merge_requests/getters.js new file mode 100644 index 00000000000..8e2b234be8d --- /dev/null +++ b/app/assets/javascripts/ide/stores/modules/merge_requests/getters.js @@ -0,0 +1,4 @@ +export const getData = state => type => state[type]; + +export const assignedData = state => state.assigned; +export const createdData = state => state.created; diff --git a/app/assets/javascripts/ide/stores/modules/merge_requests/index.js b/app/assets/javascripts/ide/stores/modules/merge_requests/index.js index 04e7e0f08f1..2e6dfb420f4 100644 --- a/app/assets/javascripts/ide/stores/modules/merge_requests/index.js +++ b/app/assets/javascripts/ide/stores/modules/merge_requests/index.js @@ -1,5 +1,6 @@ import state from './state'; import * as actions from './actions'; +import * as getters from './getters'; import mutations from './mutations'; export default { @@ -7,4 +8,5 @@ export default { state: state(), actions, mutations, + getters, }; diff --git a/app/assets/javascripts/ide/stores/modules/merge_requests/mutations.js b/app/assets/javascripts/ide/stores/modules/merge_requests/mutations.js index 98102a68e08..971da0806bd 100644 --- a/app/assets/javascripts/ide/stores/modules/merge_requests/mutations.js +++ b/app/assets/javascripts/ide/stores/modules/merge_requests/mutations.js @@ -2,15 +2,15 @@ import * as types from './mutation_types'; export default { - [types.REQUEST_MERGE_REQUESTS](state) { - state.isLoading = true; + [types.REQUEST_MERGE_REQUESTS](state, type) { + state[type].isLoading = true; }, - [types.RECEIVE_MERGE_REQUESTS_ERROR](state) { - state.isLoading = false; + [types.RECEIVE_MERGE_REQUESTS_ERROR](state, type) { + state[type].isLoading = false; }, - [types.RECEIVE_MERGE_REQUESTS_SUCCESS](state, data) { - state.isLoading = false; - state.mergeRequests = data.map(mergeRequest => ({ + [types.RECEIVE_MERGE_REQUESTS_SUCCESS](state, { type, data }) { + state[type].isLoading = false; + state[type].mergeRequests = data.map(mergeRequest => ({ id: mergeRequest.id, iid: mergeRequest.iid, title: mergeRequest.title, @@ -20,7 +20,7 @@ export default { .replace(`/merge_requests/${mergeRequest.iid}`, ''), })); }, - [types.RESET_MERGE_REQUESTS](state) { - state.mergeRequests = []; + [types.RESET_MERGE_REQUESTS](state, type) { + state[type].mergeRequests = []; }, }; diff --git a/app/assets/javascripts/ide/stores/modules/merge_requests/state.js b/app/assets/javascripts/ide/stores/modules/merge_requests/state.js index 2947b686c1c..57eb6b04283 100644 --- a/app/assets/javascripts/ide/stores/modules/merge_requests/state.js +++ b/app/assets/javascripts/ide/stores/modules/merge_requests/state.js @@ -1,8 +1,13 @@ -import { scopes, states } from './constants'; +import { states } from './constants'; export default () => ({ - isLoading: false, - mergeRequests: [], - scope: scopes.assignedToMe, + created: { + isLoading: false, + mergeRequests: [], + }, + assigned: { + isLoading: false, + mergeRequests: [], + }, state: states.opened, }); diff --git a/app/assets/javascripts/ide/stores/modules/pipelines/actions.js b/app/assets/javascripts/ide/stores/modules/pipelines/actions.js index 1ebe487263b..fe1dc9ac8f8 100644 --- a/app/assets/javascripts/ide/stores/modules/pipelines/actions.js +++ b/app/assets/javascripts/ide/stores/modules/pipelines/actions.js @@ -4,6 +4,7 @@ import { __ } from '../../../../locale'; import flash from '../../../../flash'; import Poll from '../../../../lib/utils/poll'; import service from '../../../services'; +import { rightSidebarViews } from '../../../constants'; import * as types from './mutation_types'; let eTagPoll; @@ -11,8 +12,12 @@ let eTagPoll; export const clearEtagPoll = () => { eTagPoll = null; }; -export const stopPipelinePolling = () => eTagPoll && eTagPoll.stop(); -export const restartPipelinePolling = () => eTagPoll && eTagPoll.restart(); +export const stopPipelinePolling = () => { + if (eTagPoll) eTagPoll.stop(); +}; +export const restartPipelinePolling = () => { + if (eTagPoll) eTagPoll.restart(); +}; export const requestLatestPipeline = ({ commit }) => commit(types.REQUEST_LATEST_PIPELINE); export const receiveLatestPipelineError = ({ commit, dispatch }) => { @@ -50,9 +55,9 @@ export const fetchLatestPipeline = ({ dispatch, rootGetters }) => { Visibility.change(() => { if (!Visibility.hidden()) { - eTagPoll.restart(); + dispatch('restartPipelinePolling'); } else { - eTagPoll.stop(); + dispatch('stopPipelinePolling'); } }); }; @@ -77,4 +82,33 @@ export const fetchJobs = ({ dispatch }, stage) => { export const toggleStageCollapsed = ({ commit }, stageId) => commit(types.TOGGLE_STAGE_COLLAPSE, stageId); +export const setDetailJob = ({ commit, dispatch }, job) => { + commit(types.SET_DETAIL_JOB, job); + dispatch('setRightPane', job ? rightSidebarViews.jobsDetail : rightSidebarViews.pipelines, { + root: true, + }); +}; + +export const requestJobTrace = ({ commit }) => commit(types.REQUEST_JOB_TRACE); +export const receiveJobTraceError = ({ commit }) => { + flash(__('Error fetching job trace')); + commit(types.RECEIVE_JOB_TRACE_ERROR); +}; +export const receiveJobTraceSuccess = ({ commit }, data) => + commit(types.RECEIVE_JOB_TRACE_SUCCESS, data); + +export const fetchJobTrace = ({ dispatch, state }) => { + dispatch('requestJobTrace'); + + return axios + .get(`${state.detailJob.path}/trace`, { params: { format: 'json' } }) + .then(({ data }) => dispatch('receiveJobTraceSuccess', data)) + .catch(() => dispatch('receiveJobTraceError')); +}; + +export const resetLatestPipeline = ({ commit }) => { + commit(types.RECEIVE_LASTEST_PIPELINE_SUCCESS, null); + commit(types.SET_DETAIL_JOB, null); +}; + export default () => {}; diff --git a/app/assets/javascripts/ide/stores/modules/pipelines/mutation_types.js b/app/assets/javascripts/ide/stores/modules/pipelines/mutation_types.js index 3ddc8409c5b..f4c36b9d96f 100644 --- a/app/assets/javascripts/ide/stores/modules/pipelines/mutation_types.js +++ b/app/assets/javascripts/ide/stores/modules/pipelines/mutation_types.js @@ -7,3 +7,9 @@ export const RECEIVE_JOBS_ERROR = 'RECEIVE_JOBS_ERROR'; export const RECEIVE_JOBS_SUCCESS = 'RECEIVE_JOBS_SUCCESS'; export const TOGGLE_STAGE_COLLAPSE = 'TOGGLE_STAGE_COLLAPSE'; + +export const SET_DETAIL_JOB = 'SET_DETAIL_JOB'; + +export const REQUEST_JOB_TRACE = 'REQUEST_JOB_TRACE'; +export const RECEIVE_JOB_TRACE_ERROR = 'RECEIVE_JOB_TRACE_ERROR'; +export const RECEIVE_JOB_TRACE_SUCCESS = 'RECEIVE_JOB_TRACE_SUCCESS'; diff --git a/app/assets/javascripts/ide/stores/modules/pipelines/mutations.js b/app/assets/javascripts/ide/stores/modules/pipelines/mutations.js index 745797e1ee5..5a2213bbe89 100644 --- a/app/assets/javascripts/ide/stores/modules/pipelines/mutations.js +++ b/app/assets/javascripts/ide/stores/modules/pipelines/mutations.js @@ -63,4 +63,17 @@ export default { isCollapsed: stage.id === id ? !stage.isCollapsed : stage.isCollapsed, })); }, + [types.SET_DETAIL_JOB](state, job) { + state.detailJob = { ...job }; + }, + [types.REQUEST_JOB_TRACE](state) { + state.detailJob.isLoading = true; + }, + [types.RECEIVE_JOB_TRACE_ERROR](state) { + state.detailJob.isLoading = false; + }, + [types.RECEIVE_JOB_TRACE_SUCCESS](state, data) { + state.detailJob.isLoading = false; + state.detailJob.output = data.html; + }, }; diff --git a/app/assets/javascripts/ide/stores/modules/pipelines/state.js b/app/assets/javascripts/ide/stores/modules/pipelines/state.js index 0f83b315fff..8651e267b53 100644 --- a/app/assets/javascripts/ide/stores/modules/pipelines/state.js +++ b/app/assets/javascripts/ide/stores/modules/pipelines/state.js @@ -3,4 +3,5 @@ export default () => ({ isLoadingJobs: false, latestPipeline: null, stages: [], + detailJob: null, }); diff --git a/app/assets/javascripts/ide/stores/modules/pipelines/utils.js b/app/assets/javascripts/ide/stores/modules/pipelines/utils.js index 9f4b0d7d726..a6caca2d2dc 100644 --- a/app/assets/javascripts/ide/stores/modules/pipelines/utils.js +++ b/app/assets/javascripts/ide/stores/modules/pipelines/utils.js @@ -4,4 +4,8 @@ export const normalizeJob = job => ({ name: job.name, status: job.status, path: job.build_path, + rawPath: `${job.build_path}/raw`, + started: job.started, + output: '', + isLoading: false, }); diff --git a/app/assets/javascripts/ide/stores/mutation_types.js b/app/assets/javascripts/ide/stores/mutation_types.js index fbfb92105d6..99b315ac4db 100644 --- a/app/assets/javascripts/ide/stores/mutation_types.js +++ b/app/assets/javascripts/ide/stores/mutation_types.js @@ -68,3 +68,6 @@ export const TOGGLE_FILE_FINDER = 'TOGGLE_FILE_FINDER'; export const BURST_UNUSED_SEAL = 'BURST_UNUSED_SEAL'; export const SET_RIGHT_PANE = 'SET_RIGHT_PANE'; + +export const CLEAR_PROJECTS = 'CLEAR_PROJECTS'; +export const RESET_OPEN_FILES = 'RESET_OPEN_FILES'; diff --git a/app/assets/javascripts/ide/stores/mutations.js b/app/assets/javascripts/ide/stores/mutations.js index eeaa7cb0ec3..48f1da4eccf 100644 --- a/app/assets/javascripts/ide/stores/mutations.js +++ b/app/assets/javascripts/ide/stores/mutations.js @@ -157,6 +157,12 @@ export default { [types.SET_LINKS](state, links) { Object.assign(state, { links }); }, + [types.CLEAR_PROJECTS](state) { + Object.assign(state, { projects: {}, trees: {} }); + }, + [types.RESET_OPEN_FILES](state) { + Object.assign(state, { openFiles: [] }); + }, ...projectMutations, ...mergeRequestMutation, ...fileMutations, diff --git a/app/assets/javascripts/ide/stores/mutations/file.js b/app/assets/javascripts/ide/stores/mutations/file.js index 13f123b6630..46547820425 100644 --- a/app/assets/javascripts/ide/stores/mutations/file.js +++ b/app/assets/javascripts/ide/stores/mutations/file.js @@ -1,5 +1,6 @@ /* eslint-disable no-param-reassign */ import * as types from '../mutation_types'; +import { diffModes } from '../../constants'; export default { [types.SET_FILE_ACTIVE](state, { path, active }) { @@ -46,6 +47,7 @@ export default { baseRaw: null, html: data.html, size: data.size, + lastCommitSha: data.last_commit_sha, }); }, [types.SET_FILE_RAW_DATA](state, { file, raw }) { @@ -85,8 +87,19 @@ export default { }); }, [types.SET_FILE_MERGE_REQUEST_CHANGE](state, { file, mrChange }) { + let diffMode = diffModes.replaced; + if (mrChange.new_file) { + diffMode = diffModes.new; + } else if (mrChange.deleted_file) { + diffMode = diffModes.deleted; + } else if (mrChange.renamed_file) { + diffMode = diffModes.renamed; + } Object.assign(state.entries[file.path], { - mrChange, + mrChange: { + ...mrChange, + diffMode, + }, }); }, [types.SET_FILE_VIEWMODE](state, { file, viewMode }) { diff --git a/app/assets/javascripts/ide/stores/utils.js b/app/assets/javascripts/ide/stores/utils.js index e0b9766fbee..10368a4d97c 100644 --- a/app/assets/javascripts/ide/stores/utils.js +++ b/app/assets/javascripts/ide/stores/utils.js @@ -17,6 +17,7 @@ export const dataStructure = () => ({ changed: false, staged: false, lastCommitPath: '', + lastCommitSha: '', lastCommit: { id: '', url: '', @@ -39,7 +40,7 @@ export const dataStructure = () => ({ editorColumn: 1, fileLanguage: '', eol: '', - viewMode: 'edit', + viewMode: 'editor', previewMode: null, size: 0, parentPath: null, @@ -104,7 +105,7 @@ export const setPageTitle = title => { document.title = title; }; -export const createCommitPayload = (branch, newBranch, state, rootState) => ({ +export const createCommitPayload = ({ branch, newBranch, state, rootState }) => ({ branch, commit_message: state.commitMessage, actions: rootState.stagedFiles.map(f => ({ @@ -112,6 +113,7 @@ export const createCommitPayload = (branch, newBranch, state, rootState) => ({ file_path: f.path, content: f.content, encoding: f.base64 ? 'base64' : 'text', + last_commit_id: newBranch ? undefined : f.lastCommitSha, })), start_branch: newBranch ? rootState.currentBranchId : undefined, }); 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 0a1c253c637..fa35c215880 100644 --- a/app/assets/javascripts/ide/stores/workers/files_decorator_worker.js +++ b/app/assets/javascripts/ide/stores/workers/files_decorator_worker.js @@ -1,6 +1,7 @@ import { viewerInformationForPath } from '~/vue_shared/components/content_viewer/lib/viewer_utils'; import { decorateData, sortTree } from '../utils'; +// eslint-disable-next-line no-restricted-globals self.addEventListener('message', e => { const { data, projectId, branchId, tempFile = false, content = '', base64 = false } = e.data; @@ -89,6 +90,7 @@ self.addEventListener('message', e => { return acc; }, {}); + // eslint-disable-next-line no-restricted-globals self.postMessage({ entries, treeList: sortTree(treeList), diff --git a/app/assets/javascripts/importer_status.js b/app/assets/javascripts/importer_status.js index b469e1e2adc..f9ff0722c01 100644 --- a/app/assets/javascripts/importer_status.js +++ b/app/assets/javascripts/importer_status.js @@ -58,7 +58,7 @@ class ImporterStatus { job.find('.import-target').html(`<a href="${data.full_path}">${data.full_path}</a>`); $('table.import-jobs tbody').prepend(job); - job.addClass('active'); + job.addClass('table-active'); const connectingVerb = this.ciCdOnly ? __('connecting') : __('importing'); job.find('.import-actions').html(sprintf( _.escape(__('%{loadingIcon} Started')), { @@ -67,7 +67,15 @@ class ImporterStatus { false, )); }) - .catch(() => flash(__('An error occurred while importing project'))); + .catch((error) => { + let details = error; + + if (error.response && error.response.data && error.response.data.errors) { + details = error.response.data.errors; + } + + flash(__(`An error occurred while importing project: ${details}`)); + }); } autoUpdate() { @@ -81,7 +89,7 @@ class ImporterStatus { switch (job.import_status) { case 'finished': - jobItem.removeClass('active').addClass('success'); + jobItem.removeClass('table-active').addClass('table-success'); statusField.html(`<span><i class="fa fa-check"></i> ${__('Done')}</span>`); break; case 'scheduled': diff --git a/app/assets/javascripts/init_changes_dropdown.js b/app/assets/javascripts/init_changes_dropdown.js index 09cca1dc7d9..5c5a6e01848 100644 --- a/app/assets/javascripts/init_changes_dropdown.js +++ b/app/assets/javascripts/init_changes_dropdown.js @@ -1,5 +1,5 @@ import $ from 'jquery'; -import stickyMonitor from './lib/utils/sticky'; +import { stickyMonitor } from './lib/utils/sticky'; export default (stickyTop) => { stickyMonitor(document.querySelector('.js-diff-files-changed'), stickyTop); diff --git a/app/assets/javascripts/issue_show/components/app.vue b/app/assets/javascripts/issue_show/components/app.vue index e87a8ed7fea..b6364318537 100644 --- a/app/assets/javascripts/issue_show/components/app.vue +++ b/app/assets/javascripts/issue_show/components/app.vue @@ -226,7 +226,7 @@ .then(res => res.data) .then(data => this.checkForSpam(data)) .then((data) => { - if (location.pathname !== data.web_url) { + if (window.location.pathname !== data.web_url) { visitUrl(data.web_url); } diff --git a/app/assets/javascripts/issue_show/components/description.vue b/app/assets/javascripts/issue_show/components/description.vue index ae577e04a56..1174177f561 100644 --- a/app/assets/javascripts/issue_show/components/description.vue +++ b/app/assets/javascripts/issue_show/components/description.vue @@ -110,25 +110,25 @@ <template> <div v-if="descriptionHtml" - class="description" :class="{ 'js-task-list-container': canUpdate }" + class="description" > <div - class="wiki" + ref="gfm-content" :class="{ 'issue-realtime-pre-pulse': preAnimation, 'issue-realtime-trigger-pulse': pulseAnimation }" - v-html="descriptionHtml" - ref="gfm-content"> + class="wiki" + v-html="descriptionHtml"> </div> <textarea - class="hidden js-task-list-field" v-if="descriptionText" v-model="descriptionText" :data-update-url="updateUrl" + class="hidden js-task-list-field" > </textarea> diff --git a/app/assets/javascripts/issue_show/components/edit_actions.vue b/app/assets/javascripts/issue_show/components/edit_actions.vue index 7ef5e679881..597c6d69a81 100644 --- a/app/assets/javascripts/issue_show/components/edit_actions.vue +++ b/app/assets/javascripts/issue_show/components/edit_actions.vue @@ -38,7 +38,7 @@ }, deleteIssuable() { // eslint-disable-next-line no-alert - if (confirm('Issue will be removed! Are you sure?')) { + if (window.confirm('Issue will be removed! Are you sure?')) { this.deleteLoading = true; eventHub.$emit('delete.issuable'); @@ -51,16 +51,16 @@ <template> <div class="prepend-top-default append-bottom-default clearfix"> <button - class="btn btn-save float-left" :class="{ disabled: formState.updateLoading || !isSubmitEnabled }" - type="submit" :disabled="formState.updateLoading || !isSubmitEnabled" + class="btn btn-save float-left" + type="submit" @click.prevent="updateIssuable"> Save changes <i + v-if="formState.updateLoading" class="fa fa-spinner fa-spin" - aria-hidden="true" - v-if="formState.updateLoading"> + aria-hidden="true"> </i> </button> <button @@ -71,16 +71,16 @@ </button> <button v-if="shouldShowDeleteButton" - class="btn btn-danger float-right append-right-default" :class="{ disabled: deleteLoading }" - type="button" :disabled="deleteLoading" + class="btn btn-danger float-right append-right-default" + type="button" @click="deleteIssuable"> Delete <i + v-if="deleteLoading" class="fa fa-spinner fa-spin" - aria-hidden="true" - v-if="deleteLoading"> + aria-hidden="true"> </i> </button> </div> diff --git a/app/assets/javascripts/issue_show/components/edited.vue b/app/assets/javascripts/issue_show/components/edited.vue index 01097b5b35e..5ff5b1630b1 100644 --- a/app/assets/javascripts/issue_show/components/edited.vue +++ b/app/assets/javascripts/issue_show/components/edited.vue @@ -37,16 +37,16 @@ Edited <time-ago-tooltip v-if="updatedAt" - tooltip-placement="bottom" :time="updatedAt" + tooltip-placement="bottom" /> <span v-if="hasUpdatedBy" > by <a - class="author_link" :href="updatedByPath" + class="author_link" > <span>{{ updatedByName }}</span> </a> diff --git a/app/assets/javascripts/issue_show/components/fields/description.vue b/app/assets/javascripts/issue_show/components/fields/description.vue index d9fa2764d65..5f58f671c73 100644 --- a/app/assets/javascripts/issue_show/components/fields/description.vue +++ b/app/assets/javascripts/issue_show/components/fields/description.vue @@ -52,13 +52,13 @@ > <textarea id="issue-description" + ref="textarea" + slot="textarea" + v-model="formState.description" class="note-textarea js-gfm-input js-autosize markdown-area" data-supports-quick-actions="false" aria-label="Description" - v-model="formState.description" - ref="textarea" - slot="textarea" - placeholder="Write a comment or drag your files here..." + placeholder="Write a comment or drag your files here…" @keydown.meta.enter="updateIssuable" @keydown.ctrl.enter="updateIssuable"> </textarea> diff --git a/app/assets/javascripts/issue_show/components/fields/description_template.vue b/app/assets/javascripts/issue_show/components/fields/description_template.vue index 7db0488e306..e90d9fad94e 100644 --- a/app/assets/javascripts/issue_show/components/fields/description_template.vue +++ b/app/assets/javascripts/issue_show/components/fields/description_template.vue @@ -48,15 +48,15 @@ class="dropdown js-issuable-selector-wrap" data-issuable-type="issue"> <button + ref="toggle" + :data-namespace-path="projectNamespace" + :data-project-path="projectPath" + :data-data="issuableTemplatesJson" class="dropdown-menu-toggle js-issuable-selector" type="button" - ref="toggle" data-field-name="issuable_template" data-selected="null" - data-toggle="dropdown" - :data-namespace-path="projectNamespace" - :data-project-path="projectPath" - :data-data="issuableTemplatesJson"> + data-toggle="dropdown"> <span class="dropdown-toggle-text"> Choose a template </span> diff --git a/app/assets/javascripts/issue_show/components/fields/title.vue b/app/assets/javascripts/issue_show/components/fields/title.vue index c3abb9fd9d5..7d1526a64b4 100644 --- a/app/assets/javascripts/issue_show/components/fields/title.vue +++ b/app/assets/javascripts/issue_show/components/fields/title.vue @@ -21,11 +21,11 @@ </label> <input id="issuable-title" + v-model="formState.title" class="form-control" type="text" placeholder="Title" aria-label="Title" - v-model="formState.title" @keydown.meta.enter="updateIssuable" @keydown.ctrl.enter="updateIssuable" /> </fieldset> diff --git a/app/assets/javascripts/issue_show/components/form.vue b/app/assets/javascripts/issue_show/components/form.vue index ab8bd34762f..5bfc072e3da 100644 --- a/app/assets/javascripts/issue_show/components/form.vue +++ b/app/assets/javascripts/issue_show/components/form.vue @@ -72,8 +72,8 @@ <locked-warning v-if="formState.lockedWarningVisible" /> <div class="row"> <div - class="col-sm-4 col-lg-3" - v-if="hasIssuableTemplates"> + v-if="hasIssuableTemplates" + class="col-sm-4 col-lg-3"> <description-template :form-state="formState" :issuable-templates="issuableTemplates" diff --git a/app/assets/javascripts/issue_show/components/locked_warning.vue b/app/assets/javascripts/issue_show/components/locked_warning.vue index 1c2789f154a..ad0d40faf32 100644 --- a/app/assets/javascripts/issue_show/components/locked_warning.vue +++ b/app/assets/javascripts/issue_show/components/locked_warning.vue @@ -2,7 +2,7 @@ export default { computed: { currentPath() { - return location.pathname; + return window.location.pathname; }, }, }; diff --git a/app/assets/javascripts/issue_show/components/title.vue b/app/assets/javascripts/issue_show/components/title.vue index aec890a2ff6..12101c0daa5 100644 --- a/app/assets/javascripts/issue_show/components/title.vue +++ b/app/assets/javascripts/issue_show/components/title.vue @@ -67,11 +67,11 @@ <template> <div class="title-container"> <h2 - class="title" :class="{ 'issue-realtime-pre-pulse': preAnimation, 'issue-realtime-trigger-pulse': pulseAnimation }" + class="title" v-html="titleHtml" > </h2> @@ -80,11 +80,11 @@ v-if="showInlineEditButton && canUpdate" type="button" class="btn btn-default btn-edit btn-svg js-issuable-edit" - v-html="pencilIcon" title="Edit title and description" data-placement="bottom" data-container="body" @click="edit" + v-html="pencilIcon" > </button> </div> diff --git a/app/assets/javascripts/job.js b/app/assets/javascripts/job.js index 611e8200b4d..fc13f467675 100644 --- a/app/assets/javascripts/job.js +++ b/app/assets/javascripts/job.js @@ -1,14 +1,17 @@ import $ from 'jquery'; import _ from 'underscore'; -import StickyFill from 'stickyfilljs'; +import { polyfillSticky } from './lib/utils/sticky'; import axios from './lib/utils/axios_utils'; import { visitUrl } from './lib/utils/url_utility'; import bp from './breakpoints'; import { numberToHumanSize } from './lib/utils/number_utils'; import { setCiStatusFavicon } from './lib/utils/common_utils'; +import { isScrolledToBottom, scrollDown } from './lib/utils/scroll_utils'; +import LogOutputBehaviours from './lib/utils/logoutput_behaviours'; -export default class Job { +export default class Job extends LogOutputBehaviours { constructor(options) { + super(); this.timeout = null; this.state = null; this.fetchingStatusFavicon = false; @@ -29,10 +32,6 @@ export default class Job { this.$buildTraceOutput = $('.js-build-output'); this.$topBar = $('.js-top-bar'); - // Scroll controllers - this.$scrollTopBtn = $('.js-scroll-up'); - this.$scrollBottomBtn = $('.js-scroll-down'); - clearTimeout(this.timeout); this.initSidebar(); @@ -48,23 +47,14 @@ export default class Job { .off('click', '.stage-item') .on('click', '.stage-item', this.updateDropdown); - // add event listeners to the scroll buttons - this.$scrollTopBtn - .off('click') - .on('click', this.scrollToTop.bind(this)); - - this.$scrollBottomBtn - .off('click') - .on('click', this.scrollToBottom.bind(this)); - this.scrollThrottled = _.throttle(this.toggleScroll.bind(this), 100); this.$window .off('scroll') .on('scroll', () => { - if (!this.isScrolledToBottom()) { + if (!isScrolledToBottom()) { this.toggleScrollAnimation(false); - } else if (this.isScrolledToBottom() && !this.isLogComplete) { + } else if (isScrolledToBottom() && !this.isLogComplete) { this.toggleScrollAnimation(true); } this.scrollThrottled(); @@ -80,70 +70,11 @@ export default class Job { } initAffixTopArea() { - /** - 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 use a polyfill - */ - if (this.$topBar.css('position') !== 'static') return; - - StickyFill.add(this.$topBar); - } - - // eslint-disable-next-line class-methods-use-this - canScroll() { - return $(document).height() > $(window).height(); - } - - toggleScroll() { - const $document = $(document); - const currentPosition = $document.scrollTop(); - const scrollHeight = $document.height(); - - const windowHeight = $(window).height(); - if (this.canScroll()) { - if (currentPosition > 0 && - (scrollHeight - currentPosition !== windowHeight)) { - // User is in the middle of the log - - this.toggleDisableButton(this.$scrollTopBtn, false); - this.toggleDisableButton(this.$scrollBottomBtn, false); - } else if (currentPosition === 0) { - // User is at Top of Log - - this.toggleDisableButton(this.$scrollTopBtn, true); - this.toggleDisableButton(this.$scrollBottomBtn, false); - } else if (this.isScrolledToBottom()) { - // User is at the bottom of the build log. - - this.toggleDisableButton(this.$scrollTopBtn, false); - this.toggleDisableButton(this.$scrollBottomBtn, true); - } - } else { - this.toggleDisableButton(this.$scrollTopBtn, true); - this.toggleDisableButton(this.$scrollBottomBtn, true); - } - } - // eslint-disable-next-line class-methods-use-this - isScrolledToBottom() { - const $document = $(document); - - const currentPosition = $document.scrollTop(); - const scrollHeight = $document.height(); - - const windowHeight = $(window).height(); - - return scrollHeight - currentPosition === windowHeight; - } - - // eslint-disable-next-line class-methods-use-this - scrollDown() { - const $document = $(document); - $document.scrollTop($document.height()); + polyfillSticky(this.$topBar); } scrollToBottom() { - this.scrollDown(); + scrollDown(); this.hasBeenScrolled = true; this.toggleScroll(); } @@ -154,12 +85,6 @@ export default class Job { this.toggleScroll(); } - // eslint-disable-next-line class-methods-use-this - toggleDisableButton($button, disable) { - if (disable && $button.prop('disabled')) return; - $button.prop('disabled', disable); - } - toggleScrollAnimation(toggle) { this.$scrollBottomBtn.toggleClass('animate', toggle); } @@ -191,7 +116,7 @@ export default class Job { this.state = log.state; } - this.isScrollInBottom = this.isScrolledToBottom(); + this.isScrollInBottom = isScrolledToBottom(); if (log.append) { this.$buildTraceOutput.append(log.html); @@ -231,7 +156,7 @@ export default class Job { }) .then(() => { if (this.isScrollInBottom) { - this.scrollDown(); + scrollDown(); } }) .then(() => this.toggleScroll()); diff --git a/app/assets/javascripts/jobs/components/header.vue b/app/assets/javascripts/jobs/components/header.vue index c1044f4cd42..1e7f4b2c3f7 100644 --- a/app/assets/javascripts/jobs/components/header.vue +++ b/app/assets/javascripts/jobs/components/header.vue @@ -42,6 +42,9 @@ export default { jobStarted() { return !this.job.started === false; }, + headerTime() { + return this.jobStarted ? this.job.started : this.job.created_at; + }, }, watch: { job() { @@ -71,13 +74,13 @@ export default { <ci-header v-if="shouldRenderContent" :status="status" - item-name="Job" :item-id="job.id" - :time="job.created_at" + :time="headerTime" :user="job.user" :actions="actions" :has-sidebar-button="true" :should-render-triggered-label="jobStarted" + item-name="Job" /> <loading-icon v-if="isLoading" diff --git a/app/assets/javascripts/jobs/components/sidebar_details_block.vue b/app/assets/javascripts/jobs/components/sidebar_details_block.vue index 8f3c66b0cbe..d2adf628050 100644 --- a/app/assets/javascripts/jobs/components/sidebar_details_block.vue +++ b/app/assets/javascripts/jobs/components/sidebar_details_block.vue @@ -101,8 +101,8 @@ export default { {{ __('Retry') }} </a> <button - type="button" :aria-label="__('Toggle Sidebar')" + type="button" class="btn btn-blank gutter-toggle float-right d-block d-md-none js-sidebar-build-toggle" > <i @@ -114,20 +114,20 @@ export default { </div> <template v-if="shouldRenderContent"> <div - class="block retry-link" v-if="job.retry_path || job.new_issue_path" + class="block retry-link" > <a v-if="job.new_issue_path" - class="js-new-issue btn btn-new btn-inverted" :href="job.new_issue_path" + class="js-new-issue btn btn-new btn-inverted" > {{ __('New issue') }} </a> <a v-if="canUserRetry" - class="js-retry-job btn btn-inverted-secondary" :href="job.retry_path" + class="js-retry-job btn btn-inverted-secondary" data-method="post" rel="nofollow" > @@ -136,8 +136,8 @@ export default { </div> <div :class="{block : renderBlock }"> <p - class="build-detail-row js-job-mr" v-if="job.merge_request" + class="build-detail-row js-job-mr" > <span class="build-light-text"> {{ __('Merge Request:') }} @@ -148,51 +148,51 @@ export default { </p> <detail-row - class="js-job-duration" v-if="job.duration" - title="Duration" :value="duration" + class="js-job-duration" + title="Duration" /> <detail-row - class="js-job-finished" v-if="job.finished_at" - title="Finished" :value="timeFormated(job.finished_at)" + class="js-job-finished" + title="Finished" /> <detail-row - class="js-job-erased" v-if="job.erased_at" - title="Erased" :value="timeFormated(job.erased_at)" + class="js-job-erased" + title="Erased" /> <detail-row - class="js-job-queued" v-if="job.queued" - title="Queued" :value="queued" + class="js-job-queued" + title="Queued" /> <detail-row - class="js-job-timeout" v-if="hasTimeout" - title="Timeout" :help-url="runnerHelpUrl" :value="timeout" + class="js-job-timeout" + title="Timeout" /> <detail-row - class="js-job-runner" v-if="job.runner" - title="Runner" :value="runnerId" + class="js-job-runner" + title="Runner" /> <detail-row - class="js-job-coverage" v-if="job.coverage" - title="Coverage" :value="coverage" + class="js-job-coverage" + title="Coverage" /> <p - class="build-detail-row js-job-tags" v-if="job.tags.length" + class="build-detail-row js-job-tags" > <span class="build-light-text"> {{ __('Tags:') }} @@ -210,8 +210,8 @@ export default { class="btn-group prepend-top-5" role="group"> <a - class="js-cancel-job btn btn-sm btn-default" :href="job.cancel_path" + class="js-cancel-job btn btn-sm btn-default" data-method="post" rel="nofollow" > @@ -221,8 +221,8 @@ export default { </div> </template> <loading-icon - class="prepend-top-10" v-if="isLoading" + class="prepend-top-10" size="2" /> </div> diff --git a/app/assets/javascripts/label_manager.js b/app/assets/javascripts/label_manager.js index 8c3de6e4045..8b01024b7d4 100644 --- a/app/assets/javascripts/label_manager.js +++ b/app/assets/javascripts/label_manager.js @@ -13,6 +13,7 @@ export default class LabelManager { this.otherLabels = otherLabels || $('.js-other-labels'); this.errorMessage = 'Unable to update label prioritization at this time'; this.emptyState = document.querySelector('#js-priority-labels-empty-state'); + this.$badgeItemTemplate = $('#js-badge-item-template'); this.sortable = Sortable.create(this.prioritizedLabels.get(0), { filter: '.empty-message', forceFallback: true, @@ -63,7 +64,11 @@ export default class LabelManager { $target = this.otherLabels; $from = this.prioritizedLabels; } - $label.detach().appendTo($target); + + const $detachedLabel = $label.detach(); + this.toggleLabelPriorityBadge($detachedLabel, action); + $detachedLabel.appendTo($target); + if ($from.find('li').length) { $from.find('.empty-message').removeClass('hidden'); } @@ -88,6 +93,14 @@ export default class LabelManager { } } + toggleLabelPriorityBadge($label, action) { + if (action === 'remove') { + $('.js-priority-badge', $label).remove(); + } else { + $('.label-links', $label).append(this.$badgeItemTemplate.clone().html()); + } + } + onPrioritySortUpdate() { this.savePrioritySort() .catch(() => flash(this.errorMessage)); diff --git a/app/assets/javascripts/labels_select.js b/app/assets/javascripts/labels_select.js index eafdaf4a672..7d0ff53f366 100644 --- a/app/assets/javascripts/labels_select.js +++ b/app/assets/javascripts/labels_select.js @@ -426,7 +426,7 @@ export default class LabelsSelect { const tpl = _.template([ '<% _.each(labels, function(label){ %>', '<a href="<%- issueUpdateURL.slice(0, issueUpdateURL.lastIndexOf("/")) %>?label_name[]=<%- encodeURIComponent(label.title) %>">', - '<span class="label has-tooltip color-label" title="<%- label.description %>" style="background-color: <%- label.color %>; color: <%- label.text_color %>;">', + '<span class="badge label has-tooltip color-label" title="<%- label.description %>" style="background-color: <%- label.color %>; color: <%- label.text_color %>;">', '<%- label.title %>', '</span>', '</a>', diff --git a/app/assets/javascripts/lazy_loader.js b/app/assets/javascripts/lazy_loader.js index dbbf1637a47..9482d131344 100644 --- a/app/assets/javascripts/lazy_loader.js +++ b/app/assets/javascripts/lazy_loader.js @@ -44,8 +44,8 @@ export default class LazyLoader { requestAnimationFrame(() => this.checkElementsInView()); } checkElementsInView() { - const scrollTop = pageYOffset; - const visHeight = scrollTop + innerHeight + SCROLL_THRESHOLD; + const scrollTop = window.pageYOffset; + const visHeight = scrollTop + window.innerHeight + SCROLL_THRESHOLD; // Loading Images which are in the current viewport or close to them this.lazyImages = this.lazyImages.filter((selectedImage) => { diff --git a/app/assets/javascripts/lib/utils/bootstrap_linked_tabs.js b/app/assets/javascripts/lib/utils/bootstrap_linked_tabs.js index 3873f4528ce..c28ed04f94f 100644 --- a/app/assets/javascripts/lib/utils/bootstrap_linked_tabs.js +++ b/app/assets/javascripts/lib/utils/bootstrap_linked_tabs.js @@ -93,7 +93,7 @@ export default class LinkedTabs { const newState = `${copySource}${this.currentLocation.search}${this.currentLocation.hash}`; - history.replaceState({ + window.history.replaceState({ url: newState, }, document.title, newState); return newState; diff --git a/app/assets/javascripts/lib/utils/common_utils.js b/app/assets/javascripts/lib/utils/common_utils.js index 8b5445d012b..d0b0e5e1ba1 100644 --- a/app/assets/javascripts/lib/utils/common_utils.js +++ b/app/assets/javascripts/lib/utils/common_utils.js @@ -197,7 +197,10 @@ export const insertText = (target, text) => { // eslint-disable-next-line no-param-reassign target.value = newText; // eslint-disable-next-line no-param-reassign - target.selectionStart = target.selectionEnd = selectionStart + insertedText.length; + target.selectionStart = selectionStart + insertedText.length; + + // eslint-disable-next-line no-param-reassign + target.selectionEnd = selectionStart + insertedText.length; // Trigger autosave target.dispatchEvent(new Event('input')); @@ -384,6 +387,49 @@ export const backOff = (fn, timeout = 60000) => { }); }; +export const createOverlayIcon = (iconPath, overlayPath) => { + const faviconImage = document.createElement('img'); + + return new Promise((resolve) => { + faviconImage.onload = () => { + const size = 32; + + const canvas = document.createElement('canvas'); + canvas.width = size; + canvas.height = size; + + const context = canvas.getContext('2d'); + context.clearRect(0, 0, size, size); + context.drawImage( + faviconImage, 0, 0, faviconImage.width, faviconImage.height, 0, 0, size, size, + ); + + const overlayImage = document.createElement('img'); + overlayImage.onload = () => { + context.drawImage( + overlayImage, 0, 0, overlayImage.width, overlayImage.height, 0, 0, size, size, + ); + + const faviconWithOverlayUrl = canvas.toDataURL(); + + resolve(faviconWithOverlayUrl); + }; + overlayImage.src = overlayPath; + }; + faviconImage.src = iconPath; + }); +}; + +export const setFaviconOverlay = (overlayPath) => { + const faviconEl = document.getElementById('favicon'); + + if (!faviconEl) { return null; } + + const iconPath = faviconEl.getAttribute('data-original-href'); + + return createOverlayIcon(iconPath, overlayPath).then(faviconWithOverlayUrl => faviconEl.setAttribute('href', faviconWithOverlayUrl)); +}; + export const setFavicon = (faviconPath) => { const faviconEl = document.getElementById('favicon'); if (faviconEl && faviconPath) { @@ -393,8 +439,9 @@ export const setFavicon = (faviconPath) => { export const resetFavicon = () => { const faviconEl = document.getElementById('favicon'); - const originalFavicon = faviconEl ? faviconEl.getAttribute('href') : null; + if (faviconEl) { + const originalFavicon = faviconEl.getAttribute('data-original-href'); faviconEl.setAttribute('href', originalFavicon); } }; @@ -403,10 +450,9 @@ export const setCiStatusFavicon = pageUrl => axios.get(pageUrl) .then(({ data }) => { if (data && data.favicon) { - setFavicon(data.favicon); - } else { - resetFavicon(); + return setFaviconOverlay(data.favicon); } + return resetFavicon(); }) .catch(resetFavicon); diff --git a/app/assets/javascripts/lib/utils/datetime_utility.js b/app/assets/javascripts/lib/utils/datetime_utility.js index 0ff23bbb061..7cca32dc6fa 100644 --- a/app/assets/javascripts/lib/utils/datetime_utility.js +++ b/app/assets/javascripts/lib/utils/datetime_utility.js @@ -79,37 +79,37 @@ export function getTimeago() { if (!timeagoInstance) { const localeRemaining = function getLocaleRemaining(number, index) { return [ - [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|just now'), s__('Timeago|right now')], + [s__('Timeago|%s seconds ago'), s__('Timeago|%s seconds remaining')], + [s__('Timeago|1 minute ago'), s__('Timeago|1 minute remaining')], [s__('Timeago|%s minutes ago'), s__('Timeago|%s minutes remaining')], - [s__('Timeago|about an hour ago'), s__('Timeago|1 hour remaining')], - [s__('Timeago|about %s hours ago'), s__('Timeago|%s hours remaining')], - [s__('Timeago|a day ago'), s__('Timeago|1 day remaining')], + [s__('Timeago|1 hour ago'), s__('Timeago|1 hour remaining')], + [s__('Timeago|%s hours ago'), s__('Timeago|%s hours remaining')], + [s__('Timeago|1 day ago'), s__('Timeago|1 day remaining')], [s__('Timeago|%s days ago'), s__('Timeago|%s days remaining')], - [s__('Timeago|a week ago'), s__('Timeago|1 week remaining')], + [s__('Timeago|1 week ago'), s__('Timeago|1 week remaining')], [s__('Timeago|%s weeks ago'), s__('Timeago|%s weeks remaining')], - [s__('Timeago|a month ago'), s__('Timeago|1 month remaining')], + [s__('Timeago|1 month ago'), s__('Timeago|1 month remaining')], [s__('Timeago|%s months ago'), s__('Timeago|%s months remaining')], - [s__('Timeago|a year ago'), s__('Timeago|1 year remaining')], + [s__('Timeago|1 year ago'), s__('Timeago|1 year remaining')], [s__('Timeago|%s years ago'), s__('Timeago|%s years remaining')], ][index]; }; const locale = function getLocale(number, index) { return [ - [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|just now'), s__('Timeago|right now')], + [s__('Timeago|%s seconds ago'), s__('Timeago|in %s seconds')], + [s__('Timeago|1 minute ago'), s__('Timeago|in 1 minute')], [s__('Timeago|%s minutes ago'), s__('Timeago|in %s minutes')], - [s__('Timeago|about an hour ago'), s__('Timeago|in 1 hour')], - [s__('Timeago|about %s hours ago'), s__('Timeago|in %s hours')], - [s__('Timeago|a day ago'), s__('Timeago|in 1 day')], + [s__('Timeago|1 hour ago'), s__('Timeago|in 1 hour')], + [s__('Timeago|%s hours ago'), s__('Timeago|in %s hours')], + [s__('Timeago|1 day ago'), s__('Timeago|in 1 day')], [s__('Timeago|%s days ago'), s__('Timeago|in %s days')], - [s__('Timeago|a week ago'), s__('Timeago|in 1 week')], + [s__('Timeago|1 week ago'), s__('Timeago|in 1 week')], [s__('Timeago|%s weeks ago'), s__('Timeago|in %s weeks')], - [s__('Timeago|a month ago'), s__('Timeago|in 1 month')], + [s__('Timeago|1 month ago'), s__('Timeago|in 1 month')], [s__('Timeago|%s months ago'), s__('Timeago|in %s months')], - [s__('Timeago|a year ago'), s__('Timeago|in 1 year')], + [s__('Timeago|1 year ago'), s__('Timeago|in 1 year')], [s__('Timeago|%s years ago'), s__('Timeago|in %s years')], ][index]; }; @@ -270,6 +270,17 @@ export const totalDaysInMonth = date => { }; /** + * Returns number of days in a quarter from provided + * months array. + * + * @param {Array} quarter + */ +export const totalDaysInQuarter = quarter => quarter.reduce( + (acc, month) => acc + totalDaysInMonth(month), + 0, +); + +/** * Returns list of Dates referring to Sundays of the month * based on provided date * @@ -309,42 +320,27 @@ export const getSundays = date => { }; /** - * Returns list of Dates representing a timeframe of Months from month of provided date (inclusive) - * up to provided length - * - * For eg; - * If current month is January 2018 and `length` provided is `6` - * Then this method will return list of Date objects as follows; - * - * [ October 2017, November 2017, December 2017, January 2018, February 2018, March 2018 ] - * - * If current month is March 2018 and `length` provided is `3` - * Then this method will return list of Date objects as follows; - * - * [ February 2018, March 2018, April 2018 ] + * Returns list of Dates representing a timeframe of months from startDate and length * + * @param {Date} startDate * @param {Number} length - * @param {Date} date */ -export const getTimeframeWindow = (length, date) => { - if (!length) { +export const getTimeframeWindowFrom = (startDate, length) => { + if (!(startDate instanceof Date) || !length) { return []; } - const currentDate = date instanceof Date ? date : new Date(); - const currentMonthIndex = Math.floor(length / 2); - const timeframe = []; - - // Move date object backward to the first month of timeframe - currentDate.setDate(1); - currentDate.setMonth(currentDate.getMonth() - currentMonthIndex); - - // Iterate and update date for the size of length + // Iterate and set date for the size of length // and push date reference to timeframe list - for (let i = 0; i < length; i += 1) { - timeframe.push(new Date(currentDate.getTime())); - currentDate.setMonth(currentDate.getMonth() + 1); - } + const timeframe = new Array(length) + .fill() + .map( + (val, i) => new Date( + startDate.getFullYear(), + startDate.getMonth() + i, + 1, + ), + ); // Change date of last timeframe item to last date of the month timeframe[length - 1].setDate(totalDaysInMonth(timeframe[length - 1])); @@ -352,6 +348,29 @@ export const getTimeframeWindow = (length, date) => { return timeframe; }; +/** + * Returns count of day within current quarter from provided date + * and array of months for the quarter + * + * Eg; + * If date is 15 Feb 2018 + * and quarter is [Jan, Feb, Mar] + * + * Then 15th Feb is 46th day of the quarter + * Where 31 (days in Jan) + 15 (date of Feb). + * + * @param {Date} date + * @param {Array} quarter + */ +export const dayInQuarter = (date, quarter) => quarter.reduce((acc, month) => { + if (date.getMonth() > month.getMonth()) { + return acc + totalDaysInMonth(month); + } else if (date.getMonth() === month.getMonth()) { + return acc + date.getDate(); + } + return acc + 0; +}, 0); + window.gl = window.gl || {}; window.gl.utils = { ...(window.gl.utils || {}), diff --git a/app/assets/javascripts/lib/utils/http_status.js b/app/assets/javascripts/lib/utils/http_status.js index bb151929431..229d53b18b0 100644 --- a/app/assets/javascripts/lib/utils/http_status.js +++ b/app/assets/javascripts/lib/utils/http_status.js @@ -8,4 +8,5 @@ export default { OK: 200, MULTIPLE_CHOICES: 300, BAD_REQUEST: 400, + NOT_FOUND: 404, }; diff --git a/app/assets/javascripts/lib/utils/logoutput_behaviours.js b/app/assets/javascripts/lib/utils/logoutput_behaviours.js new file mode 100644 index 00000000000..1bf99d935ef --- /dev/null +++ b/app/assets/javascripts/lib/utils/logoutput_behaviours.js @@ -0,0 +1,46 @@ +import $ from 'jquery'; +import { canScroll, isScrolledToBottom, toggleDisableButton } from './scroll_utils'; + +export default class LogOutputBehaviours { + constructor() { + // Scroll buttons + this.$scrollTopBtn = $('.js-scroll-up'); + this.$scrollBottomBtn = $('.js-scroll-down'); + + this.$scrollTopBtn.off('click').on('click', this.scrollToTop.bind(this)); + this.$scrollBottomBtn.off('click').on('click', this.scrollToBottom.bind(this)); + } + + toggleScroll() { + const $document = $(document); + const currentPosition = $document.scrollTop(); + const scrollHeight = $document.height(); + + const windowHeight = $(window).height(); + if (canScroll()) { + if (currentPosition > 0 && scrollHeight - currentPosition !== windowHeight) { + // User is in the middle of the log + + toggleDisableButton(this.$scrollTopBtn, false); + toggleDisableButton(this.$scrollBottomBtn, false); + } else if (currentPosition === 0) { + // User is at Top of Log + + toggleDisableButton(this.$scrollTopBtn, true); + toggleDisableButton(this.$scrollBottomBtn, false); + } else if (isScrolledToBottom()) { + // User is at the bottom of the build log. + + toggleDisableButton(this.$scrollTopBtn, false); + toggleDisableButton(this.$scrollBottomBtn, true); + } + } else { + toggleDisableButton(this.$scrollTopBtn, true); + toggleDisableButton(this.$scrollBottomBtn, true); + } + } + + toggleScrollAnimation(toggle) { + this.$scrollBottomBtn.toggleClass('animate', toggle); + } +} diff --git a/app/assets/javascripts/lib/utils/number_utils.js b/app/assets/javascripts/lib/utils/number_utils.js index a02c79b787e..f086d962221 100644 --- a/app/assets/javascripts/lib/utils/number_utils.js +++ b/app/assets/javascripts/lib/utils/number_utils.js @@ -12,7 +12,7 @@ export function formatRelevantDigits(number) { let digitsLeft = ''; let relevantDigits = 0; let formattedNumber = ''; - if (!isNaN(Number(number))) { + if (!Number.isNaN(Number(number))) { digitsLeft = number.toString().split('.')[0]; switch (digitsLeft.length) { case 1: diff --git a/app/assets/javascripts/lib/utils/scroll_utils.js b/app/assets/javascripts/lib/utils/scroll_utils.js new file mode 100644 index 00000000000..9313b570863 --- /dev/null +++ b/app/assets/javascripts/lib/utils/scroll_utils.js @@ -0,0 +1,29 @@ +import $ from 'jquery'; + +export const canScroll = () => $(document).height() > $(window).height(); + +/** + * Checks if the entire page is scrolled down all the way to the bottom + */ +export const isScrolledToBottom = () => { + const $document = $(document); + + const currentPosition = $document.scrollTop(); + const scrollHeight = $document.height(); + + const windowHeight = $(window).height(); + + return scrollHeight - currentPosition === windowHeight; +}; + +export const scrollDown = () => { + const $document = $(document); + $document.scrollTop($document.height()); +}; + +export const toggleDisableButton = ($button, disable) => { + if (disable && $button.prop('disabled')) return; + $button.prop('disabled', disable); +}; + +export default {}; diff --git a/app/assets/javascripts/lib/utils/sticky.js b/app/assets/javascripts/lib/utils/sticky.js index 098afcfa1b4..15a4dd62012 100644 --- a/app/assets/javascripts/lib/utils/sticky.js +++ b/app/assets/javascripts/lib/utils/sticky.js @@ -1,3 +1,5 @@ +import StickyFill from 'stickyfilljs'; + export const createPlaceholder = () => { const placeholder = document.createElement('div'); placeholder.classList.add('sticky-placeholder'); @@ -28,7 +30,16 @@ export const isSticky = (el, scrollY, stickyTop, insertPlaceholder) => { } }; -export default (el, stickyTop, insertPlaceholder = true) => { +/** + * Create a listener that will toggle a 'is-stuck' class, based on the current scroll position. + * + * - If the current environment does not support `position: sticky`, do nothing. + * + * @param {HTMLElement} el The `position: sticky` element. + * @param {Number} stickyTop Used to determine when an element is stuck. + * @param {Boolean} insertPlaceholder Should a placeholder element be created when element is stuck? + */ +export const stickyMonitor = (el, stickyTop, insertPlaceholder = true) => { if (!el) return; if (typeof CSS === 'undefined' || !(CSS.supports('(position: -webkit-sticky) or (position: sticky)'))) return; @@ -37,3 +48,13 @@ export default (el, stickyTop, insertPlaceholder = true) => { passive: true, }); }; + +/** + * Polyfill the `position: sticky` behavior. + * + * - If the current environment supports `position: sticky`, do nothing. + * - Can receive an iterable element list (NodeList, jQuery collection, etc.) or single HTMLElement. + */ +export const polyfillSticky = (el) => { + StickyFill.add(el); +}; diff --git a/app/assets/javascripts/lib/utils/url_utility.js b/app/assets/javascripts/lib/utils/url_utility.js index dd17544b656..72b72f4247d 100644 --- a/app/assets/javascripts/lib/utils/url_utility.js +++ b/app/assets/javascripts/lib/utils/url_utility.js @@ -85,9 +85,9 @@ export function redirectTo(url) { } export function webIDEUrl(route = undefined) { - let returnUrl = `${gon.relative_url_root}/-/ide/`; + let returnUrl = `${gon.relative_url_root || ''}/-/ide/`; if (route) { - returnUrl += `project${route}`; + returnUrl += `project${route.replace(new RegExp(`^${gon.relative_url_root || ''}`), '')}`; } return returnUrl; } diff --git a/app/assets/javascripts/line_highlighter.js b/app/assets/javascripts/line_highlighter.js index f2323f57455..303c5d8a894 100644 --- a/app/assets/javascripts/line_highlighter.js +++ b/app/assets/javascripts/line_highlighter.js @@ -35,7 +35,7 @@ const LineHighlighter = function(options = {}) { options.highlightLineClass = options.highlightLineClass || 'hll'; options.fileHolderSelector = options.fileHolderSelector || '.file-holder'; options.scrollFileHolder = options.scrollFileHolder || false; - options.hash = options.hash || location.hash; + options.hash = options.hash || window.location.hash; this.options = options; this._hash = options.hash; @@ -145,6 +145,8 @@ LineHighlighter.prototype.highlightRange = function(range) { var i, lineNumber, ref, ref1, results; if (range[1]) { results = []; + + // eslint-disable-next-line no-multi-assign for (lineNumber = i = ref = range[0], ref1 = range[1]; ref <= ref1 ? i <= ref1 : i >= ref1; lineNumber = ref <= ref1 ? (i += 1) : (i -= 1)) { results.push(this.highlightLine(lineNumber)); } @@ -170,7 +172,7 @@ LineHighlighter.prototype.setHash = function(firstLineNumber, lastLineNumber) { // // This method is stubbed in tests. LineHighlighter.prototype.__setLocationHash__ = function(value) { - return history.pushState({ + return window.history.pushState({ url: value // We're using pushState instead of assigning location.hash directly to // prevent the page from scrolling on the hashchange event diff --git a/app/assets/javascripts/main.js b/app/assets/javascripts/main.js index 9803bebfd10..c9ce838cd48 100644 --- a/app/assets/javascripts/main.js +++ b/app/assets/javascripts/main.js @@ -144,6 +144,7 @@ document.addEventListener('DOMContentLoaded', () => { $body.tooltip({ selector: '.has-tooltip, [data-toggle="tooltip"]', trigger: 'hover', + boundary: 'viewport', placement(tip, el) { return $(el).data('placement') || 'bottom'; }, diff --git a/app/assets/javascripts/merge_conflicts/components/diff_file_editor.js b/app/assets/javascripts/merge_conflicts/components/diff_file_editor.js index 2cb238529aa..e4ed8111824 100644 --- a/app/assets/javascripts/merge_conflicts/components/diff_file_editor.js +++ b/app/assets/javascripts/merge_conflicts/components/diff_file_editor.js @@ -11,9 +11,18 @@ import { __ } from '~/locale'; global.mergeConflicts.diffFileEditor = Vue.extend({ props: { - file: Object, - onCancelDiscardConfirmation: Function, - onAcceptDiscardConfirmation: Function + file: { + type: Object, + required: true, + }, + onCancelDiscardConfirmation: { + type: Function, + required: true, + }, + onAcceptDiscardConfirmation: { + type: Function, + required: true, + }, }, data() { return { diff --git a/app/assets/javascripts/merge_conflicts/components/inline_conflict_lines.js b/app/assets/javascripts/merge_conflicts/components/inline_conflict_lines.js index 56d6678e1bd..827cf5f478d 100644 --- a/app/assets/javascripts/merge_conflicts/components/inline_conflict_lines.js +++ b/app/assets/javascripts/merge_conflicts/components/inline_conflict_lines.js @@ -1,14 +1,19 @@ -/* eslint-disable no-param-reassign, comma-dangle */ +/* eslint-disable no-param-reassign */ import Vue from 'vue'; +import actionsMixin from '../mixins/line_conflict_actions'; +import utilsMixin from '../mixins/line_conflict_utils'; -((global) => { +(global => { global.mergeConflicts = global.mergeConflicts || {}; global.mergeConflicts.inlineConflictLines = Vue.extend({ + mixins: [utilsMixin, actionsMixin], props: { - file: Object + file: { + type: Object, + required: true, + }, }, - mixins: [global.mergeConflicts.utils, global.mergeConflicts.actions], }); })(window.gl || (window.gl = {})); diff --git a/app/assets/javascripts/merge_conflicts/components/parallel_conflict_lines.js b/app/assets/javascripts/merge_conflicts/components/parallel_conflict_lines.js index 0fc4a13450a..57e73e38d88 100644 --- a/app/assets/javascripts/merge_conflicts/components/parallel_conflict_lines.js +++ b/app/assets/javascripts/merge_conflicts/components/parallel_conflict_lines.js @@ -1,15 +1,20 @@ /* eslint-disable no-param-reassign, comma-dangle */ import Vue from 'vue'; +import actionsMixin from '../mixins/line_conflict_actions'; +import utilsMixin from '../mixins/line_conflict_utils'; ((global) => { global.mergeConflicts = global.mergeConflicts || {}; global.mergeConflicts.parallelConflictLines = Vue.extend({ + mixins: [utilsMixin, actionsMixin], props: { - file: Object + file: { + type: Object, + required: true, + }, }, - mixins: [global.mergeConflicts.utils, global.mergeConflicts.actions], template: ` <table> <tr class="line_holder parallel" v-for="section in file.parallelLines"> diff --git a/app/assets/javascripts/merge_conflicts/merge_conflict_service.js b/app/assets/javascripts/merge_conflicts/merge_conflict_service.js index c68b47c9348..64d69159222 100644 --- a/app/assets/javascripts/merge_conflicts/merge_conflict_service.js +++ b/app/assets/javascripts/merge_conflicts/merge_conflict_service.js @@ -1,23 +1,16 @@ -/* eslint-disable no-param-reassign, comma-dangle */ import axios from '../lib/utils/axios_utils'; -((global) => { - global.mergeConflicts = global.mergeConflicts || {}; - - class mergeConflictsService { - constructor(options) { - this.conflictsPath = options.conflictsPath; - this.resolveConflictsPath = options.resolveConflictsPath; - } - - fetchConflictsData() { - return axios.get(this.conflictsPath); - } +export default class MergeConflictsService { + constructor(options) { + this.conflictsPath = options.conflictsPath; + this.resolveConflictsPath = options.resolveConflictsPath; + } - submitResolveConflicts(data) { - return axios.post(this.resolveConflictsPath, data); - } + fetchConflictsData() { + return axios.get(this.conflictsPath); } - global.mergeConflicts.mergeConflictsService = mergeConflictsService; -})(window.gl || (window.gl = {})); + submitResolveConflicts(data) { + return axios.post(this.resolveConflictsPath, data); + } +} diff --git a/app/assets/javascripts/merge_conflicts/merge_conflicts_bundle.js b/app/assets/javascripts/merge_conflicts/merge_conflicts_bundle.js index 4abd5433bb5..491858c3602 100644 --- a/app/assets/javascripts/merge_conflicts/merge_conflicts_bundle.js +++ b/app/assets/javascripts/merge_conflicts/merge_conflicts_bundle.js @@ -1,13 +1,9 @@ -/* eslint-disable new-cap, comma-dangle, no-new */ - import $ from 'jquery'; import Vue from 'vue'; -import Flash from '../flash'; +import createFlash from '../flash'; import initIssuableSidebar from '../init_issuable_sidebar'; import './merge_conflict_store'; -import './merge_conflict_service'; -import './mixins/line_conflict_utils'; -import './mixins/line_conflict_actions'; +import MergeConflictsService from './merge_conflict_service'; import './components/diff_file_editor'; import './components/inline_conflict_lines'; import './components/parallel_conflict_lines'; @@ -17,9 +13,9 @@ export default function initMergeConflicts() { const INTERACTIVE_RESOLVE_MODE = 'interactive'; const conflictsEl = document.querySelector('#conflicts'); const mergeConflictsStore = gl.mergeConflicts.mergeConflictsStore; - const mergeConflictsService = new gl.mergeConflicts.mergeConflictsService({ + const mergeConflictsService = new MergeConflictsService({ conflictsPath: conflictsEl.dataset.conflictsPath, - resolveConflictsPath: conflictsEl.dataset.resolveConflictsPath + resolveConflictsPath: conflictsEl.dataset.resolveConflictsPath, }); initIssuableSidebar(); @@ -29,17 +25,26 @@ export default function initMergeConflicts() { components: { 'diff-file-editor': gl.mergeConflicts.diffFileEditor, 'inline-conflict-lines': gl.mergeConflicts.inlineConflictLines, - 'parallel-conflict-lines': gl.mergeConflicts.parallelConflictLines + 'parallel-conflict-lines': gl.mergeConflicts.parallelConflictLines, }, data: mergeConflictsStore.state, computed: { - conflictsCountText() { return mergeConflictsStore.getConflictsCountText(); }, - readyToCommit() { return mergeConflictsStore.isReadyToCommit(); }, - commitButtonText() { return mergeConflictsStore.getCommitButtonText(); }, - showDiffViewTypeSwitcher() { return mergeConflictsStore.fileTextTypePresent(); } + conflictsCountText() { + return mergeConflictsStore.getConflictsCountText(); + }, + readyToCommit() { + return mergeConflictsStore.isReadyToCommit(); + }, + commitButtonText() { + return mergeConflictsStore.getCommitButtonText(); + }, + showDiffViewTypeSwitcher() { + return mergeConflictsStore.fileTextTypePresent(); + }, }, created() { - mergeConflictsService.fetchConflictsData() + mergeConflictsService + .fetchConflictsData() .then(({ data }) => { if (data.type === 'error') { mergeConflictsStore.setFailedRequest(data.message); @@ -87,9 +92,9 @@ export default function initMergeConflicts() { }) .catch(() => { mergeConflictsStore.setSubmitState(false); - new Flash('Failed to save merge conflicts resolutions. Please try again!'); + createFlash('Failed to save merge conflicts resolutions. Please try again!'); }); - } - } + }, + }, }); } diff --git a/app/assets/javascripts/merge_conflicts/mixins/line_conflict_actions.js b/app/assets/javascripts/merge_conflicts/mixins/line_conflict_actions.js index 53e000d7e9e..364ae2b2688 100644 --- a/app/assets/javascripts/merge_conflicts/mixins/line_conflict_actions.js +++ b/app/assets/javascripts/merge_conflicts/mixins/line_conflict_actions.js @@ -1,13 +1,7 @@ -/* eslint-disable no-param-reassign, comma-dangle */ - -((global) => { - global.mergeConflicts = global.mergeConflicts || {}; - - global.mergeConflicts.actions = { - methods: { - handleSelected(file, sectionId, selection) { - gl.mergeConflicts.mergeConflictsStore.handleSelected(file, sectionId, selection); - } - } - }; -})(window.gl || (window.gl = {})); +export default { + methods: { + handleSelected(file, sectionId, selection) { + gl.mergeConflicts.mergeConflictsStore.handleSelected(file, sectionId, selection); + }, + }, +}; diff --git a/app/assets/javascripts/merge_conflicts/mixins/line_conflict_utils.js b/app/assets/javascripts/merge_conflicts/mixins/line_conflict_utils.js index 0f475f62ee6..d25032fb142 100644 --- a/app/assets/javascripts/merge_conflicts/mixins/line_conflict_utils.js +++ b/app/assets/javascripts/merge_conflicts/mixins/line_conflict_utils.js @@ -1,19 +1,13 @@ -/* eslint-disable no-param-reassign, quote-props, comma-dangle */ - -((global) => { - global.mergeConflicts = global.mergeConflicts || {}; - - global.mergeConflicts.utils = { - methods: { - lineCssClass(line) { - return { - 'head': line.isHead, - 'origin': line.isOrigin, - 'match': line.hasMatch, - 'selected': line.isSelected, - 'unselected': line.isUnselected - }; - } - } - }; -})(window.gl || (window.gl = {})); +export default { + methods: { + lineCssClass(line) { + return { + head: line.isHead, + origin: line.isOrigin, + match: line.hasMatch, + selected: line.isSelected, + unselected: line.isUnselected, + }; + }, + }, +}; diff --git a/app/assets/javascripts/merge_request_tabs.js b/app/assets/javascripts/merge_request_tabs.js index 493c119dc6f..65ab41559be 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').toggleClass('hidden', !status); + $('.mr-loading-status .loading').toggleClass('hide', !status); } diffViewType() { diff --git a/app/assets/javascripts/milestone.js b/app/assets/javascripts/milestone.js index 325fa570f37..6da04020881 100644 --- a/app/assets/javascripts/milestone.js +++ b/app/assets/javascripts/milestone.js @@ -18,13 +18,13 @@ export default class Milestone { return $('a[data-toggle="tab"]').on('show.bs.tab', (e) => { const $target = $(e.target); - location.hash = $target.attr('href'); + window.location.hash = $target.attr('href'); this.loadTab($target); }); } // eslint-disable-next-line class-methods-use-this loadInitialTab() { - const $target = $(`.js-milestone-tabs a[href="${location.hash}"]`); + const $target = $(`.js-milestone-tabs a[href="${window.location.hash}"]`); if ($target.length) { $target.tab('show'); diff --git a/app/assets/javascripts/milestone_select.js b/app/assets/javascripts/milestone_select.js index f8b3d3061f0..334279137d8 100644 --- a/app/assets/javascripts/milestone_select.js +++ b/app/assets/javascripts/milestone_select.js @@ -16,10 +16,10 @@ export default class MilestoneSelect { typeof currentProject === 'string' ? JSON.parse(currentProject) : currentProject; } - this.init(els, options); + MilestoneSelect.init(els, options); } - init(els, options) { + static init(els, options) { let $els = $(els); if (!els) { @@ -56,7 +56,7 @@ export default class MilestoneSelect { if (issueUpdateURL) { milestoneLinkTemplate = _.template( - '<a href="/<%- full_path %>/milestones/<%- iid %>" class="bold has-tooltip" data-container="body" title="<%- remaining %>"><%- title %></a>', + '<a href="<%- web_url %>" class="bold has-tooltip" data-container="body" title="<%- remaining %>"><%- title %></a>', ); milestoneLinkNoneTemplate = '<span class="no-value">None</span>'; } @@ -224,7 +224,6 @@ export default class MilestoneSelect { $selectBox.hide(); $value.css('display', ''); if (data.milestone != null) { - data.milestone.full_path = this.currentProject.full_path; data.milestone.remaining = timeFor(data.milestone.due_date); data.milestone.name = data.milestone.title; $value.html(milestoneLinkTemplate(data.milestone)); diff --git a/app/assets/javascripts/monitoring/components/dashboard.vue b/app/assets/javascripts/monitoring/components/dashboard.vue index f5572be5fbf..e1c8b6a6d4a 100644 --- a/app/assets/javascripts/monitoring/components/dashboard.vue +++ b/app/assets/javascripts/monitoring/components/dashboard.vue @@ -139,7 +139,7 @@ export default { this.updateAspectRatio = true; }, toggleAspectRatio() { - this.updatedAspectRatios = this.updatedAspectRatios += 1; + this.updatedAspectRatios += 1; if (this.store.getMetricsCount() === this.updatedAspectRatios) { this.updateAspectRatio = !this.updateAspectRatio; this.updatedAspectRatios = 0; @@ -174,7 +174,10 @@ export default { :tags-path="tagsPath" :show-legend="showLegend" :small-graph="forceSmallGraph" - /> + > + <!-- EE content --> + {{ null }} + </graph> </graph-group> </div> <empty-state diff --git a/app/assets/javascripts/monitoring/components/empty_state.vue b/app/assets/javascripts/monitoring/components/empty_state.vue index c77f451c2d3..82b9a4b1adb 100644 --- a/app/assets/javascripts/monitoring/components/empty_state.vue +++ b/app/assets/javascripts/monitoring/components/empty_state.vue @@ -107,8 +107,8 @@ export default { <div class="state-button"> <a v-if="currentState.buttonPath" - class="btn btn-success" :href="currentState.buttonPath" + class="btn btn-success" > {{ currentState.buttonText }} </a> @@ -116,8 +116,8 @@ export default { <div class="state-button"> <a v-if="currentState.secondaryButtonPath" - class="btn" :href="currentState.secondaryButtonPath" + class="btn" > {{ currentState.secondaryButtonText }} </a> diff --git a/app/assets/javascripts/monitoring/components/graph.vue b/app/assets/javascripts/monitoring/components/graph.vue index de6755e0414..e5680a0499f 100644 --- a/app/assets/javascripts/monitoring/components/graph.vue +++ b/app/assets/javascripts/monitoring/components/graph.vue @@ -154,7 +154,7 @@ export default { point.x = e.clientX; point.y = e.clientY; point = point.matrixTransform(this.$refs.graphData.getScreenCTM().inverse()); - point.x = point.x += 7; + point.x += 7; const firstTimeSeries = this.timeSeries[0]; const timeValueOverlay = firstTimeSeries.timeSeriesScaleX.invert(point.x); const overlayIndex = bisectDate(firstTimeSeries.values, timeValueOverlay, 1); @@ -232,20 +232,25 @@ export default { @mouseover="showFlagContent = true" @mouseleave="showFlagContent = false" > - <h5 class="text-center graph-title"> - {{ graphData.title }} - </h5> + <div class="prometheus-graph-header"> + <h5 class="prometheus-graph-title"> + {{ graphData.title }} + </h5> + <div class="prometheus-graph-widgets"> + <slot></slot> + </div> + </div> <div - class="prometheus-svg-container" :style="paddingBottomRootSvg" + class="prometheus-svg-container" > <svg - :viewBox="outerViewBox" ref="baseSvg" + :viewBox="outerViewBox" > <g - class="x-axis" :transform="axisTransform" + class="x-axis" /> <g class="y-axis" @@ -260,9 +265,9 @@ export default { :unit-of-display="unitOfDisplay" /> <svg - class="graph-data" - :viewBox="innerViewBox" ref="graphData" + :viewBox="innerViewBox" + class="graph-data" > <graph-path v-for="(path, index) in timeSeries" @@ -282,11 +287,11 @@ export default { :graph-height-offset="graphHeightOffset" /> <rect - class="prometheus-graph-overlay" + ref="graphOverlay" :width="(graphWidth - 70)" :height="(graphHeight - 100)" + class="prometheus-graph-overlay" transform="translate(-5, 20)" - ref="graphOverlay" @mousemove="handleMouseOverGraph($event)" /> </svg> diff --git a/app/assets/javascripts/monitoring/components/graph/axis.vue b/app/assets/javascripts/monitoring/components/graph/axis.vue index fc4b3689dfd..8a604a51eb2 100644 --- a/app/assets/javascripts/monitoring/components/graph/axis.vue +++ b/app/assets/javascripts/monitoring/components/graph/axis.vue @@ -92,48 +92,48 @@ export default { <template> <g class="axis-label-container"> <line + :y1="yPosition" + :x2="graphWidth + 20" + :y2="yPosition" class="label-x-axis-line" stroke="#000000" stroke-width="1" x1="10" - :y1="yPosition" - :x2="graphWidth + 20" - :y2="yPosition" /> <line + :x2="10" + :y2="yPosition" class="label-y-axis-line" stroke="#000000" stroke-width="1" x1="10" y1="0" - :x2="10" - :y2="yPosition" /> <rect - class="rect-axis-text" :transform="rectTransform" :width="yLabelWidth" :height="yLabelHeight" + class="rect-axis-text" /> <text + ref="ylabel" + :transform="textTransform" class="label-axis-text y-label-text" text-anchor="middle" - :transform="textTransform" - ref="ylabel" > {{ yAxisLabelSentenceCase }} </text> <rect - class="rect-axis-text" :x="xPosition + 60" :y="graphHeight - 80" + class="rect-axis-text" width="35" height="50" /> <text - class="label-axis-text x-label-text" :x="xPosition + 60" :y="yPosition" + class="label-axis-text x-label-text" dy=".35em" > {{ timeString }} diff --git a/app/assets/javascripts/monitoring/components/graph/deployment.vue b/app/assets/javascripts/monitoring/components/graph/deployment.vue index 4012191ceb9..a7289ed53e8 100644 --- a/app/assets/javascripts/monitoring/components/graph/deployment.vue +++ b/app/assets/javascripts/monitoring/components/graph/deployment.vue @@ -33,18 +33,18 @@ export default { :key="index" :transform="transformDeploymentGroup(deployment)"> <rect + :height="calculatedHeight" x="0" y="0" - :height="calculatedHeight" width="3" fill="url(#shadow-gradient)" /> <line + :y2="calculatedHeight" class="deployment-line" x1="0" y1="0" x2="0" - :y2="calculatedHeight" stroke="#000" /> </g> diff --git a/app/assets/javascripts/monitoring/components/graph/flag.vue b/app/assets/javascripts/monitoring/components/graph/flag.vue index 8a771107de8..92fe98508ad 100644 --- a/app/assets/javascripts/monitoring/components/graph/flag.vue +++ b/app/assets/javascripts/monitoring/components/graph/flag.vue @@ -97,7 +97,7 @@ export default { ? this.deploymentFlagData.seriesIndex : indexFromCoordinates; const value = series.values[index] && series.values[index].value; - if (isNaN(value)) { + if (Number.isNaN(value)) { return '-'; } return `${formatRelevantDigits(value)}${this.unitOfDisplay}`; @@ -117,13 +117,13 @@ export default { <template> <div - class="prometheus-graph-cursor" :style="cursorStyle" + class="prometheus-graph-cursor" > <div v-if="showFlagContent" - class="prometheus-graph-flag popover" :class="flagOrientation" + class="prometheus-graph-flag popover" > <div class="arrow"></div> <div class="popover-title"> @@ -139,8 +139,8 @@ export default { > <div> <icon - name="commit" :size="12" + name="commit" /> <a :href="deploymentFlagData.commitUrl"> {{ deploymentFlagData.sha.slice(0, 8) }} @@ -150,8 +150,8 @@ export default { v-if="deploymentFlagData.tag" > <icon - name="label" :size="12" + name="label" /> <a :href="deploymentFlagData.tagUrl"> {{ deploymentFlagData.ref }} diff --git a/app/assets/javascripts/monitoring/components/graph/legend.vue b/app/assets/javascripts/monitoring/components/graph/legend.vue index da9280cf1f1..3276f3a1ceb 100644 --- a/app/assets/javascripts/monitoring/components/graph/legend.vue +++ b/app/assets/javascripts/monitoring/components/graph/legend.vue @@ -31,8 +31,8 @@ export default { <table class="prometheus-table"> <tr v-for="(series, index) in timeSeries" - :key="index" v-if="series.shouldRenderLegend" + :key="index" :class="isStable(series)" > <td> @@ -40,11 +40,11 @@ export default { </td> <track-line :track="series" /> <td - class="legend-metric-title" - v-if="timeSeries.length > 1"> + v-if="timeSeries.length > 1" + class="legend-metric-title"> <track-info - :track="series" - v-if="series.metricTag" /> + v-if="series.metricTag" + :track="series" /> <track-info v-else :track="series"> @@ -62,8 +62,8 @@ export default { :key="`track-line-${trackIndex}`"/> <td :key="`track-info-${trackIndex}`"> <track-info - class="legend-metric-title" - :track="track" /> + :track="track" + class="legend-metric-title" /> </td> </template> </tr> diff --git a/app/assets/javascripts/monitoring/components/graph/path.vue b/app/assets/javascripts/monitoring/components/graph/path.vue index 52f8aa2ee3f..a9b7ce586ce 100644 --- a/app/assets/javascripts/monitoring/components/graph/path.vue +++ b/app/assets/javascripts/monitoring/components/graph/path.vue @@ -44,26 +44,26 @@ export default { <template> <g transform="translate(-5, 20)"> <circle - class="circle-path" + v-if="showDot" :cx="currentCoordinates.currentX" :cy="currentCoordinates.currentY" :fill="lineColor" :stroke="lineColor" + class="circle-path" r="3" - v-if="showDot" /> <path - class="metric-area" :d="generatedAreaPath" :fill="areaColor" + class="metric-area" /> <path - class="metric-line" :d="generatedLinePath" :stroke="lineColor" + :stroke-dasharray="strokeDashArray" + class="metric-line" fill="none" stroke-width="1" - :stroke-dasharray="strokeDashArray" /> </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 18be65fd1ef..ba3f93b39ff 100644 --- a/app/assets/javascripts/monitoring/components/graph/track_line.vue +++ b/app/assets/javascripts/monitoring/components/graph/track_line.vue @@ -24,11 +24,11 @@ export default { <line :stroke-dasharray="stylizedLine" :stroke="track.lineColor" - stroke-width="4" :x1="0" :x2="16" :y1="4" :y2="4" + stroke-width="4" /> </svg> </td> diff --git a/app/assets/javascripts/monitoring/utils/multiple_time_series.js b/app/assets/javascripts/monitoring/utils/multiple_time_series.js index 4d3f1f1a7cc..ed3a27dd68b 100644 --- a/app/assets/javascripts/monitoring/utils/multiple_time_series.js +++ b/app/assets/javascripts/monitoring/utils/multiple_time_series.js @@ -73,7 +73,7 @@ function queryTimeSeries(query, graphWidth, graphHeight, graphHeightOffset, xDom timeSeriesScaleX.ticks(d3.timeMinute, 60); timeSeriesScaleY.domain(yDom); - const defined = d => !isNaN(d.value) && d.value != null; + const defined = d => !Number.isNaN(d.value) && d.value != null; const lineFunction = d3 .line() diff --git a/app/assets/javascripts/network/branch_graph.js b/app/assets/javascripts/network/branch_graph.js index bd007c707f2..e4096ddb00d 100644 --- a/app/assets/javascripts/network/branch_graph.js +++ b/app/assets/javascripts/network/branch_graph.js @@ -112,6 +112,8 @@ export default (function() { fill: "#444" }); ref = this.days; + + // eslint-disable-next-line no-multi-assign for (mm = j = 0, len = ref.length; j < len; mm = (j += 1)) { day = ref[mm]; if (cuday !== day[0] || cumonth !== day[1]) { @@ -285,6 +287,8 @@ export default (function() { r = this.r; ref = commit.parents; results = []; + + // eslint-disable-next-line no-multi-assign for (i = j = 0, len = ref.length; j < len; i = (j += 1)) { parent = ref[i]; parentCommit = this.preparedCommits[parent[0]]; diff --git a/app/assets/javascripts/notebook/cells/code.vue b/app/assets/javascripts/notebook/cells/code.vue index b4067d229aa..18cef82cec0 100644 --- a/app/assets/javascripts/notebook/cells/code.vue +++ b/app/assets/javascripts/notebook/cells/code.vue @@ -39,10 +39,10 @@ export default { <template> <div class="cell"> <code-cell - type="input" :raw-code="rawInputCode" :count="cell.execution_count" - :code-css-class="codeCssClass" /> + :code-css-class="codeCssClass" + type="input" /> <output-cell v-if="hasOutput" :count="cell.execution_count" diff --git a/app/assets/javascripts/notebook/cells/code/index.vue b/app/assets/javascripts/notebook/cells/code/index.vue index 0f3083f05b2..7d2a1a33b98 100644 --- a/app/assets/javascripts/notebook/cells/code/index.vue +++ b/app/assets/javascripts/notebook/cells/code/index.vue @@ -48,9 +48,9 @@ :type="promptType" :count="count" /> <pre - class="language-python" - :class="codeCssClass" ref="code" + :class="codeCssClass" + class="language-python" v-text="code"> </pre> </div> diff --git a/app/assets/javascripts/notebook/cells/output/index.vue b/app/assets/javascripts/notebook/cells/output/index.vue index 91b2269a83a..4183b976814 100644 --- a/app/assets/javascripts/notebook/cells/output/index.vue +++ b/app/assets/javascripts/notebook/cells/output/index.vue @@ -78,10 +78,10 @@ <template> <component :is="componentName" - type="output" :output-type="outputType" :count="count" :raw-code="rawCode" :code-css-class="codeCssClass" + type="output" /> </template> diff --git a/app/assets/javascripts/notes.js b/app/assets/javascripts/notes.js index b2c1a26bbae..27c5dedcf0b 100644 --- a/app/assets/javascripts/notes.js +++ b/app/assets/javascripts/notes.js @@ -315,7 +315,7 @@ export default class Notes { if (discussionNoteForm.length) { if ($textarea.val() !== '') { if ( - !confirm('Are you sure you want to cancel creating this comment?') + !window.confirm('Are you sure you want to cancel creating this comment?') ) { return; } @@ -329,7 +329,7 @@ export default class Notes { newText = $textarea.val(); if (originalText !== newText) { if ( - !confirm('Are you sure you want to cancel editing this comment?') + !window.confirm('Are you sure you want to cancel editing this comment?') ) { return; } @@ -1675,7 +1675,7 @@ export default class Notes { <div class="note-header"> <div class="note-header-info"> <a href="/${_.escape(currentUsername)}"> - <span class="d-none d-sm-block">${_.escape( + <span class="d-none d-sm-inline-block">${_.escape( currentUsername, )}</span> <span class="note-headline-light">${_.escape( @@ -1694,7 +1694,7 @@ export default class Notes { </li>`, ); - $tempNote.find('.d-none.d-sm-block').text(_.escape(currentUserFullname)); + $tempNote.find('.d-none.d-sm-inline-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 72d7e22fba0..ad6dd3d9a09 100644 --- a/app/assets/javascripts/notes/components/comment_form.vue +++ b/app/assets/javascripts/notes/components/comment_form.vue @@ -312,8 +312,8 @@ Please check your network connection and try again.`; <div> <note-signed-out-widget v-if="!isLoggedIn" /> <discussion-locked-widget - issuable-type="issue" v-else-if="isLocked(getNoteableData) && !canCreateNote" + issuable-type="issue" /> <ul v-else-if="canCreateNote" @@ -345,23 +345,23 @@ Please check your network connection and try again.`; /> <markdown-field + ref="markdownField" :markdown-preview-path="markdownPreviewPath" :markdown-docs-path="markdownDocsPath" :quick-actions-docs-path="quickActionsDocsPath" - :add-spacing-classes="false" - ref="markdownField"> + :add-spacing-classes="false"> <textarea id="note-body" + ref="textarea" + slot="textarea" + v-model="note" + :disabled="isSubmitting" name="note[note]" class="note-textarea js-vue-comment-form js-gfm-input js-autosize markdown-area js-vue-textarea" data-supports-quick-actions="true" aria-label="Description" - v-model="note" - ref="textarea" - slot="textarea" - :disabled="isSubmitting" - placeholder="Write a comment or drag your files here..." + placeholder="Write a comment or drag your files here…" @keydown.up="editCurrentUserLastNote()" @keydown.meta.enter="handleSave()" @keydown.ctrl.enter="handleSave()"> @@ -372,10 +372,10 @@ js-gfm-input js-autosize markdown-area js-vue-textarea" class="float-left btn-group append-right-10 comment-type-dropdown js-comment-type-dropdown droplab-dropdown"> <button - @click.prevent="handleSave()" :disabled="isSubmitButtonDisabled" class="btn btn-create comment-btn js-comment-button js-comment-submit-button" - type="submit"> + type="submit" + @click.prevent="handleSave()"> {{ __(commentButtonTitle) }} </button> <button @@ -434,20 +434,20 @@ append-right-10 comment-type-dropdown js-comment-type-dropdown droplab-dropdown" <loading-button v-if="canUpdateIssue" :loading="isToggleStateButtonLoading" - @click="handleSave(true)" :container-class="[ actionButtonClassNames, 'btn btn-comment btn-comment-and-close js-action-button' ]" :disabled="isToggleStateButtonLoading || isSubmitting" :label="issueActionButtonTitle" + @click="handleSave(true)" /> <button - type="button" v-if="note.length" - @click="discard" - class="btn btn-cancel js-note-discard"> + type="button" + class="btn btn-cancel js-note-discard" + @click="discard"> Discard draft </button> </div> diff --git a/app/assets/javascripts/notes/components/diff_file_header.vue b/app/assets/javascripts/notes/components/diff_file_header.vue index 94d9dc69964..fc7b52be241 100644 --- a/app/assets/javascripts/notes/components/diff_file_header.vue +++ b/app/assets/javascripts/notes/components/diff_file_header.vue @@ -29,12 +29,12 @@ export default { <span> <icon name="archive" /> <strong - v-html="diffFile.submoduleLink" class="file-title-name" + v-html="diffFile.submoduleLink" ></strong> <clipboard-button - title="Copy file path to clipboard" :text="diffFile.submoduleLink" + title="Copy file path to clipboard" css-class="btn-default btn-transparent btn-clipboard" /> </span> @@ -48,16 +48,16 @@ export default { <span v-html="diffFile.blobIcon"></span> <span v-if="diffFile.renamedFile"> <strong - class="file-title-name has-tooltip" :title="diffFile.oldPath" + class="file-title-name has-tooltip" data-container="body" > {{ diffFile.oldPath }} </strong> → <strong - class="file-title-name has-tooltip" :title="diffFile.newPath" + class="file-title-name has-tooltip" data-container="body" > {{ diffFile.newPath }} @@ -66,8 +66,8 @@ export default { <strong v-else - class="file-title-name has-tooltip" :title="diffFile.oldPath" + class="file-title-name has-tooltip" data-container="body" > {{ diffFile.filePath }} @@ -78,8 +78,8 @@ export default { </component> <clipboard-button - title="Copy file path to clipboard" :text="diffFile.filePath" + title="Copy file path to clipboard" css-class="btn-default btn-transparent btn-clipboard" /> diff --git a/app/assets/javascripts/notes/components/diff_with_note.vue b/app/assets/javascripts/notes/components/diff_with_note.vue index ee01ec85bbb..cafb28910eb 100644 --- a/app/assets/javascripts/notes/components/diff_with_note.vue +++ b/app/assets/javascripts/notes/components/diff_with_note.vue @@ -60,8 +60,8 @@ export default { <template> <div ref="fileHolder" - class="diff-file file-holder" :class="diffFileClass" + class="diff-file file-holder" > <div class="js-file-title file-title file-title-flex-parent"> <diff-file-header @@ -74,11 +74,11 @@ export default { > <table> <component + v-for="(html, index) in diffRows" :is="rowTag(html)" :class="html.className" - v-for="(html, index) in diffRows" - v-html="html.outerHTML" :key="index" + v-html="html.outerHTML" /> <tr class="notes_holder"> <td diff --git a/app/assets/javascripts/notes/components/discussion_counter.vue b/app/assets/javascripts/notes/components/discussion_counter.vue index cbe4774a360..68e17ac8055 100644 --- a/app/assets/javascripts/notes/components/discussion_counter.vue +++ b/app/assets/javascripts/notes/components/discussion_counter.vue @@ -95,8 +95,8 @@ export default { class="btn-group" role="group"> <a - :href="resolveAllDiscussionsIssuePath" v-tooltip + :href="resolveAllDiscussionsIssuePath" title="Resolve all discussions in new issue" data-container="body" class="new-issue-for-discussion btn btn-default discussion-create-issue-btn"> @@ -108,11 +108,11 @@ export default { class="btn-group" role="group"> <button - @click="jumpToFirstDiscussion" v-tooltip title="Jump to first unresolved discussion" data-container="body" - class="btn btn-default discussion-next-btn"> + class="btn btn-default discussion-next-btn" + @click="jumpToFirstDiscussion"> <span v-html="nextDiscussionSvg"></span> </button> </div> diff --git a/app/assets/javascripts/notes/components/discussion_locked_widget.vue b/app/assets/javascripts/notes/components/discussion_locked_widget.vue index 13283b187d1..de0a5f8489b 100644 --- a/app/assets/javascripts/notes/components/discussion_locked_widget.vue +++ b/app/assets/javascripts/notes/components/discussion_locked_widget.vue @@ -14,8 +14,8 @@ export default { <div class="disabled-comment text-center"> <span class="issuable-note-warning inline"> <icon - name="lock" :size="16" + name="lock" class="icon" /> <span> diff --git a/app/assets/javascripts/notes/components/note_actions.vue b/app/assets/javascripts/notes/components/note_actions.vue index 626b0799581..0bf4258a257 100644 --- a/app/assets/javascripts/notes/components/note_actions.vue +++ b/app/assets/javascripts/notes/components/note_actions.vue @@ -129,12 +129,12 @@ export default { class="note-actions-item"> <button v-tooltip - @click="onResolve" :class="{ 'is-disabled': !resolvable, 'is-active': isResolved }" :title="resolveButtonTitle" :aria-label="resolveButtonTitle" type="button" - class="line-resolve-btn note-action-button"> + class="line-resolve-btn note-action-button" + @click="onResolve"> <template v-if="!isResolving"> <div v-if="isResolved" @@ -164,16 +164,16 @@ export default { > <loading-icon :inline="true" /> <span - v-html="emojiSmiling" - class="link-highlight award-control-icon-neutral"> + class="link-highlight award-control-icon-neutral" + v-html="emojiSmiling"> </span> <span - v-html="emojiSmiley" - class="link-highlight award-control-icon-positive"> + class="link-highlight award-control-icon-positive" + v-html="emojiSmiley"> </span> <span - v-html="emojiSmile" - class="link-highlight award-control-icon-super-positive"> + class="link-highlight award-control-icon-super-positive" + v-html="emojiSmile"> </span> </a> </div> @@ -181,16 +181,16 @@ export default { v-if="canEdit" class="note-actions-item"> <button - @click="onEdit" v-tooltip type="button" title="Edit comment" class="note-action-button js-note-edit btn btn-transparent" data-container="body" - data-placement="bottom"> + data-placement="bottom" + @click="onEdit"> <span - v-html="editSvg" - class="link-highlight"> + class="link-highlight" + v-html="editSvg"> </span> </button> </div> @@ -218,9 +218,9 @@ export default { </li> <li v-if="canEdit"> <button - @click.prevent="onDelete" class="btn btn-transparent js-note-delete js-note-delete" - type="button"> + type="button" + @click.prevent="onDelete"> <span class="text-danger"> Delete comment </span> diff --git a/app/assets/javascripts/notes/components/note_awards_list.vue b/app/assets/javascripts/notes/components/note_awards_list.vue index e8fd155a1ee..521b4d16286 100644 --- a/app/assets/javascripts/notes/components/note_awards_list.vue +++ b/app/assets/javascripts/notes/components/note_awards_list.vue @@ -199,10 +199,10 @@ export default { :key="index" :class="getAwardClassBindings(awardList, awardName)" :title="awardTitle(awardList)" - @click="handleAward(awardName)" class="btn award-control" data-placement="bottom" - type="button"> + type="button" + @click="handleAward(awardName)"> <span v-html="getAwardHTML(awardName)"></span> <span class="award-control-text js-counter"> {{ awardList.length }} @@ -220,16 +220,16 @@ export default { data-placement="bottom" type="button"> <span - v-html="emojiSmiling" - class="award-control-icon award-control-icon-neutral"> + class="award-control-icon award-control-icon-neutral" + v-html="emojiSmiling"> </span> <span - v-html="emojiSmiley" - class="award-control-icon award-control-icon-positive"> + class="award-control-icon award-control-icon-positive" + v-html="emojiSmiley"> </span> <span - v-html="emojiSmile" - class="award-control-icon award-control-icon-super-positive"> + class="award-control-icon award-control-icon-super-positive" + v-html="emojiSmile"> </span> <i aria-hidden="true" diff --git a/app/assets/javascripts/notes/components/note_body.vue b/app/assets/javascripts/notes/components/note_body.vue index 0cb626c14f4..864edcd2ec6 100644 --- a/app/assets/javascripts/notes/components/note_body.vue +++ b/app/assets/javascripts/notes/components/note_body.vue @@ -80,20 +80,20 @@ export default { <template> <div - :class="{ 'js-task-list-container': canEdit }" ref="note-body" + :class="{ 'js-task-list-container': canEdit }" class="note-body"> <div - v-html="note.note_html" - class="note-text md"></div> + class="note-text md" + v-html="note.note_html"></div> <note-form v-if="isEditing" ref="noteForm" - @handleFormUpdate="handleFormUpdate" - @cancelFormEdition="formCancelHandler" :is-editing="isEditing" :note-body="noteBody" :note-id="note.id" + @handleFormUpdate="handleFormUpdate" + @cancelFormEdition="formCancelHandler" /> <textarea v-if="canEdit" diff --git a/app/assets/javascripts/notes/components/note_form.vue b/app/assets/javascripts/notes/components/note_form.vue index c59a2e7a406..7254ef3357d 100644 --- a/app/assets/javascripts/notes/components/note_form.vue +++ b/app/assets/javascripts/notes/components/note_form.vue @@ -165,15 +165,15 @@ export default { :add-spacing-classes="false"> <textarea id="note_note" + ref="textarea" + slot="textarea" + :data-supports-quick-actions="!isEditing" + v-model="updatedNoteBody" name="note[note]" class="note-textarea js-gfm-input js-autosize markdown-area js-vue-issue-note-form js-vue-textarea" - :data-supports-quick-actions="!isEditing" aria-label="Description" - v-model="updatedNoteBody" - ref="textarea" - slot="textarea" - placeholder="Write a comment or drag your files here..." + placeholder="Write a comment or drag your files here…" @keydown.meta.enter="handleUpdate()" @keydown.ctrl.enter="handleUpdate()" @keydown.up="editMyLastNote()" @@ -182,23 +182,23 @@ js-autosize markdown-area js-vue-issue-note-form js-vue-textarea" </markdown-field> <div class="note-form-actions clearfix"> <button - type="button" - @click="handleUpdate()" :disabled="isDisabled" - class="js-vue-issue-save btn btn-save"> + type="button" + class="js-vue-issue-save btn btn-save" + @click="handleUpdate()"> {{ saveButtonTitle }} </button> <button v-if="note.resolvable" - @click.prevent="handleUpdate(true)" class="btn btn-nr btn-default append-right-10 js-comment-resolve-button" + @click.prevent="handleUpdate(true)" > {{ resolveButtonTitle }} </button> <button - @click="cancelHandler()" class="btn btn-cancel note-edit-cancel" - type="button"> + type="button" + @click="cancelHandler()"> Cancel </button> </div> diff --git a/app/assets/javascripts/notes/components/note_header.vue b/app/assets/javascripts/notes/components/note_header.vue index a4081957207..ffe3ba9c805 100644 --- a/app/assets/javascripts/notes/components/note_header.vue +++ b/app/assets/javascripts/notes/components/note_header.vue @@ -66,9 +66,9 @@ export default { v-if="includeToggle" class="discussion-actions"> <button - @click="handleToggle" class="note-action-button discussion-toggle-button js-vue-toggle-button" - type="button"> + type="button" + @click="handleToggle"> <i :class="toggleChevronClass" class="fa" @@ -90,16 +90,16 @@ export default { </template> <span v-if="actionTextHtml" - v-html="actionTextHtml" - class="system-note-message"> + class="system-note-message" + v-html="actionTextHtml"> </span> <span class="system-note-separator"> · </span> <a :href="noteTimestampLink" - @click="updateTargetNoteHash" - class="note-timestamp system-note-separator"> + class="note-timestamp system-note-separator" + @click="updateTargetNoteHash"> <time-ago-tooltip :time="createdAt" tooltip-placement="bottom" diff --git a/app/assets/javascripts/notes/components/noteable_discussion.vue b/app/assets/javascripts/notes/components/noteable_discussion.vue index 7f5aacaa3a2..3f865431155 100644 --- a/app/assets/javascripts/notes/components/noteable_discussion.vue +++ b/app/assets/javascripts/notes/components/noteable_discussion.vue @@ -152,7 +152,7 @@ export default { const msg = 'Are you sure you want to cancel creating this comment?'; // eslint-disable-next-line no-alert - if (!confirm(msg)) { + if (!window.confirm(msg)) { return; } } @@ -229,9 +229,9 @@ Please check your network connection and try again.`; :note-id="discussion.id" :include-toggle="true" :expanded="note.expanded" - @toggleHandler="toggleDiscussionHandler" action-text="started a discussion" class="discussion" + @toggleHandler="toggleDiscussionHandler" /> <note-edited-text v-if="lastUpdatedAt" @@ -269,19 +269,19 @@ Please check your network connection and try again.`; class="btn-group" role="group"> <button - @click="showReplyForm" type="button" class="js-vue-discussion-reply btn btn-text-field" - title="Add a reply">Reply...</button> + title="Add a reply" + @click="showReplyForm">Reply...</button> </div> <div v-if="note.resolvable" class="btn-group" role="group"> <button - @click="resolveHandler()" type="button" class="btn btn-default" + @click="resolveHandler()" > <i v-if="isResolving" @@ -301,8 +301,8 @@ Please check your network connection and try again.`; class="btn-group" role="group"> <a - :href="note.resolve_with_issue_path" v-tooltip + :href="note.resolve_with_issue_path" class="new-issue-for-discussion btn btn-default discussion-create-issue-btn" title="Resolve this discussion in a new issue" @@ -316,11 +316,11 @@ Please check your network connection and try again.`; class="btn-group" role="group"> <button - @click="jumpToDiscussion" v-tooltip class="btn btn-default discussion-next-btn" title="Jump to next unresolved discussion" data-container="body" + @click="jumpToDiscussion" > <span v-html="nextDiscussionsSvg"></span> </button> @@ -330,12 +330,12 @@ Please check your network connection and try again.`; </template> <note-form v-if="isReplying" - save-button-title="Comment" + ref="noteForm" :note="note" :is-editing="false" + save-button-title="Comment" @handleFormUpdate="saveReply" - @cancelFormEdition="cancelReplyForm" - ref="noteForm" /> + @cancelFormEdition="cancelReplyForm" /> <note-signed-out-widget v-if="!canReply" /> </div> </div> diff --git a/app/assets/javascripts/notes/components/noteable_note.vue b/app/assets/javascripts/notes/components/noteable_note.vue index 566f5c68e66..713f93456b1 100644 --- a/app/assets/javascripts/notes/components/noteable_note.vue +++ b/app/assets/javascripts/notes/components/noteable_note.vue @@ -77,7 +77,7 @@ export default { }, deleteHandler() { // eslint-disable-next-line no-alert - if (confirm('Are you sure you want to delete this comment?')) { + if (window.confirm('Are you sure you want to delete this comment?')) { this.isDeleting = true; this.deleteNote(this.note) @@ -129,7 +129,7 @@ export default { formCancelHandler(shouldConfirm, isDirty) { if (shouldConfirm && isDirty) { // eslint-disable-next-line no-alert - if (!confirm('Are you sure you want to cancel editing this comment?')) + if (!window.confirm('Are you sure you want to cancel editing this comment?')) return; } this.$refs.noteBody.resetAutoSave(); @@ -151,10 +151,10 @@ export default { <template> <li - class="note timeline-entry" :id="noteAnchorId" :class="classNameBindings" - :data-award-url="note.toggle_award_path"> + :data-award-url="note.toggle_award_path" + class="note timeline-entry"> <div class="timeline-entry-inner"> <div class="timeline-icon"> <user-avatar-link @@ -191,12 +191,12 @@ export default { /> </div> <note-body + ref="noteBody" :note="note" :can-edit="note.current_user.can_edit" :is-editing="isEditing" @handleFormUpdate="formUpdateHandler" @cancelFormEdition="formCancelHandler" - ref="noteBody" /> </div> </div> diff --git a/app/assets/javascripts/notes/constants.js b/app/assets/javascripts/notes/constants.js index c4de4826eda..5b5b1e89058 100644 --- a/app/assets/javascripts/notes/constants.js +++ b/app/assets/javascripts/notes/constants.js @@ -14,6 +14,7 @@ export const EPIC_NOTEABLE_TYPE = 'epic'; export const MERGE_REQUEST_NOTEABLE_TYPE = 'merge_request'; export const UNRESOLVE_NOTE_METHOD_NAME = 'delete'; export const RESOLVE_NOTE_METHOD_NAME = 'post'; +export const DESCRIPTION_TYPE = 'changed the description'; export const NOTEABLE_TYPE_MAPPING = { Issue: ISSUE_NOTEABLE_TYPE, diff --git a/app/assets/javascripts/notes/stores/collapse_utils.js b/app/assets/javascripts/notes/stores/collapse_utils.js new file mode 100644 index 00000000000..fa4a1c56b20 --- /dev/null +++ b/app/assets/javascripts/notes/stores/collapse_utils.js @@ -0,0 +1,108 @@ +import { n__, s__, sprintf } from '~/locale'; +import { DESCRIPTION_TYPE } from '../constants'; + +/** + * Changes the description from a note, returns 'changed the description n number of times' + */ +export const changeDescriptionNote = (note, descriptionChangedTimes, timeDifferenceMinutes) => { + const descriptionNote = Object.assign({}, note); + + descriptionNote.note_html = sprintf( + s__(`MergeRequest| + %{paragraphStart}changed the description %{descriptionChangedTimes} times %{timeDifferenceMinutes}%{paragraphEnd}`), + { + paragraphStart: '<p dir="auto">', + paragraphEnd: '</p>', + descriptionChangedTimes, + timeDifferenceMinutes: n__('within %d minute ', 'within %d minutes ', timeDifferenceMinutes), + }, + false, + ); + + descriptionNote.times_updated = descriptionChangedTimes; + + return descriptionNote; +}; + +/** + * Checks the time difference between two notes from their 'created_at' dates + * returns an integer + */ + +export const getTimeDifferenceMinutes = (noteBeggining, noteEnd) => { + const descriptionNoteBegin = new Date(noteBeggining.created_at); + const descriptionNoteEnd = new Date(noteEnd.created_at); + const timeDifferenceMinutes = (descriptionNoteEnd - descriptionNoteBegin) / 1000 / 60; + + return Math.ceil(timeDifferenceMinutes); +}; + +/** + * Checks if a note is a system note and if the content is description + * + * @param {Object} note + * @returns {Boolean} + */ +export const isDescriptionSystemNote = note => note.system && note.note === DESCRIPTION_TYPE; + +/** + * Collapses the system notes of a description type, e.g. Changed the description, n minutes ago + * the notes will collapse as long as they happen no more than 10 minutes away from each away + * in between the notes can be anything, another type of system note + * (such as 'changed the weight') or a comment. + * + * @param {Array} notes + * @returns {Array} + */ +export const collapseSystemNotes = notes => { + let lastDescriptionSystemNote = null; + let lastDescriptionSystemNoteIndex = -1; + let descriptionChangedTimes = 1; + + return notes.slice(0).reduce((acc, currentNote) => { + const note = currentNote.notes[0]; + + if (isDescriptionSystemNote(note)) { + // is it the first one? + if (!lastDescriptionSystemNote) { + lastDescriptionSystemNote = note; + lastDescriptionSystemNoteIndex = acc.length; + } else if (lastDescriptionSystemNote) { + const timeDifferenceMinutes = getTimeDifferenceMinutes( + lastDescriptionSystemNote, + note, + ); + + // are they less than 10 minutes appart? + if (timeDifferenceMinutes > 10) { + // reset counter + descriptionChangedTimes = 1; + // update the previous system note + lastDescriptionSystemNote = note; + lastDescriptionSystemNoteIndex = acc.length; + } else { + // increase counter + descriptionChangedTimes += 1; + + // delete the previous one + acc.splice(lastDescriptionSystemNoteIndex, 1); + + // replace the text of the current system note with the collapsed note. + currentNote.notes.splice( + 0, + 1, + changeDescriptionNote(note, descriptionChangedTimes, timeDifferenceMinutes), + ); + + // update the previous system note index + lastDescriptionSystemNoteIndex = acc.length; + } + } + } + acc.push(currentNote); + return acc; + }, []); +}; + +// for babel-rewire +export default {}; diff --git a/app/assets/javascripts/notes/stores/getters.js b/app/assets/javascripts/notes/stores/getters.js index 787be6f4c99..bc373e0d0fc 100644 --- a/app/assets/javascripts/notes/stores/getters.js +++ b/app/assets/javascripts/notes/stores/getters.js @@ -1,6 +1,8 @@ import _ from 'underscore'; +import { collapseSystemNotes } from './collapse_utils'; + +export const notes = state => collapseSystemNotes(state.notes); -export const notes = state => state.notes; export const targetNoteHash = state => state.targetNoteHash; export const getNotesData = state => state.notesData; diff --git a/app/assets/javascripts/pages/admin/jobs/index/components/stop_jobs_modal.vue b/app/assets/javascripts/pages/admin/jobs/index/components/stop_jobs_modal.vue index ba1d8e4d8db..bc84666779e 100644 --- a/app/assets/javascripts/pages/admin/jobs/index/components/stop_jobs_modal.vue +++ b/app/assets/javascripts/pages/admin/jobs/index/components/stop_jobs_modal.vue @@ -40,8 +40,8 @@ <gl-modal id="stop-jobs-modal" :header-title-text="s__('AdminArea|Stop all jobs?')" - footer-primary-button-variant="danger" :footer-primary-button-text="s__('AdminArea|Stop jobs')" + footer-primary-button-variant="danger" @submit="onSubmit" > {{ text }} diff --git a/app/assets/javascripts/pages/admin/projects/index/components/delete_project_modal.vue b/app/assets/javascripts/pages/admin/projects/index/components/delete_project_modal.vue index 343c65edb37..ff66d3a8ac4 100644 --- a/app/assets/javascripts/pages/admin/projects/index/components/delete_project_modal.vue +++ b/app/assets/javascripts/pages/admin/projects/index/components/delete_project_modal.vue @@ -83,9 +83,9 @@ id="delete-project-modal" :title="title" :text="text" - kind="danger" :primary-button-label="primaryButtonLabel" :submit-disabled="!canSubmit" + kind="danger" @submit="onSubmit" @cancel="onCancel" > @@ -107,15 +107,15 @@ value="delete" /> <input + :value="csrfToken" type="hidden" name="authenticity_token" - :value="csrfToken" /> <input + v-model="enteredProjectName" name="projectName" class="form-control" type="text" - v-model="enteredProjectName" aria-labelledby="input-label" autocomplete="off" /> 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 9ce176744ba..cc2805a1901 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 @@ -116,10 +116,10 @@ id="delete-user-modal" :title="title" :text="text" - kind="danger" :primary-button-label="primaryButtonLabel" :secondary-button-label="secondaryButtonLabel" :submit-disabled="!canSubmit" + kind="danger" @submit="onSubmit" @cancel="onCancel" > @@ -141,15 +141,15 @@ value="delete" /> <input + :value="csrfToken" type="hidden" name="authenticity_token" - :value="csrfToken" /> <input + v-model="enteredUsername" type="text" name="username" class="form-control" - v-model="enteredUsername" aria-labelledby="input-label" autocomplete="off" /> @@ -160,11 +160,11 @@ slot-scope="props" > <button + :disabled="!canSubmit" type="button" class="btn js-secondary-button btn-warning" - :disabled="!canSubmit" - @click="onSecondaryAction" data-dismiss="modal" + @click="onSecondaryAction" > {{ secondaryButtonLabel }} </button> diff --git a/app/assets/javascripts/pages/milestones/shared/components/delete_milestone_modal.vue b/app/assets/javascripts/pages/milestones/shared/components/delete_milestone_modal.vue index 16f792d635a..4061c11ba8f 100644 --- a/app/assets/javascripts/pages/milestones/shared/components/delete_milestone_modal.vue +++ b/app/assets/javascripts/pages/milestones/shared/components/delete_milestone_modal.vue @@ -96,8 +96,8 @@ Once deleted, it cannot be undone or recovered.`), id="delete-milestone-modal" :title="title" :text="text" - kind="danger" :primary-button-label="s__('Milestones|Delete milestone')" + kind="danger" @submit="onSubmit"> <template diff --git a/app/assets/javascripts/pages/milestones/shared/components/promote_milestone_modal.vue b/app/assets/javascripts/pages/milestones/shared/components/promote_milestone_modal.vue index 2bda2aeb3a1..2c683a39f42 100644 --- a/app/assets/javascripts/pages/milestones/shared/components/promote_milestone_modal.vue +++ b/app/assets/javascripts/pages/milestones/shared/components/promote_milestone_modal.vue @@ -53,8 +53,8 @@ <template> <gl-modal id="promote-milestone-modal" - footer-primary-button-variant="warning" :footer-primary-button-text="s__('Milestones|Promote Milestone')" + footer-primary-button-variant="warning" @submit="onSubmit" > <template diff --git a/app/assets/javascripts/pages/projects/graphs/show/stat_graph_contributors.js b/app/assets/javascripts/pages/projects/graphs/show/stat_graph_contributors.js index 653e2502d01..37336a8cb69 100644 --- a/app/assets/javascripts/pages/projects/graphs/show/stat_graph_contributors.js +++ b/app/assets/javascripts/pages/projects/graphs/show/stat_graph_contributors.js @@ -36,7 +36,9 @@ export default (function() { var author_graph, author_header; author_header = _this.create_author_header(d); $(".contributors-list").append(author_header); - _this.authors[d.author_name] = author_graph = new ContributorsAuthorGraph(d.dates); + + author_graph = new ContributorsAuthorGraph(d.dates); + _this.authors[d.author_name] = author_graph; return author_graph.draw(); }; })(this)); diff --git a/app/assets/javascripts/pages/projects/graphs/show/stat_graph_contributors_util.js b/app/assets/javascripts/pages/projects/graphs/show/stat_graph_contributors_util.js index 77135ad1f0e..165446a4db6 100644 --- a/app/assets/javascripts/pages/projects/graphs/show/stat_graph_contributors_util.js +++ b/app/assets/javascripts/pages/projects/graphs/show/stat_graph_contributors_util.js @@ -111,10 +111,15 @@ export default { parse_log_entry: function(log_entry, field, date_range) { var parsed_entry; parsed_entry = {}; + parsed_entry.author_name = log_entry.author_name; parsed_entry.author_email = log_entry.author_email; parsed_entry.dates = {}; - parsed_entry.commits = parsed_entry.additions = parsed_entry.deletions = 0; + + parsed_entry.commits = 0; + parsed_entry.additions = 0; + parsed_entry.deletions = 0; + _.each(_.omit(log_entry, 'author_name', 'author_email'), (function(_this) { return function(value, key) { if (_this.in_range(value.date, date_range)) { diff --git a/app/assets/javascripts/pages/projects/labels/components/promote_label_modal.vue b/app/assets/javascripts/pages/projects/labels/components/promote_label_modal.vue index ad6df51bb7a..5d2247f6c6d 100644 --- a/app/assets/javascripts/pages/projects/labels/components/promote_label_modal.vue +++ b/app/assets/javascripts/pages/projects/labels/components/promote_label_modal.vue @@ -71,8 +71,8 @@ <template> <gl-modal id="promote-label-modal" - footer-primary-button-variant="warning" :footer-primary-button-text="s__('Labels|Promote Label')" + footer-primary-button-variant="warning" @submit="onSubmit" > <div diff --git a/app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/interval_pattern_input.vue b/app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/interval_pattern_input.vue index 2d18fa2044b..d0613804067 100644 --- a/app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/interval_pattern_input.vue +++ b/app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/interval_pattern_input.vue @@ -65,11 +65,11 @@ <div class="cron-preset-radio-input"> <input id="custom" - class="label-light" - type="radio" :name="inputNameAttribute" :value="cronInterval" :checked="isEditable" + class="label-light" + type="radio" @click="toggleCustomInput(true)" /> @@ -90,11 +90,11 @@ <div class="cron-preset-radio-input"> <input id="every-day" - class="label-light" - type="radio" v-model="cronInterval" :name="inputNameAttribute" :value="cronIntervalPresets.everyDay" + class="label-light" + type="radio" @click="toggleCustomInput(false)" /> @@ -109,11 +109,11 @@ <div class="cron-preset-radio-input"> <input id="every-week" - class="label-light" - type="radio" v-model="cronInterval" :name="inputNameAttribute" :value="cronIntervalPresets.everyWeek" + class="label-light" + type="radio" @click="toggleCustomInput(false)" /> @@ -128,11 +128,11 @@ <div class="cron-preset-radio-input"> <input id="every-month" - class="label-light" - type="radio" v-model="cronInterval" :name="inputNameAttribute" :value="cronIntervalPresets.everyMonth" + class="label-light" + type="radio" @click="toggleCustomInput(false)" /> @@ -147,13 +147,13 @@ <div class="cron-interval-input-wrapper"> <input id="schedule_cron" - class="form-control inline cron-interval-input" - type="text" :placeholder="__('Define a custom pattern with cron syntax')" - required="true" v-model="cronInterval" :name="inputNameAttribute" :disabled="!isEditable" + class="form-control inline cron-interval-input" + type="text" + required="true" /> </div> </div> diff --git a/app/assets/javascripts/pages/projects/settings/ci_cd/show/index.js b/app/assets/javascripts/pages/projects/settings/ci_cd/show/index.js index 6c2a785c0af..37ef77c8e43 100644 --- a/app/assets/javascripts/pages/projects/settings/ci_cd/show/index.js +++ b/app/assets/javascripts/pages/projects/settings/ci_cd/show/index.js @@ -22,4 +22,18 @@ document.addEventListener('DOMContentLoaded', () => { errorBox: variableListEl.querySelector('.js-ci-variable-error-box'), saveEndpoint: variableListEl.dataset.saveEndpoint, }); + + // hide extra auto devops settings based on data-attributes + const autoDevOpsSettings = document.querySelector('.js-auto-devops-settings'); + const autoDevOpsExtraSettings = document.querySelector('.js-extra-settings'); + + autoDevOpsSettings.addEventListener('click', event => { + const target = event.target; + if (target.classList.contains('js-toggle-extra-settings')) { + autoDevOpsExtraSettings.classList.toggle( + 'hidden', + !!(target.dataset && target.dataset.hideExtraSettings), + ); + } + }); }); diff --git a/app/assets/javascripts/pages/projects/shared/permissions/components/project_feature_setting.vue b/app/assets/javascripts/pages/projects/shared/permissions/components/project_feature_setting.vue index 9b13b2a524f..06101290f6c 100644 --- a/app/assets/javascripts/pages/projects/shared/permissions/components/project_feature_setting.vue +++ b/app/assets/javascripts/pages/projects/shared/permissions/components/project_feature_setting.vue @@ -72,25 +72,25 @@ <template> <div - class="project-feature-controls" :data-for="name" + class="project-feature-controls" > <input v-if="name" - type="hidden" :name="name" :value="value" + type="hidden" /> <project-feature-toggle :value="featureEnabled" - @change="toggleFeature" :disabled-input="disabledInput" + @change="toggleFeature" /> <div class="select-wrapper"> <select + :disabled="displaySelectInput" class="form-control project-repo-select select-control" @change="selectOption" - :disabled="displaySelectInput" > <option v-for="[optionValue, optionName] in displayOptions" diff --git a/app/assets/javascripts/pages/projects/shared/permissions/components/settings_panel.vue b/app/assets/javascripts/pages/projects/shared/permissions/components/settings_panel.vue index 06b0ab184ed..ae88b765abf 100644 --- a/app/assets/javascripts/pages/projects/shared/permissions/components/settings_panel.vue +++ b/app/assets/javascripts/pages/projects/shared/permissions/components/settings_panel.vue @@ -175,16 +175,16 @@ <div> <div class="project-visibility-setting"> <project-setting-row - label="Project visibility" :help-path="visibilityHelpPath" + label="Project visibility" > <div class="project-feature-controls"> <div class="select-wrapper"> <select - name="project[visibility_level]" v-model="visibilityLevel" - class="form-control select-control" :disabled="!canChangeVisibilityLevel" + name="project[visibility_level]" + class="form-control select-control" > <option :value="visibilityOptions.PRIVATE" @@ -219,30 +219,30 @@ class="request-access" > <input + :value="requestAccessEnabled" type="hidden" name="project[request_access_enabled]" - :value="requestAccessEnabled" /> <input - type="checkbox" v-model="requestAccessEnabled" + type="checkbox" /> Allow users to request access </label> </project-setting-row> </div> <div - class="project-feature-settings" :class="{ 'highlight-changes': highlightChangesClass }" + class="project-feature-settings" > <project-setting-row label="Issues" help-text="Lightweight issue tracking system for this project" > <project-feature-setting - name="project[project_feature_attributes][issues_access_level]" :options="featureAccessLevelOptions" v-model="issuesAccessLevel" + name="project[project_feature_attributes][issues_access_level]" /> </project-setting-row> <project-setting-row @@ -250,9 +250,9 @@ help-text="View and edit files in this project" > <project-feature-setting - name="project[project_feature_attributes][repository_access_level]" :options="featureAccessLevelOptions" v-model="repositoryAccessLevel" + name="project[project_feature_attributes][repository_access_level]" /> </project-setting-row> <div class="project-feature-setting-group"> @@ -261,10 +261,10 @@ help-text="Submit changes to be merged upstream" > <project-feature-setting - name="project[project_feature_attributes][merge_requests_access_level]" :options="repoFeatureAccessLevelOptions" v-model="mergeRequestsAccessLevel" :disabled-input="!repositoryEnabled" + name="project[project_feature_attributes][merge_requests_access_level]" /> </project-setting-row> <project-setting-row @@ -272,34 +272,34 @@ help-text="Build, test, and deploy your changes" > <project-feature-setting - name="project[project_feature_attributes][builds_access_level]" :options="repoFeatureAccessLevelOptions" v-model="buildsAccessLevel" :disabled-input="!repositoryEnabled" + name="project[project_feature_attributes][builds_access_level]" /> </project-setting-row> <project-setting-row v-if="registryAvailable" - label="Container registry" :help-path="registryHelpPath" + label="Container registry" help-text="Every project can have its own space to store its Docker images" > <project-feature-toggle - name="project[container_registry_enabled]" v-model="containerRegistryEnabled" :disabled-input="!repositoryEnabled" + name="project[container_registry_enabled]" /> </project-setting-row> <project-setting-row v-if="lfsAvailable" - label="Git Large File Storage" :help-path="lfsHelpPath" + label="Git Large File Storage" help-text="Manages large files such as audio, video, and graphics files" > <project-feature-toggle - name="project[lfs_enabled]" v-model="lfsEnabled" :disabled-input="!repositoryEnabled" + name="project[lfs_enabled]" /> </project-setting-row> </div> @@ -308,9 +308,9 @@ help-text="Pages for project documentation" > <project-feature-setting - name="project[project_feature_attributes][wiki_access_level]" :options="featureAccessLevelOptions" v-model="wikiAccessLevel" + name="project[project_feature_attributes][wiki_access_level]" /> </project-setting-row> <project-setting-row @@ -318,9 +318,9 @@ help-text="Share code pastes with others out of Git repository" > <project-feature-setting - name="project[project_feature_attributes][snippets_access_level]" :options="featureAccessLevelOptions" v-model="snippetsAccessLevel" + name="project[project_feature_attributes][snippets_access_level]" /> </project-setting-row> </div> 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 index 5765eed4d45..0289209ff1e 100644 --- a/app/assets/javascripts/pages/projects/wikis/components/delete_wiki_modal.vue +++ b/app/assets/javascripts/pages/projects/wikis/components/delete_wiki_modal.vue @@ -50,8 +50,8 @@ export default { <gl-modal id="delete-wiki-modal" :header-title-text="title" - footer-primary-button-variant="danger" :footer-primary-button-text="s__('WikiPageConfirmDelete|Delete page')" + footer-primary-button-variant="danger" @submit="onSubmit" > {{ message }} @@ -68,9 +68,9 @@ export default { value="delete" /> <input + :value="csrfToken" type="hidden" name="authenticity_token" - :value="csrfToken" /> </form> </gl-modal> diff --git a/app/assets/javascripts/pages/sessions/new/username_validator.js b/app/assets/javascripts/pages/sessions/new/username_validator.js index 825de01b5a2..87213c94eda 100644 --- a/app/assets/javascripts/pages/sessions/new/username_validator.js +++ b/app/assets/javascripts/pages/sessions/new/username_validator.js @@ -62,13 +62,13 @@ export default class UsernameValidator { return this.setPendingState(); } - if (!this.state.available) { - return this.setUnavailableState(); - } - if (!this.state.valid) { return this.setInvalidState(); } + + if (!this.state.available) { + return this.setUnavailableState(); + } } interceptInvalid(event) { @@ -89,7 +89,6 @@ export default class UsernameValidator { setAvailabilityState(usernameTaken) { if (usernameTaken) { - this.state.valid = false; this.state.available = false; } else { this.state.available = true; diff --git a/app/assets/javascripts/pages/users/user_tabs.js b/app/assets/javascripts/pages/users/user_tabs.js index 9404b06615e..a2ca03536f2 100644 --- a/app/assets/javascripts/pages/users/user_tabs.js +++ b/app/assets/javascripts/pages/users/user_tabs.js @@ -180,14 +180,14 @@ export default class UserTabs { } toggleLoading(status) { - return this.$parentEl.find('.loading-status .loading').toggleClass('hidden', !status); + return this.$parentEl.find('.loading-status .loading').toggleClass('hide', !status); } setCurrentAction(source) { let newState = source; newState = newState.replace(/\/+$/, ''); newState += this.windowLocation.search + this.windowLocation.hash; - history.replaceState( + window.history.replaceState( { url: newState, }, diff --git a/app/assets/javascripts/pdf/index.vue b/app/assets/javascripts/pdf/index.vue index 00f32d9de78..2f480ecdc69 100644 --- a/app/assets/javascripts/pdf/index.vue +++ b/app/assets/javascripts/pdf/index.vue @@ -56,8 +56,8 @@ <template> <div - class="pdf-viewer" - v-if="hasPDF"> + v-if="hasPDF" + class="pdf-viewer"> <page v-for="(page, index) in pages" :key="index" diff --git a/app/assets/javascripts/pdf/page/index.vue b/app/assets/javascripts/pdf/page/index.vue index fcba819beba..9f06833d560 100644 --- a/app/assets/javascripts/pdf/page/index.vue +++ b/app/assets/javascripts/pdf/page/index.vue @@ -43,9 +43,9 @@ <template> <canvas - class="pdf-page" ref="canvas" :data-page="number" + class="pdf-page" > </canvas> </template> diff --git a/app/assets/javascripts/performance_bar/components/detailed_metric.vue b/app/assets/javascripts/performance_bar/components/detailed_metric.vue index db8a0055acd..dc7d6d29b8f 100644 --- a/app/assets/javascripts/performance_bar/components/detailed_metric.vue +++ b/app/assets/javascripts/performance_bar/components/detailed_metric.vue @@ -39,9 +39,9 @@ export default { </script> <template> <div + v-if="currentRequest.details" :id="`peek-view-${metric}`" class="view" - v-if="currentRequest.details" > <button :data-target="`#modal-peek-${metric}-details`" @@ -56,6 +56,7 @@ export default { <gl-modal :id="`modal-peek-${metric}-details`" :header-title-text="header" + modal-size="xl" class="performance-bar-modal" > <table diff --git a/app/assets/javascripts/pipelines/components/graph/action_component.vue b/app/assets/javascripts/pipelines/components/graph/action_component.vue index 82b4ce083fb..1f152ed438d 100644 --- a/app/assets/javascripts/pipelines/components/graph/action_component.vue +++ b/app/assets/javascripts/pipelines/components/graph/action_component.vue @@ -83,15 +83,16 @@ export default { </script> <template> <button - type="button" - @click="onClickAction" v-tooltip :title="tooltipText" + :class="cssClass" + :disabled="isDisabled" + type="button" class="js-ci-action btn btn-blank btn-transparent ci-action-icon-container ci-action-icon-wrapper" - :class="cssClass" data-container="body" - :disabled="isDisabled" + data-boundary="viewport" + @click="onClickAction" > <icon :name="actionIcon"/> </button> 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 e64afc94ef9..e047d10ac93 100644 --- a/app/assets/javascripts/pipelines/components/graph/dropdown_job_component.vue +++ b/app/assets/javascripts/pipelines/components/graph/dropdown_job_component.vue @@ -82,12 +82,13 @@ export default { <div class="ci-job-dropdown-container dropdown dropright"> <button v-tooltip + :title="tooltipText" type="button" data-toggle="dropdown" data-container="body" data-boundary="viewport" + data-display="static" class="dropdown-menu-toggle build-content" - :title="tooltipText" > <job-name-component diff --git a/app/assets/javascripts/pipelines/components/graph/job_component.vue b/app/assets/javascripts/pipelines/components/graph/job_component.vue index dc16d395bcb..886e62ab1a7 100644 --- a/app/assets/javascripts/pipelines/components/graph/job_component.vue +++ b/app/assets/javascripts/pipelines/components/graph/job_component.vue @@ -107,11 +107,11 @@ export default { </a> <div - v-else v-tooltip - class="js-job-component-tooltip non-details-job-component" + v-else :title="tooltipText" :class="cssClassJobName" + class="js-job-component-tooltip non-details-job-component" data-html="true" data-container="body" > 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 f32368947e8..2c728582b7c 100644 --- a/app/assets/javascripts/pipelines/components/graph/stage_column_component.vue +++ b/app/assets/javascripts/pipelines/components/graph/stage_column_component.vue @@ -52,8 +52,8 @@ export default { </script> <template> <li - class="stage-column" - :class="stageConnectorClass"> + :class="stageConnectorClass" + class="stage-column"> <div class="stage-name"> {{ title }} </div> @@ -62,9 +62,9 @@ export default { <li v-for="(job, index) in jobs" :key="job.id" - class="build" :class="buildConnnectorClass(index)" :id="jobId(job)" + class="build" > <div class="curve"></div> diff --git a/app/assets/javascripts/pipelines/components/header_component.vue b/app/assets/javascripts/pipelines/components/header_component.vue index e08c2092680..5b212ee8931 100644 --- a/app/assets/javascripts/pipelines/components/header_component.vue +++ b/app/assets/javascripts/pipelines/components/header_component.vue @@ -82,11 +82,11 @@ <ci-header v-if="shouldRenderContent" :status="status" - item-name="Pipeline" :item-id="pipeline.id" :time="pipeline.created_at" :user="pipeline.user" :actions="actions" + item-name="Pipeline" @actionClicked="postAction" /> <loading-icon diff --git a/app/assets/javascripts/pipelines/components/nav_controls.vue b/app/assets/javascripts/pipelines/components/nav_controls.vue index eba5678e3e5..1fce9f16ee0 100644 --- a/app/assets/javascripts/pipelines/components/nav_controls.vue +++ b/app/assets/javascripts/pipelines/components/nav_controls.vue @@ -50,10 +50,10 @@ <loading-button v-if="resetCachePath" - @click="onClickResetCache" :loading="isResetCacheButtonLoading" - class="btn btn-default js-clear-cache" :label="s__('Pipelines|Clear Runner Caches')" + class="btn btn-default js-clear-cache" + @click="onClickResetCache" /> <a diff --git a/app/assets/javascripts/pipelines/components/pipeline_url.vue b/app/assets/javascripts/pipelines/components/pipeline_url.vue index 4d965733f95..a107e579457 100644 --- a/app/assets/javascripts/pipelines/components/pipeline_url.vue +++ b/app/assets/javascripts/pipelines/components/pipeline_url.vue @@ -55,10 +55,10 @@ <span>by</span> <user-avatar-link v-if="user" - class="js-pipeline-url-user" :link-href="pipeline.user.path" :img-src="pipeline.user.avatar_url" :tooltip-text="pipeline.user.name" + class="js-pipeline-url-user" /> <span v-if="!user" @@ -67,31 +67,31 @@ </span> <div class="label-container"> <span - v-if="pipeline.flags.latest" v-tooltip + v-if="pipeline.flags.latest" 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 badge badge-danger" - :title="pipeline.yaml_errors"> + v-if="pipeline.flags.yaml_errors" + :title="pipeline.yaml_errors" + class="js-pipeline-url-yaml badge badge-danger"> yaml invalid </span> <span - v-if="pipeline.flags.failure_reason" v-tooltip - class="js-pipeline-url-failure badge badge-danger" - :title="pipeline.failure_reason"> + v-if="pipeline.flags.failure_reason" + :title="pipeline.failure_reason" + class="js-pipeline-url-failure badge badge-danger"> error </span> <a + v-popover="popoverOptions" v-if="pipeline.flags.auto_devops" tabindex="0" class="js-pipeline-url-autodevops badge badge-info autodevops-badge" - v-popover="popoverOptions" role="button"> Auto DevOps </a> diff --git a/app/assets/javascripts/pipelines/components/pipelines.vue b/app/assets/javascripts/pipelines/components/pipelines.vue index 497a09cec65..b31b4bad7a0 100644 --- a/app/assets/javascripts/pipelines/components/pipelines.vue +++ b/app/assets/javascripts/pipelines/components/pipelines.vue @@ -282,8 +282,8 @@ <template> <div class="pipelines-container"> <div - class="top-area scrolling-tabs-container inner-page-scroll-tabs" v-if="shouldRenderTabs || shouldRenderButtons" + class="top-area scrolling-tabs-container inner-page-scroll-tabs" > <div class="fade-left"> <i @@ -303,8 +303,8 @@ <navigation-tabs v-if="shouldRenderTabs" :tabs="tabs" - @onChangeTab="onChangeTab" scope="pipelines" + @onChangeTab="onChangeTab" /> <navigation-controls @@ -312,8 +312,8 @@ :new-pipeline-path="newPipelinePath" :reset-cache-path="resetCachePath" :ci-lint-path="ciLintPath" - @resetRunnersCache="handleResetRunnersCache" :is-reset-cache-button-loading="isResetCacheButtonLoading" + @resetRunnersCache="handleResetRunnersCache" /> </div> @@ -347,8 +347,8 @@ /> <div - class="table-holder" v-else-if="stateToRender === $options.stateMap.tableList" + class="table-holder" > <pipelines-table-component diff --git a/app/assets/javascripts/pipelines/components/pipelines_actions.vue b/app/assets/javascripts/pipelines/components/pipelines_actions.vue index e9bc3cf14ca..5070c253f11 100644 --- a/app/assets/javascripts/pipelines/components/pipelines_actions.vue +++ b/app/assets/javascripts/pipelines/components/pipelines_actions.vue @@ -44,13 +44,13 @@ <div class="btn-group"> <button v-tooltip + :disabled="isLoading" type="button" class="dropdown-new btn btn-default js-pipeline-dropdown-manual-actions" title="Manual job" data-toggle="dropdown" data-placement="top" aria-label="Manual job" - :disabled="isLoading" > <icon name="play" @@ -69,11 +69,11 @@ :key="i" > <button + :class="{ disabled: isActionDisabled(action) }" + :disabled="isActionDisabled(action)" type="button" class="js-pipeline-action-link no-btn btn" @click="onClickAction(action.path)" - :class="{ disabled: isActionDisabled(action) }" - :disabled="isActionDisabled(action)" > {{ action.name }} </button> diff --git a/app/assets/javascripts/pipelines/components/pipelines_artifacts.vue b/app/assets/javascripts/pipelines/components/pipelines_artifacts.vue index 31fcc9dd412..490df47e154 100644 --- a/app/assets/javascripts/pipelines/components/pipelines_artifacts.vue +++ b/app/assets/javascripts/pipelines/components/pipelines_artifacts.vue @@ -42,9 +42,9 @@ v-for="(artifact, i) in artifacts" :key="i"> <a + :href="artifact.path" rel="nofollow" download - :href="artifact.path" > Download {{ artifact.name }} artifacts </a> diff --git a/app/assets/javascripts/pipelines/components/pipelines_table.vue b/app/assets/javascripts/pipelines/components/pipelines_table.vue index 4318abe97e0..2e777783636 100644 --- a/app/assets/javascripts/pipelines/components/pipelines_table.vue +++ b/app/assets/javascripts/pipelines/components/pipelines_table.vue @@ -114,8 +114,8 @@ <modal id="confirmation-modal" :header-title-text="modalTitle" - footer-primary-button-variant="danger" :footer-primary-button-text="s__('Pipeline|Stop pipeline')" + footer-primary-button-variant="danger" @submit="onSubmit" > <span v-html="modalText"></span> diff --git a/app/assets/javascripts/pipelines/components/pipelines_table_row.vue b/app/assets/javascripts/pipelines/components/pipelines_table_row.vue index a3c17479e6f..b2744a30c2a 100644 --- a/app/assets/javascripts/pipelines/components/pipelines_table_row.vue +++ b/app/assets/javascripts/pipelines/components/pipelines_table_row.vue @@ -301,9 +301,9 @@ <div class="table-mobile-content"> <template v-if="pipeline.details.stages.length > 0"> <div - class="stage-container dropdown js-mini-pipeline-graph" v-for="(stage, index) in pipeline.details.stages" - :key="index"> + :key="index" + class="stage-container dropdown js-mini-pipeline-graph"> <pipeline-stage :type="$options.pipelinesTable" :stage="stage" @@ -331,28 +331,28 @@ <pipelines-artifacts-component v-if="pipeline.details.artifacts.length" - class="d-none d-sm-none d-md-block" :artifacts="pipeline.details.artifacts" + class="d-none d-sm-none d-md-block" /> <loading-button v-if="pipeline.flags.retryable" - @click="handleRetryClick" - container-class="js-pipelines-retry-button btn btn-default btn-retry" :loading="isRetrying" :disabled="isRetrying" + container-class="js-pipelines-retry-button btn btn-default btn-retry" + @click="handleRetryClick" > <icon name="repeat" /> </loading-button> <loading-button v-if="pipeline.flags.cancelable" - @click="handleCancelClick" + :loading="isCancelling" + :disabled="isCancelling" data-toggle="modal" data-target="#confirmation-modal" container-class="js-pipelines-cancel-button btn btn-remove" - :loading="isCancelling" - :disabled="isCancelling" + @click="handleCancelClick" > <icon name="close" /> </loading-button> diff --git a/app/assets/javascripts/pipelines/components/stage.vue b/app/assets/javascripts/pipelines/components/stage.vue index f9769815796..b9231c002fd 100644 --- a/app/assets/javascripts/pipelines/components/stage.vue +++ b/app/assets/javascripts/pipelines/components/stage.vue @@ -158,22 +158,23 @@ export default { <div class="dropdown"> <button v-tooltip + id="stageDropdown" + ref="dropdown" :class="triggerButtonClass" - @click="onClickStage" - class="mini-pipeline-graph-dropdown-toggle js-builds-dropdown-button" :title="stage.title" + class="mini-pipeline-graph-dropdown-toggle js-builds-dropdown-button" data-placement="top" data-toggle="dropdown" + data-display="static" type="button" - id="stageDropdown" aria-haspopup="true" aria-expanded="false" - ref="dropdown" + @click="onClickStage" > <span - aria-hidden="true" :aria-label="stage.title" + aria-hidden="true" > <icon :name="borderlessIcon" /> </span> diff --git a/app/assets/javascripts/pipelines/components/time_ago.vue b/app/assets/javascripts/pipelines/components/time_ago.vue index 79dbdca4010..0a97df2dc18 100644 --- a/app/assets/javascripts/pipelines/components/time_ago.vue +++ b/app/assets/javascripts/pipelines/components/time_ago.vue @@ -66,8 +66,8 @@ </div> <div class="table-mobile-content"> <p - class="duration" v-if="hasDuration" + class="duration" > <span v-html="iconTimerSvg"> </span> @@ -75,8 +75,8 @@ </p> <p - class="finished-at d-none d-sm-none d-md-block" v-if="hasFinishedTime" + class="finished-at d-none d-sm-none d-md-block" > <i @@ -87,9 +87,9 @@ <time v-tooltip + :title="tooltipTitle(finishedTime)" data-placement="top" - data-container="body" - :title="tooltipTitle(finishedTime)"> + data-container="body"> {{ timeFormated(finishedTime) }} </time> </p> diff --git a/app/assets/javascripts/profile/account/components/delete_account_modal.vue b/app/assets/javascripts/profile/account/components/delete_account_modal.vue index f50002afbf2..974629fa2af 100644 --- a/app/assets/javascripts/profile/account/components/delete_account_modal.vue +++ b/app/assets/javascripts/profile/account/components/delete_account_modal.vue @@ -80,10 +80,10 @@ Once you confirm %{deleteAccount}, it cannot be undone or recovered.`), id="delete-account-modal" :title="s__('Profiles|Delete your account?')" :text="text" - kind="danger" :primary-button-label="s__('Profiles|Delete account')" - @submit="onSubmit" - :submit-disabled="!canSubmit()"> + :submit-disabled="!canSubmit()" + kind="danger" + @submit="onSubmit"> <template slot="body" @@ -101,9 +101,9 @@ Once you confirm %{deleteAccount}, it cannot be undone or recovered.`), value="delete" /> <input + :value="csrfToken" type="hidden" name="authenticity_token" - :value="csrfToken" /> <p @@ -114,18 +114,18 @@ Once you confirm %{deleteAccount}, it cannot be undone or recovered.`), <input v-if="confirmWithPassword" + v-model="enteredPassword" name="password" class="form-control" type="password" - v-model="enteredPassword" aria-labelledby="input-label" /> <input v-else + v-model="enteredUsername" name="username" class="form-control" type="text" - v-model="enteredUsername" aria-labelledby="input-label" /> </form> diff --git a/app/assets/javascripts/profile/account/components/update_username.vue b/app/assets/javascripts/profile/account/components/update_username.vue index b37febe523c..ef484ddfd61 100644 --- a/app/assets/javascripts/profile/account/components/update_username.vue +++ b/app/assets/javascripts/profile/account/components/update_username.vue @@ -93,10 +93,10 @@ Please update your Git repository remotes as soon as possible.`), </div> <input :id="$options.inputId" - class="form-control" - required="required" v-model="newUsername" :disabled="isRequestPending" + class="form-control" + required="required" /> </div> <p class="form-text text-muted"> @@ -105,18 +105,18 @@ Please update your Git repository remotes as soon as possible.`), </div> <button :data-target="`#${$options.modalId}`" + :disabled="isRequestPending || newUsername === username" class="btn btn-warning" type="button" data-toggle="modal" - :disabled="isRequestPending || newUsername === username" > {{ $options.buttonText }} </button> <gl-modal :id="$options.modalId" :header-title-text="s__('Profiles|Change username') + '?'" - footer-primary-button-variant="warning" :footer-primary-button-text="$options.buttonText" + footer-primary-button-variant="warning" @submit="onConfirm" > <span v-html="modalText"></span> diff --git a/app/assets/javascripts/profile/gl_crop.js b/app/assets/javascripts/profile/gl_crop.js index 8f93156cdd1..ba120c4bbdf 100644 --- a/app/assets/javascripts/profile/gl_crop.js +++ b/app/assets/javascripts/profile/gl_crop.js @@ -139,6 +139,8 @@ import _ from 'underscore'; var array, binary, i, k, len, v; binary = atob(dataURL.split(',')[1]); array = []; + + // eslint-disable-next-line no-multi-assign for (k = i = 0, len = binary.length; i < len; k = (i += 1)) { v = binary[k]; array.push(binary.charCodeAt(k)); diff --git a/app/assets/javascripts/project_find_file.js b/app/assets/javascripts/project_find_file.js index 4c4acd487f8..17497283695 100644 --- a/app/assets/javascripts/project_find_file.js +++ b/app/assets/javascripts/project_find_file.js @@ -91,6 +91,8 @@ export default class ProjectFindFile { var blobItemUrl, filePath, html, i, j, len, matches, results; this.element.find(".tree-table > tbody").empty(); results = []; + + // eslint-disable-next-line no-multi-assign for (i = j = 0, len = filePaths.length; j < len; i = (j += 1)) { filePath = filePaths[i]; if (i === 20) { @@ -150,7 +152,7 @@ export default class ProjectFindFile { } goToTree() { - return location.href = this.options.treeUrl; + return window.location.href = this.options.treeUrl; } goToBlob() { diff --git a/app/assets/javascripts/project_import.js b/app/assets/javascripts/project_import.js index d2d26d6f67e..5a0d2b642eb 100644 --- a/app/assets/javascripts/project_import.js +++ b/app/assets/javascripts/project_import.js @@ -2,7 +2,7 @@ import { visitUrl } from './lib/utils/url_utility'; export default function projectImport() { setTimeout(() => { - visitUrl(location.href); + visitUrl(window.location.href); }, 5000); } diff --git a/app/assets/javascripts/project_label_subscription.js b/app/assets/javascripts/project_label_subscription.js index 6f06944ebb6..9049f87e037 100644 --- a/app/assets/javascripts/project_label_subscription.js +++ b/app/assets/javascripts/project_label_subscription.js @@ -3,6 +3,17 @@ import { __ } from './locale'; import axios from './lib/utils/axios_utils'; import flash from './flash'; +const tooltipTitles = { + group: { + subscribed: __('Unsubscribe at group level'), + unsubscribed: __('Subscribe at group level'), + }, + project: { + subscribed: __('Unsubscribe at project level'), + unsubscribed: __('Subscribe at project level'), + }, +}; + export default class ProjectLabelSubscription { constructor(container) { this.$container = $(container); @@ -15,12 +26,10 @@ export default class ProjectLabelSubscription { event.preventDefault(); const $btn = $(event.currentTarget); - const $span = $btn.find('span'); const url = $btn.attr('data-url'); const oldStatus = $btn.attr('data-status'); $btn.addClass('disabled'); - $span.toggleClass('hidden'); axios.post(url).then(() => { let newStatus; @@ -32,21 +41,28 @@ export default class ProjectLabelSubscription { [newStatus, newAction] = ['unsubscribed', 'Subscribe']; } - $span.toggleClass('hidden'); $btn.removeClass('disabled'); this.$buttons.attr('data-status', newStatus); this.$buttons.find('> span').text(newAction); - this.$buttons.map((button) => { + this.$buttons.map((i, button) => { const $button = $(button); + const originalTitle = $button.attr('data-original-title'); - if ($button.attr('data-original-title')) { - $button.tooltip('hide').attr('data-original-title', newAction).tooltip('_fixTitle'); + if (originalTitle) { + ProjectLabelSubscription.setNewTitle($button, originalTitle, newStatus, newAction); } return button; }); }).catch(() => flash(__('There was an error subscribing to this label.'))); } + + static setNewTitle($button, originalTitle, newStatus) { + const type = /group/.test(originalTitle) ? 'group' : 'project'; + const newTitle = tooltipTitles[type][newStatus]; + + $button.attr('title', newTitle).tooltip('_fixTitle'); + } } diff --git a/app/assets/javascripts/projects/gke_cluster_dropdowns/components/gke_machine_type_dropdown.vue b/app/assets/javascripts/projects/gke_cluster_dropdowns/components/gke_machine_type_dropdown.vue index ab7d2d41ece..d4497924ad8 100644 --- a/app/assets/javascripts/projects/gke_cluster_dropdowns/components/gke_machine_type_dropdown.vue +++ b/app/assets/javascripts/projects/gke_cluster_dropdowns/components/gke_machine_type_dropdown.vue @@ -89,14 +89,13 @@ export default { <div> <div class="js-gcp-machine-type-dropdown dropdown" - :class="{ 'gl-show-field-errors': hasErrors }" > <dropdown-hidden-input :name="fieldName" :value="selectedMachineType" /> <dropdown-button - :class="{ 'gl-field-error-outline': hasErrors }" + :class="{ 'border-danger': hasErrors }" :is-disabled="isDisabled" :is-loading="isLoading" :toggle-text="toggleText" @@ -132,9 +131,12 @@ export default { </div> </div> <span - class="form-text text-muted" - :class="{ 'gl-field-error': hasErrors }" v-if="hasErrors" + :class="{ + 'text-danger': hasErrors, + 'text-muted': !hasErrors + }" + class="form-text" > {{ errorMessage }} </span> diff --git a/app/assets/javascripts/projects/gke_cluster_dropdowns/components/gke_project_id_dropdown.vue b/app/assets/javascripts/projects/gke_cluster_dropdowns/components/gke_project_id_dropdown.vue index 25350ef0fa9..08d0a122579 100644 --- a/app/assets/javascripts/projects/gke_cluster_dropdowns/components/gke_project_id_dropdown.vue +++ b/app/assets/javascripts/projects/gke_cluster_dropdowns/components/gke_project_id_dropdown.vue @@ -147,7 +147,6 @@ export default { <div> <div class="js-gcp-project-id-dropdown dropdown" - :class="{ 'gl-show-field-errors': hasErrors }" > <dropdown-hidden-input :name="fieldName" @@ -155,7 +154,7 @@ export default { /> <dropdown-button :class="{ - 'gl-field-error-outline': hasErrors, + 'border-danger': hasErrors, 'read-only': hasOneProject }" :is-disabled="isDisabled" @@ -193,8 +192,11 @@ export default { </div> </div> <span - class="form-text text-muted" - :class="{ 'gl-field-error': hasErrors }" + :class="{ + 'text-danger': hasErrors, + 'text-muted': !hasErrors + }" + class="form-text" v-html="helpText" ></span> </div> diff --git a/app/assets/javascripts/projects/gke_cluster_dropdowns/components/gke_zone_dropdown.vue b/app/assets/javascripts/projects/gke_cluster_dropdowns/components/gke_zone_dropdown.vue index 8ee4eefcd91..b5476684c6a 100644 --- a/app/assets/javascripts/projects/gke_cluster_dropdowns/components/gke_zone_dropdown.vue +++ b/app/assets/javascripts/projects/gke_cluster_dropdowns/components/gke_zone_dropdown.vue @@ -63,14 +63,13 @@ export default { <div> <div class="js-gcp-zone-dropdown dropdown" - :class="{ 'gl-show-field-errors': hasErrors }" > <dropdown-hidden-input :name="fieldName" :value="selectedZone" /> <dropdown-button - :class="{ 'gl-field-error-outline': hasErrors }" + :class="{ 'border-danger': hasErrors }" :is-disabled="isDisabled" :is-loading="isLoading" :toggle-text="toggleText" @@ -106,9 +105,12 @@ export default { </div> </div> <span - class="form-text text-muted" - :class="{ 'gl-field-error': hasErrors }" v-if="hasErrors" + :class="{ + 'text-danger': hasErrors, + 'text-muted': !hasErrors + }" + class="form-text" > {{ errorMessage }} </span> diff --git a/app/assets/javascripts/projects/project_new.js b/app/assets/javascripts/projects/project_new.js index 888b1d6ce33..002edb4663c 100644 --- a/app/assets/javascripts/projects/project_new.js +++ b/app/assets/javascripts/projects/project_new.js @@ -90,7 +90,7 @@ const bindEvents = () => { function chooseTemplate() { $('.template-option').hide(); $projectFieldsForm.addClass('selected'); - $selectedIcon.removeClass('active'); + $selectedIcon.removeClass('d-block'); const value = $(this).val(); const templates = { rails: { @@ -109,7 +109,7 @@ const bindEvents = () => { const selectedTemplate = templates[value]; $selectedTemplateText.text(selectedTemplate.text); - $(selectedTemplate.icon).addClass('active'); + $(selectedTemplate.icon).addClass('d-block'); $templateProjectNameInput.focus(); } diff --git a/app/assets/javascripts/projects/tree/components/commit_pipeline_status_component.vue b/app/assets/javascripts/projects/tree/components/commit_pipeline_status_component.vue index 63f20a0041d..c772fca14bb 100644 --- a/app/assets/javascripts/projects/tree/components/commit_pipeline_status_component.vue +++ b/app/assets/javascripts/projects/tree/components/commit_pipeline_status_component.vue @@ -100,9 +100,9 @@ <template> <div> <loading-icon + v-if="isLoading" label="Loading pipeline status" size="3" - v-if="isLoading" /> <a v-else @@ -112,8 +112,8 @@ v-tooltip :title="statusTitle" :aria-label="statusTitle" - data-container="body" :status="ciStatus" + data-container="body" /> </a> </div> diff --git a/app/assets/javascripts/projects_dropdown/components/app.vue b/app/assets/javascripts/projects_dropdown/components/app.vue index 0bbd8a41753..73d49488299 100644 --- a/app/assets/javascripts/projects_dropdown/components/app.vue +++ b/app/assets/javascripts/projects_dropdown/components/app.vue @@ -132,14 +132,14 @@ export default { <div> <search/> <loading-icon - class="loading-animation prepend-top-20" - size="2" v-if="isLoadingProjects" :label="s__('ProjectsDropdown|Loading projects')" + class="loading-animation prepend-top-20" + size="2" /> <div - class="section-header" v-if="isFrequentsListVisible" + class="section-header" > {{ s__('ProjectsDropdown|Frequently visited') }} </div> diff --git a/app/assets/javascripts/projects_dropdown/components/projects_list_frequent.vue b/app/assets/javascripts/projects_dropdown/components/projects_list_frequent.vue index 246dbeaaded..625e0aa548c 100644 --- a/app/assets/javascripts/projects_dropdown/components/projects_list_frequent.vue +++ b/app/assets/javascripts/projects_dropdown/components/projects_list_frequent.vue @@ -37,14 +37,14 @@ class="list-unstyled" > <li - class="section-empty" v-if="isListEmpty" + class="section-empty" > {{ listEmptyMessage }} </li> <projects-list-item - v-else v-for="(project, index) in projects" + v-else :key="index" :project-id="project.id" :project-name="project.name" diff --git a/app/assets/javascripts/projects_dropdown/components/projects_list_item.vue b/app/assets/javascripts/projects_dropdown/components/projects_list_item.vue index 759cdd1ded9..eafbf6c99e2 100644 --- a/app/assets/javascripts/projects_dropdown/components/projects_list_item.vue +++ b/app/assets/javascripts/projects_dropdown/components/projects_list_item.vue @@ -79,36 +79,36 @@ class="projects-list-item-container" > <a - class="clearfix" :href="webUrl" + class="clearfix" > <div class="project-item-avatar-container" > <img v-if="hasAvatar" - class="avatar s32" :src="avatarUrl" + class="avatar s32" /> <identicon v-else - size-class="s32" :entity-id="projectId" :entity-name="projectName" + size-class="s32" /> </div> <div class="project-item-metadata-container" > <div - class="project-title" :title="projectName" + class="project-title" v-html="highlightedProjectName" > </div> <div - class="project-namespace" :title="namespace" + class="project-namespace" >{{ truncatedNamespace }}</div> </div> </a> diff --git a/app/assets/javascripts/projects_dropdown/components/projects_list_search.vue b/app/assets/javascripts/projects_dropdown/components/projects_list_search.vue index 8d0c29177e6..76e9cb9e53f 100644 --- a/app/assets/javascripts/projects_dropdown/components/projects_list_search.vue +++ b/app/assets/javascripts/projects_dropdown/components/projects_list_search.vue @@ -48,8 +48,8 @@ export default { {{ listEmptyMessage }} </li> <projects-list-item - v-else v-for="(project, index) in projects" + v-else :key="index" :project-id="project.id" :project-name="project.name" diff --git a/app/assets/javascripts/projects_dropdown/components/search.vue b/app/assets/javascripts/projects_dropdown/components/search.vue index 7fcd62dcdb8..28f2a18f2a6 100644 --- a/app/assets/javascripts/projects_dropdown/components/search.vue +++ b/app/assets/javascripts/projects_dropdown/components/search.vue @@ -49,11 +49,11 @@ class="search-input-container d-none d-sm-block" > <input - type="search" - class="form-control" ref="search" v-model="searchQuery" :placeholder="s__('ProjectsDropdown|Search your projects')" + type="search" + class="form-control" /> <i v-if="!searchQuery" diff --git a/app/assets/javascripts/prometheus_metrics/prometheus_metrics.js b/app/assets/javascripts/prometheus_metrics/prometheus_metrics.js index 1a75fdd75db..078ccbbbac2 100644 --- a/app/assets/javascripts/prometheus_metrics/prometheus_metrics.js +++ b/app/assets/javascripts/prometheus_metrics/prometheus_metrics.js @@ -107,7 +107,7 @@ export default class PrometheusMetrics { if (data && data.success) { stop(data); } else { - this.backOffRequestCounter = this.backOffRequestCounter += 1; + this.backOffRequestCounter += 1; if (this.backOffRequestCounter < 3) { next(); } else { diff --git a/app/assets/javascripts/registry/components/app.vue b/app/assets/javascripts/registry/components/app.vue index ea0f7199a70..31f88675912 100644 --- a/app/assets/javascripts/registry/components/app.vue +++ b/app/assets/javascripts/registry/components/app.vue @@ -48,8 +48,8 @@ /> <collapsible-container - v-else-if="!isLoading && repos.length" v-for="(item, index) in repos" + v-else-if="!isLoading && repos.length" :key="index" :repo="item" /> diff --git a/app/assets/javascripts/registry/components/collapsible_container.vue b/app/assets/javascripts/registry/components/collapsible_container.vue index 2fc3778820b..4116c4a0489 100644 --- a/app/assets/javascripts/registry/components/collapsible_container.vue +++ b/app/assets/javascripts/registry/components/collapsible_container.vue @@ -62,15 +62,15 @@ <div class="container-image-head"> <button type="button" - @click="toggleRepo" class="js-toggle-repo btn-link" + @click="toggleRepo" > <i - class="fa" :class="{ 'fa-chevron-right': !isOpen, 'fa-chevron-up': isOpen, }" + class="fa" aria-hidden="true" > </i> @@ -86,12 +86,12 @@ <div class="controls d-none d-sm-block float-right"> <button + v-tooltip v-if="repo.canDelete" - type="button" - class="js-remove-repo btn btn-danger" :title="s__('ContainerRegistry|Remove repository')" :aria-label="s__('ContainerRegistry|Remove repository')" - v-tooltip + type="button" + class="js-remove-repo btn btn-danger" @click="handleDeleteRepository" > <i diff --git a/app/assets/javascripts/registry/components/table_registry.vue b/app/assets/javascripts/registry/components/table_registry.vue index e4a4b3bb129..9f4973c3490 100644 --- a/app/assets/javascripts/registry/components/table_registry.vue +++ b/app/assets/javascripts/registry/components/table_registry.vue @@ -118,13 +118,13 @@ <td class="content"> <button + v-tooltip v-if="item.canDelete" - type="button" - 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')" + type="button" + class="js-delete-registry btn btn-danger d-none d-sm-block float-right" data-container="body" - v-tooltip @click="handleDeleteRegistry(item)" > <i diff --git a/app/assets/javascripts/search_autocomplete.js b/app/assets/javascripts/search_autocomplete.js index 2da022fde63..ef3c71eeafe 100644 --- a/app/assets/javascripts/search_autocomplete.js +++ b/app/assets/javascripts/search_autocomplete.js @@ -438,7 +438,7 @@ export default class SearchAutocomplete { } onClick(item, $el, e) { - if (location.pathname.indexOf(item.url) !== -1) { + if (window.location.pathname.indexOf(item.url) !== -1) { if (!e.metaKey) e.preventDefault(); if (!this.badgePresent) { if (item.category === 'Projects') { diff --git a/app/assets/javascripts/settings_panels.js b/app/assets/javascripts/settings_panels.js index eecde4550f9..37b4a2a4c63 100644 --- a/app/assets/javascripts/settings_panels.js +++ b/app/assets/javascripts/settings_panels.js @@ -42,8 +42,8 @@ export default function initSettingsPanels() { } }); - if (location.hash) { - const $target = $(location.hash); + if (window.location.hash) { + const $target = $(window.location.hash); if ($target.length && $target.hasClass('settings')) { expandSection($target); } diff --git a/app/assets/javascripts/shortcuts_find_file.js b/app/assets/javascripts/shortcuts_find_file.js index 1e246a56b85..8658081c6c2 100644 --- a/app/assets/javascripts/shortcuts_find_file.js +++ b/app/assets/javascripts/shortcuts_find_file.js @@ -13,8 +13,8 @@ export default class ShortcutsFindFile extends ShortcutsNavigation { element === this.projectFindFile.inputElement[0] && (combo === 'up' || combo === 'down' || combo === 'esc' || combo === 'enter') ) { - // when press up/down key in textbox, cusor prevent to move to home/end - event.preventDefault(); + // when press up/down key in textbox, cursor prevent to move to home/end + e.preventDefault(); return false; } diff --git a/app/assets/javascripts/sidebar/components/assignees/assignees.vue b/app/assets/javascripts/sidebar/components/assignees/assignees.vue index 5a374d84796..d22a1e1ac66 100644 --- a/app/assets/javascripts/sidebar/components/assignees/assignees.vue +++ b/app/assets/javascripts/sidebar/components/assignees/assignees.vue @@ -125,13 +125,13 @@ export default { <template> <div> <div - class="sidebar-collapsed-icon sidebar-collapsed-user" - :class="{ 'multiple-users': hasMoreThanOneAssignee }" v-tooltip + :class="{ 'multiple-users': hasMoreThanOneAssignee }" + :title="collapsedTooltipTitle" + class="sidebar-collapsed-icon sidebar-collapsed-user" data-container="body" data-placement="left" data-boundary="viewport" - :title="collapsedTooltipTitle" > <i v-if="hasNoUsers" @@ -140,17 +140,17 @@ export default { > </i> <button - type="button" - class="btn-link" v-for="(user, index) in users" v-if="shouldRenderCollapsedAssignee(index)" :key="user.id" + type="button" + class="btn-link" > <img - width="24" - class="avatar avatar-inline s24" :alt="assigneeAlt(user)" :src="avatarUrl(user)" + width="24" + class="avatar avatar-inline s24" /> <span class="author"> {{ user.name }} @@ -186,14 +186,14 @@ export default { </template> <template v-else-if="hasOneUser"> <a - class="author_link bold" :href="assigneeUrl(firstUser)" + class="author_link bold" > <img - width="32" - class="avatar avatar-inline s32" :alt="assigneeAlt(firstUser)" :src="avatarUrl(firstUser)" + width="32" + class="avatar avatar-inline s32" /> <span class="author"> {{ firstUser.name }} @@ -206,23 +206,23 @@ export default { <template v-else> <div class="user-list"> <div - class="user-item" v-for="(user, index) in users" v-if="renderAssignee(index)" :key="user.id" + class="user-item" > <a + :href="assigneeUrl(user)" + :data-title="user.name" class="user-link has-tooltip" data-container="body" data-placement="bottom" - :href="assigneeUrl(user)" - :data-title="user.name" > <img - width="32" - class="avatar avatar-inline s32" :alt="assigneeAlt(user)" :src="avatarUrl(user)" + width="32" + class="avatar avatar-inline s32" /> </a> </div> diff --git a/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.vue b/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.vue index b04a2eff798..123c92aff64 100644 --- a/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.vue +++ b/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.vue @@ -90,12 +90,12 @@ export default { /> <assignees v-if="!store.isFetching.assignees" - class="value" :root-path="store.rootPath" :users="store.assignees" :editable="store.editable" - @assign-self="assignSelf" :issuable-type="issuableType" + class="value" + @assign-self="assignSelf" /> </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 3f6e2f05396..2b8d6207dea 100644 --- a/app/assets/javascripts/sidebar/components/confidential/confidential_issue_sidebar.vue +++ b/app/assets/javascripts/sidebar/components/confidential/confidential_issue_sidebar.vue @@ -54,7 +54,7 @@ export default { updateConfidentialAttribute(confidential) { this.service .update('issue', { confidential }) - .then(() => location.reload()) + .then(() => window.location.reload()) .catch(() => { Flash( __( @@ -70,13 +70,13 @@ export default { <template> <div class="block issuable-sidebar-item confidentiality"> <div - class="sidebar-collapsed-icon" - @click="toggleForm" v-tooltip + :title="tooltipLabel" + class="sidebar-collapsed-icon" data-container="body" data-placement="left" data-boundary="viewport" - :title="tooltipLabel" + @click="toggleForm" > <icon :name="confidentialityIcon" @@ -104,8 +104,8 @@ export default { v-if="!isConfidential" class="no-value sidebar-item-value"> <icon - name="eye" :size="16" + name="eye" aria-hidden="true" class="sidebar-item-icon inline" /> @@ -115,8 +115,8 @@ export default { v-else class="value sidebar-item-value hide-collapsed"> <icon - name="eye-slash" :size="16" + name="eye-slash" aria-hidden="true" class="sidebar-item-icon inline is-active" /> diff --git a/app/assets/javascripts/sidebar/components/lock/edit_form.vue b/app/assets/javascripts/sidebar/components/lock/edit_form.vue index d392977e5e2..4906dad22e1 100644 --- a/app/assets/javascripts/sidebar/components/lock/edit_form.vue +++ b/app/assets/javascripts/sidebar/components/lock/edit_form.vue @@ -44,14 +44,14 @@ export default { <div class="dropdown show"> <div class="dropdown-menu sidebar-item-warning-message"> <p - class="text" v-if="isLocked" + class="text" v-html="unlockWarning"> </p> <p - class="text" v-else + class="text" v-html="lockWarning"> </p> 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 fb69c741dcd..8bbc59f623a 100644 --- a/app/assets/javascripts/sidebar/components/lock/lock_issue_sidebar.vue +++ b/app/assets/javascripts/sidebar/components/lock/lock_issue_sidebar.vue @@ -76,7 +76,7 @@ export default { .update(this.issuableType, { discussion_locked: locked, }) - .then(() => location.reload()) + .then(() => window.location.reload()) .catch(() => Flash( this.__( @@ -94,13 +94,13 @@ export default { <template> <div class="block issuable-sidebar-item lock"> <div - class="sidebar-collapsed-icon" - @click="toggleForm" v-tooltip + :title="tooltipLabel" + class="sidebar-collapsed-icon" data-container="body" data-placement="left" data-boundary="viewport" - :title="tooltipLabel" + @click="toggleForm" > <icon :name="lockIcon" @@ -134,8 +134,8 @@ export default { class="value sidebar-item-value" > <icon - name="lock" :size="16" + name="lock" aria-hidden="true" class="sidebar-item-icon inline is-active" /> @@ -147,8 +147,8 @@ export default { class="no-value sidebar-item-value hide-collapsed" > <icon - name="lock-open" :size="16" + name="lock-open" aria-hidden="true" class="sidebar-item-icon inline" /> diff --git a/app/assets/javascripts/sidebar/components/participants/participants.vue b/app/assets/javascripts/sidebar/components/participants/participants.vue index 0a945fc7fd5..33dd6c981b6 100644 --- a/app/assets/javascripts/sidebar/components/participants/participants.vue +++ b/app/assets/javascripts/sidebar/components/participants/participants.vue @@ -80,12 +80,12 @@ <template> <div> <div - class="sidebar-collapsed-icon" v-tooltip + :title="participantLabel" + class="sidebar-collapsed-icon" data-container="body" data-placement="left" data-boundary="viewport" - :title="participantLabel" @click="onClickCollapsedIcon" > <i @@ -119,15 +119,15 @@ class="participants-author js-participants-author" > <a - class="author_link" :href="participant.web_url" + class="author_link" > <user-avatar-image :lazy="true" :img-src="participant.avatar_url" - css-classes="avatar-inline" :size="24" :tooltip-text="participant.name" + css-classes="avatar-inline" tooltip-placement="bottom" /> </a> diff --git a/app/assets/javascripts/sidebar/components/subscriptions/subscriptions.vue b/app/assets/javascripts/sidebar/components/subscriptions/subscriptions.vue index 6745c1aafff..448c8fc3602 100644 --- a/app/assets/javascripts/sidebar/components/subscriptions/subscriptions.vue +++ b/app/assets/javascripts/sidebar/components/subscriptions/subscriptions.vue @@ -97,9 +97,9 @@ </span> <toggle-button ref="toggleButton" - class="float-right hide-collapsed js-issuable-subscribe-button" :is-loading="showLoadingState" :value="subscribed" + class="float-right hide-collapsed js-issuable-subscribe-button" @change="toggleSubscription" /> </div> 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 209af1ce152..1d030c4f67f 100644 --- a/app/assets/javascripts/sidebar/components/time_tracking/collapsed_state.vue +++ b/app/assets/javascripts/sidebar/components/time_tracking/collapsed_state.vue @@ -110,12 +110,12 @@ <template> <div - class="sidebar-collapsed-icon" v-tooltip + :title="tooltipText" + class="sidebar-collapsed-icon" data-container="body" data-placement="left" data-boundary="viewport" - :title="tooltipText" > <icon name="timer" /> <div class="time-tracking-collapsed-summary"> 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 6f79310b1cc..d335c3f55af 100644 --- a/app/assets/javascripts/sidebar/components/time_tracking/comparison_pane.vue +++ b/app/assets/javascripts/sidebar/components/time_tracking/comparison_pane.vue @@ -1,8 +1,12 @@ <script> import { parseSeconds, stringifyTime } from '../../../lib/utils/pretty_time'; +import tooltip from '../../../vue_shared/directives/tooltip'; export default { name: 'TimeTrackingComparisonPane', + directives: { + tooltip, + }, props: { timeSpent: { type: Number, @@ -50,19 +54,17 @@ export default { <template> <div class="time-tracking-comparison-pane"> <div + v-tooltip + :title="timeRemainingTooltip" + :class="timeRemainingStatusClass" class="compare-meter" data-toggle="tooltip" data-placement="top" role="timeRemainingDisplay" - :aria-valuenow="timeRemainingTooltip" - :title="timeRemainingTooltip" - :data-original-title="timeRemainingTooltip" - :class="timeRemainingStatusClass" > <div - class="meter-container" - role="timeSpentPercent" :aria-valuenow="timeRemainingPercent" + class="meter-container" > <div :style="{ width: timeRemainingPercent }" diff --git a/app/assets/javascripts/sidebar/components/time_tracking/help_state.vue b/app/assets/javascripts/sidebar/components/time_tracking/help_state.vue index 825063d9ba6..19ec0f05a26 100644 --- a/app/assets/javascripts/sidebar/components/time_tracking/help_state.vue +++ b/app/assets/javascripts/sidebar/components/time_tracking/help_state.vue @@ -45,8 +45,8 @@ export default { <p v-html="spendText"> </p> <a - class="btn btn-default learn-more-button" :href="href" + class="btn btn-default learn-more-button" > {{ __('Learn more') }} </a> 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 7d56d2fa5ee..ca3b9338c29 100644 --- a/app/assets/javascripts/sidebar/components/time_tracking/time_tracker.vue +++ b/app/assets/javascripts/sidebar/components/time_tracking/time_tracker.vue @@ -101,8 +101,8 @@ export default { <template> <div - class="time_tracker time-tracking-component-wrap" v-cloak + class="time_tracker time-tracking-component-wrap" > <time-tracking-collapsed-state :show-comparison-state="showComparisonState" @@ -116,8 +116,8 @@ export default { <div class="title hide-collapsed"> {{ __('Time tracking') }} <div - class="help-button float-right" v-if="!showHelpState" + class="help-button float-right" @click="toggleHelpState(true)" > <i @@ -127,8 +127,8 @@ export default { </i> </div> <div - class="close-help-button float-right" v-if="showHelpState" + class="close-help-button float-right" @click="toggleHelpState(false)" > <i diff --git a/app/assets/javascripts/sidebar/sidebar_mediator.js b/app/assets/javascripts/sidebar/sidebar_mediator.js index d86557e870a..d9ca5e46770 100644 --- a/app/assets/javascripts/sidebar/sidebar_mediator.js +++ b/app/assets/javascripts/sidebar/sidebar_mediator.js @@ -80,7 +80,7 @@ export default class SidebarMediator { return this.service.moveIssue(this.store.moveToProjectId) .then(response => response.json()) .then((data) => { - if (location.pathname !== data.web_url) { + if (window.location.pathname !== data.web_url) { visitUrl(data.web_url); } }); diff --git a/app/assets/javascripts/single_file_diff.js b/app/assets/javascripts/single_file_diff.js index 1afff0dba38..ae27c676fa0 100644 --- a/app/assets/javascripts/single_file_diff.js +++ b/app/assets/javascripts/single_file_diff.js @@ -11,7 +11,7 @@ import syntaxHighlight from './syntax_highlight'; const WRAPPER = '<div class="diff-content"></div>'; const LOADING_HTML = '<i class="fa fa-spinner fa-spin"></i>'; const ERROR_HTML = '<div class="nothing-here-block"><i class="fa fa-warning"></i> Could not load diff</div>'; -const COLLAPSED_HTML = '<div class="nothing-here-block diff-collapsed">This diff is collapsed. <a class="click-to-expand">Click to expand it.</a></div>'; +const COLLAPSED_HTML = '<div class="nothing-here-block diff-collapsed">This diff is collapsed. <button class="click-to-expand btn btn-link">Click to expand it.</button></div>'; export default class SingleFileDiff { constructor(file) { diff --git a/app/assets/javascripts/snippet/snippet_embed.js b/app/assets/javascripts/snippet/snippet_embed.js index 81ec483f2d9..873a506a92f 100644 --- a/app/assets/javascripts/snippet/snippet_embed.js +++ b/app/assets/javascripts/snippet/snippet_embed.js @@ -1,5 +1,5 @@ export default () => { - const { protocol, host, pathname } = location; + const { protocol, host, pathname } = window.location; const shareBtn = document.querySelector('.js-share-btn'); const embedBtn = document.querySelector('.js-embed-btn'); const snippetUrlArea = document.querySelector('.js-snippet-url-area'); diff --git a/app/assets/javascripts/users_select.js b/app/assets/javascripts/users_select.js index cd954f75613..349614460e1 100644 --- a/app/assets/javascripts/users_select.js +++ b/app/assets/javascripts/users_select.js @@ -259,6 +259,7 @@ function UsersSelect(currentUser, els, options = {}) { showDivider = 0; if (firstUser) { // Move current user to the front of the list + // eslint-disable-next-line no-multi-assign for (index = j = 0, len = users.length; j < len; index = (j += 1)) { obj = users[index]; if (obj.username === firstUser) { @@ -561,6 +562,8 @@ function UsersSelect(currentUser, els, options = {}) { if (firstUser) { // Move current user to the front of the list ref = data.results; + + // eslint-disable-next-line no-multi-assign for (index = j = 0, len = ref.length; j < len; index = (j += 1)) { obj = ref[index]; if (obj.username === firstUser) { 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 1608858da22..c44419d24e6 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/deployment.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/deployment.vue @@ -118,8 +118,8 @@ export default { </a> </template> <span - v-if="hasDeploymentTime" v-tooltip + v-if="hasDeploymentTime" :title="deployment.deployed_at_formatted" class="js-deploy-time" > @@ -127,9 +127,9 @@ export default { </span> <loading-button v-if="deployment.stop_url" + :loading="isStopping" container-class="btn btn-default btn-sm prepend-left-default" label="Stop environment" - :loading="isStopping" @click="stopEnvironment" /> </div> diff --git a/app/assets/javascripts/vue_merge_request_widget/components/memory_usage.vue b/app/assets/javascripts/vue_merge_request_widget/components/memory_usage.vue index f012f9c6772..5e76f6b1cac 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/memory_usage.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/memory_usage.vue @@ -105,7 +105,7 @@ export default { MRWidgetService.fetchMetrics(this.metricsUrl) .then((res) => { if (res.status === statusCodes.NO_CONTENT) { - this.backOffRequestCounter = this.backOffRequestCounter += 1; + this.backOffRequestCounter += 1; /* eslint-disable no-unused-expressions */ this.backOffRequestCounter < 3 ? next() : stop(res); } else { 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 8338fde61c7..22c2f74f900 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 @@ -35,17 +35,17 @@ <template> <a :href="authorUrl" - class="author-link inline" :v-tooltip="showAuthorTooltip" :title="author.name" + class="author-link inline" > <img :src="avatarUrl" class="avatar avatar-inline s16" /> <span - class="author" v-if="showAuthorName" + class="author" > {{ author.name }} </span> 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 644e4b7d81a..ba16cb9e2c8 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,11 +1,15 @@ <script> + import tooltip from '~/vue_shared/directives/tooltip'; import MrWidgetAuthor from './mr_widget_author.vue'; export default { - name: 'MRWidgetAuthorTime', + name: 'MrWidgetAuthorTime', components: { MrWidgetAuthor, }, + directives: { + tooltip, + }, props: { actionText: { type: String, @@ -31,9 +35,8 @@ {{ actionText }} <mr-widget-author :author="author" /> <time + v-tooltip :title="dateTitle" - data-toggle="tooltip" - data-placement="top" data-container="body" > {{ dateReadable }} 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 51a0fda6555..3ce9d8dc26a 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 @@ -59,11 +59,11 @@ export default { <strong> {{ s__("mrWidget|Request to merge") }} <span - class="label-branch js-source-branch" :class="{ 'label-truncated': isSourceBranchLong }" :title="isSourceBranchLong ? mr.sourceBranch : ''" - data-placement="bottom" :v-tooltip="isSourceBranchLong" + class="label-branch js-source-branch" + data-placement="bottom" v-html="mr.sourceBranchLink" > </span> @@ -77,10 +77,10 @@ export default { {{ s__("mrWidget|into") }} <span - class="label-branch" :v-tooltip="isTargetBranchLong" :class="{ 'label-truncatedtooltip': isTargetBranchLong }" :title="isTargetBranchLong ? mr.targetBranch : ''" + class="label-branch" data-placement="bottom" > <a @@ -108,9 +108,9 @@ export default { {{ s__("mrWidget|Web IDE") }} </a> <button + :disabled="mr.sourceBranchRemoved" data-target="#modal_merge_info" data-toggle="modal" - :disabled="mr.sourceBranchRemoved" class="btn btn-sm btn-default inline js-check-out-branch" type="button" > @@ -134,8 +134,8 @@ export default { <ul class="dropdown-menu dropdown-menu-right"> <li> <a - class="js-download-email-patches" :href="mr.emailPatchesPath" + class="js-download-email-patches" download > {{ s__("mrWidget|Email patches") }} @@ -143,8 +143,8 @@ export default { </li> <li> <a - class="js-download-plain-diff" :href="mr.plainDiffPath" + class="js-download-plain-diff" download > {{ s__("mrWidget|Plain diff") }} diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue index 48dff8c4916..2f0b5e12c12 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue @@ -67,8 +67,8 @@ export default { </template> <template v-else-if="hasPipeline"> <a - class="append-right-10" :href="status.details_path" + class="append-right-10" > <ci-icon :status="status" /> </a> @@ -96,8 +96,8 @@ export default { <span class="mr-widget-pipeline-graph"> <span - class="stage-cell" v-if="hasStages" + class="stage-cell" > <div v-for="(stage, i) in pipeline.details.stages" diff --git a/app/assets/javascripts/vue_merge_request_widget/components/source_branch_removal_status.vue b/app/assets/javascripts/vue_merge_request_widget/components/source_branch_removal_status.vue index 460437ceeff..56879c04d16 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/source_branch_removal_status.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/source_branch_removal_status.vue @@ -25,9 +25,9 @@ </span> <i v-tooltip - class="fa fa-question-circle" :title="tooltipTitle" :aria-label="tooltipTitle" + class="fa fa-question-circle" > </i> </p> 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 b404a592234..2133124347c 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 @@ -39,10 +39,10 @@ {{ s__("mrWidget|This merge request failed to be merged automatically") }} </span> <button - @click="refreshWidget" :disabled="isRefreshing" type="button" class="btn btn-sm btn-default" + @click="refreshWidget" > <loading-icon v-if="isRefreshing" diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_checking.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_checking.vue index caeaac75b45..ae6630dcd6f 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_checking.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_checking.vue @@ -11,8 +11,8 @@ <template> <div class="mr-widget-body media"> <status-icon - status="loading" :show-disabled-button="true" + status="loading" /> <div class="media-body space-children"> <span class="bold"> diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_closed.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_closed.vue index 68b691fc914..25a57e520ee 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_closed.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_closed.vue @@ -1,11 +1,11 @@ <script> - import mrWidgetAuthorTime from '../../components/mr_widget_author_time.vue'; + import MrWidgetAuthorTime from '../../components/mr_widget_author_time.vue'; import statusIcon from '../mr_widget_status_icon.vue'; export default { name: 'MRWidgetClosed', components: { - mrWidgetAuthorTime, + MrWidgetAuthorTime, statusIcon, }, props: { 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 5c1500ab801..dff9ec657b9 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 @@ -20,8 +20,8 @@ <template> <div class="mr-widget-body media"> <status-icon - status="warning" :show-disabled-button="true" + status="warning" /> <div class="media-body space-children"> 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 05fecd4de35..c302960f16e 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,5 +1,6 @@ <script> import { n__ } from '~/locale'; +import { stripHtml } from '~/lib/utils/text_utility'; import statusIcon from '../mr_widget_status_icon.vue'; import eventHub from '../../event_hub'; @@ -27,6 +28,9 @@ export default { }, computed: { + mergeError() { + return this.mr.mergeError ? stripHtml(this.mr.mergeError, ' ').trim() : ''; + }, timerText() { return n__( 'Refreshing in a second to show the updated status...', @@ -76,16 +80,16 @@ export default { </template> <template v-else> <status-icon - status="warning" :show-disabled-button="true" + status="warning" /> <div class="media-body space-children"> <span class="bold"> <span - class="has-error-message" v-if="mr.mergeError" + class="has-error-message" > - {{ mr.mergeError }}. + {{ mergeError }}. </span> <span v-else> {{ s__("mrWidget|Merge failed.") }} @@ -97,9 +101,9 @@ export default { </span> </span> <button - @click="refresh" class="btn btn-default btn-sm js-refresh-button" type="button" + @click="refresh" > {{ s__("mrWidget|Refresh now") }} </button> 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 e8352c362d6..0d9a560c88e 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 @@ -90,11 +90,11 @@ </span> <a v-if="mr.canCancelAutomaticMerge" - @click.prevent="cancelAutomaticMerge" :disabled="isCancellingAutoMerge" role="button" href="#" - class="btn btn-sm btn-default js-cancel-auto-merge"> + class="btn btn-sm btn-default js-cancel-auto-merge" + @click.prevent="cancelAutomaticMerge"> <i v-if="isCancellingAutoMerge" class="fa fa-spinner fa-spin" @@ -127,10 +127,10 @@ <a v-if="canRemoveSourceBranch" :disabled="isRemovingSourceBranch" - @click.prevent="removeSourceBranch" role="button" class="btn btn-sm btn-default js-remove-source-branch" href="#" + @click.prevent="removeSourceBranch" > <i v-if="isRemovingSourceBranch" 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 eb581b807a2..1a444c04a1d 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 @@ -4,7 +4,7 @@ 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 MrWidgetAuthorTime from '../../components/mr_widget_author_time.vue'; import statusIcon from '../mr_widget_status_icon.vue'; import eventHub from '../../event_hub'; @@ -14,7 +14,7 @@ tooltip, }, components: { - mrWidgetAuthorTime, + MrWidgetAuthorTime, loadingIcon, statusIcon, ClipboardButton, @@ -116,44 +116,44 @@ :date-readable="mr.metrics.readableMergedAt" /> <a - v-if="mr.canRevertInCurrentMR" v-tooltip + v-if="mr.canRevertInCurrentMR" + :title="revertTitle" class="btn btn-close btn-sm" href="#modal-revert-commit" data-toggle="modal" data-container="body" - :title="revertTitle" > {{ revertLabel }} </a> <a - v-else-if="mr.revertInForkPath" v-tooltip - class="btn btn-close btn-sm" - data-method="post" + v-else-if="mr.revertInForkPath" :href="mr.revertInForkPath" :title="revertTitle" + class="btn btn-close btn-sm" + data-method="post" > {{ revertLabel }} </a> <a - v-if="mr.canCherryPickInCurrentMR" v-tooltip + v-if="mr.canCherryPickInCurrentMR" + :title="cherryPickTitle" class="btn btn-default btn-sm" href="#modal-cherry-pick-commit" data-toggle="modal" data-container="body" - :title="cherryPickTitle" > {{ cherryPickLabel }} </a> <a - v-else-if="mr.cherryPickInForkPath" v-tooltip - class="btn btn-default btn-sm" - data-method="post" + v-else-if="mr.cherryPickInForkPath" :href="mr.cherryPickInForkPath" :title="cherryPickTitle" + class="btn btn-default btn-sm" + data-method="post" > {{ cherryPickLabel }} </a> @@ -173,7 +173,7 @@ </a> <clipboard-button :title="__('Copy commit SHA to clipboard')" - :text="mr.shortMergeCommitSha" + :text="mr.mergeCommitSha" css-class="btn-default btn-transparent btn-clipboard js-mr-merged-copy-sha" /> </p> @@ -186,10 +186,10 @@ > <span>{{ s__("mrWidget|You can remove source branch now") }}</span> <button - @click="removeSourceBranch" :disabled="isMakingRequest" type="button" class="btn btn-sm btn-default js-remove-branch-button" + @click="removeSourceBranch" > {{ s__("mrWidget|Remove Source Branch") }} </button> diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_missing_branch.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_missing_branch.vue index 718c0e4b3c6..b0e96f74626 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_missing_branch.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_missing_branch.vue @@ -39,8 +39,8 @@ <template> <div class="mr-widget-body media"> <status-icon - status="warning" :show-disabled-button="true" + status="warning" /> <div class="media-body space-children"> @@ -51,9 +51,9 @@ {{ missingBranchNameMessage }} <i v-tooltip - class="fa fa-question-circle" :title="message" :aria-label="message" + class="fa fa-question-circle" > </i> </span> diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_not_allowed.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_not_allowed.vue index e4af50b09f8..92eee2cf5dd 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_not_allowed.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_not_allowed.vue @@ -12,8 +12,8 @@ <template> <div class="mr-widget-body media"> <status-icon - status="success" :show-disabled-button="true" + status="success" /> <div class="media-body space-children"> <span class="bold"> diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_pipeline_blocked.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_pipeline_blocked.vue index 6d7cc03f7ad..37ee5215cea 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_pipeline_blocked.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_pipeline_blocked.vue @@ -11,8 +11,8 @@ <template> <div class="mr-widget-body media"> <status-icon - status="warning" :show-disabled-button="true" + status="warning" /> <div class="media-body space-children"> <span class="bold"> diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_rebase.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_rebase.vue index 143fd328d88..2d8c3d6be87 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_rebase.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_rebase.vue @@ -110,9 +110,9 @@ js-toggle-container accept-action media space-children" > <button + :disabled="isMakingRequest" type="button" class="btn btn-sm btn-reopen btn-success qa-mr-rebase-button" - :disabled="isMakingRequest" @click="rebase" > <loading-icon v-if="isMakingRequest" /> diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_squash_before_merge.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_squash_before_merge.vue index 875c3323dbb..25c1044fe2b 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_squash_before_merge.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_squash_before_merge.vue @@ -37,23 +37,23 @@ export default { <div class="accept-control inline"> <label class="merge-param-checkbox"> <input + :disabled="isMergeButtonDisabled" + v-model="squashBeforeMerge" type="checkbox" name="squash" class="qa-squash-checkbox" - :disabled="isMergeButtonDisabled" - v-model="squashBeforeMerge" @change="updateSquashModel" /> {{ __('Squash commits') }} </label> <a + v-tooltip :href="mr.squashBeforeMergeHelpPath" data-title="About this feature" data-placement="bottom" target="_blank" rel="noopener noreferrer nofollow" data-container="body" - v-tooltip > <icon name="question-o" diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/pipeline_failed.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/pipeline_failed.vue index 8d55477929f..2bb1a34412e 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/pipeline_failed.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/pipeline_failed.vue @@ -12,8 +12,8 @@ export default { <template> <div class="mr-widget-body media"> <status-icon - status="warning" :show-disabled-button="true" + status="warning" /> <div class="media-body space-children"> <span class="bold"> 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 3a194320bd8..fe777a07189 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 @@ -235,11 +235,11 @@ export default { <div class="mr-widget-body-controls media space-children"> <span class="btn-group append-bottom-5"> <button - @click="handleMergeButtonClick()" :disabled="isMergeButtonDisabled" :class="mergeButtonClass" type="button" - class="qa-merge-button"> + class="qa-merge-button" + @click="handleMergeButtonClick()"> <i v-if="isMakingRequest" class="fa fa-spinner fa-spin" @@ -265,28 +265,28 @@ export default { role="menu"> <li> <a - @click.prevent="handleMergeButtonClick(true)" class="merge_when_pipeline_succeeds" - href="#"> + href="#" + @click.prevent="handleMergeButtonClick(true)"> <span class="media"> <span - v-html="successSvg" class="merge-opt-icon" - aria-hidden="true"></span> + aria-hidden="true" + v-html="successSvg"></span> <span class="media-body merge-opt-title">Merge when pipeline succeeds</span> </span> </a> </li> <li> <a - @click.prevent="handleMergeButtonClick(false, true)" class="accept-merge-request" - href="#"> + href="#" + @click.prevent="handleMergeButtonClick(false, true)"> <span class="media"> <span - v-html="warningSvg" class="merge-opt-icon" - aria-hidden="true"></span> + aria-hidden="true" + v-html="warningSvg"></span> <span class="media-body merge-opt-title">Merge immediately</span> </span> </a> @@ -299,8 +299,8 @@ export default { <input id="remove-source-branch-input" v-model="removeSourceBranch" - class="js-remove-source-branch-checkbox" :disabled="isRemoveSourceBranchButtonDisabled" + class="js-remove-source-branch-checkbox" type="checkbox"/> Remove source branch </label> @@ -317,10 +317,10 @@ export default { </span> <button v-else - @click="toggleCommitMessageEditor" :disabled="isMergeButtonDisabled" class="js-modify-commit-message-button btn btn-default btn-sm" - type="button"> + type="button" + @click="toggleCommitMessageEditor"> Modify commit message </button> </template> @@ -356,8 +356,8 @@ export default { </p> <div class="hint"> <a - @click.prevent="updateCommitMessage" href="#" + @click.prevent="updateCommitMessage" > {{ commitMessageLinkTitle }} </a> diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/sha_mismatch.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/sha_mismatch.vue index 7cc07401911..16c903c923f 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/sha_mismatch.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/sha_mismatch.vue @@ -12,8 +12,8 @@ export default { <template> <div class="mr-widget-body media"> <status-icon - status="warning" :show-disabled-button="true" + status="warning" /> <div class="media-body space-children"> <span class="bold"> 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 8ea3f22ecc2..5eb2058a03b 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 @@ -18,8 +18,8 @@ export default { <template> <div class="mr-widget-body media"> <status-icon - status="warning" :show-disabled-button="true" + status="warning" /> <div class="media-body space-children"> <span class="bold"> diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/work_in_progress.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/work_in_progress.vue index fe2608e8212..89c9a41f316 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/work_in_progress.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/work_in_progress.vue @@ -43,8 +43,8 @@ export default { <template> <div class="mr-widget-body media"> <status-icon - status="warning" :show-disabled-button="Boolean(mr.removeWIPPath)" + status="warning" /> <div class="media-body space-children"> <span class="bold"> @@ -60,10 +60,10 @@ export default { </span> <button v-if="mr.removeWIPPath" - @click="removeWIP" :disabled="isMakingRequest" type="button" - class="btn btn-default btn-xs js-remove-wip"> + class="btn btn-default btn-sm js-remove-wip" + @click="removeWIP"> <i v-if="isMakingRequest" class="fa fa-spinner fa-spin" 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 f69fe03fcb3..bc4ba3d050b 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 @@ -36,7 +36,7 @@ import { notify, SourceBranchRemovalStatus, } from './dependencies'; -import { setFavicon } from '../lib/utils/common_utils'; +import { setFaviconOverlay } from '../lib/utils/common_utils'; export default { el: '#js-vue-mr-widget', @@ -159,8 +159,9 @@ export default { }, setFaviconHelper() { if (this.mr.ciStatusFaviconPath) { - setFavicon(this.mr.ciStatusFaviconPath); + return setFaviconOverlay(this.mr.ciStatusFaviconPath); } + return Promise.resolve(); }, fetchDeployments() { return this.service.fetchDeployments() @@ -265,10 +266,10 @@ export default { /> <section - v-if="mr.maintainerEditAllowed" + v-if="mr.allowCollaboration" class="mr-info-list mr-links" > - {{ s__("mrWidget|Allows edits from maintainers") }} + {{ s__("mrWidget|Allows commits from members who can merge to the target branch") }} </section> <mr-widget-related-links @@ -282,8 +283,8 @@ export default { /> </div> <div - class="mr-widget-footer" v-if="shouldRenderMergeHelp" + class="mr-widget-footer" > <mr-widget-merge-help /> </div> 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 e5b7e1f1c68..c881cd496d1 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 @@ -26,6 +26,7 @@ export default class MergeRequestStore { this.mergeStatus = data.merge_status; this.commitMessage = data.merge_commit_message; this.shortMergeCommitSha = data.short_merge_commit_sha; + this.mergeCommitSha = data.merge_commit_sha; this.commitMessageWithDescription = data.merge_commit_message_with_description; this.commitsCount = data.commits_count; this.divergedCommitsCount = data.diverged_commits_count; @@ -83,7 +84,7 @@ export default class MergeRequestStore { this.canBeMerged = data.can_be_merged || false; this.isMergeAllowed = data.mergeable || false; this.mergeOngoing = data.merge_ongoing; - this.maintainerEditAllowed = data.allow_maintainer_to_push; + this.allowCollaboration = data.allow_collaboration; // Cherry-pick and Revert actions related this.canCherryPickInCurrentMR = currentUser.can_cherry_pick_on_current_merge_request || false; 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 0d64efcbf68..a2518e2a611 100644 --- a/app/assets/javascripts/vue_shared/components/ci_badge_link.vue +++ b/app/assets/javascripts/vue_shared/components/ci_badge_link.vue @@ -50,9 +50,9 @@ export default { </script> <template> <a + v-tooltip :href="status.details_path" :class="cssClass" - v-tooltip :title="!showText ? status.text : ''" > <ci-icon :status="status" /> diff --git a/app/assets/javascripts/vue_shared/components/clipboard_button.vue b/app/assets/javascripts/vue_shared/components/clipboard_button.vue index cb2cc3901ad..dc5760bce28 100644 --- a/app/assets/javascripts/vue_shared/components/clipboard_button.vue +++ b/app/assets/javascripts/vue_shared/components/clipboard_button.vue @@ -49,14 +49,14 @@ export default { <template> <button - type="button" - class="btn" + v-tooltip :class="cssClass" :title="title" :data-clipboard-text="text" - v-tooltip :data-container="tooltipContainer" :data-placement="tooltipPlacement" + type="button" + class="btn" > <i aria-hidden="true" diff --git a/app/assets/javascripts/vue_shared/components/commit.vue b/app/assets/javascripts/vue_shared/components/commit.vue index 8f250a6c989..13bca99dcb3 100644 --- a/app/assets/javascripts/vue_shared/components/commit.vue +++ b/app/assets/javascripts/vue_shared/components/commit.vue @@ -124,11 +124,11 @@ export default { </div> <a - class="ref-name" - :href="commitRef.ref_url" v-tooltip - data-container="body" + :href="commitRef.ref_url" :title="commitRef.name" + class="ref-name" + data-container="body" > {{ commitRef.name }} </a> @@ -139,8 +139,8 @@ export default { /> <a - class="commit-sha" :href="commitUrl" + class="commit-sha" > {{ shortSha }} </a> @@ -152,15 +152,15 @@ export default { > <user-avatar-link v-if="hasAuthor" - class="avatar-image-container" :link-href="author.path" :img-src="author.avatar_url" :img-alt="userImageAltDescription" :tooltip-text="author.username" + class="avatar-image-container" /> <a - class="commit-row-message" :href="commitUrl" + class="commit-row-message" > {{ title }} </a> 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 7b5367ac19b..f1ef50d0e3d 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 @@ -32,7 +32,10 @@ export default { <div class="file-container"> <div class="file-content"> <p class="prepend-top-10 file-info"> - {{ fileName }} ({{ fileSizeReadable }}) + {{ fileName }} + <template v-if="fileSize > 0"> + ({{ fileSizeReadable }}) + </template> </p> <a :href="path" @@ -41,9 +44,9 @@ export default { download target="_blank"> <icon + :size="16" name="download" css-classes="float-left append-right-8" - :size="16" /> {{ __('Download') }} </a> diff --git a/app/assets/javascripts/vue_shared/components/content_viewer/viewers/image_viewer.vue b/app/assets/javascripts/vue_shared/components/content_viewer/viewers/image_viewer.vue index a5999f909ca..6851029018a 100644 --- a/app/assets/javascripts/vue_shared/components/content_viewer/viewers/image_viewer.vue +++ b/app/assets/javascripts/vue_shared/components/content_viewer/viewers/image_viewer.vue @@ -1,4 +1,5 @@ <script> +import _ from 'underscore'; import { numberToHumanSize } from '../../../../lib/utils/number_utils'; export default { @@ -12,6 +13,10 @@ export default { required: false, default: 0, }, + renderInfo: { + type: Boolean, + default: true, + }, }, data() { return { @@ -26,14 +31,34 @@ export default { return numberToHumanSize(this.fileSize); }, }, + beforeDestroy() { + window.removeEventListener('resize', this.resizeThrottled, false); + }, + mounted() { + // The onImgLoad may have happened before the control was actually mounted + this.onImgLoad(); + this.resizeThrottled = _.throttle(this.onImgLoad, 400); + window.addEventListener('resize', this.resizeThrottled, false); + }, methods: { onImgLoad() { const contentImg = this.$refs.contentImg; - this.isZoomable = - contentImg.naturalWidth > contentImg.width || contentImg.naturalHeight > contentImg.height; - this.width = contentImg.naturalWidth; - this.height = contentImg.naturalHeight; + if (contentImg) { + this.isZoomable = + contentImg.naturalWidth > contentImg.width || + contentImg.naturalHeight > contentImg.height; + + this.width = contentImg.naturalWidth; + this.height = contentImg.naturalHeight; + + this.$emit('imgLoaded', { + width: this.width, + height: this.height, + renderedWidth: contentImg.clientWidth, + renderedHeight: contentImg.clientHeight, + }); + } }, onImgClick() { if (this.isZoomable) this.isZoomed = !this.isZoomed; @@ -47,20 +72,22 @@ export default { <div class="file-content image_file"> <img ref="contentImg" - :class="{ 'isZoomable': isZoomable, 'isZoomed': isZoomed }" + :class="{ 'is-zoomable': isZoomable, 'is-zoomed': isZoomed }" :src="path" :alt="path" @load="onImgLoad" @click="onImgClick"/> - <p class="file-info prepend-top-10"> + <p + v-if="renderInfo" + class="file-info prepend-top-10"> <template v-if="fileSize>0"> {{ fileSizeReadable }} </template> <template v-if="fileSize>0 && width && height"> - - + | </template> <template v-if="width && height"> - {{ width }} x {{ height }} + W: {{ width }} | H: {{ height }} </template> </p> </div> diff --git a/app/assets/javascripts/vue_shared/components/deprecated_modal.vue b/app/assets/javascripts/vue_shared/components/deprecated_modal.vue index 424af5a0293..9c1e5c68649 100644 --- a/app/assets/javascripts/vue_shared/components/deprecated_modal.vue +++ b/app/assets/javascripts/vue_shared/components/deprecated_modal.vue @@ -86,8 +86,8 @@ <div class="modal-open"> <div :id="id" - class="modal" :class="id ? '' : 'd-block'" + class="modal" role="dialog" tabindex="-1" > @@ -105,9 +105,9 @@ <button type="button" class="close float-right" - @click="emitCancel($event)" data-dismiss="modal" aria-label="Close" + @click="emitCancel($event)" > <span aria-hidden="true">×</span> </button> @@ -115,22 +115,22 @@ </div> <div class="modal-body"> <slot - name="body" :text="text" + name="body" > <p>{{ text }}</p> </slot> </div> <div - class="modal-footer" v-if="!hideFooter" + class="modal-footer" > <button + :class="btnCancelKindClass" type="button" class="btn" - :class="btnCancelKindClass" - @click="emitCancel($event)" data-dismiss="modal" + @click="emitCancel($event)" > {{ closeButtonLabel }} </button> @@ -151,12 +151,12 @@ <button v-if="primaryButtonLabel" - type="button" - class="btn js-primary-button" :disabled="submitDisabled" :class="btnKindClass" - @click="emitSubmit($event)" + type="button" + class="btn js-primary-button" data-dismiss="modal" + @click="emitSubmit($event)" > {{ primaryButtonLabel }} </button> diff --git a/app/assets/javascripts/vue_shared/components/diff_viewer/constants.js b/app/assets/javascripts/vue_shared/components/diff_viewer/constants.js new file mode 100644 index 00000000000..6c1840361af --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/diff_viewer/constants.js @@ -0,0 +1,12 @@ +export const diffModes = { + replaced: 'replaced', + new: 'new', + deleted: 'deleted', + renamed: 'renamed', +}; + +export const imageViewMode = { + twoup: 'twoup', + swipe: 'swipe', + onion: 'onion', +}; diff --git a/app/assets/javascripts/vue_shared/components/diff_viewer/diff_viewer.vue b/app/assets/javascripts/vue_shared/components/diff_viewer/diff_viewer.vue new file mode 100644 index 00000000000..2c47f5b9b35 --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/diff_viewer/diff_viewer.vue @@ -0,0 +1,70 @@ +<script> +import { viewerInformationForPath } from '../content_viewer/lib/viewer_utils'; +import ImageDiffViewer from './viewers/image_diff_viewer.vue'; +import DownloadDiffViewer from './viewers/download_diff_viewer.vue'; + +export default { + props: { + diffMode: { + type: String, + required: true, + }, + newPath: { + type: String, + required: true, + }, + newSha: { + type: String, + required: true, + }, + oldPath: { + type: String, + required: true, + }, + oldSha: { + type: String, + required: true, + }, + projectPath: { + type: String, + required: false, + default: '', + }, + }, + computed: { + viewer() { + if (!this.newPath) return null; + + const previewInfo = viewerInformationForPath(this.newPath); + if (!previewInfo) return DownloadDiffViewer; + + switch (previewInfo.id) { + case 'image': + return ImageDiffViewer; + default: + return DownloadDiffViewer; + } + }, + fullOldPath() { + return `${gon.relative_url_root}/${this.projectPath}/raw/${this.oldSha}/${this.oldPath}`; + }, + fullNewPath() { + return `${gon.relative_url_root}/${this.projectPath}/raw/${this.newSha}/${this.newPath}`; + }, + }, +}; +</script> + +<template> + <div + v-if="viewer" + class="diff-file preview-container"> + <component + :is="viewer" + :diff-mode="diffMode" + :new-path="fullNewPath" + :old-path="fullOldPath" + :project-path="projectPath" + /> + </div> +</template> diff --git a/app/assets/javascripts/vue_shared/components/diff_viewer/viewers/download_diff_viewer.vue b/app/assets/javascripts/vue_shared/components/diff_viewer/viewers/download_diff_viewer.vue new file mode 100644 index 00000000000..50389b6ae63 --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/diff_viewer/viewers/download_diff_viewer.vue @@ -0,0 +1,69 @@ +<script> +import DownloadViewer from '../../content_viewer/viewers/download_viewer.vue'; +import { diffModes } from '../constants'; + +export default { + components: { + DownloadViewer, + }, + props: { + diffMode: { + type: String, + required: true, + }, + newPath: { + type: String, + required: true, + }, + oldPath: { + type: String, + required: true, + }, + projectPath: { + type: String, + required: false, + default: '', + }, + }, + diffModes, +}; +</script> + +<template> + <div class="diff-file-container"> + <div class="diff-viewer"> + <div + v-if="diffMode === $options.diffModes.replaced" + class="two-up view row"> + <div class="col-sm-6 deleted"> + <download-viewer + :path="oldPath" + :project-path="projectPath" + /> + </div> + <div class="col-sm-6 added"> + <download-viewer + :path="newPath" + :project-path="projectPath" + /> + </div> + </div> + <div + v-else-if="diffMode === $options.diffModes.new" + class="added"> + <download-viewer + :path="newPath" + :project-path="projectPath" + /> + </div> + <div + v-else + class="deleted"> + <download-viewer + :path="oldPath" + :project-path="projectPath" + /> + </div> + </div> + </div> +</template> diff --git a/app/assets/javascripts/vue_shared/components/diff_viewer/viewers/image_diff/onion_skin_viewer.vue b/app/assets/javascripts/vue_shared/components/diff_viewer/viewers/image_diff/onion_skin_viewer.vue new file mode 100644 index 00000000000..38e881d17a2 --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/diff_viewer/viewers/image_diff/onion_skin_viewer.vue @@ -0,0 +1,160 @@ +<script> +import { pixeliseValue } from '../../../lib/utils/dom_utils'; +import ImageViewer from '../../../content_viewer/viewers/image_viewer.vue'; + +export default { + components: { + ImageViewer, + }, + props: { + newPath: { + type: String, + required: true, + }, + oldPath: { + type: String, + required: true, + }, + projectPath: { + type: String, + required: false, + default: '', + }, + }, + data() { + return { + onionMaxWidth: undefined, + onionMaxHeight: undefined, + onionOldImgInfo: null, + onionNewImgInfo: null, + onionDraggerPos: 0, + onionOpacity: 1, + dragging: false, + }; + }, + computed: { + onionMaxPixelWidth() { + return pixeliseValue(this.onionMaxWidth); + }, + onionMaxPixelHeight() { + return pixeliseValue(this.onionMaxHeight); + }, + onionDraggerPixelPos() { + return pixeliseValue(this.onionDraggerPos); + }, + }, + beforeDestroy() { + document.body.removeEventListener('mouseup', this.stopDrag); + this.$refs.dragger.removeEventListener('mousedown', this.startDrag); + }, + methods: { + dragMove(e) { + if (!this.dragging) return; + const left = e.pageX - this.$refs.dragTrack.getBoundingClientRect().left; + const dragTrackWidth = + this.$refs.dragTrack.clientWidth - this.$refs.dragger.clientWidth || 100; + + let leftValue = left; + if (leftValue < 0) leftValue = 0; + if (leftValue > dragTrackWidth) leftValue = dragTrackWidth; + + this.onionOpacity = left / dragTrackWidth; + this.onionDraggerPos = leftValue; + }, + startDrag() { + this.dragging = true; + document.body.style.userSelect = 'none'; + document.body.addEventListener('mousemove', this.dragMove); + }, + stopDrag() { + this.dragging = false; + document.body.style.userSelect = ''; + document.body.removeEventListener('mousemove', this.dragMove); + }, + prepareOnionSkin() { + if (this.onionOldImgInfo && this.onionNewImgInfo) { + this.onionMaxWidth = Math.max( + this.onionOldImgInfo.renderedWidth, + this.onionNewImgInfo.renderedWidth, + ); + this.onionMaxHeight = Math.max( + this.onionOldImgInfo.renderedHeight, + this.onionNewImgInfo.renderedHeight, + ); + + this.onionOpacity = 1; + this.onionDraggerPos = + this.$refs.dragTrack.clientWidth - this.$refs.dragger.clientWidth || 100; + + document.body.addEventListener('mouseup', this.stopDrag); + } + }, + onionNewImgLoaded(imgInfo) { + this.onionNewImgInfo = imgInfo; + this.prepareOnionSkin(); + }, + onionOldImgLoaded(imgInfo) { + this.onionOldImgInfo = imgInfo; + this.prepareOnionSkin(); + }, + }, +}; +</script> + +<template> + <div class="onion-skin view"> + <div + :style="{ + 'width': onionMaxPixelWidth, + 'height': onionMaxPixelHeight, + 'user-select': dragging === true ? 'none' : '', + }" + class="onion-skin-frame"> + <div + :style="{ + 'width': onionMaxPixelWidth, + 'height': onionMaxPixelHeight, + }" + class="frame deleted"> + <image-viewer + key="onionOldImg" + :render-info="false" + :path="oldPath" + :project-path="projectPath" + @imgLoaded="onionOldImgLoaded" + /> + </div> + <div + ref="addedFrame" + :style="{ + 'opacity': onionOpacity, + 'width': onionMaxPixelWidth, + 'height': onionMaxPixelHeight, + }" + class="added frame"> + <image-viewer + key="onionNewImg" + :render-info="false" + :path="newPath" + :project-path="projectPath" + @imgLoaded="onionNewImgLoaded" + /> + </div> + <div class="controls"> + <div class="transparent"></div> + <div + ref="dragTrack" + class="drag-track" + @mousedown="startDrag" + @mouseup="stopDrag"> + <div + ref="dragger" + :style="{ 'left': onionDraggerPixelPos }" + class="dragger"> + </div> + </div> + <div class="opaque"></div> + </div> + </div> + </div> +</template> diff --git a/app/assets/javascripts/vue_shared/components/diff_viewer/viewers/image_diff/swipe_viewer.vue b/app/assets/javascripts/vue_shared/components/diff_viewer/viewers/image_diff/swipe_viewer.vue new file mode 100644 index 00000000000..86366c799a2 --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/diff_viewer/viewers/image_diff/swipe_viewer.vue @@ -0,0 +1,158 @@ +<script> +import _ from 'underscore'; +import { pixeliseValue } from '../../../lib/utils/dom_utils'; +import ImageViewer from '../../../content_viewer/viewers/image_viewer.vue'; + +export default { + components: { + ImageViewer, + }, + props: { + newPath: { + type: String, + required: true, + }, + oldPath: { + type: String, + required: true, + }, + projectPath: { + type: String, + required: false, + default: '', + }, + }, + data() { + return { + dragging: false, + swipeOldImgInfo: null, + swipeNewImgInfo: null, + swipeMaxWidth: undefined, + swipeMaxHeight: undefined, + swipeBarPos: 1, + swipeWrapWidth: undefined, + }; + }, + computed: { + swipeMaxPixelWidth() { + return pixeliseValue(this.swipeMaxWidth); + }, + swipeMaxPixelHeight() { + return pixeliseValue(this.swipeMaxHeight); + }, + swipeWrapPixelWidth() { + return pixeliseValue(this.swipeWrapWidth); + }, + swipeBarPixelPos() { + return pixeliseValue(this.swipeBarPos); + }, + }, + beforeDestroy() { + window.removeEventListener('resize', this.resizeThrottled, false); + document.body.removeEventListener('mouseup', this.stopDrag); + document.body.removeEventListener('mousemove', this.dragMove); + }, + mounted() { + window.addEventListener('resize', this.resize, false); + }, + methods: { + dragMove(e) { + if (!this.dragging) return; + + let leftValue = e.pageX - this.$refs.swipeFrame.getBoundingClientRect().left; + const spaceLeft = 20; + const { clientWidth } = this.$refs.swipeFrame; + if (leftValue <= 0) { + leftValue = 0; + } else if (leftValue > clientWidth - spaceLeft) { + leftValue = clientWidth - spaceLeft; + } + + this.swipeWrapWidth = this.swipeMaxWidth - leftValue; + this.swipeBarPos = leftValue; + }, + startDrag() { + this.dragging = true; + document.body.style.userSelect = 'none'; + document.body.addEventListener('mousemove', this.dragMove); + }, + stopDrag() { + this.dragging = false; + document.body.style.userSelect = ''; + document.body.removeEventListener('mousemove', this.dragMove); + }, + prepareSwipe() { + if (this.swipeOldImgInfo && this.swipeNewImgInfo) { + // Add 2 for border width + this.swipeMaxWidth = + Math.max(this.swipeOldImgInfo.renderedWidth, this.swipeNewImgInfo.renderedWidth) + 2; + this.swipeWrapWidth = this.swipeMaxWidth; + this.swipeMaxHeight = + Math.max(this.swipeOldImgInfo.renderedHeight, this.swipeNewImgInfo.renderedHeight) + 2; + + document.body.addEventListener('mouseup', this.stopDrag); + } + }, + swipeNewImgLoaded(imgInfo) { + this.swipeNewImgInfo = imgInfo; + this.prepareSwipe(); + }, + swipeOldImgLoaded(imgInfo) { + this.swipeOldImgInfo = imgInfo; + this.prepareSwipe(); + }, + resize: _.throttle(function throttledResize() { + this.swipeBarPos = 0; + }, 400), + }, +}; +</script> + +<template> + <div class="swipe view"> + <div + ref="swipeFrame" + :style="{ + 'width': swipeMaxPixelWidth, + 'height': swipeMaxPixelHeight, + }" + class="swipe-frame"> + <div class="frame deleted"> + <image-viewer + key="swipeOldImg" + ref="swipeOldImg" + :render-info="false" + :path="oldPath" + :project-path="projectPath" + @imgLoaded="swipeOldImgLoaded" + /> + </div> + <div + ref="swipeWrap" + :style="{ + 'width': swipeWrapPixelWidth, + 'height': swipeMaxPixelHeight, + }" + class="swipe-wrap"> + <div class="frame added"> + <image-viewer + key="swipeNewImg" + :render-info="false" + :path="newPath" + :project-path="projectPath" + @imgLoaded="swipeNewImgLoaded" + /> + </div> + </div> + <span + ref="swipeBar" + :style="{ 'left': swipeBarPixelPos }" + class="swipe-bar" + @mousedown="startDrag" + @mouseup="stopDrag"> + <span class="top-handle"></span> + <span class="bottom-handle"></span> + </span> + </div> + </div> +</template> diff --git a/app/assets/javascripts/vue_shared/components/diff_viewer/viewers/image_diff/two_up_viewer.vue b/app/assets/javascripts/vue_shared/components/diff_viewer/viewers/image_diff/two_up_viewer.vue new file mode 100644 index 00000000000..9c19266ecdf --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/diff_viewer/viewers/image_diff/two_up_viewer.vue @@ -0,0 +1,41 @@ +<script> +import ImageViewer from '../../../content_viewer/viewers/image_viewer.vue'; + +export default { + components: { + ImageViewer, + }, + props: { + newPath: { + type: String, + required: true, + }, + oldPath: { + type: String, + required: true, + }, + projectPath: { + type: String, + required: false, + default: '', + }, + }, +}; +</script> + +<template> + <div class="two-up view row"> + <div class="col-sm-6 frame deleted"> + <image-viewer + :path="oldPath" + :project-path="projectPath" + /> + </div> + <div class="col-sm-6 frame added"> + <image-viewer + :path="newPath" + :project-path="projectPath" + /> + </div> + </div> +</template> diff --git a/app/assets/javascripts/vue_shared/components/diff_viewer/viewers/image_diff_viewer.vue b/app/assets/javascripts/vue_shared/components/diff_viewer/viewers/image_diff_viewer.vue new file mode 100644 index 00000000000..1af85283277 --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/diff_viewer/viewers/image_diff_viewer.vue @@ -0,0 +1,109 @@ +<script> +import ImageViewer from '../../content_viewer/viewers/image_viewer.vue'; +import TwoUpViewer from './image_diff/two_up_viewer.vue'; +import SwipeViewer from './image_diff/swipe_viewer.vue'; +import OnionSkinViewer from './image_diff/onion_skin_viewer.vue'; +import { diffModes, imageViewMode } from '../constants'; + +export default { + components: { + ImageViewer, + TwoUpViewer, + SwipeViewer, + OnionSkinViewer, + }, + props: { + diffMode: { + type: String, + required: true, + }, + newPath: { + type: String, + required: true, + }, + oldPath: { + type: String, + required: true, + }, + projectPath: { + type: String, + required: false, + default: '', + }, + }, + data() { + return { + mode: imageViewMode.twoup, + }; + }, + methods: { + changeMode(newMode) { + this.mode = newMode; + }, + }, + diffModes, + imageViewMode, +}; +</script> + +<template> + <div class="diff-file-container"> + <div + v-if="diffMode === $options.diffModes.replaced" + class="diff-viewer"> + <div class="image js-replaced-image"> + <two-up-viewer + v-if="mode === $options.imageViewMode.twoup" + v-bind="$props"/> + <swipe-viewer + v-else-if="mode === $options.imageViewMode.swipe" + v-bind="$props"/> + <onion-skin-viewer + v-else-if="mode === $options.imageViewMode.onion" + v-bind="$props"/> + </div> + <div class="view-modes"> + <ul class="view-modes-menu"> + <li + :class="{ + active: mode === $options.imageViewMode.twoup + }" + @click="changeMode($options.imageViewMode.twoup)"> + {{ s__('ImageDiffViewer|2-up') }} + </li> + <li + :class="{ + active: mode === $options.imageViewMode.swipe + }" + @click="changeMode($options.imageViewMode.swipe)"> + {{ s__('ImageDiffViewer|Swipe') }} + </li> + <li + :class="{ + active: mode === $options.imageViewMode.onion + }" + @click="changeMode($options.imageViewMode.onion)"> + {{ s__('ImageDiffViewer|Onion skin') }} + </li> + </ul> + </div> + <div class="note-container"></div> + </div> + <div + v-else-if="diffMode === $options.diffModes.new" + class="diff-viewer added"> + <image-viewer + :path="newPath" + :project-path="projectPath" + /> + </div> + <div + v-else + class="diff-viewer deleted"> + <image-viewer + :path="oldPath" + :project-path="projectPath" + /> + </div> + </div> +</template> diff --git a/app/assets/javascripts/vue_shared/components/dropdown/dropdown_button.vue b/app/assets/javascripts/vue_shared/components/dropdown/dropdown_button.vue index c159333d89a..3cba0c5e633 100644 --- a/app/assets/javascripts/vue_shared/components/dropdown/dropdown_button.vue +++ b/app/assets/javascripts/vue_shared/components/dropdown/dropdown_button.vue @@ -28,11 +28,11 @@ export default { <template> <button + :disabled="isDisabled || isLoading" class="dropdown-menu-toggle dropdown-menu-full-width" type="button" data-toggle="dropdown" aria-expanded="false" - :disabled="isDisabled || isLoading" > <loading-icon v-show="isLoading" @@ -42,8 +42,8 @@ export default { {{ toggleText }} </span> <span - class="dropdown-toggle-icon" v-show="!isLoading" + class="dropdown-toggle-icon" > <i class="fa fa-chevron-down" diff --git a/app/assets/javascripts/vue_shared/components/dropdown/dropdown_hidden_input.vue b/app/assets/javascripts/vue_shared/components/dropdown/dropdown_hidden_input.vue index 1fe27eb97ab..b7a4613bdd2 100644 --- a/app/assets/javascripts/vue_shared/components/dropdown/dropdown_hidden_input.vue +++ b/app/assets/javascripts/vue_shared/components/dropdown/dropdown_hidden_input.vue @@ -15,8 +15,8 @@ export default { <template> <input - type="hidden" :name="name" :value="value" + type="hidden" /> </template> diff --git a/app/assets/javascripts/vue_shared/components/dropdown/dropdown_search_input.vue b/app/assets/javascripts/vue_shared/components/dropdown/dropdown_search_input.vue index c2145a26e64..7f1912f6405 100644 --- a/app/assets/javascripts/vue_shared/components/dropdown/dropdown_search_input.vue +++ b/app/assets/javascripts/vue_shared/components/dropdown/dropdown_search_input.vue @@ -23,10 +23,10 @@ export default { <template> <div class="dropdown-input"> <input - class="dropdown-input-field" - type="search" v-model="searchQuery" :placeholder="placeholderText" + class="dropdown-input-field" + type="search" autocomplete="off" /> <i diff --git a/app/assets/javascripts/vue_shared/components/expand_button.vue b/app/assets/javascripts/vue_shared/components/expand_button.vue index 9295be3e2b2..0fdea651130 100644 --- a/app/assets/javascripts/vue_shared/components/expand_button.vue +++ b/app/assets/javascripts/vue_shared/components/expand_button.vue @@ -32,10 +32,10 @@ export default { <template> <span> <button - type="button" v-show="isCollapsed" - class="text-expander btn-blank" :aria-label="ariaLabel" + type="button" + class="text-expander btn-blank" @click="onClick"> ... </button> diff --git a/app/assets/javascripts/vue_shared/components/file_icon.vue b/app/assets/javascripts/vue_shared/components/file_icon.vue index be2755452e2..878c805ada5 100644 --- a/app/assets/javascripts/vue_shared/components/file_icon.vue +++ b/app/assets/javascripts/vue_shared/components/file_icon.vue @@ -73,8 +73,8 @@ export default { <template> <span> <svg - :class="[iconSizeClass, cssClasses]" v-if="!loading && !folder" + :class="[iconSizeClass, cssClasses]" > <use v-bind="{ 'xlink:href':spriteHref }" /> </svg> diff --git a/app/assets/javascripts/vue_shared/components/file_icon/file_icon_map.js b/app/assets/javascripts/vue_shared/components/file_icon/file_icon_map.js index 9ffbaae3ea5..9bca1993ccc 100644 --- a/app/assets/javascripts/vue_shared/components/file_icon/file_icon_map.js +++ b/app/assets/javascripts/vue_shared/components/file_icon/file_icon_map.js @@ -513,7 +513,7 @@ const fileNameIcons = { 'credits.md': 'credits', 'credits.md.rendered': 'credits', '.flowconfig': 'flow', - 'favicon.ico': 'favicon', + 'favicon.png': 'favicon', 'karma.conf.js': 'karma', 'karma.conf.ts': 'karma', 'karma.conf.coffee': 'karma', diff --git a/app/assets/javascripts/vue_shared/components/gl_modal.vue b/app/assets/javascripts/vue_shared/components/gl_modal.vue index d5d5a7d3798..b298b989203 100644 --- a/app/assets/javascripts/vue_shared/components/gl_modal.vue +++ b/app/assets/javascripts/vue_shared/components/gl_modal.vue @@ -1,15 +1,21 @@ <script> const buttonVariants = ['danger', 'primary', 'success', 'warning']; +const sizeVariants = ['sm', 'md', 'lg', 'xl']; export default { name: 'GlModal', - props: { id: { type: String, required: false, default: null, }, + modalSize: { + type: String, + required: false, + default: 'md', + validator: value => sizeVariants.includes(value), + }, headerTitleText: { type: String, required: false, @@ -27,7 +33,11 @@ export default { default: '', }, }, - + computed: { + modalSizeClass() { + return this.modalSize === 'md' ? '' : `modal-${this.modalSize}`; + }, + }, methods: { emitCancel(event) { this.$emit('cancel', event); @@ -47,6 +57,7 @@ export default { role="dialog" > <div + :class="modalSizeClass" class="modal-dialog" role="document" > @@ -59,10 +70,10 @@ export default { </slot> </h4> <button + :aria-label="s__('Modal|Close')" type="button" class="close js-modal-close-action" data-dismiss="modal" - :aria-label="s__('Modal|Close')" @click="emitCancel($event)" > <span aria-hidden="true">×</span> @@ -85,9 +96,9 @@ export default { {{ s__('Modal|Cancel') }} </button> <button + :class="`btn-${footerPrimaryButtonVariant}`" type="button" class="btn js-modal-primary-action" - :class="`btn-${footerPrimaryButtonVariant}`" data-dismiss="modal" @click="emitSubmit($event)" > 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 ca17fa06a00..62d35f6547d 100644 --- a/app/assets/javascripts/vue_shared/components/header_ci_component.vue +++ b/app/assets/javascripts/vue_shared/components/header_ci_component.vue @@ -117,8 +117,8 @@ export default { </section> <section - class="header-action-buttons" v-if="actions.length" + class="header-action-buttons" > <template v-for="(action, i) in actions" @@ -135,21 +135,21 @@ export default { <a v-else-if="action.type === 'ujs-link'" :href="action.path" - data-method="post" - rel="nofollow" :class="action.cssClass" :key="i" + data-method="post" + rel="nofollow" > {{ action.label }} </a> <button v-else-if="action.type === 'button'" - @click="onClickAction(action)" :disabled="action.isLoading" :class="action.cssClass" - type="button" :key="i" + type="button" + @click="onClickAction(action)" > {{ action.label }} <i @@ -162,11 +162,11 @@ export default { </template> <button v-if="hasSidebarButton" + id="toggleSidebar" type="button" 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" > <i class="fa fa-angle-double-left" diff --git a/app/assets/javascripts/vue_shared/components/identicon.vue b/app/assets/javascripts/vue_shared/components/identicon.vue index 23010f40f26..4ffc811e714 100644 --- a/app/assets/javascripts/vue_shared/components/identicon.vue +++ b/app/assets/javascripts/vue_shared/components/identicon.vue @@ -43,9 +43,9 @@ export default { <template> <div - class="avatar identicon" :class="sizeClass" - :style="identiconStyles"> + :style="identiconStyles" + class="avatar identicon"> {{ identiconTitle }} </div> </template> diff --git a/app/assets/javascripts/vue_shared/components/issue/issue_warning.vue b/app/assets/javascripts/vue_shared/components/issue/issue_warning.vue index 3d39b3ab173..ca8ce554588 100644 --- a/app/assets/javascripts/vue_shared/components/issue/issue_warning.vue +++ b/app/assets/javascripts/vue_shared/components/issue/issue_warning.vue @@ -33,11 +33,11 @@ <template> <div class="issuable-note-warning"> <icon + v-if="!isLockedAndConfidential" :name="warningIcon" :size="16" class="icon inline" aria-hidden="true" - v-if="!isLockedAndConfidential" /> <span v-if="isLockedAndConfidential"> diff --git a/app/assets/javascripts/vue_shared/components/lib/utils/dom_utils.js b/app/assets/javascripts/vue_shared/components/lib/utils/dom_utils.js new file mode 100644 index 00000000000..02f28da8bb0 --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/lib/utils/dom_utils.js @@ -0,0 +1,5 @@ +export function pixeliseValue(val) { + return val ? `${val}px` : ''; +} + +export default {}; diff --git a/app/assets/javascripts/vue_shared/components/loading_button.vue b/app/assets/javascripts/vue_shared/components/loading_button.vue index 88c13a1f340..2ff0c056b9c 100644 --- a/app/assets/javascripts/vue_shared/components/loading_button.vue +++ b/app/assets/javascripts/vue_shared/components/loading_button.vue @@ -54,19 +54,19 @@ <template> <button - @click="onClick" - type="button" :class="containerClass" :disabled="loading || disabled" + type="button" + @click="onClick" > <transition name="fade"> <loading-icon v-if="loading" :inline="true" - class="js-loading-button-icon" :class="{ 'append-right-5': label }" + class="js-loading-button-icon" /> </transition> <transition name="fade"> diff --git a/app/assets/javascripts/vue_shared/components/loading_icon.vue b/app/assets/javascripts/vue_shared/components/loading_icon.vue index 12a75e016d7..db22c5f02cd 100644 --- a/app/assets/javascripts/vue_shared/components/loading_icon.vue +++ b/app/assets/javascripts/vue_shared/components/loading_icon.vue @@ -35,10 +35,10 @@ :is="rootElementType" class="loading-container text-center"> <i - class="fa fa-spin fa-spinner" :class="cssClass" - aria-hidden="true" :aria-label="label" + class="fa fa-spin fa-spinner" + aria-hidden="true" > </i> </component> diff --git a/app/assets/javascripts/vue_shared/components/markdown/field.vue b/app/assets/javascripts/vue_shared/components/markdown/field.vue index 12c7d125062..05e8ed2da2c 100644 --- a/app/assets/javascripts/vue_shared/components/markdown/field.vue +++ b/app/assets/javascripts/vue_shared/components/markdown/field.vue @@ -117,17 +117,17 @@ <template> <div - class="md-area js-vue-markdown-field" + ref="gl-form" :class="{ 'prepend-top-default append-bottom-default': addSpacingClasses }" - ref="gl-form"> + class="md-area js-vue-markdown-field"> <markdown-header :preview-markdown="previewMarkdown" @preview-markdown="showPreviewTab" @write-markdown="showWriteTab" /> <div - class="md-write-holder" v-show="!previewMarkdown" + class="md-write-holder" > <div class="zen-backdrop"> <slot name="textarea"></slot> @@ -137,8 +137,8 @@ aria-label="Enter zen mode" > <icon - name="screen-normal" :size="32" + name="screen-normal" /> </a> <markdown-toolbar @@ -149,8 +149,8 @@ </div> </div> <div - class="md md-preview-holder md-preview" v-show="previewMarkdown" + class="md md-preview-holder md-preview" > <div ref="markdown-preview" @@ -164,8 +164,8 @@ <template v-if="previewMarkdown && !markdownPreviewLoading"> <div v-if="referencedCommands" - v-html="referencedCommands" class="referenced-commands" + v-html="referencedCommands" > </div> <div diff --git a/app/assets/javascripts/vue_shared/components/markdown/header.vue b/app/assets/javascripts/vue_shared/components/markdown/header.vue index db453c30576..ee3628b1e3f 100644 --- a/app/assets/javascripts/vue_shared/components/markdown/header.vue +++ b/app/assets/javascripts/vue_shared/components/markdown/header.vue @@ -54,8 +54,8 @@ <div class="md-header"> <ul class="nav-links clearfix"> <li - class="md-header-tab" :class="{ active: !previewMarkdown }" + class="md-header-tab" > <a class="js-write-link" @@ -67,8 +67,8 @@ </a> </li> <li - class="md-header-tab" :class="{ active: previewMarkdown }" + class="md-header-tab" > <a class="js-preview-link" @@ -80,8 +80,8 @@ </a> </li> <li - class="md-header-toolbar" :class="{ active: !previewMarkdown }" + class="md-header-toolbar" > <toolbar-button tag="**" @@ -94,8 +94,8 @@ icon="italic" /> <toolbar-button - tag="> " :prepend="true" + tag="> " button-title="Insert a quote" icon="quote" /> @@ -106,20 +106,20 @@ icon="code" /> <toolbar-button - tag="* " :prepend="true" + tag="* " button-title="Add a bullet list" icon="list-bulleted" /> <toolbar-button - tag="1. " :prepend="true" + tag="1. " button-title="Add a numbered list" icon="list-numbered" /> <toolbar-button - tag="* [ ] " :prepend="true" + tag="* [ ] " button-title="Add a task list" icon="task-done" /> diff --git a/app/assets/javascripts/vue_shared/components/markdown/toolbar_button.vue b/app/assets/javascripts/vue_shared/components/markdown/toolbar_button.vue index 2d2d69ebeb2..9f1e009efdd 100644 --- a/app/assets/javascripts/vue_shared/components/markdown/toolbar_button.vue +++ b/app/assets/javascripts/vue_shared/components/markdown/toolbar_button.vue @@ -39,15 +39,15 @@ <template> <button v-tooltip - type="button" - class="toolbar-btn js-md" - tabindex="-1" - data-container="body" :data-md-tag="tag" :data-md-block="tagBlock" :data-md-prepend="prepend" :title="buttonTitle" :aria-label="buttonTitle" + type="button" + class="toolbar-btn js-md" + tabindex="-1" + data-container="body" > <icon :name="icon" diff --git a/app/assets/javascripts/vue_shared/components/memory_graph.vue b/app/assets/javascripts/vue_shared/components/memory_graph.vue index b07f6b07afe..522091ea889 100644 --- a/app/assets/javascripts/vue_shared/components/memory_graph.vue +++ b/app/assets/javascripts/vue_shared/components/memory_graph.vue @@ -113,19 +113,19 @@ export default { <template> <div class="memory-graph-container"> <svg - class="has-tooltip" :title="getFormattedMedian" :width="width" :height="height" + class="has-tooltip" xmlns="http://www.w3.org/2000/svg"> <path :d="pathD" :viewBox="pathViewBox" /> <circle - r="1.5" :cx="dotX" :cy="dotY" + r="1.5" tranform="translate(0 -1)" /> </svg> diff --git a/app/assets/javascripts/vue_shared/components/navigation_tabs.vue b/app/assets/javascripts/vue_shared/components/navigation_tabs.vue index 08d4936f480..99d61b5639d 100644 --- a/app/assets/javascripts/vue_shared/components/navigation_tabs.vue +++ b/app/assets/javascripts/vue_shared/components/navigation_tabs.vue @@ -59,9 +59,9 @@ export default { }" > <a + :class="`js-${scope}-tab-${tab.scope}`" role="button" @click="onTabClick(tab)" - :class="`js-${scope}-tab-${tab.scope}`" > {{ tab.name }} 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 eccba61a8c0..38115f268bb 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="d-none d-sm-block">{{ getUserData.name }}</span> + <span class="d-none d-sm-inline-block">{{ getUserData.name }}</span> <span class="note-headline-light">@{{ getUserData.username }}</span> </a> </div> diff --git a/app/assets/javascripts/vue_shared/components/panel_resizer.vue b/app/assets/javascripts/vue_shared/components/panel_resizer.vue index abbe9a22717..8c2dcc2d902 100644 --- a/app/assets/javascripts/vue_shared/components/panel_resizer.vue +++ b/app/assets/javascripts/vue_shared/components/panel_resizer.vue @@ -82,9 +82,9 @@ <template> <div - class="dragHandle" :class="className" :style="cursorStyle" + class="dragHandle" @mousedown="startDrag" @dblclick="resetSize" ></div> diff --git a/app/assets/javascripts/vue_shared/components/project_avatar/image.vue b/app/assets/javascripts/vue_shared/components/project_avatar/image.vue index 279cc1de5bb..97ca4d93bd7 100644 --- a/app/assets/javascripts/vue_shared/components/project_avatar/image.vue +++ b/app/assets/javascripts/vue_shared/components/project_avatar/image.vue @@ -85,7 +85,6 @@ <template> <img v-tooltip - class="avatar" :class="{ lazy: lazy, [avatarSizeClass]: true, @@ -99,5 +98,6 @@ :data-container="tooltipContainer" :data-placement="tooltipPlacement" :title="tooltipText" + class="avatar" /> </template> diff --git a/app/assets/javascripts/vue_shared/components/recaptcha_modal.vue b/app/assets/javascripts/vue_shared/components/recaptcha_modal.vue index 21ffdc1dc86..a2a9a5e6987 100644 --- a/app/assets/javascripts/vue_shared/components/recaptcha_modal.vue +++ b/app/assets/javascripts/vue_shared/components/recaptcha_modal.vue @@ -66,10 +66,10 @@ <template> <deprecated-modal - kind="warning" - class="recaptcha-modal js-recaptcha-modal" :hide-footer="true" :title="__('Please solve the reCAPTCHA')" + kind="warning" + class="recaptcha-modal js-recaptcha-modal" @cancel="close" > <div slot="body"> 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 71ec34f2c7a..74998a4787d 100644 --- a/app/assets/javascripts/vue_shared/components/sidebar/date_picker.vue +++ b/app/assets/javascripts/vue_shared/components/sidebar/date_picker.vue @@ -97,8 +97,8 @@ <template> <div - class="block" :class="blockClass" + class="block" > <div class="issuable-sidebar-header"> <toggle-sidebar @@ -107,8 +107,8 @@ /> </div> <collapsed-calendar-icon - class="sidebar-collapsed-icon" :text="collapsedText" + class="sidebar-collapsed-icon" /> <div class="title"> {{ label }} 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 f155ac2be02..a3fc358130f 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 @@ -143,8 +143,8 @@ export default { :value="label.id" /> <div - class="dropdown" ref="dropdown" + class="dropdown" > <dropdown-button :ability-name="abilityName" diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_button.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_button.vue index 47497c1de98..48d2f16f554 100644 --- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_button.vue +++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_button.vue @@ -53,10 +53,7 @@ export default { <template> <button - type="button" ref="dropdownButton" - class="dropdown-menu-toggle wide js-label-select js-multiselect js-context-config-modal" - data-toggle="dropdown" :class="{ 'js-extra-options': showExtraOptions }" :data-ability-name="abilityName" :data-field-name="fieldName" @@ -64,6 +61,9 @@ export default { :data-labels="labelsPath" :data-namespace-path="namespace" :data-show-any="showExtraOptions" + type="button" + class="dropdown-menu-toggle wide js-label-select js-multiselect js-context-config-modal" + data-toggle="dropdown" > <span class="dropdown-toggle-text"> {{ dropdownToggleText }} 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 3c400afdc1d..fe895136ccc 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 @@ -19,9 +19,9 @@ export default { <div class="dropdown-page-two dropdown-new-label"> <div class="dropdown-title"> <button + :aria-label="__('Go back')" type="button" class="dropdown-title-button dropdown-menu-back" - :aria-label="__('Go back')" > <i aria-hidden="true" @@ -32,9 +32,9 @@ export default { </button> {{ headerTitle }} <button + :aria-label="__('Close')" type="button" class="dropdown-title-button dropdown-menu-close" - :aria-label="__('Close')" > <i aria-hidden="true" @@ -48,19 +48,19 @@ export default { <div class="dropdown-labels-error js-label-error"></div> <input id="new_label_name" + :placeholder="__('Name new label')" type="text" class="default-dropdown-input" - :placeholder="__('Name new label')" /> <div class="suggest-colors suggest-colors-dropdown"> <a v-for="(color, index) in suggestedColors" - href="#" :key="index" :data-color="color" :style="{ backgroundColor: color, }" + href="#" > </a> @@ -69,9 +69,9 @@ export default { <div class="dropdown-label-color-preview js-dropdown-label-color-preview"></div> <input id="new_label_color" + :placeholder="__('Assign custom color like #FF0000')" type="text" class="default-dropdown-input" - :placeholder="__('Assign custom color like #FF0000')" /> </div> <div class="clearfix"> diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_footer.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_footer.vue index 5f61e9fbe80..d64ad016f9b 100644 --- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_footer.vue +++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_footer.vue @@ -34,9 +34,9 @@ export default { </li> <li> <a + :href="labelsWebUrl" data-is-link="true" class="dropdown-external-link" - :href="labelsWebUrl" > {{ manageLabelsTitle }} </a> diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_header.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_header.vue index 7664acdf19c..e98b6392827 100644 --- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_header.vue +++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_header.vue @@ -6,9 +6,9 @@ export default {}; <div class="dropdown-title"> <span>{{ __('Assign labels') }}</span> <button + :aria-label="__('Close')" type="button" class="dropdown-title-button dropdown-menu-close" - :aria-label="__('Close')" > <i aria-hidden="true" diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_search_input.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_search_input.vue index ae633460c95..80d65a2a534 100644 --- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_search_input.vue +++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_search_input.vue @@ -5,10 +5,10 @@ export default {}; <template> <div class="dropdown-input"> <input + :placeholder="__('Search')" autocomplete="off" class="dropdown-input-field" type="search" - :placeholder="__('Search')" /> <i aria-hidden="true" diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value.vue index 69d588eb25d..10e990f8a80 100644 --- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value.vue +++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value.vue @@ -35,7 +35,12 @@ export default { </script> <template> - <div class="hide-collapsed value issuable-show-labels js-value"> + <div + :class="{ + 'has-labels':!isEmpty, + }" + class="hide-collapsed value issuable-show-labels js-value" + > <span v-if="isEmpty" class="text-secondary" @@ -43,18 +48,18 @@ export default { <slot>{{ __('None') }}</slot> </span> <a - v-else v-for="label in labels" + v-else :key="label.id" :href="labelFilterUrl(label)" > <span v-tooltip - class="label color-label" - data-placement="bottom" - data-container="body" :style="labelStyle(label)" :title="label.description" + class="badge color-label" + data-placement="bottom" + data-container="body" > {{ label.title }} </span> 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 68fa2ab8d01..af297f3c408 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 @@ -37,10 +37,10 @@ export default { <template> <div v-tooltip + :title="labelsList" class="sidebar-collapsed-icon" data-placement="left" data-container="body" - :title="labelsList" @click="handleClick" > <i 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 de6f8c32e74..ac2e99abe77 100644 --- a/app/assets/javascripts/vue_shared/components/sidebar/toggle_sidebar.vue +++ b/app/assets/javascripts/vue_shared/components/sidebar/toggle_sidebar.vue @@ -28,21 +28,21 @@ export default { <template> <button + v-tooltip + :title="tooltipLabel" type="button" class="btn btn-blank gutter-toggle btn-sidebar-action" - @click="toggle" - v-tooltip data-container="body" data-placement="left" - :title="tooltipLabel" + @click="toggle" > <i - aria-label="toggle collapse" - class="fa" :class="{ 'fa-angle-double-right': !collapsed, 'fa-angle-double-left': collapsed }" + aria-label="toggle collapse" + class="fa" > </i> </button> diff --git a/app/assets/javascripts/vue_shared/components/skeleton_loading_container.vue b/app/assets/javascripts/vue_shared/components/skeleton_loading_container.vue index 16304e4815d..4a5ffbe5d5a 100644 --- a/app/assets/javascripts/vue_shared/components/skeleton_loading_container.vue +++ b/app/assets/javascripts/vue_shared/components/skeleton_loading_container.vue @@ -22,10 +22,10 @@ <template> <div - class="animation-container" :class="{ 'animation-container-small': small, }" + class="animation-container" > <div v-for="(css, index) in lineClasses" diff --git a/app/assets/javascripts/vue_shared/components/stacked_progress_bar.vue b/app/assets/javascripts/vue_shared/components/stacked_progress_bar.vue index 86f06c8d266..b1c2df54ef6 100644 --- a/app/assets/javascripts/vue_shared/components/stacked_progress_bar.vue +++ b/app/assets/javascripts/vue_shared/components/stacked_progress_bar.vue @@ -84,8 +84,8 @@ export default { <template> <div - class="stacked-progress-bar" :class="cssClass" + class="stacked-progress-bar" > <span v-if="!totalCount" @@ -96,30 +96,30 @@ export default { <span v-tooltip v-if="successPercent" - class="status-green" - data-placement="bottom" :title="successTooltip" :style="successBarStyle" + class="status-green" + data-placement="bottom" > {{ successPercent }}% </span> <span v-tooltip v-if="neutralPercent" - class="status-neutral" - data-placement="bottom" :title="neutralTooltip" :style="neutralBarStyle" + class="status-neutral" + data-placement="bottom" > {{ neutralPercent }}% </span> <span v-tooltip v-if="failurePercent" - class="status-red" - data-placement="bottom" :title="failureTooltip" :style="failureBarStyle" + class="status-red" + data-placement="bottom" > {{ failurePercent }}% </span> diff --git a/app/assets/javascripts/vue_shared/components/table_pagination.vue b/app/assets/javascripts/vue_shared/components/table_pagination.vue index 22fc5757447..2370e59d017 100644 --- a/app/assets/javascripts/vue_shared/components/table_pagination.vue +++ b/app/assets/javascripts/vue_shared/components/table_pagination.vue @@ -124,15 +124,18 @@ break; } }, + hideOnSmallScreen(item) { + return !item.first && !item.last && !item.next && !item.prev && !item.active; + }, }, }; </script> <template> <div v-if="showPagination" - class="gl-pagination" + class="gl-pagination prepend-top-default" > - <ul class="pagination clearfix"> + <ul class="pagination justify-content-center"> <li v-for="(item, index) in getItems" :key="index" @@ -142,12 +145,17 @@ 'js-next-button': item.next, 'js-last-button': item.last, 'js-first-button': item.first, + 'd-none d-md-block': hideOnSmallScreen(item), separator: item.separator, active: item.active, - disabled: item.disabled + disabled: item.disabled || item.separator }" + class="page-item" > - <a @click.prevent="changePage(item.title, item.disabled)"> + <a + class="page-link" + @click.prevent="changePage(item.title, item.disabled)" + > {{ item.title }} </a> </li> diff --git a/app/assets/javascripts/vue_shared/components/tabs/tab.vue b/app/assets/javascripts/vue_shared/components/tabs/tab.vue index 2a35d6bc151..1c6011dcfd0 100644 --- a/app/assets/javascripts/vue_shared/components/tabs/tab.vue +++ b/app/assets/javascripts/vue_shared/components/tabs/tab.vue @@ -26,15 +26,20 @@ export default { created() { this.isTab = true; }, + updated() { + if (this.$parent) { + this.$parent.$forceUpdate(); + } + }, }; </script> <template> <div - class="tab-pane" :class="{ active: localActive }" + class="tab-pane" role="tabpanel" > <slot></slot> diff --git a/app/assets/javascripts/vue_shared/components/tabs/tabs.js b/app/assets/javascripts/vue_shared/components/tabs/tabs.js index 4362264caa5..9b9e4bb47bd 100644 --- a/app/assets/javascripts/vue_shared/components/tabs/tabs.js +++ b/app/assets/javascripts/vue_shared/components/tabs/tabs.js @@ -1,4 +1,11 @@ export default { + props: { + stopPropagation: { + type: Boolean, + required: false, + default: false, + }, + }, data() { return { currentIndex: 0, @@ -13,7 +20,12 @@ export default { this.tabs = this.$children.filter(child => child.isTab); this.currentIndex = this.tabs.findIndex(tab => tab.localActive); }, - setTab(index) { + setTab(e, index) { + if (this.stopPropagation) { + e.stopPropagation(); + e.preventDefault(); + } + this.tabs[this.currentIndex].localActive = false; this.tabs[index].localActive = true; @@ -36,7 +48,7 @@ export default { href: '#', }, on: { - click: () => this.setTab(i), + click: e => this.setTab(e, i), }, }, tab.$slots.title || tab.title, diff --git a/app/assets/javascripts/vue_shared/components/toggle_button.vue b/app/assets/javascripts/vue_shared/components/toggle_button.vue index 09031d3ffa1..a897300b62b 100644 --- a/app/assets/javascripts/vue_shared/components/toggle_button.vue +++ b/app/assets/javascripts/vue_shared/components/toggle_button.vue @@ -63,26 +63,26 @@ <label class="toggle-wrapper"> <input v-if="name" - type="hidden" :name="name" :value="value" + type="hidden" /> <button - type="button" - class="project-feature-toggle" :aria-label="ariaLabel" :class="{ 'is-checked': value, 'is-disabled': disabledInput, 'is-loading': isLoading }" + type="button" + class="project-feature-toggle" @click="toggleFeature" > <loadingIcon class="loading-icon" /> <span class="toggle-icon"> <icon - css-classes="toggle-icon-svg" - :name="toggleIcon"/> + :name="toggleIcon" + css-classes="toggle-icon-svg"/> </span> </button> </label> 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 cc9cc46bb4c..3a413c74410 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 @@ -85,7 +85,6 @@ export default { <template> <img v-tooltip - class="avatar" :class="{ lazy: lazy, [avatarSizeClass]: true, @@ -99,5 +98,7 @@ export default { :data-container="tooltipContainer" :data-placement="tooltipPlacement" :title="tooltipText" + class="avatar" + data-boundary="window" /> </template> diff --git a/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_link.vue b/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_link.vue index 6955d164def..01c36fec41a 100644 --- a/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_link.vue +++ b/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_link.vue @@ -84,8 +84,8 @@ export default { <template> <a - class="user-avatar-link" - :href="linkHref"> + :href="linkHref" + class="user-avatar-link"> <user-avatar-image :img-src="imgSrc" :img-alt="imgAlt" @@ -94,8 +94,8 @@ export default { :tooltip-text="avatarTooltipText" :tooltip-placement="tooltipPlacement" /><span - v-if="shouldShowUsername" v-tooltip + v-if="shouldShowUsername" :title="tooltipText" :tooltip-placement="tooltipPlacement" >{{ username }}</span> diff --git a/app/assets/javascripts/vue_shared/models/assignee.js b/app/assets/javascripts/vue_shared/models/assignee.js new file mode 100644 index 00000000000..4a29b0d0581 --- /dev/null +++ b/app/assets/javascripts/vue_shared/models/assignee.js @@ -0,0 +1,13 @@ +export default class ListAssignee { + constructor(obj, defaultAvatar) { + this.id = obj.id; + this.name = obj.name; + this.username = obj.username; + this.avatar = obj.avatar_url || obj.avatar || defaultAvatar; + this.path = obj.path; + this.state = obj.state; + this.webUrl = obj.web_url || obj.webUrl; + } +} + +window.ListAssignee = ListAssignee; diff --git a/app/assets/stylesheets/bootstrap_migration.scss b/app/assets/stylesheets/bootstrap_migration.scss index e24f8b1d4e8..0d8e867f41d 100644 --- a/app/assets/stylesheets/bootstrap_migration.scss +++ b/app/assets/stylesheets/bootstrap_migration.scss @@ -24,16 +24,54 @@ html { font-size: 14px; } +legend { + border-bottom: 1px solid $border-color; + margin-bottom: 20px; +} + button, html [type="button"], [type="reset"], -[type="submit"] { +[type="submit"], +[role="button"] { // Override bootstrap reboot -webkit-appearance: inherit; + cursor: pointer; } -[role="button"] { - cursor: pointer; +h1, +h2, +h3, +h4, +h5, +h6 { + color: $gl-text-color; + font-weight: 600; +} + +h1, +.h1, +h2, +.h2, +h3, +.h3 { + margin-top: 20px; + margin-bottom: 10px; +} + +h4, +.h4, +h5, +.h5, +h6, +.h6 { + margin-top: 10px; + margin-bottom: 10px; +} + +h5, +.h5 { + font-size: $gl-font-size; } input[type="file"] { @@ -51,7 +89,12 @@ a { color: $gl-link-color; } +hr { + overflow: hidden; +} + .form-group.row .col-form-label { + padding-top: 0; // Bootstrap 4 aligns labels to the left // for horizontal forms @include media-breakpoint-up(md) { @@ -59,10 +102,25 @@ a { } } +kbd { + display: inline-block; +} + code { padding: 2px 4px; + color: $red-600; background-color: $red-100; border-radius: 3px; + + .code > & { + background-color: inherit; + padding: unset; + } + + .build-trace & { + background-color: inherit; + padding: inherit; + } } table { @@ -70,11 +128,6 @@ table { border-spacing: 0; } -.tooltip { - // Fix bootstrap4 bug whereby tooltips flicker when they are hovered over their borders - pointer-events: none; -} - .popover { font-size: 14px; } @@ -93,6 +146,16 @@ table { color: $gl-text-color-secondary !important; } +.bg-success, +.bg-primary, +.bg-info, +.bg-danger, +.bg-warning { + .card-header { + color: $white-light; + } +} + // Polyfill deprecated selectors .hidden { @@ -110,7 +173,10 @@ table { display: none; } -.badge { +// Add to .label so that old system notes that are saved to the db +// will still receive the correct styling +.badge, +.label { padding: 4px 5px; font-size: 12px; font-style: normal; @@ -145,6 +211,19 @@ table { &:not(:last-of-type) { border-bottom: 1px solid $well-inner-border; } + + p, + ol, + ul, + .form-group { + &:last-of-type { + margin-bottom: 0; + } + } + } + + .badge.badge-gray { + background-color: $well-expand-item; } } @@ -166,9 +245,21 @@ table { } } +.card-header { + h3.card-title, + h4.card-title { + margin-top: 0; + } +} + .nav-tabs { + // Override bootstrap's default border + border-bottom: 0; + .nav-link { - border: 0; + border-top: 0; + border-left: 0; + border-right: 0; } .nav-item { @@ -179,3 +270,60 @@ table { pre code { white-space: pre-wrap; } + +.alert, +.flash-notice { + border-radius: 0; +} + +.alert-success { + background-color: $green-500; + border-color: $green-500; +} + +.alert-info { + background-color: $blue-500; + border-color: $blue-500; +} + +.alert-warning { + background-color: $orange-500; + border-color: $orange-500; +} + +.alert-danger { + background-color: $red-500; + border-color: $red-500; +} + +.alert-success, +.alert-info, +.alert-warning, +.alert-danger, +.flash-notice { + color: $white-light; + + h4, + a, + .alert-link { + color: $white-light; + } +} + +input[type=color].form-control { + height: $input-height; +} + +.toggle-sidebar-button { + .collapse-text, + .icon-angle-double-left, + .icon-angle-double-right { + color: $gl-text-color-secondary; + } +} + +.project-templates-buttons { + .btn { + vertical-align: unset; + } +} diff --git a/app/assets/stylesheets/framework/blocks.scss b/app/assets/stylesheets/framework/blocks.scss index c5be27f2d29..0de05548c68 100644 --- a/app/assets/stylesheets/framework/blocks.scss +++ b/app/assets/stylesheets/framework/blocks.scss @@ -16,6 +16,7 @@ .click-to-expand { cursor: pointer; + vertical-align: initial; } } } diff --git a/app/assets/stylesheets/framework/buttons.scss b/app/assets/stylesheets/framework/buttons.scss index 0115f542c88..523fcb05a87 100644 --- a/app/assets/stylesheets/framework/buttons.scss +++ b/app/assets/stylesheets/framework/buttons.scss @@ -121,10 +121,6 @@ &.btn-sm { margin-left: $btn-sm-side-margin; } - - &.btn-xs { - margin-left: $btn-xs-side-margin; - } } @mixin btn-svg { @@ -150,10 +146,6 @@ line-height: 18px; } - &.btn-xs { - padding: 2px 5px; - } - &.btn-success, &.btn-new, &.btn-create, @@ -497,6 +489,10 @@ fieldset[disabled] .btn, } } +[readonly] { + cursor: default; +} + .btn-no-padding { padding: 0; } diff --git a/app/assets/stylesheets/framework/common.scss b/app/assets/stylesheets/framework/common.scss index 1e7b9534275..326499125fc 100644 --- a/app/assets/stylesheets/framework/common.scss +++ b/app/assets/stylesheets/framework/common.scss @@ -305,14 +305,6 @@ img.emoji { margin-bottom: 10px; } -.btn-sign-in { - text-shadow: none; - - @include media-breakpoint-up(sm) { - margin-top: 8px; - } -} - .side-filters { fieldset { margin-bottom: 15px; diff --git a/app/assets/stylesheets/framework/contextual_sidebar.scss b/app/assets/stylesheets/framework/contextual_sidebar.scss index 1a415e1b852..9cbaaa5dc8d 100644 --- a/app/assets/stylesheets/framework/contextual_sidebar.scss +++ b/app/assets/stylesheets/framework/contextual_sidebar.scss @@ -26,19 +26,25 @@ margin-right: 2px; width: $contextual-sidebar-width; - a { + > a, + > button { transition: padding $sidebar-transition-duration; font-weight: $gl-font-weight-bold; display: flex; + width: 100%; align-items: center; padding: 10px 16px 10px 10px; color: $gl-text-color; - } + background-color: transparent; + border: 0; + text-align: left; - &:hover, - a:hover { - background-color: $link-hover-background; - color: $gl-text-color; + &:hover, + &:focus { + background-color: $link-hover-background; + color: $gl-text-color; + outline: 0; + } } .avatar-container { diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss index b91d579cae6..74475daae14 100644 --- a/app/assets/stylesheets/framework/dropdowns.scss +++ b/app/assets/stylesheets/framework/dropdowns.scss @@ -35,6 +35,12 @@ @include media-breakpoint-down(xs) { width: 100%; } + + &.projects-dropdown-menu { + padding: 0; + overflow-y: initial; + max-height: initial; + } } .dropdown-toggle, diff --git a/app/assets/stylesheets/framework/files.scss b/app/assets/stylesheets/framework/files.scss index f77ec4b6a2c..f060254777c 100644 --- a/app/assets/stylesheets/framework/files.scss +++ b/app/assets/stylesheets/framework/files.scss @@ -400,3 +400,51 @@ span.idiff { color: $common-gray-light; border: 1px solid $common-gray-light; } + +.preview-container { + height: 100%; + overflow: auto; + + .file-container { + background-color: $gray-darker; + display: flex; + height: 100%; + align-items: center; + justify-content: center; + + text-align: center; + + .file-content { + padding: $gl-padding; + max-width: 100%; + max-height: 100%; + + img { + max-width: 90%; + max-height: 70vh; + } + + .is-zoomable { + cursor: pointer; + cursor: zoom-in; + + &.is-zoomed { + cursor: pointer; + cursor: zoom-out; + max-width: none; + max-height: none; + margin-right: $gl-padding; + } + } + } + + .file-info { + font-size: $label-font-size; + color: $diff-image-info-color; + } + } + + .md-previewer { + padding: $gl-padding; + } +} diff --git a/app/assets/stylesheets/framework/filters.scss b/app/assets/stylesheets/framework/filters.scss index 0ee5748952a..551a7e852ae 100644 --- a/app/assets/stylesheets/framework/filters.scss +++ b/app/assets/stylesheets/framework/filters.scss @@ -299,6 +299,7 @@ height: 14px; width: 14px; vertical-align: middle; + margin-bottom: 4px; } .dropdown-toggle-text { diff --git a/app/assets/stylesheets/framework/flash.scss b/app/assets/stylesheets/framework/flash.scss index 52c3f18a682..a6e324036ae 100644 --- a/app/assets/stylesheets/framework/flash.scss +++ b/app/assets/stylesheets/framework/flash.scss @@ -10,6 +10,20 @@ @extend .alert; background-color: $blue-500; margin: 0; + + &.flash-notice-persistent { + background-color: $blue-100; + color: $gl-text-color; + + a { + color: $gl-link-color; + + &:hover { + color: $gl-link-hover-color; + text-decoration: none; + } + } + } } .flash-warning { diff --git a/app/assets/stylesheets/framework/forms.scss b/app/assets/stylesheets/framework/forms.scss index c76ea532912..03520f42997 100644 --- a/app/assets/stylesheets/framework/forms.scss +++ b/app/assets/stylesheets/framework/forms.scss @@ -170,7 +170,7 @@ label { } .form-control::-webkit-input-placeholder { - color: $gl-text-color-secondary; + color: $placeholder-text-color; } .input-group { diff --git a/app/assets/stylesheets/framework/gfm.scss b/app/assets/stylesheets/framework/gfm.scss index e378e84ca1b..1cf12b1a015 100644 --- a/app/assets/stylesheets/framework/gfm.scss +++ b/app/assets/stylesheets/framework/gfm.scss @@ -19,6 +19,7 @@ .gfm-color_chip { display: inline-block; + line-height: 1; margin: 0 0 2px 4px; vertical-align: middle; border-radius: 3px; diff --git a/app/assets/stylesheets/framework/gitlab_theme.scss b/app/assets/stylesheets/framework/gitlab_theme.scss index 11e21edfc1b..b40d02f381a 100644 --- a/app/assets/stylesheets/framework/gitlab_theme.scss +++ b/app/assets/stylesheets/framework/gitlab_theme.scss @@ -3,26 +3,26 @@ */ @mixin gitlab-theme( - $color-100, - $color-200, - $color-500, - $color-700, - $color-800, - $color-900, + $location-badge-color, + $search-and-nav-links, + $active-tab-border, + $border-and-box-shadow, + $sidebar-text, + $nav-svg-color, $color-alternate ) { // Header .navbar-gitlab { - background-color: $color-900; + background-color: $nav-svg-color; .navbar-collapse { - color: $color-200; + color: $search-and-nav-links; } .container-fluid { .navbar-toggler { - border-left: 1px solid lighten($color-700, 10%); + border-left: 1px solid lighten($border-and-box-shadow, 10%); } } @@ -31,40 +31,40 @@ > li { > a:hover, > a:focus { - background-color: rgba($color-200, 0.2); + background-color: rgba($search-and-nav-links, 0.2); } &.active > a, - &.dropdown.open > a { - color: $color-900; + &.dropdown.show > a { + color: $nav-svg-color; background-color: $color-alternate; } &.line-separator { - border-left: 1px solid rgba($color-200, 0.2); + border-left: 1px solid rgba($search-and-nav-links, 0.2); } } } .navbar-sub-nav { - color: $color-200; + color: $search-and-nav-links; } .nav { > li { - color: $color-200; + color: $search-and-nav-links; > a { &.header-user-dropdown-toggle { .header-user-avatar { - border-color: $color-200; + border-color: $search-and-nav-links; } } &:hover, &:focus { @include media-breakpoint-up(sm) { - background-color: rgba($color-200, 0.2); + background-color: rgba($search-and-nav-links, 0.2); } svg { @@ -74,13 +74,13 @@ } &.active > a, - &.dropdown.open > a { - color: $color-900; + &.dropdown.show > a { + color: $nav-svg-color; background-color: $color-alternate; &:hover { svg { - fill: $color-900; + fill: $nav-svg-color; } } } @@ -88,7 +88,7 @@ .impersonated-user, .impersonated-user:hover { svg { - fill: $color-900; + fill: $nav-svg-color; } } } @@ -99,34 +99,34 @@ > a { &:hover, &:focus { - background-color: rgba($color-200, 0.2); + background-color: rgba($search-and-nav-links, 0.2); } } } .search { form { - background-color: rgba($color-200, 0.2); + background-color: rgba($search-and-nav-links, 0.2); &:hover { - background-color: rgba($color-200, 0.3); + background-color: rgba($search-and-nav-links, 0.3); } } .location-badge { - color: $color-100; - background-color: rgba($color-200, 0.1); - border-right: 1px solid $color-800; + color: $location-badge-color; + background-color: rgba($search-and-nav-links, 0.1); + border-right: 1px solid $sidebar-text; } .search-input::placeholder { - color: rgba($color-200, 0.8); + color: rgba($search-and-nav-links, 0.8); } .search-input-wrap { .search-icon, .clear-icon { - fill: rgba($color-200, 0.8); + fill: rgba($search-and-nav-links, 0.8); } } @@ -141,38 +141,34 @@ .search-input-wrap { .search-icon { - fill: rgba($color-200, 0.8); + fill: rgba($search-and-nav-links, 0.8); } } } } - .btn-sign-in { - background-color: $color-100; - color: $color-900; - } // Sidebar .nav-sidebar li.active { - box-shadow: inset 4px 0 0 $color-700; + box-shadow: inset 4px 0 0 $border-and-box-shadow; > a { - color: $color-800; + color: $sidebar-text; } svg { - fill: $color-800; + fill: $sidebar-text; } } .sidebar-top-level-items > li.active .badge.badge-pill { - color: $color-800; + color: $sidebar-text; } .nav-links li { &.active a, a.active { - border-bottom: 2px solid $color-500; + border-bottom: 2px solid $active-tab-border; .badge.badge-pill { font-weight: $gl-font-weight-bold; @@ -181,27 +177,27 @@ } .branch-header-title { - color: $color-700; + color: $border-and-box-shadow; } .ide-file-list .file.file-active { - color: $color-700; + color: $border-and-box-shadow; } .ide-sidebar-link { &.active { - color: $color-700; - box-shadow: inset 3px 0 $color-700; + color: $border-and-box-shadow; + box-shadow: inset 3px 0 $border-and-box-shadow; &.is-right { - box-shadow: inset -3px 0 $color-700; + box-shadow: inset -3px 0 $border-and-box-shadow; } } } } body { - &.ui_indigo { + &.ui-indigo { @include gitlab-theme( $indigo-100, $indigo-200, @@ -213,19 +209,19 @@ body { ); } - &.ui_dark { + &.ui-light-indigo { @include gitlab-theme( - $theme-gray-100, - $theme-gray-200, - $theme-gray-500, - $theme-gray-700, - $theme-gray-800, - $theme-gray-900, + $indigo-100, + $indigo-200, + $indigo-500, + $indigo-500, + $indigo-700, + $indigo-700, $white-light ); } - &.ui_blue { + &.ui-blue { @include gitlab-theme( $theme-blue-100, $theme-blue-200, @@ -237,7 +233,19 @@ body { ); } - &.ui_green { + &.ui-light-blue { + @include gitlab-theme( + $theme-light-blue-100, + $theme-light-blue-200, + $theme-light-blue-500, + $theme-light-blue-500, + $theme-light-blue-700, + $theme-light-blue-700, + $white-light + ); + } + + &.ui-green { @include gitlab-theme( $theme-green-100, $theme-green-200, @@ -249,7 +257,55 @@ body { ); } - &.ui_light { + &.ui-light-green { + @include gitlab-theme( + $theme-green-100, + $theme-green-200, + $theme-green-500, + $theme-green-500, + $theme-light-green-700, + $theme-light-green-700, + $white-light + ); + } + + &.ui-red { + @include gitlab-theme( + $theme-red-100, + $theme-red-200, + $theme-red-500, + $theme-red-700, + $theme-red-800, + $theme-red-900, + $white-light + ); + } + + &.ui-light-red { + @include gitlab-theme( + $theme-light-red-100, + $theme-light-red-200, + $theme-light-red-500, + $theme-light-red-500, + $theme-light-red-700, + $theme-light-red-700, + $white-light + ); + } + + &.ui-dark { + @include gitlab-theme( + $theme-gray-100, + $theme-gray-200, + $theme-gray-500, + $theme-gray-700, + $theme-gray-800, + $theme-gray-900, + $white-light + ); + } + + &.ui-light { @include gitlab-theme( $theme-gray-900, $theme-gray-700, diff --git a/app/assets/stylesheets/framework/header.scss b/app/assets/stylesheets/framework/header.scss index 2085e5646ef..db59c91e375 100644 --- a/app/assets/stylesheets/framework/header.scss +++ b/app/assets/stylesheets/framework/header.scss @@ -139,6 +139,8 @@ } .nav { + flex-wrap: nowrap; + > li:not(.d-none) a { @include media-breakpoint-down(xs) { margin-left: 0; @@ -158,11 +160,12 @@ } .navbar-toggler { + position: relative; right: -10px; border-radius: 0; min-width: 45px; padding: 0; - margin-right: -7px; + margin: $gl-padding-8 -7px $gl-padding-8 0; font-size: 14px; text-align: center; color: currentColor; @@ -186,6 +189,7 @@ display: -webkit-flex; display: flex; padding-right: 10px; + flex-direction: row; } li { @@ -264,6 +268,8 @@ .navbar-sub-nav, .navbar-nav { + align-items: center; + > li { > a:hover, > a:focus { @@ -290,6 +296,10 @@ margin: 8px; } } + + .dropdown-menu { + position: absolute; + } } .navbar-sub-nav { @@ -297,12 +307,6 @@ display: flex; margin: 0 0 0 6px; - .projects-dropdown-menu { - padding: 0; - overflow-y: initial; - max-height: initial; - } - .dropdown-chevron { position: relative; top: -1px; @@ -443,12 +447,18 @@ } .btn-sign-in { - margin-top: 3px; + background-color: $indigo-100; + color: $indigo-900; font-weight: $gl-font-weight-bold; + line-height: 18px; &:hover { background-color: $white-light; } + + @include media-breakpoint-down(xs) { + margin-top: $gl-padding-4; + } } .navbar-nav { diff --git a/app/assets/stylesheets/framework/layout.scss b/app/assets/stylesheets/framework/layout.scss index 0536c39cee7..52b5f059f20 100644 --- a/app/assets/stylesheets/framework/layout.scss +++ b/app/assets/stylesheets/framework/layout.scss @@ -54,10 +54,6 @@ body { &.limit-container-width { max-width: $limited-layout-width; } - - &.limit-container-width-sm { - max-width: $limited-layout-width-sm; - } } .alert-wrapper { @@ -115,9 +111,3 @@ body { .with-performance-bar .layout-page { margin-top: $header-height + $performance-bar-height; } - -.vertical-center { - min-height: 100vh; - display: flex; - align-items: center; -} diff --git a/app/assets/stylesheets/framework/markdown_area.scss b/app/assets/stylesheets/framework/markdown_area.scss index b893151e4fe..7290a174668 100644 --- a/app/assets/stylesheets/framework/markdown_area.scss +++ b/app/assets/stylesheets/framework/markdown_area.scss @@ -61,10 +61,6 @@ padding-top: 0; line-height: 19px; - &.btn.btn-xs { - padding: 2px 5px; - } - &:focus { margin-top: -10px; padding-top: 10px; diff --git a/app/assets/stylesheets/framework/mixins.scss b/app/assets/stylesheets/framework/mixins.scss index d76cf8f8182..0b645eb811b 100644 --- a/app/assets/stylesheets/framework/mixins.scss +++ b/app/assets/stylesheets/framework/mixins.scss @@ -186,6 +186,7 @@ overflow-y: hidden; -webkit-overflow-scrolling: touch; display: flex; + flex-wrap: nowrap; &::-webkit-scrollbar { display: none; diff --git a/app/assets/stylesheets/framework/modal.scss b/app/assets/stylesheets/framework/modal.scss index a7896cc3fc3..ffb40166c15 100644 --- a/app/assets/stylesheets/framework/modal.scss +++ b/app/assets/stylesheets/framework/modal.scss @@ -1,3 +1,7 @@ +.modal-xl { + max-width: 98%; +} + .modal-header { background-color: $modal-body-bg; diff --git a/app/assets/stylesheets/framework/pagination.scss b/app/assets/stylesheets/framework/pagination.scss index d3e013590b6..61d02511ff4 100644 --- a/app/assets/stylesheets/framework/pagination.scss +++ b/app/assets/stylesheets/framework/pagination.scss @@ -1,91 +1,14 @@ .gl-pagination { - text-align: center; - border-top: 1px solid $border-color; - margin: 0; - margin-top: 0; - - .pagination { - padding: 0; - margin: 20px 0; - - a { - cursor: pointer; - } - - .separator, - .separator:hover { - a { - cursor: default; - background-color: $gray-light; - padding: $gl-vert-padding; - } - } + a { + color: inherit; + text-decoration: none; } - - - .gap, - .gap:hover { - background-color: $gray-light; - padding: $gl-vert-padding; - cursor: default; - } -} - -.card > .gl-pagination { - margin: 0; } -/** - * Extra-small screen pagination. - */ -@media (max-width: 320px) { - .gl-pagination { - .first, - .last { - display: none; - } - - .page-item { - display: none; - - &.active { - display: inline; - } - } - } -} - -/** - * Small screen pagination - */ -@include media-breakpoint-down(xs) { - .gl-pagination { - .pagination li a { - padding: 6px 10px; - } - - .page-item { - display: none; - - &.active { - display: inline; - } - } - } -} - -/** - * Medium screen pagination - */ -@media (min-width: map-get($grid-breakpoints, xs)) and (max-width: map-get($grid-breakpoints, sm)) { - .gl-pagination { - .page-item { - display: none; - - &.active, - &.sibling { - display: inline; - } +.page-item { + &.active { + .page-link { + z-index: 3; } } } diff --git a/app/assets/stylesheets/framework/secondary_navigation_elements.scss b/app/assets/stylesheets/framework/secondary_navigation_elements.scss index 847fc8c0792..9dbb04e5443 100644 --- a/app/assets/stylesheets/framework/secondary_navigation_elements.scss +++ b/app/assets/stylesheets/framework/secondary_navigation_elements.scss @@ -197,7 +197,7 @@ flex-flow: row wrap; .nav-controls { - $controls-margin: $btn-xs-side-margin - 2px; + $controls-margin: $btn-margin-5 - 2px; flex: 0 0 100%; &.controls-flex { @@ -230,6 +230,8 @@ } .scrolling-tabs-container { + position: relative; + .merge-request-tabs-container & { overflow: hidden; } @@ -345,7 +347,7 @@ .empty-state .project-item-select-holder.btn-group { float: none; - display: inline-block; + justify-content: center; .btn { // overrides styles applied to plain `.empty-state .btn` diff --git a/app/assets/stylesheets/framework/tables.scss b/app/assets/stylesheets/framework/tables.scss index a10bd1544c5..339388392df 100644 --- a/app/assets/stylesheets/framework/tables.scss +++ b/app/assets/stylesheets/framework/tables.scss @@ -1,5 +1,6 @@ .table-holder { margin: 0; + overflow: auto; } table { @@ -38,6 +39,11 @@ table { &.wide { width: 55%; } + + &.table-th-transparent { + background: none; + color: $gl-text-color-secondary; + } } td { @@ -45,9 +51,91 @@ table { } } } + + &.responsive-table { + @include media-breakpoint-down(sm) { + thead { + display: none; + } + + &, + tbody, + td { + display: block; + } + + td { + color: $gl-text-color-secondary; + } + + tbody td.responsive-table-cell { + padding: $gl-padding 0; + width: 100%; + display: flex; + text-align: right; + align-items: center; + justify-content: space-between; + + &[data-column]::before { + content: attr(data-column); + display: block; + text-align: left; + padding-right: $gl-padding; + color: $gl-text-color-secondary; + } + + &:not([data-column]) { + flex-direction: row-reverse; + } + } + + tr.responsive-table-border-start, + tr.responsive-table-border-end { + display: block; + border: solid $gl-text-color-quaternary; + padding-left: 0; + padding-right: 0; + + > td { + border-color: $gl-text-color-quaternary; + + &, + &:last-child { + padding-left: $gl-padding; + padding-right: $gl-padding; + } + } + } + + tr.responsive-table-border-start { + border-width: 1px 1px 0; + border-radius: $border-radius-default $border-radius-default 0 0; + padding-top: 0; + padding-bottom: 0; + + > td:first-child { + border-top: 0; // always have the <table> top border + } + + > td:last-child { + border-bottom: 1px solid $gl-text-color-quaternary; + } + } + + tr.responsive-table-border-end { + border-width: 0 1px 1px; + border-radius: 0 0 $border-radius-default $border-radius-default; + margin-bottom: 2 * $gl-padding; + + > :last-child { + border-bottom: 0; + } + } + } + } } -.responsive-table { +.responsive-table:not(table) { @include media-breakpoint-down(sm) { th { width: 100%; diff --git a/app/assets/stylesheets/framework/terms.scss b/app/assets/stylesheets/framework/terms.scss index 744fd0ff796..7cda674e5c8 100644 --- a/app/assets/stylesheets/framework/terms.scss +++ b/app/assets/stylesheets/framework/terms.scss @@ -11,15 +11,15 @@ padding-top: $gl-padding; } - .panel { - .panel-heading { + .card { + .card-header { display: -webkit-flex; display: flex; align-items: center; justify-content: space-between; line-height: $line-height-base; - .title { + .card-title { display: flex; align-items: center; @@ -34,6 +34,8 @@ .navbar-collapse { padding-right: 0; + flex-grow: 0; + flex-basis: auto; .navbar-nav { margin: 0; diff --git a/app/assets/stylesheets/framework/timeline.scss b/app/assets/stylesheets/framework/timeline.scss index 75c11590547..dfb145debe7 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', map-get($grid-breakpoints, xs)) { + @include notes-media('max', map-get($grid-breakpoints, sm)) { background: none; } } @@ -34,7 +34,7 @@ .timeline-entry-inner { position: relative; - @include notes-media('max', map-get($grid-breakpoints, xs)) { + @include notes-media('max', map-get($grid-breakpoints, sm)) { .timeline-icon { display: none; } diff --git a/app/assets/stylesheets/framework/toggle.scss b/app/assets/stylesheets/framework/toggle.scss index d5cc78a6680..20394cc1e52 100644 --- a/app/assets/stylesheets/framework/toggle.scss +++ b/app/assets/stylesheets/framework/toggle.scss @@ -42,6 +42,10 @@ background: none; } + &:focus { + outline: none; + } + .toggle-icon { position: relative; display: block; diff --git a/app/assets/stylesheets/framework/typography.scss b/app/assets/stylesheets/framework/typography.scss index 97b821e0cb9..9e77ea03a24 100644 --- a/app/assets/stylesheets/framework/typography.scss +++ b/app/assets/stylesheets/framework/typography.scss @@ -114,26 +114,27 @@ font-size: 0.95em; } + blockquote, .blockquote { color: $gl-grayish-blue; font-size: inherit; padding: 8px 24px; margin: 16px 0; border-left: 3px solid $white-dark; - } - .blockquote:dir(rtl) { - border-left: 0; - border-right: 3px solid $white-dark; - } + &:dir(rtl) { + border-left: 0; + border-right: 3px solid $white-dark; + } - .blockquote p { - color: $gl-grayish-blue !important; - font-size: inherit; - line-height: 1.5; + p { + color: $gl-grayish-blue !important; + font-size: inherit; + line-height: 1.5; - &:last-child { - margin: 0; + &:last-child { + margin: 0; + } } } diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss index 946223cfff0..f30f296d41f 100644 --- a/app/assets/stylesheets/framework/variables.scss +++ b/app/assets/stylesheets/framework/variables.scss @@ -99,7 +99,7 @@ $theme-gray-200: #dfdfdf; $theme-gray-300: #cccccc; $theme-gray-400: #bababa; $theme-gray-500: #a7a7a7; -$theme-gray-600: #949494; +$theme-gray-600: #919191; $theme-gray-700: #707070; $theme-gray-800: #4f4f4f; $theme-gray-900: #2e2e2e; @@ -117,6 +117,15 @@ $theme-blue-800: #25496e; $theme-blue-900: #1a3652; $theme-blue-950: #0f2235; +$theme-light-blue-50: #f2f7fc; +$theme-light-blue-100: #ebf1f7; +$theme-light-blue-200: #c9dcf2; +$theme-light-blue-300: #83abd4; +$theme-light-blue-400: #4d86bf; +$theme-light-blue-500: #367cc2; +$theme-light-blue-600: #3771ab; +$theme-light-blue-700: #2261a1; + $theme-green-50: #f2faf6; $theme-green-100: #e4f3ea; $theme-green-200: #c0dfcd; @@ -129,6 +138,29 @@ $theme-green-800: #145d33; $theme-green-900: #0d4524; $theme-green-950: #072d16; +$theme-light-green-700: #156b39; + +$theme-red-50: #fcf4f2; +$theme-red-100: #fae9e6; +$theme-red-200: #ebcac5; +$theme-red-300: #d99b91; +$theme-red-400: #b0655a; +$theme-red-500: #ad4a3b; +$theme-red-600: #9e4133; +$theme-red-700: #912f20; +$theme-red-800: #78291d; +$theme-red-900: #691a16; +$theme-red-950: #36140f; + +$theme-light-red-50: #fff6f5; +$theme-light-red-100: #fae2de; +$theme-light-red-200: #f7d5d0; +$theme-light-red-300: #d9796a; +$theme-light-red-400: #cf604e; +$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); $almost-black: #242424; @@ -142,11 +174,6 @@ $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; @@ -164,7 +191,7 @@ $gl-font-weight-normal: 400; $gl-font-weight-bold: 600; $gl-text-color: #2e2e2e; $gl-text-color-secondary: #707070; -$gl-text-color-tertiary: #949494; +$gl-text-color-tertiary: #919191; $gl-text-color-quaternary: #d6d6d6; $gl-text-color-inverted: rgba(255, 255, 255, 1); $gl-text-color-secondary-inverted: rgba(255, 255, 255, 0.85); @@ -217,10 +244,11 @@ $tooltip-font-size: 12px; /* * Padding */ -$gl-padding-24: 24px; -$gl-padding: 16px; -$gl-padding-8: 8px; $gl-padding-4: 4px; +$gl-padding-8: 8px; +$gl-padding: 16px; +$gl-padding-24: 24px; +$gl-padding-32: 32px; $gl-col-padding: 15px; $gl-input-padding: 10px; $gl-vert-padding: 6px; @@ -238,7 +266,6 @@ $header-height: 40px; $ide-statusbar-height: 25px; $fixed-layout-width: 1280px; $limited-layout-width: 990px; -$limited-layout-width-sm: 790px; $container-text-max-width: 540px; $gl-avatar-size: 40px; $error-exclamation-point: $red-500; @@ -251,7 +278,7 @@ $active-item-blue: $blue-500; $layout-link-gray: #7e7c7c; $btn-side-margin: 10px; $btn-sm-side-margin: 7px; -$btn-xs-side-margin: 5px; +$btn-margin-5: 5px; $issue-status-expired: $orange-500; $issuable-sidebar-color: $gl-text-color-secondary; $sidebar-block-hover-color: #ebebeb; @@ -408,6 +435,22 @@ $badge-bg: rgba(0, 0, 0, 0.07); $badge-color: $gl-text-color-secondary; /* +* Pagination +*/ +$pagination-padding-y: 6px; +$pagination-padding-x: 16px; +$pagination-line-height: 20px; +$pagination-border-color: $border-color; +$pagination-active-bg: $blue-600; +$pagination-active-border-color: $blue-600; +$pagination-hover-bg: $blue-50; +$pagination-hover-border-color: $border-color; +$pagination-hover-color: $gl-text-color; +$pagination-disabled-color: #cdcdcd; +$pagination-disabled-bg: $gray-light; +$pagination-disabled-border-color: $border-color; + +/* * Status icons */ $status-icon-size: 22px; @@ -776,3 +819,19 @@ $modal-body-height: 134px; Prometheus */ $prometheus-table-row-highlight-color: $theme-gray-100; + +$priority-label-empty-state-width: 114px; + +/* + * Override Bootstrap 4 variables + */ + +$secondary: $gray-light; +$input-disabled-bg: $gray-light; +$input-border-color: $theme-gray-200; +$input-color: $gl-text-color; +$font-family-sans-serif: $regular_font; +$font-family-monospace: $monospace_font; +$input-line-height: 20px; +$btn-line-height: 20px; +$table-accent-bg: $gray-light; diff --git a/app/assets/stylesheets/mailers/highlighted_diff_email.scss b/app/assets/stylesheets/mailers/highlighted_diff_email.scss index b5eda79e5ed..1835c4364d3 100644 --- a/app/assets/stylesheets/mailers/highlighted_diff_email.scss +++ b/app/assets/stylesheets/mailers/highlighted_diff_email.scss @@ -138,6 +138,7 @@ pre { margin: 0; } +blockquote, .blockquote { color: $gl-grayish-blue; padding: 0 0 0 15px; diff --git a/app/assets/stylesheets/pages/boards.scss b/app/assets/stylesheets/pages/boards.scss index 1c3d312f7ac..7c1d1626f1c 100644 --- a/app/assets/stylesheets/pages/boards.scss +++ b/app/assets/stylesheets/pages/boards.scss @@ -80,6 +80,7 @@ overflow-x: scroll; white-space: nowrap; min-height: 200px; + display: flex; @include media-breakpoint-only(sm) { height: calc(100vh - #{$issue-board-list-difference-sm}); @@ -110,17 +111,15 @@ .board { display: inline-block; - width: calc(85vw - 15px); + flex: 1; + min-width: 300px; + max-width: 400px; height: 100%; padding-right: ($gl-padding / 2); padding-left: ($gl-padding / 2); white-space: normal; vertical-align: top; - @include media-breakpoint-up(sm) { - width: 400px; - } - &.is-expandable { .board-header { cursor: pointer; @@ -128,6 +127,8 @@ } &.is-collapsed { + flex: none; + min-width: 0; width: 50px; .board-header { @@ -282,9 +283,6 @@ 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; } diff --git a/app/assets/stylesheets/pages/builds.scss b/app/assets/stylesheets/pages/builds.scss index 9ee02ca1d83..f030189af06 100644 --- a/app/assets/stylesheets/pages/builds.scss +++ b/app/assets/stylesheets/pages/builds.scss @@ -12,26 +12,22 @@ @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); + box-shadow: 12px 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); + box-shadow: 12px 0 0 0 rgba($white-light, 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); + box-shadow: 12px 0 0 0 rgba($white-light, 0.2), 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); + box-shadow: 12px 0 0 0 rgba($white-light, 0.2), 24px 0 0 0 rgba($white-light, 0.2); } } @@ -71,10 +67,15 @@ .bash { display: block; } + + &.build-trace-rounded { + border-radius: $border-radius-base; + } } .top-bar { height: 35px; + min-height: 35px; background: $gray-light; border: 1px solid $border-color; color: $gl-text-color; diff --git a/app/assets/stylesheets/pages/clusters.scss b/app/assets/stylesheets/pages/clusters.scss index 3e4d123242c..56beb7718a4 100644 --- a/app/assets/stylesheets/pages/clusters.scss +++ b/app/assets/stylesheets/pages/clusters.scss @@ -13,6 +13,10 @@ max-width: 100%; } +.clusters-error-alert { + width: 100%; +} + .clusters-container { .nav-bar-right { padding: $gl-padding-top $gl-padding; diff --git a/app/assets/stylesheets/pages/commits.scss b/app/assets/stylesheets/pages/commits.scss index a4ca82de90e..dc8842212e0 100644 --- a/app/assets/stylesheets/pages/commits.scss +++ b/app/assets/stylesheets/pages/commits.scss @@ -193,6 +193,10 @@ display: inline-flex; } + .ci-status-icon svg { + vertical-align: text-bottom; + } + > .ci-status-link, > .btn, > .commit-sha-group { diff --git a/app/assets/stylesheets/pages/detail_page.scss b/app/assets/stylesheets/pages/detail_page.scss index 7b36bcb3c7d..2e007c52592 100644 --- a/app/assets/stylesheets/pages/detail_page.scss +++ b/app/assets/stylesheets/pages/detail_page.scss @@ -23,7 +23,8 @@ position: relative; line-height: 35px; display: flex; - flex-grow: 1; + flex: 1 1; + min-width: 0; @include media-breakpoint-up(sm) { padding-left: 0; diff --git a/app/assets/stylesheets/pages/diff.scss b/app/assets/stylesheets/pages/diff.scss index f06c9dcdf8c..fbc97ec0c95 100644 --- a/app/assets/stylesheets/pages/diff.scss +++ b/app/assets/stylesheets/pages/diff.scss @@ -189,8 +189,22 @@ img { border: 1px solid $white-light; - background-image: linear-gradient(45deg, $border-color 25%, transparent 25%, transparent 75%, $border-color 75%, $border-color 100%), - linear-gradient(45deg, $border-color 25%, transparent 25%, transparent 75%, $border-color 75%, $border-color 100%); + background-image: linear-gradient( + 45deg, + $border-color 25%, + transparent 25%, + transparent 75%, + $border-color 75%, + $border-color 100% + ), + linear-gradient( + 45deg, + $border-color 25%, + transparent 25%, + transparent 75%, + $border-color 75%, + $border-color 100% + ); background-size: 10px 10px; background-position: 0 0, 5px 5px; max-width: 100%; @@ -395,6 +409,69 @@ .line_content { white-space: pre-wrap; } + + .diff-file-container { + .frame.deleted { + border: 0; + background-color: inherit; + + .image_file img { + border: 1px solid $deleted; + } + } + + .frame.added { + border: 0; + background-color: inherit; + + .image_file img { + border: 1px solid $added; + } + } + + .swipe.view, + .onion-skin.view { + .swipe-wrap { + top: 0; + right: 0; + } + + .frame.deleted { + top: 0; + right: 0; + } + + .swipe-bar { + top: 0; + + .top-handle { + top: -14px; + left: -7px; + } + + .bottom-handle { + bottom: -14px; + left: -7px; + } + } + + .file-container { + display: inline-block; + + .file-content { + padding: 0; + + img { + max-width: none; + } + } + } + } + + .onion-skin.view .controls { + bottom: -25px; + } + } } .file-content .diff-file { @@ -536,7 +613,7 @@ margin-right: 0; border-color: $white-light; cursor: pointer; - transition: all .1s ease-out; + transition: all 0.1s ease-out; @for $i from 1 through 4 { &:nth-child(#{$i}) { @@ -563,7 +640,7 @@ height: 24px; border-radius: 50%; padding: 0; - transition: transform .1s ease-out; + transition: transform 0.1s ease-out; z-index: 100; .collapse-icon { @@ -708,11 +785,35 @@ width: 100%; height: 10px; background-color: $white-light; - background-image: linear-gradient(45deg, transparent, transparent 73%, $diff-jagged-border-gradient-color 75%, $white-light 80%), - linear-gradient(225deg, transparent, transparent 73%, $diff-jagged-border-gradient-color 75%, $white-light 80%), - linear-gradient(135deg, transparent, transparent 73%, $diff-jagged-border-gradient-color 75%, $white-light 80%), - linear-gradient(-45deg, transparent, transparent 73%, $diff-jagged-border-gradient-color 75%, $white-light 80%); - background-position: 5px 5px,0 5px,0 5px,5px 5px; + background-image: linear-gradient( + 45deg, + transparent, + transparent 73%, + $diff-jagged-border-gradient-color 75%, + $white-light 80% + ), + linear-gradient( + 225deg, + transparent, + transparent 73%, + $diff-jagged-border-gradient-color 75%, + $white-light 80% + ), + linear-gradient( + 135deg, + transparent, + transparent 73%, + $diff-jagged-border-gradient-color 75%, + $white-light 80% + ), + linear-gradient( + -45deg, + transparent, + transparent 73%, + $diff-jagged-border-gradient-color 75%, + $white-light 80% + ); + background-position: 5px 5px, 0 5px, 0 5px, 5px 5px; background-size: 10px 10px; background-repeat: repeat; } @@ -750,11 +851,16 @@ .frame.click-to-comment { position: relative; cursor: image-url('illustrations/image_comment_light_cursor.svg') - $image-comment-cursor-left-offset $image-comment-cursor-top-offset, auto; + $image-comment-cursor-left-offset $image-comment-cursor-top-offset, + auto; // Retina cursor - cursor: -webkit-image-set(image-url('illustrations/image_comment_light_cursor.svg') 1x, image-url('illustrations/image_comment_light_cursor@2x.svg') 2x) - $image-comment-cursor-left-offset $image-comment-cursor-top-offset, auto; + cursor: -webkit-image-set( + image-url('illustrations/image_comment_light_cursor.svg') 1x, + image-url('illustrations/image_comment_light_cursor@2x.svg') 2x + ) + $image-comment-cursor-left-offset $image-comment-cursor-top-offset, + auto; .comment-indicator { position: absolute; @@ -840,7 +946,7 @@ .diff-notes-collapse, .note, - .discussion-reply-holder, { + .discussion-reply-holder { display: none; } diff --git a/app/assets/stylesheets/pages/environments.scss b/app/assets/stylesheets/pages/environments.scss index cd0d67613c3..06f08ae2215 100644 --- a/app/assets/stylesheets/pages/environments.scss +++ b/app/assets/stylesheets/pages/environments.scss @@ -23,7 +23,6 @@ } .btn-group { - > a { color: $gl-text-color-secondary; } @@ -245,6 +244,7 @@ .prometheus-graph { flex: 1 0 auto; min-width: 450px; + max-width: 100%; padding: $gl-padding / 2; h5 { @@ -256,6 +256,17 @@ } } +.prometheus-graph-header { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: $gl-padding-8; + + h5 { + margin: 0; + } +} + .prometheus-graph-cursor { position: absolute; background: $theme-gray-600; diff --git a/app/assets/stylesheets/pages/groups.scss b/app/assets/stylesheets/pages/groups.scss index c2b42e02eee..05bf5596fb3 100644 --- a/app/assets/stylesheets/pages/groups.scss +++ b/app/assets/stylesheets/pages/groups.scss @@ -425,7 +425,7 @@ margin-left: 5px; > .btn { - margin-right: $btn-xs-side-margin; + margin-right: $btn-margin-5; } } } diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss index 4aea9740735..f9fd9f1ab8b 100644 --- a/app/assets/stylesheets/pages/issuable.scss +++ b/app/assets/stylesheets/pages/issuable.scss @@ -485,6 +485,15 @@ .sidebar-collapsed-user { padding-bottom: 0; margin-bottom: 10px; + + .author_link { + padding-left: 0; + + .avatar { + position: static; + margin: 0; + } + } } .issuable-header-btn { @@ -689,6 +698,8 @@ font-size: 14px; line-height: 24px; align-self: center; + overflow: hidden; + text-overflow: ellipsis; } .js-issuable-selector-wrap { diff --git a/app/assets/stylesheets/pages/labels.scss b/app/assets/stylesheets/pages/labels.scss index e178371d21f..79cac7f4ff0 100644 --- a/app/assets/stylesheets/pages/labels.scss +++ b/app/assets/stylesheets/pages/labels.scss @@ -57,67 +57,6 @@ border-bottom-left-radius: $border-radius-base; } -.label-row { - .label-name { - display: inline-block; - margin-bottom: 10px; - - @include media-breakpoint-up(sm) { - width: 200px; - margin-left: $gl-padding * 2; - margin-bottom: 0; - } - - .badge { - overflow: hidden; - text-overflow: ellipsis; - max-width: 100%; - } - } - - .label-type { - display: block; - margin-bottom: 10px; - margin-left: 50px; - - @include media-breakpoint-up(sm) { - display: inline-block; - width: 100px; - margin-left: 10px; - margin-bottom: 0; - vertical-align: top; - } - } - - .label-description { - display: block; - margin-bottom: 10px; - - .description-text { - margin-bottom: $gl-padding; - } - - a { - color: $blue-600; - } - - @include media-breakpoint-up(sm) { - display: inline-block; - max-width: 50%; - margin-left: 10px; - margin-bottom: 0; - vertical-align: top; - } - } - - .badge { - padding: 4px $grid-size; - font-size: $label-font-size; - position: relative; - top: ($grid-size / 2); - } -} - .color-label { padding: 0 $grid-size; line-height: 16px; @@ -133,26 +72,29 @@ } .manage-labels-list { - @media(min-width: map-get($grid-breakpoints, md)) { - &.content-list li { - padding: $gl-padding 0; - } - } - > li:not(.empty-message):not(.is-not-draggable) { background-color: $white-light; - cursor: move; - cursor: -webkit-grab; - cursor: -moz-grab; - - &:active { - cursor: -webkit-grabbing; - cursor: -moz-grabbing; - } + margin-bottom: 5px; + display: flex; + justify-content: space-between; + padding: $gl-padding; + border-radius: $border-radius-default; &.sortable-ghost { opacity: 0.3; } + + .prioritized-labels & { + box-shadow: 0 1px 2px $issue-boards-card-shadow; + cursor: move; + cursor: -webkit-grab; + cursor: -moz-grab; + + &:active { + cursor: -webkit-grabbing; + cursor: -moz-grabbing; + } + } } .btn-action { @@ -171,28 +113,11 @@ } } - .dropdown { - @include media-breakpoint-up(sm) { - float: right; - } - } - - @include media-breakpoint-down(xs) { - .dropdown-menu { - min-width: 100%; - } + .color-label { + padding: $gl-padding-4 $grid-size; } } -.draggable-handler { - display: inline-block; - vertical-align: top; - margin: 5px 0; - opacity: 0; - transition: opacity .3s; - color: $gray-darkest; -} - .prioritized-labels { margin-bottom: 30px; @@ -215,22 +140,6 @@ } } -.toggle-priority { - display: inline-block; - vertical-align: top; - - button { - border-color: transparent; - padding: 5px 8px; - vertical-align: top; - font-size: 14px; - - &:hover { - border-color: transparent; - } - } -} - .filtered-labels { font-size: 0; padding: 12px 16px; @@ -284,10 +193,8 @@ } .label-subscribe-button { - @media(min-width: map-get($grid-breakpoints, md)) { - min-width: 105px; - margin-left: $gl-padding; - } + width: 105px; + font-weight: 200; .label-subscribe-button-icon { &[disabled] { @@ -313,7 +220,7 @@ .label-link { display: inline-flex; - vertical-align: top; + vertical-align: text-bottom; &:hover .color-label { text-decoration: underline; @@ -324,3 +231,95 @@ font-size: $label-font-size; } } + +.labels-container { + background-color: $gray-light; + border-radius: $border-radius-default; + padding: $gl-padding $gl-padding-8; +} + +.label-actions-list { + list-style: none; + flex-shrink: 0; + padding: 0; +} + +.label-badge { + color: $theme-gray-900; + font-weight: $gl-font-weight-normal; + padding: $gl-padding-4 $gl-padding-8; + border-radius: $border-radius-default; + font-size: $label-font-size; +} + +.label-badge-blue { + background-color: $theme-blue-100; +} + +.label-badge-gray { + background-color: $theme-gray-100; +} + +.label-links { + list-style: none; + padding: 0; + white-space: nowrap; +} + +.label-link-item { + padding: 0; +} + +.label-list-item { + .content-list &::before, + .content-list &::after { + content: none; + } + + .label-name { + width: 150px; + flex-shrink: 0; + + .badge { + overflow: hidden; + text-overflow: ellipsis; + max-width: 100%; + } + } + + .label-description { + flex-grow: 1; + + a { + color: $blue-600; + } + } + + .label { + padding: 4px $grid-size; + font-size: $label-font-size; + position: relative; + top: $gl-padding-4; + } + + .label-action { + color: $theme-gray-800; + cursor: pointer; + + svg { + fill: $theme-gray-800; + } + + &:hover { + color: $blue-600; + + svg { + fill: $blue-600; + } + } + } +} + +.priority-labels-empty-state .svg-content img { + max-width: $priority-label-empty-state-width; +} diff --git a/app/assets/stylesheets/pages/members.scss b/app/assets/stylesheets/pages/members.scss index 9914555d309..5fdb2b4a90a 100644 --- a/app/assets/stylesheets/pages/members.scss +++ b/app/assets/stylesheets/pages/members.scss @@ -121,10 +121,6 @@ background: transparent; border: 0; outline: 0; - - @include media-breakpoint-up(sm) { - right: 160px; - } } .flex-project-members-panel { diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss index 9eceb3e9a33..99fe4a578be 100644 --- a/app/assets/stylesheets/pages/merge_requests.scss +++ b/app/assets/stylesheets/pages/merge_requests.scss @@ -51,12 +51,6 @@ opacity: 0.3; } - &.btn-xs { - line-height: 1; - padding: 5px 10px; - margin-top: 1px; - } - &.dropdown-toggle { .fa { color: inherit; @@ -678,6 +672,7 @@ .merge-request-tabs { display: flex; + flex-wrap: nowrap; margin-bottom: 0; padding: 0; } diff --git a/app/assets/stylesheets/pages/note_form.scss b/app/assets/stylesheets/pages/note_form.scss index 3b037d066dc..3849a04db5d 100644 --- a/app/assets/stylesheets/pages/note_form.scss +++ b/app/assets/stylesheets/pages/note_form.scss @@ -129,7 +129,7 @@ .icon svg { position: relative; top: 2px; - margin-right: $btn-xs-side-margin; + margin-right: $btn-margin-5; width: $gl-font-size; height: $gl-font-size; fill: $orange-600; diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss index f85f66b9c0b..52332ac97dd 100644 --- a/app/assets/stylesheets/pages/pipelines.scss +++ b/app/assets/stylesheets/pages/pipelines.scss @@ -36,6 +36,7 @@ } .table-holder { + overflow: unset; width: 100%; } @@ -321,18 +322,17 @@ } .build-failures { + th { + border-top: 0; + } + .build-state { padding: 20px 2px; .build-name { - float: right; font-weight: $gl-font-weight-normal; } - .ci-status-icon-failed svg { - vertical-align: middle; - } - .stage { color: $gl-text-color-secondary; font-weight: $gl-font-weight-normal; @@ -344,6 +344,81 @@ border: 0; line-height: initial; } + + .build-trace-row td { + border-top: 0; + border-bottom-width: 1px; + border-bottom-style: solid; + padding-top: 0; + } + + .build-trace { + width: 100%; + text-align: left; + margin-top: $gl-padding; + } + + .build-name { + width: 196px; + + a { + font-weight: $gl-font-weight-bold; + color: $gl-text-color; + text-decoration: none; + + &:focus, + &:hover { + text-decoration: underline; + } + } + } + + .build-actions { + width: 70px; + text-align: right; + } + + .build-stage { + width: 140px; + } + + .ci-status-icon-failed { + padding: 10px 0 10px 12px; + width: 12px + 24px; // padding-left + svg width + } + + .build-icon svg { + width: 24px; + height: 24px; + vertical-align: middle; + } + + .build-state, + .build-trace-row { + > td:last-child { + padding-right: 0; + } + } + + @include media-breakpoint-down(sm) { + td:empty { + display: none; + } + + .ci-table { + margin-top: 2 * $gl-padding; + } + + .build-trace-container { + padding-top: $gl-padding; + padding-bottom: $gl-padding; + } + + .build-trace { + margin-bottom: 0; + margin-top: 0; + } + } } .pipeline-tab-content { @@ -926,10 +1001,10 @@ button.mini-pipeline-graph-dropdown-toggle { /** * Center dropdown menu in mini graph */ - &.dropdown-menu { + .dropdown &.dropdown-menu { transform: translate(-80%, 0); - @media(min-width: map-get($grid-breakpoints, md)) { + @media (min-width: map-get($grid-breakpoints, md)) { transform: translate(-50%, 0); right: auto; left: 50%; diff --git a/app/assets/stylesheets/pages/profiles/preferences.scss b/app/assets/stylesheets/pages/profiles/preferences.scss index 68d40b56133..a353f301d07 100644 --- a/app/assets/stylesheets/pages/profiles/preferences.scss +++ b/app/assets/stylesheets/pages/profiles/preferences.scss @@ -1,25 +1,3 @@ -@mixin application-theme-preview($color-1, $color-2, $color-3, $color-4) { - .one { - background-color: $color-1; - border-top-left-radius: $border-radius-default; - } - - .two { - background-color: $color-2; - border-top-right-radius: $border-radius-default; - } - - .three { - background-color: $color-3; - border-bottom-left-radius: $border-radius-default; - } - - .four { - background-color: $color-4; - border-bottom-right-radius: $border-radius-default; - } -} - .multi-file-editor-options { label { margin-right: 20px; @@ -38,49 +16,67 @@ .application-theme { label { - margin-right: 20px; + margin: 0 $gl-padding-32 $gl-padding 0; text-align: center; } .preview { font-size: 0; - margin-bottom: 10px; + height: 48px; + border-radius: 4px; + min-width: 112px; + margin-bottom: $gl-padding-8; + + &.ui-indigo { + background-color: $indigo-900; + } - &.indigo { - @include application-theme-preview($indigo-900, $indigo-700, $indigo-800, $indigo-500); + &.ui-light-indigo { + background-color: $indigo-700; } - &.dark { - @include application-theme-preview($theme-gray-900, $theme-gray-700, $theme-gray-800, $theme-gray-600); + &.ui-blue { + background-color: $theme-blue-900; } - &.light { - @include application-theme-preview($theme-gray-600, $theme-gray-200, $theme-gray-400, $theme-gray-100); + &.ui-light-blue { + background-color: $theme-light-blue-700; } - &.blue { - @include application-theme-preview($theme-blue-900, $theme-blue-700, $theme-blue-800, $theme-blue-500); + &.ui-green { + background-color: $theme-green-900; } - &.green { - @include application-theme-preview($theme-green-900, $theme-green-700, $theme-green-800, $theme-green-500); + &.ui-light-green { + background-color: $theme-light-green-700; + } + + &.ui-red { + background-color: $theme-red-900; + } + + &.ui-light-red { + background-color: $theme-light-red-700; + } + + &.ui-dark { + background-color: $theme-gray-900; + } + + &.ui-light { + background-color: $theme-gray-200; } } .preview-row { display: block; } - - .quadrant { - display: inline-block; - height: 50px; - width: 80px; - } } .syntax-theme { label { - margin-right: 20px; + margin-right: $gl-padding-32; + margin-bottom: $gl-padding; text-align: center; .preview { @@ -89,7 +85,6 @@ img { border-radius: 4px; - max-width: 100%; } } diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss index 22964163e95..aa83e5bdebc 100644 --- a/app/assets/stylesheets/pages/projects.scss +++ b/app/assets/stylesheets/pages/projects.scss @@ -354,12 +354,6 @@ min-width: 200px; } -.deploy-keys { - .scrolling-tabs-container { - position: relative; - } -} - .deploy-key { // Ensure that the fingerprint does not overflow on small screens .fingerprint { @@ -503,6 +497,12 @@ &:not(:first-child) { border-top: 1px solid $border-color; } + + .btn-template-icon { + position: absolute; + left: $gl-padding; + top: $gl-padding; + } } .template-title { @@ -520,12 +520,6 @@ } } - svg { - position: absolute; - left: $gl-padding; - top: $gl-padding; - } - .project-fields-form { display: none; @@ -536,34 +530,23 @@ } .template-input-group { - position: relative; - - @include media-breakpoint-up(sm) { - display: flex; - } - - .input-group-prepend, - .input-group-append { + .input-group-prepend { flex: 1; - text-align: left; - padding-left: ($gl-padding * 3); - background-color: $white-light; } - .selected-template { - line-height: 20px; + .input-group-text { + width: 100%; + background-color: $white-light; } .selected-icon { + padding-right: $gl-padding; + svg { display: none; top: 7px; height: 20px; width: 20px; - - &.active { - display: block; - } } } } @@ -875,7 +858,6 @@ pre.light-well { .git-clone-holder { width: 380px; - height: 28px; .btn-clipboard { border: 1px solid $border-color; diff --git a/app/assets/stylesheets/pages/repo.scss b/app/assets/stylesheets/pages/repo.scss index 6bbcb15329c..0a56153203c 100644 --- a/app/assets/stylesheets/pages/repo.scss +++ b/app/assets/stylesheets/pages/repo.scss @@ -183,7 +183,7 @@ svg { position: relative; - top: -1px; + top: -2px; } .ide-file-changed-icon { @@ -335,7 +335,6 @@ img { max-width: 90%; - max-height: 90%; } .isZoomable { @@ -458,9 +457,9 @@ width: auto; margin-right: 0; - a:hover, - a:focus { - text-decoration: none; + > a, + > button { + height: 60px; } } @@ -541,32 +540,12 @@ margin-right: -$grid-size; min-height: 60px; - .multi-file-commit-list-item { - margin-left: 0; - margin-right: 0; - } - &.form-text.text-muted { margin-left: 0; right: 0; } } -.multi-file-commit-list-item { - .multi-file-discard-btn { - display: none; - margin-top: -2px; - margin-left: auto; - color: $gl-link-color; - } - - &:hover { - .multi-file-discard-btn { - display: flex; - } - } -} - .multi-file-addition, .multi-file-addition-solid { color: $green-500; @@ -596,7 +575,7 @@ } } -.multi-file-commit-list-item, +.multi-file-commit-list-path, .ide-file-list .file { display: flex; align-items: center; @@ -613,11 +592,9 @@ } .multi-file-commit-list-path { - padding: 0; - background: none; - border: 0; - text-align: left; - width: 100%; + &.is-active { + background-color: $white-normal; + } &:hover, &:focus { @@ -632,7 +609,7 @@ } .multi-file-commit-list-file-path { - @include str-truncated(100%); + @include str-truncated(calc(100% - 30px)); &:hover { text-decoration: underline; @@ -643,6 +620,16 @@ } } +.multi-file-discard-btn { + top: 4px; + right: 8px; + bottom: 4px; + + svg { + top: 0; + } +} + .multi-file-commit-form { position: relative; background-color: $white-light; @@ -718,9 +705,17 @@ } .ide-new-btn { + .btn { + padding-top: 3px; + padding-bottom: 3px; + } + + .dropdown { + display: flex; + } + .dropdown-toggle svg { - margin-top: -2px; - margin-bottom: 2px; + top: 0; } .dropdown-menu { @@ -829,18 +824,20 @@ } .ide-staged-action-btn { - margin-left: auto; - line-height: 22px; + width: 22px; + margin-left: -1px; + border-top-left-radius: 0; + border-bottom-left-radius: 0; + + > svg { + top: 0; + } } .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 { @@ -877,6 +874,7 @@ border-top: 1px solid transparent; border-bottom: 1px solid transparent; outline: 0; + cursor: pointer; svg { margin: 0 auto; @@ -1120,7 +1118,12 @@ .ide-context-header { .avatar { - flex: 0 0 40px; + flex: 0 0 38px; + } + + .ide-merge-requests-dropdown.dropdown-menu { + width: 385px; + max-height: initial; } } @@ -1130,11 +1133,20 @@ .sidebar-context-title { white-space: nowrap; } + + .ide-sidebar-branch-title { + min-width: 50px; + } } .ide-external-link { + position: relative; + svg { display: none; + position: absolute; + top: 2px; + right: -$gl-padding; } &:hover, @@ -1165,6 +1177,8 @@ display: flex; flex-direction: column; height: 100%; + margin-top: -$grid-size; + margin-bottom: -$grid-size; .empty-state { margin-top: auto; @@ -1181,6 +1195,17 @@ margin: 0; } } + + .build-trace, + .top-bar { + margin-left: -$gl-padding; + } + + &.build-page .top-bar { + top: 0; + font-size: 12px; + border-top-right-radius: $border-radius-default; + } } .ide-pipeline-list { @@ -1189,7 +1214,7 @@ } .ide-pipeline-header { - min-height: 50px; + min-height: 55px; padding-left: $gl-padding; padding-right: $gl-padding; @@ -1209,8 +1234,7 @@ .ci-status-icon { display: flex; justify-content: center; - height: 20px; - margin-top: -2px; + min-width: 24px; overflow: hidden; } } @@ -1240,3 +1264,56 @@ overflow: hidden; text-overflow: ellipsis; } + +.ide-job-header { + min-height: 60px; +} + +.ide-merge-requests-dropdown { + .nav-links li { + width: 50%; + padding-left: 0; + padding-right: 0; + + a { + text-align: center; + + &:not(.active) { + background-color: $gray-light; + } + } + } + + .dropdown-input { + padding-left: $gl-padding; + padding-right: $gl-padding; + + .fa { + right: 26px; + } + } + + .btn-link { + padding-top: $gl-padding; + padding-bottom: $gl-padding; + } +} + +.ide-merge-request-current-icon { + min-width: 18px; +} + +.ide-merge-requests-empty { + height: 230px; +} + +.ide-merge-requests-dropdown-content { + min-height: 230px; + max-height: 470px; +} + +.ide-merge-request-project-path { + font-size: 12px; + line-height: 16px; + color: $gl-text-color-secondary; +} diff --git a/app/assets/stylesheets/pages/search.scss b/app/assets/stylesheets/pages/search.scss index a35c4ff7c80..765c926751a 100644 --- a/app/assets/stylesheets/pages/search.scss +++ b/app/assets/stylesheets/pages/search.scss @@ -18,7 +18,8 @@ .file-finder-input:hover, .issuable-search-form:hover, .search-text-input:hover, -.form-control:hover { +.form-control:hover, +:not[readonly] { border-color: lighten($dropdown-input-focus-border, 20%); box-shadow: 0 0 4px lighten($search-input-focus-shadow-color, 20%); } @@ -28,7 +29,7 @@ input[type="checkbox"]:hover { } .search { - margin: 4px 8px 0; + margin: 0 8px; form { @extend .form-control; diff --git a/app/assets/stylesheets/pages/settings.scss b/app/assets/stylesheets/pages/settings.scss index 2b3773eebad..2f28031b9c8 100644 --- a/app/assets/stylesheets/pages/settings.scss +++ b/app/assets/stylesheets/pages/settings.scss @@ -52,7 +52,7 @@ .settings-content { max-height: 1px; - overflow-y: scroll; + overflow-y: hidden; padding-right: 110px; animation: collapseMaxHeight 300ms ease-out; // Keep the section from expanding when we scroll over it @@ -102,10 +102,6 @@ .form-text.text-muted { margin-top: 0; } - - .label-light { - margin-bottom: 0; - } } .settings-list-icon { @@ -131,12 +127,9 @@ color: $gl-danger; } -.service-settings .form-control-label { - padding-top: 0; -} - .integration-settings-form { - .card.card-body { + .card.card-body, + .info-well { padding: $gl-padding / 2; box-shadow: none; } @@ -174,7 +167,7 @@ .option-description, .option-disabled-reason { - margin-left: 45px; + margin-left: 30px; color: $project-option-descr-color; } @@ -296,7 +289,8 @@ } .btn-clipboard { - margin-left: 5px; + background-color: $white-light; + border: 1px solid $theme-gray-200; } .deploy-token-help-block { diff --git a/app/assets/stylesheets/pages/settings_ci_cd.scss b/app/assets/stylesheets/pages/settings_ci_cd.scss index a355e2dee24..777fdb3581e 100644 --- a/app/assets/stylesheets/pages/settings_ci_cd.scss +++ b/app/assets/stylesheets/pages/settings_ci_cd.scss @@ -16,3 +16,12 @@ .registry-placeholder { min-height: 60px; } + +.auto-devops-card { + margin-bottom: $gl-vert-padding; + + > .card-body { + border-radius: $card-border-radius; + padding: $gl-padding $gl-padding-24; + } +} diff --git a/app/assets/stylesheets/performance_bar.scss b/app/assets/stylesheets/performance_bar.scss index 06ef58531d7..5127ddfde6e 100644 --- a/app/assets/stylesheets/performance_bar.scss +++ b/app/assets/stylesheets/performance_bar.scss @@ -15,6 +15,7 @@ color: $perf-bar-text; select { + color: $perf-bar-text; width: 200px; } @@ -106,12 +107,12 @@ } .performance-bar-modal { - .modal-footer { - display: none; + .modal-body { + padding: 0; } - .modal-dialog { - width: 860px; + .modal-footer { + display: none; } } } diff --git a/app/assets/stylesheets/print.scss b/app/assets/stylesheets/print.scss index 90ccd4abd90..bb10928a037 100644 --- a/app/assets/stylesheets/print.scss +++ b/app/assets/stylesheets/print.scss @@ -22,9 +22,9 @@ header, nav, -nav.main-nav, nav.navbar-collapse, nav.navbar-collapse.collapse, +.nav-sidebar, .profiler-results, .tree-ref-holder, .tree-holder .breadcrumb, @@ -38,7 +38,8 @@ ul.notes-form, .edit-link, .note-action-button, .right-sidebar, -.flash-container { +.flash-container, +#js-peek { display: none !important; } diff --git a/app/controllers/admin/appearances_controller.rb b/app/controllers/admin/appearances_controller.rb index ea302f17d16..9aaec905734 100644 --- a/app/controllers/admin/appearances_controller.rb +++ b/app/controllers/admin/appearances_controller.rb @@ -41,6 +41,13 @@ class Admin::AppearancesController < Admin::ApplicationController redirect_to admin_appearances_path, notice: 'Header logo was succesfully removed.' end + def favicon + @appearance.remove_favicon! + @appearance.save + + redirect_to admin_appearances_path, notice: 'Favicon was succesfully removed.' + end + private # Use callbacks to share common setup or constraints between actions. @@ -61,6 +68,8 @@ class Admin::AppearancesController < Admin::ApplicationController logo_cache header_logo header_logo_cache + favicon + favicon_cache new_project_guidelines updated_by ] diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index db8a8cdc0d2..56312f801fb 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -91,6 +91,10 @@ class ApplicationController < ActionController::Base payload[:user_id] = logged_user.try(:id) payload[:username] = logged_user.try(:username) end + + if response.status == 422 && response.body.present? && response.content_type == 'application/json'.freeze + payload[:response] = response.body + end end # Controllers such as GitHttpController may use alternative methods @@ -130,12 +134,17 @@ class ApplicationController < ActionController::Base end def access_denied!(message = nil) + # If we display a custom access denied message to the user, we don't want to + # hide existence of the resource, rather tell them they cannot access it using + # the provided message + status = message.present? ? :forbidden : :not_found + respond_to do |format| - format.any { head :not_found } + format.any { head status } format.html do render "errors/access_denied", layout: "errors", - status: 404, + status: status, locals: { message: message } end end @@ -275,8 +284,10 @@ class ApplicationController < ActionController::Base return unless current_user return if current_user.terms_accepted? + message = _("Please accept the Terms of Service before continuing.") + if sessionless_user? - render_403 + access_denied!(message) else # Redirect to the destination if the request is a get. # Redirect to the source if it was a post, so the user can re-submit after @@ -287,7 +298,7 @@ class ApplicationController < ActionController::Base URI(request.referer).path if request.referer end - flash[:notice] = _("Please accept the Terms of Service before continuing.") + flash[:notice] = message redirect_to terms_path(redirect: redirect_path), status: :found end end diff --git a/app/controllers/boards/lists_controller.rb b/app/controllers/boards/lists_controller.rb index 381fd4d7508..e8b5934f2a9 100644 --- a/app/controllers/boards/lists_controller.rb +++ b/app/controllers/boards/lists_controller.rb @@ -56,8 +56,12 @@ module Boards private + def list_creation_attrs + %i[label_id] + end + def list_params - params.require(:list).permit(:label_id) + params.require(:list).permit(list_creation_attrs) end def move_params @@ -65,11 +69,15 @@ module Boards end def serialize_as_json(resource) - resource.as_json( + resource.as_json(serialization_attrs) + end + + def serialization_attrs + { only: [:id, :list_type, :position], methods: [:title], label: true - ) + } end end end diff --git a/app/controllers/concerns/internal_redirect.rb b/app/controllers/concerns/internal_redirect.rb index 7409b2e89a5..10b9852e329 100644 --- a/app/controllers/concerns/internal_redirect.rb +++ b/app/controllers/concerns/internal_redirect.rb @@ -23,6 +23,10 @@ module InternalRedirect nil end + def sanitize_redirect(url_or_path) + safe_redirect_path(url_or_path) || safe_redirect_path_for_url(url_or_path) + end + def host_allowed?(uri) uri.host == request.host && uri.port == request.port diff --git a/app/controllers/concerns/issuable_actions.rb b/app/controllers/concerns/issuable_actions.rb index c925b4aada5..d04eb192129 100644 --- a/app/controllers/concerns/issuable_actions.rb +++ b/app/controllers/concerns/issuable_actions.rb @@ -7,6 +7,19 @@ module IssuableActions before_action :authorize_admin_issuable!, only: :bulk_update end + def permitted_keys + [ + :issuable_ids, + :assignee_id, + :milestone_id, + :state_event, + :subscription_event, + label_ids: [], + add_label_ids: [], + remove_label_ids: [] + ] + end + def show respond_to do |format| format.html @@ -140,24 +153,15 @@ module IssuableActions end def bulk_update_params - permitted_keys = [ - :issuable_ids, - :assignee_id, - :milestone_id, - :state_event, - :subscription_event, - label_ids: [], - add_label_ids: [], - remove_label_ids: [] - ] + permitted_keys_array = permitted_keys.dup if resource_name == 'issue' - permitted_keys << { assignee_ids: [] } + permitted_keys_array << { assignee_ids: [] } else - permitted_keys.unshift(:assignee_id) + permitted_keys_array.unshift(:assignee_id) end - params.require(:update).permit(permitted_keys) + params.require(:update).permit(permitted_keys_array) end def resource_name diff --git a/app/controllers/concerns/issuable_collections.rb b/app/controllers/concerns/issuable_collections.rb index ca1b80a36a0..2ef2ee76855 100644 --- a/app/controllers/concerns/issuable_collections.rb +++ b/app/controllers/concerns/issuable_collections.rb @@ -95,12 +95,7 @@ module IssuableCollections elsif @group @filter_params[:group_id] = @group.id @filter_params[:include_subgroups] = true - else - # TODO: this filter ignore issues/mr created in public or - # internal repos where you are not a member. Enable this filter - # or improve current implementation to filter only issues you - # created or assigned or mentioned - # @filter_params[:authorized_only] = true + @filter_params[:use_cte_for_search] = true end @filter_params.permit(finder_type.valid_params) diff --git a/app/controllers/concerns/issues_action.rb b/app/controllers/concerns/issues_action.rb index b6eb7d292fc..9d58656773d 100644 --- a/app/controllers/concerns/issues_action.rb +++ b/app/controllers/concerns/issues_action.rb @@ -1,6 +1,7 @@ module IssuesAction extend ActiveSupport::Concern include IssuableCollections + include IssuesCalendar # rubocop:disable Gitlab/ModuleWithInstanceVariables def issues @@ -17,18 +18,9 @@ module IssuesAction end # rubocop:enable Gitlab/ModuleWithInstanceVariables - # rubocop:disable Gitlab/ModuleWithInstanceVariables def issues_calendar - @issues = issuables_collection - .non_archived - .with_due_date - .limit(100) - - respond_to do |format| - format.ics { response.headers['Content-Disposition'] = 'inline' } - end + render_issues_calendar(issuables_collection) end - # rubocop:enable Gitlab/ModuleWithInstanceVariables private diff --git a/app/controllers/concerns/issues_calendar.rb b/app/controllers/concerns/issues_calendar.rb new file mode 100644 index 00000000000..671a204621d --- /dev/null +++ b/app/controllers/concerns/issues_calendar.rb @@ -0,0 +1,24 @@ +module IssuesCalendar + extend ActiveSupport::Concern + + # rubocop:disable Gitlab/ModuleWithInstanceVariables + def render_issues_calendar(issuables) + @issues = issuables + .non_archived + .with_due_date + .limit(100) + + respond_to do |format| + format.ics do + # NOTE: with text/calendar as Content-Type, the browser always downloads + # the content as a file (even ignoring the Content-Disposition + # header). We want to display the content inline when accessed + # from GitLab, similarly to the RSS feed. + if request.referer&.start_with?(::Settings.gitlab.base_url) + response.headers['Content-Type'] = 'text/plain' + end + end + end + end + # rubocop:enable Gitlab/ModuleWithInstanceVariables +end diff --git a/app/controllers/concerns/uploads_actions.rb b/app/controllers/concerns/uploads_actions.rb index b9b9b6e4e88..16374146ae4 100644 --- a/app/controllers/concerns/uploads_actions.rb +++ b/app/controllers/concerns/uploads_actions.rb @@ -1,8 +1,14 @@ module UploadsActions + extend ActiveSupport::Concern + include Gitlab::Utils::StrongMemoize include SendFileUpload - UPLOAD_MOUNTS = %w(avatar attachment file logo header_logo).freeze + UPLOAD_MOUNTS = %w(avatar attachment file logo header_logo favicon).freeze + + included do + prepend_before_action :set_html_format, only: :show + end def create link_to_file = UploadService.new(model, params[:file], uploader_class).execute @@ -31,11 +37,23 @@ module UploadsActions disposition = uploader.image_or_video? ? 'inline' : 'attachment' + uploaders = [uploader, *uploader.versions.values] + uploader = uploaders.find { |version| version.filename == params[:filename] } + + return render_404 unless uploader + send_upload(uploader, attachment: uploader.filename, disposition: disposition) end private + # Explicitly set the format. + # Otherwise rails 5 will set it from a file extension. + # See https://github.com/rails/rails/commit/84e8accd6fb83031e4c27e44925d7596655285f7#diff-2b8f2fbb113b55ca8e16001c393da8f1 + def set_html_format + request.format = :html + end + def uploader_class raise NotImplementedError end diff --git a/app/controllers/graphql_controller.rb b/app/controllers/graphql_controller.rb new file mode 100644 index 00000000000..0a1cf169aca --- /dev/null +++ b/app/controllers/graphql_controller.rb @@ -0,0 +1,45 @@ +class GraphqlController < ApplicationController + # Unauthenticated users have access to the API for public data + skip_before_action :authenticate_user! + + before_action :check_graphql_feature_flag! + + def execute + variables = Gitlab::Graphql::Variables.new(params[:variables]).to_h + query = params[:query] + operation_name = params[:operationName] + context = { + current_user: current_user + } + result = GitlabSchema.execute(query, variables: variables, context: context, operation_name: operation_name) + render json: result + end + + rescue_from StandardError do |exception| + log_exception(exception) + + render_error("Internal server error") + end + + rescue_from Gitlab::Graphql::Variables::Invalid do |exception| + render_error(exception.message, status: :unprocessable_entity) + end + + private + + # Overridden from the ApplicationController to make the response look like + # a GraphQL response. That is nicely picked up in Graphiql. + def render_404 + render_error("Not found!", status: :not_found) + end + + def render_error(message, status: 500) + error = { errors: [message: message] } + + render json: error, status: status + end + + def check_graphql_feature_flag! + render_404 unless Feature.enabled?(:graphql) + end +end diff --git a/app/controllers/groups/group_members_controller.rb b/app/controllers/groups/group_members_controller.rb index ef3eba80154..ef5d5e5c742 100644 --- a/app/controllers/groups/group_members_controller.rb +++ b/app/controllers/groups/group_members_controller.rb @@ -3,8 +3,12 @@ class Groups::GroupMembersController < Groups::ApplicationController include MembersPresentation include SortingHelper + def self.admin_not_required_endpoints + %i[index leave request_access] + end + # Authorize - before_action :authorize_admin_group_member!, except: [:index, :leave, :request_access] + before_action :authorize_admin_group_member!, except: admin_not_required_endpoints skip_cross_project_access_check :index, :create, :update, :destroy, :request_access, :approve_access_request, :leave, :resend_invite, diff --git a/app/controllers/groups/labels_controller.rb b/app/controllers/groups/labels_controller.rb index 58be330f466..863f50e8e66 100644 --- a/app/controllers/groups/labels_controller.rb +++ b/app/controllers/groups/labels_controller.rb @@ -2,6 +2,7 @@ class Groups::LabelsController < Groups::ApplicationController include ToggleSubscriptionAction before_action :label, only: [:edit, :update, :destroy] + before_action :available_labels, only: [:index] before_action :authorize_admin_labels!, only: [:new, :create, :edit, :update, :destroy] before_action :save_previous_label_path, only: [:edit] @@ -12,17 +13,8 @@ class Groups::LabelsController < Groups::ApplicationController format.html do @labels = @group.labels.page(params[:page]) end - format.json do - available_labels = LabelsFinder.new( - current_user, - group_id: @group.id, - only_group_labels: params[:only_group_labels], - include_ancestor_groups: params[:include_ancestor_groups], - include_descendant_groups: params[:include_descendant_groups] - ).execute - - render json: LabelSerializer.new.represent_appearance(available_labels) + render json: LabelSerializer.new.represent_appearance(@available_labels) end end end @@ -113,4 +105,15 @@ class Groups::LabelsController < Groups::ApplicationController def save_previous_label_path session[:previous_labels_path] = URI(request.referer || '').path end + + def available_labels + @available_labels ||= + LabelsFinder.new( + current_user, + group_id: @group.id, + only_group_labels: params[:only_group_labels], + include_ancestor_groups: params[:include_ancestor_groups], + include_descendant_groups: params[:include_descendant_groups] + ).execute + end end diff --git a/app/controllers/groups/milestones_controller.rb b/app/controllers/groups/milestones_controller.rb index 5903689dc62..9bd51de7e97 100644 --- a/app/controllers/groups/milestones_controller.rb +++ b/app/controllers/groups/milestones_controller.rb @@ -76,12 +76,15 @@ class Groups::MilestonesController < Groups::ApplicationController def milestones milestones = MilestonesFinder.new(search_params).execute - legacy_milestones = GroupMilestone.build_collection(group, group_projects, params) @sort = params[:sort] || 'due_date_asc' MilestoneArray.sort(milestones + legacy_milestones, @sort) end + def legacy_milestones + GroupMilestone.build_collection(group, group_projects, params) + end + def milestone @milestone = if params[:title] diff --git a/app/controllers/groups/shared_projects_controller.rb b/app/controllers/groups/shared_projects_controller.rb index f2f835767e0..7dec1f5f402 100644 --- a/app/controllers/groups/shared_projects_controller.rb +++ b/app/controllers/groups/shared_projects_controller.rb @@ -24,7 +24,9 @@ module Groups # Make the `search` param consistent for the frontend, # which will be using `filter`. params[:search] ||= params[:filter] if params[:filter] - params.permit(:sort, :search) + # Don't show archived projects + params[:non_archived] = true + params.permit(:sort, :search, :non_archived) end end end diff --git a/app/controllers/import/base_controller.rb b/app/controllers/import/base_controller.rb index 663269a0f92..5766c6924cd 100644 --- a/app/controllers/import/base_controller.rb +++ b/app/controllers/import/base_controller.rb @@ -25,4 +25,8 @@ class Import::BaseController < ApplicationController current_user.namespace end + + def project_save_error(project) + project.errors.full_messages.join(', ') + end end diff --git a/app/controllers/import/bitbucket_controller.rb b/app/controllers/import/bitbucket_controller.rb index 77af5fb9c4f..fa31933e778 100644 --- a/app/controllers/import/bitbucket_controller.rb +++ b/app/controllers/import/bitbucket_controller.rb @@ -55,7 +55,7 @@ class Import::BitbucketController < Import::BaseController if project.persisted? render json: ProjectSerializer.new.represent(project) else - render json: { errors: project.errors.full_messages }, status: :unprocessable_entity + render json: { errors: project_save_error(project) }, status: :unprocessable_entity end else render json: { errors: 'This namespace has already been taken! Please choose another one.' }, status: :unprocessable_entity diff --git a/app/controllers/import/fogbugz_controller.rb b/app/controllers/import/fogbugz_controller.rb index 25ec13b8075..2d665e05ac3 100644 --- a/app/controllers/import/fogbugz_controller.rb +++ b/app/controllers/import/fogbugz_controller.rb @@ -66,7 +66,7 @@ class Import::FogbugzController < Import::BaseController if project.persisted? render json: ProjectSerializer.new.represent(project) else - render json: { errors: project.errors.full_messages }, status: :unprocessable_entity + render json: { errors: project_save_error(project) }, status: :unprocessable_entity end end diff --git a/app/controllers/import/github_controller.rb b/app/controllers/import/github_controller.rb index f67ec4c248b..c9870332c0f 100644 --- a/app/controllers/import/github_controller.rb +++ b/app/controllers/import/github_controller.rb @@ -48,7 +48,7 @@ class Import::GithubController < Import::BaseController if project.persisted? render json: ProjectSerializer.new.represent(project) else - render json: { errors: project.errors.full_messages }, status: :unprocessable_entity + render json: { errors: project_save_error(project) }, status: :unprocessable_entity end else render json: { errors: 'This namespace has already been taken! Please choose another one.' }, status: :unprocessable_entity diff --git a/app/controllers/import/gitlab_controller.rb b/app/controllers/import/gitlab_controller.rb index 39e2e9e094b..fccbdbca0f6 100644 --- a/app/controllers/import/gitlab_controller.rb +++ b/app/controllers/import/gitlab_controller.rb @@ -32,7 +32,7 @@ class Import::GitlabController < Import::BaseController if project.persisted? render json: ProjectSerializer.new.represent(project) else - render json: { errors: project.errors.full_messages }, status: :unprocessable_entity + render json: { errors: project_save_error(project) }, status: :unprocessable_entity end else render json: { errors: 'This namespace has already been taken! Please choose another one.' }, status: :unprocessable_entity diff --git a/app/controllers/import/google_code_controller.rb b/app/controllers/import/google_code_controller.rb index 9b26a00f7c7..3bce27e810a 100644 --- a/app/controllers/import/google_code_controller.rb +++ b/app/controllers/import/google_code_controller.rb @@ -92,7 +92,7 @@ class Import::GoogleCodeController < Import::BaseController if project.persisted? render json: ProjectSerializer.new.represent(project) else - render json: { errors: project.errors.full_messages }, status: :unprocessable_entity + render json: { errors: project_save_error(project) }, status: :unprocessable_entity end end diff --git a/app/controllers/projects/artifacts_controller.rb b/app/controllers/projects/artifacts_controller.rb index abc283d7aa9..6484a713f8e 100644 --- a/app/controllers/projects/artifacts_controller.rb +++ b/app/controllers/projects/artifacts_controller.rb @@ -7,6 +7,7 @@ class Projects::ArtifactsController < Projects::ApplicationController before_action :authorize_read_build! before_action :authorize_update_build!, only: [:keep] before_action :extract_ref_name_and_path + before_action :set_request_format, only: [:file] before_action :validate_artifacts! before_action :entry, only: [:file] @@ -101,4 +102,12 @@ class Projects::ArtifactsController < Projects::ApplicationController render_404 unless @entry.exists? end + + def set_request_format + request.format = :html if set_request_format? + end + + def set_request_format? + request.format != :json + end end diff --git a/app/controllers/projects/blob_controller.rb b/app/controllers/projects/blob_controller.rb index 0c1c286a0a4..a8c0a68fc17 100644 --- a/app/controllers/projects/blob_controller.rb +++ b/app/controllers/projects/blob_controller.rb @@ -7,6 +7,7 @@ class Projects::BlobController < Projects::ApplicationController prepend_before_action :authenticate_user!, only: [:edit] + before_action :set_request_format, only: [:edit, :show, :update] before_action :require_non_empty_project, except: [:new, :create] before_action :authorize_download_code! @@ -188,6 +189,18 @@ class Projects::BlobController < Projects::ApplicationController .last_for_path(@repository, @ref, @path).sha end + # In Rails 4.2 if params[:format] is empty, Rails set it to :html + # But since Rails 5.0 the framework now looks for an extension. + # E.g. for `blob/master/CHANGELOG.md` in Rails 4 the format would be `:html`, but in Rails 5 on it'd be `:md` + # This before_action explicitly sets the `:html` format for all requests unless `:format` is set by a client e.g. by JS for XHR requests. + def set_request_format + request.format = :html if set_request_format? + end + + def set_request_format? + params[:id].present? && params[:format].blank? && request.format != "json" + end + def show_html environment_params = @repository.branch_exists?(@ref) ? { ref: @ref } : { commit: @commit } @environment = EnvironmentsFinder.new(@project, current_user, environment_params).execute.last @@ -197,15 +210,14 @@ class Projects::BlobController < Projects::ApplicationController end def show_json - json = blob_json(@blob) - return render_404 unless json - + set_last_commit_sha path_segments = @path.split('/') path_segments.pop tree_path = path_segments.join('/') - render json: json.merge( + json = { id: @blob.id, + last_commit_sha: @last_commit_sha, path: blob.path, name: blob.name, extension: blob.extension, @@ -221,6 +233,10 @@ class Projects::BlobController < Projects::ApplicationController commits_path: project_commits_path(project, @id), tree_path: project_tree_path(project, File.join(@ref, tree_path)), permalink: project_blob_path(project, File.join(@commit.id, @path)) - ) + } + + json.merge!(blob_json(@blob) || {}) unless params[:viewer] == 'none' + + render json: json end end diff --git a/app/controllers/projects/branches_controller.rb b/app/controllers/projects/branches_controller.rb index b7b36f770f5..cd7250b10fc 100644 --- a/app/controllers/projects/branches_controller.rb +++ b/app/controllers/projects/branches_controller.rb @@ -31,7 +31,10 @@ class Projects::BranchesController < Projects::ApplicationController end end - render + # https://gitlab.com/gitlab-org/gitlab-ce/issues/48097 + Gitlab::GitalyClient.allow_n_plus_1_calls do + render + end end format.json do branches = BranchesFinder.new(@repository, params).execute diff --git a/app/controllers/projects/clusters/applications_controller.rb b/app/controllers/projects/clusters/applications_controller.rb index 4d758402850..a5c82caa897 100644 --- a/app/controllers/projects/clusters/applications_controller.rb +++ b/app/controllers/projects/clusters/applications_controller.rb @@ -42,6 +42,6 @@ class Projects::Clusters::ApplicationsController < Projects::ApplicationControll owner: current_user } - Applications::CreateService.new(current_user, oauth_application_params).execute + Applications::CreateService.new(current_user, oauth_application_params).execute(request) end end diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb index 35c36c725e2..7c897b2d86c 100644 --- a/app/controllers/projects/issues_controller.rb +++ b/app/controllers/projects/issues_controller.rb @@ -4,6 +4,7 @@ class Projects::IssuesController < Projects::ApplicationController include IssuableActions include ToggleAwardEmoji include IssuableCollections + include IssuesCalendar include SpammableActions prepend_before_action :authenticate_user!, only: [:new] @@ -40,14 +41,7 @@ class Projects::IssuesController < Projects::ApplicationController end def calendar - @issues = @issuables - .non_archived - .with_due_date - .limit(100) - - respond_to do |format| - format.ics { response.headers['Content-Disposition'] = 'inline' } - end + render_issues_calendar(@issuables) end def new diff --git a/app/controllers/projects/lfs_storage_controller.rb b/app/controllers/projects/lfs_storage_controller.rb index 43d8867a536..45c98d60822 100644 --- a/app/controllers/projects/lfs_storage_controller.rb +++ b/app/controllers/projects/lfs_storage_controller.rb @@ -18,7 +18,7 @@ class Projects::LfsStorageController < Projects::GitHttpClientController def upload_authorize set_workhorse_internal_api_content_type - authorized = LfsObjectUploader.workhorse_authorize + authorized = LfsObjectUploader.workhorse_authorize(has_length: true) authorized.merge!(LfsOid: oid, LfsSize: size) render json: authorized diff --git a/app/controllers/projects/merge_requests/application_controller.rb b/app/controllers/projects/merge_requests/application_controller.rb index 29632bef7e5..8e4aeec16dc 100644 --- a/app/controllers/projects/merge_requests/application_controller.rb +++ b/app/controllers/projects/merge_requests/application_controller.rb @@ -15,7 +15,7 @@ class Projects::MergeRequests::ApplicationController < Projects::ApplicationCont def merge_request_params_attributes [ - :allow_maintainer_to_push, + :allow_collaboration, :assignee_id, :description, :force_remove_source_branch, diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb index ecea6e1b2bf..38918b3cd52 100644 --- a/app/controllers/projects/merge_requests_controller.rb +++ b/app/controllers/projects/merge_requests_controller.rb @@ -28,15 +28,14 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo end def show - validates_merge_request - close_merge_request_without_source_project - check_if_can_be_merged - - # Return if the response has already been rendered - return if response_body + close_merge_request_if_no_source_project + mark_merge_request_mergeable respond_to do |format| format.html do + # use next to appease Rubocop + next render('invalid') if target_branch_missing? + # Build a note object for comment form @note = @project.notes.new(noteable: @merge_request) @@ -116,7 +115,7 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo end format.json do - render json: @merge_request.to_json(include: { milestone: {}, assignee: { only: [:name, :username], methods: [:avatar_url] }, labels: { methods: :text_color } }, methods: [:task_status, :task_status_short]) + render json: serializer.represent(@merge_request, serializer: 'basic') end end rescue ActiveRecord::StaleObjectError @@ -234,20 +233,6 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo alias_method :issuable, :merge_request alias_method :awardable, :merge_request - def validates_merge_request - # Show git not found page - # if there is no saved commits between source & target branch - if @merge_request.has_no_commits? - # and if target branch doesn't exist - return invalid_mr unless @merge_request.target_branch_exists? - end - end - - def invalid_mr - # Render special view for MR with removed target branch - render 'invalid' - end - def merge_params params.permit(merge_params_attributes) end @@ -261,7 +246,7 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo @merge_request.head_pipeline && @merge_request.head_pipeline.active? end - def close_merge_request_without_source_project + def close_merge_request_if_no_source_project if !@merge_request.source_project && @merge_request.open? @merge_request.close end @@ -269,7 +254,11 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo private - def check_if_can_be_merged + def target_branch_missing? + @merge_request.has_no_commits? && !@merge_request.target_branch_exists? + end + + def mark_merge_request_mergeable @merge_request.check_if_can_be_merged end diff --git a/app/controllers/projects/milestones_controller.rb b/app/controllers/projects/milestones_controller.rb index c5a044541f1..f85dcfe6bfc 100644 --- a/app/controllers/projects/milestones_controller.rb +++ b/app/controllers/projects/milestones_controller.rb @@ -1,4 +1,5 @@ class Projects::MilestonesController < Projects::ApplicationController + include Gitlab::Utils::StrongMemoize include MilestoneActions before_action :check_issuables_available! @@ -103,7 +104,7 @@ class Projects::MilestonesController < Projects::ApplicationController protected def milestones - @milestones ||= begin + strong_memoize(:milestones) do MilestonesFinder.new(search_params).execute end end @@ -121,10 +122,10 @@ class Projects::MilestonesController < Projects::ApplicationController end def search_params - if @project.group && can?(current_user, :read_group, @project.group) - group = @project.group + if request.format.json? && @project.group && can?(current_user, :read_group, @project.group) + groups = @project.group.self_and_ancestors_ids end - params.permit(:state).merge(project_ids: @project.id, group_ids: group&.id) + params.permit(:state).merge(project_ids: @project.id, group_ids: groups) end end diff --git a/app/controllers/projects/pipelines_controller.rb b/app/controllers/projects/pipelines_controller.rb index 6b40fc2fe68..768595ceeb4 100644 --- a/app/controllers/projects/pipelines_controller.rb +++ b/app/controllers/projects/pipelines_controller.rb @@ -23,8 +23,6 @@ class Projects::PipelinesController < Projects::ApplicationController @finished_count = limited_pipelines_count(project, 'finished') @pipelines_count = limited_pipelines_count(project) - Gitlab::Ci::Pipeline::Preloader.preload(@pipelines) - respond_to do |format| format.html format.json do @@ -34,7 +32,7 @@ class Projects::PipelinesController < Projects::ApplicationController pipelines: PipelineSerializer .new(project: @project, current_user: @current_user) .with_pagination(request, response) - .represent(@pipelines, disable_coverage: true), + .represent(@pipelines, disable_coverage: true, preload: true), count: { all: @pipelines_count, running: @running_count, diff --git a/app/controllers/projects/settings/ci_cd_controller.rb b/app/controllers/projects/settings/ci_cd_controller.rb index 1d850baf012..fb3f6eec2bd 100644 --- a/app/controllers/projects/settings/ci_cd_controller.rb +++ b/app/controllers/projects/settings/ci_cd_controller.rb @@ -41,7 +41,7 @@ module Projects :runners_token, :builds_enabled, :build_allow_git_fetch, :build_timeout_human_readable, :build_coverage_regex, :public_builds, :auto_cancel_pending_pipelines, :ci_config_path, - auto_devops_attributes: [:id, :domain, :enabled] + auto_devops_attributes: [:id, :domain, :enabled, :deploy_strategy] ) end diff --git a/app/controllers/registrations_controller.rb b/app/controllers/registrations_controller.rb index f5a222b3a48..e6d6965036e 100644 --- a/app/controllers/registrations_controller.rb +++ b/app/controllers/registrations_controller.rb @@ -3,6 +3,9 @@ class RegistrationsController < Devise::RegistrationsController include AcceptsPendingInvitations before_action :whitelist_query_limiting, only: [:destroy] + before_action :ensure_terms_accepted, + if: -> { Gitlab::CurrentSettings.current_application_settings.enforce_terms? }, + only: [:create] def new redirect_to(new_user_session_path) @@ -18,7 +21,9 @@ class RegistrationsController < Devise::RegistrationsController if !Gitlab::Recaptcha.load_configurations! || verify_recaptcha accept_pending_invitations - super + super do |new_user| + persist_accepted_terms_if_required(new_user) + end else flash[:alert] = 'There was an error with the reCAPTCHA. Please solve the reCAPTCHA again.' flash.delete :recaptcha_error @@ -40,6 +45,16 @@ class RegistrationsController < Devise::RegistrationsController protected + def persist_accepted_terms_if_required(new_user) + return unless new_user.persisted? + return unless Gitlab::CurrentSettings.current_application_settings.enforce_terms? + + if terms_accepted? + terms = ApplicationSetting::Term.latest + Users::RespondToTermsService.new(new_user, terms).execute(accepted: true) + end + end + def destroy_confirmation_valid? if current_user.confirm_deletion_with_password? current_user.valid_password?(params[:password]) @@ -91,4 +106,14 @@ class RegistrationsController < Devise::RegistrationsController def whitelist_query_limiting Gitlab::QueryLimiting.whitelist('https://gitlab.com/gitlab-org/gitlab-ce/issues/42380') end + + def ensure_terms_accepted + return if terms_accepted? + + redirect_to new_user_session_path, alert: _('You must accept our Terms of Service and privacy policy in order to register an account') + end + + def terms_accepted? + Gitlab::Utils.to_boolean(params[:terms_opt_in]) + end end diff --git a/app/controllers/users/terms_controller.rb b/app/controllers/users/terms_controller.rb index ab685b9106e..1b1560a2a00 100644 --- a/app/controllers/users/terms_controller.rb +++ b/app/controllers/users/terms_controller.rb @@ -2,6 +2,7 @@ module Users class TermsController < ApplicationController include InternalRedirect + skip_before_action :authenticate_user! skip_before_action :enforce_terms! skip_before_action :check_password_expiration skip_before_action :check_two_factor_requirement @@ -13,6 +14,10 @@ module Users def index @redirect = redirect_path + + if current_user && @term.accepted_by_user?(current_user) + flash.now[:notice] = "You have already accepted the Terms of Service as #{current_user.to_reference}" + end end def accept diff --git a/app/finders/group_members_finder.rb b/app/finders/group_members_finder.rb index 067aff408df..2a656c0d31c 100644 --- a/app/finders/group_members_finder.rb +++ b/app/finders/group_members_finder.rb @@ -3,17 +3,29 @@ class GroupMembersFinder @group = group end - def execute + def execute(include_descendants: false) group_members = @group.members + wheres = [] - return group_members unless @group.parent + return group_members unless @group.parent || include_descendants - parents_members = GroupMember.non_request - .where(source_id: @group.ancestors.select(:id)) - .where.not(user_id: @group.users.select(:id)) + wheres << "members.id IN (#{group_members.select(:id).to_sql})" - wheres = ["members.id IN (#{group_members.select(:id).to_sql})"] - wheres << "members.id IN (#{parents_members.select(:id).to_sql})" + if @group.parent + parents_members = GroupMember.non_request + .where(source_id: @group.ancestors.select(:id)) + .where.not(user_id: @group.users.select(:id)) + + wheres << "members.id IN (#{parents_members.select(:id).to_sql})" + end + + if include_descendants + descendant_members = GroupMember.non_request + .where(source_id: @group.descendants.select(:id)) + .where.not(user_id: @group.users.select(:id)) + + wheres << "members.id IN (#{descendant_members.select(:id).to_sql})" + end GroupMember.where(wheres.join(' OR ')) end diff --git a/app/finders/issuable_finder.rb b/app/finders/issuable_finder.rb index c6ef79ce15e..5d5f72c4d86 100644 --- a/app/finders/issuable_finder.rb +++ b/app/finders/issuable_finder.rb @@ -23,6 +23,7 @@ # created_before: datetime # updated_after: datetime # updated_before: datetime +# use_cte_for_search: boolean # class IssuableFinder prepend FinderWithCrossProjectAccess @@ -54,6 +55,7 @@ class IssuableFinder sort state include_subgroups + use_cte_for_search ] end @@ -74,19 +76,21 @@ class IssuableFinder items = init_collection items = filter_items(items) - # Filtering by project HAS TO be the last because we use the project IDs yielded by the issuable query thus far - items = by_project(items) + # This has to be last as we may use a CTE as an optimization fence by + # passing the use_cte_for_search param + # https://www.postgresql.org/docs/current/static/queries-with.html + items = by_search(items) sort(items) end def filter_items(items) + items = by_project(items) items = by_scope(items) items = by_created_at(items) items = by_updated_at(items) items = by_state(items) items = by_group(items) - items = by_search(items) items = by_assignee(items) items = by_author(items) items = by_non_archived(items) @@ -107,7 +111,6 @@ class IssuableFinder # def count_by_state count_params = params.merge(state: nil, sort: nil) - labels_count = label_names.any? ? label_names.count : 1 finder = self.class.new(current_user, count_params) counts = Hash.new(0) @@ -116,6 +119,11 @@ class IssuableFinder # per issuable, so we have to count those in Ruby - which is bad, but still # better than performing multiple queries. # + # This does not apply when we are using a CTE for the search, as the labels + # GROUP BY is inside the subquery in that case, so we set labels_count to 1. + labels_count = label_names.any? ? label_names.count : 1 + labels_count = 1 if use_cte_for_search? + finder.execute.reorder(nil).group(:state).count.each do |key, value| counts[Array(key).last.to_sym] += value / labels_count end @@ -159,10 +167,7 @@ class IssuableFinder finder_options = { include_subgroups: params[:include_subgroups], only_owned: true } GroupProjectsFinder.new(group: group, current_user: current_user, options: finder_options).execute else - opts = { current_user: current_user } - opts[:project_ids_relation] = item_project_ids(items) if items - - ProjectsFinder.new(opts).execute + ProjectsFinder.new(current_user: current_user).execute end @projects = projects.with_feature_available_for_user(klass, current_user).reorder(nil) @@ -329,8 +334,24 @@ class IssuableFinder items end + def use_cte_for_search? + return false unless search + return false unless Gitlab::Database.postgresql? + + params[:use_cte_for_search] + end + def by_search(items) - search ? items.full_search(search) : items + return items unless search + + if use_cte_for_search? + cte = Gitlab::SQL::RecursiveCTE.new(klass.table_name) + cte << items + + items = klass.with(cte.to_arel).from(klass.table_name) + end + + items.full_search(search) end def by_iids(items) diff --git a/app/finders/issues_finder.rb b/app/finders/issues_finder.rb index 3626670d141..24a6b9349a0 100644 --- a/app/finders/issues_finder.rb +++ b/app/finders/issues_finder.rb @@ -136,8 +136,4 @@ class IssuesFinder < IssuableFinder items end end - - def item_project_ids(items) - items&.reorder(nil)&.select(:project_id) - end end diff --git a/app/finders/members_finder.rb b/app/finders/members_finder.rb index 4734d97b8c7..4c893ae2de6 100644 --- a/app/finders/members_finder.rb +++ b/app/finders/members_finder.rb @@ -7,12 +7,12 @@ class MembersFinder @group = project.group end - def execute + def execute(include_descendants: false) project_members = project.project_members project_members = project_members.non_invite unless can?(current_user, :admin_project, project) if group - group_members = GroupMembersFinder.new(group).execute + group_members = GroupMembersFinder.new(group).execute(include_descendants: include_descendants) group_members = group_members.non_invite union = Gitlab::SQL::Union.new([project_members, group_members], remove_duplicates: false) diff --git a/app/finders/merge_requests_finder.rb b/app/finders/merge_requests_finder.rb index e2240e5e0d8..8d84ed4bdfb 100644 --- a/app/finders/merge_requests_finder.rb +++ b/app/finders/merge_requests_finder.rb @@ -56,8 +56,4 @@ class MergeRequestsFinder < IssuableFinder items.where(target_branch: target_branch) end - - def item_project_ids(items) - items&.reorder(nil)&.select(:target_project_id) - end end diff --git a/app/finders/snippets_finder.rb b/app/finders/snippets_finder.rb index d498a2d6d11..9d3772d7541 100644 --- a/app/finders/snippets_finder.rb +++ b/app/finders/snippets_finder.rb @@ -54,7 +54,10 @@ class SnippetsFinder < UnionFinder end def authorized_snippets - Snippet.where(feature_available_projects.or(not_project_related)) + # This query was intentionally converted to a raw one to get it work in Rails 5.0. + # In Rails 5.0 and 5.1 there's a bug: https://github.com/rails/arel/issues/531 + # Please convert it back when on rails 5.2 as it works again as expected since 5.2. + Snippet.where("#{feature_available_projects} OR #{not_project_related}") .public_or_visible_to_user(current_user) end @@ -86,18 +89,20 @@ class SnippetsFinder < UnionFinder def feature_available_projects # Don't return any project related snippets if the user cannot read cross project - return table[:id].eq(nil) unless Ability.allowed?(current_user, :read_cross_project) + return table[:id].eq(nil).to_sql unless Ability.allowed?(current_user, :read_cross_project) projects = projects_for_user do |part| part.with_feature_available_for_user(:snippets, current_user) end.select(:id) - arel_query = Arel::Nodes::SqlLiteral.new(projects.to_sql) - table[:project_id].in(arel_query) + # This query was intentionally converted to a raw one to get it work in Rails 5.0. + # In Rails 5.0 and 5.1 there's a bug: https://github.com/rails/arel/issues/531 + # Please convert it back when on rails 5.2 as it works again as expected since 5.2. + "snippets.project_id IN (#{projects.to_sql})" end def not_project_related - table[:project_id].eq(nil) + table[:project_id].eq(nil).to_sql end def table diff --git a/app/graphql/functions/base_function.rb b/app/graphql/functions/base_function.rb new file mode 100644 index 00000000000..42fb8f99acc --- /dev/null +++ b/app/graphql/functions/base_function.rb @@ -0,0 +1,4 @@ +module Functions + class BaseFunction < GraphQL::Function + end +end diff --git a/app/graphql/functions/echo.rb b/app/graphql/functions/echo.rb new file mode 100644 index 00000000000..e5bf109b8d7 --- /dev/null +++ b/app/graphql/functions/echo.rb @@ -0,0 +1,13 @@ +module Functions + class Echo < BaseFunction + argument :text, GraphQL::STRING_TYPE + + description "Testing endpoint to validate the API with" + + def call(obj, args, ctx) + username = ctx[:current_user]&.username + + "#{username.inspect} says: #{args[:text]}" + end + end +end diff --git a/app/graphql/gitlab_schema.rb b/app/graphql/gitlab_schema.rb new file mode 100644 index 00000000000..de4fc1d8e32 --- /dev/null +++ b/app/graphql/gitlab_schema.rb @@ -0,0 +1,8 @@ +class GitlabSchema < GraphQL::Schema + use BatchLoader::GraphQL + use Gitlab::Graphql::Authorize + use Gitlab::Graphql::Present + + query(Types::QueryType) + # mutation(Types::MutationType) +end diff --git a/app/graphql/mutations/.keep b/app/graphql/mutations/.keep new file mode 100644 index 00000000000..e69de29bb2d --- /dev/null +++ b/app/graphql/mutations/.keep diff --git a/app/graphql/resolvers/base_resolver.rb b/app/graphql/resolvers/base_resolver.rb new file mode 100644 index 00000000000..89b7f9dad6f --- /dev/null +++ b/app/graphql/resolvers/base_resolver.rb @@ -0,0 +1,4 @@ +module Resolvers + class BaseResolver < GraphQL::Schema::Resolver + end +end diff --git a/app/graphql/resolvers/full_path_resolver.rb b/app/graphql/resolvers/full_path_resolver.rb new file mode 100644 index 00000000000..4eb28aaed6c --- /dev/null +++ b/app/graphql/resolvers/full_path_resolver.rb @@ -0,0 +1,19 @@ +module Resolvers + module FullPathResolver + extend ActiveSupport::Concern + + prepended do + argument :full_path, GraphQL::ID_TYPE, + required: true, + description: 'The full path of the project or namespace, e.g., "gitlab-org/gitlab-ce"' + end + + def model_by_full_path(model, full_path) + BatchLoader.for(full_path).batch(key: "#{model.model_name.param_key}:full_path") do |full_paths, loader| + # `with_route` avoids an N+1 calculating full_path + results = model.where_full_path_in(full_paths).with_route + results.each { |project| loader.call(project.full_path, project) } + end + end + end +end diff --git a/app/graphql/resolvers/merge_request_resolver.rb b/app/graphql/resolvers/merge_request_resolver.rb new file mode 100644 index 00000000000..9f2d348e95f --- /dev/null +++ b/app/graphql/resolvers/merge_request_resolver.rb @@ -0,0 +1,20 @@ +module Resolvers + class MergeRequestResolver < BaseResolver + argument :iid, GraphQL::ID_TYPE, + required: true, + description: 'The IID of the merge request, e.g., "1"' + + type Types::MergeRequestType, null: true + + alias_method :project, :object + + def resolve(iid:) + return unless project.present? + + BatchLoader.for(iid.to_s).batch(key: project.id) do |iids, loader| + results = project.merge_requests.where(iid: iids) + results.each { |mr| loader.call(mr.iid.to_s, mr) } + end + end + end +end diff --git a/app/graphql/resolvers/project_resolver.rb b/app/graphql/resolvers/project_resolver.rb new file mode 100644 index 00000000000..ec115bad896 --- /dev/null +++ b/app/graphql/resolvers/project_resolver.rb @@ -0,0 +1,11 @@ +module Resolvers + class ProjectResolver < BaseResolver + prepend FullPathResolver + + type Types::ProjectType, null: true + + def resolve(full_path:) + model_by_full_path(Project, full_path) + end + end +end diff --git a/app/graphql/types/base_enum.rb b/app/graphql/types/base_enum.rb new file mode 100644 index 00000000000..b45a845f74f --- /dev/null +++ b/app/graphql/types/base_enum.rb @@ -0,0 +1,4 @@ +module Types + class BaseEnum < GraphQL::Schema::Enum + end +end diff --git a/app/graphql/types/base_field.rb b/app/graphql/types/base_field.rb new file mode 100644 index 00000000000..c5740a334d7 --- /dev/null +++ b/app/graphql/types/base_field.rb @@ -0,0 +1,5 @@ +module Types + class BaseField < GraphQL::Schema::Field + prepend Gitlab::Graphql::Authorize + end +end diff --git a/app/graphql/types/base_input_object.rb b/app/graphql/types/base_input_object.rb new file mode 100644 index 00000000000..309e336e6c8 --- /dev/null +++ b/app/graphql/types/base_input_object.rb @@ -0,0 +1,4 @@ +module Types + class BaseInputObject < GraphQL::Schema::InputObject + end +end diff --git a/app/graphql/types/base_interface.rb b/app/graphql/types/base_interface.rb new file mode 100644 index 00000000000..69e72dc5808 --- /dev/null +++ b/app/graphql/types/base_interface.rb @@ -0,0 +1,5 @@ +module Types + module BaseInterface + include GraphQL::Schema::Interface + end +end diff --git a/app/graphql/types/base_object.rb b/app/graphql/types/base_object.rb new file mode 100644 index 00000000000..e033ef96ce9 --- /dev/null +++ b/app/graphql/types/base_object.rb @@ -0,0 +1,7 @@ +module Types + class BaseObject < GraphQL::Schema::Object + prepend Gitlab::Graphql::Present + + field_class Types::BaseField + end +end diff --git a/app/graphql/types/base_scalar.rb b/app/graphql/types/base_scalar.rb new file mode 100644 index 00000000000..c0aa38be239 --- /dev/null +++ b/app/graphql/types/base_scalar.rb @@ -0,0 +1,4 @@ +module Types + class BaseScalar < GraphQL::Schema::Scalar + end +end diff --git a/app/graphql/types/base_union.rb b/app/graphql/types/base_union.rb new file mode 100644 index 00000000000..36337fc6ee5 --- /dev/null +++ b/app/graphql/types/base_union.rb @@ -0,0 +1,4 @@ +module Types + class BaseUnion < GraphQL::Schema::Union + end +end diff --git a/app/graphql/types/merge_request_type.rb b/app/graphql/types/merge_request_type.rb new file mode 100644 index 00000000000..d5d24952984 --- /dev/null +++ b/app/graphql/types/merge_request_type.rb @@ -0,0 +1,47 @@ +module Types + class MergeRequestType < BaseObject + present_using MergeRequestPresenter + + graphql_name 'MergeRequest' + + field :id, GraphQL::ID_TYPE, null: false + field :iid, GraphQL::ID_TYPE, null: false + field :title, GraphQL::STRING_TYPE, null: false + field :description, GraphQL::STRING_TYPE, null: true + field :state, GraphQL::STRING_TYPE, null: true + field :created_at, Types::TimeType, null: false + field :updated_at, Types::TimeType, null: false + field :source_project, Types::ProjectType, null: true + field :target_project, Types::ProjectType, null: false + # Alias for target_project + field :project, Types::ProjectType, null: false + field :project_id, GraphQL::INT_TYPE, null: false, method: :target_project_id + field :source_project_id, GraphQL::INT_TYPE, null: true + field :target_project_id, GraphQL::INT_TYPE, null: false + field :source_branch, GraphQL::STRING_TYPE, null: false + field :target_branch, GraphQL::STRING_TYPE, null: false + field :work_in_progress, GraphQL::BOOLEAN_TYPE, method: :work_in_progress?, null: false + field :merge_when_pipeline_succeeds, GraphQL::BOOLEAN_TYPE, null: true + field :diff_head_sha, GraphQL::STRING_TYPE, null: true + field :merge_commit_sha, GraphQL::STRING_TYPE, null: true + field :user_notes_count, GraphQL::INT_TYPE, null: true + field :should_remove_source_branch, GraphQL::BOOLEAN_TYPE, method: :should_remove_source_branch?, null: true + field :force_remove_source_branch, GraphQL::BOOLEAN_TYPE, method: :force_remove_source_branch?, null: true + field :merge_status, GraphQL::STRING_TYPE, null: true + field :in_progress_merge_commit_sha, GraphQL::STRING_TYPE, null: true + field :merge_error, GraphQL::STRING_TYPE, null: true + field :allow_collaboration, GraphQL::BOOLEAN_TYPE, null: true + field :should_be_rebased, GraphQL::BOOLEAN_TYPE, method: :should_be_rebased?, null: false + field :rebase_commit_sha, GraphQL::STRING_TYPE, null: true + field :rebase_in_progress, GraphQL::BOOLEAN_TYPE, method: :rebase_in_progress?, null: false + field :diff_head_sha, GraphQL::STRING_TYPE, null: true + field :merge_commit_message, GraphQL::STRING_TYPE, null: true + field :merge_ongoing, GraphQL::BOOLEAN_TYPE, method: :merge_ongoing?, null: false + field :source_branch_exists, GraphQL::BOOLEAN_TYPE, method: :source_branch_exists?, null: false + field :mergeable_discussions_state, GraphQL::BOOLEAN_TYPE, null: true + field :web_url, GraphQL::STRING_TYPE, null: true + field :upvotes, GraphQL::INT_TYPE, null: false + field :downvotes, GraphQL::INT_TYPE, null: false + field :subscribed, GraphQL::BOOLEAN_TYPE, method: :subscribed?, null: false + end +end diff --git a/app/graphql/types/mutation_type.rb b/app/graphql/types/mutation_type.rb new file mode 100644 index 00000000000..06ed91c1658 --- /dev/null +++ b/app/graphql/types/mutation_type.rb @@ -0,0 +1,7 @@ +module Types + class MutationType < BaseObject + graphql_name "Mutation" + + # TODO: Add Mutations as fields + end +end diff --git a/app/graphql/types/project_type.rb b/app/graphql/types/project_type.rb new file mode 100644 index 00000000000..d9058ae7431 --- /dev/null +++ b/app/graphql/types/project_type.rb @@ -0,0 +1,72 @@ +module Types + class ProjectType < BaseObject + graphql_name 'Project' + + field :id, GraphQL::ID_TYPE, null: false + + field :full_path, GraphQL::ID_TYPE, null: false + field :path, GraphQL::STRING_TYPE, null: false + + field :name_with_namespace, GraphQL::STRING_TYPE, null: false + field :name, GraphQL::STRING_TYPE, null: false + + field :description, GraphQL::STRING_TYPE, null: true + + field :default_branch, GraphQL::STRING_TYPE, null: true + field :tag_list, GraphQL::STRING_TYPE, null: true + + field :ssh_url_to_repo, GraphQL::STRING_TYPE, null: true + field :http_url_to_repo, GraphQL::STRING_TYPE, null: true + field :web_url, GraphQL::STRING_TYPE, null: true + + field :star_count, GraphQL::INT_TYPE, null: false + field :forks_count, GraphQL::INT_TYPE, null: false + + field :created_at, Types::TimeType, null: true + field :last_activity_at, Types::TimeType, null: true + + field :archived, GraphQL::BOOLEAN_TYPE, null: true + + field :visibility, GraphQL::STRING_TYPE, null: true + + field :container_registry_enabled, GraphQL::BOOLEAN_TYPE, null: true + field :shared_runners_enabled, GraphQL::BOOLEAN_TYPE, null: true + field :lfs_enabled, GraphQL::BOOLEAN_TYPE, null: true + field :merge_requests_ff_only_enabled, GraphQL::BOOLEAN_TYPE, null: true + + field :avatar_url, GraphQL::STRING_TYPE, null: true, resolve: -> (project, args, ctx) do + project.avatar_url(only_path: false) + end + + %i[issues merge_requests wiki snippets].each do |feature| + field "#{feature}_enabled", GraphQL::BOOLEAN_TYPE, null: true, resolve: -> (project, args, ctx) do + project.feature_available?(feature, ctx[:current_user]) + end + end + + field :jobs_enabled, GraphQL::BOOLEAN_TYPE, null: true, resolve: -> (project, args, ctx) do + project.feature_available?(:builds, ctx[:current_user]) + end + + field :public_jobs, GraphQL::BOOLEAN_TYPE, method: :public_builds, null: true + + field :open_issues_count, GraphQL::INT_TYPE, null: true, resolve: -> (project, args, ctx) do + project.open_issues_count if project.feature_available?(:issues, ctx[:current_user]) + end + + field :import_status, GraphQL::STRING_TYPE, null: true + field :ci_config_path, GraphQL::STRING_TYPE, null: true + + field :only_allow_merge_if_pipeline_succeeds, GraphQL::BOOLEAN_TYPE, null: true + field :request_access_enabled, GraphQL::BOOLEAN_TYPE, null: true + field :only_allow_merge_if_all_discussions_are_resolved, GraphQL::BOOLEAN_TYPE, null: true + field :printing_merge_request_link_enabled, GraphQL::BOOLEAN_TYPE, null: true + + field :merge_request, + Types::MergeRequestType, + null: true, + resolver: Resolvers::MergeRequestResolver do + authorize :read_merge_request + end + end +end diff --git a/app/graphql/types/query_type.rb b/app/graphql/types/query_type.rb new file mode 100644 index 00000000000..010ec2d7942 --- /dev/null +++ b/app/graphql/types/query_type.rb @@ -0,0 +1,14 @@ +module Types + class QueryType < BaseObject + graphql_name 'Query' + + field :project, Types::ProjectType, + null: true, + resolver: Resolvers::ProjectResolver, + description: "Find a project" do + authorize :read_project + end + + field :echo, GraphQL::STRING_TYPE, null: false, function: Functions::Echo.new + end +end diff --git a/app/graphql/types/time_type.rb b/app/graphql/types/time_type.rb new file mode 100644 index 00000000000..2333d82ad1e --- /dev/null +++ b/app/graphql/types/time_type.rb @@ -0,0 +1,14 @@ +module Types + class TimeType < BaseScalar + graphql_name 'Time' + description 'Time represented in ISO 8601' + + def self.coerce_input(value, ctx) + Time.parse(value) + end + + def self.coerce_result(value, ctx) + value.iso8601 + end + end +end diff --git a/app/helpers/application_settings_helper.rb b/app/helpers/application_settings_helper.rb index adc423af9e1..ef1bf283d0c 100644 --- a/app/helpers/application_settings_helper.rb +++ b/app/helpers/application_settings_helper.rb @@ -36,7 +36,7 @@ module ApplicationSettingsHelper # Return a group of checkboxes that use Bootstrap's button plugin for a # toggle button effect. - def restricted_level_checkboxes(help_block_id, checkbox_name) + def restricted_level_checkboxes(help_block_id, checkbox_name, options = {}) Gitlab::VisibilityLevel.values.map do |level| checked = restricted_visibility_levels(true).include?(level) css_class = checked ? 'active' : '' @@ -46,6 +46,7 @@ module ApplicationSettingsHelper check_box_tag(checkbox_name, level, checked, autocomplete: 'off', 'aria-describedby' => help_block_id, + 'class' => options[:class], id: tag_name) + visibility_level_icon(level) + visibility_level_label(level) end end @@ -53,7 +54,7 @@ module ApplicationSettingsHelper # Return a group of checkboxes that use Bootstrap's button plugin for a # toggle button effect. - def import_sources_checkboxes(help_block_id) + def import_sources_checkboxes(help_block_id, options = {}) Gitlab::ImportSources.options.map do |name, source| checked = Gitlab::CurrentSettings.import_sources.include?(source) css_class = checked ? 'active' : '' @@ -63,6 +64,7 @@ module ApplicationSettingsHelper check_box_tag(checkbox_name, source, checked, autocomplete: 'off', 'aria-describedby' => help_block_id, + 'class' => options[:class], id: name.tr(' ', '_')) + name end end diff --git a/app/helpers/favicon_helper.rb b/app/helpers/favicon_helper.rb new file mode 100644 index 00000000000..3a5342a8d9d --- /dev/null +++ b/app/helpers/favicon_helper.rb @@ -0,0 +1,7 @@ +module FaviconHelper + def favicon_extension_whitelist + FaviconUploader::EXTENSION_WHITELIST + .map { |extension| "'.#{extension}'"} + .to_sentence + end +end diff --git a/app/helpers/icons_helper.rb b/app/helpers/icons_helper.rb index 58372edff3c..2f304b040c7 100644 --- a/app/helpers/icons_helper.rb +++ b/app/helpers/icons_helper.rb @@ -60,7 +60,7 @@ module IconsHelper def spinner(text = nil, visible = false) css_class = 'loading' - css_class << ' hidden' unless visible + css_class << ' hide' unless visible content_tag :div, class: css_class do icon('spinner spin') + text diff --git a/app/helpers/issuables_helper.rb b/app/helpers/issuables_helper.rb index 8572c2b7276..9f501ea55fb 100644 --- a/app/helpers/issuables_helper.rb +++ b/app/helpers/issuables_helper.rb @@ -157,7 +157,7 @@ module IssuablesHelper output = "" output << "Opened #{time_ago_with_tooltip(issuable.created_at)} by ".html_safe output << content_tag(:strong) do - author_output = link_to_member(project, issuable.author, size: 24, mobile_classes: "d-none d-sm-inline-block", tooltip: true) + author_output = link_to_member(project, issuable.author, size: 24, mobile_classes: "d-none d-sm-inline", tooltip: true) author_output << link_to_member(project, issuable.author, size: 24, by_username: true, avatar: false, mobile_classes: "d-block d-sm-none") end @@ -165,7 +165,7 @@ module IssuablesHelper output << content_tag(:span, (issuable_first_contribution_icon if issuable.first_contribution?), class: 'has-tooltip', title: _('1st contribution!')) output << content_tag(:span, (issuable.task_status if issuable.tasks?), id: "task_status", class: "d-none d-sm-none d-md-inline-block") - output << content_tag(:span, (issuable.task_status_short if issuable.tasks?), id: "task_status_short", class: "d-md-none d-lg-none d-xl-inline-block") + output << content_tag(:span, (issuable.task_status_short if issuable.tasks?), id: "task_status_short", class: "d-md-none") output.html_safe end diff --git a/app/helpers/labels_helper.rb b/app/helpers/labels_helper.rb index e1b0e7a4a3e..c7df25cecef 100644 --- a/app/helpers/labels_helper.rb +++ b/app/helpers/labels_helper.rb @@ -211,6 +211,14 @@ module LabelsHelper end end + def label_status_tooltip(label, status) + type = label.is_a?(ProjectLabel) ? 'project' : 'group' + level = status.unsubscribed? ? type : status.sub('-level', '') + action = status.unsubscribed? ? 'Subscribe' : 'Unsubscribe' + + "#{action} at #{level} level" + end + # Required for Banzai::Filter::LabelReferenceFilter module_function :render_colored_label, :text_color_for_bg, :escape_once end diff --git a/app/helpers/merge_requests_helper.rb b/app/helpers/merge_requests_helper.rb index 74251c260f0..5ff06b3e0fc 100644 --- a/app/helpers/merge_requests_helper.rb +++ b/app/helpers/merge_requests_helper.rb @@ -126,8 +126,8 @@ module MergeRequestsHelper link_to(url[merge_request.project, merge_request], data: data_attrs, &block) end - def allow_maintainer_push_unavailable_reason(merge_request) - return if merge_request.can_allow_maintainer_to_push?(current_user) + def allow_collaboration_unavailable_reason(merge_request) + return if merge_request.can_allow_collaboration?(current_user) minimum_visibility = [merge_request.target_project.visibility_level, merge_request.source_project.visibility_level].min diff --git a/app/helpers/page_layout_helper.rb b/app/helpers/page_layout_helper.rb index a8397b03d63..68d892393ef 100644 --- a/app/helpers/page_layout_helper.rb +++ b/app/helpers/page_layout_helper.rb @@ -39,10 +39,7 @@ module PageLayoutHelper end def favicon - return 'favicon-yellow.ico' if Gitlab::Utils.to_boolean(ENV['CANARY']) - return 'favicon-blue.ico' if Rails.env.development? - - 'favicon.ico' + Gitlab::Favicon.main end def page_image diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb index 55078e1a2d2..daad829faa2 100644 --- a/app/helpers/projects_helper.rb +++ b/app/helpers/projects_helper.rb @@ -171,11 +171,12 @@ module ProjectsHelper key = [ project.route.cache_key, project.cache_key, + project.last_activity_date, controller.controller_name, controller.action_name, Gitlab::CurrentSettings.cache_key, "cross-project:#{can?(current_user, :read_cross_project)}", - 'v2.5' + 'v2.6' ] key << pipeline_status_cache_key(project.pipeline_status) if project.pipeline_status.has_status? @@ -238,6 +239,14 @@ module ProjectsHelper "git push --set-upstream #{repository_url}/$(git rev-parse --show-toplevel | xargs basename).git $(git rev-parse --abbrev-ref HEAD)" end + def show_xcode_link?(project = @project) + browser.platform.mac? && project.repository.xcode_project? + end + + def xcode_uri_to_repo(project = @project) + "xcode://clone?repo=#{CGI.escape(default_url_to_repo(project))}" + end + private def get_project_nav_tabs(project, current_user) @@ -381,11 +390,11 @@ module ProjectsHelper def project_status_css_class(status) case status when "started" - "active" + "table-active" when "failed" - "danger" + "table-danger" when "finished" - "success" + "table-success" end end @@ -404,7 +413,10 @@ module ProjectsHelper exports_path = File.join(Settings.shared['path'], 'tmp/project_exports') filtered_message = message.strip.gsub(exports_path, "[REPO EXPORT PATH]") - disk_path = Gitlab.config.repositories.storages[project.repository_storage].legacy_disk_path + disk_path = Gitlab::GitalyClient::StorageSettings.allow_disk_access do + Gitlab.config.repositories.storages[project.repository_storage].legacy_disk_path + end + filtered_message.gsub(disk_path.chomp('/'), "[REPOS PATH]") end diff --git a/app/helpers/workhorse_helper.rb b/app/helpers/workhorse_helper.rb index 9f78b80c71d..a82271ce0ee 100644 --- a/app/helpers/workhorse_helper.rb +++ b/app/helpers/workhorse_helper.rb @@ -6,7 +6,7 @@ module WorkhorseHelper headers.store(*Gitlab::Workhorse.send_git_blob(repository, blob)) headers['Content-Disposition'] = 'inline' headers['Content-Type'] = safe_content_type(blob) - head :ok # 'render nothing: true' messes up the Content-Type + render plain: "" end # Send a Git diff through Workhorse diff --git a/app/models/appearance.rb b/app/models/appearance.rb index 67cc84a9140..b770aadef0e 100644 --- a/app/models/appearance.rb +++ b/app/models/appearance.rb @@ -14,6 +14,7 @@ class Appearance < ActiveRecord::Base mount_uploader :logo, AttachmentUploader mount_uploader :header_logo, AttachmentUploader + mount_uploader :favicon, FaviconUploader # Overrides CacheableAttributes.current_without_cache def self.current_without_cache diff --git a/app/models/application_setting/term.rb b/app/models/application_setting/term.rb index e8ce0ccbb71..3b1dfe7e4ef 100644 --- a/app/models/application_setting/term.rb +++ b/app/models/application_setting/term.rb @@ -1,6 +1,7 @@ class ApplicationSetting class Term < ActiveRecord::Base include CacheMarkdownField + has_many :term_agreements validates :terms, presence: true @@ -9,5 +10,10 @@ class ApplicationSetting def self.latest order(:id).last end + + def accepted_by_user?(user) + user.accepted_term_id == id || + term_agreements.accepted.where(user: user).exists? + end end end diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index 75fd55a8f7b..41446946a5e 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -55,12 +55,18 @@ module Ci where('(artifacts_file IS NOT NULL AND artifacts_file <> ?) OR EXISTS (?)', '', Ci::JobArtifact.select(1).where('ci_builds.id = ci_job_artifacts.job_id').archive) end + + scope :without_archived_trace, ->() do + where('NOT EXISTS (?)', Ci::JobArtifact.select(1).where('ci_builds.id = ci_job_artifacts.job_id').trace) + end + scope :with_artifacts_stored_locally, -> { with_artifacts_archive.where(artifacts_file_store: [nil, LegacyArtifactUploader::Store::LOCAL]) } scope :with_artifacts_not_expired, ->() { with_artifacts_archive.where('artifacts_expire_at IS NULL OR artifacts_expire_at > ?', Time.now) } scope :with_expired_artifacts, ->() { with_artifacts_archive.where('artifacts_expire_at < ?', Time.now) } scope :last_month, ->() { where('created_at > ?', Date.today - 1.month) } scope :manual_actions, ->() { where(when: :manual, status: COMPLETED_STATUSES + [:manual]) } scope :ref_protected, -> { where(protected: true) } + scope :with_live_trace, -> { where('EXISTS (?)', Ci::BuildTraceChunk.where('ci_builds.id = ci_build_trace_chunks.build_id').select(1)) } scope :matches_tag_ids, -> (tag_ids) do matcher = ::ActsAsTaggableOn::Tagging @@ -144,6 +150,7 @@ module Ci after_transition any => [:success] do |build| build.run_after_commit do BuildSuccessWorker.perform_async(id) + PagesWorker.perform_async(:deploy, id) if build.pages_generator? end end @@ -183,6 +190,11 @@ module Ci pipeline.manual_actions.where.not(name: name) end + def pages_generator? + Gitlab.config.pages.enabled && + self.name == 'pages' + end + def playable? action? && (manual? || retryable?) end @@ -402,8 +414,6 @@ module Ci build_data = Gitlab::DataBuilder::Build.build(self) project.execute_hooks(build_data.dup, :job_hooks) project.execute_services(build_data.dup, :job_hooks) - PagesService.new(build_data).execute - project.running_or_pending_build_count(force: true) end def browsable_artifacts? @@ -601,6 +611,7 @@ module Ci variables .concat(pipeline.persisted_variables) .append(key: 'CI_JOB_ID', value: id.to_s) + .append(key: 'CI_JOB_URL', value: Gitlab::Routing.url_helpers.project_job_url(project, self)) .append(key: 'CI_JOB_TOKEN', value: token, public: false) .append(key: 'CI_BUILD_ID', value: id.to_s) .append(key: 'CI_BUILD_TOKEN', value: token, public: false) diff --git a/app/models/ci/group.rb b/app/models/ci/group.rb index 87898b086c6..9c1046e8715 100644 --- a/app/models/ci/group.rb +++ b/app/models/ci/group.rb @@ -31,6 +31,14 @@ module Ci end end + def self.fabricate(stage) + stage.statuses.ordered.latest + .sort_by(&:sortable_name).group_by(&:group_name) + .map do |group_name, grouped_statuses| + self.new(stage, name: group_name, jobs: grouped_statuses) + end + end + private def commit_statuses diff --git a/app/models/ci/legacy_stage.rb b/app/models/ci/legacy_stage.rb index 9b536af672b..ce691875e42 100644 --- a/app/models/ci/legacy_stage.rb +++ b/app/models/ci/legacy_stage.rb @@ -16,11 +16,7 @@ module Ci end def groups - @groups ||= statuses.ordered.latest - .sort_by(&:sortable_name).group_by(&:group_name) - .map do |group_name, grouped_statuses| - Ci::Group.new(self, name: group_name, jobs: grouped_statuses) - end + @groups ||= Ci::Group.fabricate(self) end def to_param diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb index 5eb30f4aaa0..f430f18ca9a 100644 --- a/app/models/ci/pipeline.rb +++ b/app/models/ci/pipeline.rb @@ -8,6 +8,7 @@ module Ci include Gitlab::OptimisticLocking include Gitlab::Utils::StrongMemoize include AtomicInternalId + include EnumWithNil belongs_to :project, inverse_of: :pipelines belongs_to :user @@ -18,7 +19,7 @@ module Ci s&.project&.pipelines&.maximum(:iid) || s&.project&.pipelines&.count end - has_many :stages + has_many :stages, -> { order(position: :asc) }, inverse_of: :pipeline has_many :statuses, class_name: 'CommitStatus', foreign_key: :commit_id, inverse_of: :pipeline has_many :builds, foreign_key: :commit_id, inverse_of: :pipeline has_many :trigger_requests, dependent: :destroy, foreign_key: :commit_id # rubocop:disable Cop/ActiveRecordDependent @@ -54,7 +55,7 @@ module Ci after_create :keep_around_commits, unless: :importing? - enum source: { + enum_with_nil source: { unknown: nil, push: 1, web: 2, @@ -64,7 +65,7 @@ module Ci external: 6 } - enum config_source: { + enum_with_nil config_source: { unknown_source: nil, repository_source: 1, auto_devops_source: 2 @@ -254,6 +255,20 @@ module Ci stage unless stage.statuses_count.zero? end + ## + # TODO We do not completely switch to persisted stages because of + # race conditions with setting statuses gitlab-ce#23257. + # + def ordered_stages + return legacy_stages unless complete? + + if Feature.enabled?('ci_pipeline_persisted_stages') + stages + else + legacy_stages + end + end + def legacy_stages # TODO, this needs refactoring, see gitlab-ce#26481. @@ -416,7 +431,7 @@ module Ci def number_of_warnings BatchLoader.for(id).batch(default_value: 0) do |pipeline_ids, loader| - Build.where(commit_id: pipeline_ids) + ::Ci::Build.where(commit_id: pipeline_ids) .latest .failed_but_allowed .group(:commit_id) @@ -508,7 +523,8 @@ module Ci def update_status retry_optimistic_lock(self) do - case latest_builds_status + case latest_builds_status.to_s + when 'created' then nil when 'pending' then enqueue when 'running' then run when 'success' then succeed @@ -516,6 +532,9 @@ module Ci when 'canceled' then cancel when 'skipped' then skip when 'manual' then block + else + raise HasStatus::UnknownStatusError, + "Unknown status `#{latest_builds_status}`" end end end @@ -530,7 +549,10 @@ module Ci def persisted_variables Gitlab::Ci::Variables::Collection.new.tap do |variables| - variables.append(key: 'CI_PIPELINE_ID', value: id.to_s) if persisted? + break variables unless persisted? + + variables.append(key: 'CI_PIPELINE_ID', value: id.to_s) + variables.append(key: 'CI_PIPELINE_URL', value: Gitlab::Routing.url_helpers.project_pipeline_url(project, self)) end end @@ -581,17 +603,6 @@ module Ci @latest_builds_with_artifacts ||= builds.latest.with_artifacts_archive.to_a end - # Rails 5.0 autogenerated question mark enum methods return wrong result if enum value is nil. - # They always return `false`. - # These methods overwrite autogenerated ones to return correct results. - def unknown? - Gitlab.rails5? ? source.nil? : super - end - - def unknown_source? - Gitlab.rails5? ? config_source.nil? : super - end - private def ci_yaml_from_repo diff --git a/app/models/ci/runner.rb b/app/models/ci/runner.rb index 57edd6a4956..8c9aacca8de 100644 --- a/app/models/ci/runner.rb +++ b/app/models/ci/runner.rb @@ -219,10 +219,8 @@ module Ci cache_attributes(values) - if persist_cached_data? - self.assign_attributes(values) - self.save if self.changed? - end + # We save data without validation, it will always change due to `contacted_at` + self.update_columns(values) if persist_cached_data? end def pick_build!(build) diff --git a/app/models/ci/stage.rb b/app/models/ci/stage.rb index 5a1eeb966aa..ea07f37e6c1 100644 --- a/app/models/ci/stage.rb +++ b/app/models/ci/stage.rb @@ -68,16 +68,44 @@ module Ci def update_status retry_optimistic_lock(self) do case statuses.latest.status + when 'created' then nil when 'pending' then enqueue when 'running' then run when 'success' then succeed when 'failed' then drop when 'canceled' then cancel when 'manual' then block - when 'skipped' then skip - else skip + when 'skipped', nil then skip + else + raise HasStatus::UnknownStatusError, + "Unknown status `#{statuses.latest.status}`" end end end + + def groups + @groups ||= Ci::Group.fabricate(self) + end + + def has_warnings? + number_of_warnings.positive? + end + + def number_of_warnings + BatchLoader.for(id).batch(default_value: 0) do |stage_ids, loader| + ::Ci::Build.where(stage_id: stage_ids) + .latest + .failed_but_allowed + .group(:stage_id) + .count + .each { |id, amount| loader.call(id, amount) } + end + end + + def detailed_status(current_user) + Gitlab::Ci::Status::Stage::Factory + .new(self, current_user) + .fabricate! + end end end diff --git a/app/models/commit_status.rb b/app/models/commit_status.rb index a7d05722287..97516079b66 100644 --- a/app/models/commit_status.rb +++ b/app/models/commit_status.rb @@ -3,6 +3,7 @@ class CommitStatus < ActiveRecord::Base include Importable include AfterCommitQueue include Presentable + include EnumWithNil self.table_name = 'ci_builds' @@ -39,7 +40,7 @@ class CommitStatus < ActiveRecord::Base scope :retried_ordered, -> { retried.ordered.includes(project: :namespace) } scope :after_stage, -> (index) { where('stage_idx > ?', index) } - enum failure_reason: { + enum_with_nil failure_reason: { unknown_failure: nil, script_failure: 1, api_failure: 2, @@ -190,11 +191,4 @@ class CommitStatus < ActiveRecord::Base v =~ /\d+/ ? v.to_i : v end end - - # Rails 5.0 autogenerated question mark enum methods return wrong result if enum value is nil. - # They always return `false`. - # This method overwrites the autogenerated one to return correct result. - def unknown_failure? - Gitlab.rails5? ? failure_reason.nil? : super - end end diff --git a/app/models/concerns/avatarable.rb b/app/models/concerns/avatarable.rb index 13246a774e3..095897b08e3 100644 --- a/app/models/concerns/avatarable.rb +++ b/app/models/concerns/avatarable.rb @@ -4,11 +4,14 @@ module Avatarable included do prepend ShadowMethods include ObjectStorage::BackgroundMove + include Gitlab::Utils::StrongMemoize validate :avatar_type, if: ->(user) { user.avatar.present? && user.avatar_changed? } validates :avatar, file_size: { maximum: 200.kilobytes.to_i } mount_uploader :avatar, AvatarUploader + + after_initialize :add_avatar_to_batch end module ShadowMethods @@ -18,6 +21,17 @@ module Avatarable avatar_path(only_path: args.fetch(:only_path, true)) || super end + + def retrieve_upload(identifier, paths) + upload = retrieve_upload_from_batch(identifier) + + # This fallback is needed when deleting an upload, because we may have + # already been removed from the DB. We have to check an explicit `#nil?` + # because it's a BatchLoader instance. + upload = super if upload.nil? + + upload + end end def avatar_type @@ -52,4 +66,37 @@ module Avatarable url_base + avatar.local_url end + + # Path that is persisted in the tracking Upload model. Used to fetch the + # upload from the model. + def upload_paths(identifier) + avatar_mounter.blank_uploader.store_dirs.map { |store, path| File.join(path, identifier) } + end + + private + + def retrieve_upload_from_batch(identifier) + BatchLoader.for(identifier: identifier, model: self).batch(key: self.class) do |upload_params, loader, args| + model_class = args[:key] + paths = upload_params.flat_map do |params| + params[:model].upload_paths(params[:identifier]) + end + + Upload.where(uploader: AvatarUploader, path: paths).find_each do |upload| + model = model_class.instantiate('id' => upload.model_id) + + loader.call({ model: model, identifier: File.basename(upload.path) }, upload) + end + end + end + + def add_avatar_to_batch + return unless avatar_mounter + + avatar_mounter.read_identifiers.each { |identifier| retrieve_upload_from_batch(identifier) } + end + + def avatar_mounter + strong_memoize(:avatar_mounter) { _mounter(:avatar) } + end end diff --git a/app/models/concerns/cache_markdown_field.rb b/app/models/concerns/cache_markdown_field.rb index db8cf322ef7..9f6358cecbe 100644 --- a/app/models/concerns/cache_markdown_field.rb +++ b/app/models/concerns/cache_markdown_field.rb @@ -114,7 +114,7 @@ module CacheMarkdownField end def latest_cached_markdown_version - return CacheMarkdownField::CACHE_REDCARPET_VERSION unless cached_markdown_version + return CacheMarkdownField::CACHE_COMMONMARK_VERSION unless cached_markdown_version if cached_markdown_version < CacheMarkdownField::CACHE_COMMONMARK_VERSION_START CacheMarkdownField::CACHE_REDCARPET_VERSION diff --git a/app/models/concerns/enum_with_nil.rb b/app/models/concerns/enum_with_nil.rb new file mode 100644 index 00000000000..6b37903da20 --- /dev/null +++ b/app/models/concerns/enum_with_nil.rb @@ -0,0 +1,33 @@ +module EnumWithNil + extend ActiveSupport::Concern + + included do + def self.enum_with_nil(definitions) + # use original `enum` to auto-define all methods + enum(definitions) + + # override auto-defined methods only for the + # key which uses nil value + definitions.each do |name, values| + next unless key_with_nil = values.key(nil) + + # E.g. for enum_with_nil failure_reason: { unknown_failure: nil } + # this overrides auto-generated method `unknown_failure?` + define_method("#{key_with_nil}?") do + Gitlab.rails5? ? self[name].nil? : super() + end + + # E.g. for enum_with_nil failure_reason: { unknown_failure: nil } + # this overrides auto-generated method `failure_reason` + define_method(name) do + orig = super() + + return orig unless Gitlab.rails5? + return orig unless orig.nil? + + self.class.public_send(name.to_s.pluralize).key(nil) # rubocop:disable GitlabSecurity/PublicSend + end + end + end + end +end diff --git a/app/models/concerns/has_status.rb b/app/models/concerns/has_status.rb index 7c3ed96bc28..72c236a0fc7 100644 --- a/app/models/concerns/has_status.rb +++ b/app/models/concerns/has_status.rb @@ -11,6 +11,8 @@ module HasStatus STATUSES_ENUM = { created: 0, pending: 1, running: 2, success: 3, failed: 4, canceled: 5, skipped: 6, manual: 7 }.freeze + UnknownStatusError = Class.new(StandardError) + class_methods do def status_sql scope_relevant = respond_to?(:exclude_ignored) ? exclude_ignored : all diff --git a/app/models/concerns/protected_ref_access.rb b/app/models/concerns/protected_ref_access.rb index bfda5b1678b..e3a7f2d5498 100644 --- a/app/models/concerns/protected_ref_access.rb +++ b/app/models/concerns/protected_ref_access.rb @@ -8,8 +8,8 @@ module ProtectedRefAccess ].freeze HUMAN_ACCESS_LEVELS = { - Gitlab::Access::MASTER => "Masters".freeze, - Gitlab::Access::DEVELOPER => "Developers + Masters".freeze, + Gitlab::Access::MASTER => "Maintainers".freeze, + Gitlab::Access::DEVELOPER => "Developers + Maintainers".freeze, Gitlab::Access::NO_ACCESS => "No one".freeze }.freeze diff --git a/app/models/concerns/with_uploads.rb b/app/models/concerns/with_uploads.rb index e7cfffb775b..4245d083a49 100644 --- a/app/models/concerns/with_uploads.rb +++ b/app/models/concerns/with_uploads.rb @@ -36,4 +36,8 @@ module WithUploads upload.destroy end end + + def retrieve_upload(_identifier, paths) + uploads.find_by(path: paths) + end end diff --git a/app/models/group.rb b/app/models/group.rb index 8fb77a7869d..9c171de7fc3 100644 --- a/app/models/group.rb +++ b/app/models/group.rb @@ -11,6 +11,7 @@ class Group < Namespace include GroupDescendant include TokenAuthenticatable include WithUploads + include Gitlab::Utils::StrongMemoize has_many :group_members, -> { where(requested_at: nil) }, dependent: :destroy, as: :source # rubocop:disable Cop/ActiveRecordDependent alias_method :members, :group_members @@ -26,7 +27,11 @@ class Group < Namespace has_many :milestones has_many :project_group_links, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent has_many :shared_projects, through: :project_group_links, source: :project + + # Overridden on another method + # Left here just to be dependent: :destroy has_many :notification_settings, dependent: :destroy, as: :source # rubocop:disable Cop/ActiveRecordDependent + has_many :labels, class_name: 'GroupLabel' has_many :variables, class_name: 'Ci::GroupVariable' has_many :custom_attributes, class_name: 'GroupCustomAttribute' @@ -88,6 +93,15 @@ class Group < Namespace end end + # Overrides notification_settings has_many association + # This allows to apply notification settings from parent groups + # to child groups and projects. + def notification_settings + source_type = self.class.base_class.name + + NotificationSetting.where(source_type: source_type, source_id: self_and_ancestors_ids) + end + def to_reference(_from = nil, full: nil) "#{self.class.reference_prefix}#{full_path}" end @@ -141,13 +155,14 @@ class Group < Namespace ) end - def add_user(user, access_level, current_user: nil, expires_at: nil) + def add_user(user, access_level, current_user: nil, expires_at: nil, ldap: false) GroupMember.add_user( self, user, access_level, current_user: current_user, - expires_at: expires_at + expires_at: expires_at, + ldap: ldap ) end @@ -195,6 +210,10 @@ class Group < Namespace owners.include?(user) && owners.size == 1 end + def ldap_synced? + false + end + def post_create_hook Gitlab::AppLogger.info("Group \"#{name}\" was created") @@ -220,6 +239,12 @@ class Group < Namespace members_with_parents.pluck(:user_id) end + def self_and_ancestors_ids + strong_memoize(:self_and_ancestors_ids) do + self_and_ancestors.pluck(:id) + end + end + def members_with_parents # Avoids an unnecessary SELECT when the group has no parents source_ids = diff --git a/app/models/label.rb b/app/models/label.rb index de7f1d56c64..1cf04976602 100644 --- a/app/models/label.rb +++ b/app/models/label.rb @@ -137,6 +137,10 @@ class Label < ActiveRecord::Base priority.try(:priority) end + def priority? + priorities.present? + end + def template? template end diff --git a/app/models/list.rb b/app/models/list.rb index 5daf35ef845..4edcfa78835 100644 --- a/app/models/list.rb +++ b/app/models/list.rb @@ -2,17 +2,27 @@ class List < ActiveRecord::Base belongs_to :board belongs_to :label - enum list_type: { backlog: 0, label: 1, closed: 2 } + enum list_type: { backlog: 0, label: 1, closed: 2, assignee: 3 } validates :board, :list_type, presence: true validates :label, :position, presence: true, if: :label? validates :label_id, uniqueness: { scope: :board_id }, if: :label? - validates :position, numericality: { only_integer: true, greater_than_or_equal_to: 0 }, if: :label? + validates :position, numericality: { only_integer: true, greater_than_or_equal_to: 0 }, if: :movable? before_destroy :can_be_destroyed - scope :destroyable, -> { where(list_type: list_types[:label]) } - scope :movable, -> { where(list_type: list_types[:label]) } + scope :destroyable, -> { where(list_type: list_types.slice(*destroyable_types).values) } + scope :movable, -> { where(list_type: list_types.slice(*movable_types).values) } + + class << self + def destroyable_types + [:label] + end + + def movable_types + [:label] + end + end def destroyable? label? diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index 4c1628d2bdb..324065c1162 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -1125,21 +1125,24 @@ class MergeRequest < ActiveRecord::Base project.merge_requests.merged.where(author_id: author_id).empty? end - def allow_maintainer_to_push - maintainer_push_possible? && super + # TODO: remove once production database rename completes + alias_attribute :allow_collaboration, :allow_maintainer_to_push + + def allow_collaboration + collaborative_push_possible? && allow_maintainer_to_push end - alias_method :allow_maintainer_to_push?, :allow_maintainer_to_push + alias_method :allow_collaboration?, :allow_collaboration - def maintainer_push_possible? + def collaborative_push_possible? source_project.present? && for_fork? && target_project.visibility_level > Gitlab::VisibilityLevel::PRIVATE && source_project.visibility_level > Gitlab::VisibilityLevel::PRIVATE && !ProtectedBranch.protected?(source_project, source_branch) end - def can_allow_maintainer_to_push?(user) - maintainer_push_possible? && + def can_allow_collaboration?(user) + collaborative_push_possible? && Ability.allowed?(user, :push_code, source_project) end diff --git a/app/models/note.rb b/app/models/note.rb index 02f7a9b1e4f..41c04ae0571 100644 --- a/app/models/note.rb +++ b/app/models/note.rb @@ -435,6 +435,10 @@ class Note < ActiveRecord::Base super.merge(noteable: noteable) end + def retrieve_upload(_identifier, paths) + Upload.find_by(model: self, path: paths) + end + private def keep_around_commit diff --git a/app/models/notification_recipient.rb b/app/models/notification_recipient.rb index 2c3580bbdc6..1a03dd9df56 100644 --- a/app/models/notification_recipient.rb +++ b/app/models/notification_recipient.rb @@ -1,4 +1,6 @@ class NotificationRecipient + include Gitlab::Utils::StrongMemoize + attr_reader :user, :type, :reason def initialize(user, type, **opts) unless NotificationSetting.levels.key?(type) || type == :subscription @@ -64,7 +66,7 @@ class NotificationRecipient return false unless @target return false unless @target.respond_to?(:subscriptions) - subscription = @target.subscriptions.find_by_user_id(@user.id) + subscription = @target.subscriptions.find { |subscription| subscription.user_id == @user.id } subscription && !subscription.subscribed end @@ -142,10 +144,33 @@ class NotificationRecipient return project_setting unless project_setting.nil? || project_setting.global? - group_setting = @group && user.notification_settings_for(@group) + group_setting = closest_non_global_group_notification_settting - return group_setting unless group_setting.nil? || group_setting.global? + return group_setting unless group_setting.nil? user.global_notification_setting end + + # Returns the notificaton_setting of the lowest group in hierarchy with non global level + def closest_non_global_group_notification_settting + return unless @group + return if indexed_group_notification_settings.empty? + + notification_setting = nil + + @group.self_and_ancestors_ids.each do |id| + notification_setting = indexed_group_notification_settings[id] + break if notification_setting + end + + notification_setting + end + + def indexed_group_notification_settings + strong_memoize(:indexed_group_notification_settings) do + @group.notification_settings.where(user_id: user.id) + .where.not(level: NotificationSetting.levels[:global]) + .index_by(&:source_id) + end + end end diff --git a/app/models/personal_snippet.rb b/app/models/personal_snippet.rb index 82c1c4de3a0..355624fd552 100644 --- a/app/models/personal_snippet.rb +++ b/app/models/personal_snippet.rb @@ -1,2 +1,3 @@ class PersonalSnippet < Snippet + include WithUploads end diff --git a/app/models/project.rb b/app/models/project.rb index 32298fc7f5c..0d777515536 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -25,6 +25,7 @@ class Project < ActiveRecord::Base include FastDestroyAll::Helpers include WithUploads include BatchDestroyDependentAssociations + extend Gitlab::Cache::RequestCache extend Gitlab::ConfigHelper @@ -68,7 +69,7 @@ class Project < ActiveRecord::Base add_authentication_token_field :runners_token - before_validation :mark_remote_mirrors_for_removal, if: -> { ActiveRecord::Base.connection.table_exists?(:remote_mirrors) } + before_validation :mark_remote_mirrors_for_removal, if: -> { RemoteMirror.table_exists? } before_save :ensure_runners_token @@ -228,6 +229,7 @@ class Project < ActiveRecord::Base has_many :commit_statuses has_many :pipelines, class_name: 'Ci::Pipeline', inverse_of: :project + has_many :stages, class_name: 'Ci::Stage', inverse_of: :project # Ci::Build objects store data on the file system such as artifact files and # build traces. Currently there's no efficient way of removing this data in @@ -291,6 +293,7 @@ class Project < ActiveRecord::Base validates :name, uniqueness: { scope: :namespace_id } validates :import_url, url: { protocols: %w(http https ssh git), allow_localhost: false, + enforce_user: true, ports: VALID_IMPORT_PORTS }, if: [:external_import?, :import_url_changed?] validates :star_count, numericality: { greater_than_or_equal_to: 0 } validate :check_limit, on: :create @@ -674,6 +677,12 @@ class Project < ActiveRecord::Base end end + def human_import_status_name + ensure_import_state + + import_state.human_status_name + end + def import_schedule ensure_import_state(force: true) @@ -1425,8 +1434,14 @@ class Project < ActiveRecord::Base Ci::Runner.from("(#{union.to_sql}) ci_runners") end + def active_runners + strong_memoize(:active_runners) do + all_runners.active + end + end + def any_runners?(&block) - all_runners.active.any?(&block) + active_runners.any?(&block) end def valid_runners_token?(token) @@ -1602,6 +1617,7 @@ class Project < ActiveRecord::Base def after_import repository.after_import + wiki.repository.after_import import_finish remove_import_jid update_project_counter_caches @@ -1649,12 +1665,6 @@ class Project < ActiveRecord::Base import_state.update_column(:jid, nil) end - def running_or_pending_build_count(force: false) - Rails.cache.fetch(['projects', id, 'running_or_pending_build_count'], force: force) do - builds.running_or_pending.count(:all) - end - end - # Lazy loading of the `pipeline_status` attribute def pipeline_status @pipeline_status ||= Gitlab::Cache::Ci::ProjectPipelineStatus.load_for_project(self) @@ -1974,18 +1984,18 @@ class Project < ActiveRecord::Base .limit(1) .select(1) source_of_merge_requests.opened - .where(allow_maintainer_to_push: true) + .where(allow_collaboration: true) .where('EXISTS (?)', developer_access_exists) end - def branch_allows_maintainer_push?(user, branch_name) + def branch_allows_collaboration?(user, branch_name) return false unless user cache_key = "user:#{user.id}:#{branch_name}:branch_allows_push" - memoized_results = strong_memoize(:branch_allows_maintainer_push) do + memoized_results = strong_memoize(:branch_allows_collaboration) do Hash.new do |result, cache_key| - result[cache_key] = fetch_branch_allows_maintainer_push?(user, branch_name) + result[cache_key] = fetch_branch_allows_collaboration?(user, branch_name) end end @@ -2004,6 +2014,11 @@ class Project < ActiveRecord::Base @gitlab_deploy_token ||= deploy_tokens.gitlab_deploy_token end + def any_lfs_file_locks? + lfs_file_locks.any? + end + request_cache(:any_lfs_file_locks?) { self.id } + private def storage @@ -2127,18 +2142,22 @@ class Project < ActiveRecord::Base raise ex end - def fetch_branch_allows_maintainer_push?(user, branch_name) + def fetch_branch_allows_collaboration?(user, branch_name) check_access = -> do next false if empty_repo? - merge_request = source_of_merge_requests.opened - .where(allow_maintainer_to_push: true) - .find_by(source_branch: branch_name) - merge_request&.can_be_merged_by?(user) + merge_requests = source_of_merge_requests.opened + .where(allow_collaboration: true) + + if branch_name + merge_requests.find_by(source_branch: branch_name)&.can_be_merged_by?(user) + else + merge_requests.any? { |merge_request| merge_request.can_be_merged_by?(user) } + end end if RequestStore.active? - RequestStore.fetch("project-#{id}:branch-#{branch_name}:user-#{user.id}:branch_allows_maintainer_push") do + RequestStore.fetch("project-#{id}:branch-#{branch_name}:user-#{user.id}:branch_allows_collaboration") do check_access.call end else diff --git a/app/models/project_auto_devops.rb b/app/models/project_auto_devops.rb index ed6c1eddbc1..d7d6aaceb27 100644 --- a/app/models/project_auto_devops.rb +++ b/app/models/project_auto_devops.rb @@ -1,11 +1,18 @@ class ProjectAutoDevops < ActiveRecord::Base belongs_to :project + enum deploy_strategy: { + continuous: 0, + manual: 1 + } + scope :enabled, -> { where(enabled: true) } scope :disabled, -> { where(enabled: false) } validates :domain, allow_blank: true, hostname: { allow_numeric_hostname: true } + after_save :create_gitlab_deploy_token, if: :needs_to_create_deploy_token? + def instance_domain Gitlab::CurrentSettings.auto_devops_domain end @@ -20,6 +27,30 @@ class ProjectAutoDevops < ActiveRecord::Base variables.append(key: 'AUTO_DEVOPS_DOMAIN', value: domain.presence || instance_domain) end + + if manual? + variables.append(key: 'STAGING_ENABLED', value: 1) + variables.append(key: 'INCREMENTAL_ROLLOUT_ENABLED', value: 1) + end end end + + private + + def create_gitlab_deploy_token + project.deploy_tokens.create!( + name: DeployToken::GITLAB_DEPLOY_TOKEN_NAME, + read_registry: true + ) + end + + def needs_to_create_deploy_token? + auto_devops_enabled? && + !project.public? && + !project.deploy_tokens.find_by(name: DeployToken::GITLAB_DEPLOY_TOKEN_NAME).present? + end + + def auto_devops_enabled? + Gitlab::CurrentSettings.auto_devops_enabled? || enabled? + end end diff --git a/app/models/project_services/chat_message/base_message.rb b/app/models/project_services/chat_message/base_message.rb index 22a65b5145e..f710fa85b5d 100644 --- a/app/models/project_services/chat_message/base_message.rb +++ b/app/models/project_services/chat_message/base_message.rb @@ -26,13 +26,18 @@ module ChatMessage end end - def pretext + def summary return message if markdown format(message) end + def pretext + summary + end + def fallback + format(message) end def attachments diff --git a/app/models/project_services/chat_message/pipeline_message.rb b/app/models/project_services/chat_message/pipeline_message.rb index 2135122278a..96fd23aede3 100644 --- a/app/models/project_services/chat_message/pipeline_message.rb +++ b/app/models/project_services/chat_message/pipeline_message.rb @@ -23,10 +23,6 @@ module ChatMessage '' end - def fallback - format(message) - end - def attachments return message if markdown diff --git a/app/models/project_services/chat_notification_service.rb b/app/models/project_services/chat_notification_service.rb index ae0debbd3ac..a60b4c7fd0d 100644 --- a/app/models/project_services/chat_notification_service.rb +++ b/app/models/project_services/chat_notification_service.rb @@ -155,6 +155,7 @@ class ChatNotificationService < Service end def notify_for_ref?(data) + return true if data[:object_kind] == 'tag_push' return true if data.dig(:object_attributes, :tag) return true unless notify_only_default_branch? diff --git a/app/models/project_services/gemnasium_service.rb b/app/models/project_services/gemnasium_service.rb index 84248f9590b..8a6b0ed1a5f 100644 --- a/app/models/project_services/gemnasium_service.rb +++ b/app/models/project_services/gemnasium_service.rb @@ -43,13 +43,18 @@ class GemnasiumService < Service def execute(data) return unless supported_events.include?(data[:object_kind]) + # Gitaly: this class will be removed https://gitlab.com/gitlab-org/gitlab-ee/issues/6010 + repo_path = Gitlab::GitalyClient::StorageSettings.allow_disk_access do + project.repository.path_to_repo + end + Gemnasium::GitlabService.execute( ref: data[:ref], before: data[:before], after: data[:after], token: token, api_key: api_key, - repo: project.repository.path_to_repo # Gitaly: fixed by https://gitlab.com/gitlab-org/security-products/gemnasium-migration/issues/9 + repo: repo_path ) end end diff --git a/app/models/project_services/jira_service.rb b/app/models/project_services/jira_service.rb index eb3261c902f..412d62388f0 100644 --- a/app/models/project_services/jira_service.rb +++ b/app/models/project_services/jira_service.rb @@ -265,7 +265,7 @@ class JiraService < IssueTrackerService title: title, status: status, icon: { - title: 'GitLab', url16x16: asset_url('favicon.ico', host: gitlab_config.url) + title: 'GitLab', url16x16: asset_url(Gitlab::Favicon.main, host: gitlab_config.url) } } } diff --git a/app/models/project_services/microsoft_teams_service.rb b/app/models/project_services/microsoft_teams_service.rb index 2facff53e26..99500caec0e 100644 --- a/app/models/project_services/microsoft_teams_service.rb +++ b/app/models/project_services/microsoft_teams_service.rb @@ -44,7 +44,7 @@ class MicrosoftTeamsService < ChatNotificationService def notify(message, opts) MicrosoftTeams::Notifier.new(webhook).ping( title: message.project_name, - pretext: message.pretext, + summary: message.summary, activity: message.activity, attachments: message.attachments ) diff --git a/app/models/protected_branch.rb b/app/models/protected_branch.rb index cb361a66591..dff99cfca35 100644 --- a/app/models/protected_branch.rb +++ b/app/models/protected_branch.rb @@ -5,7 +5,7 @@ class ProtectedBranch < ActiveRecord::Base protected_ref_access_levels :merge, :push def self.protected_ref_accessible_to?(ref, user, project:, action:, protected_refs: nil) - # Masters, owners and admins are allowed to create the default branch + # Maintainers, owners and admins are allowed to create the default branch if default_branch_protected? && project.empty_repo? return true if user.admin? || project.team.max_member_access(user.id) > Gitlab::Access::DEVELOPER end diff --git a/app/models/remote_mirror.rb b/app/models/remote_mirror.rb index 5cd222e18a4..c4b5dd2dc96 100644 --- a/app/models/remote_mirror.rb +++ b/app/models/remote_mirror.rb @@ -16,7 +16,7 @@ class RemoteMirror < ActiveRecord::Base belongs_to :project, inverse_of: :remote_mirrors - validates :url, presence: true, url: { protocols: %w(ssh git http https), allow_blank: true } + validates :url, presence: true, url: { protocols: %w(ssh git http https), allow_blank: true, enforce_user: true } before_save :set_new_remote_name, if: :mirror_url_changed? diff --git a/app/models/repository.rb b/app/models/repository.rb index 82cf47ba04e..e4202505634 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -270,6 +270,16 @@ class Repository end end + def archive_metadata(ref, storage_path, format = "tar.gz", append_sha:) + raw_repository.archive_metadata( + ref, + storage_path, + project.path, + format, + append_sha: append_sha + ) + end + def expire_tags_cache expire_method_caches(%i(tag_names tag_count)) @tags = nil @@ -946,6 +956,10 @@ class Repository blob_data_at(sha, path) end + def lfsconfig_for(sha) + blob_data_at(sha, '.lfsconfig') + end + def fetch_ref(source_repository, source_ref:, target_ref:) raw_repository.fetch_ref(source_repository.raw_repository, source_ref: source_ref, target_ref: target_ref) end diff --git a/app/models/term_agreement.rb b/app/models/term_agreement.rb index 8458a231bbd..c317bd0c90b 100644 --- a/app/models/term_agreement.rb +++ b/app/models/term_agreement.rb @@ -2,5 +2,7 @@ class TermAgreement < ActiveRecord::Base belongs_to :term, class_name: 'ApplicationSetting::Term' belongs_to :user + scope :accepted, -> { where(accepted: true) } + validates :user, :term, presence: true end diff --git a/app/models/timelog.rb b/app/models/timelog.rb index f4c5c581a11..659146f43e4 100644 --- a/app/models/timelog.rb +++ b/app/models/timelog.rb @@ -19,4 +19,9 @@ class Timelog < ActiveRecord::Base errors.add(:base, 'Issue or Merge Request ID is required') end end + + # Rails5 defaults to :touch_later, overwrite for normal touch + def belongs_to_touch_method + :touch + end end diff --git a/app/models/user.rb b/app/models/user.rb index e219ab800ad..8e0dc91b2a7 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -1038,7 +1038,10 @@ class User < ActiveRecord::Base def notification_settings_for(source) if notification_settings.loaded? - notification_settings.find { |notification| notification.source == source } + notification_settings.find do |notification| + notification.source_type == source.class.base_class.name && + notification.source_id == source.id + end else notification_settings.find_or_initialize_by(source: source) end diff --git a/app/policies/ci/build_policy.rb b/app/policies/ci/build_policy.rb index 8b65758f3e8..1c0cc7425ec 100644 --- a/app/policies/ci/build_policy.rb +++ b/app/policies/ci/build_policy.rb @@ -14,8 +14,8 @@ module Ci @subject.triggered_by?(@user) end - condition(:branch_allows_maintainer_push) do - @subject.project.branch_allows_maintainer_push?(@user, @subject.ref) + condition(:branch_allows_collaboration) do + @subject.project.branch_allows_collaboration?(@user, @subject.ref) end rule { protected_ref }.policy do @@ -25,7 +25,7 @@ module Ci rule { can?(:admin_build) | (can?(:update_build) & owner_of_job) }.enable :erase_build - rule { can?(:public_access) & branch_allows_maintainer_push }.policy do + rule { can?(:public_access) & branch_allows_collaboration }.policy do enable :update_build enable :update_commit_status end diff --git a/app/policies/ci/pipeline_policy.rb b/app/policies/ci/pipeline_policy.rb index 540e4235299..b81329d0625 100644 --- a/app/policies/ci/pipeline_policy.rb +++ b/app/policies/ci/pipeline_policy.rb @@ -4,13 +4,13 @@ module Ci condition(:protected_ref) { ref_protected?(@user, @subject.project, @subject.tag?, @subject.ref) } - condition(:branch_allows_maintainer_push) do - @subject.project.branch_allows_maintainer_push?(@user, @subject.ref) + condition(:branch_allows_collaboration) do + @subject.project.branch_allows_collaboration?(@user, @subject.ref) end rule { protected_ref }.prevent :update_pipeline - rule { can?(:public_access) & branch_allows_maintainer_push }.policy do + rule { can?(:public_access) & branch_allows_collaboration }.policy do enable :update_pipeline end diff --git a/app/policies/project_policy.rb b/app/policies/project_policy.rb index 99a0d7118f2..8ea5435d740 100644 --- a/app/policies/project_policy.rb +++ b/app/policies/project_policy.rb @@ -45,7 +45,7 @@ class ProjectPolicy < BasePolicy desc "User has developer access" condition(:developer) { team_access_level >= Gitlab::Access::DEVELOPER } - desc "User has master access" + desc "User has maintainer access" condition(:master) { team_access_level >= Gitlab::Access::MASTER } desc "Project is public" diff --git a/app/presenters/merge_request_presenter.rb b/app/presenters/merge_request_presenter.rb index ad839d9840a..8d466c33510 100644 --- a/app/presenters/merge_request_presenter.rb +++ b/app/presenters/merge_request_presenter.rb @@ -179,6 +179,25 @@ class MergeRequestPresenter < Gitlab::View::Presenter::Delegated .can_push_to_branch?(source_branch) end + def mergeable_discussions_state + # This avoids calling MergeRequest#mergeable_discussions_state without + # considering the state of the MR first. If a MR isn't mergeable, we can + # safely short-circuit it. + if merge_request.mergeable_state?(skip_ci_check: true, skip_discussions_check: true) + merge_request.mergeable_discussions_state? + else + false + end + end + + def web_url + Gitlab::UrlBuilder.build(merge_request) + end + + def subscribed? + merge_request.subscribed?(current_user, merge_request.target_project) + end + private def cached_can_be_reverted? diff --git a/app/serializers/merge_request_basic_entity.rb b/app/serializers/merge_request_basic_entity.rb index e4aec977f01..1c06691026d 100644 --- a/app/serializers/merge_request_basic_entity.rb +++ b/app/serializers/merge_request_basic_entity.rb @@ -5,4 +5,8 @@ class MergeRequestBasicEntity < IssuableSidebarEntity expose :state expose :source_branch_exists?, as: :source_branch_exists expose :rebase_in_progress?, as: :rebase_in_progress + expose :milestone, using: API::Entities::Milestone + expose :labels, using: LabelEntity + expose :assignee, using: API::Entities::UserBasic + expose :task_status, :task_status_short end diff --git a/app/serializers/merge_request_widget_entity.rb b/app/serializers/merge_request_widget_entity.rb index 141070aef45..8260c6c7b84 100644 --- a/app/serializers/merge_request_widget_entity.rb +++ b/app/serializers/merge_request_widget_entity.rb @@ -13,7 +13,7 @@ class MergeRequestWidgetEntity < IssuableEntity expose :squash expose :target_branch expose :target_project_id - expose :allow_maintainer_to_push + expose :allow_collaboration expose :should_be_rebased?, as: :should_be_rebased expose :ff_only_enabled do |merge_request| diff --git a/app/serializers/pipeline_details_entity.rb b/app/serializers/pipeline_details_entity.rb index 130968a44c1..8ba9cac53c4 100644 --- a/app/serializers/pipeline_details_entity.rb +++ b/app/serializers/pipeline_details_entity.rb @@ -1,6 +1,6 @@ class PipelineDetailsEntity < PipelineEntity expose :details do - expose :legacy_stages, as: :stages, using: StageEntity + expose :ordered_stages, as: :stages, using: StageEntity expose :artifacts, using: BuildArtifactEntity expose :manual_actions, using: BuildActionEntity end diff --git a/app/serializers/pipeline_serializer.rb b/app/serializers/pipeline_serializer.rb index 7181f8a6b04..17a022539bb 100644 --- a/app/serializers/pipeline_serializer.rb +++ b/app/serializers/pipeline_serializer.rb @@ -1,14 +1,11 @@ class PipelineSerializer < BaseSerializer include WithPagination - - InvalidResourceError = Class.new(StandardError) - entity PipelineDetailsEntity def represent(resource, opts = {}) if resource.is_a?(ActiveRecord::Relation) - resource = resource.preload([ + :stages, :retryable_builds, :cancelable_statuses, :trigger_requests, @@ -20,10 +17,14 @@ class PipelineSerializer < BaseSerializer end if paginated? - super(@paginator.paginate(resource), opts) - else - super(resource, opts) + resource = paginator.paginate(resource) end + + if opts.delete(:preload) + resource = Gitlab::Ci::Pipeline::Preloader.preload!(resource) + end + + super(resource, opts) end def represent_status(resource) @@ -36,7 +37,7 @@ class PipelineSerializer < BaseSerializer def represent_stages(resource) return {} unless resource.present? - data = represent(resource, { only: [{ details: [:stages] }] }) + data = represent(resource, { only: [{ details: [:stages] }], preload: true }) data.dig(:details, :stages) || [] end end diff --git a/app/serializers/status_entity.rb b/app/serializers/status_entity.rb index 8e8bda2f9df..47df7f9dcf9 100644 --- a/app/serializers/status_entity.rb +++ b/app/serializers/status_entity.rb @@ -7,16 +7,7 @@ class StatusEntity < Grape::Entity expose :details_path expose :favicon do |status| - dir = - if Gitlab::Utils.to_boolean(ENV['CANARY']) - File.join('ci_favicons', 'canary') - elsif Rails.env.development? - File.join('ci_favicons', 'dev') - else - 'ci_favicons' - end - - ActionController::Base.helpers.image_path(File.join(dir, "#{status.favicon}.ico")) + Gitlab::Favicon.status_overlay(status.favicon) end expose :action, if: -> (status, _) { status.has_action? } do diff --git a/app/services/application_settings/update_service.rb b/app/services/application_settings/update_service.rb index e70445cfb67..7bcb8f49d0d 100644 --- a/app/services/application_settings/update_service.rb +++ b/app/services/application_settings/update_service.rb @@ -1,5 +1,7 @@ module ApplicationSettings class UpdateService < ApplicationSettings::BaseService + attr_reader :params, :application_setting + def execute update_terms(@params.delete(:terms)) diff --git a/app/services/applications/create_service.rb b/app/services/applications/create_service.rb index 35d45f25a71..94a434b95dd 100644 --- a/app/services/applications/create_service.rb +++ b/app/services/applications/create_service.rb @@ -2,11 +2,10 @@ module Applications class CreateService def initialize(current_user, params) @current_user = current_user - @params = params - @ip_address = @params.delete(:ip_address) + @params = params.except(:ip_address) end - def execute(request = nil) + def execute(request) Doorkeeper::Application.create(@params) end end diff --git a/app/services/base_service.rb b/app/services/base_service.rb index 6883ba36c71..3519b7c5e7d 100644 --- a/app/services/base_service.rb +++ b/app/services/base_service.rb @@ -3,7 +3,7 @@ class BaseService attr_accessor :project, :current_user, :params - def initialize(project, user, params = {}) + def initialize(project, user = nil, params = {}) @project, @current_user, @params = project, user, params.dup end diff --git a/app/services/boards/issues/list_service.rb b/app/services/boards/issues/list_service.rb index 5a961ac89e4..b1dbe73cdf7 100644 --- a/app/services/boards/issues/list_service.rb +++ b/app/services/boards/issues/list_service.rb @@ -3,13 +3,18 @@ module Boards class ListService < Boards::BaseService def execute issues = IssuesFinder.new(current_user, filter_params).execute - issues = without_board_labels(issues) unless movable_list? || closed_list? - issues = with_list_label(issues) if movable_list? + issues = filter(issues) issues.order_by_position_and_priority end private + def filter(issues) + issues = without_board_labels(issues) unless list&.movable? || list&.closed? + issues = with_list_label(issues) if list&.label? + issues + end + def board @board ||= parent.boards.find(params[:board_id]) end @@ -20,18 +25,6 @@ module Boards @list = board.lists.find(params[:id]) if params.key?(:id) end - def movable_list? - return @movable_list if defined?(@movable_list) - - @movable_list = list.present? && list.movable? - end - - def closed_list? - return @closed_list if defined?(@closed_list) - - @closed_list = list.present? && list.closed? - end - def filter_params set_parent set_state diff --git a/app/services/boards/issues/move_service.rb b/app/services/boards/issues/move_service.rb index 3ceab209f3f..ee3112c7571 100644 --- a/app/services/boards/issues/move_service.rb +++ b/app/services/boards/issues/move_service.rb @@ -3,7 +3,7 @@ module Boards class MoveService < Boards::BaseService def execute(issue) return false unless can?(current_user, :update_issue, issue) - return false if issue_params.empty? + return false if issue_params(issue).empty? update(issue) end @@ -28,10 +28,10 @@ module Boards end def update(issue) - ::Issues::UpdateService.new(issue.project, current_user, issue_params).execute(issue) + ::Issues::UpdateService.new(issue.project, current_user, issue_params(issue)).execute(issue) end - def issue_params + def issue_params(issue) attrs = {} if move_between_lists? diff --git a/app/services/boards/lists/create_service.rb b/app/services/boards/lists/create_service.rb index 02f1c709374..6fd9885d4f3 100644 --- a/app/services/boards/lists/create_service.rb +++ b/app/services/boards/lists/create_service.rb @@ -1,16 +1,28 @@ module Boards module Lists class CreateService < Boards::BaseService + include Gitlab::Utils::StrongMemoize + def execute(board) List.transaction do - label = available_labels_for(board).find(params[:label_id]) + target = target(board) position = next_position(board) - create_list(board, label, position) + create_list(board, type, target, position) end end private + def type + :label + end + + def target(board) + strong_memoize(:target) do + available_labels_for(board).find(params[:label_id]) + end + end + def available_labels_for(board) options = { include_ancestor_groups: true } @@ -28,8 +40,8 @@ module Boards max_position.nil? ? 0 : max_position.succ end - def create_list(board, label, position) - board.lists.create(label: label, list_type: :label, position: position) + def create_list(board, type, target, position) + board.lists.create(type => target, list_type: type, position: position) end end end diff --git a/app/services/ci/register_job_service.rb b/app/services/ci/register_job_service.rb index 925775aea0b..9bdbb2c0d99 100644 --- a/app/services/ci/register_job_service.rb +++ b/app/services/ci/register_job_service.rb @@ -25,14 +25,12 @@ module Ci valid = true - if Feature.enabled?('ci_job_request_with_tags_matcher') - # pick builds that does not have other tags than runner's one - builds = builds.matches_tag_ids(runner.tags.ids) + # pick builds that does not have other tags than runner's one + builds = builds.matches_tag_ids(runner.tags.ids) - # pick builds that have at least one tag - unless runner.run_untagged? - builds = builds.with_any_tags - end + # pick builds that have at least one tag + unless runner.run_untagged? + builds = builds.with_any_tags end builds.find do |build| diff --git a/app/services/commits/create_service.rb b/app/services/commits/create_service.rb index f96f2931508..4d0578becbe 100644 --- a/app/services/commits/create_service.rb +++ b/app/services/commits/create_service.rb @@ -17,7 +17,7 @@ module Commits new_commit = create_commit! success(result: new_commit) - rescue ValidationError, ChangeError, Gitlab::Git::Index::IndexError, Gitlab::Git::CommitError, Gitlab::Git::HooksService::PreReceiveError => ex + rescue ValidationError, ChangeError, Gitlab::Git::Index::IndexError, Gitlab::Git::CommitError, Gitlab::Git::PreReceiveError => ex error(ex.message) end diff --git a/app/services/concerns/exclusive_lease_guard.rb b/app/services/concerns/exclusive_lease_guard.rb index 30be6accc32..f45436370c1 100644 --- a/app/services/concerns/exclusive_lease_guard.rb +++ b/app/services/concerns/exclusive_lease_guard.rb @@ -47,6 +47,6 @@ module ExclusiveLeaseGuard end def log_error(message, extra_args = {}) - logger.error(message) + Rails.logger.error(message) end end diff --git a/app/services/create_branch_service.rb b/app/services/create_branch_service.rb index 0ba6a0ac6b5..9b1a4d960e2 100644 --- a/app/services/create_branch_service.rb +++ b/app/services/create_branch_service.rb @@ -14,7 +14,7 @@ class CreateBranchService < BaseService else error('Invalid reference name') end - rescue Gitlab::Git::HooksService::PreReceiveError => ex + rescue Gitlab::Git::PreReceiveError => ex error(ex.message) end diff --git a/app/services/delete_branch_service.rb b/app/services/delete_branch_service.rb index 1f059c31944..e1499dcee64 100644 --- a/app/services/delete_branch_service.rb +++ b/app/services/delete_branch_service.rb @@ -16,7 +16,7 @@ class DeleteBranchService < BaseService else error('Failed to remove branch') end - rescue Gitlab::Git::HooksService::PreReceiveError => ex + rescue Gitlab::Git::PreReceiveError => ex error(ex.message) end diff --git a/app/services/lfs/unlock_file_service.rb b/app/services/lfs/unlock_file_service.rb index 7eb89339a92..7e3edf21d54 100644 --- a/app/services/lfs/unlock_file_service.rb +++ b/app/services/lfs/unlock_file_service.rb @@ -24,7 +24,7 @@ module Lfs success(lock: lock, http_status: :ok) elsif forced - error(_('You must have master access to force delete a lock'), 403) + error(_('You must have maintainer access to force delete a lock'), 403) else error(_("%{lock_path} is locked by GitLab User %{lock_user_id}") % { lock_path: lock.path, lock_user_id: lock.user_id }, 403) end diff --git a/app/services/merge_requests/base_service.rb b/app/services/merge_requests/base_service.rb index 231ab76fde4..4c420b38258 100644 --- a/app/services/merge_requests/base_service.rb +++ b/app/services/merge_requests/base_service.rb @@ -38,8 +38,8 @@ module MergeRequests def filter_params(merge_request) super - unless merge_request.can_allow_maintainer_to_push?(current_user) - params.delete(:allow_maintainer_to_push) + unless merge_request.can_allow_collaboration?(current_user) + params.delete(:allow_collaboration) end end diff --git a/app/services/merge_requests/create_from_issue_service.rb b/app/services/merge_requests/create_from_issue_service.rb index cf687b71d16..3407b312700 100644 --- a/app/services/merge_requests/create_from_issue_service.rb +++ b/app/services/merge_requests/create_from_issue_service.rb @@ -41,7 +41,9 @@ module MergeRequests end def ref - @ref || project.default_branch || 'master' + return @ref if project.repository.branch_exists?(@ref) + + project.default_branch || 'master' end def merge_request diff --git a/app/services/merge_requests/ff_merge_service.rb b/app/services/merge_requests/ff_merge_service.rb index ba6853b835a..bffc09c34f0 100644 --- a/app/services/merge_requests/ff_merge_service.rb +++ b/app/services/merge_requests/ff_merge_service.rb @@ -13,7 +13,7 @@ module MergeRequests source, merge_request.target_branch, merge_request: merge_request) - rescue Gitlab::Git::HooksService::PreReceiveError => e + rescue Gitlab::Git::PreReceiveError => e raise MergeError, e.message rescue StandardError => e raise MergeError, "Something went wrong during merge: #{e.message}" diff --git a/app/services/merge_requests/merge_service.rb b/app/services/merge_requests/merge_service.rb index 126da891c78..3d587f97906 100644 --- a/app/services/merge_requests/merge_service.rb +++ b/app/services/merge_requests/merge_service.rb @@ -79,7 +79,7 @@ module MergeRequests message = params[:commit_message] || merge_request.merge_commit_message repository.merge(current_user, source, merge_request, message) - rescue Gitlab::Git::HooksService::PreReceiveError => e + rescue Gitlab::Git::PreReceiveError => e handle_merge_error(log_message: e.message) raise MergeError, 'Something went wrong during merge pre-receive hook' rescue => e diff --git a/app/services/pages_service.rb b/app/services/pages_service.rb deleted file mode 100644 index 446eeb34d3b..00000000000 --- a/app/services/pages_service.rb +++ /dev/null @@ -1,15 +0,0 @@ -class PagesService - attr_reader :data - - def initialize(data) - @data = data - end - - def execute - return unless Settings.pages.enabled - return unless data[:build_name] == 'pages' - return unless data[:build_status] == 'success' - - PagesWorker.perform_async(:deploy, data[:build_id]) - end -end diff --git a/app/services/projects/autocomplete_service.rb b/app/services/projects/autocomplete_service.rb index 346971138b1..aa60661f7f2 100644 --- a/app/services/projects/autocomplete_service.rb +++ b/app/services/projects/autocomplete_service.rb @@ -11,7 +11,7 @@ module Projects order: { due_date: :asc, title: :asc } } - finder_params[:group_ids] = [@project.group.id] if @project.group + finder_params[:group_ids] = @project.group.self_and_ancestors_ids if @project.group MilestonesFinder.new(finder_params).execute.select([:iid, :title]) end diff --git a/app/services/projects/create_service.rb b/app/services/projects/create_service.rb index d16ecdb7b9b..a02a9052fb2 100644 --- a/app/services/projects/create_service.rb +++ b/app/services/projects/create_service.rb @@ -46,6 +46,9 @@ module Projects yield(@project) if block_given? + # If the block added errors, don't try to save the project + return @project if @project.errors.any? + @project.creator = current_user if forked_from_project_id @@ -63,6 +66,7 @@ module Projects message = "Unable to save #{e.record.type}: #{e.record.errors.full_messages.join(", ")} " fail(error: message) rescue => e + @project.errors.add(:base, e.message) if @project fail(error: e.message) end @@ -141,7 +145,6 @@ module Projects Rails.logger.error(log_message) if @project - @project.errors.add(:base, message) @project.mark_import_as_failed(message) if @project.persisted? && @project.import? end diff --git a/app/services/projects/destroy_service.rb b/app/services/projects/destroy_service.rb index de0125ed0dd..02769e72229 100644 --- a/app/services/projects/destroy_service.rb +++ b/app/services/projects/destroy_service.rb @@ -135,6 +135,7 @@ module Projects raise_error('Failed to remove some tags in project container registry. Please try again or contact administrator.') end + log_destroy_event trash_repositories! # Rails attempts to load all related records into memory before @@ -148,6 +149,10 @@ module Projects end end + def log_destroy_event + log_info("Attempting to destroy #{project.full_path} (#{project.id})") + end + ## # This method makes sure that we correctly remove registry tags # for legacy image repository (when repository path equals project path). diff --git a/app/services/projects/import_service.rb b/app/services/projects/import_service.rb index 00080717600..1781a01cbd4 100644 --- a/app/services/projects/import_service.rb +++ b/app/services/projects/import_service.rb @@ -17,6 +17,8 @@ module Projects def execute add_repository_to_project + download_lfs_objects + import_data success @@ -37,7 +39,7 @@ module Projects # We should skip the repository for a GitHub import or GitLab project import, # because these importers fetch the project repositories for us. - return if has_importer? && importer_class.try(:imports_repository?) + return if importer_imports_repository? if unknown_url? # In this case, we only want to import issues, not a repository. @@ -73,6 +75,27 @@ module Projects end end + def download_lfs_objects + # In this case, we only want to import issues + return if unknown_url? + + # If it has its own repository importer, it has to implements its own lfs import download + return if importer_imports_repository? + + return unless project.lfs_enabled? + + oids_to_download = Projects::LfsPointers::LfsImportService.new(project).execute + download_service = Projects::LfsPointers::LfsDownloadService.new(project) + + oids_to_download.each do |oid, link| + download_service.execute(oid, link) + end + rescue => e + # Right now, to avoid aborting the importing process, we silently fail + # if any exception raises. + Rails.logger.error("The Lfs import process failed. #{e.message}") + end + def import_data return unless has_importer? @@ -98,5 +121,9 @@ module Projects def unknown_url? project.import_url == Project::UNKNOWN_IMPORT_URL end + + def importer_imports_repository? + has_importer? && importer_class.try(:imports_repository?) + end end end diff --git a/app/services/projects/lfs_pointers/lfs_download_link_list_service.rb b/app/services/projects/lfs_pointers/lfs_download_link_list_service.rb new file mode 100644 index 00000000000..d9fb74b090e --- /dev/null +++ b/app/services/projects/lfs_pointers/lfs_download_link_list_service.rb @@ -0,0 +1,93 @@ +# This service lists the download link from a remote source based on the +# oids provided +module Projects + module LfsPointers + class LfsDownloadLinkListService < BaseService + DOWNLOAD_ACTION = 'download'.freeze + + DownloadLinksError = Class.new(StandardError) + DownloadLinkNotFound = Class.new(StandardError) + + attr_reader :remote_uri + + def initialize(project, remote_uri: nil) + super(project) + + @remote_uri = remote_uri + end + + # This method accepts two parameters: + # - oids: hash of oids to query. The structure is { lfs_file_oid => lfs_file_size } + # + # Returns a hash with the structure { lfs_file_oids => download_link } + def execute(oids) + return {} unless project&.lfs_enabled? && remote_uri && oids.present? + + get_download_links(oids) + end + + private + + def get_download_links(oids) + response = Gitlab::HTTP.post(remote_uri, + body: request_body(oids), + headers: headers) + + raise DownloadLinksError, response.message unless response.success? + + parse_response_links(response['objects']) + end + + def parse_response_links(objects_response) + objects_response.each_with_object({}) do |entry, link_list| + begin + oid = entry['oid'] + link = entry.dig('actions', DOWNLOAD_ACTION, 'href') + + raise DownloadLinkNotFound unless link + + link_list[oid] = add_credentials(link) + rescue DownloadLinkNotFound, URI::InvalidURIError + Rails.logger.error("Link for Lfs Object with oid #{oid} not found or invalid.") + end + end + end + + def request_body(oids) + { + operation: DOWNLOAD_ACTION, + objects: oids.map { |oid, size| { oid: oid, size: size } } + }.to_json + end + + def headers + { + 'Accept' => LfsRequest::CONTENT_TYPE, + 'Content-Type' => LfsRequest::CONTENT_TYPE + }.freeze + end + + def add_credentials(link) + uri = URI.parse(link) + + if should_add_credentials?(uri) + uri.user = remote_uri.user + uri.password = remote_uri.password + end + + uri.to_s + end + + # The download link can be a local url or an object storage url + # If the download link has the some host as the import url then + # we add the same credentials because we may need them + def should_add_credentials?(link_uri) + url_credentials? && link_uri.host == remote_uri.host + end + + def url_credentials? + remote_uri.user.present? || remote_uri.password.present? + end + end + end +end diff --git a/app/services/projects/lfs_pointers/lfs_download_service.rb b/app/services/projects/lfs_pointers/lfs_download_service.rb new file mode 100644 index 00000000000..6ea43561d61 --- /dev/null +++ b/app/services/projects/lfs_pointers/lfs_download_service.rb @@ -0,0 +1,58 @@ +# This service downloads and links lfs objects from a remote URL +module Projects + module LfsPointers + class LfsDownloadService < BaseService + def execute(oid, url) + return unless project&.lfs_enabled? && oid.present? && url.present? + + return if LfsObject.exists?(oid: oid) + + sanitized_uri = Gitlab::UrlSanitizer.new(url) + + with_tmp_file(oid) do |file| + size = download_and_save_file(file, sanitized_uri) + lfs_object = LfsObject.new(oid: oid, size: size, file: file) + + project.all_lfs_objects << lfs_object + end + rescue StandardError => e + Rails.logger.error("LFS file with oid #{oid} could't be downloaded from #{sanitized_uri.sanitized_url}: #{e.message}") + end + + private + + def download_and_save_file(file, sanitized_uri) + IO.copy_stream(open(sanitized_uri.sanitized_url, headers(sanitized_uri)), file) + end + + def headers(sanitized_uri) + {}.tap do |headers| + credentials = sanitized_uri.credentials + + if credentials[:user].present? || credentials[:password].present? + # Using authentication headers in the request + headers[:http_basic_authentication] = [credentials[:user], credentials[:password]] + end + end + end + + def with_tmp_file(oid) + create_tmp_storage_dir + + File.open(File.join(tmp_storage_dir, oid), 'w') { |file| yield file } + end + + def create_tmp_storage_dir + FileUtils.makedirs(tmp_storage_dir) unless Dir.exist?(tmp_storage_dir) + end + + def tmp_storage_dir + @tmp_storage_dir ||= File.join(storage_dir, 'tmp', 'download') + end + + def storage_dir + @storage_dir ||= Gitlab.config.lfs.storage_path + end + end + end +end diff --git a/app/services/projects/lfs_pointers/lfs_import_service.rb b/app/services/projects/lfs_pointers/lfs_import_service.rb new file mode 100644 index 00000000000..b6b0dec142f --- /dev/null +++ b/app/services/projects/lfs_pointers/lfs_import_service.rb @@ -0,0 +1,92 @@ +# This service manages the whole worflow of discovering the Lfs files in a +# repository, linking them to the project and downloading (and linking) the non +# existent ones. +module Projects + module LfsPointers + class LfsImportService < BaseService + include Gitlab::Utils::StrongMemoize + + HEAD_REV = 'HEAD'.freeze + LFS_ENDPOINT_PATTERN = /^\t?url\s*=\s*(.+)$/.freeze + LFS_BATCH_API_ENDPOINT = '/info/lfs/objects/batch'.freeze + + LfsImportError = Class.new(StandardError) + + def execute + return {} unless project&.lfs_enabled? + + if external_lfs_endpoint? + # If the endpoint host is different from the import_url it means + # that the repo is using a third party service for storing the LFS files. + # In this case, we have to disable lfs in the project + disable_lfs! + + return {} + end + + get_download_links + rescue LfsDownloadLinkListService::DownloadLinksError => e + raise LfsImportError, "The LFS objects download list couldn't be imported. Error: #{e.message}" + end + + private + + def external_lfs_endpoint? + lfsconfig_endpoint_uri && lfsconfig_endpoint_uri.host != import_uri.host + end + + def disable_lfs! + project.update(lfs_enabled: false) + end + + def get_download_links + existent_lfs = LfsListService.new(project).execute + linked_oids = LfsLinkService.new(project).execute(existent_lfs.keys) + + # Retrieving those oids not linked and which we need to download + not_linked_lfs = existent_lfs.except(*linked_oids) + + LfsDownloadLinkListService.new(project, remote_uri: current_endpoint_uri).execute(not_linked_lfs) + end + + def lfsconfig_endpoint_uri + strong_memoize(:lfsconfig_endpoint_uri) do + # Retrieveing the blob data from the .lfsconfig file + data = project.repository.lfsconfig_for(HEAD_REV) + # Parsing the data to retrieve the url + parsed_data = data&.match(LFS_ENDPOINT_PATTERN) + + if parsed_data + URI.parse(parsed_data[1]).tap do |endpoint| + endpoint.user ||= import_uri.user + endpoint.password ||= import_uri.password + end + end + end + rescue URI::InvalidURIError + raise LfsImportError, 'Invalid URL in .lfsconfig file' + end + + def import_uri + @import_uri ||= URI.parse(project.import_url) + rescue URI::InvalidURIError + raise LfsImportError, 'Invalid project import URL' + end + + def current_endpoint_uri + (lfsconfig_endpoint_uri || default_endpoint_uri) + end + + # The import url must end with '.git' here we ensure it is + def default_endpoint_uri + @default_endpoint_uri ||= begin + import_uri.dup.tap do |uri| + path = uri.path.gsub(%r(/$), '') + path += '.git' unless path.ends_with?('.git') + uri.path = path + LFS_BATCH_API_ENDPOINT + end + end + end + end + end +end diff --git a/app/services/projects/lfs_pointers/lfs_link_service.rb b/app/services/projects/lfs_pointers/lfs_link_service.rb new file mode 100644 index 00000000000..d20bdf86c58 --- /dev/null +++ b/app/services/projects/lfs_pointers/lfs_link_service.rb @@ -0,0 +1,29 @@ +# Given a list of oids, this services links the existent Lfs Objects to the project +module Projects + module LfsPointers + class LfsLinkService < BaseService + # Accept an array of oids to link + # + # Returns a hash with the same structure with oids linked + def execute(oids) + return {} unless project&.lfs_enabled? + + # Search and link existing LFS Object + link_existing_lfs_objects(oids) + end + + private + + def link_existing_lfs_objects(oids) + existent_lfs_objects = LfsObject.where(oid: oids) + + return [] unless existent_lfs_objects.any? + + not_linked_lfs_objects = existent_lfs_objects.where.not(id: project.all_lfs_objects) + project.all_lfs_objects << not_linked_lfs_objects + + existent_lfs_objects.pluck(:oid) + end + end + end +end diff --git a/app/services/projects/lfs_pointers/lfs_list_service.rb b/app/services/projects/lfs_pointers/lfs_list_service.rb new file mode 100644 index 00000000000..b770982cbc0 --- /dev/null +++ b/app/services/projects/lfs_pointers/lfs_list_service.rb @@ -0,0 +1,19 @@ +# This service list all existent Lfs objects in a repository +module Projects + module LfsPointers + class LfsListService < BaseService + REV = 'HEAD'.freeze + + # Retrieve all lfs blob pointers and returns a hash + # with the structure { lfs_file_oid => lfs_file_size } + def execute + return {} unless project&.lfs_enabled? + + Gitlab::Git::LfsChanges.new(project.repository, REV) + .all_pointers + .map! { |blob| [blob.lfs_oid, blob.lfs_size] } + .to_h + end + end + end +end diff --git a/app/services/projects/update_service.rb b/app/services/projects/update_service.rb index 679f4a9cb62..d8250cd8102 100644 --- a/app/services/projects/update_service.rb +++ b/app/services/projects/update_service.rb @@ -17,6 +17,11 @@ module Projects ensure_wiki_exists if enabling_wiki? + yield if block_given? + + # If the block added errors, don't try to save the project + return validation_failed! if project.errors.any? + if project.update_attributes(params.except(:default_branch)) if project.previous_changes.include?('path') project.rename_repo @@ -28,21 +33,25 @@ module Projects success else - model_errors = project.errors.full_messages.to_sentence - error_message = model_errors.presence || 'Project could not be updated!' - - error(error_message) + validation_failed! end end def run_auto_devops_pipeline? - return false if project.repository.gitlab_ci_yml || !project.auto_devops.previous_changes.include?('enabled') + return false if project.repository.gitlab_ci_yml || !project.auto_devops&.previous_changes&.include?('enabled') project.auto_devops.enabled? || (project.auto_devops.enabled.nil? && Gitlab::CurrentSettings.auto_devops_enabled?) end private + def validation_failed! + model_errors = project.errors.full_messages.to_sentence + error_message = model_errors.presence || 'Project could not be updated!' + + error(error_message) + end + def renaming_project_with_container_registry_tags? new_path = params[:path] @@ -53,8 +62,8 @@ module Projects def changing_default_branch? new_branch = params[:default_branch] - project.repository.exists? && - new_branch && new_branch != project.default_branch + new_branch && project.repository.exists? && + new_branch != project.default_branch end def enabling_wiki? diff --git a/app/services/quick_actions/interpret_service.rb b/app/services/quick_actions/interpret_service.rb index 0215994b1a7..9ac8fdb4cff 100644 --- a/app/services/quick_actions/interpret_service.rb +++ b/app/services/quick_actions/interpret_service.rb @@ -561,6 +561,17 @@ module QuickActions end end + desc 'Make issue confidential.' + explanation do + 'Makes this issue confidential' + end + condition do + issuable.is_a?(Issue) && current_user.can?(:"admin_#{issuable.to_ability_name}", issuable) + end + command :confidential do + @updates[:confidential] = true + end + def extract_users(params) return [] if params.nil? diff --git a/app/services/tags/create_service.rb b/app/services/tags/create_service.rb index cc76d0df3a1..3cc88d77ba1 100644 --- a/app/services/tags/create_service.rb +++ b/app/services/tags/create_service.rb @@ -13,7 +13,7 @@ module Tags new_tag = repository.add_tag(current_user, tag_name, target, message) rescue Gitlab::Git::Repository::TagExistsError return error("Tag #{tag_name} already exists") - rescue Gitlab::Git::HooksService::PreReceiveError => ex + rescue Gitlab::Git::PreReceiveError => ex return error(ex.message) end diff --git a/app/services/tags/destroy_service.rb b/app/services/tags/destroy_service.rb index d3d46064729..b84b061e460 100644 --- a/app/services/tags/destroy_service.rb +++ b/app/services/tags/destroy_service.rb @@ -21,7 +21,7 @@ module Tags else error('Failed to remove tag') end - rescue Gitlab::Git::HooksService::PreReceiveError => ex + rescue Gitlab::Git::PreReceiveError => ex error(ex.message) end diff --git a/app/services/test_hooks/project_service.rb b/app/services/test_hooks/project_service.rb index 01d5d774cd5..65183e84cce 100644 --- a/app/services/test_hooks/project_service.rb +++ b/app/services/test_hooks/project_service.rb @@ -1,11 +1,13 @@ module TestHooks class ProjectService < TestHooks::BaseService - private + attr_writer :project def project @project ||= hook.project end + private + def push_events_data throw(:validation_error, 'Ensure the project has at least one commit.') if project.empty_repo? diff --git a/app/services/validate_new_branch_service.rb b/app/services/validate_new_branch_service.rb index 7d1ed768ee8..643f2ce1481 100644 --- a/app/services/validate_new_branch_service.rb +++ b/app/services/validate_new_branch_service.rb @@ -13,7 +13,7 @@ class ValidateNewBranchService < BaseService end success - rescue Gitlab::Git::HooksService::PreReceiveError => ex + rescue Gitlab::Git::PreReceiveError => ex error(ex.message) end end diff --git a/app/uploaders/favicon_uploader.rb b/app/uploaders/favicon_uploader.rb new file mode 100644 index 00000000000..3639375d474 --- /dev/null +++ b/app/uploaders/favicon_uploader.rb @@ -0,0 +1,13 @@ +class FaviconUploader < AttachmentUploader + EXTENSION_WHITELIST = %w[png ico].freeze + + def extension_whitelist + EXTENSION_WHITELIST + end + + private + + def filename_for_different_format(filename, format) + filename.chomp(File.extname(filename)) + ".#{format}" + end +end diff --git a/app/uploaders/file_uploader.rb b/app/uploaders/file_uploader.rb index 133fdf6684d..36bc0a4575a 100644 --- a/app/uploaders/file_uploader.rb +++ b/app/uploaders/file_uploader.rb @@ -65,10 +65,10 @@ class FileUploader < GitlabUploader SecureRandom.hex end - def upload_paths(filename) + def upload_paths(identifier) [ - File.join(secret, filename), - File.join(base_dir(Store::REMOTE), secret, filename) + File.join(secret, identifier), + File.join(base_dir(Store::REMOTE), secret, identifier) ] end diff --git a/app/uploaders/object_storage.rb b/app/uploaders/object_storage.rb index 5bdca26a584..b8ecfc4ee2b 100644 --- a/app/uploaders/object_storage.rb +++ b/app/uploaders/object_storage.rb @@ -10,8 +10,17 @@ module ObjectStorage UnknownStoreError = Class.new(StandardError) ObjectStorageUnavailable = Class.new(StandardError) - DIRECT_UPLOAD_TIMEOUT = 4.hours - DIRECT_UPLOAD_EXPIRE_OFFSET = 15.minutes + class ExclusiveLeaseTaken < StandardError + def initialize(lease_key) + @lease_key = lease_key + end + + def message + *lease_key_group, _ = *@lease_key.split(":") + "Exclusive lease for #{lease_key_group.join(':')} is already taken." + end + end + TMP_UPLOAD_PATH = 'tmp/uploads'.freeze module Store @@ -24,18 +33,18 @@ module ObjectStorage module RecordsUploads extend ActiveSupport::Concern - def prepended(base) + prepended do |base| raise "#{base} must include ObjectStorage::Concern to use extensions." unless base < Concern - base.include(RecordsUploads::Concern) + base.include(::RecordsUploads::Concern) end def retrieve_from_store!(identifier) - paths = store_dirs.map { |store, path| File.join(path, identifier) } + paths = upload_paths(identifier) unless current_upload_satisfies?(paths, model) # the upload we already have isn't right, find the correct one - self.upload = uploads.find_by(model: model, path: paths) + self.upload = model&.retrieve_upload(identifier, paths) end super @@ -48,7 +57,7 @@ module ObjectStorage end def upload=(upload) - return unless upload + return if upload.nil? self.object_store = upload.store super @@ -64,6 +73,15 @@ module ObjectStorage upload.id) end + def exclusive_lease_key + # For FileUploaders, model may have many uploaders. In that case + # we want to use exclusive key per upload, not per model to allow + # parallel migration + key_object = upload || model + + "object_storage_migrate:#{key_object.class}:#{key_object.id}" + end + private def current_upload_satisfies?(paths, model) @@ -157,9 +175,9 @@ module ObjectStorage model_class.uploader_options.dig(mount_point, :mount_on) || mount_point end - def workhorse_authorize + def workhorse_authorize(has_length:, maximum_size: nil) { - RemoteObject: workhorse_remote_upload_options, + RemoteObject: workhorse_remote_upload_options(has_length: has_length, maximum_size: maximum_size), TempPath: workhorse_local_upload_path }.compact end @@ -168,23 +186,16 @@ module ObjectStorage File.join(self.root, TMP_UPLOAD_PATH) end - def workhorse_remote_upload_options + def workhorse_remote_upload_options(has_length:, maximum_size: nil) return unless self.object_store_enabled? return unless self.direct_upload_enabled? id = [CarrierWave.generate_cache_id, SecureRandom.hex].join('-') upload_path = File.join(TMP_UPLOAD_PATH, id) - connection = ::Fog::Storage.new(self.object_store_credentials) - expire_at = Time.now + DIRECT_UPLOAD_TIMEOUT + DIRECT_UPLOAD_EXPIRE_OFFSET - options = { 'Content-Type' => 'application/octet-stream' } + direct_upload = ObjectStorage::DirectUpload.new(self.object_store_credentials, remote_store_path, upload_path, + has_length: has_length, maximum_size: maximum_size) - { - ID: id, - Timeout: DIRECT_UPLOAD_TIMEOUT, - GetURL: connection.get_object_url(remote_store_path, upload_path, expire_at), - DeleteURL: connection.delete_object_url(remote_store_path, upload_path, expire_at), - StoreURL: connection.put_object_url(remote_store_path, upload_path, expire_at, options) - } + direct_upload.to_hash.merge(ID: id) end end @@ -270,7 +281,7 @@ module ObjectStorage end def delete_migrated_file(migrated_file) - migrated_file.delete if exists? + migrated_file.delete end def exists? @@ -288,6 +299,13 @@ module ObjectStorage } end + # Returns all the possible paths for an upload. + # the `upload.path` is a lookup parameter, and it may change + # depending on the `store` param. + def upload_paths(identifier) + store_dirs.map { |store, path| File.join(path, identifier) } + end + def cache!(new_file = sanitized_file) # We intercept ::UploadedFile which might be stored on remote storage # We use that for "accelerated" uploads, where we store result on remote storage @@ -307,6 +325,10 @@ module ObjectStorage super end + def exclusive_lease_key + "object_storage_migrate:#{model.class}:#{model.id}" + end + private def schedule_background_upload? @@ -373,17 +395,14 @@ module ObjectStorage end end - def exclusive_lease_key - "object_storage_migrate:#{model.class}:#{model.id}" - end - def with_exclusive_lease - uuid = Gitlab::ExclusiveLease.new(exclusive_lease_key, timeout: 1.hour.to_i).try_obtain - raise 'exclusive lease already taken' unless uuid + lease_key = exclusive_lease_key + uuid = Gitlab::ExclusiveLease.new(lease_key, timeout: 1.hour.to_i).try_obtain + raise ExclusiveLeaseTaken.new(lease_key) unless uuid yield uuid ensure - Gitlab::ExclusiveLease.cancel(exclusive_lease_key, uuid) + Gitlab::ExclusiveLease.cancel(lease_key, uuid) end # diff --git a/app/uploaders/records_uploads.rb b/app/uploaders/records_uploads.rb index 89c74a78835..301f4681fcd 100644 --- a/app/uploaders/records_uploads.rb +++ b/app/uploaders/records_uploads.rb @@ -22,7 +22,7 @@ module RecordsUploads Upload.transaction do uploads.where(path: upload_path).delete_all - upload.destroy! if upload + upload.delete if upload self.upload = build_upload.tap(&:save!) end diff --git a/app/uploaders/uploader_helper.rb b/app/uploaders/uploader_helper.rb index fd446d31092..207928b61d0 100644 --- a/app/uploaders/uploader_helper.rb +++ b/app/uploaders/uploader_helper.rb @@ -1,6 +1,6 @@ # Extra methods for uploader module UploaderHelper - IMAGE_EXT = %w[png jpg jpeg gif bmp tiff].freeze + IMAGE_EXT = %w[png jpg jpeg gif bmp tiff ico].freeze # We recommend using the .mp4 format over .mov. Videos in .mov format can # still be used but you really need to make sure they are served with the # proper MIME type video/mp4 and not video/quicktime or your videos won't play diff --git a/app/validators/url_validator.rb b/app/validators/url_validator.rb index 8648c4c75e3..6854fec582e 100644 --- a/app/validators/url_validator.rb +++ b/app/validators/url_validator.rb @@ -18,6 +18,13 @@ # This validator can also block urls pointing to localhost or the local network to # protect against Server-side Request Forgery (SSRF), or check for the right port. # +# The available options are: +# - protocols: Allowed protocols. Default: http and https +# - allow_localhost: Allow urls pointing to localhost. Default: true +# - allow_local_network: Allow urls pointing to private network addresses. Default: true +# - ports: Allowed ports. Default: all. +# - enforce_user: Validate user format. Default: false +# # Example: # class User < ActiveRecord::Base # validates :personal_url, url: { allow_localhost: false, allow_local_network: false} @@ -35,7 +42,7 @@ class UrlValidator < ActiveModel::EachValidator if value.present? value.strip! else - record.errors.add(attribute, "must be a valid URL") + record.errors.add(attribute, 'must be a valid URL') end Gitlab::UrlBlocker.validate!(value, blocker_args) @@ -51,7 +58,8 @@ class UrlValidator < ActiveModel::EachValidator protocols: DEFAULT_PROTOCOLS, ports: [], allow_localhost: true, - allow_local_network: true + allow_local_network: true, + enforce_user: false } end @@ -64,7 +72,7 @@ class UrlValidator < ActiveModel::EachValidator end def blocker_args - current_options.slice(:allow_localhost, :allow_local_network, :protocols, :ports).tap do |args| + current_options.slice(*default_options.keys).tap do |args| if allow_setting_local_requests? args[:allow_localhost] = args[:allow_local_network] = true end diff --git a/app/views/admin/appearances/_form.html.haml b/app/views/admin/appearances/_form.html.haml index 5e08837255f..a0861870ba4 100644 --- a/app/views/admin/appearances/_form.html.haml +++ b/app/views/admin/appearances/_form.html.haml @@ -11,13 +11,32 @@ = image_tag @appearance.header_logo_url, class: 'appearance-light-logo-preview' - if @appearance.persisted? %br - = link_to 'Remove header logo', header_logos_admin_appearances_path, data: { confirm: "Header logo will be removed. Are you sure?"}, method: :delete, class: "btn btn-remove btn-sm remove-logo" + = link_to 'Remove header logo', header_logos_admin_appearances_path, data: { confirm: "Header logo will be removed. Are you sure?"}, method: :delete, class: "btn btn-inverted btn-remove btn-sm remove-logo" %hr = f.hidden_field :header_logo_cache = f.file_field :header_logo, class: "" .hint Maximum file size is 1MB. Pages are optimized for a 28px tall header logo + %fieldset.app_logo + %legend + Favicon: + .form-group.row + = f.label :favicon, 'Favicon', class: 'col-sm-2 col-form-label' + .col-sm-10 + - if @appearance.favicon? + = image_tag @appearance.favicon_url, class: 'appearance-light-logo-preview' + - if @appearance.persisted? + %br + = link_to 'Remove favicon', favicon_admin_appearances_path, data: { confirm: "Favicon will be removed. Are you sure?"}, method: :delete, class: "btn btn-inverted btn-remove btn-sm remove-logo" + %hr + = f.hidden_field :favicon_cache + = f.file_field :favicon, class: '' + .hint + Maximum file size is 1MB. Image size must be 32x32px. Allowed image formats are #{favicon_extension_whitelist}. + %br + Images with incorrect dimensions are not resized automatically, and may result in unexpected behavior. + %fieldset.sign-in %legend Sign in/Sign up pages: @@ -38,7 +57,7 @@ = image_tag @appearance.logo_url, class: 'appearance-logo-preview' - if @appearance.persisted? %br - = link_to 'Remove logo', logo_admin_appearances_path, data: { confirm: "Logo will be removed. Are you sure?"}, method: :delete, class: "btn btn-remove btn-sm remove-logo" + = link_to 'Remove logo', logo_admin_appearances_path, data: { confirm: "Logo will be removed. Are you sure?"}, method: :delete, class: "btn btn-inverted btn-remove btn-sm remove-logo" %hr = f.hidden_field :logo_cache = f.file_field :logo, class: "" diff --git a/app/views/admin/application_settings/_abuse.html.haml b/app/views/admin/application_settings/_abuse.html.haml index 6b9b2a17dd9..91993838fc8 100644 --- a/app/views/admin/application_settings/_abuse.html.haml +++ b/app/views/admin/application_settings/_abuse.html.haml @@ -2,11 +2,10 @@ = form_errors(@application_setting) %fieldset - .form-group.row - = f.label :admin_notification_email, 'Abuse reports notification email', class: 'col-form-label col-sm-2' - .col-sm-10 - = f.text_field :admin_notification_email, class: 'form-control' - .form-text.text-muted - Abuse reports will be sent to this address if it is set. Abuse reports are always available in the admin area. + .form-group + = f.label :admin_notification_email, 'Abuse reports notification email', class: 'label-light' + = f.text_field :admin_notification_email, class: 'form-control' + .form-text.text-muted + Abuse reports will be sent to this address if it is set. Abuse reports are always available in the admin area. = f.submit 'Save changes', class: "btn btn-success" diff --git a/app/views/admin/application_settings/_account_and_limit.html.haml b/app/views/admin/application_settings/_account_and_limit.html.haml index 07f9ea0865b..f40939747f4 100644 --- a/app/views/admin/application_settings/_account_and_limit.html.haml +++ b/app/views/admin/application_settings/_account_and_limit.html.haml @@ -2,38 +2,32 @@ = form_errors(@application_setting) %fieldset - .form-group.row - .offset-sm-2.col-sm-10 - .form-check - = f.check_box :gravatar_enabled, class: 'form-check-input' - = f.label :gravatar_enabled, class: 'form-check-label' do - Gravatar enabled - .form-group.row - = f.label :default_projects_limit, class: 'col-form-label col-sm-2' - .col-sm-10 - = f.number_field :default_projects_limit, class: 'form-control' - .form-group.row - = f.label :max_attachment_size, 'Maximum attachment size (MB)', class: 'col-form-label col-sm-2' - .col-sm-10 - = f.number_field :max_attachment_size, class: 'form-control' - .form-group.row - = f.label :session_expire_delay, 'Session duration (minutes)', class: 'col-form-label col-sm-2' - .col-sm-10 - = f.number_field :session_expire_delay, class: 'form-control' - %span.form-text.text-muted#session_expire_delay_help_block GitLab restart is required to apply changes - .form-group.row - = f.label :user_oauth_applications, 'User OAuth applications', class: 'col-form-label col-sm-2' - .col-sm-10 - .form-check - = f.check_box :user_oauth_applications, class: 'form-check-input' - = f.label :user_oauth_applications, class: 'form-check-label' do - Allow users to register any application to use GitLab as an OAuth provider - .form-group.row - = f.label :user_default_external, 'New users set to external', class: 'col-form-label col-sm-2' - .col-sm-10 - .form-check - = f.check_box :user_default_external, class: 'form-check-input' - = f.label :user_default_external, class: 'form-check-label' do - Newly registered users will by default be external + .form-group + .form-check + = f.check_box :gravatar_enabled, class: 'form-check-input' + = f.label :gravatar_enabled, class: 'form-check-label' do + Gravatar enabled + .form-group + = f.label :default_projects_limit, class: 'label-light' + = f.number_field :default_projects_limit, class: 'form-control' + .form-group + = f.label :max_attachment_size, 'Maximum attachment size (MB)', class: 'label-light' + = f.number_field :max_attachment_size, class: 'form-control' + .form-group + = f.label :session_expire_delay, 'Session duration (minutes)', class: 'label-light' + = f.number_field :session_expire_delay, class: 'form-control' + %span.form-text.text-muted#session_expire_delay_help_block GitLab restart is required to apply changes + .form-group + = f.label :user_oauth_applications, 'User OAuth applications', class: 'label-light' + .form-check + = f.check_box :user_oauth_applications, class: 'form-check-input' + = f.label :user_oauth_applications, class: 'form-check-label' do + Allow users to register any application to use GitLab as an OAuth provider + .form-group + = f.label :user_default_external, 'New users set to external', class: 'label-light' + .form-check + = f.check_box :user_default_external, class: 'form-check-input' + = f.label :user_default_external, class: 'form-check-label' do + Newly registered users will by default be external = f.submit 'Save changes', class: 'btn btn-success' diff --git a/app/views/admin/application_settings/_background_jobs.html.haml b/app/views/admin/application_settings/_background_jobs.html.haml index fc5df02242a..fd8e695ed49 100644 --- a/app/views/admin/application_settings/_background_jobs.html.haml +++ b/app/views/admin/application_settings/_background_jobs.html.haml @@ -6,25 +6,22 @@ These settings require a = link_to 'restart', help_page_path('administration/restart_gitlab') to take effect. - .form-group.row - .offset-sm-2.col-sm-10 - .form-check - = f.check_box :sidekiq_throttling_enabled, class: 'form-check-input' - = f.label :sidekiq_throttling_enabled, class: 'form-check-label' do - Enable Sidekiq Job Throttling - .form-text.text-muted - Limit the amount of resources slow running jobs are assigned. - .form-group.row - = f.label :sidekiq_throttling_queues, 'Sidekiq queues to throttle', class: 'col-form-label col-sm-2' - .col-sm-10 - = f.select :sidekiq_throttling_queues, sidekiq_queue_options_for_select, { include_hidden: false }, multiple: true, class: 'select2 select-wide', data: { field: 'sidekiq_throttling_queues' } + .form-group + .form-check + = f.check_box :sidekiq_throttling_enabled, class: 'form-check-input' + = f.label :sidekiq_throttling_enabled, class: 'form-check-label' do + Enable Sidekiq Job Throttling .form-text.text-muted - Choose which queues you wish to throttle. - .form-group.row - = f.label :sidekiq_throttling_factor, 'Throttling Factor', class: 'col-form-label col-sm-2' - .col-sm-10 - = f.number_field :sidekiq_throttling_factor, class: 'form-control', min: '0.01', max: '0.99', step: '0.01' - .form-text.text-muted - The factor by which the queues should be throttled. A value between 0.0 and 1.0, exclusive. + Limit the amount of resources slow running jobs are assigned. + .form-group + = f.label :sidekiq_throttling_queues, 'Sidekiq queues to throttle', class: 'label-light' + = f.select :sidekiq_throttling_queues, sidekiq_queue_options_for_select, { include_hidden: false }, multiple: true, class: 'select2 select-wide', data: { field: 'sidekiq_throttling_queues' } + .form-text.text-muted + Choose which queues you wish to throttle. + .form-group + = f.label :sidekiq_throttling_factor, 'Throttling Factor', class: 'label-light' + = f.number_field :sidekiq_throttling_factor, class: 'form-control', min: '0.01', max: '0.99', step: '0.01' + .form-text.text-muted + The factor by which the queues should be throttled. A value between 0.0 and 1.0, exclusive. = f.submit 'Save changes', class: "btn btn-success" diff --git a/app/views/admin/application_settings/_ci_cd.html.haml b/app/views/admin/application_settings/_ci_cd.html.haml index 233821818e6..7c16cafe13f 100644 --- a/app/views/admin/application_settings/_ci_cd.html.haml +++ b/app/views/admin/application_settings/_ci_cd.html.haml @@ -2,46 +2,40 @@ = form_errors(@application_setting) %fieldset - .form-group.row - .offset-sm-2.col-sm-10 - .form-check - = f.check_box :auto_devops_enabled, class: 'form-check-input' - = f.label :auto_devops_enabled, class: 'form-check-label' do - Enabled Auto DevOps for projects by default - .form-text.text-muted - It will automatically build, test, and deploy applications based on a predefined CI/CD configuration - = link_to icon('question-circle'), help_page_path('topics/autodevops/index.md') - .form-group.row - = f.label :auto_devops_domain, class: 'col-form-label col-sm-2' - .col-sm-10 - = f.text_field :auto_devops_domain, class: 'form-control', placeholder: 'domain.com' - .form-text.text-muted - = s_("AdminSettings|Specify a domain to use by default for every project's Auto Review Apps and Auto Deploy stages.") - .form-group.row - .offset-sm-2.col-sm-10 - .form-check - = f.check_box :shared_runners_enabled, class: 'form-check-input' - = f.label :shared_runners_enabled, class: 'form-check-label' do - Enable shared runners for new projects - .form-group.row - = f.label :shared_runners_text, class: 'col-form-label col-sm-2' - .col-sm-10 - = f.text_area :shared_runners_text, class: 'form-control', rows: 4 - .form-text.text-muted Markdown enabled - .form-group.row - = f.label :max_artifacts_size, 'Maximum artifacts size (MB)', class: 'col-form-label col-sm-2' - .col-sm-10 - = f.number_field :max_artifacts_size, class: 'form-control' - .form-text.text-muted - Set the maximum file size for each job's artifacts - = link_to icon('question-circle'), help_page_path('user/admin_area/settings/continuous_integration', anchor: 'maximum-artifacts-size') - .form-group.row - = f.label :default_artifacts_expire_in, 'Default artifacts expiration', class: 'col-form-label col-sm-2' - .col-sm-10 - = f.text_field :default_artifacts_expire_in, class: 'form-control' - .form-text.text-muted - Set the default expiration time for each job's artifacts. - 0 for unlimited. - = link_to icon('question-circle'), help_page_path('user/admin_area/settings/continuous_integration', anchor: 'default-artifacts-expiration') + .form-group + .form-check + = f.check_box :auto_devops_enabled, class: 'form-check-input' + = f.label :auto_devops_enabled, class: 'form-check-label' do + Enabled Auto DevOps for projects by default + .form-text.text-muted + It will automatically build, test, and deploy applications based on a predefined CI/CD configuration + = link_to icon('question-circle'), help_page_path('topics/autodevops/index.md') + .form-group + = f.label :auto_devops_domain, class: 'label-light' + = f.text_field :auto_devops_domain, class: 'form-control', placeholder: 'domain.com' + .form-text.text-muted + = s_("AdminSettings|Specify a domain to use by default for every project's Auto Review Apps and Auto Deploy stages.") + .form-group + .form-check + = f.check_box :shared_runners_enabled, class: 'form-check-input' + = f.label :shared_runners_enabled, class: 'form-check-label' do + Enable shared runners for new projects + .form-group + = f.label :shared_runners_text, class: 'label-light' + = f.text_area :shared_runners_text, class: 'form-control', rows: 4 + .form-text.text-muted Markdown enabled + .form-group + = f.label :max_artifacts_size, 'Maximum artifacts size (MB)', class: 'label-light' + = f.number_field :max_artifacts_size, class: 'form-control' + .form-text.text-muted + Set the maximum file size for each job's artifacts + = link_to icon('question-circle'), help_page_path('user/admin_area/settings/continuous_integration', anchor: 'maximum-artifacts-size') + .form-group + = f.label :default_artifacts_expire_in, 'Default artifacts expiration', class: 'label-light' + = f.text_field :default_artifacts_expire_in, class: 'form-control' + .form-text.text-muted + Set the default expiration time for each job's artifacts. + 0 for unlimited. + = link_to icon('question-circle'), help_page_path('user/admin_area/settings/continuous_integration', anchor: 'default-artifacts-expiration') = f.submit 'Save changes', class: "btn btn-success" diff --git a/app/views/admin/application_settings/_email.html.haml b/app/views/admin/application_settings/_email.html.haml index 01be5878a60..99e44ffa741 100644 --- a/app/views/admin/application_settings/_email.html.haml +++ b/app/views/admin/application_settings/_email.html.haml @@ -2,25 +2,23 @@ = form_errors(@application_setting) %fieldset - .form-group.row - .offset-sm-2.col-sm-10 - .form-check - = f.check_box :email_author_in_body, class: 'form-check-input' - = f.label :email_author_in_body, class: 'form-check-label' do - Include author name in notification email body - .form-text.text-muted - Some email servers do not support overriding the email sender name. - Enable this option to include the name of the author of the issue, - merge request or comment in the email body instead. - .form-group.row - .offset-sm-2.col-sm-10 - .form-check - = f.check_box :html_emails_enabled, class: 'form-check-input' - = f.label :html_emails_enabled, class: 'form-check-label' do - Enable HTML emails - .form-text.text-muted - By default GitLab sends emails in HTML and plain text formats so mail - clients can choose what format to use. Disable this option if you only - want to send emails in plain text format. + .form-group + .form-check + = f.check_box :email_author_in_body, class: 'form-check-input' + = f.label :email_author_in_body, class: 'form-check-label' do + Include author name in notification email body + .form-text.text-muted + Some email servers do not support overriding the email sender name. + Enable this option to include the name of the author of the issue, + merge request or comment in the email body instead. + .form-group + .form-check + = f.check_box :html_emails_enabled, class: 'form-check-input' + = f.label :html_emails_enabled, class: 'form-check-label' do + Enable HTML emails + .form-text.text-muted + By default GitLab sends emails in HTML and plain text formats so mail + clients can choose what format to use. Disable this option if you only + want to send emails in plain text format. = f.submit 'Save changes', class: "btn btn-success" diff --git a/app/views/admin/application_settings/_gitaly.html.haml b/app/views/admin/application_settings/_gitaly.html.haml index 859a1c6f45c..0b4001c0824 100644 --- a/app/views/admin/application_settings/_gitaly.html.haml +++ b/app/views/admin/application_settings/_gitaly.html.haml @@ -2,26 +2,23 @@ = form_errors(@application_setting) %fieldset - .form-group.row - = f.label :gitaly_timeout_default, 'Default Timeout Period', class: 'col-form-label col-sm-2' - .col-sm-10 - = f.number_field :gitaly_timeout_default, class: 'form-control' - .form-text.text-muted - Timeout for Gitaly calls from the GitLab application (in seconds). This timeout is not enforced - for git fetch/push operations or Sidekiq jobs. - .form-group.row - = f.label :gitaly_timeout_fast, 'Fast Timeout Period', class: 'col-form-label col-sm-2' - .col-sm-10 - = f.number_field :gitaly_timeout_fast, class: 'form-control' - .form-text.text-muted - Fast operation timeout (in seconds). Some Gitaly operations are expected to be fast. - If they exceed this threshold, there may be a problem with a storage shard and 'failing fast' - can help maintain the stability of the GitLab instance. - .form-group.row - = f.label :gitaly_timeout_medium, 'Medium Timeout Period', class: 'col-form-label col-sm-2' - .col-sm-10 - = f.number_field :gitaly_timeout_medium, class: 'form-control' - .form-text.text-muted - Medium operation timeout (in seconds). This should be a value between the Fast and the Default timeout. + .form-group + = f.label :gitaly_timeout_default, 'Default Timeout Period', class: 'label-light' + = f.number_field :gitaly_timeout_default, class: 'form-control' + .form-text.text-muted + Timeout for Gitaly calls from the GitLab application (in seconds). This timeout is not enforced + for git fetch/push operations or Sidekiq jobs. + .form-group + = f.label :gitaly_timeout_fast, 'Fast Timeout Period', class: 'label-light' + = f.number_field :gitaly_timeout_fast, class: 'form-control' + .form-text.text-muted + Fast operation timeout (in seconds). Some Gitaly operations are expected to be fast. + If they exceed this threshold, there may be a problem with a storage shard and 'failing fast' + can help maintain the stability of the GitLab instance. + .form-group + = f.label :gitaly_timeout_medium, 'Medium Timeout Period', class: 'label-light' + = f.number_field :gitaly_timeout_medium, class: 'form-control' + .form-text.text-muted + Medium operation timeout (in seconds). This should be a value between the Fast and the Default timeout. = f.submit 'Save changes', class: "btn btn-success" diff --git a/app/views/admin/application_settings/_help_page.html.haml b/app/views/admin/application_settings/_help_page.html.haml index 1f6c52d8b1a..1f402fcb786 100644 --- a/app/views/admin/application_settings/_help_page.html.haml +++ b/app/views/admin/application_settings/_help_page.html.haml @@ -2,21 +2,18 @@ = form_errors(@application_setting) %fieldset - .form-group.row - = f.label :help_page_text, class: 'col-form-label col-sm-2' - .col-sm-10 - = f.text_area :help_page_text, class: 'form-control', rows: 4 - .form-text.text-muted Markdown enabled - .form-group.row - .offset-sm-2.col-sm-10 - .form-check - = f.check_box :help_page_hide_commercial_content, class: 'form-check-input' - = f.label :help_page_hide_commercial_content, class: 'form-check-label' do - Hide marketing-related entries from help - .form-group.row - = f.label :help_page_support_url, 'Support page URL', class: 'col-form-label col-sm-2' - .col-sm-10 - = f.text_field :help_page_support_url, class: 'form-control', placeholder: 'http://company.example.com/getting-help', :'aria-describedby' => 'support_help_block' - %span.form-text.text-muted#support_help_block Alternate support URL for help page + .form-group + = f.label :help_page_text, class: 'label-light' + = f.text_area :help_page_text, class: 'form-control', rows: 4 + .form-text.text-muted Markdown enabled + .form-group + .form-check + = f.check_box :help_page_hide_commercial_content, class: 'form-check-input' + = f.label :help_page_hide_commercial_content, class: 'form-check-label' do + Hide marketing-related entries from help + .form-group + = f.label :help_page_support_url, 'Support page URL', class: 'label-light' + = f.text_field :help_page_support_url, class: 'form-control', placeholder: 'http://company.example.com/getting-help', :'aria-describedby' => 'support_help_block' + %span.form-text.text-muted#support_help_block Alternate support URL for help page = f.submit 'Save changes', class: "btn btn-success" diff --git a/app/views/admin/application_settings/_influx.html.haml b/app/views/admin/application_settings/_influx.html.haml index b40a714ed8f..61e8e3199a9 100644 --- a/app/views/admin/application_settings/_influx.html.haml +++ b/app/views/admin/application_settings/_influx.html.haml @@ -8,61 +8,53 @@ = link_to 'restart', help_page_path('administration/restart_gitlab') to take effect. = link_to icon('question-circle'), help_page_path('administration/monitoring/performance/introduction') - .form-group.row - .offset-sm-2.col-sm-10 - .form-check - = f.check_box :metrics_enabled, class: 'form-check-input' - = f.label :metrics_enabled, class: 'form-check-label' do - Enable InfluxDB Metrics - .form-group.row - = f.label :metrics_host, 'InfluxDB host', class: 'col-form-label col-sm-2' - .col-sm-10 - = f.text_field :metrics_host, class: 'form-control', placeholder: 'influxdb.example.com' - .form-group.row - = f.label :metrics_port, 'InfluxDB port', class: 'col-form-label col-sm-2' - .col-sm-10 - = f.text_field :metrics_port, class: 'form-control', placeholder: '8089' - .form-text.text-muted - The UDP port to use for connecting to InfluxDB. InfluxDB requires that - your server configuration specifies a database to store data in when - sending messages to this port, without it metrics data will not be - saved. - .form-group.row - = f.label :metrics_pool_size, 'Connection pool size', class: 'col-form-label col-sm-2' - .col-sm-10 - = f.number_field :metrics_pool_size, class: 'form-control' - .form-text.text-muted - The amount of InfluxDB connections to open. Connections are opened - lazily. Users using multi-threaded application servers should ensure - enough connections are available (at minimum the amount of application - server threads). - .form-group.row - = f.label :metrics_timeout, 'Connection timeout', class: 'col-form-label col-sm-2' - .col-sm-10 - = f.number_field :metrics_timeout, class: 'form-control' - .form-text.text-muted - The amount of seconds after which an InfluxDB connection will time - out. - .form-group.row - = f.label :metrics_method_call_threshold, 'Method Call Threshold (ms)', class: 'col-form-label col-sm-2' - .col-sm-10 - = f.number_field :metrics_method_call_threshold, class: 'form-control' - .form-text.text-muted - A method call is only tracked when it takes longer to complete than - the given amount of milliseconds. - .form-group.row - = f.label :metrics_sample_interval, 'Sampler Interval (sec)', class: 'col-form-label col-sm-2' - .col-sm-10 - = f.number_field :metrics_sample_interval, class: 'form-control' - .form-text.text-muted - The sampling interval in seconds. Sampled data includes memory usage, - retained Ruby objects, file descriptors and so on. - .form-group.row - = f.label :metrics_packet_size, 'Metrics per packet', class: 'col-form-label col-sm-2' - .col-sm-10 - = f.number_field :metrics_packet_size, class: 'form-control' - .form-text.text-muted - The amount of points to store in a single UDP packet. More points - results in fewer but larger UDP packets being sent. + .form-group + .form-check + = f.check_box :metrics_enabled, class: 'form-check-input' + = f.label :metrics_enabled, class: 'form-check-label' do + Enable InfluxDB Metrics + .form-group + = f.label :metrics_host, 'InfluxDB host', class: 'label-light' + = f.text_field :metrics_host, class: 'form-control', placeholder: 'influxdb.example.com' + .form-group + = f.label :metrics_port, 'InfluxDB port', class: 'label-light' + = f.text_field :metrics_port, class: 'form-control', placeholder: '8089' + .form-text.text-muted + The UDP port to use for connecting to InfluxDB. InfluxDB requires that + your server configuration specifies a database to store data in when + sending messages to this port, without it metrics data will not be + saved. + .form-group + = f.label :metrics_pool_size, 'Connection pool size', class: 'label-light' + = f.number_field :metrics_pool_size, class: 'form-control' + .form-text.text-muted + The amount of InfluxDB connections to open. Connections are opened + lazily. Users using multi-threaded application servers should ensure + enough connections are available (at minimum the amount of application + server threads). + .form-group + = f.label :metrics_timeout, 'Connection timeout', class: 'label-light' + = f.number_field :metrics_timeout, class: 'form-control' + .form-text.text-muted + The amount of seconds after which an InfluxDB connection will time + out. + .form-group + = f.label :metrics_method_call_threshold, 'Method Call Threshold (ms)', class: 'label-light' + = f.number_field :metrics_method_call_threshold, class: 'form-control' + .form-text.text-muted + A method call is only tracked when it takes longer to complete than + the given amount of milliseconds. + .form-group + = f.label :metrics_sample_interval, 'Sampler Interval (sec)', class: 'label-light' + = f.number_field :metrics_sample_interval, class: 'form-control' + .form-text.text-muted + The sampling interval in seconds. Sampled data includes memory usage, + retained Ruby objects, file descriptors and so on. + .form-group + = f.label :metrics_packet_size, 'Metrics per packet', class: 'label-light' + = f.number_field :metrics_packet_size, class: 'form-control' + .form-text.text-muted + The amount of points to store in a single UDP packet. More points + results in fewer but larger UDP packets being sent. = f.submit 'Save changes', class: "btn btn-success" diff --git a/app/views/admin/application_settings/_ip_limits.html.haml b/app/views/admin/application_settings/_ip_limits.html.haml index 320dd52ffc2..73d570a5fee 100644 --- a/app/views/admin/application_settings/_ip_limits.html.haml +++ b/app/views/admin/application_settings/_ip_limits.html.haml @@ -2,53 +2,44 @@ = form_errors(@application_setting) %fieldset - .form-group.row - .offset-sm-2.col-sm-10 - .form-check - = f.check_box :throttle_unauthenticated_enabled, class: 'form-check-input' - = f.label :throttle_unauthenticated_enabled, class: 'form-check-label' do - Enable unauthenticated request rate limit - %span.form-text.text-muted - Helps reduce request volume (e.g. from crawlers or abusive bots) - .form-group.row - = f.label :throttle_unauthenticated_requests_per_period, 'Max requests per period per IP', class: 'col-form-label col-sm-2' - .col-sm-10 - = f.number_field :throttle_unauthenticated_requests_per_period, class: 'form-control' - .form-group.row - = f.label :throttle_unauthenticated_period_in_seconds, 'Rate limit period in seconds', class: 'col-form-label col-sm-2' - .col-sm-10 - = f.number_field :throttle_unauthenticated_period_in_seconds, class: 'form-control' - .form-group.row - .offset-sm-2.col-sm-10 - .form-check - = f.check_box :throttle_authenticated_api_enabled, class: 'form-check-input' - = f.label :throttle_authenticated_api_enabled, class: 'form-check-label' do - Enable authenticated API request rate limit - %span.form-text.text-muted - Helps reduce request volume (e.g. from crawlers or abusive bots) - .form-group.row - = f.label :throttle_authenticated_api_requests_per_period, 'Max requests per period per user', class: 'col-form-label col-sm-2' - .col-sm-10 - = f.number_field :throttle_authenticated_api_requests_per_period, class: 'form-control' - .form-group.row - = f.label :throttle_authenticated_api_period_in_seconds, 'Rate limit period in seconds', class: 'col-form-label col-sm-2' - .col-sm-10 - = f.number_field :throttle_authenticated_api_period_in_seconds, class: 'form-control' - .form-group.row - .offset-sm-2.col-sm-10 - .form-check - = f.check_box :throttle_authenticated_web_enabled, class: 'form-check-input' - = f.label :throttle_authenticated_web_enabled, class: 'form-check-label' do - Enable authenticated web request rate limit - %span.form-text.text-muted - Helps reduce request volume (e.g. from crawlers or abusive bots) - .form-group.row - = f.label :throttle_authenticated_web_requests_per_period, 'Max requests per period per user', class: 'col-form-label col-sm-2' - .col-sm-10 - = f.number_field :throttle_authenticated_web_requests_per_period, class: 'form-control' - .form-group.row - = f.label :throttle_authenticated_web_period_in_seconds, 'Rate limit period in seconds', class: 'col-form-label col-sm-2' - .col-sm-10 - = f.number_field :throttle_authenticated_web_period_in_seconds, class: 'form-control' + .form-group + .form-check + = f.check_box :throttle_unauthenticated_enabled, class: 'form-check-input' + = f.label :throttle_unauthenticated_enabled, class: 'form-check-label' do + Enable unauthenticated request rate limit + %span.form-text.text-muted + Helps reduce request volume (e.g. from crawlers or abusive bots) + .form-group + = f.label :throttle_unauthenticated_requests_per_period, 'Max requests per period per IP', class: 'label-light' + = f.number_field :throttle_unauthenticated_requests_per_period, class: 'form-control' + .form-group + = f.label :throttle_unauthenticated_period_in_seconds, 'Rate limit period in seconds', class: 'label-light' + = f.number_field :throttle_unauthenticated_period_in_seconds, class: 'form-control' + .form-group + .form-check + = f.check_box :throttle_authenticated_api_enabled, class: 'form-check-input' + = f.label :throttle_authenticated_api_enabled, class: 'form-check-label' do + Enable authenticated API request rate limit + %span.form-text.text-muted + Helps reduce request volume (e.g. from crawlers or abusive bots) + .form-group + = f.label :throttle_authenticated_api_requests_per_period, 'Max requests per period per user', class: 'label-light' + = f.number_field :throttle_authenticated_api_requests_per_period, class: 'form-control' + .form-group + = f.label :throttle_authenticated_api_period_in_seconds, 'Rate limit period in seconds', class: 'label-light' + = f.number_field :throttle_authenticated_api_period_in_seconds, class: 'form-control' + .form-group + .form-check + = f.check_box :throttle_authenticated_web_enabled, class: 'form-check-input' + = f.label :throttle_authenticated_web_enabled, class: 'form-check-label' do + Enable authenticated web request rate limit + %span.form-text.text-muted + Helps reduce request volume (e.g. from crawlers or abusive bots) + .form-group + = f.label :throttle_authenticated_web_requests_per_period, 'Max requests per period per user', class: 'label-light' + = f.number_field :throttle_authenticated_web_requests_per_period, class: 'form-control' + .form-group + = f.label :throttle_authenticated_web_period_in_seconds, 'Rate limit period in seconds', class: 'label-light' + = f.number_field :throttle_authenticated_web_period_in_seconds, class: 'form-control' = f.submit 'Save changes', class: "btn btn-success" diff --git a/app/views/admin/application_settings/_koding.html.haml b/app/views/admin/application_settings/_koding.html.haml index 341c7641fcc..ae60f68f5fe 100644 --- a/app/views/admin/application_settings/_koding.html.haml +++ b/app/views/admin/application_settings/_koding.html.haml @@ -2,23 +2,21 @@ = form_errors(@application_setting) %fieldset - .form-group.row - .offset-sm-2.col-sm-10 - .form-check - = f.check_box :koding_enabled, class: 'form-check-input' - = f.label :koding_enabled, class: 'form-check-label' do - Enable Koding - .form-text.text-muted - Koding integration has been deprecated since GitLab 10.0. If you disable your Koding integration, you will not be able to enable it again. - .form-group.row - = f.label :koding_url, 'Koding URL', class: 'col-form-label col-sm-2' - .col-sm-10 - = f.text_field :koding_url, class: 'form-control', placeholder: 'http://gitlab.your-koding-instance.com:8090' - .form-text.text-muted - Koding has integration enabled out of the box for the - %strong gitlab - team, and you need to provide that team's URL here. Learn more in the - = succeed "." do - = link_to "Koding administration documentation", help_page_path("administration/integration/koding") + .form-group + .form-check + = f.check_box :koding_enabled, class: 'form-check-input' + = f.label :koding_enabled, class: 'form-check-label' do + Enable Koding + .form-text.text-muted + Koding integration has been deprecated since GitLab 10.0. If you disable your Koding integration, you will not be able to enable it again. + .form-group + = f.label :koding_url, 'Koding URL', class: 'label-light' + = f.text_field :koding_url, class: 'form-control', placeholder: 'http://gitlab.your-koding-instance.com:8090' + .form-text.text-muted + Koding has integration enabled out of the box for the + %strong gitlab + team, and you need to provide that team's URL here. Learn more in the + = succeed "." do + = link_to "Koding administration documentation", help_page_path("administration/integration/koding") = f.submit 'Save changes', class: "btn btn-success" diff --git a/app/views/admin/application_settings/_logging.html.haml b/app/views/admin/application_settings/_logging.html.haml index f5c1e126c70..a6e549cd1f0 100644 --- a/app/views/admin/application_settings/_logging.html.haml +++ b/app/views/admin/application_settings/_logging.html.haml @@ -2,35 +2,31 @@ = form_errors(@application_setting) %fieldset - .form-group.row - .offset-sm-2.col-sm-10 - .form-check - = f.check_box :sentry_enabled, class: 'form-check-input' - = f.label :sentry_enabled, class: 'form-check-label' do - Enable Sentry - .form-text.text-muted - %p This setting requires a restart to take effect. - Sentry is an error reporting and logging tool which is currently not shipped with GitLab, get it here: - %a{ href: 'https://getsentry.com', target: '_blank', rel: 'noopener noreferrer' } https://getsentry.com + .form-group + .form-check + = f.check_box :sentry_enabled, class: 'form-check-input' + = f.label :sentry_enabled, class: 'form-check-label' do + Enable Sentry + .form-text.text-muted + %p This setting requires a restart to take effect. + Sentry is an error reporting and logging tool which is currently not shipped with GitLab, get it here: + %a{ href: 'https://getsentry.com', target: '_blank', rel: 'noopener noreferrer' } https://getsentry.com - .form-group.row - = f.label :sentry_dsn, 'Sentry DSN', class: 'col-form-label col-sm-2' - .col-sm-10 - = f.text_field :sentry_dsn, class: 'form-control' + .form-group + = f.label :sentry_dsn, 'Sentry DSN', class: 'label-light' + = f.text_field :sentry_dsn, class: 'form-control' - .form-group.row - .offset-sm-2.col-sm-10 - .form-check - = f.check_box :clientside_sentry_enabled, class: 'form-check-input' - = f.label :clientside_sentry_enabled, class: 'form-check-label' do - Enable Clientside Sentry - .form-text.text-muted - Sentry can also be used for reporting and logging clientside exceptions. - %a{ href: 'https://sentry.io/for/javascript/', target: '_blank', rel: 'noopener noreferrer' } https://sentry.io/for/javascript/ + .form-group + .form-check + = f.check_box :clientside_sentry_enabled, class: 'form-check-input' + = f.label :clientside_sentry_enabled, class: 'form-check-label' do + Enable Clientside Sentry + .form-text.text-muted + Sentry can also be used for reporting and logging clientside exceptions. + %a{ href: 'https://sentry.io/for/javascript/', target: '_blank', rel: 'noopener noreferrer' } https://sentry.io/for/javascript/ - .form-group.row - = f.label :clientside_sentry_dsn, 'Clientside Sentry DSN', class: 'col-form-label col-sm-2' - .col-sm-10 - = f.text_field :clientside_sentry_dsn, class: 'form-control' + .form-group + = f.label :clientside_sentry_dsn, 'Clientside Sentry DSN', class: 'label-light' + = f.text_field :clientside_sentry_dsn, class: 'form-control' = f.submit 'Save changes', class: "btn btn-success" diff --git a/app/views/admin/application_settings/_outbound.html.haml b/app/views/admin/application_settings/_outbound.html.haml index 5dadb7b814b..e046242bee0 100644 --- a/app/views/admin/application_settings/_outbound.html.haml +++ b/app/views/admin/application_settings/_outbound.html.haml @@ -2,11 +2,10 @@ = form_errors(@application_setting) %fieldset - .form-group.row - .offset-sm-2.col-sm-10 - .form-check - = f.check_box :allow_local_requests_from_hooks_and_services, class: 'form-check-input' - = f.label :allow_local_requests_from_hooks_and_services, class: 'form-check-label' do - Allow requests to the local network from hooks and services + .form-group + .form-check + = f.check_box :allow_local_requests_from_hooks_and_services, class: 'form-check-input' + = f.label :allow_local_requests_from_hooks_and_services, class: 'form-check-label' do + Allow requests to the local network from hooks and services = f.submit 'Save changes', class: "btn btn-success" diff --git a/app/views/admin/application_settings/_pages.html.haml b/app/views/admin/application_settings/_pages.html.haml index f1889c3105f..f168ec62ffd 100644 --- a/app/views/admin/application_settings/_pages.html.haml +++ b/app/views/admin/application_settings/_pages.html.haml @@ -2,21 +2,19 @@ = form_errors(@application_setting) %fieldset - .form-group.row - = f.label :max_pages_size, 'Maximum size of pages (MB)', class: 'col-form-label col-sm-2' - .col-sm-10 - = f.number_field :max_pages_size, class: 'form-control' - .form-text.text-muted 0 for unlimited - .form-group.row - .offset-sm-2.col-sm-10 - .form-check - = f.check_box :pages_domain_verification_enabled, class: 'form-check-input' - = f.label :pages_domain_verification_enabled, class: 'form-check-label' do - Require users to prove ownership of custom domains - .form-text.text-muted - Domain verification is an essential security measure for public GitLab - sites. Users are required to demonstrate they control a domain before - it is enabled - = link_to icon('question-circle'), help_page_path('user/project/pages/getting_started_part_three.md', anchor: 'dns-txt-record') + .form-group + = f.label :max_pages_size, 'Maximum size of pages (MB)', class: 'label-light' + = f.number_field :max_pages_size, class: 'form-control' + .form-text.text-muted 0 for unlimited + .form-group + .form-check + = f.check_box :pages_domain_verification_enabled, class: 'form-check-input' + = f.label :pages_domain_verification_enabled, class: 'form-check-label' do + Require users to prove ownership of custom domains + .form-text.text-muted + Domain verification is an essential security measure for public GitLab + sites. Users are required to demonstrate they control a domain before + it is enabled + = link_to icon('question-circle'), help_page_path('user/project/pages/getting_started_part_three.md', anchor: 'dns-txt-record') = f.submit 'Save changes', class: "btn btn-success" diff --git a/app/views/admin/application_settings/_performance.html.haml b/app/views/admin/application_settings/_performance.html.haml index 57c22ce563f..ffa25af77ed 100644 --- a/app/views/admin/application_settings/_performance.html.haml +++ b/app/views/admin/application_settings/_performance.html.haml @@ -2,18 +2,17 @@ = form_errors(@application_setting) %fieldset - .form-group.row - .offset-sm-2.col-sm-10 - .form-check - = f.check_box :authorized_keys_enabled, class: 'form-check-input' - = f.label :authorized_keys_enabled, class: 'form-check-label' do - Write to "authorized_keys" file - .form-text.text-muted - By default, we write to the "authorized_keys" file to support Git - over SSH without additional configuration. GitLab can be optimized - to authenticate SSH keys via the database file. Only uncheck this - if you have configured your OpenSSH server to use the - AuthorizedKeysCommand. Click on the help icon for more details. - = link_to icon('question-circle'), help_page_path('administration/operations/fast_ssh_key_lookup') + .form-group + .form-check + = f.check_box :authorized_keys_enabled, class: 'form-check-input' + = f.label :authorized_keys_enabled, class: 'form-check-label' do + Write to "authorized_keys" file + .form-text.text-muted + By default, we write to the "authorized_keys" file to support Git + over SSH without additional configuration. GitLab can be optimized + to authenticate SSH keys via the database file. Only uncheck this + if you have configured your OpenSSH server to use the + AuthorizedKeysCommand. Click on the help icon for more details. + = link_to icon('question-circle'), help_page_path('administration/operations/fast_ssh_key_lookup') = f.submit 'Save changes', class: "btn btn-success" diff --git a/app/views/admin/application_settings/_performance_bar.html.haml b/app/views/admin/application_settings/_performance_bar.html.haml index ed4de2234f7..ddbfcc6b77b 100644 --- a/app/views/admin/application_settings/_performance_bar.html.haml +++ b/app/views/admin/application_settings/_performance_bar.html.haml @@ -2,15 +2,13 @@ = form_errors(@application_setting) %fieldset - .form-group.row - .offset-sm-2.col-sm-10 - .form-check - = f.check_box :performance_bar_enabled, class: 'form-check-input' - = f.label :performance_bar_enabled, class: 'form-check-label' do - Enable the Performance Bar - .form-group.row - = f.label :performance_bar_allowed_group_path, 'Allowed group', class: 'col-form-label col-sm-2' - .col-sm-10 - = f.text_field :performance_bar_allowed_group_path, class: 'form-control', placeholder: 'my-org/my-group', value: @application_setting.performance_bar_allowed_group&.full_path + .form-group + .form-check + = f.check_box :performance_bar_enabled, class: 'form-check-input' + = f.label :performance_bar_enabled, class: 'form-check-label' do + Enable the Performance Bar + .form-group + = f.label :performance_bar_allowed_group_path, 'Allowed group', class: 'label-light' + = f.text_field :performance_bar_allowed_group_path, class: 'form-control', placeholder: 'my-org/my-group', value: @application_setting.performance_bar_allowed_group&.full_path = f.submit 'Save changes', class: "btn btn-success" diff --git a/app/views/admin/application_settings/_plantuml.html.haml b/app/views/admin/application_settings/_plantuml.html.haml index e0dc058762e..259f18b3b96 100644 --- a/app/views/admin/application_settings/_plantuml.html.haml +++ b/app/views/admin/application_settings/_plantuml.html.haml @@ -2,19 +2,17 @@ = form_errors(@application_setting) %fieldset - .form-group.row - .offset-sm-2.col-sm-10 - .form-check - = f.check_box :plantuml_enabled, class: 'form-check-input' - = f.label :plantuml_enabled, class: 'form-check-label' do - Enable PlantUML - .form-group.row - = f.label :plantuml_url, 'PlantUML URL', class: 'col-form-label col-sm-2' - .col-sm-10 - = f.text_field :plantuml_url, class: 'form-control', placeholder: 'http://gitlab.your-plantuml-instance.com:8080' - .form-text.text-muted - Allow rendering of - = link_to "PlantUML", "http://plantuml.com" - diagrams in Asciidoc documents using an external PlantUML service. + .form-group + .form-check + = f.check_box :plantuml_enabled, class: 'form-check-input' + = f.label :plantuml_enabled, class: 'form-check-label' do + Enable PlantUML + .form-group + = f.label :plantuml_url, 'PlantUML URL', class: 'label-light' + = f.text_field :plantuml_url, class: 'form-control', placeholder: 'http://gitlab.your-plantuml-instance.com:8080' + .form-text.text-muted + Allow rendering of + = link_to "PlantUML", "http://plantuml.com" + diagrams in Asciidoc documents using an external PlantUML service. = f.submit 'Save changes', class: "btn btn-success" diff --git a/app/views/admin/application_settings/_prometheus.html.haml b/app/views/admin/application_settings/_prometheus.html.haml index d3c3656e96a..ad92b18b2c9 100644 --- a/app/views/admin/application_settings/_prometheus.html.haml +++ b/app/views/admin/application_settings/_prometheus.html.haml @@ -11,18 +11,17 @@ = link_to 'restart', help_page_path('administration/restart_gitlab') to take effect. = link_to icon('question-circle'), help_page_path('administration/monitoring/prometheus/index') - .form-group.row - .offset-sm-2.col-sm-10 - .form-check - = f.check_box :prometheus_metrics_enabled, class: 'form-check-input' - = f.label :prometheus_metrics_enabled, class: 'form-check-label' do - Enable Prometheus Metrics - - unless Gitlab::Metrics.metrics_folder_present? - .form-text.text-muted - %strong.cred WARNING: - Environment variable - %code prometheus_multiproc_dir - does not exist or is not pointing to a valid directory. - = link_to icon('question-circle'), help_page_path('administration/monitoring/prometheus/gitlab_metrics', anchor: 'metrics-shared-directory') + .form-group + .form-check + = f.check_box :prometheus_metrics_enabled, class: 'form-check-input' + = f.label :prometheus_metrics_enabled, class: 'form-check-label' do + Enable Prometheus Metrics + - unless Gitlab::Metrics.metrics_folder_present? + .form-text.text-muted + %strong.cred WARNING: + Environment variable + %code prometheus_multiproc_dir + does not exist or is not pointing to a valid directory. + = link_to icon('question-circle'), help_page_path('administration/monitoring/prometheus/gitlab_metrics', anchor: 'metrics-shared-directory') = f.submit 'Save changes', class: "btn btn-success" diff --git a/app/views/admin/application_settings/_realtime.html.haml b/app/views/admin/application_settings/_realtime.html.haml index 63a592cc2fd..120cf4909b2 100644 --- a/app/views/admin/application_settings/_realtime.html.haml +++ b/app/views/admin/application_settings/_realtime.html.haml @@ -2,18 +2,17 @@ = form_errors(@application_setting) %fieldset - .form-group.row - = f.label :polling_interval_multiplier, 'Polling interval multiplier', class: 'col-form-label col-sm-2' - .col-sm-10 - = f.text_field :polling_interval_multiplier, class: 'form-control' - .form-text.text-muted - Change this value to influence how frequently the GitLab UI polls for updates. - If you set the value to 2 all polling intervals are multiplied - by 2, which means that polling happens half as frequently. - The multiplier can also have a decimal value. - The default value (1) is a reasonable choice for the majority of GitLab - installations. Set to 0 to completely disable polling. - = link_to icon('question-circle'), help_page_path('administration/polling') + .form-group + = f.label :polling_interval_multiplier, 'Polling interval multiplier', class: 'label-light' + = f.text_field :polling_interval_multiplier, class: 'form-control' + .form-text.text-muted + Change this value to influence how frequently the GitLab UI polls for updates. + If you set the value to 2 all polling intervals are multiplied + by 2, which means that polling happens half as frequently. + The multiplier can also have a decimal value. + The default value (1) is a reasonable choice for the majority of GitLab + installations. Set to 0 to completely disable polling. + = link_to icon('question-circle'), help_page_path('administration/polling') = f.submit 'Save changes', class: "btn btn-success" diff --git a/app/views/admin/application_settings/_registry.html.haml b/app/views/admin/application_settings/_registry.html.haml index 8524cbfe4d9..beac70482e5 100644 --- a/app/views/admin/application_settings/_registry.html.haml +++ b/app/views/admin/application_settings/_registry.html.haml @@ -2,9 +2,8 @@ = form_errors(@application_setting) %fieldset - .form-group.row - = f.label :container_registry_token_expire_delay, 'Authorization token duration (minutes)', class: 'col-form-label col-sm-2' - .col-sm-10 - = f.number_field :container_registry_token_expire_delay, class: 'form-control' + .form-group + = f.label :container_registry_token_expire_delay, 'Authorization token duration (minutes)', class: 'label-light' + = f.number_field :container_registry_token_expire_delay, class: 'form-control' = f.submit 'Save changes', class: "btn btn-success" diff --git a/app/views/admin/application_settings/_repository_check.html.haml b/app/views/admin/application_settings/_repository_check.html.haml index 1311f17ecda..57facc380eb 100644 --- a/app/views/admin/application_settings/_repository_check.html.haml +++ b/app/views/admin/application_settings/_repository_check.html.haml @@ -4,59 +4,53 @@ %fieldset .sub-section %h4 Repository checks - .form-group.row - .offset-sm-2.col-sm-10 - .form-check - = f.check_box :repository_checks_enabled, class: 'form-check-input' - = f.label :repository_checks_enabled, class: 'form-check-label' do - Enable Repository Checks - .form-text.text-muted - GitLab will periodically run - %a{ href: 'https://git-scm.com/docs/git-fsck', target: 'blank' } 'git fsck' - in all project and wiki repositories to look for silent disk corruption issues. - .form-group.row - .offset-sm-2.col-sm-10 - = link_to 'Clear all repository checks', clear_repository_check_states_admin_application_settings_path, data: { confirm: 'This will clear repository check states for ALL projects in the database. This cannot be undone. Are you sure?' }, method: :put, class: "btn btn-sm btn-remove" + .form-group + .form-check + = f.check_box :repository_checks_enabled, class: 'form-check-input' + = f.label :repository_checks_enabled, class: 'form-check-label' do + Enable Repository Checks .form-text.text-muted - If you got a lot of false alarms from repository checks you can choose to clear all repository check information from the database. + GitLab will periodically run + %a{ href: 'https://git-scm.com/docs/git-fsck', target: 'blank' } 'git fsck' + in all project and wiki repositories to look for silent disk corruption issues. + .form-group + = link_to 'Clear all repository checks', clear_repository_check_states_admin_application_settings_path, data: { confirm: 'This will clear repository check states for ALL projects in the database. This cannot be undone. Are you sure?' }, method: :put, class: "btn btn-sm btn-remove" + .form-text.text-muted + If you got a lot of false alarms from repository checks you can choose to clear all repository check information from the database. .sub-section %h4 Housekeeping - .form-group.row - .offset-sm-2.col-sm-10 - .form-check - = f.check_box :housekeeping_enabled, class: 'form-check-input' - = f.label :housekeeping_enabled, class: 'form-check-label' do - Enable automatic repository housekeeping (git repack, git gc) - .form-text.text-muted - If you keep automatic housekeeping disabled for a long time Git - repository access on your GitLab server will become slower and your - repositories will use more disk space. We recommend to always leave - this enabled. - .form-check - = f.check_box :housekeeping_bitmaps_enabled, class: 'form-check-input' - = f.label :housekeeping_bitmaps_enabled, class: 'form-check-label' do - Enable Git pack file bitmap creation - .form-text.text-muted - Creating pack file bitmaps makes housekeeping take a little longer but - bitmaps should accelerate 'git clone' performance. - .form-group.row - = f.label :housekeeping_incremental_repack_period, 'Incremental repack period', class: 'col-form-label col-sm-2' - .col-sm-10 - = f.number_field :housekeeping_incremental_repack_period, class: 'form-control' + .form-group + .form-check + = f.check_box :housekeeping_enabled, class: 'form-check-input' + = f.label :housekeeping_enabled, class: 'form-check-label' do + Enable automatic repository housekeeping (git repack, git gc) .form-text.text-muted - Number of Git pushes after which an incremental 'git repack' is run. - .form-group.row - = f.label :housekeeping_full_repack_period, 'Full repack period', class: 'col-form-label col-sm-2' - .col-sm-10 - = f.number_field :housekeeping_full_repack_period, class: 'form-control' + If you keep automatic housekeeping disabled for a long time Git + repository access on your GitLab server will become slower and your + repositories will use more disk space. We recommend to always leave + this enabled. + .form-check + = f.check_box :housekeeping_bitmaps_enabled, class: 'form-check-input' + = f.label :housekeeping_bitmaps_enabled, class: 'form-check-label' do + Enable Git pack file bitmap creation .form-text.text-muted - Number of Git pushes after which a full 'git repack' is run. - .form-group.row - = f.label :housekeeping_gc_period, 'Git GC period', class: 'col-form-label col-sm-2' - .col-sm-10 - = f.number_field :housekeeping_gc_period, class: 'form-control' - .form-text.text-muted - Number of Git pushes after which 'git gc' is run. + Creating pack file bitmaps makes housekeeping take a little longer but + bitmaps should accelerate 'git clone' performance. + .form-group + = f.label :housekeeping_incremental_repack_period, 'Incremental repack period', class: 'label-light' + = f.number_field :housekeeping_incremental_repack_period, class: 'form-control' + .form-text.text-muted + Number of Git pushes after which an incremental 'git repack' is run. + .form-group + = f.label :housekeeping_full_repack_period, 'Full repack period', class: 'label-light' + = f.number_field :housekeeping_full_repack_period, class: 'form-control' + .form-text.text-muted + Number of Git pushes after which a full 'git repack' is run. + .form-group + = f.label :housekeeping_gc_period, 'Git GC period', class: 'label-light' + = f.number_field :housekeeping_gc_period, class: 'form-control' + .form-text.text-muted + Number of Git pushes after which 'git gc' is run. = f.submit 'Save changes', class: "btn btn-success" diff --git a/app/views/admin/application_settings/_repository_mirrors_form.html.haml b/app/views/admin/application_settings/_repository_mirrors_form.html.haml index 187c6c28bb1..beeb5169361 100644 --- a/app/views/admin/application_settings/_repository_mirrors_form.html.haml +++ b/app/views/admin/application_settings/_repository_mirrors_form.html.haml @@ -2,15 +2,14 @@ = form_errors(@application_setting) %fieldset - .form-group.row - = f.label :mirror_available, 'Enable mirror configuration', class: 'control-label col-sm-4' - .col-sm-8 - .form-check - = f.check_box :mirror_available, class: 'form-check-input' - = f.label :mirror_available, class: 'form-check-label' do - Allow mirrors to be setup for projects - %span.form-text.text-muted - If disabled, only admins will be able to setup mirrors in projects. - = link_to icon('question-circle'), help_page_path('workflow/repository_mirroring') + .form-group + = f.label :mirror_available, 'Enable mirror configuration', class: 'label-light' + .form-check + = f.check_box :mirror_available, class: 'form-check-input' + = f.label :mirror_available, class: 'form-check-label' do + Allow mirrors to be setup for projects + %span.form-text.text-muted + If disabled, only admins will be able to setup mirrors in projects. + = link_to icon('question-circle'), help_page_path('workflow/repository_mirroring') = f.submit 'Save changes', class: "btn btn-success" diff --git a/app/views/admin/application_settings/_repository_storage.html.haml b/app/views/admin/application_settings/_repository_storage.html.haml index 89d2c114b22..5a303666353 100644 --- a/app/views/admin/application_settings/_repository_storage.html.haml +++ b/app/views/admin/application_settings/_repository_storage.html.haml @@ -3,56 +3,49 @@ %fieldset .sub-section - .form-group.row - .offset-sm-2.col-sm-10 - .form-check - = f.check_box :hashed_storage_enabled, class: 'form-check-input' - = f.label :hashed_storage_enabled, class: 'form-check-label' do - Create new projects using hashed storage paths - .form-text.text-muted - Enable immutable, hash-based paths and repository names to store repositories on disk. This prevents - repositories from having to be moved or renamed when the Project URL changes and may improve disk I/O performance. - %em (EXPERIMENTAL) - .form-group.row - = f.label :repository_storages, 'Storage paths for new projects', class: 'col-form-label col-sm-2' - .col-sm-10 - = f.select :repository_storages, repository_storages_options_for_select(@application_setting.repository_storages), - {include_hidden: false}, multiple: true, class: 'form-control' + .form-group + .form-check + = f.check_box :hashed_storage_enabled, class: 'form-check-input' + = f.label :hashed_storage_enabled, class: 'form-check-label' do + Create new projects using hashed storage paths .form-text.text-muted - Manage repository storage paths. Learn more in the - = succeed "." do - = link_to "repository storages documentation", help_page_path("administration/repository_storage_paths") + Enable immutable, hash-based paths and repository names to store repositories on disk. This prevents + repositories from having to be moved or renamed when the Project URL changes and may improve disk I/O performance. + %em (EXPERIMENTAL) + .form-group + = f.label :repository_storages, 'Storage paths for new projects', class: 'label-light' + = f.select :repository_storages, repository_storages_options_for_select(@application_setting.repository_storages), + {include_hidden: false}, multiple: true, class: 'form-control' + .form-text.text-muted + Manage repository storage paths. Learn more in the + = succeed "." do + = link_to "repository storages documentation", help_page_path("administration/repository_storage_paths") .sub-section %h4 Circuit breaker - .form-group.row - = f.label :circuitbreaker_check_interval, _('Check interval'), class: 'col-form-label col-sm-2' - .col-sm-10 - = f.number_field :circuitbreaker_check_interval, class: 'form-control' - .form-text.text-muted - = circuitbreaker_check_interval_help_text - .form-group.row - = f.label :circuitbreaker_access_retries, _('Number of access attempts'), class: 'col-form-label col-sm-2' - .col-sm-10 - = f.number_field :circuitbreaker_access_retries, class: 'form-control' - .form-text.text-muted - = circuitbreaker_access_retries_help_text - .form-group.row - = f.label :circuitbreaker_storage_timeout, _('Seconds to wait for a storage access attempt'), class: 'col-form-label col-sm-2' - .col-sm-10 - = f.number_field :circuitbreaker_storage_timeout, class: 'form-control' - .form-text.text-muted - = circuitbreaker_storage_timeout_help_text - .form-group.row - = f.label :circuitbreaker_failure_count_threshold, _('Maximum git storage failures'), class: 'col-form-label col-sm-2' - .col-sm-10 - = f.number_field :circuitbreaker_failure_count_threshold, class: 'form-control' - .form-text.text-muted - = circuitbreaker_failure_count_help_text - .form-group.row - = f.label :circuitbreaker_failure_reset_time, _('Seconds before reseting failure information'), class: 'col-form-label col-sm-2' - .col-sm-10 - = f.number_field :circuitbreaker_failure_reset_time, class: 'form-control' - .form-text.text-muted - = circuitbreaker_failure_reset_time_help_text + .form-group + = f.label :circuitbreaker_check_interval, _('Check interval'), class: 'label-light' + = f.number_field :circuitbreaker_check_interval, class: 'form-control' + .form-text.text-muted + = circuitbreaker_check_interval_help_text + .form-group + = f.label :circuitbreaker_access_retries, _('Number of access attempts'), class: 'label-light' + = f.number_field :circuitbreaker_access_retries, class: 'form-control' + .form-text.text-muted + = circuitbreaker_access_retries_help_text + .form-group + = f.label :circuitbreaker_storage_timeout, _('Seconds to wait for a storage access attempt'), class: 'label-light' + = f.number_field :circuitbreaker_storage_timeout, class: 'form-control' + .form-text.text-muted + = circuitbreaker_storage_timeout_help_text + .form-group + = f.label :circuitbreaker_failure_count_threshold, _('Maximum git storage failures'), class: 'label-light' + = f.number_field :circuitbreaker_failure_count_threshold, class: 'form-control' + .form-text.text-muted + = circuitbreaker_failure_count_help_text + .form-group + = f.label :circuitbreaker_failure_reset_time, _('Seconds before reseting failure information'), class: 'label-light' + = f.number_field :circuitbreaker_failure_reset_time, class: 'form-control' + .form-text.text-muted + = circuitbreaker_failure_reset_time_help_text = f.submit 'Save changes', class: "btn btn-success" diff --git a/app/views/admin/application_settings/_signin.html.haml b/app/views/admin/application_settings/_signin.html.haml index 2ba26158162..69d1a43c511 100644 --- a/app/views/admin/application_settings/_signin.html.haml +++ b/app/views/admin/application_settings/_signin.html.haml @@ -2,59 +2,51 @@ = form_errors(@application_setting) %fieldset - .form-group.row - .offset-sm-2.col-sm-10 - .form-check - = f.check_box :password_authentication_enabled_for_web, class: 'form-check-input' - = f.label :password_authentication_enabled_for_web, class: 'form-check-label' do - Password authentication enabled for web interface - .form-text.text-muted - When disabled, an external authentication provider must be used. - .form-group.row - .offset-sm-2.col-sm-10 - .form-check - = f.check_box :password_authentication_enabled_for_git, class: 'form-check-input' - = f.label :password_authentication_enabled_for_git, class: 'form-check-label' do - Password authentication enabled for Git over HTTP(S) - .form-text.text-muted - When disabled, a Personal Access Token - - if Gitlab::Auth::LDAP::Config.enabled? - or LDAP password - must be used to authenticate. + .form-group + .form-check + = f.check_box :password_authentication_enabled_for_web, class: 'form-check-input' + = f.label :password_authentication_enabled_for_web, class: 'form-check-label' do + Password authentication enabled for web interface + .form-text.text-muted + When disabled, an external authentication provider must be used. + .form-group + .form-check + = f.check_box :password_authentication_enabled_for_git, class: 'form-check-input' + = f.label :password_authentication_enabled_for_git, class: 'form-check-label' do + Password authentication enabled for Git over HTTP(S) + .form-text.text-muted + When disabled, a Personal Access Token + - if Gitlab::Auth::LDAP::Config.enabled? + or LDAP password + must be used to authenticate. - if omniauth_enabled? && button_based_providers.any? - .form-group.row - = f.label :enabled_oauth_sign_in_sources, 'Enabled OAuth sign-in sources', class: 'col-form-label col-sm-2' + .form-group + = f.label :enabled_oauth_sign_in_sources, 'Enabled OAuth sign-in sources', class: 'label-light' = hidden_field_tag 'application_setting[enabled_oauth_sign_in_sources][]' - .col-sm-10 - .btn-group{ data: { toggle: 'buttons' } } - - oauth_providers_checkboxes.each do |source| - = source - .form-group.row - = f.label :two_factor_authentication, 'Two-factor authentication', class: 'col-form-label col-sm-2' - .col-sm-10 - .form-check - = f.check_box :require_two_factor_authentication, class: 'form-check-input' - = f.label :require_two_factor_authentication, class: 'form-check-label' do - Require all users to setup Two-factor authentication - .form-group.row - = f.label :two_factor_authentication, 'Two-factor grace period (hours)', class: 'col-form-label col-sm-2' - .col-sm-10 - = f.number_field :two_factor_grace_period, min: 0, class: 'form-control', placeholder: '0' - .form-text.text-muted Amount of time (in hours) that users are allowed to skip forced configuration of two-factor authentication - .form-group.row - = f.label :home_page_url, 'Home page URL', class: 'col-form-label col-sm-2' - .col-sm-10 - = f.text_field :home_page_url, class: 'form-control', placeholder: 'http://company.example.com', :'aria-describedby' => 'home_help_block' - %span.form-text.text-muted#home_help_block We will redirect non-logged in users to this page - .form-group.row - = f.label :after_sign_out_path, class: 'col-form-label col-sm-2' - .col-sm-10 - = f.text_field :after_sign_out_path, class: 'form-control', placeholder: 'http://company.example.com', :'aria-describedby' => 'after_sign_out_path_help_block' - %span.form-text.text-muted#after_sign_out_path_help_block We will redirect users to this page after they sign out - .form-group.row - = f.label :sign_in_text, class: 'col-form-label col-sm-2' - .col-sm-10 - = f.text_area :sign_in_text, class: 'form-control', rows: 4 - .form-text.text-muted Markdown enabled + .btn-group{ data: { toggle: 'buttons' } } + - oauth_providers_checkboxes.each do |source| + = source + .form-group + = f.label :two_factor_authentication, 'Two-factor authentication', class: 'label-light' + .form-check + = f.check_box :require_two_factor_authentication, class: 'form-check-input' + = f.label :require_two_factor_authentication, class: 'form-check-label' do + Require all users to setup Two-factor authentication + .form-group + = f.label :two_factor_authentication, 'Two-factor grace period (hours)', class: 'label-light' + = f.number_field :two_factor_grace_period, min: 0, class: 'form-control', placeholder: '0' + .form-text.text-muted Amount of time (in hours) that users are allowed to skip forced configuration of two-factor authentication + .form-group + = f.label :home_page_url, 'Home page URL', class: 'label-light' + = f.text_field :home_page_url, class: 'form-control', placeholder: 'http://company.example.com', :'aria-describedby' => 'home_help_block' + %span.form-text.text-muted#home_help_block We will redirect non-logged in users to this page + .form-group + = f.label :after_sign_out_path, class: 'label-light' + = f.text_field :after_sign_out_path, class: 'form-control', placeholder: 'http://company.example.com', :'aria-describedby' => 'after_sign_out_path_help_block' + %span.form-text.text-muted#after_sign_out_path_help_block We will redirect users to this page after they sign out + .form-group + = f.label :sign_in_text, class: 'label-light' + = f.text_area :sign_in_text, class: 'form-control', rows: 4 + .form-text.text-muted Markdown enabled = f.submit 'Save changes', class: "btn btn-success" diff --git a/app/views/admin/application_settings/_signup.html.haml b/app/views/admin/application_settings/_signup.html.haml index 279f96389e9..b9ba9128cc9 100644 --- a/app/views/admin/application_settings/_signup.html.haml +++ b/app/views/admin/application_settings/_signup.html.haml @@ -2,57 +2,49 @@ = form_errors(@application_setting) %fieldset - .form-group.row - .offset-sm-2.col-sm-10 - .form-check - = f.check_box :signup_enabled, class: 'form-check-input' - = f.label :signup_enabled, class: 'form-check-label' do - Sign-up enabled - .form-group.row - .offset-sm-2.col-sm-10 - .form-check - = f.check_box :send_user_confirmation_email, class: 'form-check-input' - = f.label :send_user_confirmation_email, class: 'form-check-label' do - Send confirmation email on sign-up - .form-group.row - = f.label :domain_whitelist, 'Whitelisted domains for sign-ups', class: 'col-form-label col-sm-2' - .col-sm-10 - = f.text_area :domain_whitelist_raw, placeholder: 'domain.com', class: 'form-control', rows: 8 - .form-text.text-muted ONLY users with e-mail addresses that match these domain(s) will be able to sign-up. Wildcards allowed. Use separate lines for multiple entries. Ex: domain.com, *.domain.com - .form-group.row - = f.label :domain_blacklist_enabled, 'Domain Blacklist', class: 'col-form-label col-sm-2' - .col-sm-10 - .form-check - = f.check_box :domain_blacklist_enabled, class: 'form-check-input' - = f.label :domain_blacklist_enabled, class: 'form-check-label' do - Enable domain blacklist for sign ups - .form-group.row - .offset-sm-2.col-sm-10 - .form-check - = radio_button_tag :blacklist_type, :file, class: 'form-check-input' - = label_tag :blacklist_type_file, class: 'form-check-label' do - .option-title - Upload blacklist file - .form-check - = radio_button_tag :blacklist_type, :raw, @application_setting.domain_blacklist.present? || @application_setting.domain_blacklist.blank?, class: 'form-check-input' - = label_tag :blacklist_type_raw, class: 'form-check-label' do - .option-title - Enter blacklist manually - .form-group.row.blacklist-file - = f.label :domain_blacklist_file, 'Blacklist file', class: 'col-form-label col-sm-2' - .col-sm-10 - = f.file_field :domain_blacklist_file, class: 'form-control', accept: '.txt,.conf' - .form-text.text-muted Users with e-mail addresses that match these domain(s) will NOT be able to sign-up. Wildcards allowed. Use separate lines or commas for multiple entries. - .form-group.row.blacklist-raw - = f.label :domain_blacklist, 'Blacklisted domains for sign-ups', class: 'col-form-label col-sm-2' - .col-sm-10 - = f.text_area :domain_blacklist_raw, placeholder: 'domain.com', class: 'form-control', rows: 8 - .form-text.text-muted Users with e-mail addresses that match these domain(s) will NOT be able to sign-up. Wildcards allowed. Use separate lines for multiple entries. Ex: domain.com, *.domain.com + .form-group + .form-check + = f.check_box :signup_enabled, class: 'form-check-input' + = f.label :signup_enabled, class: 'form-check-label' do + Sign-up enabled + .form-group + .form-check + = f.check_box :send_user_confirmation_email, class: 'form-check-input' + = f.label :send_user_confirmation_email, class: 'form-check-label' do + Send confirmation email on sign-up + .form-group + = f.label :domain_whitelist, 'Whitelisted domains for sign-ups', class: 'label-light' + = f.text_area :domain_whitelist_raw, placeholder: 'domain.com', class: 'form-control', rows: 8 + .form-text.text-muted ONLY users with e-mail addresses that match these domain(s) will be able to sign-up. Wildcards allowed. Use separate lines for multiple entries. Ex: domain.com, *.domain.com + .form-group + = f.label :domain_blacklist_enabled, 'Domain Blacklist', class: 'label-light' + .form-check + = f.check_box :domain_blacklist_enabled, class: 'form-check-input' + = f.label :domain_blacklist_enabled, class: 'form-check-label' do + Enable domain blacklist for sign ups + .form-group + .form-check + = radio_button_tag :blacklist_type, :file, false, class: 'form-check-input' + = label_tag :blacklist_type_file, class: 'form-check-label' do + .option-title + Upload blacklist file + .form-check + = radio_button_tag :blacklist_type, :raw, @application_setting.domain_blacklist.present? || @application_setting.domain_blacklist.blank?, class: 'form-check-input' + = label_tag :blacklist_type_raw, class: 'form-check-label' do + .option-title + Enter blacklist manually + .form-group.blacklist-file + = f.label :domain_blacklist_file, 'Blacklist file', class: 'label-light' + = f.file_field :domain_blacklist_file, class: 'form-control', accept: '.txt,.conf' + .form-text.text-muted Users with e-mail addresses that match these domain(s) will NOT be able to sign-up. Wildcards allowed. Use separate lines or commas for multiple entries. + .form-group.blacklist-raw + = f.label :domain_blacklist, 'Blacklisted domains for sign-ups', class: 'label-light' + = f.text_area :domain_blacklist_raw, placeholder: 'domain.com', class: 'form-control', rows: 8 + .form-text.text-muted Users with e-mail addresses that match these domain(s) will NOT be able to sign-up. Wildcards allowed. Use separate lines for multiple entries. Ex: domain.com, *.domain.com - .form-group.row - = f.label :after_sign_up_text, class: 'col-form-label col-sm-2' - .col-sm-10 - = f.text_area :after_sign_up_text, class: 'form-control', rows: 4 - .form-text.text-muted Markdown enabled + .form-group + = f.label :after_sign_up_text, class: 'label-light' + = f.text_area :after_sign_up_text, class: 'form-control', rows: 4 + .form-text.text-muted Markdown enabled = f.submit 'Save changes', class: "btn btn-success" diff --git a/app/views/admin/application_settings/_spam.html.haml b/app/views/admin/application_settings/_spam.html.haml index fb38e4ae922..8f0dce962a9 100644 --- a/app/views/admin/application_settings/_spam.html.haml +++ b/app/views/admin/application_settings/_spam.html.haml @@ -2,64 +2,56 @@ = form_errors(@application_setting) %fieldset - .form-group.row - .offset-sm-2.col-sm-10 - .form-check - = f.check_box :recaptcha_enabled, class: 'form-check-input' - = f.label :recaptcha_enabled, class: 'form-check-label' do - Enable reCAPTCHA - %span.form-text.text-muted#recaptcha_help_block Helps prevent bots from creating accounts - - .form-group.row - = f.label :recaptcha_site_key, 'reCAPTCHA Site Key', class: 'col-form-label col-sm-2' - .col-sm-10 - = f.text_field :recaptcha_site_key, class: 'form-control' - .form-text.text-muted - Generate site and private keys at - %a{ href: 'http://www.google.com/recaptcha', target: 'blank' } http://www.google.com/recaptcha - - .form-group.row - = f.label :recaptcha_private_key, 'reCAPTCHA Private Key', class: 'col-form-label col-sm-2' - .col-sm-10 - = f.text_field :recaptcha_private_key, class: 'form-control' - - .form-group.row - .offset-sm-2.col-sm-10 - .form-check - = f.check_box :akismet_enabled, class: 'form-check-input' - = f.label :akismet_enabled, class: 'form-check-label' do - Enable Akismet - %span.form-text.text-muted#akismet_help_block Helps prevent bots from creating issues - - .form-group.row - = f.label :akismet_api_key, 'Akismet API Key', class: 'col-form-label col-sm-2' - .col-sm-10 - = f.text_field :akismet_api_key, class: 'form-control' - .form-text.text-muted - Generate API key at - %a{ href: 'http://www.akismet.com', target: 'blank' } http://www.akismet.com - - .form-group.row - .offset-sm-2.col-sm-10 - .form-check - = f.check_box :unique_ips_limit_enabled, class: 'form-check-input' - = f.label :unique_ips_limit_enabled, class: 'form-check-label' do - Limit sign in from multiple ips - %span.form-text.text-muted#unique_ip_help_block - Helps prevent malicious users hide their activity - - .form-group.row - = f.label :unique_ips_limit_per_user, 'IPs per user', class: 'col-form-label col-sm-2' - .col-sm-10 - = f.number_field :unique_ips_limit_per_user, class: 'form-control' - .form-text.text-muted - Maximum number of unique IPs per user - - .form-group.row - = f.label :unique_ips_limit_time_window, 'IP expiration time', class: 'col-form-label col-sm-2' - .col-sm-10 - = f.number_field :unique_ips_limit_time_window, class: 'form-control' - .form-text.text-muted - How many seconds an IP will be counted towards the limit + .form-group + .form-check + = f.check_box :recaptcha_enabled, class: 'form-check-input' + = f.label :recaptcha_enabled, class: 'form-check-label' do + Enable reCAPTCHA + %span.form-text.text-muted#recaptcha_help_block Helps prevent bots from creating accounts + + .form-group + = f.label :recaptcha_site_key, 'reCAPTCHA Site Key', class: 'label-light' + = f.text_field :recaptcha_site_key, class: 'form-control' + .form-text.text-muted + Generate site and private keys at + %a{ href: 'http://www.google.com/recaptcha', target: 'blank' } http://www.google.com/recaptcha + + .form-group + = f.label :recaptcha_private_key, 'reCAPTCHA Private Key', class: 'label-light' + = f.text_field :recaptcha_private_key, class: 'form-control' + + .form-group + .form-check + = f.check_box :akismet_enabled, class: 'form-check-input' + = f.label :akismet_enabled, class: 'form-check-label' do + Enable Akismet + %span.form-text.text-muted#akismet_help_block Helps prevent bots from creating issues + + .form-group + = f.label :akismet_api_key, 'Akismet API Key', class: 'label-light' + = f.text_field :akismet_api_key, class: 'form-control' + .form-text.text-muted + Generate API key at + %a{ href: 'http://www.akismet.com', target: 'blank' } http://www.akismet.com + + .form-group + .form-check + = f.check_box :unique_ips_limit_enabled, class: 'form-check-input' + = f.label :unique_ips_limit_enabled, class: 'form-check-label' do + Limit sign in from multiple ips + %span.form-text.text-muted#unique_ip_help_block + Helps prevent malicious users hide their activity + + .form-group + = f.label :unique_ips_limit_per_user, 'IPs per user', class: 'label-light' + = f.number_field :unique_ips_limit_per_user, class: 'form-control' + .form-text.text-muted + Maximum number of unique IPs per user + + .form-group + = f.label :unique_ips_limit_time_window, 'IP expiration time', class: 'label-light' + = f.number_field :unique_ips_limit_time_window, class: 'form-control' + .form-text.text-muted + How many seconds an IP will be counted towards the limit = f.submit 'Save changes', class: "btn btn-success" diff --git a/app/views/admin/application_settings/_terminal.html.haml b/app/views/admin/application_settings/_terminal.html.haml index ae02d07e556..543628ff0ee 100644 --- a/app/views/admin/application_settings/_terminal.html.haml +++ b/app/views/admin/application_settings/_terminal.html.haml @@ -2,12 +2,11 @@ = form_errors(@application_setting) %fieldset - .form-group.row - = f.label :terminal_max_session_time, 'Max session time', class: 'col-form-label col-sm-2' - .col-sm-10 - = f.number_field :terminal_max_session_time, class: 'form-control' - .form-text.text-muted - Maximum time for web terminal websocket connection (in seconds). - 0 for unlimited. + .form-group + = f.label :terminal_max_session_time, 'Max session time', class: 'label-light' + = f.number_field :terminal_max_session_time, class: 'form-control' + .form-text.text-muted + Maximum time for web terminal websocket connection (in seconds). + 0 for unlimited. = f.submit 'Save changes', class: "btn btn-success" diff --git a/app/views/admin/application_settings/_terms.html.haml b/app/views/admin/application_settings/_terms.html.haml index 257565ce193..d3dc8659d1b 100644 --- a/app/views/admin/application_settings/_terms.html.haml +++ b/app/views/admin/application_settings/_terms.html.haml @@ -1,22 +1,19 @@ -= form_for @application_setting, url: admin_application_settings_path do |f| += form_for @application_setting, url: admin_application_settings_path, html: { class: 'fieldset-form' } do |f| = form_errors(@application_setting) %fieldset - .form-group.row - .col-sm-12 - .form-check - = f.check_box :enforce_terms, class: 'form-check-input' - = f.label :enforce_terms, class: 'form-check-label' do - = _("Require all users to accept Terms of Service when they access GitLab.") - .form-text.text-muted - = _("When enabled, users cannot use GitLab until the terms have been accepted.") - .form-group.row - .col-sm-12 - = f.label :terms do - = _("Terms of Service Agreement") - .col-sm-12 - = f.text_area :terms, class: 'form-control', rows: 8 + .form-group + .form-check + = f.check_box :enforce_terms, class: 'form-check-input' + = f.label :enforce_terms, class: 'form-check-label' do + = _("Require all users to accept Terms of Service and Privacy Policy when they access GitLab.") .form-text.text-muted - = _("Markdown enabled") + = _("When enabled, users cannot use GitLab until the terms have been accepted.") + .form-group + = f.label :terms do + = _("Terms of Service Agreement and Privacy Policy") + = f.text_area :terms, class: 'form-control', rows: 8 + .form-text.text-muted + = _("Markdown enabled") = f.submit _("Save changes"), class: "btn btn-success" diff --git a/app/views/admin/application_settings/_usage.html.haml b/app/views/admin/application_settings/_usage.html.haml index c110fd4d60d..49a3ee33a85 100644 --- a/app/views/admin/application_settings/_usage.html.haml +++ b/app/views/admin/application_settings/_usage.html.haml @@ -2,36 +2,34 @@ = form_errors(@application_setting) %fieldset - .form-group.row - .offset-sm-2.col-sm-10 - .form-check - = f.check_box :version_check_enabled, class: 'form-check-input' - = f.label :version_check_enabled, class: 'form-check-label' do - Enable version check - .form-text.text-muted - GitLab will inform you if a new version is available. - = link_to 'Learn more', help_page_path("user/admin_area/settings/usage_statistics", anchor: "version-check") - about what information is shared with GitLab Inc. - .form-group.row - .offset-sm-2.col-sm-10 - - can_be_configured = @application_setting.usage_ping_can_be_configured? - .form-check - = f.check_box :usage_ping_enabled, disabled: !can_be_configured, class: 'form-check-input' - = f.label :usage_ping_enabled, class: 'form-check-label' do - Enable usage ping - .form-text.text-muted - - if can_be_configured - To help improve GitLab and its user experience, GitLab will - periodically collect usage information. - = link_to 'Learn more', help_page_path("user/admin_area/settings/usage_statistics", anchor: "usage-ping") - about what information is shared with GitLab Inc. Visit - = link_to 'Cohorts', admin_cohorts_path(anchor: 'usage-ping') - to see the JSON payload sent. - - else - The usage ping is disabled, and cannot be configured through this - form. For more information, see the documentation on - = succeed '.' do - = link_to 'deactivating the usage ping', help_page_path('user/admin_area/settings/usage_statistics', anchor: 'deactivate-the-usage-ping') + .form-group + .form-check + = f.check_box :version_check_enabled, class: 'form-check-input' + = f.label :version_check_enabled, class: 'form-check-label' do + Enable version check + .form-text.text-muted + GitLab will inform you if a new version is available. + = link_to 'Learn more', help_page_path("user/admin_area/settings/usage_statistics", anchor: "version-check") + about what information is shared with GitLab Inc. + .form-group + - can_be_configured = @application_setting.usage_ping_can_be_configured? + .form-check + = f.check_box :usage_ping_enabled, disabled: !can_be_configured, class: 'form-check-input' + = f.label :usage_ping_enabled, class: 'form-check-label' do + Enable usage ping + .form-text.text-muted + - if can_be_configured + To help improve GitLab and its user experience, GitLab will + periodically collect usage information. + = link_to 'Learn more', help_page_path("user/admin_area/settings/usage_statistics", anchor: "usage-ping") + about what information is shared with GitLab Inc. Visit + = link_to 'Cohorts', admin_cohorts_path(anchor: 'usage-ping') + to see the JSON payload sent. + - else + The usage ping is disabled, and cannot be configured through this + form. For more information, see the documentation on + = succeed '.' do + = link_to 'deactivating the usage ping', help_page_path('user/admin_area/settings/usage_statistics', anchor: 'deactivate-the-usage-ping') = f.submit 'Save changes', class: "btn btn-success" diff --git a/app/views/admin/application_settings/_visibility_and_access.html.haml b/app/views/admin/application_settings/_visibility_and_access.html.haml index 2b4d3bab54d..4cc3e6a7d03 100644 --- a/app/views/admin/application_settings/_visibility_and_access.html.haml +++ b/app/views/admin/application_settings/_visibility_and_access.html.haml @@ -2,66 +2,57 @@ = form_errors(@application_setting) %fieldset - .form-group.row - = f.label :default_branch_protection, class: 'col-form-label col-sm-2' - .col-sm-10 - = f.select :default_branch_protection, options_for_select(Gitlab::Access.protection_options, @application_setting.default_branch_protection), {}, class: 'form-control' - .form-group.row.visibility-level-setting - = f.label :default_project_visibility, class: 'col-form-label col-sm-2' - .col-sm-10 - = render('shared/visibility_radios', model_method: :default_project_visibility, form: f, selected_level: @application_setting.default_project_visibility, form_model: Project.new) - .form-group.row.visibility-level-setting - = f.label :default_snippet_visibility, class: 'col-form-label col-sm-2' - .col-sm-10 - = render('shared/visibility_radios', model_method: :default_snippet_visibility, form: f, selected_level: @application_setting.default_snippet_visibility, form_model: ProjectSnippet.new) - .form-group.row.visibility-level-setting - = f.label :default_group_visibility, class: 'col-form-label col-sm-2' - .col-sm-10 - = render('shared/visibility_radios', model_method: :default_group_visibility, form: f, selected_level: @application_setting.default_group_visibility, form_model: Group.new) - .form-group.row - = f.label :restricted_visibility_levels, class: 'col-form-label col-sm-2' - .col-sm-10 - - checkbox_name = 'application_setting[restricted_visibility_levels][]' - = hidden_field_tag(checkbox_name) - - restricted_level_checkboxes('restricted-visibility-help', checkbox_name).each do |level| - .form-check - = level - %span.form-text.text-muted#restricted-visibility-help - Selected levels cannot be used by non-admin users for groups, projects or snippets. - If the public level is restricted, user profiles are only visible to logged in users. - .form-group.row - = f.label :import_sources, class: 'col-form-label col-sm-2' - .col-sm-10 - = hidden_field_tag 'application_setting[import_sources][]' - - import_sources_checkboxes('import-sources-help').each do |source| - .form-check= source - %span.form-text.text-muted#import-sources-help - Enabled sources for code import during project creation. OmniAuth must be configured for GitHub - = link_to "(?)", help_page_path("integration/github") - , Bitbucket - = link_to "(?)", help_page_path("integration/bitbucket") - and GitLab.com - = link_to "(?)", help_page_path("integration/gitlab") - - .form-group.row - .offset-sm-2.col-sm-10 + .form-group + = f.label :default_branch_protection, class: 'label-light' + = f.select :default_branch_protection, options_for_select(Gitlab::Access.protection_options, @application_setting.default_branch_protection), {}, class: 'form-control' + .form-group.visibility-level-setting + = f.label :default_project_visibility, class: 'label-light' + = render('shared/visibility_radios', model_method: :default_project_visibility, form: f, selected_level: @application_setting.default_project_visibility, form_model: Project.new) + .form-group.visibility-level-setting + = f.label :default_snippet_visibility, class: 'label-light' + = render('shared/visibility_radios', model_method: :default_snippet_visibility, form: f, selected_level: @application_setting.default_snippet_visibility, form_model: ProjectSnippet.new) + .form-group.visibility-level-setting + = f.label :default_group_visibility, class: 'label-light' + = render('shared/visibility_radios', model_method: :default_group_visibility, form: f, selected_level: @application_setting.default_group_visibility, form_model: Group.new) + .form-group + = f.label :restricted_visibility_levels, class: 'label-light' + - checkbox_name = 'application_setting[restricted_visibility_levels][]' + = hidden_field_tag(checkbox_name) + - restricted_level_checkboxes('restricted-visibility-help', checkbox_name, class: 'form-check-input').each do |level| .form-check - = f.check_box :project_export_enabled, class: 'form-check-input' - = f.label :project_export_enabled, class: 'form-check-label' do - Project export enabled + = level + %span.form-text.text-muted#restricted-visibility-help + Selected levels cannot be used by non-admin users for groups, projects or snippets. + If the public level is restricted, user profiles are only visible to logged in users. + .form-group + = f.label :import_sources, class: 'label-light' + = hidden_field_tag 'application_setting[import_sources][]' + - import_sources_checkboxes('import-sources-help', class: 'form-check-input').each do |source| + .form-check= source + %span.form-text.text-muted#import-sources-help + Enabled sources for code import during project creation. OmniAuth must be configured for GitHub + = link_to "(?)", help_page_path("integration/github") + , Bitbucket + = link_to "(?)", help_page_path("integration/bitbucket") + and GitLab.com + = link_to "(?)", help_page_path("integration/gitlab") + + .form-group + .form-check + = f.check_box :project_export_enabled, class: 'form-check-input' + = f.label :project_export_enabled, class: 'form-check-label' do + Project export enabled - .form-group.row - %label.col-form-label.col-sm-2 Enabled Git access protocols - .col-sm-10 - = select(:application_setting, :enabled_git_access_protocol, [['Both SSH and HTTP(S)', nil], ['Only SSH', 'ssh'], ['Only HTTP(S)', 'http']], {}, class: 'form-control') - %span.form-text.text-muted#clone-protocol-help - Allow only the selected protocols to be used for Git access. + .form-group + %label.label-light Enabled Git access protocols + = select(:application_setting, :enabled_git_access_protocol, [['Both SSH and HTTP(S)', nil], ['Only SSH', 'ssh'], ['Only HTTP(S)', 'http']], {}, class: 'form-control') + %span.form-text.text-muted#clone-protocol-help + Allow only the selected protocols to be used for Git access. - ApplicationSetting::SUPPORTED_KEY_TYPES.each do |type| - field_name = :"#{type}_key_restriction" - .form-group.row - = f.label field_name, "#{type.upcase} SSH keys", class: 'col-form-label col-sm-2' - .col-sm-10 - = f.select field_name, key_restriction_options_for_select(type), {}, class: 'form-control' + .form-group + = f.label field_name, "#{type.upcase} SSH keys", class: 'label-light' + = f.select field_name, key_restriction_options_for_select(type), {}, class: 'form-control' = f.submit 'Save changes', class: "btn btn-success" diff --git a/app/views/admin/application_settings/show.html.haml b/app/views/admin/application_settings/show.html.haml index 3f440c76ee0..38607ffca1c 100644 --- a/app/views/admin/application_settings/show.html.haml +++ b/app/views/admin/application_settings/show.html.haml @@ -17,7 +17,7 @@ %section.settings.as-account-limit.no-animate#js-account-settings{ class: ('expanded' if expanded) } .settings-header %h4 - = _('Account and limit settings') + = _('Account and limit') %button.btn.js-settings-toggle{ type: 'button' } = expanded ? _('Collapse') : _('Expand') %p @@ -50,11 +50,11 @@ %section.settings.as-terms.no-animate#js-terms-settings{ class: ('expanded' if expanded) } .settings-header %h4 - = _('Terms of Service') + = _('Terms of Service and Privacy Policy') %button.btn.btn-default.js-settings-toggle{ type: 'button' } = expanded ? _('Collapse') : _('Expand') %p - = _('Include a Terms of Service agreement that all users must accept.') + = _('Include a Terms of Service agreement and Privacy Policy that all users must accept.') .settings-content = render 'terms' @@ -169,7 +169,7 @@ .settings-content = render 'logging' -%section.settings.as-repository-storage.no-animate#js-repository-storage-settings{ class: ('expanded' if expanded) } +%section.qa-repository-storage-settings.settings.as-repository-storage.no-animate#js-repository-storage-settings{ class: ('expanded' if expanded) } .settings-header %h4 = _('Repository storage') @@ -317,7 +317,7 @@ %section.settings.as-mirror.no-animate#js-mirror-settings{ class: ('expanded' if expanded) } .settings-header %h4 - = _('Repository mirror settings') + = _('Repository mirror') %button.btn.js-settings-toggle{ type: 'button' } = expanded ? 'Collapse' : 'Expand' %p diff --git a/app/views/admin/gitaly_servers/index.html.haml b/app/views/admin/gitaly_servers/index.html.haml index d0cf5761726..9b24f411a75 100644 --- a/app/views/admin/gitaly_servers/index.html.haml +++ b/app/views/admin/gitaly_servers/index.html.haml @@ -6,10 +6,10 @@ - if @gitaly_servers.any? .table-holder %table.table.responsive-table - %thead.d-none.d-sm-none.d-md-block + %thead %tr %th= _("Storage") - %th= n_("Gitaly|Address") + %th= s_("Gitaly|Address") %th= _("Server version") %th= _("Git version") %th= _("Up to date") diff --git a/app/views/admin/groups/_form.html.haml b/app/views/admin/groups/_form.html.haml index dc4dccc9e0d..c8008771236 100644 --- a/app/views/admin/groups/_form.html.haml +++ b/app/views/admin/groups/_form.html.haml @@ -2,6 +2,9 @@ = form_errors(@group) = render 'shared/group_form', f: f + = render_if_exists 'shared/repository_size_limit_setting', form: f, type: :group + = render_if_exists 'admin/namespace_plan', f: f + .form-group.row.group-description-holder = f.label :avatar, "Group avatar", class: 'col-form-label col-sm-2' .col-sm-10 @@ -15,6 +18,8 @@ = render 'groups/group_admin_settings', f: f + = render_if_exists 'namespaces/shared_runners_minutes_settings', group: @group, form: f + - if @group.new_record? .form-group.row .offset-sm-2.col-sm-10 @@ -28,3 +33,5 @@ .form-actions = f.submit 'Save changes', class: "btn btn-save" = link_to 'Cancel', admin_group_path(@group), class: "btn btn-cancel" + += render_if_exists 'ldap_group_links/ldap_syncrhonizations', group: @group diff --git a/app/views/admin/groups/_group.html.haml b/app/views/admin/groups/_group.html.haml index e7c70a6f187..3f96988c203 100644 --- a/app/views/admin/groups/_group.html.haml +++ b/app/views/admin/groups/_group.html.haml @@ -1,3 +1,4 @@ +- group = local_assigns.fetch(:group) - css_class = 'no-description' if group.description.blank? %li.group-row{ class: css_class } @@ -8,6 +9,8 @@ %span.badge.badge-pill = storage_counter(group.storage_size) + = render_if_exists 'admin/namespace_plan_badge', namespace: group + %span = icon('bookmark') = number_with_delimiter(group.projects.count) diff --git a/app/views/admin/groups/show.html.haml b/app/views/admin/groups/show.html.haml index 6d75ccd5add..a40f98ad24f 100644 --- a/app/views/admin/groups/show.html.haml +++ b/app/views/admin/groups/show.html.haml @@ -40,6 +40,8 @@ %strong = @group.created_at.to_s(:medium) + = render_if_exists 'admin/namespace_plan_info', namespace: @group + %li %span.light Storage: %strong= storage_counter(@group.storage_size) @@ -58,6 +60,10 @@ = group_lfs_status(@group) = link_to icon('question-circle'), help_page_path('workflow/lfs/manage_large_binaries_with_git_lfs') + = render_if_exists 'namespaces/shared_runner_status', namespace: @group + + = render_if_exists 'ldap_group_links/ldap_group_links_show', group: @group + .card .card-header %h3.card-title @@ -104,7 +110,7 @@ = form_tag admin_group_members_update_path(@group), id: "new_project_member", class: "bulk_import", method: :put do %div - = users_select_tag(:user_ids, multiple: true, email_user: true, scope: :all) + = users_select_tag(:user_ids, multiple: true, email_user: true, skip_ldap: @group.ldap_synced?, scope: :all) .prepend-top-10 = select_tag :access_level, options_for_select(GroupMember.access_level_roles), class: "project-access-select select2" %hr diff --git a/app/views/admin/projects/show.html.haml b/app/views/admin/projects/show.html.haml index 0a22a142858..ccba1c461fc 100644 --- a/app/views/admin/projects/show.html.haml +++ b/app/views/admin/projects/show.html.haml @@ -116,8 +116,8 @@ .card-body = form_for @project, url: transfer_admin_project_path(@project), method: :put do |f| .form-group.row - = f.label :new_namespace_id, "Namespace", class: 'col-form-label col-sm-2' - .col-sm-10 + = f.label :new_namespace_id, "Namespace", class: 'col-form-label col-sm-3' + .col-sm-9 .dropdown = dropdown_toggle('Search for Namespace', { toggle: 'dropdown', field_name: 'new_namespace_id' }, { toggle_class: 'js-namespace-select large' }) .dropdown-menu.dropdown-select @@ -127,7 +127,7 @@ = dropdown_loading .form-group.row - .offset-sm-2.col-sm-10 + .offset-sm-3.col-sm-9 = f.submit 'Transfer', class: 'btn btn-primary' .card.repository-check diff --git a/app/views/admin/runners/index.html.haml b/app/views/admin/runners/index.html.haml index f38aeb151df..8dfd176f1b7 100644 --- a/app/views/admin/runners/index.html.haml +++ b/app/views/admin/runners/index.html.haml @@ -67,7 +67,7 @@ %th Projects %th Jobs %th Tags - %th= link_to 'Last contact', admin_runners_path(params.slice(:search).merge(sort: 'contacted_asc')) + %th= link_to 'Last contact', admin_runners_path(safe_params.slice(:search).merge(sort: 'contacted_asc')) %th - @runners.each do |runner| diff --git a/app/views/admin/services/_form.html.haml b/app/views/admin/services/_form.html.haml index 144dceacbdd..993006e8745 100644 --- a/app/views/admin/services/_form.html.haml +++ b/app/views/admin/services/_form.html.haml @@ -7,5 +7,4 @@ = render 'shared/service_settings', form: form, subject: @service .footer-block.row-content-block - .form-actions - = form.submit 'Save', class: 'btn btn-save' + = form.submit 'Save', class: 'btn btn-save' diff --git a/app/views/admin/users/_access_levels.html.haml b/app/views/admin/users/_access_levels.html.haml index 35a331283ab..04acc5f8423 100644 --- a/app/views/admin/users/_access_levels.html.haml +++ b/app/views/admin/users/_access_levels.html.haml @@ -1,26 +1,26 @@ %fieldset %legend Access - .form-group - = f.label :projects_limit, class: 'col-form-label' + .form-group.row + = f.label :projects_limit, class: 'col-form-label col-sm-2' .col-sm-10= f.number_field :projects_limit, min: 0, max: Gitlab::Database::MAX_INT_VALUE, class: 'form-control' - .form-group - = f.label :can_create_group, class: 'col-form-label' + .form-group.row + = f.label :can_create_group, class: 'col-form-label col-sm-2' .col-sm-10= f.check_box :can_create_group - .form-group - = f.label :access_level, class: 'col-form-label' + .form-group.row + = f.label :access_level, class: 'col-form-label col-sm-2' .col-sm-10 - editing_current_user = (current_user == @user) = f.radio_button :access_level, :regular, disabled: editing_current_user - = label_tag :regular do + = label_tag :regular, class: 'font-weight-bold' do Regular %p.light Regular users have access to their groups and projects = f.radio_button :access_level, :admin, disabled: editing_current_user - = label_tag :admin do + = label_tag :admin, class: 'font-weight-bold' do Admin %p.light Administrators have access to all groups, projects and users and can manage all features in this installation @@ -28,8 +28,8 @@ %p.light You cannot remove your own admin rights. - .form-group - = f.label :external, class: 'col-form-label' + .form-group.row + = f.label :external, class: 'col-form-label col-sm-2' .col-sm-10 = f.check_box :external do External diff --git a/app/views/admin/users/_form.html.haml b/app/views/admin/users/_form.html.haml index 010cb2ac354..58be07fc83e 100644 --- a/app/views/admin/users/_form.html.haml +++ b/app/views/admin/users/_form.html.haml @@ -56,7 +56,7 @@ = f.label :linkedin, class: 'col-form-label col-sm-2' .col-sm-10= f.text_field :linkedin, class: 'form-control' .form-group.row - = f.label :twitter, class: 'col-form-label' + = f.label :twitter, class: 'col-form-label col-sm-2' .col-sm-10= f.text_field :twitter, class: 'form-control' .form-group.row = f.label :website_url, 'Website', class: 'col-form-label col-sm-2' diff --git a/app/views/admin/users/_user.html.haml b/app/views/admin/users/_user.html.haml index 5e176b61d68..b2163ee85fa 100644 --- a/app/views/admin/users/_user.html.haml +++ b/app/views/admin/users/_user.html.haml @@ -38,7 +38,7 @@ %li.divider - if user.can_be_removed? %li - %button.delete-user-button.btn.btn-danger{ data: { toggle: 'modal', + %button.delete-user-button.btn.text-danger{ data: { toggle: 'modal', target: '#delete-user-modal', delete_user_url: admin_user_path(user), block_user_url: block_admin_user_path(user), @@ -47,7 +47,7 @@ = s_('AdminUsers|Delete user') %li - %button.delete-user-button.btn.btn-danger{ data: { toggle: 'modal', + %button.delete-user-button.btn.text-danger{ data: { toggle: 'modal', target: '#delete-user-modal', delete_user_url: admin_user_path(user, hard_delete: true), block_user_url: block_admin_user_path(user), diff --git a/app/views/award_emoji/_awards_block.html.haml b/app/views/award_emoji/_awards_block.html.haml index 4b3c52af16a..8ca9fb4512e 100644 --- a/app/views/award_emoji/_awards_block.html.haml +++ b/app/views/award_emoji/_awards_block.html.haml @@ -12,9 +12,9 @@ - if can?(current_user, :award_emoji, awardable) .award-menu-holder.js-award-holder %button.btn.award-control.has-tooltip.js-add-award{ type: 'button', - 'aria-label': 'Add reaction', + 'aria-label': _('Add reaction'), class: ("js-user-authored" if user_authored), - data: { title: 'Add reaction', placement: "bottom" } } + data: { title: _('Add reaction'), placement: "bottom" } } %span{ class: "award-control-icon award-control-icon-neutral" }= custom_icon('emoji_slightly_smiling_face') %span{ class: "award-control-icon award-control-icon-positive" }= custom_icon('emoji_smiley') %span{ class: "award-control-icon award-control-icon-super-positive" }= custom_icon('emoji_smile') diff --git a/app/views/ci/variables/_variable_row.html.haml b/app/views/ci/variables/_variable_row.html.haml index 571eb28f195..6ee55836dd2 100644 --- a/app/views/ci/variables/_variable_row.html.haml +++ b/app/views/ci/variables/_variable_row.html.haml @@ -43,5 +43,6 @@ %span.toggle-icon = sprite_icon('status_success_borderless', size: 16, css_class: 'toggle-icon-svg toggle-status-checked') = sprite_icon('status_failed_borderless', size: 16, css_class: 'toggle-icon-svg toggle-status-unchecked') + = render_if_exists 'ci/variables/environment_scope', form_field: form_field, variable: variable %button.js-row-remove-button.ci-variable-row-remove-button{ type: 'button', 'aria-label': s_('CiVariables|Remove variable row') } = icon('minus-circle') diff --git a/app/views/devise/shared/_signup_box.html.haml b/app/views/devise/shared/_signup_box.html.haml index 2554b2688bb..ee7369f54a9 100644 --- a/app/views/devise/shared/_signup_box.html.haml +++ b/app/views/devise/shared/_signup_box.html.haml @@ -22,6 +22,13 @@ = f.label :password = f.password_field :password, class: "form-control bottom", required: true, pattern: ".{#{@minimum_password_length},}", title: "Minimum length is #{@minimum_password_length} characters." %p.gl-field-hint Minimum length is #{@minimum_password_length} characters + - if Gitlab::CurrentSettings.current_application_settings.enforce_terms? + .form-group + = check_box_tag :terms_opt_in, '1', false, required: true + = label_tag :terms_opt_in do + - terms_link = link_to s_("I accept the|Terms of Service and Privacy Policy"), terms_path, target: "_blank" + - accept_terms_label = _("I accept the %{terms_link}") % { terms_link: terms_link } + = accept_terms_label.html_safe %div - if Gitlab::Recaptcha.enabled? = recaptcha_tags diff --git a/app/views/email_rejection_mailer/rejection.html.haml b/app/views/email_rejection_mailer/rejection.html.haml index 7f7d841fe21..c4ae7befe4e 100644 --- a/app/views/email_rejection_mailer/rejection.html.haml +++ b/app/views/email_rejection_mailer/rejection.html.haml @@ -2,3 +2,4 @@ Unfortunately, your email message to GitLab could not be processed. = markdown @reason += render_if_exists 'shared/additional_email_text' diff --git a/app/views/email_rejection_mailer/rejection.text.haml b/app/views/email_rejection_mailer/rejection.text.haml index af518b5b583..0e13b2a6473 100644 --- a/app/views/email_rejection_mailer/rejection.text.haml +++ b/app/views/email_rejection_mailer/rejection.text.haml @@ -1,3 +1,4 @@ Unfortunately, your email message to GitLab could not be processed. \ = @reason += render_if_exists 'shared/additional_email_text' diff --git a/app/views/errors/access_denied.html.haml b/app/views/errors/access_denied.html.haml index 227c7884915..8ae29b9d337 100644 --- a/app/views/errors/access_denied.html.haml +++ b/app/views/errors/access_denied.html.haml @@ -1,4 +1,4 @@ -- message = local_assigns.fetch(:message) +- message = local_assigns.fetch(:message, nil) - content_for(:title, 'Access Denied') = image_tag('illustrations/error-403.svg', alt: '403', lazy: false) diff --git a/app/views/explore/projects/_filter.html.haml b/app/views/explore/projects/_filter.html.haml index 845f4046d0d..6abb56ba6d2 100644 --- a/app/views/explore/projects/_filter.html.haml +++ b/app/views/explore/projects/_filter.html.haml @@ -1,6 +1,6 @@ - if current_user .dropdown - %button.dropdown-toggle{ href: '#', "data-toggle" => "dropdown" } + %button.dropdown-toggle{ href: '#', "data-toggle" => "dropdown", 'data-display' => 'static' } = icon('globe') %span.light Visibility: - if params[:visibility_level].present? diff --git a/app/views/groups/_activities.html.haml b/app/views/groups/_activities.html.haml index fd6e7111f38..577c63503a8 100644 --- a/app/views/groups/_activities.html.haml +++ b/app/views/groups/_activities.html.haml @@ -1,4 +1,4 @@ -.nav-block +.nav-block.activities .controls = link_to group_path(@group, rss_url_options), class: 'btn rss-btn has-tooltip' , title: 'Subscribe' do %i.fa.fa-rss diff --git a/app/views/groups/group_members/index.html.haml b/app/views/groups/group_members/index.html.haml index 6a0321bcd2b..13d584f5f1d 100644 --- a/app/views/groups/group_members/index.html.haml +++ b/app/views/groups/group_members/index.html.haml @@ -25,9 +25,10 @@ %span.badge= @members.total_count = form_tag group_group_members_path(@group), method: :get, class: 'form-inline member-search-form flex-project-members-form' do .form-group - = search_field_tag :search, params[:search], { placeholder: 'Find existing members by name', class: 'form-control', spellcheck: false } - %button.member-search-btn{ type: "submit", "aria-label" => "Submit search" } - = icon("search") + .position-relative.append-right-8 + = search_field_tag :search, params[:search], { placeholder: 'Find existing members by name', class: 'form-control', spellcheck: false } + %button.member-search-btn{ type: "submit", "aria-label" => "Submit search" } + = icon("search") - if can_manage_members = render 'shared/members/filter_2fa_dropdown' = render 'shared/members/sort_dropdown' diff --git a/app/views/groups/labels/index.html.haml b/app/views/groups/labels/index.html.haml index ac7e12fcd0b..db7eaff6658 100644 --- a/app/views/groups/labels/index.html.haml +++ b/app/views/groups/labels/index.html.haml @@ -1,21 +1,32 @@ -- page_title 'Labels' - +- @no_container = true +- page_title "Labels" +- can_admin_label = can?(current_user, :admin_label, @group) +- hide_class = '' +- hide = @available_labels.empty? || (params[:page].present? && params[:page] != '1') - issuables = ['issues', 'merge requests'] -.top-area.adjust - .nav-text - = _("Labels can be applied to %{features}. Group labels are available for any project within the group.") % { features: issuables.to_sentence } +- if can_admin_label + - content_for(:header_content) do + .nav-controls + = link_to _('New label'), new_group_label_path(@group), class: "btn btn-new" + +- if @labels.exists? + #promote-label-modal + %div{ class: container_class } + .top-area.adjust + .nav-text + = _('Labels can be applied to %{features}. Group labels are available for any project within the group.') % { features: issuables.to_sentence } - .nav-controls - - if can?(current_user, :admin_label, @group) - = link_to "New label", new_group_label_path(@group), class: "btn btn-new" + .labels-container.prepend-top-5 + .other-labels + - if can_admin_label + %h5{ class: ('hide' if hide) } Labels + %ul.content-list.manage-labels-list.js-other-labels + = render partial: 'shared/label', subject: @group, collection: @labels, as: :label, locals: { use_label_priority: false } + = paginate @labels, theme: 'gitlab' +- else + = render 'shared/empty_states/labels' -.labels - .other-labels - - if @labels.present? - %ul.content-list.manage-labels-list.js-other-labels - = render partial: 'shared/label', subject: @group, collection: @labels, as: :label - = paginate @labels, theme: 'gitlab' - - else - .nothing-here-block - = _("No labels created yet.") +%template#js-badge-item-template + %li.label-link-item.js-priority-badge.inline.prepend-left-10 + .label-badge.label-badge-blue= _('Prioritized label') diff --git a/app/views/groups/settings/ci_cd/show.html.haml b/app/views/groups/settings/ci_cd/show.html.haml index 383d955d71f..647948c7dff 100644 --- a/app/views/groups/settings/ci_cd/show.html.haml +++ b/app/views/groups/settings/ci_cd/show.html.haml @@ -7,7 +7,7 @@ .settings-header %h4 = _('Variables') - = link_to icon('question-circle'), help_page_path('ci/variables/README', anchor: 'secret-variables'), target: '_blank', rel: 'noopener noreferrer' + = link_to icon('question-circle'), help_page_path('ci/variables/README', anchor: 'variables'), target: '_blank', rel: 'noopener noreferrer' %button.btn.btn-default.js-settings-toggle{ type: "button" } = expanded ? _('Collapse') : _('Expand') %p.append-bottom-0 @@ -18,7 +18,7 @@ %section.settings#runners-settings.no-animate{ class: ('expanded' if expanded) } .settings-header %h4 - = _('Runners settings') + = _('Runners') %button.btn.btn-default.js-settings-toggle{ type: "button" } = expanded ? _('Collapse') : _('Expand') %p diff --git a/app/views/groups/show.html.haml b/app/views/groups/show.html.haml index 8b0ef3cd87a..5a88619f769 100644 --- a/app/views/groups/show.html.haml +++ b/app/views/groups/show.html.haml @@ -18,7 +18,7 @@ - if can_create_subgroups .btn-group.new-project-subgroup.droplab-dropdown.js-new-project-subgroup{ data: { project_path: new_project_path(namespace_id: @group.id), subgroup_path: new_group_path(parent_id: @group.id) } } %input.btn.btn-success.dropdown-primary.js-new-group-child{ type: "button", value: new_project_label, data: { action: "new-project" } } - %button.btn.btn-success.dropdown-toggle.js-dropdown-toggle{ type: "button", data: { "dropdown-trigger" => "#new-project-or-subgroup-dropdown" } } + %button.btn.btn-success.dropdown-toggle.js-dropdown-toggle{ type: "button", data: { "dropdown-trigger" => "#new-project-or-subgroup-dropdown", 'display' => 'static' } } = icon("caret-down", class: "dropdown-btn-icon") %ul#new-project-or-subgroup-dropdown.dropdown-menu.dropdown-menu-right{ data: { dropdown: true } } %li.droplab-item-selected{ role: "button", data: { value: "new-project", text: new_project_label } } diff --git a/app/views/help/_shortcuts.html.haml b/app/views/help/_shortcuts.html.haml index 29db29235c1..c23fe0b5c49 100644 --- a/app/views/help/_shortcuts.html.haml +++ b/app/views/help/_shortcuts.html.haml @@ -18,71 +18,71 @@ %th Global Shortcuts %tr %td.shortcut - .key s + %kbd s %td Focus Search %tr %td.shortcut - .key f + %kbd f %td Focus Filter - if performance_bar_enabled? %tr %td.shortcut - .key p b + %kbd p b %td Show/hide the Performance Bar %tr %td.shortcut - .key ? + %kbd ? %td Show/hide this dialog %tr %td.shortcut - if browser.platform.mac? - .key ⌘ shift p + %kbd ⌘ shift p - else - .key ctrl shift p + %kbd ctrl shift p %td Toggle Markdown preview %tr %td.shortcut - .key + %kbd %i.fa.fa-arrow-up %td Edit last comment (when focused on an empty textarea) %tr %td.shortcut - .key shift t + %kbd shift t %td Go to todos %tr %td.shortcut - .key shift a + %kbd shift a %td Go to the activity feed %tr %td.shortcut - .key shift p + %kbd shift p %td Go to projects %tr %td.shortcut - .key shift i + %kbd shift i %td Go to issues %tr %td.shortcut - .key shift m + %kbd shift m %td Go to merge requests %tr %td.shortcut - .key shift g + %kbd shift g %td Go to groups %tr %td.shortcut - .key shift l + %kbd shift l %td Go to milestones %tr %td.shortcut - .key shift s + %kbd shift s %td Go to snippets %tbody @@ -91,21 +91,21 @@ %th Finding Project File %tr %td.shortcut - .key + %kbd %i.fa.fa-arrow-up %td Move selection up %tr %td.shortcut - .key + %kbd %i.fa.fa-arrow-down %td Move selection down %tr %td.shortcut - .key enter + %kbd enter %td Open Selection %tr %td.shortcut - .key esc + %kbd esc %td Go back .col-lg-4 %table.shortcut-mappings @@ -115,95 +115,95 @@ %th Project %tr %td.shortcut - .key g - .key p + %kbd g + %kbd p %td Go to the project's overview page %tr %td.shortcut - .key g - .key v + %kbd g + %kbd v %td Go to the project's activity feed %tr %td.shortcut - .key g - .key f + %kbd g + %kbd f %td Go to files %tr %td.shortcut - .key g - .key c + %kbd g + %kbd c %td Go to commits %tr %td.shortcut - .key g - .key j + %kbd g + %kbd j %td Go to jobs %tr %td.shortcut - .key g - .key n + %kbd g + %kbd n %td Go to network graph %tr %td.shortcut - .key g - .key d + %kbd g + %kbd d %td Go to repository charts %tr %td.shortcut - .key g - .key i + %kbd g + %kbd i %td Go to issues %tr %td.shortcut - .key g - .key b + %kbd g + %kbd b %td Go to issue boards %tr %td.shortcut - .key g - .key m + %kbd g + %kbd m %td Go to merge requests %tr %td.shortcut - .key g - .key e + %kbd g + %kbd e %td Go to environments %tr %td.shortcut - .key g - .key k + %kbd g + %kbd k %td Go to kubernetes %tr %td.shortcut - .key g - .key s + %kbd g + %kbd s %td Go to snippets %tr %td.shortcut - .key g - .key w + %kbd g + %kbd w %td Go to wiki %tr %td.shortcut - .key t + %kbd t %td Go to finding file %tr %td.shortcut - .key i + %kbd i %td New issue %tbody @@ -212,17 +212,17 @@ %th Project Files browsing %tr %td.shortcut - .key + %kbd %i.fa.fa-arrow-up %td Move selection up %tr %td.shortcut - .key + %kbd %i.fa.fa-arrow-down %td Move selection down %tr %td.shortcut - .key enter + %kbd enter %td Open Selection %tbody %tr @@ -230,7 +230,7 @@ %th Project File %tr %td.shortcut - .key y + %kbd y %td Go to file permalink %tbody %tr @@ -239,115 +239,115 @@ %tr %td.shortcut - if browser.platform.mac? - .key ⌘ p + %kbd ⌘ p - else - .key ctrl p + %kbd ctrl p %td Go to file .col-lg-4 %table.shortcut-mappings - %tbody.hidden-shortcut.network{ style: 'display:none' } + %tbody.hidden-shortcut{ style: 'display:none' } %tr %th %th Network Graph %tr %td.shortcut - .key + %kbd %i.fa.fa-arrow-left \/ - .key h + %kbd h %td Scroll left %tr %td.shortcut - .key + %kbd %i.fa.fa-arrow-right \/ - .key l + %kbd l %td Scroll right %tr %td.shortcut - .key + %kbd %i.fa.fa-arrow-up \/ - .key k + %kbd k %td Scroll up %tr %td.shortcut - .key + %kbd %i.fa.fa-arrow-down \/ - .key j + %kbd j %td Scroll down %tr %td.shortcut - .key + %kbd shift %i.fa.fa-arrow-up \/ - .key + %kbd shift k %td Scroll to top %tr %td.shortcut - .key + %kbd shift %i.fa.fa-arrow-down \/ - .key + %kbd shift j %td Scroll to bottom - %tbody.hidden-shortcut.issues{ style: 'display:none' } + %tbody.hidden-shortcut{ style: 'display:none' } %tr %th %th Issues %tr %td.shortcut - .key a + %kbd a %td Change assignee %tr %td.shortcut - .key m + %kbd m %td Change milestone %tr %td.shortcut - .key r + %kbd r %td Reply (quoting selected text) %tr %td.shortcut - .key e + %kbd e %td Edit issue %tr %td.shortcut - .key l + %kbd l %td Change Label - %tbody.hidden-shortcut.merge_requests{ style: 'display:none' } + %tbody.hidden-shortcut{ style: 'display:none' } %tr %th %th Merge Requests %tr %td.shortcut - .key a + %kbd a %td Change assignee %tr %td.shortcut - .key m + %kbd m %td Change milestone %tr %td.shortcut - .key r + %kbd r %td Reply (quoting selected text) %tr %td.shortcut - .key e + %kbd e %td Edit merge request %tr %td.shortcut - .key l + %kbd l %td Change Label - %tbody.hidden-shortcut.wiki{ style: 'display:none' } + %tbody.hidden-shortcut{ style: 'display:none' } %tr %th %th Wiki pages %tr %td.shortcut - .key e + %kbd e %td Edit wiki page diff --git a/app/views/help/ui.html.haml b/app/views/help/ui.html.haml index de8369ed7b9..b32b602ceb3 100644 --- a/app/views/help/ui.html.haml +++ b/app/views/help/ui.html.haml @@ -443,8 +443,6 @@ .col-md-6 .alert.alert-success = lorem - .alert.alert-primary - = lorem .alert.alert-info = lorem .col-md-6 diff --git a/app/views/import/gitea/new.html.haml b/app/views/import/gitea/new.html.haml index eb9790c7903..581576a8a3d 100644 --- a/app/views/import/gitea/new.html.haml +++ b/app/views/import/gitea/new.html.haml @@ -12,11 +12,11 @@ = form_tag personal_access_token_import_gitea_path do .form-group.row - = label_tag :gitea_host_url, 'Gitea Host URL', class: 'col-form-label col-sm-8' + = label_tag :gitea_host_url, 'Gitea Host URL', class: 'col-form-label col-sm-2' .col-sm-4 = text_field_tag :gitea_host_url, nil, placeholder: 'https://try.gitea.io', class: 'form-control' .form-group.row - = label_tag :personal_access_token, 'Personal Access Token', class: 'col-form-label col-sm-8' + = label_tag :personal_access_token, 'Personal Access Token', class: 'col-form-label col-sm-2' .col-sm-4 = text_field_tag :personal_access_token, nil, class: 'form-control' .form-actions diff --git a/app/views/import/github/new.html.haml b/app/views/import/github/new.html.haml index c63cf2b31cb..b9ebb1a39d9 100644 --- a/app/views/import/github/new.html.haml +++ b/app/views/import/github/new.html.haml @@ -19,7 +19,7 @@ = form_tag personal_access_token_import_github_path, method: :post, class: 'form-inline' do .form-group - = text_field_tag :personal_access_token, '', class: 'form-control', placeholder: _('Personal Access Token'), size: 40 + = text_field_tag :personal_access_token, '', class: 'form-control append-right-8', placeholder: _('Personal Access Token'), size: 40 = submit_tag _('List your GitHub repositories'), class: 'btn btn-success' - unless github_import_configured? diff --git a/app/views/import/gitlab_projects/new.html.haml b/app/views/import/gitlab_projects/new.html.haml index 2d059e78490..cc672a5ea7c 100644 --- a/app/views/import/gitlab_projects/new.html.haml +++ b/app/views/import/gitlab_projects/new.html.haml @@ -20,7 +20,7 @@ - else .input-group-prepend.static-namespace.has-tooltip{ title: user_url(current_user.username) + '/' } - .input-group-text + .input-group-text.border-0 #{user_url(current_user.username)}/ = hidden_field_tag :namespace_id, value: current_user.namespace_id .form-group.col-12.col-sm-6.project-path @@ -37,6 +37,6 @@ .form-group = file_field_tag :file, class: '' .row - .form-actions + .form-actions.col-sm-12 = submit_tag 'Import project', class: 'btn btn-create' = link_to 'Cancel', new_project_path, class: 'btn btn-cancel' diff --git a/app/views/kaminari/gitlab/_first_page.html.haml b/app/views/kaminari/gitlab/_first_page.html.haml index 369165da02a..3b7d4a1c578 100644 --- a/app/views/kaminari/gitlab/_first_page.html.haml +++ b/app/views/kaminari/gitlab/_first_page.html.haml @@ -5,5 +5,5 @@ -# total_pages: total number of pages -# per_page: number of items to fetch per page -# remote: data-remote -%li.first.page-item +%li.page-item.js-first-button = link_to_unless current_page.first?, raw(t 'views.pagination.first'), url, remote: remote, class: 'page-link' diff --git a/app/views/kaminari/gitlab/_gap.html.haml b/app/views/kaminari/gitlab/_gap.html.haml index 6eec30212d1..849f92fdc95 100644 --- a/app/views/kaminari/gitlab/_gap.html.haml +++ b/app/views/kaminari/gitlab/_gap.html.haml @@ -4,5 +4,5 @@ -# total_pages: total number of pages -# per_page: number of items to fetch per page -# remote: data-remote -%li.page-item.disabled +%li.page-item.disabled.d-none.d-md-block = link_to raw(t 'views.pagination.truncate'), '#', class: 'page-link' diff --git a/app/views/kaminari/gitlab/_last_page.html.haml b/app/views/kaminari/gitlab/_last_page.html.haml index 8b49db58281..7836e17f877 100644 --- a/app/views/kaminari/gitlab/_last_page.html.haml +++ b/app/views/kaminari/gitlab/_last_page.html.haml @@ -5,5 +5,5 @@ -# total_pages: total number of pages -# per_page: number of items to fetch per page -# remote: data-remote -%li.last.page-item +%li.page-item.js-last-button = link_to_unless current_page.last?, raw(t 'views.pagination.last'), url, {remote: remote, class: 'page-link'} diff --git a/app/views/kaminari/gitlab/_next_page.html.haml b/app/views/kaminari/gitlab/_next_page.html.haml index 05f151555ad..a7fa1a21a6c 100644 --- a/app/views/kaminari/gitlab/_next_page.html.haml +++ b/app/views/kaminari/gitlab/_next_page.html.haml @@ -8,5 +8,5 @@ - page_url = current_page.last? ? '#' : url -%li.page-item{ class: ('disabled' if current_page.last?) } +%li.page-item.js-next-button{ class: ('disabled' if current_page.last?) } = link_to raw(t 'views.pagination.next'), page_url, rel: 'next', remote: remote, class: 'page-link' diff --git a/app/views/kaminari/gitlab/_page.html.haml b/app/views/kaminari/gitlab/_page.html.haml index 8a40e13a537..d0dc1784540 100644 --- a/app/views/kaminari/gitlab/_page.html.haml +++ b/app/views/kaminari/gitlab/_page.html.haml @@ -6,5 +6,5 @@ -# total_pages: total number of pages -# per_page: number of items to fetch per page -# remote: data-remote -%li.page-item.js-pagination-page{ class: [active_when(page.current?), ('sibling' if page.next? || page.prev?)] } +%li.page-item.js-pagination-page{ class: [active_when(page.current?), ('sibling' if page.next? || page.prev?), ('d-none d-md-block' if !page.current?) ] } = link_to page, url, { remote: remote, rel: page.next? ? 'next' : page.prev? ? 'prev' : nil, class: 'page-link' } diff --git a/app/views/kaminari/gitlab/_paginator.html.haml b/app/views/kaminari/gitlab/_paginator.html.haml index a6435deb4bf..ac9e274dbc7 100644 --- a/app/views/kaminari/gitlab/_paginator.html.haml +++ b/app/views/kaminari/gitlab/_paginator.html.haml @@ -6,7 +6,7 @@ -# remote: data-remote -# paginator: the paginator that renders the pagination tags inside = paginator.render do - .gl-pagination + .gl-pagination.prepend-top-default %ul.pagination.justify-content-center - unless current_page.first? = first_page_tag unless total_pages < 5 # As kaminari will always show the first 5 pages diff --git a/app/views/kaminari/gitlab/_prev_page.html.haml b/app/views/kaminari/gitlab/_prev_page.html.haml index f4a11a449b7..12b0e106a62 100644 --- a/app/views/kaminari/gitlab/_prev_page.html.haml +++ b/app/views/kaminari/gitlab/_prev_page.html.haml @@ -8,5 +8,5 @@ - page_url = current_page.first? ? '#' : url -%li.page-item{ class: ('disabled' if current_page.first?) } +%li.page-item.js-previous-button{ class: ('disabled' if current_page.first?) } = link_to raw(t 'views.pagination.previous'), page_url, rel: 'prev', remote: remote, class: 'page-link' diff --git a/app/views/kaminari/gitlab/_without_count.html.haml b/app/views/kaminari/gitlab/_without_count.html.haml index 1425a809052..f780400ebcb 100644 --- a/app/views/kaminari/gitlab/_without_count.html.haml +++ b/app/views/kaminari/gitlab/_without_count.html.haml @@ -1,5 +1,5 @@ -.gl-pagination - %ul.pagination.clearfix +.gl-pagination.prepend-top-default + %ul.pagination.justify-content-center - if previous_path %li.page-item.prev = link_to(t('views.pagination.previous'), previous_path, rel: 'prev', class: 'page-link') diff --git a/app/views/layouts/_head.html.haml b/app/views/layouts/_head.html.haml index 02bdfe9aa3c..9253a0652da 100644 --- a/app/views/layouts/_head.html.haml +++ b/app/views/layouts/_head.html.haml @@ -25,7 +25,7 @@ %title= page_title(site_name) %meta{ name: "description", content: page_description } - = favicon_link_tag favicon, id: 'favicon' + = favicon_link_tag favicon, id: 'favicon', data: { original_href: favicon }, type: 'image/png' = stylesheet_link_tag "application", media: "all" = stylesheet_link_tag "print", media: "print" diff --git a/app/views/layouts/devise.html.haml b/app/views/layouts/devise.html.haml index 82ca7252424..81f35615555 100644 --- a/app/views/layouts/devise.html.haml +++ b/app/views/layouts/devise.html.haml @@ -1,7 +1,7 @@ !!! 5 %html.devise-layout-html{ class: system_message_class } = render "layouts/head" - %body.ui_indigo.login-page.application.navless{ data: { page: body_data_page } } + %body.ui-indigo.login-page.application.navless{ data: { page: body_data_page } } .page-wrap = render "layouts/header/empty" .login-page-broadcast diff --git a/app/views/layouts/devise_empty.html.haml b/app/views/layouts/devise_empty.html.haml index adf90cb7667..52805e0da73 100644 --- a/app/views/layouts/devise_empty.html.haml +++ b/app/views/layouts/devise_empty.html.haml @@ -1,7 +1,7 @@ !!! 5 %html{ lang: "en", class: system_message_class } = render "layouts/head" - %body.ui_indigo.login-page.application.navless + %body.ui-indigo.login-page.application.navless = render "layouts/header/empty" = render "layouts/broadcast" .container.navless-container diff --git a/app/views/layouts/header/_default.html.haml b/app/views/layouts/header/_default.html.haml index 1bca837a311..5cec443e969 100644 --- a/app/views/layouts/header/_default.html.haml +++ b/app/views/layouts/header/_default.html.haml @@ -61,7 +61,7 @@ - if header_link?(:sign_in) %li.nav-item %div - = link_to "Sign in / Register", new_session_path(:user, redirect_to_referer: 'yes'), class: 'nav-link btn btn-sign-in' + = link_to "Sign in / Register", new_session_path(:user, redirect_to_referer: 'yes'), class: 'btn btn-sign-in' %button.navbar-toggler.d-block.d-sm-none{ type: 'button' } %span.sr-only Toggle navigation diff --git a/app/views/layouts/header/_new_dropdown.haml b/app/views/layouts/header/_new_dropdown.haml index db8137cc248..d35df706036 100644 --- a/app/views/layouts/header/_new_dropdown.haml +++ b/app/views/layouts/header/_new_dropdown.haml @@ -1,5 +1,5 @@ %li.header-new.dropdown - = link_to new_project_path, class: "header-new-dropdown-toggle has-tooltip qa-new-menu-toggle", title: "New...", ref: 'tooltip', aria: { label: "New..." }, data: { toggle: 'dropdown', placement: 'bottom', container: 'body' } do + = link_to new_project_path, class: "header-new-dropdown-toggle has-tooltip qa-new-menu-toggle", title: "New...", ref: 'tooltip', aria: { label: "New..." }, data: { toggle: 'dropdown', placement: 'bottom', container: 'body', display: 'static' } do = sprite_icon('plus-square', size: 16) = sprite_icon('angle-down', css_class: 'caret-down') .dropdown-menu.dropdown-menu-right diff --git a/app/views/layouts/terms.html.haml b/app/views/layouts/terms.html.haml index a8964b19ba1..977eb350365 100644 --- a/app/views/layouts/terms.html.haml +++ b/app/views/layouts/terms.html.haml @@ -14,9 +14,9 @@ %div{ class: "#{container_class} limit-container-width" } .content{ id: "content-body" } - .panel.panel-default - .panel-heading - .title + .card + .card-header + .card-title = brand_header_logo - logo_text = brand_header_logo_type - if logo_text.present? diff --git a/app/views/notify/new_merge_request_email.html.haml b/app/views/notify/new_merge_request_email.html.haml index 0a9adc6f243..dd6a84e503d 100644 --- a/app/views/notify/new_merge_request_email.html.haml +++ b/app/views/notify/new_merge_request_email.html.haml @@ -9,6 +9,8 @@ %p Assignee: #{@merge_request.assignee_name} += render_if_exists 'notify/merge_request_approvers', merge_request: @merge_request + - if @merge_request.description %div = markdown(@merge_request.description, pipeline: :email, author: @merge_request.author) diff --git a/app/views/notify/new_merge_request_email.text.erb b/app/views/notify/new_merge_request_email.text.erb index 7d98400e6fe..d5b8f8d764f 100644 --- a/app/views/notify/new_merge_request_email.text.erb +++ b/app/views/notify/new_merge_request_email.text.erb @@ -5,6 +5,6 @@ New Merge Request <%= @merge_request.to_reference %> <%= merge_path_description(@merge_request, 'to') %> Author: <%= @merge_request.author_name %> Assignee: <%= @merge_request.assignee_name %> +<%= render_if_exists 'notify/merge_request_approvers', merge_request: @merge_request %> <%= @merge_request.description %> - diff --git a/app/views/profiles/preferences/show.html.haml b/app/views/profiles/preferences/show.html.haml index ce312943154..8f1078bd41d 100644 --- a/app/views/profiles/preferences/show.html.haml +++ b/app/views/profiles/preferences/show.html.haml @@ -4,18 +4,12 @@ = form_for @user, url: profile_preferences_path, remote: true, method: :put, html: { class: 'row prepend-top-default js-preferences-form' } do |f| .col-lg-4.application-theme %h4.prepend-top-0 - s_('Preferences|Navigation theme') + = s_('Preferences|Navigation theme') %p Customize the appearance of the application header and navigation sidebar. .col-lg-8.application-theme - Gitlab::Themes.each do |theme| = label_tag do - .preview{ class: theme.name.downcase } - .preview-row - .quadrant.one - .quadrant.two - .preview-row - .quadrant.three - .quadrant.four + .preview{ class: theme.css_class } = f.radio_button :theme_id, theme.id, checked: Gitlab::Themes.for_user(@user).id == theme.id = theme.name diff --git a/app/views/projects/_home_panel.html.haml b/app/views/projects/_home_panel.html.haml index 075badb9e56..89940512bc6 100644 --- a/app/views/projects/_home_panel.html.haml +++ b/app/views/projects/_home_panel.html.haml @@ -42,6 +42,10 @@ .project-clone-holder = render "shared/clone_panel" + - if show_xcode_link?(@project) + .project-action-button.project-xcode.inline + = render "projects/buttons/xcode_link" + - if current_user - if can?(current_user, :download_code, @project) = render 'projects/buttons/download', project: @project, ref: @ref diff --git a/app/views/projects/_new_project_fields.html.haml b/app/views/projects/_new_project_fields.html.haml index cfbd0459e3e..6f957533287 100644 --- a/app/views/projects/_new_project_fields.html.haml +++ b/app/views/projects/_new_project_fields.html.haml @@ -16,7 +16,7 @@ - else .input-group-prepend.static-namespace.has-tooltip{ title: user_url(current_user.username) + '/' } - .input-group-text + .input-group-text.border-0 #{user_url(current_user.username)}/ = f.hidden_field :namespace_id, value: current_user.namespace_id .form-group.project-path.col-sm-6 diff --git a/app/views/projects/_project_templates.html.haml b/app/views/projects/_project_templates.html.haml index 9d27f51926e..d08807b5135 100644 --- a/app/views/projects/_project_templates.html.haml +++ b/app/views/projects/_project_templates.html.haml @@ -10,16 +10,18 @@ %a.btn.btn-default{ href: template.preview, rel: 'noopener noreferrer', target: '_blank' } Preview .project-fields-form - .form-group - %label.label-light - Template - .input-group.template-input-group - .input-group-prepend - .input-group-text - .selected-icon - - Gitlab::ProjectTemplate.all.each do |template| - = custom_icon(template.logo) - .selected-template - %button.btn.btn-default.change-template{ type: "button" } Change template + .row + .form-group.col-sm-12 + %label.label-light + Template + .input-group.template-input-group + .input-group-prepend + .input-group-text + .selected-icon + - Gitlab::ProjectTemplate.all.each do |template| + = custom_icon(template.logo) + .selected-template + .input-group-append + %button.btn.btn-default.change-template{ type: "button" } Change template = render 'new_project_fields', f: f, project_name_id: "template-project-name" diff --git a/app/views/projects/_stat_anchor_list.html.haml b/app/views/projects/_stat_anchor_list.html.haml index 8bffd1396ae..15ec58289e3 100644 --- a/app/views/projects/_stat_anchor_list.html.haml +++ b/app/views/projects/_stat_anchor_list.html.haml @@ -5,4 +5,4 @@ - anchors.each do |anchor| %li.nav-item = link_to_if anchor.link, anchor.label, anchor.link, class: anchor.enabled ? 'nav-link stat-link' : "nav-link btn btn-#{anchor.class_modifier || 'missing'}" do - %span.stat-text= anchor.label + .stat-text= anchor.label diff --git a/app/views/projects/artifacts/browse.html.haml b/app/views/projects/artifacts/browse.html.haml index 7ff7466e561..87b165e581a 100644 --- a/app/views/projects/artifacts/browse.html.haml +++ b/app/views/projects/artifacts/browse.html.haml @@ -9,10 +9,10 @@ .tree-holder .nav-block %ul.breadcrumb.repo-breadcrumb - %li + %li.breadcrumb-item = link_to 'Artifacts', browse_project_job_artifacts_path(@project, @build) - path_breadcrumbs do |title, path| - %li + %li.breadcrumb-item = link_to truncate(title, length: 40), browse_project_job_artifacts_path(@project, @build, path) .tree-controls diff --git a/app/views/projects/blob/_header.html.haml b/app/views/projects/blob/_header.html.haml index 1b150ec3e5c..0a0b3ce1d6f 100644 --- a/app/views/projects/blob/_header.html.haml +++ b/app/views/projects/blob/_header.html.haml @@ -11,6 +11,7 @@ = view_on_environment_button(@commit.sha, @path, @environment) if @environment .btn-group{ role: "group" }< + = render_if_exists 'projects/blob/header_file_locks_link' = edit_blob_button = ide_edit_button - if current_user @@ -18,3 +19,4 @@ = delete_blob_link = render 'projects/fork_suggestion' += render_if_exists 'projects/blob/header_file_locks', project: @project, path: @path diff --git a/app/views/projects/blob/viewers/_download.html.haml b/app/views/projects/blob/viewers/_download.html.haml index f9b1da05a00..fda4b9c92cd 100644 --- a/app/views/projects/blob/viewers/_download.html.haml +++ b/app/views/projects/blob/viewers/_download.html.haml @@ -1,5 +1,5 @@ .file-content.blob_file.blob-no-preview - .center.render-error.vertical-center + .center.render-error = link_to blob_raw_path do %h1.light = sprite_icon('download') diff --git a/app/views/projects/branches/_branch.html.haml b/app/views/projects/branches/_branch.html.haml index f641d7bc51a..88f9b7dfc9f 100644 --- a/app/views/projects/branches/_branch.html.haml +++ b/app/views/projects/branches/_branch.html.haml @@ -72,7 +72,7 @@ - else %button{ class: "btn btn-remove remove-row js-ajax-loading-spinner has-tooltip disabled", disabled: true, - title: s_('Branches|Only a project master or owner can delete a protected branch') } + title: s_('Branches|Only a project maintainer or owner can delete a protected branch') } = icon("trash-o") - else = link_to project_branch_path(@project, branch.name), diff --git a/app/views/projects/buttons/_download.html.haml b/app/views/projects/buttons/_download.html.haml index c75093c4c24..f7551434d47 100644 --- a/app/views/projects/buttons/_download.html.haml +++ b/app/views/projects/buttons/_download.html.haml @@ -3,7 +3,7 @@ - if !project.empty_repo? && can?(current_user, :download_code, project) - archive_prefix = "#{project.path}-#{ref.tr('/', '-')}" .project-action-button.dropdown.inline> - %button.btn.has-tooltip{ title: s_('DownloadSource|Download'), 'data-toggle' => 'dropdown', 'aria-label' => s_('DownloadSource|Download') } + %button.btn.has-tooltip{ title: s_('DownloadSource|Download'), 'data-toggle' => 'dropdown', 'aria-label' => s_('DownloadSource|Download'), 'data-display' => 'static' } = sprite_icon('download') = icon("caret-down") %span.sr-only= _('Select Archive Format') diff --git a/app/views/projects/buttons/_dropdown.html.haml b/app/views/projects/buttons/_dropdown.html.haml index 84245d72f4a..8b9c52f0802 100644 --- a/app/views/projects/buttons/_dropdown.html.haml +++ b/app/views/projects/buttons/_dropdown.html.haml @@ -8,7 +8,7 @@ - if show_menu .project-action-button.dropdown.inline - %a.btn.dropdown-toggle.has-tooltip{ href: '#', title: _('Create new...'), 'data-toggle' => 'dropdown', 'data-container' => 'body', 'aria-label' => _('Create new...') } + %a.btn.dropdown-toggle.has-tooltip{ href: '#', title: _('Create new...'), 'data-toggle' => 'dropdown', 'data-container' => 'body', 'aria-label' => _('Create new...'), 'data-display' => 'static' } = icon('plus') = icon("caret-down") %ul.dropdown-menu.dropdown-menu-right.project-home-dropdown diff --git a/app/views/projects/buttons/_xcode_link.html.haml b/app/views/projects/buttons/_xcode_link.html.haml new file mode 100644 index 00000000000..a8b32fb0ef5 --- /dev/null +++ b/app/views/projects/buttons/_xcode_link.html.haml @@ -0,0 +1,2 @@ +%a.btn.btn-default{ href: xcode_uri_to_repo(@project) } + = _("Open in Xcode") diff --git a/app/views/projects/clusters/_advanced_settings.html.haml b/app/views/projects/clusters/_advanced_settings.html.haml index e9bdc54364b..243e8cd9ba0 100644 --- a/app/views/projects/clusters/_advanced_settings.html.haml +++ b/app/views/projects/clusters/_advanced_settings.html.haml @@ -7,8 +7,8 @@ - link_gke = link_to(s_('ClusterIntegration|Google Kubernetes Engine'), @cluster.gke_cluster_url, target: '_blank', rel: 'noopener noreferrer') = s_('ClusterIntegration|Manage your Kubernetes cluster by visiting %{link_gke}').html_safe % { link_gke: link_gke } - .card.form-group - %label.text-danger + .sub-section.form-group + %h4.text-danger = s_('ClusterIntegration|Remove Kubernetes cluster integration') %p = s_("ClusterIntegration|Remove this Kubernetes cluster's configuration from this project. This will not delete your actual Kubernetes cluster.") diff --git a/app/views/projects/clusters/_sidebar.html.haml b/app/views/projects/clusters/_sidebar.html.haml index 73cd7c50922..3d10348212f 100644 --- a/app/views/projects/clusters/_sidebar.html.haml +++ b/app/views/projects/clusters/_sidebar.html.haml @@ -1,7 +1,9 @@ +- clusters_help_url = help_page_path('user/project/clusters/index.md') +- help_link_start = "<a href=\"%{url}\" target=\"_blank\" rel=\"noopener noreferrer\">".html_safe +- help_link_end = '</a>'.html_safe %h4.prepend-top-0 = s_('ClusterIntegration|Kubernetes cluster integration') %p = s_('ClusterIntegration|With a Kubernetes cluster associated to this project, you can use review apps, deploy your applications, run your pipelines, and much more in an easy way.') %p - - link = link_to(_('Kubernetes'), help_page_path('user/project/clusters/index'), target: '_blank', rel: 'noopener noreferrer') - = s_('ClusterIntegration|Learn more about %{link_to_documentation}').html_safe % { link_to_documentation: link } + = s_('ClusterIntegration|Learn more about %{help_link_start}Kubernetes%{help_link_end}.').html_safe % { help_link_start: help_link_start % { url: clusters_help_url }, help_link_end: help_link_end } diff --git a/app/views/projects/clusters/gcp/_form.html.haml b/app/views/projects/clusters/gcp/_form.html.haml index 1494998f95f..0a2e320556d 100644 --- a/app/views/projects/clusters/gcp/_form.html.haml +++ b/app/views/projects/clusters/gcp/_form.html.haml @@ -1,4 +1,10 @@ = javascript_include_tag 'https://apis.google.com/js/api.js' +- external_link_icon = icon('external-link') +- zones_link_url = 'https://cloud.google.com/compute/docs/regions-zones/regions-zones' +- machine_type_link_url = 'https://cloud.google.com/compute/docs/machine-types' +- pricing_link_url = 'https://cloud.google.com/compute/pricing#machinetype' +- help_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe +- help_link_end = ' %{external_link_icon}</a>'.html_safe % { external_link_icon: external_link_icon } %p - link_to_help_page = link_to(s_('ClusterIntegration|help page'), help_page_path('user/project/clusters/index'), target: '_blank', rel: 'noopener noreferrer') @@ -9,15 +15,15 @@ = form_for @gcp_cluster, html: { class: 'js-gke-cluster-creation prepend-top-20', data: { token: token_in_session } }, url: create_gcp_namespace_project_clusters_path(@project.namespace, @project), as: :cluster do |field| = form_errors(@gcp_cluster) .form-group - = field.label :name, s_('ClusterIntegration|Kubernetes cluster name') + = field.label :name, s_('ClusterIntegration|Kubernetes cluster name'), class: 'label-light' = field.text_field :name, class: 'form-control', placeholder: s_('ClusterIntegration|Kubernetes cluster name') .form-group - = field.label :environment_scope, s_('ClusterIntegration|Environment scope') + = field.label :environment_scope, s_('ClusterIntegration|Environment scope'), class: 'label-light' = field.text_field :environment_scope, class: 'form-control', readonly: !has_multiple_clusters?(@project), placeholder: s_('ClusterIntegration|Environment scope') = field.fields_for :provider_gcp, @gcp_cluster.provider_gcp do |provider_gcp_field| .form-group - = provider_gcp_field.label :gcp_project_id, s_('ClusterIntegration|Google Cloud Platform project ID') + = provider_gcp_field.label :gcp_project_id, s_('ClusterIntegration|Google Cloud Platform project'), class: 'label-light' .js-gcp-project-id-dropdown-entry-point{ data: { docsUrl: 'https://console.cloud.google.com/home/dashboard' } } = provider_gcp_field.hidden_field :gcp_project_id .dropdown @@ -28,7 +34,7 @@ %span.form-text.text-muted .form-group - = provider_gcp_field.label :zone, s_('ClusterIntegration|Zone') + = provider_gcp_field.label :zone, s_('ClusterIntegration|Zone'), class: 'label-light' .js-gcp-zone-dropdown-entry-point = provider_gcp_field.hidden_field :zone .dropdown @@ -36,13 +42,15 @@ %span.dropdown-toggle-text = _('Select project to choose zone') = icon('chevron-down') + %p.form-text.text-muted + = s_('ClusterIntegration|Learn more about %{help_link_start}zones%{help_link_end}.').html_safe % { help_link_start: help_link_start % { url: zones_link_url }, help_link_end: help_link_end } .form-group - = provider_gcp_field.label :num_nodes, s_('ClusterIntegration|Number of nodes') + = provider_gcp_field.label :num_nodes, s_('ClusterIntegration|Number of nodes'), class: 'label-light' = provider_gcp_field.text_field :num_nodes, class: 'form-control', placeholder: '3' .form-group - = provider_gcp_field.label :machine_type, s_('ClusterIntegration|Machine type') + = provider_gcp_field.label :machine_type, s_('ClusterIntegration|Machine type'), class: 'label-light' .js-gcp-machine-type-dropdown-entry-point = provider_gcp_field.hidden_field :machine_type .dropdown @@ -50,6 +58,8 @@ %span.dropdown-toggle-text = _('Select project and zone to choose machine type') = icon('chevron-down') + %p.form-text.text-muted + = s_('ClusterIntegration|Learn more about %{help_link_start_machine_type}machine types%{help_link_end} and %{help_link_start_pricing}pricing%{help_link_end}.').html_safe % { help_link_start_machine_type: help_link_start % { url: machine_type_link_url }, help_link_start_pricing: help_link_start % { url: pricing_link_url }, help_link_end: help_link_end } .form-group = field.submit s_('ClusterIntegration|Create Kubernetes cluster'), class: 'js-gke-cluster-creation-submit btn btn-success', disabled: true diff --git a/app/views/projects/clusters/user/_form.html.haml b/app/views/projects/clusters/user/_form.html.haml index 2f090e3fb86..5590ade893c 100644 --- a/app/views/projects/clusters/user/_form.html.haml +++ b/app/views/projects/clusters/user/_form.html.haml @@ -1,27 +1,27 @@ = form_for @user_cluster, url: create_user_namespace_project_clusters_path(@project.namespace, @project), as: :cluster do |field| = form_errors(@user_cluster) .form-group - = field.label :name, s_('ClusterIntegration|Kubernetes cluster name') + = field.label :name, s_('ClusterIntegration|Kubernetes cluster name'), class: 'label-light' = field.text_field :name, class: 'form-control', placeholder: s_('ClusterIntegration|Kubernetes cluster name') .form-group - = field.label :environment_scope, s_('ClusterIntegration|Environment scope') + = field.label :environment_scope, s_('ClusterIntegration|Environment scope'), class: 'label-light' = field.text_field :environment_scope, class: 'form-control', readonly: !has_multiple_clusters?(@project), placeholder: s_('ClusterIntegration|Environment scope') = field.fields_for :platform_kubernetes, @user_cluster.platform_kubernetes do |platform_kubernetes_field| .form-group - = platform_kubernetes_field.label :api_url, s_('ClusterIntegration|API URL') + = platform_kubernetes_field.label :api_url, s_('ClusterIntegration|API URL'), class: 'label-light' = platform_kubernetes_field.text_field :api_url, class: 'form-control', placeholder: s_('ClusterIntegration|API URL') .form-group - = platform_kubernetes_field.label :ca_cert, s_('ClusterIntegration|CA Certificate') + = platform_kubernetes_field.label :ca_cert, s_('ClusterIntegration|CA Certificate'), class: 'label-light' = platform_kubernetes_field.text_area :ca_cert, class: 'form-control', placeholder: s_('ClusterIntegration|Certificate Authority bundle (PEM format)') .form-group - = platform_kubernetes_field.label :token, s_('ClusterIntegration|Token') + = platform_kubernetes_field.label :token, s_('ClusterIntegration|Token'), class: 'label-light' = platform_kubernetes_field.text_field :token, class: 'form-control', placeholder: s_('ClusterIntegration|Service token'), autocomplete: 'off' .form-group - = platform_kubernetes_field.label :namespace, s_('ClusterIntegration|Project namespace (optional, unique)') + = platform_kubernetes_field.label :namespace, s_('ClusterIntegration|Project namespace (optional, unique)'), class: 'label-light' = platform_kubernetes_field.text_field :namespace, class: 'form-control', placeholder: s_('ClusterIntegration|Project namespace') .form-group diff --git a/app/views/projects/clusters/user/_show.html.haml b/app/views/projects/clusters/user/_show.html.haml index 77d7a055474..4d117f435dc 100644 --- a/app/views/projects/clusters/user/_show.html.haml +++ b/app/views/projects/clusters/user/_show.html.haml @@ -1,20 +1,20 @@ = form_for @cluster, url: namespace_project_cluster_path(@project.namespace, @project, @cluster), as: :cluster do |field| = form_errors(@cluster) .form-group - = field.label :name, s_('ClusterIntegration|Kubernetes cluster name') + = field.label :name, s_('ClusterIntegration|Kubernetes cluster name'), class: 'label-light' = field.text_field :name, class: 'form-control', placeholder: s_('ClusterIntegration|Kubernetes cluster name') = field.fields_for :platform_kubernetes, @cluster.platform_kubernetes do |platform_kubernetes_field| .form-group - = platform_kubernetes_field.label :api_url, s_('ClusterIntegration|API URL') + = platform_kubernetes_field.label :api_url, s_('ClusterIntegration|API URL'), class: 'label-light' = platform_kubernetes_field.text_field :api_url, class: 'form-control', placeholder: s_('ClusterIntegration|API URL') .form-group - = platform_kubernetes_field.label :ca_cert, s_('ClusterIntegration|CA Certificate') + = platform_kubernetes_field.label :ca_cert, s_('ClusterIntegration|CA Certificate'), class: 'label-light' = platform_kubernetes_field.text_area :ca_cert, class: 'form-control', placeholder: s_('ClusterIntegration|Certificate Authority bundle (PEM format)') .form-group - = platform_kubernetes_field.label :token, s_('ClusterIntegration|Token') + = platform_kubernetes_field.label :token, s_('ClusterIntegration|Token'), class: 'label-light' .input-group = platform_kubernetes_field.text_field :token, class: 'form-control js-cluster-token', type: 'password', placeholder: s_('ClusterIntegration|Token'), autocomplete: 'off' %span.input-group-append.clipboard-addon @@ -23,7 +23,7 @@ = s_('ClusterIntegration|Show') .form-group - = platform_kubernetes_field.label :namespace, s_('ClusterIntegration|Project namespace (optional, unique)') + = platform_kubernetes_field.label :namespace, s_('ClusterIntegration|Project namespace (optional, unique)'), class: 'label-light' = platform_kubernetes_field.text_field :namespace, class: 'form-control', placeholder: s_('ClusterIntegration|Project namespace') .form-group diff --git a/app/views/projects/commit/_change.html.haml b/app/views/projects/commit/_change.html.haml index 30605927fd1..3d97e93c9e9 100644 --- a/app/views/projects/commit/_change.html.haml +++ b/app/views/projects/commit/_change.html.haml @@ -20,18 +20,18 @@ %span{ "aria-hidden": true } × .modal-body - if description - %p.append-bottom-20= description + %p= description = form_tag [type.underscore, @project.namespace.becomes(Namespace), @project, commit], method: :post, remote: false, class: "js-#{type}-form js-requires-input" do - .form-group.row.branch - = label_tag 'start_branch', branch_label, class: 'col-form-label col-sm-2' - .col-sm-10 - = hidden_field_tag :start_branch, @project.default_branch, id: 'start_branch' - = dropdown_tag(@project.default_branch, options: { title: s_("BranchSwitcherTitle|Switch branch"), filter: true, placeholder: s_("BranchSwitcherPlaceholder|Search branches"), toggle_class: 'js-project-refs-dropdown dynamic', dropdown_class: 'dropdown-menu-selectable', data: { field_name: "start_branch", selected: @project.default_branch, start_branch: @project.default_branch, refs_url: project_branches_path(@project), submit_form_on_click: false } }) + .form-group.branch + = label_tag 'start_branch', branch_label, class: 'label-light' - - if can?(current_user, :push_code, @project) - = render 'shared/new_merge_request_checkbox' - - else - = hidden_field_tag 'create_merge_request', 1, id: nil + = hidden_field_tag :start_branch, @project.default_branch, id: 'start_branch' + = dropdown_tag(@project.default_branch, options: { title: s_("BranchSwitcherTitle|Switch branch"), filter: true, placeholder: s_("BranchSwitcherPlaceholder|Search branches"), toggle_class: 'js-project-refs-dropdown dynamic', dropdown_class: 'dropdown-menu-selectable', data: { field_name: "start_branch", selected: @project.default_branch, start_branch: @project.default_branch, refs_url: project_branches_path(@project), submit_form_on_click: false } }) + + - if can?(current_user, :push_code, @project) + = render 'shared/new_merge_request_checkbox' + - else + = hidden_field_tag 'create_merge_request', 1, id: nil .form-actions = submit_tag label, class: 'btn btn-create' = link_to _("Cancel"), '#', class: "btn btn-cancel", "data-dismiss" => "modal" diff --git a/app/views/projects/commit/branches.html.haml b/app/views/projects/commit/branches.html.haml index a91e31afc2b..0b8e5105bc0 100644 --- a/app/views/projects/commit/branches.html.haml +++ b/app/views/projects/commit/branches.html.haml @@ -6,7 +6,7 @@ - if @branches.any? || @tags.any? || @tags_limit_exceeded %span - = link_to "#", class: "js-details-expand label label-gray ref-name" do + = link_to "#", class: "js-details-expand badge badge-gray ref-name" do = sprite_icon('ellipsis_h', size: 12, css_class: 'vertical-align-middle') %span.js-details-content.hide = commit_branches_links(@project, @branches) diff --git a/app/views/projects/deploy_keys/_index.html.haml b/app/views/projects/deploy_keys/_index.html.haml index 6af57d3ab26..fb1ea471dec 100644 --- a/app/views/projects/deploy_keys/_index.html.haml +++ b/app/views/projects/deploy_keys/_index.html.haml @@ -1,5 +1,5 @@ - expanded = Rails.env.test? -%section.settings.no-animate{ class: ('expanded' if expanded) } +%section.qa-deploy-keys-settings.settings.no-animate{ class: ('expanded' if expanded) } .settings-header %h4 Deploy Keys diff --git a/app/views/projects/deploy_tokens/_index.html.haml b/app/views/projects/deploy_tokens/_index.html.haml index 50e5950ced4..725720d2222 100644 --- a/app/views/projects/deploy_tokens/_index.html.haml +++ b/app/views/projects/deploy_tokens/_index.html.haml @@ -10,9 +10,8 @@ .settings-content - if @new_deploy_token.persisted? = render 'projects/deploy_tokens/new_deploy_token', deploy_token: @new_deploy_token - - else - %h5.prepend-top-0 - = s_('DeployTokens|Add a deploy token') - = render 'projects/deploy_tokens/form', project: @project, token: @new_deploy_token, presenter: @deploy_tokens - %hr + %h5.prepend-top-0 + = s_('DeployTokens|Add a deploy token') + = render 'projects/deploy_tokens/form', project: @project, token: @new_deploy_token, presenter: @deploy_tokens + %hr = render 'projects/deploy_tokens/table', project: @project, active_tokens: @deploy_tokens diff --git a/app/views/projects/deploy_tokens/_new_deploy_token.html.haml b/app/views/projects/deploy_tokens/_new_deploy_token.html.haml index 1e715681e59..5dd9ffba074 100644 --- a/app/views/projects/deploy_tokens/_new_deploy_token.html.haml +++ b/app/views/projects/deploy_tokens/_new_deploy_token.html.haml @@ -1,14 +1,18 @@ -.created-deploy-token-container - %h5.prepend-top-0 - = s_('DeployTokens|Your New Deploy Token') +.created-deploy-token-container.info-well + .well-segment + %h5.prepend-top-0 + = s_('DeployTokens|Your New Deploy Token') - .form-group - = text_field_tag 'deploy-token-user', deploy_token.username, readonly: true, class: 'deploy-token-field form-control js-select-on-focus' - = clipboard_button(text: deploy_token.username, title: s_('DeployTokens|Copy username to clipboard'), placement: 'left') - %span.deploy-token-help-block.prepend-top-5.text-success= s_("DeployTokens|Use this username as a login.") + .form-group + .input-group + = text_field_tag 'deploy-token-user', deploy_token.username, readonly: true, class: 'deploy-token-field form-control js-select-on-focus' + .input-group-append + = clipboard_button(text: deploy_token.username, title: s_('DeployTokens|Copy username to clipboard'), placement: 'left') + %span.deploy-token-help-block.prepend-top-5.text-success= s_("DeployTokens|Use this username as a login.") - .form-group - = text_field_tag 'deploy-token', deploy_token.token, readonly: true, class: 'deploy-token-field form-control js-select-on-focus' - = clipboard_button(text: deploy_token.token, title: s_('DeployTokens|Copy deploy token to clipboard'), placement: 'left') - %span.deploy-token-help-block.prepend-top-5.text-danger= s_("DeployTokens|Use this token as a password. Make sure you save it - you won't be able to access it again.") -%hr + .form-group + .input-group + = text_field_tag 'deploy-token', deploy_token.token, readonly: true, class: 'deploy-token-field form-control js-select-on-focus' + .input-group-append + = clipboard_button(text: deploy_token.token, title: s_('DeployTokens|Copy deploy token to clipboard'), placement: 'left') + %span.deploy-token-help-block.prepend-top-5.text-danger= s_("DeployTokens|Use this token as a password. Make sure you save it - you won't be able to access it again.") diff --git a/app/views/projects/diffs/_collapsed.html.haml b/app/views/projects/diffs/_collapsed.html.haml index 5762f4d86d7..9bd1255fe00 100644 --- a/app/views/projects/diffs/_collapsed.html.haml +++ b/app/views/projects/diffs/_collapsed.html.haml @@ -2,4 +2,4 @@ - url = url_for(safe_params.merge(action: :diff_for_path, old_path: diff_file.old_path, new_path: diff_file.new_path, file_identifier: diff_file.file_identifier)) .nothing-here-block.diff-collapsed{ data: { diff_for_path: url } } This diff is collapsed. - %a.click-to-expand Click to expand it. + %button.click-to-expand.btn.btn-link Click to expand it. diff --git a/app/views/projects/edit.html.haml b/app/views/projects/edit.html.haml index 77ba422e87e..9f175d2376f 100644 --- a/app/views/projects/edit.html.haml +++ b/app/views/projects/edit.html.haml @@ -7,7 +7,7 @@ %section.settings.general-settings.no-animate{ class: ('expanded' if expanded) } .settings-header %h4 - General project settings + General project %button.btn.js-settings-toggle{ type: 'button' } = expanded ? 'Collapse' : 'Expand' %p @@ -82,10 +82,10 @@ = render_if_exists 'projects/issues_settings' - %section.settings.merge-requests-feature.no-animate{ class: [('expanded' if expanded), ('hidden' if @project.project_feature.send(:merge_requests_access_level) == 0)] } + %section.qa-merge-request-settings.settings.merge-requests-feature.no-animate{ class: [('expanded' if expanded), ('hidden' if @project.project_feature.send(:merge_requests_access_level) == 0)] } .settings-header %h4 - Merge request settings + Merge request %button.btn.js-settings-toggle{ type: 'button' } = expanded ? 'Collapse' : 'Expand' %p @@ -101,10 +101,10 @@ = render 'export', project: @project - %section.settings.advanced-settings.no-animate{ class: ('expanded' if expanded) } + %section.qa-advanced-settings.settings.advanced-settings.no-animate{ class: ('expanded' if expanded) } .settings-header %h4 - Advanced settings + Advanced %button.btn.js-settings-toggle{ type: 'button' } = expanded ? 'Collapse' : 'Expand' %p diff --git a/app/views/projects/empty.html.haml b/app/views/projects/empty.html.haml index bf6fc8af12d..d47dc3d8143 100644 --- a/app/views/projects/empty.html.haml +++ b/app/views/projects/empty.html.haml @@ -1,3 +1,4 @@ +- @content_class = "limit-container-width" unless fluid_layout - @no_container = true - breadcrumb_title _("Details") @@ -6,7 +7,7 @@ = render "home_panel" .project-empty-note-panel - %div{ class: [container_class, ("limit-container-width-sm" unless fluid_layout)] } + %div{ class: [container_class, ("limit-container-width" unless fluid_layout)] } .prepend-top-20 %h4 = _('The repository for this project is empty') @@ -36,7 +37,7 @@ = render 'stat_anchor_list', anchors: @project.empty_repo_statistics_buttons - if can?(current_user, :push_code, @project) - %div{ class: [container_class, ("limit-container-width-sm" unless fluid_layout)] } + %div{ class: [container_class, ("limit-container-width" unless fluid_layout)] } .prepend-top-20 .empty_wrapper %h3#repo-command-line-instructions.page-title-empty @@ -44,14 +45,14 @@ .git-empty %fieldset %h5 Git global setup - %pre.card.bg-light + %pre.bg-light :preserve git config --global user.name "#{h git_user_name}" git config --global user.email "#{h git_user_email}" %fieldset %h5 Create a new repository - %pre.card.bg-light + %pre.bg-light :preserve git clone #{ content_tag(:span, default_url_to_repo, class: 'clone')} cd #{h @project.path} @@ -64,7 +65,7 @@ %fieldset %h5 Existing folder - %pre.card.bg-light + %pre.bg-light :preserve cd existing_folder git init @@ -77,7 +78,7 @@ %fieldset %h5 Existing Git repository - %pre.card.bg-light + %pre.bg-light :preserve cd existing_repo git remote rename origin old-origin diff --git a/app/views/projects/issues/_new_branch.html.haml b/app/views/projects/issues/_new_branch.html.haml index 76438fae663..acb1a446ec4 100644 --- a/app/views/projects/issues/_new_branch.html.haml +++ b/app/views/projects/issues/_new_branch.html.haml @@ -18,7 +18,7 @@ %button.btn.js-create-merge-request.btn-success.btn-inverted{ type: 'button', data: { action: data_action } } = value - %button.btn.create-merge-request-dropdown-toggle.dropdown-toggle.btn-success.btn-inverted.js-dropdown-toggle{ type: 'button', data: { dropdown: { trigger: '#create-merge-request-dropdown' } } } + %button.btn.create-merge-request-dropdown-toggle.dropdown-toggle.btn-success.btn-inverted.js-dropdown-toggle{ type: 'button', data: { dropdown: { trigger: '#create-merge-request-dropdown' }, display: 'static' } } = icon('caret-down') .droplab-dropdown diff --git a/app/views/projects/jobs/show.html.haml b/app/views/projects/jobs/show.html.haml index ec9a04c0eab..1f33bb3a129 100644 --- a/app/views/projects/jobs/show.html.haml +++ b/app/views/projects/jobs/show.html.haml @@ -86,9 +86,7 @@ %button.js-scroll-down.btn-scroll.btn-transparent.btn-blank{ type: 'button', disabled: true } = custom_icon('scroll_down') - %pre.build-trace#build-trace - %code.bash.js-build-output - .build-loader-animation.js-build-refresh + = render 'shared/builds/build_output' - else = render "empty_states" diff --git a/app/views/projects/labels/index.html.haml b/app/views/projects/labels/index.html.haml index 9c78bade254..fb5b0fc15c9 100644 --- a/app/views/projects/labels/index.html.haml +++ b/app/views/projects/labels/index.html.haml @@ -1,40 +1,44 @@ - @no_container = true - page_title "Labels" -- hide_class = '' - can_admin_label = can?(current_user, :admin_label, @project) +- hide_class = '' + +- if can_admin_label + - content_for(:header_content) do + .nav-controls + = link_to _('New label'), new_project_label_path(@project), class: "btn btn-new" - if @labels.exists? || @prioritized_labels.exists? #promote-label-modal %div{ class: container_class } .top-area.adjust .nav-text - Labels can be applied to issues and merge requests. + = _('Labels can be applied to issues and merge requests.') - if can_admin_label - Star a label to make it a priority label. Order the prioritized labels to change their relative priority, by dragging. + = _('Star a label to make it a priority label. Order the prioritized labels to change their relative priority, by dragging.') - - if can_admin_label - .nav-controls - = link_to new_project_label_path(@project), class: "btn btn-new" do - New label - - .labels + .labels-container.prepend-top-5 - if can_admin_label -# Only show it in the first page - hide = @available_labels.empty? || (params[:page].present? && params[:page] != '1') .prioritized-labels{ class: ('hide' if hide) } - %h5 Prioritized Labels - %ul.content-list.manage-labels-list.js-prioritized-labels{ "data-url" => set_priorities_project_labels_path(@project) } - #js-priority-labels-empty-state{ class: "#{'hidden' unless @prioritized_labels.empty?}" } + %h5.prepend-top-10= _('Prioritized Labels') + .content-list.manage-labels-list.js-prioritized-labels{ "data-url" => set_priorities_project_labels_path(@project) } + #js-priority-labels-empty-state.priority-labels-empty-state{ class: "#{'hidden' unless @prioritized_labels.empty?}" } = render 'shared/empty_states/priority_labels' - if @prioritized_labels.present? - = render partial: 'shared/label', subject: @project, collection: @prioritized_labels, as: :label + = render partial: 'shared/label', subject: @project, collection: @prioritized_labels, as: :label, locals: { force_priority: true } - if @labels.present? .other-labels - if can_admin_label - %h5{ class: ('hide' if hide) } Other Labels - %ul.content-list.manage-labels-list.js-other-labels + %h5{ class: ('hide' if hide) }= _('Other Labels') + .content-list.manage-labels-list.js-other-labels = render partial: 'shared/label', subject: @project, collection: @labels, as: :label = paginate @labels, theme: 'gitlab' - else = render 'shared/empty_states/labels' + +%template#js-badge-item-template + %li.label-link-item.js-priority-badge.inline.prepend-left-10 + .label-badge.label-badge-blue= _('Prioritized label') diff --git a/app/views/projects/merge_requests/creations/_new_submit.html.haml b/app/views/projects/merge_requests/creations/_new_submit.html.haml index ebcd99f2a9b..b2eacabc21a 100644 --- a/app/views/projects/merge_requests/creations/_new_submit.html.haml +++ b/app/views/projects/merge_requests/creations/_new_submit.html.haml @@ -25,7 +25,7 @@ = custom_icon ('illustration_no_commits') - else %ul.merge-request-tabs.nav.nav-tabs.nav-links.no-top.no-bottom - %li.commits-tab.active + %li.commits-tab = link_to url_for(safe_params), data: {target: 'div#commits', action: 'new', toggle: 'tab'} do Commits %span.badge.badge-pill= @commits.size diff --git a/app/views/projects/merge_requests/diffs/_version_controls.html.haml b/app/views/projects/merge_requests/diffs/_version_controls.html.haml index 1c26f0405d2..52bf584d550 100644 --- a/app/views/projects/merge_requests/diffs/_version_controls.html.haml +++ b/app/views/projects/merge_requests/diffs/_version_controls.html.haml @@ -3,7 +3,7 @@ .mr-version-menus-container.content-block Changes between %span.dropdown.inline.mr-version-dropdown - %a.dropdown-toggle.btn.btn-default{ data: {toggle: :dropdown} } + %a.dropdown-toggle.btn.btn-default{ data: { toggle: :dropdown, display: 'static' } } %span - if @merge_request_diff.latest? latest version @@ -36,7 +36,7 @@ - if @merge_request_diff.base_commit_sha and %span.dropdown.inline.mr-version-compare-dropdown - %a.btn.btn-default.dropdown-toggle{ data: {toggle: :dropdown} } + %a.btn.btn-default.dropdown-toggle{ data: { toggle: :dropdown, display: 'static' } } - if @start_version version #{version_index(@start_version)} - else diff --git a/app/views/projects/merge_requests/show.html.haml b/app/views/projects/merge_requests/show.html.haml index 01e38ffee20..2f1877a15c2 100644 --- a/app/views/projects/merge_requests/show.html.haml +++ b/app/views/projects/merge_requests/show.html.haml @@ -32,26 +32,25 @@ .scrolling-tabs-container.inner-page-scroll-tabs.is-smaller .fade-left= icon('angle-left') .fade-right= icon('angle-right') - .nav-links.scrolling-tabs.nav.nav-tabs - %ul.merge-request-tabs.nav-tabs.nav - %li.notes-tab - = tab_link_for @merge_request, :show, force_link: @commit.present? do - Discussion - %span.badge.badge-pill= @merge_request.related_notes.user.count - - if @merge_request.source_project - %li.commits-tab - = tab_link_for @merge_request, :commits do - Commits - %span.badge.badge-pill= @commits_count - - if @pipelines.any? - %li.pipelines-tab - = tab_link_for @merge_request, :pipelines do - Pipelines - %span.badge.badge-pill.js-pipelines-mr-count= @pipelines.size - %li.diffs-tab - = tab_link_for @merge_request, :diffs do - Changes - %span.badge.badge-pill= @merge_request.diff_size + %ul.merge-request-tabs.nav-tabs.nav.nav-links.scrolling-tabs + %li.notes-tab + = tab_link_for @merge_request, :show, force_link: @commit.present? do + Discussion + %span.badge.badge-pill= @merge_request.related_notes.user.count + - if @merge_request.source_project + %li.commits-tab + = tab_link_for @merge_request, :commits do + Commits + %span.badge.badge-pill= @commits_count + - if @pipelines.any? + %li.pipelines-tab + = tab_link_for @merge_request, :pipelines do + Pipelines + %span.badge.badge-pill.js-pipelines-mr-count= @pipelines.size + %li.diffs-tab + = tab_link_for @merge_request, :diffs do + Changes + %span.badge.badge-pill= @merge_request.diff_size - if has_vue_discussions_cookie? #js-vue-discussion-counter diff --git a/app/views/projects/milestones/show.html.haml b/app/views/projects/milestones/show.html.haml index b478fbbb15e..f7b04c436a6 100644 --- a/app/views/projects/milestones/show.html.haml +++ b/app/views/projects/milestones/show.html.haml @@ -69,6 +69,8 @@ .wiki = markdown_field(@milestone, :description) + = render_if_exists 'shared/milestones/burndown', milestone: @milestone, project: @project + - if can?(current_user, :read_issue, @project) && @milestone.total_items_count(current_user).zero? .alert.alert-success.prepend-top-default %span Assign some issues to this milestone. diff --git a/app/views/projects/new.html.haml b/app/views/projects/new.html.haml index 35a09f06bfa..5bb1bfb7059 100644 --- a/app/views/projects/new.html.haml +++ b/app/views/projects/new.html.haml @@ -29,16 +29,16 @@ .col-lg-9.js-toggle-container %ul.nav.nav-tabs.nav-links.gitlab-tabs{ role: 'tablist' } - %li{ class: active_when(active_tab == 'blank'), role: 'presentation' } - %a{ href: '#blank-project-pane', id: 'blank-project-tab', data: { toggle: 'tab' }, role: 'tab' } + %li.nav-item{ role: 'presentation' } + %a.nav-link.active{ href: '#blank-project-pane', id: 'blank-project-tab', data: { toggle: 'tab' }, role: 'tab' } %span.d-none.d-sm-block Blank project %span.d-block.d-sm-none Blank - %li{ class: active_when(active_tab == 'template'), role: 'presentation' } - %a{ href: '#create-from-template-pane', id: 'create-from-template-tab', data: { toggle: 'tab' }, role: 'tab' } + %li.nav-item{ role: 'presentation' } + %a.nav-link{ href: '#create-from-template-pane', id: 'create-from-template-tab', data: { toggle: 'tab' }, role: 'tab' } %span.d-none.d-sm-block Create from template %span.d-block.d-sm-none Template - %li{ class: active_when(active_tab == 'import'), role: 'presentation' } - %a{ href: '#import-project-pane', id: 'import-project-tab', data: { toggle: 'tab' }, role: 'tab' } + %li.nav-item{ role: 'presentation' } + %a.nav-link{ href: '#import-project-pane', id: 'import-project-tab', data: { toggle: 'tab' }, role: 'tab' } %span.d-none.d-sm-block Import project %span.d-block.d-sm-none Import diff --git a/app/views/projects/pages/_destroy.haml b/app/views/projects/pages/_destroy.haml index 4ada19a1368..9b77c4e3494 100644 --- a/app/views/projects/pages/_destroy.haml +++ b/app/views/projects/pages/_destroy.haml @@ -5,7 +5,7 @@ .errors-holder .card-body %p - Removing the pages will prevent from exposing them to outside world. + Removing pages will prevent them from being exposed to the outside world. .form-actions = link_to 'Remove pages', project_pages_path(@project), data: { confirm: 'Are you sure?'}, method: :delete, class: "btn btn-remove" - else diff --git a/app/views/projects/pipelines/_with_tabs.html.haml b/app/views/projects/pipelines/_with_tabs.html.haml index 118391aac64..951f80b378d 100644 --- a/app/views/projects/pipelines/_with_tabs.html.haml +++ b/app/views/projects/pipelines/_with_tabs.html.haml @@ -1,7 +1,7 @@ .tabs-holder %ul.pipelines-tabs.nav-links.no-top.no-bottom.mobile-separator.nav.nav-tabs %li.js-pipeline-tab-link - = link_to project_pipeline_path(@project, @pipeline), data: { target: '#js-tab-pipeline', action: 'pipelines', toggle: 'tab' }, class: 'pipeline-tab' do + = link_to project_pipeline_path(@project, @pipeline), data: { target: '#js-tab-pipeline', action: 'pipelines', toggle: 'tab' }, class: 'pipeline-tab' do = _("Pipeline") %li.js-builds-tab-link = link_to builds_project_pipeline_path(@project, @pipeline), data: { target: '#js-tab-builds', action: 'builds', toggle: 'tab' }, class: 'builds-tab' do @@ -43,12 +43,36 @@ = render partial: "projects/stage/stage", collection: pipeline.legacy_stages, as: :stage - if @pipeline.failed_builds.present? - #js-tab-failures.build-failures.tab-pane - - @pipeline.failed_builds.each_with_index do |build, index| - .build-state - %span.ci-status-icon-failed= custom_icon('icon_status_failed') - %span.stage - = build.stage.titleize - %span.build-name - = link_to build.name, pipeline_job_url(pipeline, build) - %pre.build-log= build_summary(build, skip: index >= 10) + #js-tab-failures.build-failures.tab-pane.build-page + %table.table.responsive-table.ci-table.responsive-table-sm-rounded + %thead + %th.table-th-transparent + %th.table-th-transparent= _("Name") + %th.table-th-transparent= _("Stage") + %th.table-th-transparent= _("Failure") + + %tbody + - @pipeline.failed_builds.each_with_index do |build, index| + - job = build.present(current_user: current_user) + %tr.build-state.responsive-table-border-start + %td.responsive-table-cell.ci-status-icon-failed{ data: { column: "Status"} } + .d-none.d-md-block.build-icon + = custom_icon("icon_status_#{build.status}") + .d-md-none.build-badge + = render "ci/status/badge", link: false, status: job.detailed_status(current_user) + %td.responsive-table-cell.build-name{ data: { column: _("Name")} } + = link_to build.name, pipeline_job_url(pipeline, build) + %td.responsive-table-cell.build-stage{ data: { column: _("Stage")} } + = build.stage.titleize + %td.responsive-table-cell.build-failure{ data: { column: _("Failure")} } + = build.present.callout_failure_message + %td.responsive-table-cell.build-actions + = link_to retry_project_job_path(build.project, build, return_to: request.original_url), method: :post, title: _('Retry'), class: 'btn btn-build' do + = icon('repeat') + %tr.build-trace-row.responsive-table-border-end + %td + %td.responsive-table-cell.build-trace-container{ colspan: 4 } + %pre.build-trace.build-trace-rounded + %code.bash.js-build-output + = build_summary(build) + diff --git a/app/views/projects/project_members/_groups.html.haml b/app/views/projects/project_members/_groups.html.haml index 128f52ff648..3f05e06b0c6 100644 --- a/app/views/projects/project_members/_groups.html.haml +++ b/app/views/projects/project_members/_groups.html.haml @@ -3,5 +3,5 @@ Groups with access to %strong= @project.name %span.badge.badge-pill= group_links.size - %ul.content-list + %ul.content-list.members-list = render partial: 'shared/members/group', collection: group_links, as: :group_link diff --git a/app/views/projects/project_members/_team.html.haml b/app/views/projects/project_members/_team.html.haml index a30870a241c..0c5a187f208 100644 --- a/app/views/projects/project_members/_team.html.haml +++ b/app/views/projects/project_members/_team.html.haml @@ -9,9 +9,10 @@ %span.badge.badge-pill= members.total_count = form_tag project_project_members_path(project), method: :get, class: 'form-inline member-search-form flex-project-members-form' do .form-group - = search_field_tag :search, params[:search], { placeholder: 'Find existing members by name', class: 'form-control', spellcheck: false } - %button.member-search-btn{ type: "submit", "aria-label" => "Submit search" } - = icon("search") + .position-relative + = search_field_tag :search, params[:search], { placeholder: 'Find existing members by name', class: 'form-control', spellcheck: false } + %button.member-search-btn{ type: "submit", "aria-label" => "Submit search" } + = icon("search") = render 'shared/members/sort_dropdown' %ul.content-list.members-list = render partial: 'shared/members/member', collection: members, as: :member diff --git a/app/views/projects/project_members/index.html.haml b/app/views/projects/project_members/index.html.haml index a56023e98cd..9716322f8a1 100644 --- a/app/views/projects/project_members/index.html.haml +++ b/app/views/projects/project_members/index.html.haml @@ -12,17 +12,17 @@ - else %p Members can be added by project - %i Masters + %i Maintainers or %i Owners .light - if can?(current_user, :admin_project_member, @project) %ul.nav-links.nav.nav-tabs.gitlab-tabs{ role: 'tablist' } - %li.active{ role: 'presentation' } - %a{ href: '#add-member-pane', id: 'add-member-tab', data: { toggle: 'tab' }, role: 'tab' } Add member + %li.nav-tab{ role: 'presentation' } + %a.nav-link.active{ href: '#add-member-pane', id: 'add-member-tab', data: { toggle: 'tab' }, role: 'tab' } Add member - if @project.allowed_to_share_with_group? - %li{ role: 'presentation' } - %a{ href: '#share-with-group-pane', id: 'share-with-group-tab', data: { toggle: 'tab' }, role: 'tab' } Share with group + %li.nav-tab{ role: 'presentation' } + %a.nav-link{ href: '#share-with-group-pane', id: 'share-with-group-tab', data: { toggle: 'tab' }, role: 'tab' } Share with group .tab-content.gitlab-tab-content .tab-pane.active{ id: 'add-member-pane', role: 'tabpanel' } diff --git a/app/views/projects/protected_branches/shared/_branches_list.html.haml b/app/views/projects/protected_branches/shared/_branches_list.html.haml index a2cd7752fc4..9a06eca89bb 100644 --- a/app/views/projects/protected_branches/shared/_branches_list.html.haml +++ b/app/views/projects/protected_branches/shared/_branches_list.html.haml @@ -1,7 +1,7 @@ .protected-branches-list.js-protected-branches-list.qa-protected-branches-list - if @protected_branches.empty? - .card-header - %h3.card-title + .card-header.bg-white + %h3.card-title.mb-0 Protected branch (#{@protected_branches_count}) %p.settings-message.text-center There are currently no protected branches, protect a branch with the form above. diff --git a/app/views/projects/protected_branches/shared/_index.html.haml b/app/views/projects/protected_branches/shared/_index.html.haml index fd5c1aa342a..4f1c6c92484 100644 --- a/app/views/projects/protected_branches/shared/_index.html.haml +++ b/app/views/projects/protected_branches/shared/_index.html.haml @@ -1,6 +1,6 @@ - expanded = Rails.env.test? -%section.settings.no-animate{ class: ('expanded' if expanded) } +%section.qa-protected-branches-settings.settings.no-animate{ class: ('expanded' if expanded) } .settings-header %h4 Protected Branches @@ -12,8 +12,8 @@ %p By default, protected branches are designed to: %ul - %li prevent their creation, if not already created, from everybody except Masters - %li prevent pushes from everybody except Masters + %li prevent their creation, if not already created, from everybody except Maintainers + %li prevent pushes from everybody except Maintainers %li prevent <strong>anyone</strong> from force pushing to the branch %li prevent <strong>anyone</strong> from deleting the branch %p Read more about #{link_to "protected branches", help_page_path("user/project/protected_branches")} and #{link_to "project permissions", help_page_path("user/permissions")}. diff --git a/app/views/projects/protected_tags/shared/_index.html.haml b/app/views/projects/protected_tags/shared/_index.html.haml index c33723d8072..fe2903b456f 100644 --- a/app/views/projects/protected_tags/shared/_index.html.haml +++ b/app/views/projects/protected_tags/shared/_index.html.haml @@ -12,7 +12,7 @@ %p By default, protected tags are designed to: %ul - %li Prevent tag creation by everybody except Masters + %li Prevent tag creation by everybody except Maintainers %li Prevent <strong>anyone</strong> from updating the tag %li Prevent <strong>anyone</strong> from deleting the tag diff --git a/app/views/projects/refs/logs_tree.js.haml b/app/views/projects/refs/logs_tree.js.haml index d07bb661615..506bf54b3f8 100644 --- a/app/views/projects/refs/logs_tree.js.haml +++ b/app/views/projects/refs/logs_tree.js.haml @@ -8,6 +8,8 @@ row.find("td.tree-time-ago").html('#{escape_javascript time_ago_with_tooltip(commit.committed_date)}'); row.find("td.tree-commit").html('#{escape_javascript render("projects/tree/tree_commit_column", commit: commit)}'); + = render_if_exists 'projects/refs/logs_tree_lock_label', lock_label: content_data[:lock_label] + - if @more_log_url :plain if($('#tree-slider').length) { diff --git a/app/views/projects/releases/edit.html.haml b/app/views/projects/releases/edit.html.haml index b4787032966..d6f758608a0 100644 --- a/app/views/projects/releases/edit.html.haml +++ b/app/views/projects/releases/edit.html.haml @@ -13,7 +13,7 @@ = form_for(@release, method: :put, url: project_tag_release_path(@project, @tag.name), html: { class: 'common-note-form release-form js-quick-submit' }) do |f| = render layout: 'projects/md_preview', locals: { url: preview_markdown_path(@project), referenced_users: true } do - = render 'projects/zen', f: f, attr: :description, classes: 'note-textarea', placeholder: "Write your release notes or drag files here..." + = render 'projects/zen', f: f, attr: :description, classes: 'note-textarea', placeholder: "Write your release notes or drag files here…" = render 'shared/notes/hints' .error-alert .prepend-top-default diff --git a/app/views/projects/runners/_group_runners.html.haml b/app/views/projects/runners/_group_runners.html.haml index dfed0553f84..86de71c732b 100644 --- a/app/views/projects/runners/_group_runners.html.haml +++ b/app/views/projects/runners/_group_runners.html.haml @@ -26,9 +26,9 @@ - if can?(current_user, :admin_pipeline, @project.group) - group_link = link_to _('Group CI/CD settings'), group_settings_ci_cd_path(@project.group) - = _('Group masters can register group runners in the %{link}').html_safe % { link: group_link } + = _('Group maintainers can register group runners in the %{link}').html_safe % { link: group_link } - else - = _('Ask your group master to setup a group Runner.') + = _('Ask your group maintainer to setup a group Runner.') - else %h4.underlined-title diff --git a/app/views/projects/services/_index.html.haml b/app/views/projects/services/_index.html.haml index acbab8b85c9..16e48814578 100644 --- a/app/views/projects/services/_index.html.haml +++ b/app/views/projects/services/_index.html.haml @@ -8,7 +8,7 @@ %colgroup %col %col - %col.d-none.d-sm-block + %col %col{ width: "120" } %thead %tr diff --git a/app/views/projects/services/mattermost_slash_commands/_help.html.haml b/app/views/projects/services/mattermost_slash_commands/_help.html.haml index 62bef77be97..b20614dc88f 100644 --- a/app/views/projects/services/mattermost_slash_commands/_help.html.haml +++ b/app/views/projects/services/mattermost_slash_commands/_help.html.haml @@ -1,18 +1,19 @@ - enabled = Gitlab.config.mattermost.enabled -.card - %p - This service allows users to perform common operations on this - project by entering slash commands in Mattermost. - = link_to help_page_path('user/project/integrations/mattermost_slash_commands.md'), target: '_blank' do - View documentation - = icon('external-link') - %p.inline - See list of available commands in Mattermost after setting up this service, - by entering - %kbd.inline /<trigger> help - - unless enabled || @service.template? - = render 'projects/services/mattermost_slash_commands/detailed_help', subject: @service +.info-well + .well-segment + %p + This service allows users to perform common operations on this + project by entering slash commands in Mattermost. + = link_to help_page_path('user/project/integrations/mattermost_slash_commands.md'), target: '_blank' do + View documentation + = icon('external-link') + %p.inline + See list of available commands in Mattermost after setting up this service, + by entering + %kbd.inline /<trigger> help + - unless enabled || @service.template? + = render 'projects/services/mattermost_slash_commands/detailed_help', subject: @service - if enabled && !@service.template? = render 'projects/services/mattermost_slash_commands/installation_info', subject: @service diff --git a/app/views/projects/services/prometheus/_configuration_banner.html.haml b/app/views/projects/services/prometheus/_configuration_banner.html.haml index 2cc2a6b2b5b..898b55e4b39 100644 --- a/app/views/projects/services/prometheus/_configuration_banner.html.haml +++ b/app/views/projects/services/prometheus/_configuration_banner.html.haml @@ -2,7 +2,7 @@ = s_('PrometheusService|Auto configuration') - if service.manual_configuration? - .well + .info-well = s_('PrometheusService|To enable the installation of Prometheus on your clusters, deactivate the manual configuration below') - else .container-fluid diff --git a/app/views/projects/services/prometheus/_help.html.haml b/app/views/projects/services/prometheus/_help.html.haml index 15e7362c2ba..35d655e4b32 100644 --- a/app/views/projects/services/prometheus/_help.html.haml +++ b/app/views/projects/services/prometheus/_help.html.haml @@ -5,5 +5,5 @@ = s_('PrometheusService|Manual configuration') - unless @service.editable? - .card + .info-well = s_('PrometheusService|To enable manual configuration, uninstall Prometheus from your clusters') diff --git a/app/views/projects/services/slack_slash_commands/_help.html.haml b/app/views/projects/services/slack_slash_commands/_help.html.haml index 44f58ad05e5..9d045d84b52 100644 --- a/app/views/projects/services/slack_slash_commands/_help.html.haml +++ b/app/views/projects/services/slack_slash_commands/_help.html.haml @@ -1,99 +1,100 @@ - pretty_name = defined?(@project) ? @project.full_name : 'namespace / path' - run_actions_text = "Perform common operations on GitLab project: #{pretty_name}" -.card - %p - This service allows users to perform common operations on this - project by entering slash commands in Slack. - = link_to help_page_path('user/project/integrations/slack_slash_commands.md'), target: '_blank' do - View documentation - = icon('external-link') - %p.inline - See list of available commands in Slack after setting up this service, - by entering - %kbd.inline /<command> help - - unless @service.template? - %p To setup this service: - %ul.list-unstyled.indent-list - %li - 1. - = link_to 'https://my.slack.com/services/new/slash-commands', target: '_blank', rel: 'noreferrer noopener nofollow' do - Add a slash command - = icon('external-link') - in your Slack team with these options: +.info-well + .well-segment + %p + This service allows users to perform common operations on this + project by entering slash commands in Slack. + = link_to help_page_path('user/project/integrations/slack_slash_commands.md'), target: '_blank' do + View documentation + = icon('external-link') + %p.inline + See list of available commands in Slack after setting up this service, + by entering + %kbd.inline /<command> help + - unless @service.template? + %p To setup this service: + %ul.list-unstyled.indent-list + %li + 1. + = link_to 'https://my.slack.com/services/new/slash-commands', target: '_blank', rel: 'noreferrer noopener nofollow' do + Add a slash command + = icon('external-link') + in your Slack team with these options: - %hr + %hr - .help-form - .form-group - = label_tag nil, 'Command', class: 'col-sm-2 col-12 col-form-label' - .col-sm-10.col-12.text-block - %p Fill in the word that works best for your team. - %p - Suggestions: - %code= 'gitlab' - %code= @project.path # Path contains no spaces, but dashes - %code= @project.full_path + .help-form + .form-group + = label_tag nil, 'Command', class: 'col-sm-2 col-12 col-form-label' + .col-sm-10.col-12.text-block + %p Fill in the word that works best for your team. + %p + Suggestions: + %code= 'gitlab' + %code= @project.path # Path contains no spaces, but dashes + %code= @project.full_path - .form-group - = label_tag :url, 'URL', class: 'col-sm-2 col-12 col-form-label' - .col-sm-10.col-12.input-group - = text_field_tag :url, service_trigger_url(subject), class: 'form-control form-control-sm', readonly: 'readonly' - .input-group-append - = clipboard_button(target: '#url', class: 'input-group-text') + .form-group + = label_tag :url, 'URL', class: 'col-sm-2 col-12 col-form-label' + .col-sm-10.col-12.input-group + = text_field_tag :url, service_trigger_url(subject), class: 'form-control form-control-sm', readonly: 'readonly' + .input-group-append + = clipboard_button(target: '#url', class: 'input-group-text') - .form-group - = label_tag nil, 'Method', class: 'col-sm-2 col-12 col-form-label' - .col-sm-10.col-12.text-block POST + .form-group + = label_tag nil, 'Method', class: 'col-sm-2 col-12 col-form-label' + .col-sm-10.col-12.text-block POST - .form-group - = label_tag :customize_name, 'Customize name', class: 'col-sm-2 col-12 col-form-label' - .col-sm-10.col-12.input-group - = text_field_tag :customize_name, 'GitLab', class: 'form-control form-control-sm', readonly: 'readonly' - .input-group-append - = clipboard_button(target: '#customize_name', class: 'input-group-text') + .form-group + = label_tag :customize_name, 'Customize name', class: 'col-sm-2 col-12 col-form-label' + .col-sm-10.col-12.input-group + = text_field_tag :customize_name, 'GitLab', class: 'form-control form-control-sm', readonly: 'readonly' + .input-group-append + = clipboard_button(target: '#customize_name', class: 'input-group-text') - .form-group - = label_tag nil, 'Customize icon', class: 'col-sm-2 col-12 col-form-label' - .col-sm-10.col-12.text-block - = image_tag(asset_url('slash-command-logo.png'), width: 36, height: 36) - = link_to('Download image', asset_url('gitlab_logo.png'), class: 'btn btn-sm', target: '_blank', rel: 'noopener noreferrer') + .form-group + = label_tag nil, 'Customize icon', class: 'col-sm-2 col-12 col-form-label' + .col-sm-10.col-12.text-block + = image_tag(asset_url('slash-command-logo.png'), width: 36, height: 36) + = link_to('Download image', asset_url('gitlab_logo.png'), class: 'btn btn-sm', target: '_blank', rel: 'noopener noreferrer') - .form-group - = label_tag nil, 'Autocomplete', class: 'col-sm-2 col-12 col-form-label' - .col-sm-10.col-12.text-block Show this command in the autocomplete list + .form-group + = label_tag nil, 'Autocomplete', class: 'col-sm-2 col-12 col-form-label' + .col-sm-10.col-12.text-block Show this command in the autocomplete list - .form-group - = label_tag :autocomplete_description, 'Autocomplete description', class: 'col-sm-2 col-12 col-form-label' - .col-sm-10.col-12.input-group - = text_field_tag :autocomplete_description, run_actions_text, class: 'form-control form-control-sm', readonly: 'readonly' - .input-group-append - = clipboard_button(target: '#autocomplete_description', class: 'input-group-text') + .form-group + = label_tag :autocomplete_description, 'Autocomplete description', class: 'col-sm-2 col-12 col-form-label' + .col-sm-10.col-12.input-group + = text_field_tag :autocomplete_description, run_actions_text, class: 'form-control form-control-sm', readonly: 'readonly' + .input-group-append + = clipboard_button(target: '#autocomplete_description', class: 'input-group-text') - .form-group - = label_tag :autocomplete_usage_hint, 'Autocomplete usage hint', class: 'col-sm-2 col-12 col-form-label' - .col-sm-10.col-12.input-group - = text_field_tag :autocomplete_usage_hint, '[help]', class: 'form-control form-control-sm', readonly: 'readonly' - .input-group-append - = clipboard_button(target: '#autocomplete_usage_hint', class: 'input-group-text') + .form-group + = label_tag :autocomplete_usage_hint, 'Autocomplete usage hint', class: 'col-sm-2 col-12 col-form-label' + .col-sm-10.col-12.input-group + = text_field_tag :autocomplete_usage_hint, '[help]', class: 'form-control form-control-sm', readonly: 'readonly' + .input-group-append + = clipboard_button(target: '#autocomplete_usage_hint', class: 'input-group-text') - .form-group - = label_tag :descriptive_label, 'Descriptive label', class: 'col-sm-2 col-12 col-form-label' - .col-sm-10.col-12.input-group - = text_field_tag :descriptive_label, 'Perform common operations on GitLab project', class: 'form-control form-control-sm', readonly: 'readonly' - .input-group-append - = clipboard_button(target: '#descriptive_label', class: 'input-group-text') + .form-group + = label_tag :descriptive_label, 'Descriptive label', class: 'col-sm-2 col-12 col-form-label' + .col-sm-10.col-12.input-group + = text_field_tag :descriptive_label, 'Perform common operations on GitLab project', class: 'form-control form-control-sm', readonly: 'readonly' + .input-group-append + = clipboard_button(target: '#descriptive_label', class: 'input-group-text') - %hr + %hr - %ul.list-unstyled.indent-list - %li - 2. Paste the - %strong Token - into the field below - %li - 3. Select the - %strong Active - checkbox, press - %strong Save changes - and start using GitLab inside Slack! + %ul.list-unstyled.indent-list + %li + 2. Paste the + %strong Token + into the field below + %li + 3. Select the + %strong Active + checkbox, press + %strong Save changes + and start using GitLab inside Slack! diff --git a/app/views/projects/settings/ci_cd/_autodevops_form.html.haml b/app/views/projects/settings/ci_cd/_autodevops_form.html.haml index bbabb98dafe..4359362bb05 100644 --- a/app/views/projects/settings/ci_cd/_autodevops_form.html.haml +++ b/app/views/projects/settings/ci_cd/_autodevops_form.html.haml @@ -1,43 +1,66 @@ -.row.prepend-top-default +.row .col-lg-12 = form_for @project, url: project_settings_ci_cd_path(@project) do |f| = form_errors(@project) - %fieldset.builds-feature + %fieldset.builds-feature.js-auto-devops-settings .form-group - message = auto_devops_warning_message(@project) - ci_file_formatted = '<code>.gitlab-ci.yml</code>'.html_safe - if message - %p.settings-message.text-center + %p.auto-devops-warning-message.settings-message.text-center = message.html_safe = f.fields_for :auto_devops_attributes, @auto_devops do |form| - .form-check - = form.radio_button :enabled, 'true', class: 'form-check-input' - = form.label :enabled_true, class: 'form-check-label' do - %strong= s_('CICD|Enable Auto DevOps') - %br - = s_('CICD|The Auto DevOps pipeline configuration will be used when there is no %{ci_file} in the project.').html_safe % { ci_file: ci_file_formatted } + .card.auto-devops-card + .card-body + .form-check + = form.radio_button :enabled, 'true', class: 'form-check-input js-toggle-extra-settings' + = form.label :enabled_true, class: 'form-check-label' do + %strong= s_('CICD|Enable Auto DevOps') + .form-text.text-muted + = s_('CICD|The Auto DevOps pipeline configuration will be used when there is no %{ci_file} in the project.').html_safe % { ci_file: ci_file_formatted } - .form-check - = form.radio_button :enabled, 'false', class: 'form-check-input' - = form.label :enabled_false, class: 'form-check-label' do - %strong= s_('CICD|Disable Auto DevOps') - %br - = s_('CICD|An explicit %{ci_file} needs to be specified before you can begin using Continuous Integration and Delivery.').html_safe % { ci_file: ci_file_formatted } + .card.auto-devops-card + .card-body + .form-check + = form.radio_button :enabled, '', class: 'form-check-input js-toggle-extra-settings' + = form.label :enabled_, class: 'form-check-label' do + %strong= s_('CICD|Instance default (%{state})') % { state: "#{Gitlab::CurrentSettings.auto_devops_enabled? ? _('enabled') : _('disabled')}" } + .form-text.text-muted + = s_('CICD|Follow the instance default to either have Auto DevOps enabled or disabled when there is no project specific %{ci_file}.').html_safe % { ci_file: ci_file_formatted } - .form-check - = form.radio_button :enabled, '', class: 'form-check-input' - = form.label :enabled_, class: 'form-check-label' do - %strong= s_('CICD|Instance default (%{state})') % { state: "#{Gitlab::CurrentSettings.auto_devops_enabled? ? _('enabled') : _('disabled')}" } - %br - = s_('CICD|Follow the instance default to either have Auto DevOps enabled or disabled when there is no project specific %{ci_file}.').html_safe % { ci_file: ci_file_formatted } + .card.auto-devops-card.js-extra-settings{ class: form.object&.enabled == false ? 'hidden' : nil } + .card-body.bg-light + = form.label :domain do + %strong= _('Domain') + = form.text_field :domain, class: 'form-control', placeholder: 'domain.com' + .form-text.text-muted + = s_('CICD|You need to specify a domain if you want to use Auto Review Apps and Auto Deploy stages.') + - if cluster_ingress_ip = cluster_ingress_ip(@project) + = s_('%{nip_domain} can be used as an alternative to a custom domain.').html_safe % { nip_domain: "<code>#{cluster_ingress_ip}.nip.io</code>".html_safe } + = link_to icon('question-circle'), help_page_path('topics/autodevops/index.md', anchor: 'auto-devops-base-domain'), target: '_blank' - = form.label :domain, class:"prepend-top-10" do - = _('Domain') - = form.text_field :domain, class: 'form-control', placeholder: 'domain.com' - .form-text.text-muted - = s_('CICD|A domain is required to use Auto Review Apps and Auto Deploy Stages.') - - if cluster_ingress_ip = cluster_ingress_ip(@project) - = s_('%{nip_domain} can be used as an alternative to a custom domain.').html_safe % { nip_domain: "<code>#{cluster_ingress_ip}.nip.io</code>".html_safe } - = link_to icon('question-circle'), help_page_path('topics/autodevops/index.md', anchor: 'auto-devops-base-domain'), target: '_blank' + %label.prepend-top-10 + %strong= s_('CICD|Deployment strategy') + %p.settings-message.text-center + = s_('CICD|Deployment strategy needs a domain name to work correctly.') + .form-check + = form.radio_button :deploy_strategy, 'continuous', class: 'form-check-input' + = form.label :deploy_strategy_continuous, class: 'form-check-label' do + %strong= s_('CICD|Continuous deployment to production') + = link_to icon('question-circle'), help_page_path('topics/autodevops/index.md', anchor: 'auto-deploy'), target: '_blank' + .form-check + = form.radio_button :deploy_strategy, 'manual', class: 'form-check-input' + = form.label :deploy_strategy_manual, class: 'form-check-label' do + %strong= s_('CICD|Automatic deployment to staging, manual deployment to production') + = link_to icon('question-circle'), help_page_path('ci/environments.md', anchor: 'manually-deploying-to-environments'), target: '_blank' + + .card.auto-devops-card + .card-body + .form-check + = form.radio_button :enabled, 'false', class: 'form-check-input js-toggle-extra-settings', data: { hide_extra_settings: true } + = form.label :enabled_false, class: 'form-check-label' do + %strong= s_('CICD|Disable Auto DevOps') + .form-text.text-muted + = s_('CICD|An explicit %{ci_file} needs to be specified before you can begin using Continuous Integration and Delivery.').html_safe % { ci_file: ci_file_formatted } = f.submit 'Save changes', class: "btn btn-success prepend-top-15" diff --git a/app/views/projects/settings/ci_cd/_form.html.haml b/app/views/projects/settings/ci_cd/_form.html.haml index e93b240a007..7142a9d635e 100644 --- a/app/views/projects/settings/ci_cd/_form.html.haml +++ b/app/views/projects/settings/ci_cd/_form.html.haml @@ -7,7 +7,7 @@ = f.label :runners_token, "Runner token", class: 'label-light' .form-control.js-secret-value-placeholder = '*' * 20 - = f.text_field :runners_token, class: "form-control hidden js-secret-value", placeholder: 'xEeFCaDAB89' + = f.text_field :runners_token, class: "form-control hide js-secret-value", placeholder: 'xEeFCaDAB89' %p.form-text.text-muted The secure token used by the Runner to checkout the project %button.btn.btn-info.prepend-top-10.js-secret-value-reveal-button{ type: 'button', data: { secret_reveal_status: 'false' } } = _('Reveal value') diff --git a/app/views/projects/settings/ci_cd/show.html.haml b/app/views/projects/settings/ci_cd/show.html.haml index ed17bd4f7dc..56c175f5649 100644 --- a/app/views/projects/settings/ci_cd/show.html.haml +++ b/app/views/projects/settings/ci_cd/show.html.haml @@ -8,7 +8,7 @@ %section.settings#js-general-pipeline-settings.no-animate{ class: ('expanded' if general_expanded) } .settings-header %h4 - General pipelines settings + General pipelines %button.btn.js-settings-toggle{ type: 'button' } = expanded ? 'Collapse' : 'Expand' %p @@ -16,7 +16,7 @@ .settings-content = render 'form' -%section.settings#autodevops-settings.no-animate{ class: ('expanded' if expanded) } +%section.qa-autodevops-settings.settings#autodevops-settings.no-animate{ class: ('expanded' if expanded) } .settings-header %h4 = s_('CICD|Auto DevOps') @@ -28,10 +28,10 @@ .settings-content = render 'autodevops_form' -%section.settings.no-animate{ class: ('expanded' if expanded) } +%section.qa-runners-settings.settings.no-animate{ class: ('expanded' if expanded) } .settings-header %h4 - Runners settings + Runners %button.btn.js-settings-toggle{ type: 'button' } = expanded ? 'Collapse' : 'Expand' %p @@ -39,11 +39,11 @@ .settings-content = render 'projects/runners/index' -%section.settings.no-animate{ class: ('expanded' if expanded) } +%section.qa-variables-settings.settings.no-animate{ class: ('expanded' if expanded) } .settings-header %h4 = _('Variables') - = link_to icon('question-circle'), help_page_path('ci/variables/README', anchor: 'secret-variables'), target: '_blank', rel: 'noopener noreferrer' + = link_to icon('question-circle'), help_page_path('ci/variables/README', anchor: 'variables'), target: '_blank', rel: 'noopener noreferrer' %button.btn.js-settings-toggle{ type: 'button' } = expanded ? 'Collapse' : 'Expand' %p.append-bottom-0 diff --git a/app/views/projects/tags/new.html.haml b/app/views/projects/tags/new.html.haml index b596748ca5f..da822ac5675 100644 --- a/app/views/projects/tags/new.html.haml +++ b/app/views/projects/tags/new.html.haml @@ -36,7 +36,7 @@ = label_tag :release_description, s_('TagsPage|Release notes'), class: 'col-form-label col-sm-2' .col-sm-10 = render layout: 'projects/md_preview', locals: { url: preview_markdown_path(@project), referenced_users: true } do - = render 'projects/zen', attr: :release_description, classes: 'note-textarea', placeholder: s_('TagsPage|Write your release notes or drag files here...'), current_text: @release_description + = render 'projects/zen', attr: :release_description, classes: 'note-textarea', placeholder: s_('TagsPage|Write your release notes or drag files here…'), current_text: @release_description = render 'shared/notes/hints' .form-text.text-muted = s_('TagsPage|Optionally, add release notes to the tag. They will be stored in the GitLab database and displayed on the tags page.') diff --git a/app/views/projects/tree/_tree_header.html.haml b/app/views/projects/tree/_tree_header.html.haml index 4fc1a284693..9d196075bf1 100644 --- a/app/views/projects/tree/_tree_header.html.haml +++ b/app/views/projects/tree/_tree_header.html.haml @@ -6,7 +6,7 @@ = render 'shared/ref_switcher', destination: 'tree', path: @path, show_create: true - if on_top_of_branch? - - addtotree_toggle_attributes = { href: '#', 'data-toggle': 'dropdown', 'data-target': '.add-to-tree-dropdown' } + - addtotree_toggle_attributes = { href: '#', 'data-toggle': 'dropdown', 'data-target': '.add-to-tree-dropdown', 'data-boundary': 'window' } - else - addtotree_toggle_attributes = { title: _("You can only add files when you are on a branch"), data: { container: 'body' }, class: 'disabled has-tooltip' } diff --git a/app/views/projects/wikis/_form.html.haml b/app/views/projects/wikis/_form.html.haml index bcceb69954a..26fe1de31fe 100644 --- a/app/views/projects/wikis/_form.html.haml +++ b/app/views/projects/wikis/_form.html.haml @@ -25,7 +25,7 @@ .col-sm-12= f.label :content, class: 'control-label-full-width' .col-sm-12 = render layout: 'projects/md_preview', locals: { url: project_wiki_preview_markdown_path(@project, @page.slug) } do - = render 'projects/zen', f: f, attr: :content, classes: 'note-textarea', placeholder: s_("WikiPage|Write your content or drag files here...") + = render 'projects/zen', f: f, attr: :content, classes: 'note-textarea', placeholder: s_("WikiPage|Write your content or drag files here…") = render 'shared/notes/hints' .clearfix diff --git a/app/views/projects/wikis/edit.html.haml b/app/views/projects/wikis/edit.html.haml index 35c7dc2984a..d80d2957466 100644 --- a/app/views/projects/wikis/edit.html.haml +++ b/app/views/projects/wikis/edit.html.haml @@ -1,4 +1,4 @@ -- @content_class = "limit-container-width limit-container-width-sm" unless fluid_layout +- @content_class = "limit-container-width" unless fluid_layout - page_title _("Edit"), @page.title.capitalize, _("Wiki") = wiki_page_errors(@error) diff --git a/app/views/projects/wikis/git_access.html.haml b/app/views/projects/wikis/git_access.html.haml index 909987d8090..8c2cbd495a0 100644 --- a/app/views/projects/wikis/git_access.html.haml +++ b/app/views/projects/wikis/git_access.html.haml @@ -1,4 +1,4 @@ -- @content_class = "limit-container-width limit-container-width-sm" unless fluid_layout +- @content_class = "limit-container-width" unless fluid_layout - page_title s_("WikiClone|Git Access"), _("Wiki") .wiki-page-header.has-sidebar-toggle diff --git a/app/views/projects/wikis/show.html.haml b/app/views/projects/wikis/show.html.haml index ff72c8bb75d..a08973c7f32 100644 --- a/app/views/projects/wikis/show.html.haml +++ b/app/views/projects/wikis/show.html.haml @@ -1,4 +1,4 @@ -- @content_class = "limit-container-width limit-container-width-sm" unless fluid_layout +- @content_class = "limit-container-width" unless fluid_layout - breadcrumb_title @page.title.capitalize - wiki_breadcrumb_dropdown_links(@page.slug) - page_title @page.title.capitalize, _("Wiki") diff --git a/app/views/search/results/_blob_data.html.haml b/app/views/search/results/_blob_data.html.haml index 0115be41ff1..143e9f91ca3 100644 --- a/app/views/search/results/_blob_data.html.haml +++ b/app/views/search/results/_blob_data.html.haml @@ -3,7 +3,8 @@ .js-file-title.file-title = link_to blob_link do %i.fa.fa-file - = search_blob_title(project, file_name) + %strong + = search_blob_title(project, file_name) - if blob.data .file-content.code.term = render 'shared/file_highlight', blob: blob, first_line_number: blob.startline diff --git a/app/views/search/results/_commit.html.haml b/app/views/search/results/_commit.html.haml index f34eaf89027..ed5a3badf11 100644 --- a/app/views/search/results/_commit.html.haml +++ b/app/views/search/results/_commit.html.haml @@ -1 +1 @@ -= render 'projects/commits/commit', project: @project, commit: commit, ref: nil += render 'projects/commits/commit', project: commit.project, commit: commit, ref: nil, show_project_name: @project.nil? diff --git a/app/views/shared/_clone_panel.html.haml b/app/views/shared/_clone_panel.html.haml index 5fc02ba3160..3655c2a1d42 100644 --- a/app/views/shared/_clone_panel.html.haml +++ b/app/views/shared/_clone_panel.html.haml @@ -11,7 +11,7 @@ %span = default_clone_protocol.upcase = icon('caret-down') - %ul.dropdown-menu.dropdown-menu-selectable.dropdown-menu-right.clone-options-dropdown + %ul.dropdown-menu.dropdown-menu-selectable.clone-options-dropdown %li = ssh_clone_button(project) %li diff --git a/app/views/shared/_field.html.haml b/app/views/shared/_field.html.haml index 0c8d90d92f5..b89045e726a 100644 --- a/app/views/shared/_field.html.haml +++ b/app/views/shared/_field.html.haml @@ -9,11 +9,11 @@ - help = field[:help] - disabled = disable_fields_service?(@service) -.form-group +.form-group.row - if type == "password" && value.present? - = form.label name, "Enter new #{title.downcase}", class: "col-form-label" + = form.label name, "Enter new #{title.downcase}", class: "col-form-label col-sm-2" - else - = form.label name, title, class: "col-form-label" + = form.label name, title, class: "col-form-label col-sm-2" .col-sm-10 - if type == 'text' = form.text_field name, class: "form-control", placeholder: placeholder, required: required, disabled: disabled diff --git a/app/views/shared/_label.html.haml b/app/views/shared/_label.html.haml index ba5b65a209d..5eec7b02b54 100644 --- a/app/views/shared/_label.html.haml +++ b/app/views/shared/_label.html.haml @@ -1,93 +1,70 @@ - label_css_id = dom_id(label) - status = label_subscription_status(label, @project).inquiry if current_user - subject = local_assigns[:subject] +- use_label_priority = local_assigns.fetch(:use_label_priority, false) +- force_priority = local_assigns.fetch(:force_priority, use_label_priority ? label.priority.present? : false) - toggle_subscription_path = toggle_subscription_label_path(label, @project) if current_user - show_label_merge_requests_link = show_label_issuables_link?(label, :merge_requests, project: @project) - show_label_issues_link = show_label_issuables_link?(label, :issues, project: @project) +- tooltip_title = label_status_tooltip(label, status) if status %li.label-list-item{ id: label_css_id, data: { id: label.id } } - = render "shared/label_row", label: label - - .d-inline-block.d-sm-none.dropdown - %button.btn.btn-default.label-options-toggle{ type: 'button', data: { toggle: "dropdown" } } - Options - = icon('caret-down') - .dropdown-menu.dropdown-menu-right - %ul - - if show_label_merge_requests_link - %li - = link_to_label(label, subject: subject, type: :merge_request) do - View merge requests - - if show_label_issues_link - %li - = link_to_label(label, subject: subject) do - View open issues - - if current_user - %li.label-subscription - - if can_subscribe_to_label_in_different_levels?(label) - %a.js-unsubscribe-button.label-subscribe-button{ role: 'button', href: '#', class: ('hidden' if status.unsubscribed?), data: { url: toggle_subscription_path } } - %span Unsubscribe - %a.js-subscribe-button.label-subscribe-button{ role: 'button', href: '#', class: ('hidden' unless status.unsubscribed?), data: { url: toggle_subscription_project_label_path(@project, label) } } - %span Subscribe at project level - %a.js-subscribe-button.label-subscribe-button{ role: 'button', href: '#', class: ('hidden' unless status.unsubscribed?), data: { url: toggle_subscription_group_label_path(label.group, label) } } - %span Subscribe at group level - - else - %a.js-subscribe-button.label-subscribe-button{ role: 'button', href: '#', data: { status: status, url: toggle_subscription_path } } - %span= label_subscription_toggle_button_text(label, @project) - - - if can?(current_user, :admin_label, label) - %li - = link_to 'Edit', edit_label_path(label) - %li - = link_to 'Delete', - destroy_label_path(label), - title: 'Delete', - method: :delete, - data: {confirm: 'Remove this label? Are you sure?'}, - class: 'text-danger' - - .float-right.d-none.d-sm-none.d-md-block - - if can?(current_user, :admin_label, label) - - if label.is_a?(ProjectLabel) && label.project.group && can?(current_user, :admin_label, label.project.group) - %button.js-promote-project-label-button.btn.btn-transparent.btn-action.has-tooltip{ title: _('Promote to Group Label'), - disabled: true, - type: 'button', - data: { url: promote_project_label_path(label.project, label), - label_title: label.title, - label_color: label.color, - label_text_color: label.text_color, - group_name: label.project.group.name, - target: '#promote-label-modal', - container: 'body', - toggle: 'modal' } } - = sprite_icon('level-up') - = link_to edit_label_path(label), title: "Edit", class: 'btn btn-transparent btn-action', data: {toggle: "tooltip"} do - %span.sr-only Edit - = sprite_icon('pencil') - %span{ data: { toggle: 'modal', target: "#modal-delete-label-#{label.id}" } } - = link_to "#", title: "Delete", class: 'btn btn-transparent btn-action remove-row', data: { toggle: "tooltip" } do - %span.sr-only Delete - = sprite_icon('remove') + = render "shared/label_row", label: label, subject: subject, force_priority: force_priority + %ul.label-actions-list + - if @project + %li.inline + .label-badge.label-badge-gray= label.model_name.human.capitalize + - if can?(current_user, :admin_label, @project) + %li.inline.js-toggle-priority{ data: { url: remove_priority_project_label_path(@project, label), + dom_id: dom_id(label), type: label.type } } + %button.label-action.add-priority.btn.btn-transparent.has-tooltip{ title: _('Prioritize'), type: 'button', data: { placement: 'top' }, aria_label: _('Prioritize label') } + = sprite_icon('star-o') + %button.label-action.remove-priority.btn.btn-transparent.has-tooltip{ title: _('Remove priority'), type: 'button', data: { placement: 'top' }, aria_label: _('Deprioritize label') } + = sprite_icon('star') + %li.inline + = link_to edit_label_path(label), class: 'btn btn-transparent label-action', aria_label: 'Edit label' do + = sprite_icon('pencil') + %li.inline + .dropdown + %button{ type: 'button', class: 'btn btn-transparent js-label-options-dropdown label-action', data: { toggle: 'dropdown' }, aria_label: _('Label actions dropdown') } + = sprite_icon('ellipsis_v') + .dropdown-menu.dropdown-open-left + %ul + - if label.is_a?(ProjectLabel) && label.project.group && can?(current_user, :admin_label, label.project.group) + %li + %button.js-promote-project-label-button.btn.btn-transparent.btn-action{ disabled: true, type: 'button', + data: { url: promote_project_label_path(label.project, label), + label_title: label.title, + label_color: label.color, + label_text_color: label.text_color, + group_name: label.project.group.name, + target: '#promote-label-modal', + container: 'body', + toggle: 'modal' } } + = _('Promote to group label') + %li + %span{ data: { toggle: 'modal', target: "#modal-delete-label-#{label.id}" } } + %button.text-danger.remove-row{ type: 'button' }= _('Delete') - if current_user - .label-subscription.inline + %li.inline.label-subscription - if can_subscribe_to_label_in_different_levels?(label) - %button.js-unsubscribe-button.label-subscribe-button.btn.btn-default{ type: 'button', class: ('hidden' if status.unsubscribed?), data: { url: toggle_subscription_path } } - %span Unsubscribe - = icon('spinner spin', class: 'label-subscribe-button-loading') - + %button.js-unsubscribe-button.label-subscribe-button.btn.btn-default{ class: ('hidden' if status.unsubscribed?), data: { url: toggle_subscription_path, toggle: 'tooltip' }, title: tooltip_title } + %span= _('Unsubscribe') .dropdown.dropdown-group-label{ class: ('hidden' unless status.unsubscribed?) } - %button.dropdown-menu-toggle{ type: 'button', 'data-toggle' => 'dropdown' } - %span Subscribe - = icon('chevron-down') - %ul.dropdown-menu - %li - %a.js-subscribe-button{ class: ('hidden' unless status.unsubscribed?), data: { url: toggle_subscription_project_label_path(@project, label) } } - Project level - %a.js-subscribe-button{ class: ('hidden' unless status.unsubscribed?), data: { url: toggle_subscription_group_label_path(label.group, label) } } - Group level + %button.label-subscribe-button.btn.btn-default{ data: { toggle: 'dropdown' } } + %span + = _('Subscribe') + = sprite_icon('chevron-down') + .dropdown-menu.dropdown-open-left + %ul + %li + %button.js-subscribe-button.label-subscribe-button.btn.btn-default{ class: ('hidden' unless status.unsubscribed?), data: { status: status, url: toggle_subscription_project_label_path(@project, label) } } + %span= _('Subscribe at project level') + %li + %button.js-subscribe-button.js-group-level.label-subscribe-button.btn.btn-default{ class: ('hidden' unless status.unsubscribed?), data: { status: status, url: toggle_subscription_group_label_path(label.group, label) } } + %span= _('Subscribe at group level') - else - %button.js-subscribe-button.label-subscribe-button.btn.btn-default{ type: 'button', data: { status: status, url: toggle_subscription_path } } + %button.js-subscribe-button.label-subscribe-button.btn.btn-default{ data: { status: status, url: toggle_subscription_path, toggle: 'tooltip' }, title: tooltip_title } %span= label_subscription_toggle_button_text(label, @project) - = icon('spinner spin', class: 'label-subscribe-button-loading') = render 'shared/delete_label_modal', label: label diff --git a/app/views/shared/_label_row.html.haml b/app/views/shared/_label_row.html.haml index f1c1ca9b2c9..0ae3ab8f090 100644 --- a/app/views/shared/_label_row.html.haml +++ b/app/views/shared/_label_row.html.haml @@ -1,30 +1,23 @@ - subject = local_assigns[:subject] +- force_priority = local_assigns.fetch(:force_priority, false) - show_label_issues_link = show_label_issuables_link?(label, :issues, project: @project) - show_label_merge_requests_link = show_label_issuables_link?(label, :merge_requests, project: @project) -%span.label-row - - if can?(current_user, :admin_label, @project) - .draggable-handler - = icon('bars') - .js-toggle-priority.toggle-priority{ data: { url: remove_priority_project_label_path(@project, label), - dom_id: dom_id(label), type: label.type } } - %button.add-priority.btn.has-tooltip{ title: 'Prioritize', type: 'button', :'data-placement' => 'top' } - = icon('star-o') - %button.remove-priority.btn.has-tooltip{ title: 'Remove priority', type: 'button', :'data-placement' => 'top' } - = icon('star') - %span.label-name - = link_to_label(label, subject: @project, tooltip: false) - - if defined?(@project) && @project.group.present? - %span.label-type - = label.model_name.human.titleize - - %span.label-description +.label-name + = link_to_label(label, subject: @project, tooltip: false) +.label-description + .append-right-default.prepend-left-default - if label.description.present? - .description-text + .description-text.append-bottom-10 = markdown_field(label, :description) - .d-none.d-sm-none.d-md-block + %ul.label-links - if show_label_issues_link - = link_to_label(label, subject: subject) { 'Issues' } + %li.label-link-item.inline + = link_to_label(label, subject: subject) { 'Issues' } - if show_label_merge_requests_link · - = link_to_label(label, subject: subject, type: :merge_request) { 'Merge requests' } + %li.label-link-item.inline + = link_to_label(label, subject: subject, type: :merge_request) { _('Merge requests') } + - if force_priority + %li.label-link-item.js-priority-badge.inline.prepend-left-10 + .label-badge.label-badge-blue= _('Prioritized label') diff --git a/app/views/shared/_new_merge_request_checkbox.html.haml b/app/views/shared/_new_merge_request_checkbox.html.haml index 165109b6b70..24c0dfe247f 100644 --- a/app/views/shared/_new_merge_request_checkbox.html.haml +++ b/app/views/shared/_new_merge_request_checkbox.html.haml @@ -1,4 +1,4 @@ -.form-check +.form-check.prepend-top-8 - nonce = SecureRandom.hex = check_box_tag 'create_merge_request', 1, true, class: 'js-create-merge-request form-check-input', id: "create_merge_request-#{nonce}" = label_tag "create_merge_request-#{nonce}", class: 'form-check-label' do diff --git a/app/views/shared/_service_settings.html.haml b/app/views/shared/_service_settings.html.haml index 0ebf365c7bd..6fa61c15493 100644 --- a/app/views/shared/_service_settings.html.haml +++ b/app/views/shared/_service_settings.html.haml @@ -3,8 +3,9 @@ - if lookup_context.template_exists?('help', "projects/services/#{@service.to_param}", true) = render "projects/services/#{@service.to_param}/help", subject: subject - elsif @service.help.present? - .card - = markdown @service.help + .info-well + .well-segment + = markdown @service.help .service-settings - if @service.show_active_box? @@ -15,25 +16,24 @@ - if @service.configurable_events.present? .form-group.row - = form.label :url, "Trigger", class: 'col-form-label col-sm-2' + .col-sm-2.text-right Trigger .col-sm-10 - @service.configurable_events.each do |event| - %div - = form.check_box service_event_field_name(event), class: 'float-left' - .prepend-left-20 - = form.label service_event_field_name(event), class: 'list-label' do + .form-group + .form-check + = form.check_box service_event_field_name(event), class: 'form-check-input' + = form.label service_event_field_name(event), class: 'form-check-label' do %strong = event.humanize - - field = @service.event_field(event) + - field = @service.event_field(event) - - if field - %p + - if field = form.text_field field[:name], class: "form-control", placeholder: field[:placeholder] - %p.light - = @service.class.event_description(event) + %p.text-muted + = @service.class.event_description(event) - @service.global_fields.each do |field| - type = field[:type] diff --git a/app/views/shared/_visibility_level.html.haml b/app/views/shared/_visibility_level.html.haml index d67409ffe14..01ce1225b8d 100644 --- a/app/views/shared/_visibility_level.html.haml +++ b/app/views/shared/_visibility_level.html.haml @@ -1,11 +1,11 @@ - with_label = local_assigns.fetch(:with_label, true) -.form-group.visibility-level-setting +.form-group.row.visibility-level-setting - if with_label = f.label :visibility_level, class: 'col-form-label col-sm-2' do Visibility Level = link_to icon('question-circle'), help_page_path("public_access/public_access") - %div{ :class => ("col-sm-10" if with_label) } + %div{ :class => (with_label ? "col-sm-10" : "col-sm-12") } - if can_change_visibility_level = render('shared/visibility_radios', model_method: :visibility_level, form: f, selected_level: visibility_level, form_model: form_model) - else diff --git a/app/views/shared/boards/components/_board.html.haml b/app/views/shared/boards/components/_board.html.haml index 67476a3f573..76843ce7cc0 100644 --- a/app/views/shared/boards/components/_board.html.haml +++ b/app/views/shared/boards/components/_board.html.haml @@ -1,4 +1,4 @@ -.board{ ":class" => '{ "is-draggable": !list.preset, "is-expandable": list.isExpandable, "is-collapsed": !list.isExpanded }', +.board{ ":class" => '{ "is-draggable": !list.preset, "is-expandable": list.isExpandable, "is-collapsed": !list.isExpanded, "board-type-assignee": list.type === "assignee" }', ":data-id" => "list.id" } .board-inner %header.board-header{ ":class" => '{ "has-border": list.label && list.label.color }', ":style" => "{ borderTopColor: (list.label && list.label.color ? list.label.color : null) }", "@click" => "toggleExpanded($event)" } @@ -7,10 +7,18 @@ ":class": "{ \"fa-caret-down\": list.isExpanded, \"fa-caret-right\": !list.isExpanded }", "aria-hidden": "true" } + %a.user-avatar-link.js-no-trigger{ "v-if": "list.type === \"assignee\"", ":href": "list.assignee.path" } + -# haml-lint:disable AltText + %img.avatar.s20.has-tooltip{ height: "20", width: "20", ":src": "list.assignee.avatar", ":alt": "list.assignee.name" } + %span.board-title-text.has-tooltip{ "v-if": "list.type !== \"label\"", - ":title" => '(list.label ? list.label.description : "")', data: { container: "body" } } + ":title" => '((list.label && list.label.description) || list.title || "")', data: { container: "body" } } {{ list.title }} + %span.board-title-sub-text.prepend-left-5.has-tooltip{ "v-if": "list.type === \"assignee\"", + ":title" => '(list.assignee && list.assignee.username || "")' } + @{{ list.assignee.username }} + %span.has-tooltip{ "v-if": "list.type === \"label\"", ":title" => '(list.label ? list.label.description : "")', data: { container: "body", placement: "bottom" }, diff --git a/app/views/shared/boards/components/sidebar/_labels.html.haml b/app/views/shared/boards/components/sidebar/_labels.html.haml index a112a9f1f7e..daee691e358 100644 --- a/app/views/shared/boards/components/sidebar/_labels.html.haml +++ b/app/views/shared/boards/components/sidebar/_labels.html.haml @@ -9,7 +9,7 @@ None %a{ href: "#", "v-for" => "label in issue.labels" } - %span.badge.color-label.has-tooltip{ ":style" => "{ backgroundColor: label.color, color: label.textColor }" } + .badge.color-label.has-tooltip{ ":style" => "{ backgroundColor: label.color, color: label.textColor }" } {{ label.title }} - if can_admin_issue? .selectbox diff --git a/app/views/shared/builds/_build_output.html.haml b/app/views/shared/builds/_build_output.html.haml new file mode 100644 index 00000000000..0e18128a8f1 --- /dev/null +++ b/app/views/shared/builds/_build_output.html.haml @@ -0,0 +1,3 @@ +%pre.build-trace#build-trace + %code.bash.js-build-output + .build-loader-animation.js-build-refresh diff --git a/app/views/shared/empty_states/_wikis.html.haml b/app/views/shared/empty_states/_wikis.html.haml index fabb1f39a34..f1a41074c28 100644 --- a/app/views/shared/empty_states/_wikis.html.haml +++ b/app/views/shared/empty_states/_wikis.html.haml @@ -8,7 +8,7 @@ %h4 = s_('WikiEmpty|The wiki lets you write documentation for your project') %p.text-left - = s_("WikiEmpty|A wiki is where you can store all the details about your project. This can include why you've created it, it's principles, how to use it, and so on.") + = s_("WikiEmpty|A wiki is where you can store all the details about your project. This can include why you've created it, its principles, how to use it, and so on.") = create_link - elsif can?(current_user, :read_issue, @project) diff --git a/app/views/shared/form_elements/_description.html.haml b/app/views/shared/form_elements/_description.html.haml index e5dfa7dbf71..25df2fe5cd6 100644 --- a/app/views/shared/form_elements/_description.html.haml +++ b/app/views/shared/form_elements/_description.html.haml @@ -16,7 +16,7 @@ = render layout: 'projects/md_preview', locals: { url: preview_url, referenced_users: true } do = render 'projects/zen', f: form, attr: :description, classes: 'note-textarea qa-issuable-form-description', - placeholder: "Write a comment or drag your files here...", + placeholder: "Write a comment or drag your files here…", supports_quick_actions: supports_quick_actions = render 'shared/notes/hints', supports_quick_actions: supports_quick_actions .clearfix diff --git a/app/views/shared/issuable/_board_create_list_dropdown.html.haml b/app/views/shared/issuable/_board_create_list_dropdown.html.haml new file mode 100644 index 00000000000..23b2e1b91e5 --- /dev/null +++ b/app/views/shared/issuable/_board_create_list_dropdown.html.haml @@ -0,0 +1,8 @@ +.dropdown.prepend-left-10#js-add-list + %button.btn.btn-create.btn-inverted.js-new-board-list{ type: "button", data: board_list_data } + Add list + .dropdown-menu.dropdown-menu-paging.dropdown-menu-right.dropdown-menu-issues-board-new.dropdown-menu-selectable.js-tab-container-labels + = render partial: "shared/issuable/label_page_default", locals: { show_footer: true, show_create: true, show_boards_content: true, title: "Add list" } + - if can?(current_user, :admin_label, board.parent) + = render partial: "shared/issuable/label_page_create" + = dropdown_loading diff --git a/app/views/shared/issuable/_label_page_create.html.haml b/app/views/shared/issuable/_label_page_create.html.haml index 9f4021802df..55edaa7eda4 100644 --- a/app/views/shared/issuable/_label_page_create.html.haml +++ b/app/views/shared/issuable/_label_page_create.html.haml @@ -1,6 +1,7 @@ +- show_close = local_assigns.fetch(:show_close, true) - subject = @project || @group .dropdown-page-two.dropdown-new-label - = dropdown_title(create_label_title(subject), options: { back: true }) + = dropdown_title(create_label_title(subject), options: { back: true, close: show_close }) = dropdown_content do .dropdown-labels-error.js-label-error %input#new_label_name.default-dropdown-input{ type: "text", placeholder: _('Name new label') } diff --git a/app/views/shared/issuable/_label_page_default.html.haml b/app/views/shared/issuable/_label_page_default.html.haml index 2bd922bca2b..aa4a5f0e0d3 100644 --- a/app/views/shared/issuable/_label_page_default.html.haml +++ b/app/views/shared/issuable/_label_page_default.html.haml @@ -1,15 +1,18 @@ - title = local_assigns.fetch(:title, _('Assign labels')) +- content_title = local_assigns.fetch(:content_title, _('Create lists from labels. Issues with that label appear in that list.')) +- show_title = local_assigns.fetch(:show_title, true) - show_create = local_assigns.fetch(:show_create, true) - show_footer = local_assigns.fetch(:show_footer, true) - filter_placeholder = local_assigns.fetch(:filter_placeholder, 'Search') - show_boards_content = local_assigns.fetch(:show_boards_content, false) - subject = @project || @group .dropdown-page-one - = dropdown_title(title) + - if show_title + = dropdown_title(title) - if show_boards_content .issue-board-dropdown-content %p - = _('Create lists from labels. Issues with that label appear in that list.') + = content_title = dropdown_filter(filter_placeholder) = dropdown_content - if current_board_parent && show_footer diff --git a/app/views/shared/issuable/_milestone_dropdown.html.haml b/app/views/shared/issuable/_milestone_dropdown.html.haml index 955b8866c2c..37625a4a163 100644 --- a/app/views/shared/issuable/_milestone_dropdown.html.haml +++ b/app/views/shared/issuable/_milestone_dropdown.html.haml @@ -1,6 +1,8 @@ - project = @target_project || @project - extra_class = extra_class || '' - show_menu_above = show_menu_above || false +- selected = local_assigns.fetch(:selected, nil) + - selected_text = selected.try(:title) || params[:milestone_title] - dropdown_title = local_assigns.fetch(:dropdown_title, "Filter by milestone") - if selected.present? || params[:milestone_title].present? diff --git a/app/views/shared/issuable/_search_bar.html.haml b/app/views/shared/issuable/_search_bar.html.haml index 644f7c4dd28..ef9ea2194ee 100644 --- a/app/views/shared/issuable/_search_bar.html.haml +++ b/app/views/shared/issuable/_search_bar.html.haml @@ -104,14 +104,7 @@ .filter-dropdown-container - if type == :boards - if can?(current_user, :admin_list, board.parent) - .dropdown.prepend-left-10#js-add-list - %button.btn.btn-create.btn-inverted.js-new-board-list{ type: "button", data: board_list_data } - Add list - .dropdown-menu.dropdown-menu-paging.dropdown-menu-right.dropdown-menu-issues-board-new.dropdown-menu-selectable - = render partial: "shared/issuable/label_page_default", locals: { show_footer: true, show_create: true, show_boards_content: true, title: "Add list" } - - if can?(current_user, :admin_label, board.parent) - = render partial: "shared/issuable/label_page_create" - = dropdown_loading + = render_if_exists 'shared/issuable/board_create_list_dropdown', board: board - if @project #js-add-issues-btn.prepend-left-10{ data: { can_admin_list: can?(current_user, :admin_list, @project) } } - elsif type != :boards_modal diff --git a/app/views/shared/issuable/_sidebar.html.haml b/app/views/shared/issuable/_sidebar.html.haml index 9e50e888b35..0ca35ea1298 100644 --- a/app/views/shared/issuable/_sidebar.html.haml +++ b/app/views/shared/issuable/_sidebar.html.haml @@ -43,7 +43,7 @@ .selectbox.hide-collapsed = f.hidden_field 'milestone_id', value: issuable.milestone_id, id: nil - = dropdown_tag('Milestone', options: { title: _('Assign milestone'), toggle_class: 'js-milestone-select js-extra-options', filter: true, dropdown_class: 'dropdown-menu-selectable', placeholder: _('Search milestones'), data: { show_no: true, field_name: "#{issuable.to_ability_name}[milestone_id]", project_id: @project.id, issuable_id: issuable.id, milestones: project_milestones_path(@project, :json), ability_name: issuable.to_ability_name, issue_update: issuable_json_path(issuable), use_id: true, default_no: true, selected: (issuable.milestone.name if issuable.milestone), null_default: true }}) + = dropdown_tag('Milestone', options: { title: _('Assign milestone'), toggle_class: 'js-milestone-select js-extra-options', filter: true, dropdown_class: 'dropdown-menu-selectable', placeholder: _('Search milestones'), data: { show_no: true, field_name: "#{issuable.to_ability_name}[milestone_id]", project_id: @project.id, issuable_id: issuable.id, milestones: project_milestones_path(@project, :json), ability_name: issuable.to_ability_name, issue_update: issuable_json_path(issuable), use_id: true, default_no: true, selected: (issuable.milestone.name if issuable.milestone), null_default: true, display: 'static' }}) - if issuable.has_attribute?(:time_estimate) #issuable-time-tracker.block // Fallback while content is loading @@ -77,7 +77,7 @@ .selectbox.hide-collapsed = f.hidden_field :due_date, value: issuable.due_date.try(:strftime, 'yy-mm-dd') .dropdown - %button.dropdown-menu-toggle.js-due-date-select{ type: 'button', data: { toggle: 'dropdown', field_name: "#{issuable.to_ability_name}[due_date]", ability_name: issuable.to_ability_name, issue_update: issuable_json_path(issuable) } } + %button.dropdown-menu-toggle.js-due-date-select{ type: 'button', data: { toggle: 'dropdown', field_name: "#{issuable.to_ability_name}[due_date]", ability_name: issuable.to_ability_name, issue_update: issuable_json_path(issuable), display: 'static' } } %span.dropdown-toggle-text = _('Due date') = icon('chevron-down', 'aria-hidden': 'true') @@ -109,7 +109,7 @@ - selected_labels.each do |label| = hidden_field_tag "#{issuable.to_ability_name}[label_names][]", label.id, id: nil .dropdown - %button.dropdown-menu-toggle.js-label-select.js-multiselect.js-label-sidebar-dropdown{ type: "button", data: {toggle: "dropdown", default_label: "Labels", field_name: "#{issuable.to_ability_name}[label_names][]", ability_name: issuable.to_ability_name, show_no: "true", show_any: "true", namespace_path: @project.try(:namespace).try(:full_path), project_path: @project.try(:path), issue_update: issuable_json_path(issuable), labels: (labels_filter_path(false) if @project) } } + %button.dropdown-menu-toggle.js-label-select.js-multiselect.js-label-sidebar-dropdown{ type: "button", data: {toggle: "dropdown", default_label: "Labels", field_name: "#{issuable.to_ability_name}[label_names][]", ability_name: issuable.to_ability_name, show_no: "true", show_any: "true", namespace_path: @project.try(:namespace).try(:full_path), project_path: @project.try(:path), issue_update: issuable_json_path(issuable), labels: (labels_filter_path(false) if @project), display: 'static' } } %span.dropdown-toggle-text{ class: ("is-default" if selected_labels.empty?) } = multi_label_name(selected_labels, "Labels") = icon('chevron-down', 'aria-hidden': 'true') diff --git a/app/views/shared/issuable/_sidebar_assignees.html.haml b/app/views/shared/issuable/_sidebar_assignees.html.haml index e1cde527ad7..8a13c7a3b83 100644 --- a/app/views/shared/issuable/_sidebar_assignees.html.haml +++ b/app/views/shared/issuable/_sidebar_assignees.html.haml @@ -37,7 +37,7 @@ - issuable.assignees.each do |assignee| = hidden_field_tag "#{issuable.to_ability_name}[assignee_ids][]", assignee.id, id: nil, data: { avatar_url: assignee.avatar_url, name: assignee.name, username: assignee.username } - - options = { toggle_class: 'js-user-search js-author-search', title: _('Assign to'), filter: true, dropdown_class: 'dropdown-menu-user dropdown-menu-selectable dropdown-menu-author', placeholder: _('Search users'), data: { first_user: current_user&.username, current_user: true, project_id: @project&.id, author_id: issuable.author_id, field_name: "#{issuable.to_ability_name}[assignee_ids][]", issue_update: issuable_json_path(issuable), ability_name: issuable.to_ability_name, null_user: true } } + - options = { toggle_class: 'js-user-search js-author-search', title: _('Assign to'), filter: true, dropdown_class: 'dropdown-menu-user dropdown-menu-selectable dropdown-menu-author', placeholder: _('Search users'), data: { first_user: current_user&.username, current_user: true, project_id: @project&.id, author_id: issuable.author_id, field_name: "#{issuable.to_ability_name}[assignee_ids][]", issue_update: issuable_json_path(issuable), ability_name: issuable.to_ability_name, null_user: true, display: 'static' } } - title = _('Select assignee') - if issuable.is_a?(Issue) @@ -50,7 +50,7 @@ - data[:multi_select] = true - data['dropdown-title'] = title - data['dropdown-header'] = dropdown_options[:data][:'dropdown-header'] - - data['max-select'] = dropdown_options[:data][:'max-select'] + - data['max-select'] = dropdown_options[:data][:'max-select'] if dropdown_options[:data][:'max-select'] - options[:data].merge!(data) = dropdown_tag(title, options: options) diff --git a/app/views/shared/issuable/form/_contribution.html.haml b/app/views/shared/issuable/form/_contribution.html.haml index b34549240e0..bc9a1edc39c 100644 --- a/app/views/shared/issuable/form/_contribution.html.haml +++ b/app/views/shared/issuable/form/_contribution.html.haml @@ -7,14 +7,14 @@ %hr -.form-group - .col-form-label +.form-group.row + %label.col-form-label.col-sm-2 = _('Contribution') .col-sm-10 - .form-check - = form.check_box :allow_maintainer_to_push, disabled: !issuable.can_allow_maintainer_to_push?(current_user), class: 'form-check-input' - = form.label :allow_maintainer_to_push, class: 'form-check-label' do - = _('Allow edits from maintainers.') - = link_to 'About this feature', help_page_path('user/project/merge_requests/maintainer_access') + .form-check.prepend-top-5 + = form.check_box :allow_collaboration, disabled: !issuable.can_allow_collaboration?(current_user), class: 'form-check-input' + = form.label :allow_collaboration, class: 'form-check-label' do + = _('Allow commits from members who can merge to the target branch.') + = link_to 'About this feature', help_page_path('user/project/merge_requests/allow_collaboration') .form-text.text-muted - = allow_maintainer_push_unavailable_reason(issuable) + = allow_collaboration_unavailable_reason(issuable) diff --git a/app/views/shared/issuable/form/_metadata.html.haml b/app/views/shared/issuable/form/_metadata.html.haml index 1e27253aaeb..bd87bb38e77 100644 --- a/app/views/shared/issuable/form/_metadata.html.haml +++ b/app/views/shared/issuable/form/_metadata.html.haml @@ -15,17 +15,20 @@ - else = render "shared/issuable/form/metadata_merge_request_assignee", issuable: issuable, form: form, has_due_date: has_due_date .form-group.row.issue-milestone - = form.label :milestone_id, "Milestone", class: "col-form-label #{has_due_date ? "col-md-2 col-lg-4" : "col-2"}" - .col-10{ class: ("col-md-8" if has_due_date) } + = form.label :milestone_id, "Milestone", class: "col-form-label #{has_due_date ? "col-md-2 col-lg-4" : "col-sm-2"}" + .col-sm-10{ class: ("col-md-8" if has_due_date) } .issuable-form-select-holder = render "shared/issuable/milestone_dropdown", selected: issuable.milestone, name: "#{issuable.class.model_name.param_key}[milestone_id]", show_any: false, show_upcoming: false, show_started: false, extra_class: "js-issuable-form-dropdown js-dropdown-keep-input", dropdown_title: "Select milestone" .form-group.row - has_labels = @labels && @labels.any? - = form.label :label_ids, "Labels", class: "col-form-label #{has_due_date ? "col-md-2 col-lg-4" : "col-2"}" + = form.label :label_ids, "Labels", class: "col-form-label #{has_due_date ? "col-md-2 col-lg-4" : "col-sm-2"}" = form.hidden_field :label_ids, multiple: true, value: '' - .col-10{ class: "#{"col-md-8" if has_due_date} #{'issuable-form-padding-top' if !has_labels}" } + .col-sm-10{ class: "#{"col-md-8" if has_due_date} #{'issuable-form-padding-top' if !has_labels}" } .issuable-form-select-holder - = render "shared/issuable/label_dropdown", classes: ["js-issuable-form-dropdown"], selected: issuable.labels, data_options: { field_name: "#{issuable.class.model_name.param_key}[label_ids][]", show_any: false}, dropdown_title: "Select label" + = render "shared/issuable/label_dropdown", classes: ["js-issuable-form-dropdown"], selected: issuable.labels, data_options: { field_name: "#{issuable.class.model_name.param_key}[label_ids][]", show_any: false }, dropdown_title: "Select label" + + = render_if_exists "shared/issuable/form/weight", issuable: issuable, form: form + - if has_due_date .col-lg-6 .form-group.row diff --git a/app/views/shared/members/_group.html.haml b/app/views/shared/members/_group.html.haml index 67b8843a27f..d0b492b43f3 100644 --- a/app/views/shared/members/_group.html.haml +++ b/app/views/shared/members/_group.html.haml @@ -5,16 +5,16 @@ %li.member.group_member{ id: dom_id } %span.list-item-name = group_icon(group, class: "avatar s40", alt: '') - %strong - = link_to group.full_name, group_path(group) - .cgray - Given access #{time_ago_with_tooltip(group_link.created_at)} - - if group_link.expires? - · - %span{ class: ('text-warning' if group_link.expires_soon?) } - Expires in #{distance_of_time_in_words_to_now(group_link.expires_at)} + .user-info + = link_to group.full_name, group_path(group), class: 'member' + .cgray + Given access #{time_ago_with_tooltip(group_link.created_at)} + - if group_link.expires? + · + %span{ class: ('text-warning' if group_link.expires_soon?) } + Expires in #{distance_of_time_in_words_to_now(group_link.expires_at)} .controls.member-controls - = form_tag project_group_link_path(@project, group_link), method: :put, remote: true, class: 'js-edit-member-form' do + = form_tag project_group_link_path(@project, group_link), method: :put, remote: true, class: 'js-edit-member-form form-group row append-right-5' do = hidden_field_tag "group_link[group_access]", group_link.group_access .member-form-control.dropdown.append-right-5 %button.dropdown-menu-toggle.js-member-permissions-dropdown{ type: "button", diff --git a/app/views/shared/notes/_edit_form.html.haml b/app/views/shared/notes/_edit_form.html.haml index 8923e5602a4..71a5b94e958 100644 --- a/app/views/shared/notes/_edit_form.html.haml +++ b/app/views/shared/notes/_edit_form.html.haml @@ -3,7 +3,7 @@ = hidden_field_tag :target_id, '', class: 'js-form-target-id' = hidden_field_tag :target_type, '', class: 'js-form-target-type' = render layout: 'projects/md_preview', locals: { url: preview_markdown_path(project), referenced_users: true } do - = render 'projects/zen', attr: 'note[note]', classes: 'note-textarea js-note-text js-task-list-field', placeholder: "Write a comment or drag your files here..." + = render 'projects/zen', attr: 'note[note]', classes: 'note-textarea js-note-text js-task-list-field', placeholder: "Write a comment or drag your files here…" = render 'shared/notes/hints' .note-form-actions.clearfix diff --git a/app/views/shared/notes/_form.html.haml b/app/views/shared/notes/_form.html.haml index 71c0d740bc8..c360f1ffe2a 100644 --- a/app/views/shared/notes/_form.html.haml +++ b/app/views/shared/notes/_form.html.haml @@ -29,7 +29,7 @@ = render 'projects/zen', f: f, attr: :note, classes: 'note-textarea js-note-text', - placeholder: "Write a comment or drag your files here...", + placeholder: "Write a comment or drag your files here…", supports_quick_actions: supports_quick_actions, supports_autocomplete: supports_autocomplete = render 'shared/notes/hints', supports_quick_actions: supports_quick_actions diff --git a/app/views/shared/notifications/_button.html.haml b/app/views/shared/notifications/_button.html.haml index e99d8d0973f..09ddf732ada 100644 --- a/app/views/shared/notifications/_button.html.haml +++ b/app/views/shared/notifications/_button.html.haml @@ -6,14 +6,14 @@ .js-notification-toggle-btns %div{ class: ("btn-group" if notification_setting.custom?) } - if notification_setting.custom? - %button.dropdown-new.btn.btn-default.has-tooltip.notifications-btn#notifications-button{ type: "button", title: "Notification setting", "aria-label" => "Notification setting: #{notification_title(notification_setting.level)}", data: { container: "body", toggle: "modal", target: "#" + notifications_menu_identifier("modal", notification_setting) } } + %button.dropdown-new.btn.btn-default.has-tooltip.notifications-btn#notifications-button{ type: "button", title: "Notification setting", "aria-label" => "Notification setting: #{notification_title(notification_setting.level)}", data: { container: "body", toggle: "modal", target: "#" + notifications_menu_identifier("modal", notification_setting), display: 'static' } } = icon("bell", class: "js-notification-loading") = notification_title(notification_setting.level) %button.btn.dropdown-toggle{ data: { toggle: "dropdown", target: notifications_menu_identifier("dropdown", notification_setting) } } = icon('caret-down') .sr-only Toggle dropdown - else - %button.dropdown-new.btn.btn-default.has-tooltip.notifications-btn#notifications-button{ type: "button", title: "Notification setting", "aria-label" => "Notification setting: #{notification_title(notification_setting.level)}", data: { container: "body", toggle: "dropdown", target: notifications_menu_identifier("dropdown", notification_setting) } } + %button.dropdown-new.btn.btn-default.has-tooltip.notifications-btn#notifications-button{ type: "button", title: "Notification setting", "aria-label" => "Notification setting: #{notification_title(notification_setting.level)}", data: { container: "body", toggle: "dropdown", target: notifications_menu_identifier("dropdown", notification_setting), display: 'static' } } = icon("bell", class: "js-notification-loading") = notification_title(notification_setting.level) = icon("caret-down") diff --git a/app/views/shared/projects/_edit_information.html.haml b/app/views/shared/projects/_edit_information.html.haml index ec9dc8f62c2..9230e045a81 100644 --- a/app/views/shared/projects/_edit_information.html.haml +++ b/app/views/shared/projects/_edit_information.html.haml @@ -1,6 +1,6 @@ - unless can?(current_user, :push_code, @project) .inline.prepend-left-10 - - if @project.branch_allows_maintainer_push?(current_user, selected_branch) + - if @project.branch_allows_collaboration?(current_user, selected_branch) = commit_in_single_accessible_branch - else = commit_in_fork_help diff --git a/app/views/shared/tokens/_scopes_form.html.haml b/app/views/shared/tokens/_scopes_form.html.haml index ae437dd16d6..2d0bb722189 100644 --- a/app/views/shared/tokens/_scopes_form.html.haml +++ b/app/views/shared/tokens/_scopes_form.html.haml @@ -5,6 +5,6 @@ - scopes.each do |scope| %fieldset = check_box_tag "#{prefix}[scopes][]", scope, token.scopes.include?(scope), id: "#{prefix}_scopes_#{scope}" - = label_tag ("#{prefix}_scopes_#{scope}"), scope + = label_tag ("#{prefix}_scopes_#{scope}"), scope, class: "label-light" %span= t(scope, scope: [:doorkeeper, :scopes]) .scope-description= t scope, scope: [:doorkeeper, :scope_desc] diff --git a/app/views/users/terms/index.html.haml b/app/views/users/terms/index.html.haml index e0fe551cf36..a869cf9cdee 100644 --- a/app/views/users/terms/index.html.haml +++ b/app/views/users/terms/index.html.haml @@ -1,13 +1,18 @@ - redirect_params = { redirect: @redirect } if @redirect -.panel-content.rendered-terms +.card-body.rendered-terms = markdown_field(@term, :terms) -.row-content-block.footer-block.clearfix - - if can?(current_user, :accept_terms, @term) - .float-right - = button_to accept_term_path(@term, redirect_params), class: 'btn btn-success prepend-left-8' do - = _('Accept terms') - - if can?(current_user, :decline_terms, @term) - .float-right - = button_to decline_term_path(@term, redirect_params), class: 'btn btn-default prepend-left-8' do - = _('Decline and sign out') +- if current_user + .card-footer.footer-block.clearfix + - if can?(current_user, :accept_terms, @term) + .float-right + = button_to accept_term_path(@term, redirect_params), class: 'btn btn-success prepend-left-8' do + = _('Accept terms') + - else + .pull-right + = link_to root_path, class: 'btn btn-success prepend-left-8' do + = _('Continue') + - if can?(current_user, :decline_terms, @term) + .float-right + = button_to decline_term_path(@term, redirect_params), class: 'btn btn-default prepend-left-8' do + = _('Decline and sign out') diff --git a/app/workers/all_queues.yml b/app/workers/all_queues.yml index 93e57512edb..30b6796a7d6 100644 --- a/app/workers/all_queues.yml +++ b/app/workers/all_queues.yml @@ -17,6 +17,7 @@ - cronjob:stuck_ci_jobs - cronjob:stuck_import_jobs - cronjob:stuck_merge_jobs +- cronjob:ci_archive_traces_cron - cronjob:trending_projects - cronjob:issue_due_scheduler @@ -30,12 +31,14 @@ - github_importer:github_import_import_diff_note - github_importer:github_import_import_issue - github_importer:github_import_import_note +- github_importer:github_import_import_lfs_object - github_importer:github_import_import_pull_request - github_importer:github_import_refresh_import_jid - github_importer:github_import_stage_finish_import - github_importer:github_import_stage_import_base_data - github_importer:github_import_stage_import_issues_and_diff_notes - github_importer:github_import_stage_import_notes +- github_importer:github_import_stage_import_lfs_objects - github_importer:github_import_stage_import_pull_requests - github_importer:github_import_stage_import_repository diff --git a/app/workers/ci/archive_traces_cron_worker.rb b/app/workers/ci/archive_traces_cron_worker.rb new file mode 100644 index 00000000000..2ac65f41f4e --- /dev/null +++ b/app/workers/ci/archive_traces_cron_worker.rb @@ -0,0 +1,26 @@ +module Ci + class ArchiveTracesCronWorker + include ApplicationWorker + include CronjobQueue + + def perform + # Archive stale live traces which still resides in redis or database + # This could happen when ArchiveTraceWorker sidekiq jobs were lost by receiving SIGKILL + # More details in https://gitlab.com/gitlab-org/gitlab-ce/issues/36791 + Ci::Build.finished.with_live_trace.find_each(batch_size: 100) do |build| + begin + build.trace.archive! + rescue => e + failed_archive_counter.increment + Rails.logger.error "Failed to archive stale live trace. id: #{build.id} message: #{e.message}" + end + end + end + + private + + def failed_archive_counter + @failed_archive_counter ||= Gitlab::Metrics.counter(:job_trace_archive_failed_total, "Counter of failed attempts of traces archiving") + end + end +end diff --git a/app/workers/git_garbage_collect_worker.rb b/app/workers/git_garbage_collect_worker.rb index be4203bc7ad..ae5c5fac834 100644 --- a/app/workers/git_garbage_collect_worker.rb +++ b/app/workers/git_garbage_collect_worker.rb @@ -6,12 +6,6 @@ class GitGarbageCollectWorker # Timeout set to 24h LEASE_TIMEOUT = 86400 - GITALY_MIGRATED_TASKS = { - gc: :garbage_collect, - full_repack: :repack_full, - incremental_repack: :repack_incremental - }.freeze - def perform(project_id, task = :gc, lease_key = nil, lease_uuid = nil) project = Project.find(project_id) active_uuid = get_lease_uuid(lease_key) @@ -27,21 +21,7 @@ class GitGarbageCollectWorker end task = task.to_sym - cmd = command(task) - - gitaly_migrate(GITALY_MIGRATED_TASKS[task]) do |is_enabled| - if is_enabled - gitaly_call(task, project.repository.raw_repository) - else - repo_path = project.repository.path_to_repo - description = "'#{cmd.join(' ')}' in #{repo_path}" - Gitlab::GitLogger.info(description) - - output, status = Gitlab::Popen.popen(cmd, repo_path) - - Gitlab::GitLogger.error("#{description} failed:\n#{output}") unless status.zero? - end - end + gitaly_call(task, project.repository.raw_repository) # Refresh the branch cache in case garbage collection caused a ref lookup to fail flush_ref_caches(project) if task == :gc @@ -82,21 +62,12 @@ class GitGarbageCollectWorker when :incremental_repack client.repack_incremental end - end - - def command(task) - case task - when :gc - git(write_bitmaps: bitmaps_enabled?) + %w[gc] - when :full_repack - git(write_bitmaps: bitmaps_enabled?) + %w[repack -A -d --pack-kept-objects] - when :incremental_repack - # Normal git repack fails when bitmaps are enabled. It is impossible to - # create a bitmap here anyway. - git(write_bitmaps: false) + %w[repack -d] - else - raise "Invalid gc task: #{task.inspect}" - end + rescue GRPC::NotFound => e + Gitlab::GitLogger.error("#{method} failed:\nRepository not found") + raise Gitlab::Git::Repository::NoRepository.new(e) + rescue GRPC::BadStatus => e + Gitlab::GitLogger.error("#{method} failed:\n#{e}") + raise Gitlab::Git::CommandError.new(e) end def flush_ref_caches(project) @@ -108,19 +79,4 @@ class GitGarbageCollectWorker def bitmaps_enabled? Gitlab::CurrentSettings.housekeeping_bitmaps_enabled end - - def git(write_bitmaps:) - config_value = write_bitmaps ? 'true' : 'false' - %W[git -c repack.writeBitmaps=#{config_value}] - end - - def gitaly_migrate(method, &block) - Gitlab::GitalyClient.migrate(method, &block) - rescue GRPC::NotFound => e - Gitlab::GitLogger.error("#{method} failed:\nRepository not found") - raise Gitlab::Git::Repository::NoRepository.new(e) - rescue GRPC::BadStatus => e - Gitlab::GitLogger.error("#{method} failed:\n#{e}") - raise Gitlab::Git::CommandError.new(e) - end end diff --git a/app/workers/gitlab/github_import/advance_stage_worker.rb b/app/workers/gitlab/github_import/advance_stage_worker.rb index 8d708e15a66..be0b6c180b0 100644 --- a/app/workers/gitlab/github_import/advance_stage_worker.rb +++ b/app/workers/gitlab/github_import/advance_stage_worker.rb @@ -21,6 +21,7 @@ module Gitlab STAGES = { issues_and_diff_notes: Stage::ImportIssuesAndDiffNotesWorker, notes: Stage::ImportNotesWorker, + lfs_objects: Stage::ImportLfsObjectsWorker, finish: Stage::FinishImportWorker }.freeze diff --git a/app/workers/gitlab/github_import/import_lfs_object_worker.rb b/app/workers/gitlab/github_import/import_lfs_object_worker.rb new file mode 100644 index 00000000000..520c5cb091a --- /dev/null +++ b/app/workers/gitlab/github_import/import_lfs_object_worker.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +module Gitlab + module GithubImport + class ImportLfsObjectWorker + include ObjectImporter + + def representation_class + Representation::LfsObject + end + + def importer_class + Importer::LfsObjectImporter + end + + def counter_name + :github_importer_imported_lfs_objects + end + + def counter_description + 'The number of imported GitHub Lfs Objects' + end + end + end +end diff --git a/app/workers/gitlab/github_import/stage/import_lfs_objects_worker.rb b/app/workers/gitlab/github_import/stage/import_lfs_objects_worker.rb new file mode 100644 index 00000000000..29257603a9d --- /dev/null +++ b/app/workers/gitlab/github_import/stage/import_lfs_objects_worker.rb @@ -0,0 +1,32 @@ +# frozen_string_literal: true + +module Gitlab + module GithubImport + module Stage + class ImportLfsObjectsWorker + include ApplicationWorker + include GithubImport::Queue + include StageMethods + + def perform(project_id) + return unless (project = find_project(project_id)) + + import(project) + end + + # project - An instance of Project. + def import(project) + waiter = Importer::LfsObjectsImporter + .new(project, nil) + .execute + + AdvanceStageWorker.perform_async( + project.id, + { waiter.key => waiter.jobs_remaining }, + :finish + ) + end + end + end + end +end diff --git a/app/workers/gitlab/github_import/stage/import_notes_worker.rb b/app/workers/gitlab/github_import/stage/import_notes_worker.rb index 5f4678a595f..ccf0013180d 100644 --- a/app/workers/gitlab/github_import/stage/import_notes_worker.rb +++ b/app/workers/gitlab/github_import/stage/import_notes_worker.rb @@ -18,7 +18,7 @@ module Gitlab AdvanceStageWorker.perform_async( project.id, { waiter.key => waiter.jobs_remaining }, - :finish + :lfs_objects ) end end diff --git a/app/workers/repository_fork_worker.rb b/app/workers/repository_fork_worker.rb index 08b1c3a7d7a..dbb215f1964 100644 --- a/app/workers/repository_fork_worker.rb +++ b/app/workers/repository_fork_worker.rb @@ -8,27 +8,12 @@ class RepositoryForkWorker target_project_id = args.shift target_project = Project.find(target_project_id) - # By v10.8, we should've drained the queue of all jobs using the old arguments. - # We can remove the else clause if we're no longer logging the message in that clause. - # See https://gitlab.com/gitlab-org/gitaly/issues/1110 - if args.empty? - source_project = target_project.forked_from_project - unless source_project - return target_project.mark_import_as_failed('Source project cannot be found.') - end - - fork_repository(target_project, source_project.repository_storage, source_project.disk_path) - else - Rails.logger.info("Project #{target_project.id} is being forked using old-style arguments.") - - source_repository_storage_path, source_disk_path = *args - - source_repository_storage_name = Gitlab.config.repositories.storages.find do |_, info| - info.legacy_disk_path == source_repository_storage_path - end&.first || raise("no shard found for path '#{source_repository_storage_path}'") - - fork_repository(target_project, source_repository_storage_name, source_disk_path) + source_project = target_project.forked_from_project + unless source_project + return target_project.mark_import_as_failed('Source project cannot be found.') end + + fork_repository(target_project, source_project.repository_storage, source_project.disk_path) end private diff --git a/app/workers/storage_migrator_worker.rb b/app/workers/storage_migrator_worker.rb index f92421a667d..0aff0c4c7c6 100644 --- a/app/workers/storage_migrator_worker.rb +++ b/app/workers/storage_migrator_worker.rb @@ -1,29 +1,8 @@ class StorageMigratorWorker include ApplicationWorker - BATCH_SIZE = 100 - def perform(start, finish) - projects = build_relation(start, finish) - - projects.with_route.find_each(batch_size: BATCH_SIZE) do |project| - Rails.logger.info "Starting storage migration of #{project.full_path} (ID=#{project.id})..." - - begin - project.migrate_to_hashed_storage! - rescue => err - Rails.logger.error("#{err.message} migrating storage of #{project.full_path} (ID=#{project.id}), trace - #{err.backtrace}") - end - end - end - - def build_relation(start, finish) - relation = Project - table = Project.arel_table - - relation = relation.where(table[:id].gteq(start)) if start - relation = relation.where(table[:id].lteq(finish)) if finish - - relation + migrator = Gitlab::HashedStorage::Migrator.new + migrator.bulk_migrate(start, finish) end end |