summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--app/assets/javascripts/boards/components/board_card.vue20
-rw-r--r--app/assets/javascripts/boards/components/board_list.vue218
-rw-r--r--app/assets/javascripts/boards/constants.js11
-rw-r--r--app/assets/javascripts/boards/index.js18
-rw-r--r--app/assets/javascripts/boards/models/list.js92
-rw-r--r--app/assets/javascripts/boards/services/board_service.js10
-rw-r--r--app/assets/javascripts/boards/stores/boards_store.js148
-rw-r--r--app/assets/javascripts/test_utils/index.js2
-rw-r--r--app/assets/stylesheets/pages/boards.scss6
-rw-r--r--app/controllers/groups/boards_controller.rb3
-rw-r--r--app/controllers/projects/boards_controller.rb3
-rw-r--r--app/models/project.rb10
-rw-r--r--app/models/project_services/jira_service.rb2
-rw-r--r--changelogs/unreleased/19152-improve-jira-username-validation.yml5
-rw-r--r--doc/development/contributing/issue_workflow.md9
-rw-r--r--doc/gitlab-basics/create-project.md2
-rw-r--r--doc/gitlab-basics/start-using-git.md4
-rw-r--r--doc/topics/git/how_to_install_git/index.md2
-rw-r--r--doc/university/README.md28
-rw-r--r--doc/university/bookclub/index.md4
-rw-r--r--doc/university/process/README.md4
-rw-r--r--doc/university/support/README.md6
-rw-r--r--doc/university/training/end-user/README.md4
-rw-r--r--doc/university/training/topics/env_setup.md2
-rw-r--r--doc/university/training/topics/merge_conflicts.md2
-rw-r--r--doc/university/training/user_training.md4
-rw-r--r--doc/user/application_security/container_scanning/index.md19
-rw-r--r--doc/user/project/img/issue_boards_multi_select.pngbin0 -> 21091 bytes
-rw-r--r--doc/user/project/issue_board.md12
-rw-r--r--doc/user/project/pages/custom_domains_ssl_tls_certification/dns_concepts.md8
-rw-r--r--doc/user/project/pages/custom_domains_ssl_tls_certification/index.md2
-rw-r--r--doc/user/project/pages/custom_domains_ssl_tls_certification/ssl_tls_concepts.md2
-rw-r--r--doc/user/project/pages/getting_started_part_four.md4
-rw-r--r--lib/gitlab/import_export/project_tree_restorer.rb64
-rw-r--r--locale/gitlab.pot9
-rw-r--r--spec/features/boards/multi_select_spec.rb129
-rw-r--r--spec/javascripts/boards/board_card_spec.js14
-rw-r--r--spec/javascripts/boards/boards_store_spec.js132
-rw-r--r--spec/models/project_services/jira_service_spec.rb35
-rw-r--r--spec/models/project_spec.rb55
40 files changed, 975 insertions, 129 deletions
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/project.rb b/app/models/project.rb
index 80628cc07df..26ed9b0cd1e 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -2258,6 +2258,16 @@ class Project < ApplicationRecord
setting
end
+ def drop_visibility_level!
+ if group && group.visibility_level < visibility_level
+ self.visibility_level = group.visibility_level
+ end
+
+ if Gitlab::CurrentSettings.restricted_visibility_levels.include?(visibility_level)
+ self.visibility_level = Gitlab::VisibilityLevel::PRIVATE
+ end
+ end
+
private
def closest_namespace_setting(name)
diff --git a/app/models/project_services/jira_service.rb b/app/models/project_services/jira_service.rb
index 6ebec17e703..2bc6567ca15 100644
--- a/app/models/project_services/jira_service.rb
+++ b/app/models/project_services/jira_service.rb
@@ -64,7 +64,7 @@ class JiraService < IssueTrackerService
url = URI.parse(client_url)
{
- username: username,
+ username: username&.strip,
password: password,
site: URI.join(url, '/').to_s, # Intended to find the root
context_path: url.path,
diff --git a/changelogs/unreleased/19152-improve-jira-username-validation.yml b/changelogs/unreleased/19152-improve-jira-username-validation.yml
new file mode 100644
index 00000000000..8f007497d32
--- /dev/null
+++ b/changelogs/unreleased/19152-improve-jira-username-validation.yml
@@ -0,0 +1,5 @@
+---
+title: 'JIRA Service: Improve username/email validation'
+merge_request: 18397
+author:
+type: fixed
diff --git a/doc/development/contributing/issue_workflow.md b/doc/development/contributing/issue_workflow.md
index a93bd50534f..349bb371835 100644
--- a/doc/development/contributing/issue_workflow.md
+++ b/doc/development/contributing/issue_workflow.md
@@ -26,10 +26,11 @@ on those issues. Please select someone with relevant experience from the
If there is nobody mentioned with that expertise look in the commit history for
the affected files to find someone.
-We also use [GitLab Triage](https://gitlab.com/gitlab-org/gitlab-triage) to
-automate some triaging policies. This is currently set up as a
-[scheduled pipeline](https://gitlab.com/gitlab-org/quality/triage-ops/pipeline_schedules/10512/edit)
-running on [quality/triage-ops](https://gitlab.com/gitlab-org/quality/triage-ops) project.
+We also use [GitLab Triage](https://gitlab.com/gitlab-org/gitlab-triage) to automate
+some triaging policies. This is currently set up as a scheduled pipeline
+(`https://gitlab.com/gitlab-org/quality/triage-ops/pipeline_schedules/10512/editpipeline_schedules/10512/edit`,
+must have at least developer access to the project) running on [quality/triage-ops](https://gitlab.com/gitlab-org/quality/triage-ops)
+project.
## Labels
diff --git a/doc/gitlab-basics/create-project.md b/doc/gitlab-basics/create-project.md
index 2ebddc14a06..8edce515ec8 100644
--- a/doc/gitlab-basics/create-project.md
+++ b/doc/gitlab-basics/create-project.md
@@ -81,7 +81,7 @@ You can improve the existing built-in templates or contribute new ones in the
#### Custom project templates **(PREMIUM)**
> [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/6860) in
-[GitLab Premium](https://about.gitlab.com/pricing) 11.2.
+[GitLab Premium](https://about.gitlab.com/pricing/) 11.2.
Creating new projects based on custom project templates is a convenient option for
quickly starting projects.
diff --git a/doc/gitlab-basics/start-using-git.md b/doc/gitlab-basics/start-using-git.md
index 8d4320f7d44..05329993c64 100644
--- a/doc/gitlab-basics/start-using-git.md
+++ b/doc/gitlab-basics/start-using-git.md
@@ -13,9 +13,9 @@ make sure that you have created and/or signed into an account on GitLab.
Depending on your operating system, you will need to use a shell of your preference.
Here are some suggestions:
-- [Terminal](http://blog.teamtreehouse.com/introduction-to-the-mac-os-x-command-line) on macOS
+- [Terminal](https://blog.teamtreehouse.com/introduction-to-the-mac-os-x-command-line) on macOS
- [GitBash](https://msysgit.github.io) on Windows
-- [Linux Terminal](http://www.howtogeek.com/140679/beginner-geek-how-to-start-using-the-linux-terminal/) on Linux
+- [Linux Terminal](https://www.howtogeek.com/140679/beginner-geek-how-to-start-using-the-linux-terminal/) on Linux
## Check if Git has already been installed
diff --git a/doc/topics/git/how_to_install_git/index.md b/doc/topics/git/how_to_install_git/index.md
index c7bede2d269..db474559627 100644
--- a/doc/topics/git/how_to_install_git/index.md
+++ b/doc/topics/git/how_to_install_git/index.md
@@ -38,7 +38,7 @@ through the macOS App Store.
### Installing Homebrew
-Once Xcode is installed browse to the [Homebrew website](http://brew.sh/index.html)
+Once Xcode is installed browse to the [Homebrew website](https://brew.sh/index.html)
for the official Homebrew installation instructions.
### Installing Git via Homebrew
diff --git a/doc/university/README.md b/doc/university/README.md
index e0bc54518db..8dbf6299e90 100644
--- a/doc/university/README.md
+++ b/doc/university/README.md
@@ -26,7 +26,7 @@ The GitLab University curriculum is composed of GitLab videos, screencasts, pres
### 1.1. Version Control and Git
1. [Version Control Systems](https://docs.google.com/presentation/d/16sX7hUrCZyOFbpvnrAFrg6tVO5_yT98IgdAqOmXwBho/edit#slide=id.g72f2e4906_2_29)
-1. [Code School: An Introduction to Git](https://www.codeschool.com/account/courses/try-git)
+1. [Katakoda: Learn Git Version Control using Interactive Browser-Based Scenarios](https://www.katacoda.com/courses/git)
### 1.2. GitLab Basics
@@ -34,12 +34,12 @@ The GitLab University curriculum is composed of GitLab videos, screencasts, pres
1. [Why Use Git and GitLab - Slides](https://docs.google.com/a/gitlab.com/presentation/d/1RcZhFmn5VPvoFu6UMxhMOy7lAsToeBZRjLRn0LIdaNc/edit?usp=drive_web)
1. [GitLab Basics - Article](../gitlab-basics/README.md)
1. [Git and GitLab Basics - Video](https://www.youtube.com/watch?v=03wb9FvO4Ak&index=5&list=PLFGfElNsQthbQu_IWlNOxul0TbS_2JH-e)
-1. [Git and GitLab Basics - Online Course](https://courses.platzi.com/classes/git-gitlab/concepto/part-1/part-23370/material/)
+1. [Git and GitLab Basics - Online Course](https://courses.platzi.com/classes/57-git-gitlab/2475-part-233-2/)
1. [Comparison of GitLab Versions](https://about.gitlab.com/features/#compare)
### 1.3. Your GitLab Account
-1. [Create a GitLab Account - Online Course](https://courses.platzi.com/classes/git-gitlab/concepto/first-steps/create-an-account-on-gitlab/material/)
+1. [Create a GitLab Account - Online Course](https://courses.platzi.com/classes/57-git-gitlab/2434-create-an-account-on-gitlab/)
1. [Create and Add your SSH key to GitLab - Video](https://www.youtube.com/watch?v=54mxyLo3Mqk)
### 1.4. GitLab Projects
@@ -59,7 +59,7 @@ The GitLab University curriculum is composed of GitLab videos, screencasts, pres
### 1.6. The GitLab team
-1. [About GitLab](https://about.gitlab.com/about/)
+1. [About GitLab](https://about.gitlab.com/company/)
1. [GitLab Direction](https://about.gitlab.com/direction/)
1. [GitLab Master Plan](https://about.gitlab.com/2016/09/13/gitlab-master-plan/)
1. [Making GitLab Great for Everyone - Video](https://www.youtube.com/watch?v=GGC40y4vMx0) - Response to "Dear GitHub" letter
@@ -70,7 +70,7 @@ The GitLab University curriculum is composed of GitLab videos, screencasts, pres
### 1.7 Community and Support
-1. [Getting Help](https://about.gitlab.com/getting-help/)
+1. [Getting Help](https://about.gitlab.com/get-help/)
- Proposing Features and Reporting and Tracking bugs for GitLab
- The GitLab IRC channel, Gitter Chat Room, Community Forum and Mailing List
- Getting Technical Support
@@ -107,7 +107,7 @@ The GitLab University curriculum is composed of GitLab videos, screencasts, pres
### 2.3. Continuous Integration
1. [Operating Systems, Servers, VMs, Containers and Unix - Video](https://www.youtube.com/watch?v=V61kL6IC-zY&index=8&list=PLFGfElNsQthbQu_IWlNOxul0TbS_2JH-e)
-1. [GitLab CI - Product Page](https://about.gitlab.com/gitlab-ci/)
+1. [GitLab CI - Product Page](https://about.gitlab.com/product/continuous-integration/)
1. [Getting started with GitLab and GitLab CI](https://about.gitlab.com/2015/12/14/getting-started-with-gitlab-and-gitlab-ci/)
1. [GitLab Container Registry](https://about.gitlab.com/2016/05/23/gitlab-container-registry/)
1. [GitLab and Docker - Video](https://www.youtube.com/watch?v=ugOrCcbdHko&index=12&list=PLFGfElNsQthbQu_IWlNOxul0TbS_2JH-e)
@@ -120,7 +120,7 @@ The GitLab University curriculum is composed of GitLab videos, screencasts, pres
1. [Setting up GitLab CI for iOS projects](https://about.gitlab.com/2016/03/10/setting-up-gitlab-ci-for-ios-projects/)
1. [IBM: Continuous Delivery vs Continuous Deployment - Video](https://www.youtube.com/watch?v=igwFj8PPSnw)
1. [Amazon: Transition to Continuous Delivery - Video](https://www.youtube.com/watch?v=esEFaY0FDKc)
-1. [TechBeacon: Doing continuous delivery? Focus first on reducing release cycle times](https://techbeacon.com/doing-continuous-delivery-focus-first-reducing-release-cycle-times)
+1. [TechBeacon: Doing continuous delivery? Focus first on reducing release cycle times](https://techbeacon.com/devops/doing-continuous-delivery-focus-first-reducing-release-cycle-times)
1. See **[Integrations](#39-integrations)** for integrations with other CI services.
### 2.4. Workflow
@@ -133,11 +133,11 @@ The GitLab University curriculum is composed of GitLab videos, screencasts, pres
### 2.5. GitLab Comparisons
-1. [GitLab Compared to Other Tools](https://about.gitlab.com/comparison/)
+1. [GitLab Compared to Other Tools](https://about.gitlab.com/devops-tools/)
1. [Comparing GitLab Terminology](https://about.gitlab.com/2016/01/27/comparing-terms-gitlab-github-bitbucket/)
1. [GitLab Compared to Atlassian (Recording 2016-03-03)](https://youtu.be/Nbzp1t45ERo)
-1. [GitLab Position FAQ](https://about.gitlab.com/handbook/positioning-faq)
-1. [Customer review of GitLab with points on why they prefer GitLab](https://www.enovate.co.uk/web-design-blog/2015/11/25/gitlab-review/)
+1. [GitLab Position FAQ](https://about.gitlab.com/handbook/positioning-faq/)
+1. [Customer review of GitLab with points on why they prefer GitLab](https://www.enovate.co.uk/blog/2015/11/25/gitlab-review)
## 3. GitLab Advanced
@@ -145,13 +145,13 @@ The GitLab University curriculum is composed of GitLab videos, screencasts, pres
1. [Xebia Labs: Dev Ops Terminology](https://xebialabs.com/glossary/)
1. [Xebia Labs: Periodic Table of DevOps Tools](https://xebialabs.com/periodic-table-of-devops-tools/)
-1. [Puppet Labs: State of Dev Ops 2016 - Book](https://puppet.com/resources/white-paper/2016-state-of-devops-report)
+1. [Puppet Labs: State of Dev Ops 2016 - Book](https://puppet.com/resources/whitepaper/2016-state-of-devops-report)
### 3.2. Installing GitLab with Omnibus
1. [What is Omnibus - Video](https://www.youtube.com/watch?v=XTmpKudd-Oo)
1. [How to Install GitLab with Omnibus - Video](https://www.youtube.com/watch?v=Q69YaOjqNhg)
-1. [Installing GitLab - Online Course](https://courses.platzi.com/classes/git-gitlab/concepto/part-1/part-3/material/)
+1. [Installing GitLab - Online Course](https://courses.platzi.com/classes/57-git-gitlab/2476-part-0/)
1. [Using a Non-Packaged PostgreSQL Database](https://gitlab.com/gitlab-org/omnibus-gitlab/blob/master/README.md#using-a-non-packaged-postgresql-database-management-server)
1. [Installing GitLab on Microsoft Azure](https://about.gitlab.com/2016/07/13/how-to-setup-a-gitlab-instance-on-microsoft-azure/)
1. [Installing GitLab on Digital Ocean](https://about.gitlab.com/2016/04/27/getting-started-with-gitlab-and-digitalocean/)
@@ -176,7 +176,7 @@ The GitLab University curriculum is composed of GitLab videos, screencasts, pres
1. [Scalability and High Availability - Video](https://www.youtube.com/watch?v=cXRMJJb6sp4&list=PLFGfElNsQthbQu_IWlNOxul0TbS_2JH-e&index=2)
1. [High Availability - Video](https://www.youtube.com/watch?v=36KS808u6bE&index=15&list=PLFGfElNsQthbQu_IWlNOxul0TbS_2JH-e)
-1. [High Availability Documentation](https://about.gitlab.com/high-availability/)
+1. [High Availability Documentation](https://about.gitlab.com/solutions/high-availability/)
### 3.8 Cycle Analytics
@@ -205,7 +205,7 @@ NOTE: **Note:**
Some content can only be accessed by GitLab team members.
1. [Support Path](support/README.md)
-1. [Sales Path](https://about.gitlab.com/handbook/sales-onboarding/)
+1. [Sales Path](https://about.gitlab.com/handbook/sales/onboarding/)
1. [User Training](training/user_training.md)
1. [GitLab Flow Training](training/gitlab_flow.md)
1. [Training Topics](training/index.md)
diff --git a/doc/university/bookclub/index.md b/doc/university/bookclub/index.md
index 330078e979f..c6dad663c0e 100644
--- a/doc/university/bookclub/index.md
+++ b/doc/university/bookclub/index.md
@@ -15,10 +15,10 @@ See the [book list](booklist.md) for additional recommendations.
1. **Remote: Office not required**
David Heinemeier Hansson and Jason Fried, 2013
- ([amazon](http://www.amazon.co.uk/Remote-Required-David-Heinemeier-Hansson/dp/0091954673))
+ ([amazon](https://www.amazon.co.uk/Remote-Required-David-Heinemeier-Hansson/dp/0091954673))
1. **The Year Without Pants**
- Scott Berkun, 2013 ([ScottBerkun.com](http://scottberkun.com/yearwithoutpants/))
+ Scott Berkun, 2013 ([ScottBerkun.com](https://scottberkun.com/yearwithoutpants/))
Any other books you'd like to suggest? Edit this page and add them to the queue.
diff --git a/doc/university/process/README.md b/doc/university/process/README.md
index d640edae10f..a9835ce879c 100644
--- a/doc/university/process/README.md
+++ b/doc/university/process/README.md
@@ -21,8 +21,8 @@ please submit a merge request to add an upcoming class, assign to
people, only interns, only customers, etc.).
1. To allow people to contribute all content should be in Git.
1. The content can go in a subdirectory under `/doc/university/`.
-1. To make, view or modify the slides of the classes use [Deckset](http://www.decksetapp.com/)
- or [RevealJS](http://lab.hakim.se/reveal-js/). Do not use Powerpoint or Google
+1. To make, view or modify the slides of the classes use [Deckset](https://www.deckset.com)
+ or [RevealJS](https://revealjs.com/#/). Do not use Powerpoint or Google
Slides since this prevents everyone from contributing.
1. Please upload any video recordings to our Youtube channel. We prefer them to
be public, if needed they can be unlisted but if so they should be linked from
diff --git a/doc/university/support/README.md b/doc/university/support/README.md
index 76cc258355f..1c77fbeb8d6 100644
--- a/doc/university/support/README.md
+++ b/doc/university/support/README.md
@@ -70,7 +70,7 @@ Sometimes we need to upgrade customers from old versions of GitLab to latest, so
- [Perform the MySQL to PostgreSQL migration to convert your backup](../../update/mysql_to_postgresql.md)
- [Upgrade to Omnibus 7.14](https://docs.gitlab.com/omnibus/update/README.html#upgrading-from-a-non-omnibus-installation-to-an-omnibus-installation)
- [Restore backup using our Restore rake task](../../raketasks/backup_restore.md#restore)
- - [Upgrade to latest EE](https://about.gitlab.com/downloads-ee)
+ - [Upgrade to latest EE](https://about.gitlab.com/update/)
- (GitLab inc. only) Acquire and apply a license for the Enterprise Edition product, ask in #support
- Perform a downgrade from [EE to CE](../../downgrade_ee_to_ce/README.md)
@@ -147,12 +147,12 @@ Some tickets need specific knowledge or a deep understanding of a particular com
- Read about [Escalation](https://about.gitlab.com/handbook/support/workflows/issue_escalations.html)
- Find the macros in Zendesk for ticket escalations
-- Take a look at the [GitLab.com Team page](https://about.gitlab.com/team/) to find the resident experts in their fields
+- Take a look at the [GitLab.com Team page](https://about.gitlab.com/company/team/) to find the resident experts in their fields
### Learn about raising issues and fielding feature proposals
- Understand what's in the pipeline and proposed features at GitLab: [Direction Page](https://about.gitlab.com/direction/)
-- Practice searching issues and filtering using [labels](https://gitlab.com/gitlab-org/gitlab-foss/labels) to find existing feature proposals and bugs
+- Practice searching issues and filtering using [labels](https://gitlab.com/gitlab-org/gitlab/-/labels) to find existing feature proposals and bugs
- If raising a new issue always provide a relevant label and a link to the relevant ticket in Zendesk
- Add [customer labels](https://gitlab.com/gitlab-org/gitlab-foss/issues?label_name%5B%5D=customer) for those issues relevant to our subscribers
- Take a look at the [existing issue templates](https://gitlab.com/gitlab-org/gitlab/blob/master/CONTRIBUTING.md#issue-tracker) to see what is expected
diff --git a/doc/university/training/end-user/README.md b/doc/university/training/end-user/README.md
index 40ad71df0be..4c86aedff8f 100644
--- a/doc/university/training/end-user/README.md
+++ b/doc/university/training/end-user/README.md
@@ -48,7 +48,7 @@ Workshop Time!
### Setup
- Windows: Install 'Git for Windows'
- - <https://git-for-windows.github.io>
+ - <https://gitforwindows.org>
- Mac: Type `git` in the Terminal application.
- If it's not installed, it will prompt you to install it.
- Linux
@@ -253,7 +253,7 @@ git push origin conflicts_branch -f
- When to use `git merge` and when to use `git rebase`
- Rebase when updating your branch with master
- Merge when bringing changes from feature to master
-- Reference: <https://www.atlassian.com/git/tutorials/merging-vs-rebasing/>
+- Reference: <https://www.atlassian.com/git/tutorials/merging-vs-rebasing>
## Revert and Unstage
diff --git a/doc/university/training/topics/env_setup.md b/doc/university/training/topics/env_setup.md
index 92d2613c5d2..f65b4f68868 100644
--- a/doc/university/training/topics/env_setup.md
+++ b/doc/university/training/topics/env_setup.md
@@ -7,7 +7,7 @@ comments: false
## Install
- **Windows**
- - Install 'Git for Windows' from <https://git-for-windows.github.io>
+ - Install 'Git for Windows' from <https://gitforwindows.org>
- **Mac**
- Type '`git`' in the Terminal application.
diff --git a/doc/university/training/topics/merge_conflicts.md b/doc/university/training/topics/merge_conflicts.md
index 12798f7ff08..a01b3dbf3e0 100644
--- a/doc/university/training/topics/merge_conflicts.md
+++ b/doc/university/training/topics/merge_conflicts.md
@@ -63,4 +63,4 @@ git push origin conflicts_branch -f
- When to use `git merge` and when to use `git rebase`
- Rebase when updating your branch with master
- Merge when bringing changes from feature to master
-- Reference: <https://www.atlassian.com/git/tutorials/merging-vs-rebasing/>
+- Reference: <https://www.atlassian.com/git/tutorials/merging-vs-rebasing>
diff --git a/doc/university/training/user_training.md b/doc/university/training/user_training.md
index 262b153e3e2..08b7635f9ce 100644
--- a/doc/university/training/user_training.md
+++ b/doc/university/training/user_training.md
@@ -39,7 +39,7 @@ Use the tools at your disposal when you get stuck.
- Windows: Install 'Git for Windows'
-> <https://git-for-windows.github.io>
+> <https://gitforwindows.org>
- Mac: Type '`git`' in the Terminal application.
@@ -242,7 +242,7 @@ See GitLab merge requests for examples: <https://gitlab.com/gitlab-org/gitlab-fo
1. Create an annotated tag.
1. Push the tags to the remote repository.
-Additional resources: <http://git-scm.com/book/en/Git-Basics-Tagging>.
+Additional resources: <https://git-scm.com/book/en/v2/Git-Basics-Tagging>.
## Commands (tags)
diff --git a/doc/user/application_security/container_scanning/index.md b/doc/user/application_security/container_scanning/index.md
index ad3f0663ed5..14dae56f087 100644
--- a/doc/user/application_security/container_scanning/index.md
+++ b/doc/user/application_security/container_scanning/index.md
@@ -87,7 +87,7 @@ The results will be saved as a
that you can later download and analyze.
Due to implementation limitations, we always take the latest Container Scanning
artifact available. Behind the scenes, the
-[GitLab Container Scanning analyzer](https://gitlab.com/gitlab-org/security-products/container-scanning)
+[GitLab Klar analyzer](https://gitlab.com/gitlab-org/security-products/analyzers/klar/)
is used and runs the scans.
## Example
@@ -145,6 +145,23 @@ container_scanning:
GIT_STRATEGY: fetch
```
+### Available variables
+
+Container Scanning can be [configured](#overriding-the-container-scanning-template)
+using environment variables.
+
+| Environment Variable | Description | Default |
+| ------ | ------ | ------ |
+| `KLAR_TRACE` | Set to true to enable more verbose output from klar. | `"false"` |
+| `DOCKER_USER` | Username for accessing a Docker registry requiring authentication. | `$CI_REGISTRY_USER` |
+| `DOCKER_PASSWORD` | Password for accessing a Docker registry requiring authentication. | `$CI_REGISTRY_PASSWORD` |
+| `CLAIR_OUTPUT` | Severity level threshold. Vulnerabilities with severity level higher than or equal to this threshold will be outputted. Supported levels are `Unknown`, `Negligible`, `Low`, `Medium`, `High`, `Critical` and `Defcon1`. | `Unknown` |
+| `REGISTRY_INSECURE` | Allow [Klar](https://github.com/optiopay/klar) to access insecure registries (HTTP only). Should only be set to `true` when testing the image locally. | `"false"` |
+| `CLAIR_VULNERABILITIES_DB_URL` | This variable is explicitly set in the [services section](https://gitlab.com/gitlab-org/gitlab/blob/30522ca8b901223ac8c32b633d8d67f340b159c1/lib/gitlab/ci/templates/Security/Container-Scanning.gitlab-ci.yml#L17-19) of the `Container-Scanning.gitlab-ci.yml` file and defaults to `clair-vulnerabilities-db`. This value represents the address that the [postgres server hosting the vulnerabilities definitions](https://hub.docker.com/r/arminc/clair-db) is running on and **shouldn't be changed** unless you're running the image locally as described in the [Running the scanning tool](https://gitlab.com/gitlab-org/security-products/analyzers/klar/#running-the-scanning-tool) section of the [klar readme](https://gitlab.com/gitlab-org/security-products/analyzers/klar). | `clair-vulnerabilities-db` |
+| `CI_APPLICATION_REPOSITORY` | Docker repository URL for the image to be scanned. | `$CI_REGISTRY_IMAGE/$CI_COMMIT_REF_SLUG` |
+| `CI_APPLICATION_TAG` | Docker respository tag for the image to be scanned. | `$CI_COMMIT_SHA` |
+| `CLAIR_DB_IMAGE_TAG` | The Docker image tag for the [postgres server hosting the vulnerabilities definitions](https://hub.docker.com/r/arminc/clair-db). It can be useful to override this value with a specific version, for example, to provide a consistent set of vulnerabilities for integration testing purposes. | `latest` |
+
## Security Dashboard
The Security Dashboard is a good place to get an overview of all the security
diff --git a/doc/user/project/img/issue_boards_multi_select.png b/doc/user/project/img/issue_boards_multi_select.png
new file mode 100644
index 00000000000..34ec0c1c58e
--- /dev/null
+++ b/doc/user/project/img/issue_boards_multi_select.png
Binary files differ
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/pages/custom_domains_ssl_tls_certification/dns_concepts.md b/doc/user/project/pages/custom_domains_ssl_tls_certification/dns_concepts.md
index dc75eb450a3..e561da423f4 100644
--- a/doc/user/project/pages/custom_domains_ssl_tls_certification/dns_concepts.md
+++ b/doc/user/project/pages/custom_domains_ssl_tls_certification/dns_concepts.md
@@ -27,16 +27,16 @@ to do it for you.
To help you out, we've gathered some instructions on how to do that
for the most popular hosting services:
-- [Amazon](http://docs.aws.amazon.com/gettingstarted/latest/swh/getting-started-configure-route53.html)
+- [Amazon](https://docs.aws.amazon.com/AmazonS3/latest/dev/website-hosting-custom-domain-walkthrough.html)
- [Bluehost](https://my.bluehost.com/cgi/help/559)
- [CloudFlare](https://support.cloudflare.com/hc/en-us/articles/200169096-How-do-I-add-A-records-)
-- [cPanel](https://documentation.cpanel.net/display/ALD/Edit+DNS+Zone)
+- [cPanel](https://documentation.cpanel.net/display/84Docs/Edit+DNS+Zone)
- [DreamHost](https://help.dreamhost.com/hc/en-us/articles/215414867-How-do-I-add-custom-DNS-records-)
- [Go Daddy](https://www.godaddy.com/help/add-an-a-record-19238)
-- [Hostgator](http://support.hostgator.com/articles/changing-dns-records)
+- [Hostgator](https://www.hostgator.com/help/article/changing-dns-records)
- [Inmotion hosting](https://my.bluehost.com/cgi/help/559)
- [Media Temple](https://mediatemple.net/community/products/dv/204403794/how-can-i-change-the-dns-records-for-my-domain)
-- [Microsoft](https://msdn.microsoft.com/en-us/library/bb727018.aspx)
+- [Microsoft](https://docs.microsoft.com/en-us/previous-versions/windows/it-pro/windows-2000-server/bb727018(v=technet.10))
If your hosting service is not listed above, you can just try to
search the web for `how to add dns record on <my hosting service>`.
diff --git a/doc/user/project/pages/custom_domains_ssl_tls_certification/index.md b/doc/user/project/pages/custom_domains_ssl_tls_certification/index.md
index d36f81f375c..a5d8de9a6cd 100644
--- a/doc/user/project/pages/custom_domains_ssl_tls_certification/index.md
+++ b/doc/user/project/pages/custom_domains_ssl_tls_certification/index.md
@@ -162,7 +162,7 @@ from the GitLab project.
> - Domain verification is **required for GitLab.com users**;
for GitLab self-managed instances, your GitLab administrator has the option
to [disabled custom domain verification](../../../../administration/pages/index.md#custom-domain-verification).
-> - [DNS propagation may take some time (up to 24h)](http://www.inmotionhosting.com/support/domain-names/dns-nameserver-changes/domain-names-dns-changes),
+> - [DNS propagation may take some time (up to 24h)](https://www.inmotionhosting.com/support/domain-names/dns-nameserver-changes/domain-names-dns-changes),
although it's usually a matter of minutes to complete. Until it does, verification
will fail and attempts to visit your domain will respond with a 404.
> - Once your domain has been verified, leave the verification record
diff --git a/doc/user/project/pages/custom_domains_ssl_tls_certification/ssl_tls_concepts.md b/doc/user/project/pages/custom_domains_ssl_tls_certification/ssl_tls_concepts.md
index b31ae053a81..30dc15b94c4 100644
--- a/doc/user/project/pages/custom_domains_ssl_tls_certification/ssl_tls_concepts.md
+++ b/doc/user/project/pages/custom_domains_ssl_tls_certification/ssl_tls_concepts.md
@@ -31,7 +31,7 @@ security measure, necessary just for big companies, like banks and shoppings sit
with financial transactions.
Now we have a different picture. [According to Josh Aas](https://letsencrypt.org/2015/10/29/phishing-and-malware.html), Executive Director at [ISRG](https://en.wikipedia.org/wiki/Internet_Security_Research_Group):
-> _We’ve since come to realize that HTTPS is important for almost all websites. It’s important for any website that allows people to log in with a password, any website that [tracks its users](https://www.washingtonpost.com/news/the-switch/wp/2013/12/10/nsa-uses-google-cookies-to-pinpoint-targets-for-hacking/) in any way, any website that [doesn’t want its content altered](http://arstechnica.com/tech-policy/2014/09/why-comcasts-javascript-ad-injections-threaten-security-net-neutrality/), and for any site that offers content people might not want others to know they are consuming. We’ve also learned that any site not secured by HTTPS [can be used to attack other sites](https://krebsonsecurity.com/2015/04/dont-be-fodder-for-chinas-great-cannon/)._
+> _We’ve since come to realize that HTTPS is important for almost all websites. It’s important for any website that allows people to log in with a password, any website that [tracks its users](https://www.washingtonpost.com/news/the-switch/wp/2013/12/10/nsa-uses-google-cookies-to-pinpoint-targets-for-hacking/) in any way, any website that [doesn’t want its content altered](https://arstechnica.com/tech-policy/2014/09/why-comcasts-javascript-ad-injections-threaten-security-net-neutrality/), and for any site that offers content people might not want others to know they are consuming. We’ve also learned that any site not secured by HTTPS [can be used to attack other sites](https://krebsonsecurity.com/2015/04/dont-be-fodder-for-chinas-great-cannon/)._
Therefore, the reason why certificates are so important is that they encrypt
the connection between the **client** (you, me, your visitors)
diff --git a/doc/user/project/pages/getting_started_part_four.md b/doc/user/project/pages/getting_started_part_four.md
index da472307650..a544250eb0e 100644
--- a/doc/user/project/pages/getting_started_part_four.md
+++ b/doc/user/project/pages/getting_started_part_four.md
@@ -38,7 +38,7 @@ Explaining [every detail of GitLab CI/CD](../../../ci/yaml/README.md)
and GitLab Runner is out of the scope of this guide, but we'll
need to understand just a few things to be able to write our own
`.gitlab-ci.yml` or tweak an existing one. It's an
-[Yaml](http://docs.ansible.com/ansible/YAMLSyntax.html) file,
+[Yaml](https://docs.ansible.com/ansible/latest/reference_appendices/YAMLSyntax.html) file,
with its own syntax. You can always check your CI syntax with
the [GitLab CI Lint Tool](https://gitlab.com/ci/lint).
@@ -127,7 +127,7 @@ pages:
The script above would be enough to build your Jekyll
site with GitLab Pages. But, from Jekyll 3.4.0 on, its default
template originated by `jekyll new project` requires
-[Bundler](http://bundler.io/) to install Jekyll dependencies
+[Bundler](https://bundler.io) to install Jekyll dependencies
and the default theme. To adjust our script to meet these new
requirements, we only need to install and build Jekyll with Bundler:
diff --git a/lib/gitlab/import_export/project_tree_restorer.rb b/lib/gitlab/import_export/project_tree_restorer.rb
index 9beb84a2422..3fa5765fd4a 100644
--- a/lib/gitlab/import_export/project_tree_restorer.rb
+++ b/lib/gitlab/import_export/project_tree_restorer.rb
@@ -32,7 +32,7 @@ module Gitlab
ActiveRecord::Base.uncached do
ActiveRecord::Base.no_touching do
- update_project_params
+ update_project_params!
create_relations
end
end
@@ -70,7 +70,7 @@ module Gitlab
# the configuration yaml file too.
# Finally, it updates each attribute in the newly imported project.
def create_relations
- project_relations_without_project_members.each do |relation_key, relation_definition|
+ project_relations.each do |relation_key, relation_definition|
relation_key_s = relation_key.to_s
if relation_definition.present?
@@ -124,56 +124,40 @@ module Gitlab
# no-op
end
- def project_relations_without_project_members
- # We remove `project_members` as they are deserialized separately
- project_relations.except(:project_members)
- end
-
def project_relations
- reader.attributes_finder.find_relations_tree(:project)
+ @project_relations ||= reader.attributes_finder.find_relations_tree(:project)
end
- def update_project_params
+ def update_project_params!
Gitlab::Timeless.timeless(@project) do
- @project.update(project_params)
- end
- end
+ project_params = @tree_hash.reject do |key, value|
+ project_relations.include?(key.to_sym)
+ end
- def project_params
- @project_params ||= begin
- attrs = json_params.merge(override_params).merge(visibility_level, external_label)
+ project_params = project_params.merge(present_project_override_params)
# Cleaning all imported and overridden params
- Gitlab::ImportExport::AttributeCleaner.clean(relation_hash: attrs,
- relation_class: Project,
- excluded_keys: excluded_keys_for_relation(:project))
+ project_params = Gitlab::ImportExport::AttributeCleaner.clean(
+ relation_hash: project_params,
+ relation_class: Project,
+ excluded_keys: excluded_keys_for_relation(:project))
+
+ @project.assign_attributes(project_params)
+ @project.drop_visibility_level!
+ @project.save!
end
end
- def override_params
- @override_params ||= @project.import_data&.data&.fetch('override_params', nil) || {}
- end
-
- def json_params
- @json_params ||= @tree_hash.reject do |key, value|
- # return params that are not 1 to many or 1 to 1 relations
- value.respond_to?(:each) && !Project.column_names.include?(key)
- end
- end
-
- def visibility_level
- level = override_params['visibility_level'] || json_params['visibility_level'] || @project.visibility_level
- level = @project.group.visibility_level if @project.group && level.to_i > @project.group.visibility_level
- level = Gitlab::VisibilityLevel::PRIVATE if level == Gitlab::VisibilityLevel::INTERNAL && Gitlab::CurrentSettings.restricted_visibility_levels.include?(level)
-
- { 'visibility_level' => level }
+ def present_project_override_params
+ # we filter out the empty strings from the overrides
+ # keeping the default values configured
+ project_override_params.transform_values do |value|
+ value.is_a?(String) ? value.presence : value
+ end.compact
end
- def external_label
- label = override_params['external_authorization_classification_label'].presence ||
- json_params['external_authorization_classification_label'].presence
-
- { 'external_authorization_classification_label' => label }
+ def project_override_params
+ @project_override_params ||= @project.import_data&.data&.fetch('override_params', nil) || {}
end
# Given a relation hash containing one or more models and its relationships,
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index c3c1b551b50..6716fc6fcd2 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -14842,6 +14842,9 @@ msgid_plural "Showing %d events"
msgstr[0] ""
msgstr[1] ""
+msgid "Showing %{pageSize} of %{total} issues"
+msgstr ""
+
msgid "Showing Latest Version"
msgstr ""
@@ -15097,6 +15100,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/features/boards/multi_select_spec.rb b/spec/features/boards/multi_select_spec.rb
new file mode 100644
index 00000000000..ff3cb0271bd
--- /dev/null
+++ b/spec/features/boards/multi_select_spec.rb
@@ -0,0 +1,129 @@
+# 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
+
+ it 'adds label when issues are moved to different card', :js do
+ page.within(all('.js-board-list')[1]) do
+ expect(find('.board-card:nth-child(1)')).not_to have_content(label2.title)
+ expect(find('.board-card:nth-child(2)')).not_to have_content(label2.title)
+ end
+
+ 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(label2.title)
+ expect(find('.board-card:nth-child(2)')).to have_content(label2.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/project_services/jira_service_spec.rb b/spec/models/project_services/jira_service_spec.rb
index 28d2ea79641..bdb7c7aeff4 100644
--- a/spec/models/project_services/jira_service_spec.rb
+++ b/spec/models/project_services/jira_service_spec.rb
@@ -19,7 +19,7 @@ describe JiraService do
described_class.create(
project: create(:project),
active: true,
- username: 'username',
+ username: 'username ',
password: 'test',
jira_issue_transition_id: 24,
url: 'http://jira.test.com/path/'
@@ -35,6 +35,15 @@ describe JiraService do
it 'leaves out trailing slashes in context' do
expect(service.options[:context_path]).to eq('/path')
end
+
+ it 'leaves out trailing whitespaces in username' do
+ expect(service.options[:username]).to eq('username')
+ end
+
+ it 'provides additional cookies to allow basic auth with oracle webgate' do
+ expect(service.options[:use_cookies]).to eq(true)
+ expect(service.options[:additional_cookies]).to eq(['OBBasicAuth=fromDialog'])
+ end
end
describe 'Associations' do
@@ -68,7 +77,7 @@ describe JiraService do
expect(subject.properties).to be_nil
end
- it 'sets title correctly' do
+ it 'sets title correctly' do
service = subject
expect(service.title).to eq('custom title')
@@ -93,7 +102,7 @@ describe JiraService do
end
# we need to make sure we are able to read both from properties and jira_tracker_data table
- # TODO: change this as part of #63084
+ # TODO: change this as part of https://gitlab.com/gitlab-org/gitlab/issues/29404
context 'overriding properties' do
let(:access_params) do
{ url: url, api_url: api_url, username: username, password: password,
@@ -604,26 +613,6 @@ describe JiraService do
end
end
- describe 'additional cookies' do
- let(:project) { create(:project) }
-
- context 'provides additional cookies to allow basic auth with oracle webgate' do
- before do
- @service = project.create_jira_service(
- active: true, properties: { url: 'http://jira.com' })
- end
-
- after do
- @service.destroy!
- end
-
- it 'is initialized' do
- expect(@service.options[:use_cookies]).to eq(true)
- expect(@service.options[:additional_cookies]).to eq(['OBBasicAuth=fromDialog'])
- end
- end
- end
-
describe 'project and issue urls' do
context 'when gitlab.yml was initialized' do
it 'is prepopulated with the settings' do
diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb
index 2870c55c7c6..ddba5fca12c 100644
--- a/spec/models/project_spec.rb
+++ b/spec/models/project_spec.rb
@@ -5179,6 +5179,61 @@ describe Project do
end
end
+ describe '#drop_visibility_level!' do
+ context 'when has a group' do
+ let(:group) { create(:group, visibility_level: group_visibility_level) }
+ let(:project) { build(:project, namespace: group, visibility_level: project_visibility_level) }
+
+ context 'when the group `visibility_level` is more strict' do
+ let(:group_visibility_level) { Gitlab::VisibilityLevel::PRIVATE }
+ let(:project_visibility_level) { Gitlab::VisibilityLevel::INTERNAL }
+
+ it 'sets `visibility_level` value from the group' do
+ expect { project.drop_visibility_level! }
+ .to change { project.visibility_level }
+ .to(Gitlab::VisibilityLevel::PRIVATE)
+ end
+ end
+
+ context 'when the group `visibility_level` is less strict' do
+ let(:group_visibility_level) { Gitlab::VisibilityLevel::INTERNAL }
+ let(:project_visibility_level) { Gitlab::VisibilityLevel::PRIVATE }
+
+ it 'does not change the value of the `visibility_level` field' do
+ expect { project.drop_visibility_level! }
+ .not_to change { project.visibility_level }
+ end
+ end
+ end
+
+ context 'when `restricted_visibility_levels` of the GitLab instance exist' do
+ before do
+ stub_application_setting(restricted_visibility_levels: [Gitlab::VisibilityLevel::INTERNAL])
+ end
+
+ let(:project) { build(:project, visibility_level: project_visibility_level) }
+
+ context 'when `visibility_level` is included into `restricted_visibility_levels`' do
+ let(:project_visibility_level) { Gitlab::VisibilityLevel::INTERNAL }
+
+ it 'sets `visibility_level` value to `PRIVATE`' do
+ expect { project.drop_visibility_level! }
+ .to change { project.visibility_level }
+ .to(Gitlab::VisibilityLevel::PRIVATE)
+ end
+ end
+
+ context 'when `restricted_visibility_levels` does not include `visibility_level`' do
+ let(:project_visibility_level) { Gitlab::VisibilityLevel::PUBLIC }
+
+ it 'does not change the value of the `visibility_level` field' do
+ expect { project.drop_visibility_level! }
+ .to not_change { project.visibility_level }
+ end
+ end
+ end
+ end
+
def rugged_config
rugged_repo(project.repository).config
end