Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
0dea53d5e5
commit
f03a645e74
40 changed files with 975 additions and 129 deletions
|
@ -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,
|
||||
|
|
|
@ -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 => {
|
||||
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)),
|
||||
);
|
||||
|
||||
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>
|
||||
|
|
11
app/assets/javascripts/boards/constants.js
Normal file
11
app/assets/javascripts/boards/constants.js
Normal file
|
@ -0,0 +1,11 @@
|
|||
export const ListType = {
|
||||
assignee: 'assignee',
|
||||
milestone: 'milestone',
|
||||
backlog: 'backlog',
|
||||
closed: 'closed',
|
||||
label: 'label',
|
||||
};
|
||||
|
||||
export default {
|
||||
ListType,
|
||||
};
|
|
@ -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) {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: 'JIRA Service: Improve username/email validation'
|
||||
merge_request: 18397
|
||||
author:
|
||||
type: fixed
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
BIN
doc/user/project/img/issue_boards_multi_select.png
Normal file
BIN
doc/user/project/img/issue_boards_multi_select.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 21 KiB |
|
@ -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).
|
||||
|
|
|
@ -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>`.
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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:
|
||||
|
||||
|
|
|
@ -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
|
||||
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,
|
||||
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) || {}
|
||||
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 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 }
|
||||
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,
|
||||
|
|
|
@ -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 ""
|
||||
|
||||
|
|
129
spec/features/boards/multi_select_spec.rb
Normal file
129
spec/features/boards/multi_select_spec.rb
Normal file
|
@ -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
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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
|
||||
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue