diff options
53 files changed, 900 insertions, 82 deletions
diff --git a/GITLAB_ELASTICSEARCH_INDEXER_VERSION b/GITLAB_ELASTICSEARCH_INDEXER_VERSION index f0bb29e7638..88c5fb891dc 100644 --- a/GITLAB_ELASTICSEARCH_INDEXER_VERSION +++ b/GITLAB_ELASTICSEARCH_INDEXER_VERSION @@ -1 +1 @@ -1.3.0 +1.4.0 diff --git a/app/assets/javascripts/boards/components/board_card.vue b/app/assets/javascripts/boards/components/board_card.vue index faf722f61af..12d68256598 100644 --- a/app/assets/javascripts/boards/components/board_card.vue +++ b/app/assets/javascripts/boards/components/board_card.vue @@ -42,12 +42,19 @@ export default { return { showDetail: false, detailIssue: boardsStore.detail, + multiSelect: boardsStore.multiSelect, }; }, computed: { issueDetailVisible() { return this.detailIssue.issue && this.detailIssue.issue.id === this.issue.id; }, + multiSelectVisible() { + return this.multiSelect.list.findIndex(issue => issue.id === this.issue.id) > -1; + }, + canMultiSelect() { + return gon.features && gon.features.multiSelectBoard; + }, }, methods: { mouseDown() { @@ -58,14 +65,20 @@ export default { }, showIssue(e) { if (e.target.classList.contains('js-no-trigger')) return; - if (this.showDetail) { this.showDetail = false; + // If CMD or CTRL is clicked + const isMultiSelect = this.canMultiSelect && (e.ctrlKey || e.metaKey); + if (boardsStore.detail.issue && boardsStore.detail.issue.id === this.issue.id) { - eventHub.$emit('clearDetailIssue'); + eventHub.$emit('clearDetailIssue', isMultiSelect); + + if (isMultiSelect) { + eventHub.$emit('newDetailIssue', this.issue, isMultiSelect); + } } else { - eventHub.$emit('newDetailIssue', this.issue); + eventHub.$emit('newDetailIssue', this.issue, isMultiSelect); boardsStore.setListDetail(this.list); } } @@ -77,6 +90,7 @@ export default { <template> <li :class="{ + 'multi-select': multiSelectVisible, 'user-can-drag': !disabled && issue.id, 'is-disabled': disabled || !issue.id, 'is-active': issueDetailVisible, diff --git a/app/assets/javascripts/boards/components/board_list.vue b/app/assets/javascripts/boards/components/board_list.vue index de41698ca04..1273fcc6a91 100644 --- a/app/assets/javascripts/boards/components/board_list.vue +++ b/app/assets/javascripts/boards/components/board_list.vue @@ -1,12 +1,22 @@ <script> -/* eslint-disable @gitlab/vue-i18n/no-bare-strings */ -import Sortable from 'sortablejs'; +import { Sortable, MultiDrag } from 'sortablejs'; import { GlLoadingIcon } from '@gitlab/ui'; +import _ from 'underscore'; import boardNewIssue from './board_new_issue.vue'; import boardCard from './board_card.vue'; import eventHub from '../eventhub'; import boardsStore from '../stores/boards_store'; -import { getBoardSortableDefaultOptions, sortableStart } from '../mixins/sortable_default_options'; +import { sprintf, __ } from '~/locale'; +import createFlash from '~/flash'; +import { + getBoardSortableDefaultOptions, + sortableStart, + sortableEnd, +} from '../mixins/sortable_default_options'; + +if (gon.features && gon.features.multiSelectBoard) { + Sortable.mount(new MultiDrag()); +} export default { name: 'BoardList', @@ -54,6 +64,14 @@ export default { showIssueForm: false, }; }, + computed: { + paginatedIssueText() { + return sprintf(__('Showing %{pageSize} of %{total} issues'), { + pageSize: this.list.issues.length, + total: this.list.issuesSize, + }); + }, + }, watch: { filters: { handler() { @@ -87,11 +105,20 @@ export default { eventHub.$on(`scroll-board-list-${this.list.id}`, this.scrollToTop); }, mounted() { + const multiSelectOpts = {}; + if (gon.features && gon.features.multiSelectBoard) { + multiSelectOpts.multiDrag = true; + multiSelectOpts.selectedClass = 'js-multi-select'; + multiSelectOpts.animation = 500; + } + const options = getBoardSortableDefaultOptions({ scroll: true, disabled: this.disabled, filter: '.board-list-count, .is-disabled', dataIdAttr: 'data-issue-id', + removeCloneOnHide: false, + ...multiSelectOpts, group: { name: 'issues', /** @@ -145,25 +172,66 @@ export default { card.showDetail = false; const { list } = card; + const issue = list.findIssue(Number(e.item.dataset.issueId)); + boardsStore.startMoving(list, issue); sortableStart(); }, onAdd: e => { - boardsStore.moveIssueToList( - boardsStore.moving.list, - this.list, - boardsStore.moving.issue, - e.newIndex, - ); + const { items = [], newIndicies = [] } = e; + if (items.length) { + // Not using e.newIndex here instead taking a min of all + // the newIndicies. Basically we have to find that during + // a drop what is the index we're going to start putting + // all the dropped elements from. + const newIndex = Math.min(...newIndicies.map(obj => obj.index).filter(i => i !== -1)); + const issues = items.map(item => + boardsStore.moving.list.findIssue(Number(item.dataset.issueId)), + ); - this.$nextTick(() => { - e.item.remove(); - }); + boardsStore.moveMultipleIssuesToList({ + listFrom: boardsStore.moving.list, + listTo: this.list, + issues, + newIndex, + }); + } else { + boardsStore.moveIssueToList( + boardsStore.moving.list, + this.list, + boardsStore.moving.issue, + e.newIndex, + ); + this.$nextTick(() => { + e.item.remove(); + }); + } }, onUpdate: e => { const sortedArray = this.sortable.toArray().filter(id => id !== '-1'); + + const { items = [], newIndicies = [], oldIndicies = [] } = e; + if (items.length) { + const newIndex = Math.min(...newIndicies.map(obj => obj.index)); + const issues = items.map(item => + boardsStore.moving.list.findIssue(Number(item.dataset.issueId)), + ); + boardsStore.moveMultipleIssuesInList({ + list: this.list, + issues, + oldIndicies: oldIndicies.map(obj => obj.index), + newIndex, + idArray: sortedArray, + }); + e.items.forEach(el => { + Sortable.utils.deselect(el); + }); + boardsStore.clearMultiSelect(); + return; + } + boardsStore.moveIssueInList( this.list, boardsStore.moving.issue, @@ -172,9 +240,133 @@ export default { sortedArray, ); }, + onEnd: e => { + const { items = [], clones = [], to } = e; + + // This is not a multi select operation + if (!items.length && !clones.length) { + sortableEnd(); + return; + } + + let toList; + if (to) { + const containerEl = to.closest('.js-board-list'); + toList = boardsStore.findList('id', Number(containerEl.dataset.board)); + } + + /** + * onEnd is called irrespective if the cards were moved in the + * same list or the other list. Don't remove items if it's same list. + */ + const isSameList = toList && toList.id === this.list.id; + + if (toList && !isSameList && boardsStore.shouldRemoveIssue(this.list, toList)) { + const issues = items.map(item => this.list.findIssue(Number(item.dataset.issueId))); + + if (_.compact(issues).length && !boardsStore.issuesAreContiguous(this.list, issues)) { + const indexes = []; + const ids = this.list.issues.map(i => i.id); + issues.forEach(issue => { + const index = ids.indexOf(issue.id); + if (index > -1) { + indexes.push(index); + } + }); + + // Descending sort because splice would cause index discrepancy otherwise + const sortedIndexes = indexes.sort((a, b) => (a < b ? 1 : -1)); + + sortedIndexes.forEach(i => { + /** + * **setTimeout and splice each element one-by-one in a loop + * is intended.** + * + * The problem here is all the indexes are in the list but are + * non-contiguous. Due to that, when we splice all the indexes, + * at once, Vue -- during a re-render -- is unable to find reference + * nodes and the entire app crashes. + * + * If the indexes are contiguous, this piece of code is not + * executed. If it is, this is a possible regression. Only when + * issue indexes are far apart, this logic should ever kick in. + */ + setTimeout(() => { + this.list.issues.splice(i, 1); + }, 0); + }); + } + } + + if (!toList) { + createFlash(__('Something went wrong while performing the action.')); + } + + if (!isSameList) { + boardsStore.clearMultiSelect(); + + // Since Vue's list does not re-render the same keyed item, we'll + // remove `multi-select` class to express it's unselected + if (clones && clones.length) { + clones.forEach(el => el.classList.remove('multi-select')); + } + + // Due to some bug which I am unable to figure out + // Sortable does not deselect some pending items from the + // source list. + // We'll just do it forcefully here. + Array.from(document.querySelectorAll('.js-multi-select') || []).forEach(item => { + Sortable.utils.deselect(item); + }); + + /** + * SortableJS leaves all the moving items "as is" on the DOM. + * Vue picks up and rehydrates the DOM, but we need to explicity + * remove the "trash" items from the DOM. + * + * This is in parity to the logic on single item move from a list/in + * a list. For reference, look at the implementation of onAdd method. + */ + this.$nextTick(() => { + if (items && items.length) { + items.forEach(item => { + item.remove(); + }); + } + }); + } + sortableEnd(); + }, onMove(e) { return !e.related.classList.contains('board-list-count'); }, + onSelect(e) { + const { + item: { classList }, + } = e; + + if ( + classList && + classList.contains('js-multi-select') && + !classList.contains('multi-select') + ) { + Sortable.utils.deselect(e.item); + } + }, + onDeselect: e => { + const { + item: { dataset, classList }, + } = e; + + if ( + classList && + classList.contains('multi-select') && + !classList.contains('js-multi-select') + ) { + const issue = this.list.findIssue(Number(dataset.issueId)); + boardsStore.toggleMultiSelect(issue); + } + }, }); this.sortable = Sortable.create(this.$refs.list, options); @@ -260,7 +452,7 @@ export default { <li v-if="showCount" class="board-list-count text-center" data-issue-id="-1"> <gl-loading-icon v-show="list.loadingMore" label="Loading more issues" /> <span v-if="list.issues.length === list.issuesSize">{{ __('Showing all issues') }}</span> - <span v-else> Showing {{ list.issues.length }} of {{ list.issuesSize }} issues </span> + <span v-else>{{ paginatedIssueText }}</span> </li> </ul> </div> diff --git a/app/assets/javascripts/boards/constants.js b/app/assets/javascripts/boards/constants.js new file mode 100644 index 00000000000..3c66c7a0660 --- /dev/null +++ b/app/assets/javascripts/boards/constants.js @@ -0,0 +1,11 @@ +export const ListType = { + assignee: 'assignee', + milestone: 'milestone', + backlog: 'backlog', + closed: 'closed', + label: 'label', +}; + +export default { + ListType, +}; diff --git a/app/assets/javascripts/boards/index.js b/app/assets/javascripts/boards/index.js index da2669e7cde..befca70eeae 100644 --- a/app/assets/javascripts/boards/index.js +++ b/app/assets/javascripts/boards/index.js @@ -146,7 +146,7 @@ export default () => { updateTokens() { this.filterManager.updateTokens(); }, - updateDetailIssue(newIssue) { + updateDetailIssue(newIssue, multiSelect = false) { const { sidebarInfoEndpoint } = newIssue; if (sidebarInfoEndpoint && newIssue.subscribed === undefined) { newIssue.setFetchingState('subscriptions', true); @@ -185,9 +185,23 @@ export default () => { }); } + if (multiSelect) { + boardsStore.toggleMultiSelect(newIssue); + + if (boardsStore.detail.issue) { + boardsStore.clearDetailIssue(); + return; + } + + return; + } + boardsStore.setIssueDetail(newIssue); }, - clearDetailIssue() { + clearDetailIssue(multiSelect = false) { + if (multiSelect) { + boardsStore.clearMultiSelect(); + } boardsStore.clearDetailIssue(); }, toggleSubscription(id) { diff --git a/app/assets/javascripts/boards/models/list.js b/app/assets/javascripts/boards/models/list.js index b3e56a34c28..1e213c324eb 100644 --- a/app/assets/javascripts/boards/models/list.js +++ b/app/assets/javascripts/boards/models/list.js @@ -5,6 +5,7 @@ import ListLabel from './label'; import ListAssignee from './assignee'; import ListIssue from 'ee_else_ce/boards/models/issue'; import { urlParamsToObject } from '~/lib/utils/common_utils'; +import flash from '~/flash'; import boardsStore from '../stores/boards_store'; import ListMilestone from './milestone'; @@ -176,6 +177,53 @@ class List { }); } + addMultipleIssues(issues, listFrom, newIndex) { + let moveBeforeId = null; + let moveAfterId = null; + + const listHasIssues = issues.every(issue => this.findIssue(issue.id)); + + if (!listHasIssues) { + if (newIndex !== undefined) { + if (this.issues[newIndex - 1]) { + moveBeforeId = this.issues[newIndex - 1].id; + } + + if (this.issues[newIndex]) { + moveAfterId = this.issues[newIndex].id; + } + + this.issues.splice(newIndex, 0, ...issues); + } else { + this.issues.push(...issues); + } + + if (this.label) { + issues.forEach(issue => issue.addLabel(this.label)); + } + + if (this.assignee) { + if (listFrom && listFrom.type === 'assignee') { + issues.forEach(issue => issue.removeAssignee(listFrom.assignee)); + } + issues.forEach(issue => issue.addAssignee(this.assignee)); + } + + if (IS_EE && this.milestone) { + if (listFrom && listFrom.type === 'milestone') { + issues.forEach(issue => issue.removeMilestone(listFrom.milestone)); + } + issues.forEach(issue => issue.addMilestone(this.milestone)); + } + + if (listFrom) { + this.issuesSize += issues.length; + + this.updateMultipleIssues(issues, listFrom, moveBeforeId, moveAfterId); + } + } + } + addIssue(issue, listFrom, newIndex) { let moveBeforeId = null; let moveAfterId = null; @@ -230,6 +278,23 @@ class List { }); } + moveMultipleIssues({ issues, oldIndicies, newIndex, moveBeforeId, moveAfterId }) { + oldIndicies.reverse().forEach(index => { + this.issues.splice(index, 1); + }); + this.issues.splice(newIndex, 0, ...issues); + + gl.boardService + .moveMultipleIssues({ + ids: issues.map(issue => issue.id), + fromListId: null, + toListId: null, + moveBeforeId, + moveAfterId, + }) + .catch(() => flash(__('Something went wrong while moving issues.'))); + } + updateIssueLabel(issue, listFrom, moveBeforeId, moveAfterId) { gl.boardService .moveIssue(issue.id, listFrom.id, this.id, moveBeforeId, moveAfterId) @@ -238,10 +303,37 @@ class List { }); } + updateMultipleIssues(issues, listFrom, moveBeforeId, moveAfterId) { + gl.boardService + .moveMultipleIssues({ + ids: issues.map(issue => issue.id), + fromListId: listFrom.id, + toListId: this.id, + moveBeforeId, + moveAfterId, + }) + .catch(() => flash(__('Something went wrong while moving issues.'))); + } + findIssue(id) { return this.issues.find(issue => issue.id === id); } + removeMultipleIssues(removeIssues) { + const ids = removeIssues.map(issue => issue.id); + + this.issues = this.issues.filter(issue => { + const matchesRemove = ids.includes(issue.id); + + if (matchesRemove) { + this.issuesSize -= 1; + issue.removeLabel(this.label); + } + + return !matchesRemove; + }); + } + removeIssue(removeIssue) { this.issues = this.issues.filter(issue => { const matchesRemove = removeIssue.id === issue.id; diff --git a/app/assets/javascripts/boards/services/board_service.js b/app/assets/javascripts/boards/services/board_service.js index 0d11db89511..03369febb4a 100644 --- a/app/assets/javascripts/boards/services/board_service.js +++ b/app/assets/javascripts/boards/services/board_service.js @@ -48,6 +48,16 @@ export default class BoardService { return boardsStore.moveIssue(id, fromListId, toListId, moveBeforeId, moveAfterId); } + moveMultipleIssues({ + ids, + fromListId = null, + toListId = null, + moveBeforeId = null, + moveAfterId = null, + }) { + return boardsStore.moveMultipleIssues({ ids, fromListId, toListId, moveBeforeId, moveAfterId }); + } + newIssue(id, issue) { return boardsStore.newIssue(id, issue); } diff --git a/app/assets/javascripts/boards/stores/boards_store.js b/app/assets/javascripts/boards/stores/boards_store.js index 6da1cca9628..8b737d1dab0 100644 --- a/app/assets/javascripts/boards/stores/boards_store.js +++ b/app/assets/javascripts/boards/stores/boards_store.js @@ -11,6 +11,7 @@ import { __ } from '~/locale'; import axios from '~/lib/utils/axios_utils'; import { mergeUrlParams } from '~/lib/utils/url_utility'; import eventHub from '../eventhub'; +import { ListType } from '../constants'; const boardsStore = { disabled: false, @@ -39,6 +40,7 @@ const boardsStore = { issue: {}, list: {}, }, + multiSelect: { list: [] }, setEndpoints({ boardsEndpoint, listsEndpoint, bulkUpdatePath, boardId, recentBoardsEndpoint }) { const listsEndpointGenerate = `${listsEndpoint}/generate.json`; @@ -51,7 +53,6 @@ const boardsStore = { recentBoardsEndpoint: `${recentBoardsEndpoint}.json`, }; }, - create() { this.state.lists = []; this.filter.path = getUrlParamsArray().join('&'); @@ -134,6 +135,107 @@ const boardsStore = { Object.assign(this.moving, { list, issue }); }, + moveMultipleIssuesToList({ listFrom, listTo, issues, newIndex }) { + const issueTo = issues.map(issue => listTo.findIssue(issue.id)); + const issueLists = _.flatten(issues.map(issue => issue.getLists())); + const listLabels = issueLists.map(list => list.label); + + const hasMoveableIssues = _.compact(issueTo).length > 0; + + if (!hasMoveableIssues) { + // Check if target list assignee is already present in this issue + if ( + listTo.type === ListType.assignee && + listFrom.type === ListType.assignee && + issues.some(issue => issue.findAssignee(listTo.assignee)) + ) { + const targetIssues = issues.map(issue => listTo.findIssue(issue.id)); + targetIssues.forEach(targetIssue => targetIssue.removeAssignee(listFrom.assignee)); + } else if (listTo.type === 'milestone') { + const currentMilestones = issues.map(issue => issue.milestone); + const currentLists = this.state.lists + .filter(list => list.type === 'milestone' && list.id !== listTo.id) + .filter(list => + list.issues.some(listIssue => issues.some(issue => listIssue.id === issue.id)), + ); + + issues.forEach(issue => { + currentMilestones.forEach(milestone => { + issue.removeMilestone(milestone); + }); + }); + + issues.forEach(issue => { + issue.addMilestone(listTo.milestone); + }); + + currentLists.forEach(currentList => { + issues.forEach(issue => { + currentList.removeIssue(issue); + }); + }); + + listTo.addMultipleIssues(issues, listFrom, newIndex); + } else { + // Add to new lists issues if it doesn't already exist + listTo.addMultipleIssues(issues, listFrom, newIndex); + } + } else { + listTo.updateMultipleIssues(issues, listFrom); + issues.forEach(issue => { + issue.removeLabel(listFrom.label); + }); + } + + if (listTo.type === ListType.closed && listFrom.type !== ListType.backlog) { + issueLists.forEach(list => { + issues.forEach(issue => { + list.removeIssue(issue); + }); + }); + + issues.forEach(issue => { + issue.removeLabels(listLabels); + }); + } else if (listTo.type === ListType.backlog && listFrom.type === ListType.assignee) { + issues.forEach(issue => { + issue.removeAssignee(listFrom.assignee); + }); + issueLists.forEach(list => { + issues.forEach(issue => { + list.removeIssue(issue); + }); + }); + } else if (listTo.type === ListType.backlog && listFrom.type === ListType.milestone) { + issues.forEach(issue => { + issue.removeMilestone(listFrom.milestone); + }); + issueLists.forEach(list => { + issues.forEach(issue => { + list.removeIssue(issue); + }); + }); + } else if ( + this.shouldRemoveIssue(listFrom, listTo) && + this.issuesAreContiguous(listFrom, issues) + ) { + listFrom.removeMultipleIssues(issues); + } + }, + + issuesAreContiguous(list, issues) { + // When there's only 1 issue selected, we can return early. + if (issues.length === 1) return true; + + // Create list of ids for issues involved. + const listIssueIds = list.issues.map(issue => issue.id); + const movedIssueIds = issues.map(issue => issue.id); + + // Check if moved issue IDs is sub-array + // of source list issue IDs (i.e. contiguous selection). + return listIssueIds.join('|').includes(movedIssueIds.join('|')); + }, + moveIssueToList(listFrom, listTo, issue, newIndex) { const issueTo = listTo.findIssue(issue.id); const issueLists = issue.getLists(); @@ -195,6 +297,17 @@ const boardsStore = { list.moveIssue(issue, oldIndex, newIndex, beforeId, afterId); }, + moveMultipleIssuesInList({ list, issues, oldIndicies, newIndex, idArray }) { + const beforeId = parseInt(idArray[newIndex - 1], 10) || null; + const afterId = parseInt(idArray[newIndex + issues.length], 10) || null; + list.moveMultipleIssues({ + issues, + oldIndicies, + newIndex, + moveBeforeId: beforeId, + moveAfterId: afterId, + }); + }, findList(key, val, type = 'label') { const filteredList = this.state.lists.filter(list => { const byType = type @@ -260,6 +373,10 @@ const boardsStore = { }`; }, + generateMultiDragPath(boardId) { + return `${gon.relative_url_root}/-/boards/${boardId ? `${boardId}` : ''}/issues/bulk_move`; + }, + all() { return axios.get(this.state.endpoints.listsEndpoint); }, @@ -309,6 +426,16 @@ const boardsStore = { }); }, + moveMultipleIssues({ ids, fromListId, toListId, moveBeforeId, moveAfterId }) { + return axios.put(this.generateMultiDragPath(this.state.endpoints.boardId), { + from_list_id: fromListId, + to_list_id: toListId, + move_before_id: moveBeforeId, + move_after_id: moveAfterId, + ids, + }); + }, + newIssue(id, issue) { return axios.post(this.generateIssuesPath(id), { issue, @@ -379,6 +506,25 @@ const boardsStore = { setCurrentBoard(board) { this.state.currentBoard = board; }, + + toggleMultiSelect(issue) { + const selectedIssueIds = this.multiSelect.list.map(issue => issue.id); + const index = selectedIssueIds.indexOf(issue.id); + + if (index === -1) { + this.multiSelect.list.push(issue); + return; + } + + this.multiSelect.list = [ + ...this.multiSelect.list.slice(0, index), + ...this.multiSelect.list.slice(index + 1), + ]; + }, + + clearMultiSelect() { + this.multiSelect.list = []; + }, }; BoardsStoreEE.initEESpecific(boardsStore); diff --git a/app/assets/javascripts/test_utils/index.js b/app/assets/javascripts/test_utils/index.js index 1e75ee60671..1a1f3e8d0a8 100644 --- a/app/assets/javascripts/test_utils/index.js +++ b/app/assets/javascripts/test_utils/index.js @@ -1,8 +1,10 @@ import 'core-js/es/map'; import 'core-js/es/set'; +import { Sortable } from 'sortablejs'; import simulateDrag from './simulate_drag'; import simulateInput from './simulate_input'; // Export to global space for rspec to use window.simulateDrag = simulateDrag; window.simulateInput = simulateInput; +window.Sortable = Sortable; diff --git a/app/assets/stylesheets/pages/boards.scss b/app/assets/stylesheets/pages/boards.scss index d540a347dde..2a7a53d8bd7 100644 --- a/app/assets/stylesheets/pages/boards.scss +++ b/app/assets/stylesheets/pages/boards.scss @@ -245,6 +245,7 @@ box-shadow: 0 1px 2px $issue-boards-card-shadow; line-height: $gl-padding; list-style: none; + position: relative; &:not(:last-child) { margin-bottom: $gl-padding-8; @@ -255,6 +256,11 @@ background-color: $blue-50; } + &.multi-select { + border-color: $blue-200; + background-color: $blue-50; + } + .badge { border: 0; outline: 0; diff --git a/app/controllers/groups/boards_controller.rb b/app/controllers/groups/boards_controller.rb index 40b8d5ed72c..3c86f3108ab 100644 --- a/app/controllers/groups/boards_controller.rb +++ b/app/controllers/groups/boards_controller.rb @@ -5,6 +5,9 @@ class Groups::BoardsController < Groups::ApplicationController include RecordUserLastActivity before_action :assign_endpoint_vars + before_action do + push_frontend_feature_flag(:multi_select_board) + end private diff --git a/app/controllers/projects/boards_controller.rb b/app/controllers/projects/boards_controller.rb index 14b02993e6e..3b335fa4af4 100644 --- a/app/controllers/projects/boards_controller.rb +++ b/app/controllers/projects/boards_controller.rb @@ -7,6 +7,9 @@ class Projects::BoardsController < Projects::ApplicationController before_action :check_issues_available! before_action :authorize_read_board!, only: [:index, :show] before_action :assign_endpoint_vars + before_action do + push_frontend_feature_flag(:multi_select_board) + end private diff --git a/app/models/ci/persistent_ref.rb b/app/models/ci/persistent_ref.rb index 9bb67c88577..be3d4aa3203 100644 --- a/app/models/ci/persistent_ref.rb +++ b/app/models/ci/persistent_ref.rb @@ -14,15 +14,13 @@ module Ci delegate :ref_exists?, :create_ref, :delete_refs, to: :repository def exist? - return unless enabled? - ref_exists?(path) rescue false end def create - return unless enabled? && !exist? + return if exist? create_ref(sha, path) rescue => e @@ -31,8 +29,6 @@ module Ci end def delete - return unless enabled? - delete_refs(path) rescue Gitlab::Git::Repository::NoRepository # no-op @@ -44,11 +40,5 @@ module Ci def path "refs/#{Repository::REF_PIPELINES}/#{pipeline.id}" end - - private - - def enabled? - Feature.enabled?(:depend_on_persistent_pipeline_ref, project) - end end end diff --git a/app/workers/gitlab/github_import/stage/finish_import_worker.rb b/app/workers/gitlab/github_import/stage/finish_import_worker.rb index a779e631516..ee64f62637e 100644 --- a/app/workers/gitlab/github_import/stage/finish_import_worker.rb +++ b/app/workers/gitlab/github_import/stage/finish_import_worker.rb @@ -8,6 +8,10 @@ module Gitlab include GithubImport::Queue include StageMethods + # technical debt: https://gitlab.com/gitlab-org/gitlab/issues/33991 + sidekiq_options memory_killer_memory_growth_kb: ENV.fetch('MEMORY_KILLER_FINISH_IMPORT_WORKER_MEMORY_GROWTH_KB', 50).to_i + sidekiq_options memory_killer_max_memory_growth_kb: ENV.fetch('MEMORY_KILLER_FINISH_IMPORT_WORKER_MAX_MEMORY_GROWTH_KB', 200_000).to_i + # project - An instance of Project. def import(_, project) project.after_import diff --git a/app/workers/gitlab/github_import/stage/import_repository_worker.rb b/app/workers/gitlab/github_import/stage/import_repository_worker.rb index 4d16cef1130..b5e30470070 100644 --- a/app/workers/gitlab/github_import/stage/import_repository_worker.rb +++ b/app/workers/gitlab/github_import/stage/import_repository_worker.rb @@ -8,6 +8,10 @@ module Gitlab include GithubImport::Queue include StageMethods + # technical debt: https://gitlab.com/gitlab-org/gitlab/issues/33991 + sidekiq_options memory_killer_memory_growth_kb: ENV.fetch('MEMORY_KILLER_IMPORT_REPOSITORY_WORKER_MEMORY_GROWTH_KB', 50).to_i + sidekiq_options memory_killer_max_memory_growth_kb: ENV.fetch('MEMORY_KILLER_IMPORT_REPOSITORY_WORKER_MAX_MEMORY_GROWTH_KB', 300_000).to_i + # client - An instance of Gitlab::GithubImport::Client. # project - An instance of Project. def import(client, project) diff --git a/app/workers/repository_import_worker.rb b/app/workers/repository_import_worker.rb index 5be439ecbc5..85771fa8b31 100644 --- a/app/workers/repository_import_worker.rb +++ b/app/workers/repository_import_worker.rb @@ -6,6 +6,10 @@ class RepositoryImportWorker include ProjectStartImport include ProjectImportOptions + # technical debt: https://gitlab.com/gitlab-org/gitlab/issues/33991 + sidekiq_options memory_killer_memory_growth_kb: ENV.fetch('MEMORY_KILLER_REPOSITORY_IMPORT_WORKER_MEMORY_GROWTH_KB', 50).to_i + sidekiq_options memory_killer_max_memory_growth_kb: ENV.fetch('MEMORY_KILLER_REPOSITORY_IMPORT_WORKER_MAX_MEMORY_GROWTH_KB', 300_000).to_i + def perform(project_id) @project = Project.find(project_id) diff --git a/changelogs/unreleased/bump-elasticsearch-indexer-to-v1-4-0.yml b/changelogs/unreleased/bump-elasticsearch-indexer-to-v1-4-0.yml new file mode 100644 index 00000000000..561514a2dc4 --- /dev/null +++ b/changelogs/unreleased/bump-elasticsearch-indexer-to-v1-4-0.yml @@ -0,0 +1,5 @@ +--- +title: Bump GITLAB_ELASTICSEARCH_INDEXER_VERSION=v1.4.0 +merge_request: 18558 +author: +type: fixed diff --git a/config/helpers/is_ee_env.js b/config/helpers/is_ee_env.js index 3fe9bb891eb..801cf6abc81 100644 --- a/config/helpers/is_ee_env.js +++ b/config/helpers/is_ee_env.js @@ -3,7 +3,12 @@ const path = require('path'); const ROOT_PATH = path.resolve(__dirname, '../..'); +// The `IS_GITLAB_EE` is always `string` or `nil` +// Thus the nil or empty string will result +// in using default value: true +// +// The behavior needs to be synchronised with +// lib/gitlab.rb: Gitlab.ee? module.exports = - process.env.IS_GITLAB_EE !== undefined - ? JSON.parse(process.env.IS_GITLAB_EE) - : fs.existsSync(path.join(ROOT_PATH, 'ee')); + fs.existsSync(path.join(ROOT_PATH, 'ee', 'app', 'models', 'license.rb')) && + (!process.env.IS_GITLAB_EE || JSON.parse(process.env.IS_GITLAB_EE)); diff --git a/doc/ci/pipelines.md b/doc/ci/pipelines.md index da67df3e774..0d0a40aceaa 100644 --- a/doc/ci/pipelines.md +++ b/doc/ci/pipelines.md @@ -436,15 +436,3 @@ To illustrate its life cycle: even if the commit history of the `example` branch has been overwritten by force-push. 1. GitLab Runner fetches the persistent pipeline ref and gets source code from the checkout-SHA. 1. When the pipeline finished, its persistent ref is cleaned up in a background process. - -NOTE: **NOTE**: At this moment, this feature is off dy default and can be manually enabled -by enabling `depend_on_persistent_pipeline_ref` feature flag, however, we'd remove this -feature flag and make it enabled by deafult by the day we release 12.4 _if we don't find any issues_. -If you'd be interested in manually turning on this behavior, please ask the administrator -to execute the following commands in rails console. - -```shell -> sudo gitlab-rails console # Login to Rails console of GitLab instance. -> project = Project.find_by_full_path('namespace/project-name') # Get the project instance. -> Feature.enable(:depend_on_persistent_pipeline_ref, project) # Enable the feature flag. -``` diff --git a/doc/user/admin_area/settings/continuous_integration.md b/doc/user/admin_area/settings/continuous_integration.md index ca2a0127ede..9b07ca13eee 100644 --- a/doc/user/admin_area/settings/continuous_integration.md +++ b/doc/user/admin_area/settings/continuous_integration.md @@ -29,26 +29,30 @@ If you want to disable it for a specific project, you can do so in ## Maximum artifacts size **(CORE ONLY)** The maximum size of the [job artifacts](../../../administration/job_artifacts.md) -can be set at the project level, group level, and at the instance level. The value is in *MB* and -the default is 100MB per job; on GitLab.com it's [set to 1G](../../gitlab_com/index.md#gitlab-cicd). +can be set at the project level, group level, and at the instance level. The value is: -To change it at the instance level: +- In *MB* and the default is 100MB per job. +- [Set to 1G](../../gitlab_com/index.md#gitlab-cicd) on GitLab.com. -1. Go to **Admin area > Settings > Continuous Integration and Deployment**. -1. Change the value of maximum artifacts size (in MB). -1. Hit **Save changes** for the changes to take effect. +To change it at the: -at the group level (this will override the instance setting): +- Instance level: -1. Go to **Group > Settings > CI / CD > General Pipelines**. -1. Change the value of maximum artifacts size (in MB). -1. Hit **Save changes** for the changes to take effect. + 1. Go to **Admin area > Settings > Continuous Integration and Deployment**. + 1. Change the value of maximum artifacts size (in MB). + 1. Hit **Save changes** for the changes to take effect. -at the project level (this will override the instance and group settings): +- [Group level](../../group/index.md#group-settings) (this will override the instance setting): -1. Go to **Project > Settings > CI / CD > General Pipelines**. -1. Change the value of maximum artifacts size (in MB). -1. Hit **Save changes** for the changes to take effect. + 1. Go to the group's **Settings > CI / CD > General Pipelines**. + 1. Change the value of **maximum artifacts size (in MB)**. + 1. Press **Save changes** for the changes to take effect. + +- [Project level](../../project/pipelines/settings.md) (this will override the instance and group settings): + + 1. Go to the project's **Settings > CI / CD > General Pipelines**. + 1. Change the value of **maximum artifacts size (in MB)**. + 1. Press **Save changes** for the changes to take effect. ## Default artifacts expiration **(CORE ONLY)** diff --git a/doc/user/group/index.md b/doc/user/group/index.md index 258f1264b48..93ad32b3e45 100644 --- a/doc/user/group/index.md +++ b/doc/user/group/index.md @@ -451,6 +451,11 @@ For performance reasons, we may delay the update up to 1 hour and 30 minutes. If your namespace shows `N/A` as the total storage usage, you can trigger a recalculation by pushing a commit to any project in that namespace. +### Maximum artifacts size **(CORE ONLY)** + +For information about setting a maximum artifact size for a group, see +[Maximum artifacts size](../admin_area/settings/continuous_integration.md#maximum-artifacts-size-core-only). + ## User contribution analysis **(STARTER)** With [GitLab Contribution Analytics](contribution_analytics/index.md), diff --git a/doc/user/project/img/issue_boards_multi_select.png b/doc/user/project/img/issue_boards_multi_select.png Binary files differnew file mode 100644 index 00000000000..34ec0c1c58e --- /dev/null +++ b/doc/user/project/img/issue_boards_multi_select.png diff --git a/doc/user/project/issue_board.md b/doc/user/project/issue_board.md index 0c0068ddd09..e9a7a15b630 100644 --- a/doc/user/project/issue_board.md +++ b/doc/user/project/issue_board.md @@ -180,6 +180,18 @@ These are shortcuts to your last 4 visited boards. When you're revisiting an issue board in a project or group with multiple boards, GitLab will automatically load the last board you visited. +### Multi-select Issue Cards + +As the name suggest, multi-select issue cards allows more than one issue card +to be dragged and dropped across different lists. This becomes helpful while +moving and grooming a lot of issues at once. + +You can multi-select an issue card by pressing `CTRL` + `Left mouse click` on +Windows or `CMD` + `Left mouse click` on MacOS. Once done, start by dragging one +of the issue card you have selected and drop it in the new list you want. + +![Multi-select Issue Cards](img/issue_boards_multi_select.png) + ### Configurable Issue Boards **(STARTER)** > Introduced in [GitLab Starter Edition 10.2](https://about.gitlab.com/2017/11/22/gitlab-10-2-released/#issue-boards-configuration). diff --git a/doc/user/project/pipelines/settings.md b/doc/user/project/pipelines/settings.md index 7b708a03b50..59e04907e21 100644 --- a/doc/user/project/pipelines/settings.md +++ b/doc/user/project/pipelines/settings.md @@ -60,6 +60,11 @@ if the job surpasses the threshold, it is marked as failed. Project defined timeout (either specific timeout set by user or the default 60 minutes timeout) may be [overridden on Runner level](../../../ci/runners/README.html#setting-maximum-job-timeout-for-a-runner). +## Maximum artifacts size **(CORE ONLY)** + +For information about setting a maximum artifact size for a project, see +[Maximum artifacts size](../../admin_area/settings/continuous_integration.md#maximum-artifacts-size-core-only). + ## Custom CI config path > [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/merge_requests/12509) in GitLab 9.4. diff --git a/lib/gitlab.rb b/lib/gitlab.rb index b337f5cbf2c..0cc9a6a5fb1 100644 --- a/lib/gitlab.rb +++ b/lib/gitlab.rb @@ -65,14 +65,18 @@ module Gitlab def self.ee? @is_ee ||= - if ENV['IS_GITLAB_EE'] && !ENV['IS_GITLAB_EE'].empty? - Gitlab::Utils.to_boolean(ENV['IS_GITLAB_EE']) - else - # We may use this method when the Rails environment is not loaded. This - # means that checking the presence of the License class could result in - # this method returning `false`, even for an EE installation. - root.join('ee/app/models/license.rb').exist? - end + # We use this method when the Rails environment is not loaded. This + # means that checking the presence of the License class could result in + # this method returning `false`, even for an EE installation. + # + # The `IS_GITLAB_EE` is always `string` or `nil` + # Thus the nil or empty string will result + # in using default value: true + # + # The behavior needs to be synchronised with + # config/helpers/is_ee_env.js + root.join('ee/app/models/license.rb').exist? && + (ENV['IS_GITLAB_EE'].to_s.empty? || Gitlab::Utils.to_boolean(ENV['IS_GITLAB_EE'])) end def self.ee diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 4a58534ba37..b5ffe1bc28a 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -14911,6 +14911,9 @@ msgid_plural "Showing %d events" msgstr[0] "" msgstr[1] "" +msgid "Showing %{pageSize} of %{total} issues" +msgstr "" + msgid "Showing Latest Version" msgstr "" @@ -15166,6 +15169,12 @@ msgstr "" msgid "Something went wrong while merging this merge request. Please try again." msgstr "" +msgid "Something went wrong while moving issues." +msgstr "" + +msgid "Something went wrong while performing the action." +msgstr "" + msgid "Something went wrong while reopening the %{issuable}. Please try again later" msgstr "" diff --git a/spec/db/production/settings_spec.rb b/spec/db/production/settings_spec.rb index db19e98b851..02e25aa37e3 100644 --- a/spec/db/production/settings_spec.rb +++ b/spec/db/production/settings_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' require 'rainbow/ext/string' diff --git a/spec/features/boards/multi_select_spec.rb b/spec/features/boards/multi_select_spec.rb new file mode 100644 index 00000000000..885dc08e38d --- /dev/null +++ b/spec/features/boards/multi_select_spec.rb @@ -0,0 +1,110 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe 'Multi Select Issue', :js do + include DragTo + + let(:group) { create(:group, :nested) } + let(:project) { create(:project, :public, namespace: group) } + let(:board) { create(:board, project: project) } + let(:user) { create(:user) } + + def drag(selector: '.board-list', list_from_index: 1, from_index: 0, to_index: 0, list_to_index: 1, duration: 1000) + drag_to(selector: selector, + scrollable: '#board-app', + list_from_index: list_from_index, + from_index: from_index, + to_index: to_index, + list_to_index: list_to_index, + duration: duration) + end + + def wait_for_board_cards(board_number, expected_cards) + page.within(find(".board:nth-child(#{board_number})")) do + expect(page.find('.board-header')).to have_content(expected_cards.to_s) + expect(page).to have_selector('.board-card', count: expected_cards) + end + end + + def multi_select(selector, action = 'select') + element = page.find(selector) + script = "var el = document.querySelector('#{selector}');" + script += "var mousedown = new MouseEvent('mousedown', { button: 0, bubbles: true });" + script += "var mouseup = new MouseEvent('mouseup', { ctrlKey: true, button: 0, bubbles:true });" + script += "el.dispatchEvent(mousedown); el.dispatchEvent(mouseup);" + script += "Sortable.utils.#{action}(el);" + + page.execute_script(script, element) + end + + before do + project.add_maintainer(user) + + sign_in(user) + end + + context 'with lists' do + let(:label1) { create(:label, project: project, name: 'Label 1', description: 'Test') } + let(:label2) { create(:label, project: project, name: 'Label 2', description: 'Test') } + let!(:list1) { create(:list, board: board, label: label1, position: 0) } + let!(:list2) { create(:list, board: board, label: label2, position: 1) } + let!(:issue1) { create(:labeled_issue, project: project, title: 'Issue 1', description: '', assignees: [user], labels: [label1], relative_position: 1) } + let!(:issue2) { create(:labeled_issue, project: project, title: 'Issue 2', description: '', author: user, labels: [label1], relative_position: 2) } + let!(:issue3) { create(:labeled_issue, project: project, title: 'Issue 3', description: '', labels: [label1], relative_position: 3) } + let!(:issue4) { create(:labeled_issue, project: project, title: 'Issue 4', description: '', labels: [label1], relative_position: 4) } + let!(:issue5) { create(:labeled_issue, project: project, title: 'Issue 5', description: '', labels: [label1], relative_position: 5) } + + before do + visit project_board_path(project, board) + + wait_for_requests + end + + it 'moves multiple issues to another list', :js do + multi_select('.board-card:nth-child(1)') + multi_select('.board-card:nth-child(2)') + drag(list_from_index: 1, list_to_index: 2) + + wait_for_requests + + page.within(all('.js-board-list')[2]) do + expect(find('.board-card:nth-child(1)')).to have_content(issue1.title) + expect(find('.board-card:nth-child(2)')).to have_content(issue2.title) + end + end + + it 'maintains order when moved', :js do + multi_select('.board-card:nth-child(3)') + multi_select('.board-card:nth-child(2)') + multi_select('.board-card:nth-child(1)') + + drag(list_from_index: 1, list_to_index: 2) + + wait_for_requests + + page.within(all('.js-board-list')[2]) do + expect(find('.board-card:nth-child(1)')).to have_content(issue1.title) + expect(find('.board-card:nth-child(2)')).to have_content(issue2.title) + expect(find('.board-card:nth-child(3)')).to have_content(issue3.title) + end + end + + it 'move issues in the same list', :js do + multi_select('.board-card:nth-child(3)') + multi_select('.board-card:nth-child(4)') + + drag(list_from_index: 1, list_to_index: 1, from_index: 2, to_index: 4) + + wait_for_requests + + page.within(all('.js-board-list')[1]) do + expect(find('.board-card:nth-child(1)')).to have_content(issue1.title) + expect(find('.board-card:nth-child(2)')).to have_content(issue2.title) + expect(find('.board-card:nth-child(3)')).to have_content(issue5.title) + expect(find('.board-card:nth-child(4)')).to have_content(issue3.title) + expect(find('.board-card:nth-child(5)')).to have_content(issue4.title) + end + end + end +end diff --git a/spec/javascripts/boards/board_card_spec.js b/spec/javascripts/boards/board_card_spec.js index 13b708a03d5..9f441ca319e 100644 --- a/spec/javascripts/boards/board_card_spec.js +++ b/spec/javascripts/boards/board_card_spec.js @@ -67,6 +67,16 @@ describe('Board card', () => { expect(vm.issueDetailVisible).toBe(true); }); + it("returns false when multiSelect doesn't contain issue", () => { + expect(vm.multiSelectVisible).toBe(false); + }); + + it('returns true when multiSelect contains issue', () => { + boardsStore.multiSelect.list = [vm.issue]; + + expect(vm.multiSelectVisible).toBe(true); + }); + it('adds user-can-drag class if not disabled', () => { expect(vm.$el.classList.contains('user-can-drag')).toBe(true); }); @@ -180,7 +190,7 @@ describe('Board card', () => { triggerEvent('mousedown'); triggerEvent('mouseup'); - expect(eventHub.$emit).toHaveBeenCalledWith('newDetailIssue', vm.issue); + expect(eventHub.$emit).toHaveBeenCalledWith('newDetailIssue', vm.issue, undefined); expect(boardsStore.detail.list).toEqual(vm.list); }); @@ -203,7 +213,7 @@ describe('Board card', () => { triggerEvent('mousedown'); triggerEvent('mouseup'); - expect(eventHub.$emit).toHaveBeenCalledWith('clearDetailIssue'); + expect(eventHub.$emit).toHaveBeenCalledWith('clearDetailIssue', undefined); }); }); }); diff --git a/spec/javascripts/boards/boards_store_spec.js b/spec/javascripts/boards/boards_store_spec.js index 11352140ba4..678fe5befa8 100644 --- a/spec/javascripts/boards/boards_store_spec.js +++ b/spec/javascripts/boards/boards_store_spec.js @@ -12,6 +12,7 @@ import '~/boards/services/board_service'; import boardsStore from '~/boards/stores/boards_store'; import eventHub from '~/boards/eventhub'; import { listObj, listObjDuplicate, boardsMockInterceptor, mockBoardService } from './mock_data'; +import waitForPromises from '../../frontend/helpers/wait_for_promises'; describe('Store', () => { let mock; @@ -29,6 +30,13 @@ describe('Store', () => { }), ); + spyOn(gl.boardService, 'moveMultipleIssues').and.callFake( + () => + new Promise(resolve => { + resolve(); + }), + ); + Cookies.set('issue_board_welcome_hidden', 'false', { expires: 365 * 10, path: '', @@ -376,4 +384,128 @@ describe('Store', () => { expect(state.currentBoard).toEqual(dummyBoard); }); }); + + describe('toggleMultiSelect', () => { + let basicIssueObj; + + beforeAll(() => { + basicIssueObj = { id: 987654 }; + }); + + afterEach(() => { + boardsStore.clearMultiSelect(); + }); + + it('adds issue when not present', () => { + boardsStore.toggleMultiSelect(basicIssueObj); + + const selectedIds = boardsStore.multiSelect.list.map(x => x.id); + + expect(selectedIds.includes(basicIssueObj.id)).toEqual(true); + }); + + it('removes issue when issue is present', () => { + boardsStore.toggleMultiSelect(basicIssueObj); + let selectedIds = boardsStore.multiSelect.list.map(x => x.id); + + expect(selectedIds.includes(basicIssueObj.id)).toEqual(true); + + boardsStore.toggleMultiSelect(basicIssueObj); + selectedIds = boardsStore.multiSelect.list.map(x => x.id); + + expect(selectedIds.includes(basicIssueObj.id)).toEqual(false); + }); + }); + + describe('clearMultiSelect', () => { + it('clears all the multi selected issues', () => { + const issue1 = { id: 12345 }; + const issue2 = { id: 12346 }; + + boardsStore.toggleMultiSelect(issue1); + boardsStore.toggleMultiSelect(issue2); + + expect(boardsStore.multiSelect.list.length).toEqual(2); + + boardsStore.clearMultiSelect(); + + expect(boardsStore.multiSelect.list.length).toEqual(0); + }); + }); + + describe('moveMultipleIssuesToList', () => { + it('move issues on the new index', done => { + const listOne = boardsStore.addList(listObj); + const listTwo = boardsStore.addList(listObjDuplicate); + + expect(boardsStore.state.lists.length).toBe(2); + + setTimeout(() => { + expect(listOne.issues.length).toBe(1); + expect(listTwo.issues.length).toBe(1); + + boardsStore.moveMultipleIssuesToList({ + listFrom: listOne, + listTo: listTwo, + issues: listOne.issues, + newIndex: 0, + }); + + expect(listTwo.issues.length).toBe(1); + + done(); + }, 0); + }); + }); + + describe('moveMultipleIssuesInList', () => { + it('moves multiple issues in list', done => { + const issueObj = { + title: 'Issue #1', + id: 12345, + iid: 2, + confidential: false, + labels: [], + assignees: [], + }; + const issue1 = new ListIssue(issueObj); + const issue2 = new ListIssue({ + ...issueObj, + title: 'Issue #2', + id: 12346, + }); + + const list = boardsStore.addList(listObj); + + waitForPromises() + .then(() => { + list.addIssue(issue1); + list.addIssue(issue2); + + expect(list.issues.length).toBe(3); + expect(list.issues[0].id).not.toBe(issue2.id); + + boardsStore.moveMultipleIssuesInList({ + list, + issues: [issue1, issue2], + oldIndicies: [0], + newIndex: 1, + idArray: [1, 12345, 12346], + }); + + expect(list.issues[0].id).toBe(issue1.id); + + expect(gl.boardService.moveMultipleIssues).toHaveBeenCalledWith({ + ids: [issue1.id, issue2.id], + fromListId: null, + toListId: null, + moveBeforeId: 1, + moveAfterId: null, + }); + + done(); + }) + .catch(done.fail); + }); + }); }); diff --git a/spec/models/ci/persistent_ref_spec.rb b/spec/models/ci/persistent_ref_spec.rb index 71e0b03dff9..be447476e2c 100644 --- a/spec/models/ci/persistent_ref_spec.rb +++ b/spec/models/ci/persistent_ref_spec.rb @@ -45,18 +45,6 @@ describe Ci::PersistentRef do expect(pipeline.persistent_ref).to be_exist end - context 'when depend_on_persistent_pipeline_ref feature flag is disabled' do - before do - stub_feature_flags(depend_on_persistent_pipeline_ref: false) - end - - it 'does not create a persistent ref' do - expect(project.repository).not_to receive(:create_ref) - - subject - end - end - context 'when sha does not exist in the repository' do let(:sha) { 'not-exist' } diff --git a/spec/tasks/cache/clear/redis_spec.rb b/spec/tasks/cache/clear/redis_spec.rb index 97c8c943f3a..4f597988763 100644 --- a/spec/tasks/cache/clear/redis_spec.rb +++ b/spec/tasks/cache/clear/redis_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'rake_helper' describe 'clearing redis cache' do diff --git a/spec/tasks/config_lint_spec.rb b/spec/tasks/config_lint_spec.rb index 83d54259dfa..c6c11d76388 100644 --- a/spec/tasks/config_lint_spec.rb +++ b/spec/tasks/config_lint_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'rake_helper' Rake.application.rake_require 'tasks/config_lint' diff --git a/spec/tasks/gitlab/artifacts/check_rake_spec.rb b/spec/tasks/gitlab/artifacts/check_rake_spec.rb index d495b08aca0..04015f0e21a 100644 --- a/spec/tasks/gitlab/artifacts/check_rake_spec.rb +++ b/spec/tasks/gitlab/artifacts/check_rake_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'rake_helper' describe 'gitlab:artifacts rake tasks' do diff --git a/spec/tasks/gitlab/artifacts/migrate_rake_spec.rb b/spec/tasks/gitlab/artifacts/migrate_rake_spec.rb index afa9ff50146..55bfb7acd9d 100644 --- a/spec/tasks/gitlab/artifacts/migrate_rake_spec.rb +++ b/spec/tasks/gitlab/artifacts/migrate_rake_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'rake_helper' describe 'gitlab:artifacts namespace rake task' do diff --git a/spec/tasks/gitlab/backup_rake_spec.rb b/spec/tasks/gitlab/backup_rake_spec.rb index bdbd39475b9..b307453f078 100644 --- a/spec/tasks/gitlab/backup_rake_spec.rb +++ b/spec/tasks/gitlab/backup_rake_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' require 'rake' diff --git a/spec/tasks/gitlab/check_rake_spec.rb b/spec/tasks/gitlab/check_rake_spec.rb index 0fcb9b269f3..1469143d2ac 100644 --- a/spec/tasks/gitlab/check_rake_spec.rb +++ b/spec/tasks/gitlab/check_rake_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'rake_helper' describe 'check.rake' do diff --git a/spec/tasks/gitlab/cleanup_rake_spec.rb b/spec/tasks/gitlab/cleanup_rake_spec.rb index 6c09bb5d9f9..3c3e5eea838 100644 --- a/spec/tasks/gitlab/cleanup_rake_spec.rb +++ b/spec/tasks/gitlab/cleanup_rake_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'rake_helper' describe 'gitlab:cleanup rake tasks' do diff --git a/spec/tasks/gitlab/db_rake_spec.rb b/spec/tasks/gitlab/db_rake_spec.rb index 5818892d56a..49b9a572423 100644 --- a/spec/tasks/gitlab/db_rake_spec.rb +++ b/spec/tasks/gitlab/db_rake_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' require 'rake' diff --git a/spec/tasks/gitlab/git_rake_spec.rb b/spec/tasks/gitlab/git_rake_spec.rb index 57b006e1a39..b8156e55ec7 100644 --- a/spec/tasks/gitlab/git_rake_spec.rb +++ b/spec/tasks/gitlab/git_rake_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'rake_helper' describe 'gitlab:git rake tasks' do diff --git a/spec/tasks/gitlab/gitaly_rake_spec.rb b/spec/tasks/gitlab/gitaly_rake_spec.rb index 2f3fc7839c1..0cc92680582 100644 --- a/spec/tasks/gitlab/gitaly_rake_spec.rb +++ b/spec/tasks/gitlab/gitaly_rake_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'rake_helper' describe 'gitlab:gitaly namespace rake task' do diff --git a/spec/tasks/gitlab/info_rake_spec.rb b/spec/tasks/gitlab/info_rake_spec.rb index ca74378a12a..8d6b3985380 100644 --- a/spec/tasks/gitlab/info_rake_spec.rb +++ b/spec/tasks/gitlab/info_rake_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'rake_helper' describe 'gitlab:env:info' do diff --git a/spec/tasks/gitlab/ldap_rake_spec.rb b/spec/tasks/gitlab/ldap_rake_spec.rb index 279234f2887..bbc3f625088 100644 --- a/spec/tasks/gitlab/ldap_rake_spec.rb +++ b/spec/tasks/gitlab/ldap_rake_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'rake_helper' describe 'gitlab:ldap:rename_provider rake task' do diff --git a/spec/tasks/gitlab/lfs/check_rake_spec.rb b/spec/tasks/gitlab/lfs/check_rake_spec.rb index 2610edf8bac..3d698efdcb6 100644 --- a/spec/tasks/gitlab/lfs/check_rake_spec.rb +++ b/spec/tasks/gitlab/lfs/check_rake_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'rake_helper' describe 'gitlab:lfs rake tasks' do diff --git a/spec/tasks/gitlab/lfs/migrate_rake_spec.rb b/spec/tasks/gitlab/lfs/migrate_rake_spec.rb index a85a0031a6c..fc7be0eebcd 100644 --- a/spec/tasks/gitlab/lfs/migrate_rake_spec.rb +++ b/spec/tasks/gitlab/lfs/migrate_rake_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'rake_helper' describe 'gitlab:lfs namespace rake task' do diff --git a/spec/tasks/gitlab/shell_rake_spec.rb b/spec/tasks/gitlab/shell_rake_spec.rb index c3e912b02c5..e3b7967bd19 100644 --- a/spec/tasks/gitlab/shell_rake_spec.rb +++ b/spec/tasks/gitlab/shell_rake_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'rake_helper' describe 'gitlab:shell rake tasks' do diff --git a/spec/tasks/gitlab/storage_rake_spec.rb b/spec/tasks/gitlab/storage_rake_spec.rb index 0e47408fc72..ae11e091000 100644 --- a/spec/tasks/gitlab/storage_rake_spec.rb +++ b/spec/tasks/gitlab/storage_rake_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'rake_helper' describe 'rake gitlab:storage:*', :sidekiq do diff --git a/spec/tasks/gitlab/task_helpers_spec.rb b/spec/tasks/gitlab/task_helpers_spec.rb index e9322ec4931..4b4f7d7c956 100644 --- a/spec/tasks/gitlab/task_helpers_spec.rb +++ b/spec/tasks/gitlab/task_helpers_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' class TestHelpersTest diff --git a/spec/tasks/gitlab/uploads/check_rake_spec.rb b/spec/tasks/gitlab/uploads/check_rake_spec.rb index 5d597c66133..91f0cedb246 100644 --- a/spec/tasks/gitlab/uploads/check_rake_spec.rb +++ b/spec/tasks/gitlab/uploads/check_rake_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'rake_helper' describe 'gitlab:uploads rake tasks' do diff --git a/spec/tasks/gitlab/uploads/migrate_rake_spec.rb b/spec/tasks/gitlab/uploads/migrate_rake_spec.rb index 8d1e355a7d3..2f773bdfeec 100644 --- a/spec/tasks/gitlab/uploads/migrate_rake_spec.rb +++ b/spec/tasks/gitlab/uploads/migrate_rake_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'rake_helper' describe 'gitlab:uploads:migrate and migrate_to_local rake tasks' do diff --git a/spec/tasks/gitlab/web_hook_rake_spec.rb b/spec/tasks/gitlab/web_hook_rake_spec.rb index 7bdf33ff6b0..be31507000d 100644 --- a/spec/tasks/gitlab/web_hook_rake_spec.rb +++ b/spec/tasks/gitlab/web_hook_rake_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'rake_helper' describe 'gitlab:web_hook namespace rake tasks' do diff --git a/spec/tasks/gitlab/workhorse_rake_spec.rb b/spec/tasks/gitlab/workhorse_rake_spec.rb index 42516d36c67..b7877a84185 100644 --- a/spec/tasks/gitlab/workhorse_rake_spec.rb +++ b/spec/tasks/gitlab/workhorse_rake_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'rake_helper' describe 'gitlab:workhorse namespace rake task' do diff --git a/spec/tasks/tokens_spec.rb b/spec/tasks/tokens_spec.rb index 4188e7caccb..9c69155056a 100644 --- a/spec/tasks/tokens_spec.rb +++ b/spec/tasks/tokens_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'rake_helper' describe 'tokens rake tasks' do |