Commit 29b69fa4 authored by Dmitriy Zaporozhets's avatar Dmitriy Zaporozhets

Merge remote-tracking branch 'ee-com/master' into ce-to-ee

Signed-off-by: default avatarDmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>
parents a1aa0a36 b6d57464
......@@ -14,7 +14,7 @@ linters:
# Whether or not to prefer `border: 0` over `border: none`.
BorderZero:
enabled: false
enabled: true
# Reports when you define a rule set using a selector with chained classes
# (a.k.a. adjoining classes).
......
......@@ -68,7 +68,6 @@ $(() => {
rootPath: $boardApp.dataset.rootPath,
bulkUpdatePath: $boardApp.dataset.bulkUpdatePath,
detailIssue: Store.detail,
milestoneTitle: $boardApp.dataset.boardMilestoneTitle,
defaultAvatar: $boardApp.dataset.defaultAvatar,
},
computed: {
......@@ -77,16 +76,6 @@ $(() => {
},
},
created () {
if (this.milestoneTitle) {
const milestoneTitleParam = `milestone_title=${this.milestoneTitle}`;
Store.filter.path = [milestoneTitleParam].concat(
Store.filter.path.split('&').filter(param => param.match(/^milestone_title=(.*)$/g) === null)
).join('&');
Store.updateFiltersUrl(true);
}
gl.boardService = new BoardService({
boardsEndpoint: this.boardsEndpoint,
listsEndpoint: this.listsEndpoint,
......@@ -102,7 +91,7 @@ $(() => {
eventHub.$off('updateTokens', this.updateTokens);
},
mounted () {
this.filterManager = new FilteredSearchBoards(Store.filter, true, [(this.milestoneTitle ? 'milestone' : null)]);
this.filterManager = new FilteredSearchBoards(Store.filter, true, Store.cantEdit);
this.filterManager.setup();
Store.disabled = this.disabled;
......@@ -146,6 +135,40 @@ $(() => {
},
});
const configEl = document.querySelector('.js-board-config');
if (configEl) {
gl.boardConfigToggle = new Vue({
el: configEl,
data() {
return {
canAdminList: convertPermissionToBoolean(
this.$options.el.dataset.canAdminList,
),
};
},
methods: {
showPage: page => gl.issueBoards.BoardsStore.showPage(page),
},
computed: {
buttonText() {
return this.canAdminList ? 'Edit board' : 'View scope';
},
},
template: `
<div class="prepend-left-10">
<button
class="btn btn-inverted"
type="button"
@click.prevent="showPage('edit')"
>
{{ buttonText }}
</button>
</div>
`,
});
}
gl.IssueBoardsModalAddBtn = new Vue({
mixins: [gl.issueBoards.ModalMixins],
el: document.getElementById('js-add-issues-btn'),
......
<script>
import UsersSelect from '~/users_select';
import loadingIcon from '~/vue_shared/components/loading_icon.vue';
import UserAvatarImage from '~/vue_shared/components/user_avatar/user_avatar_image.vue';
export default {
props: {
anyUserText: {
type: String,
required: false,
default: 'Any user',
},
board: {
type: Object,
required: true,
},
canEdit: {
type: Boolean,
required: false,
default: false,
},
fieldName: {
type: String,
required: true,
},
groupId: {
type: Number,
required: false,
default: 0,
},
label: {
type: String,
required: true,
},
placeholderText: {
type: String,
required: false,
default: 'Select user',
},
projectId: {
type: Number,
required: false,
default: 0,
},
selected: {
type: Object,
required: true,
},
wrapperClass: {
type: String,
required: false,
default: '',
},
},
components: {
loadingIcon,
UserAvatarImage,
},
computed: {
hasValue() {
return this.selected.id > 0;
},
selectedId() {
return this.selected ? this.selected.id : null;
},
},
watch: {
selected() {
this.initSelect();
},
},
methods: {
initSelect() {
this.userDropdown = new UsersSelect(null, this.$refs.dropdown, {
handleClick: this.selectUser,
});
},
selectUser(user, isMarking) {
let assignee = user;
if (!isMarking) {
// correctly select "unassigned" in Assignee dropdown
assignee = {
id: undefined,
};
}
this.board.assignee_id = assignee.id;
this.board.assignee = assignee;
},
},
mounted() {
this.initSelect();
},
};
</script>
<template>
<div
class="block"
:class="wrapperClass"
>
<div class="title append-bottom-10">
{{ label }}
<button
v-if="canEdit"
type="button"
class="edit-link btn btn-blank pull-right"
>
Edit
</button>
</div>
<div class="value">
<div
v-if="hasValue"
class="media"
>
<div class="align-center">
<user-avatar-image
:img-src="selected.avatar_url"
:size="32"
/>
</div>
<div class="media-body">
<div class="bold author">
{{ selected.name }}
</div>
<div class="username">
@{{ selected.username }}
</div>
</div>
</div>
<div
v-else
class="text-secondary"
>
{{ anyUserText }}
</div>
</div>
<div
class="selectbox"
style="display: none"
>
<div class="dropdown">
<button
class="dropdown-menu-toggle wide"
ref="dropdown"
:data-field-name="fieldName"
:data-dropdown-title="placeholderText"
:data-any-user="anyUserText"
:data-group-id="groupId"
:data-project-id="projectId"
:data-selected="selectedId"
data-toggle="dropdown"
aria-expanded="false"
type="button"
>
<span class="dropdown-toggle-text">
{{ placeholderText }}
</span>
<i
aria-hidden="true"
class="fa fa-chevron-down"
data-hidden="true"
/>
</button>
<div class="dropdown-menu dropdown-select dropdown-menu-paging dropdown-menu-user dropdown-menu-selectable dropdown-menu-author">
<div class="dropdown-input">
<input
autocomplete="off"
class="dropdown-input-field"
placeholder="Search"
type="search"
>
<i
aria-hidden="true"
class="fa fa-search dropdown-input-search"
data-hidden="true"
/>
<i
aria-hidden="true"
class="fa fa-times dropdown-input-clear js-dropdown-input-clear"
data-hidden="true"
role="button"
/>
</div>
<div class="dropdown-content"></div>
<div class="dropdown-loading">
<loading-icon />
</div>
</div>
</div>
</div>
</div>
</template>
<script>
/* global BoardService */
import Flash from '~/flash';
import PopupDialog from '~/vue_shared/components/popup_dialog.vue';
import BoardMilestoneSelect from './milestone_select.vue';
import BoardWeightSelect from './weight_select.vue';
import BoardLabelsSelect from './labels_select.vue';
import AssigneeSelect from './assignee_select.vue';
window.gl = window.gl || {};
window.gl.issueBoards = window.gl.issueBoards || {};
const Store = gl.issueBoards.BoardsStore;
const boardDefaults = {
id: false,
name: '',
labels: [],
milestone_id: undefined,
assignee: {},
assignee_id: undefined,
weight: null,
};
export default {
props: {
canAdminBoard: {
type: Boolean,
required: true,
},
milestonePath: {
type: String,
required: true,
},
labelsPath: {
type: String,
required: true,
},
scopedIssueBoardFeatureEnabled: {
type: Boolean,
required: false,
default: false,
},
projectId: {
type: Number,
required: false,
default: 0,
},
groupId: {
type: Number,
required: false,
default: 0,
},
weights: {
type: String,
required: false,
},
},
data() {
return {
board: { ...boardDefaults, ...this.currentBoard },
expanded: false,
issue: {},
currentBoard: Store.state.currentBoard,
currentPage: Store.state.currentPage,
milestones: [],
milestoneDropdownOpen: false,
isLoading: false,
};
},
components: {
AssigneeSelect,
BoardLabelsSelect,
BoardMilestoneSelect,
BoardWeightSelect,
PopupDialog,
},
computed: {
isNewForm() {
return this.currentPage === 'new';
},
isDeleteForm() {
return this.currentPage === 'delete';
},
isEditForm() {
return this.currentPage === 'edit';
},
buttonText() {
if (this.isNewForm) {
return 'Create board';
}
if (this.isDeleteForm) {
return 'Delete';
}
return 'Save changes';
},
buttonKind() {
if (this.isNewForm) {
return 'success';
}
if (this.isDeleteForm) {
return 'danger';
}
return 'info';
},
title() {
if (this.isNewForm) {
return 'Create new board';
}
if (this.isDeleteForm) {
return 'Delete board';
}
if (this.readonly) {
return 'Board scope';
}
return 'Edit board';
},
expandButtonText() {
return this.expanded ? 'Collapse' : 'Expand';
},
collapseScope() {
return this.isNewForm;
},
readonly() {
return !this.canAdminBoard;
},
weightsArray() {
return JSON.parse(this.weights);
},
submitDisabled() {
return this.isLoading || this.board.name.length === 0;
},
},
methods: {
submit() {
if (this.board.name.length === 0) return;
this.isLoading = true;
if (this.isDeleteForm) {
gl.boardService.deleteBoard(this.currentBoard)
.then(() => {
gl.utils.visitUrl(Store.rootPath);
})
.catch(() => {
Flash('Failed to delete board. Please try again.');
this.isLoading = false;
});
} else {
gl.boardService.createBoard(this.board)
.then(resp => resp.json())
.then((data) => {
gl.utils.visitUrl(data.board_path);
})
.catch(() => {
Flash('Unable to save your changes. Please try again.');
this.isLoading = false;
});
}
},
cancel() {
Store.state.currentPage = '';
},
resetFormState() {
if (this.isNewForm) {
// Clear the form when we open the "New board" modal
this.board = { ...boardDefaults };
} else if (this.currentBoard && Object.keys(this.currentBoard).length) {
this.board = { ...boardDefaults, ...this.currentBoard };
}
},
},
mounted() {
this.resetFormState();
if (this.$refs.name) {
this.$refs.name.focus();
}
},
};
</script>
<template>
<popup-dialog
v-show="currentPage"
modal-dialog-class="board-config-modal"
:hide-footer="readonly"
:title="title"
:primary-button-label="buttonText"
:kind="buttonKind"
:submit-disabled="submitDisabled"
@toggle="cancel"
@submit="submit"
>
<template slot="body">
<p v-if="isDeleteForm">
Are you sure you want to delete this board?
</p>
<form
v-else
class="js-board-config-modal"
@submit.prevent
>
<div
v-if="!readonly"
class="append-bottom-20"
>
<label
class="form-section-title label-light"
for="board-new-name"
>
Board name
</label>
<input
ref="name"
class="form-control"
type="text"
id="board-new-name"
v-model="board.name"
@keyup.enter="submit"
placeholder="Enter board name"
>
</div>
<div v-if="scopedIssueBoardFeatureEnabled">
<div
v-if="canAdminBoard"
class="media append-bottom-10"
>
<label class="form-section-title label-light media-body">
Board scope
</label>
<button
type="button"
class="btn"
@click="expanded = !expanded"
v-if="collapseScope"
>
{{ expandButtonText }}
</button>
</div>
<p class="text-secondary append-bottom-10">
Board scope affects which issues are displayed for anyone who visits this board
</p>
<div v-if="!collapseScope || expanded">
<board-milestone-select
:board="board"
:milestone-path="milestonePath"
:can-edit="canAdminBoard"
/>
<board-labels-select
:board="board"
:can-edit="canAdminBoard"
:labels-path="labelsPath"
/>
<assignee-select
any-user-text="Any assignee"
:board="board"
field-name="assignee_id"
label="Assignee"
:selected="board.assignee"
:can-edit="canAdminBoard"
placeholder-text="Select assignee"
:project-id="projectId"
:group-id="groupId"
wrapper-class="assignee"
/>
<board-weight-select
:board="board"
:weights="weightsArray"
v-model="board.weight"
:can-edit="canAdminBoard"
/>
</div>
</div>
</form>
</template>
</popup-dialog>
</template>
/* global BoardService */
import Vue from 'vue';
import boardMilestoneSelect from './milestone_select';
import extraMilestones from '../mixins/extra_milestones';
(() => {
window.gl = window.gl || {};
window.gl.issueBoards = window.gl.issueBoards || {};
const Store = gl.issueBoards.BoardsStore;
gl.issueBoards.BoardSelectorForm = Vue.extend({
props: {
milestonePath: {
type: String,
required: true,
},
},
data() {
return {
board: {
id: false,
name: '',
milestone: extraMilestones[0],
milestone_id: extraMilestones[0].id,
},
currentBoard: Store.state.currentBoard,
currentPage: Store.state.currentPage,
milestones: [],
milestoneDropdownOpen: false,
extraMilestones,
};
},
components: {
boardMilestoneSelect,
},
mounted() {
if (this.currentBoard && Object.keys(this.currentBoard).length && this.currentPage !== 'new') {
this.board = Vue.util.extend({}, this.currentBoard);
}
},
computed: {
buttonText() {
if (this.currentPage === 'new') {
return 'Create';
}
return 'Save';
},
milestoneToggleText() {
return this.board.milestone.title || 'Milestone';
},
submitDisabled() {
if (this.currentPage !== 'milestone') {
return this.board.name === '';
}
return false;
},
},
methods: {
refreshPage() {
location.href = location.pathname;
},
loadMilestones(e) {
this.milestoneDropdownOpen = !this.milestoneDropdownOpen;
BoardService.loadMilestones.call(this);
if (this.milestoneDropdownOpen) {
this.$nextTick(() => {
const milestoneDropdown = this.$refs.milestoneDropdown;
const rect = e.target.getBoundingClientRect();
milestoneDropdown.style.width = `${rect.width}px`;
});
}
},
submit() {
gl.boardService.createBoard(this.board)
.then(resp => resp.json())
.then((data) => {
if (this.currentBoard && this.currentPage !== 'new') {
this.currentBoard.name = this.board.name;
if (this.currentPage === 'milestone') {
// We reload the page to make sure the store & state of the app are correct
this.refreshPage();
}
// Enable the button thanks to our jQuery disabling it
$(this.$refs.submitBtn).enable();
// Reset the selectors current page
Store.state.currentPage = '';
Store.state.reload = true;
} else if (this.currentPage === 'new') {
gl.utils.visitUrl(`${Store.rootPath}/${data.id}`);
}
})
.catch(() => {
// https://gitlab.com/gitlab-org/gitlab-ce/issues/30821
});
},
cancel() {
Store.state.currentPage = '';
},
selectMilestone(milestone) {
this.milestoneDropdownOpen = false;
this.board.milestone_id = milestone.id;
this.board.milestone = {
title: milestone.title,
};
},
},
});
})();
......@@ -51,10 +51,6 @@ export default {
project_id: this.selectedProject.id,
});
if (Store.state.currentBoard) {
issue.milestone_id = Store.state.currentBoard.milestone_id;
}
eventHub.$emit(`scroll-board-list-${this.list.id}`);
this.cancel();
......
import Vue from 'vue';
import './board_new_form';
import BoardForm from './board_form.vue';
(() => {
window.gl = window.gl || {};
......@@ -11,7 +11,7 @@ import './board_new_form';
gl.issueBoards.BoardsSelector = Vue.extend({
components: {
'board-selector-form': gl.issueBoards.BoardSelectorForm,
BoardForm,
},
props: {
currentBoard: {
......@@ -60,19 +60,6 @@ import './board_new_form';
showDelete() {
return this.boards.length > 1;
},
title() {
if (this.currentPage === 'edit') {
return 'Edit board name';
} else if (this.currentPage === 'milestone') {
return 'Edit board milestone';
} else if (this.currentPage === 'new') {
return 'Create new board';
} else if (this.currentPage === 'delete') {
return 'Delete board';
}
return 'Go to a board';
},
},
methods: {
showPage(page) {
......
......@@ -158,7 +158,7 @@ gl.issueBoards.IssueCardInner = Vue.extend({
class="card-number"
v-if="issueId"
>
<span v-if="groupId && issue.project">{{issue.project.path}}</span>{{ issueId }}
<template v-if="groupId && issue.project">{{issue.project.path}}</template>{{ issueId }}
</span>
</h4>
<div class="card-assignee">
......
<script>
/* global ListLabel */
import LabelsSelect from '~/labels_select';
import loadingIcon from '~/vue_shared/components/loading_icon.vue';
export default {
props: {
board: {
type: Object,
required: true,
},
labelsPath: {
type: String,
required: true,
},
canEdit: {
type: Boolean,
required: false,
default: false,
},
},
components: {
loadingIcon,
},
computed: {
labelIds() {
return this.board.labels.map(label => label.id);
},
isEmpty() {
return this.board.labels.length === 0;
},
},
mounted() {
this.labelsDropdown = new LabelsSelect(this.$refs.dropdownButton, {
handleClick: this.handleClick,
});
},
methods: {
labelStyle(label) {
return {
color: label.textColor,
backgroundColor: label.color,
};
},
handleClick(label) {
if (label.isAny) {
this.board.labels = [];
} else if (!this.board.labels.find(l => l.id === label.id)) {
this.board.labels.push(new ListLabel({
id: label.id,
title: label.title,
color: label.color[0],
textColor: label.text_color,
}));
} else {
let labels = this.board.labels;
labels = labels.filter(selected => selected.id !== label.id);
this.board.labels = labels;
}
},
},
};
</script>
<template>
<div class="block labels">
<div class="title append-bottom-10">
Labels
<button
v-if="canEdit"
type="button"
class="edit-link btn btn-blank pull-right"
>
Edit
</button>
</div>
<div class="value issuable-show-labels">
<span
v-if="isEmpty"
class="text-secondary"
>
Any Label
</span>
<a
v-else
href="#"
v-for="label in board.labels"
:key="label.id"
>
<span
class="label color-label"
:style="labelStyle(label)"
>
{{ label.title }}
</span>
</a>
</div>
<div
class="selectbox"
style="display: none"
>
<input
type="hidden"
name="label_id[]"
v-for="labelId in labelIds"
:key="labelId"
:value="labelId"
>
<div class="dropdown">
<button
ref="dropdownButton"
:data-labels="labelsPath"
class="dropdown-menu-toggle wide js-label-select js-multiselect js-extra-options js-board-config-modal"
data-field-name="label_id[]"
:data-show-any="true"
data-toggle="dropdown"
type="button"
>
<span class="dropdown-toggle-text">
Label
</span>
<i
aria-hidden="true"
class="fa fa-chevron-down"
data-hidden="true"
/>
</button>
<div class="dropdown-menu dropdown-select dropdown-menu-paging dropdown-menu-labels dropdown-menu-selectable">
<div class="dropdown-input">
<input
autocomplete="off"
class="dropdown-input-field"
placeholder="Search"
type="search"
>
<i
aria-hidden="true"
class="fa fa-search dropdown-input-search"
data-hidden="true"
/>
<i
aria-hidden="true"
class="fa fa-times dropdown-input-clear js-dropdown-input-clear"
data-hidden="true"
role="button"
/>
</div>
<div class="dropdown-content"></div>
<div class="dropdown-loading">
<loading-icon />
</div>
</div>
</div>
</div>
</div>
</template>
/* global BoardService */
import extraMilestones from '../mixins/extra_milestones';
export default {
props: {
board: {
type: Object,
required: true,
},
milestonePath: {
type: String,
required: true,
},
selectMilestone: {
type: Function,
required: true,
},
},
data() {
return {
loading: false,
milestones: [],
extraMilestones,
};
},
mounted() {
BoardService.loadMilestones.call(this);
},
template: `
<div>
<div class="text-center">
<i
v-if="loading"
class="fa fa-spinner fa-spin"></i>
</div>
<ul
class="board-milestone-list"
v-if="!loading">
<li v-for="milestone in extraMilestones">
<a
href="#"
@click.prevent.stop="selectMilestone(milestone)">
<i
class="fa fa-check"
v-if="board.milestone_id === milestone.id"></i>
{{ milestone.title }}
</a>
</li>
<li class="divider"></li>
<li v-for="milestone in milestones">
<a
href="#"
@click.prevent.stop="selectMilestone(milestone)">
<i
class="fa fa-check"
v-if="board.milestone_id === milestone.id"></i>
{{ milestone.title }}
</a>
</li>
</ul>
</div>
`,
};
<script>
/* global BoardService, MilestoneSelect */
import '~/milestone_select';
import loadingIcon from '~/vue_shared/components/loading_icon.vue';
const ANY_MILESTONE = 'Any Milestone';
const NO_MILESTONE = 'No Milestone';
export default {
props: {
board: {
type: Object,
required: true,
},
milestonePath: {
type: String,
required: true,
},
canEdit: {
type: Boolean,
required: false,
default: false,
},
},
components: {
loadingIcon,
},
computed: {
milestoneTitle() {
if (this.noMilestone) return NO_MILESTONE;
return this.board.milestone ? this.board.milestone.title : ANY_MILESTONE;
},
noMilestone() {
return this.milestoneId === 0;
},
milestoneId() {
return this.board.milestone_id;
},
milestoneTitleClass() {
return this.milestoneTitle === ANY_MILESTONE ? 'text-secondary' : 'bold';
},
selected() {
if (this.noMilestone) return NO_MILESTONE;
return this.board.milestone ? this.board.milestone.name : '';
},
},
methods: {
selectMilestone(milestone) {
let id = milestone.id;
// swap the IDs of 'Any' and 'No' milestone to what backend requires
if (milestone.title === ANY_MILESTONE) {
id = -1;
} else if (milestone.title === NO_MILESTONE) {
id = 0;
}
this.board.milestone_id = id;
this.board.milestone = {
...milestone,
id,
};
},
},
mounted() {
this.milestoneDropdown = new MilestoneSelect(null, this.$refs.dropdownButton, {
handleClick: this.selectMilestone,
});
},
};
</script>
<template>
<div class="block milestone">
<div class="title append-bottom-10">
Milestone
<button
v-if="canEdit"
type="button"
class="edit-link btn btn-blank pull-right"
>
Edit
</button>
</div>
<div
class="value"
:class="milestoneTitleClass"
>
{{ milestoneTitle }}
</div>
<div
class="selectbox"
style="display: none;"
>
<input
:value="milestoneId"
name="milestone_id"
type="hidden"
>
<div class="dropdown">
<button
ref="dropdownButton"
:data-selected="selected"
class="dropdown-menu-toggle wide"
:data-milestones="milestonePath"
:data-show-no="true"
:data-show-any="true"
:data-show-started="true"
:data-show-upcoming="true"
data-toggle="dropdown"
:data-use-id="true"
type="button"
>
Milestone
<i
aria-hidden="true"
data-hidden="true"
class="fa fa-chevron-down"
/>
</button>
<div class="dropdown-menu dropdown-select dropdown-menu-selectable">
<div
class="dropdown-input"
>
<input
type="search"
class="dropdown-input-field"
placeholder="Search milestones"
autocomplete="off"
>
<i
aria-hidden="true"
data-hidden="true"
class="fa fa-search dropdown-input-search"
/>
<i
role="button"
aria-hidden="true"
data-hidden="true"
class="fa fa-times dropdown-input-clear js-dropdown-input-clear"
/>
</div>
<div class="dropdown-content" />
<div class="dropdown-loading">
<loading-icon />
</div>
</div>
</div>
</div>
</div>
</template>
......@@ -30,11 +30,16 @@ gl.issueBoards.ModalFooter = Vue.extend({
const list = this.modal.selectedList || this.state.lists[firstListIndex];
const selectedIssues = ModalStore.getSelectedIssues();
const issueIds = selectedIssues.map(issue => issue.id);
const currentBoard = this.state.currentBoard;
const boardLabelIds = currentBoard.labels.map(label => label.id);
const assigneeIds = currentBoard.assignee && [currentBoard.assignee.id];
// Post the data to the backend
gl.boardService.bulkUpdate(issueIds, {
add_label_ids: [list.label.id],
milestone_id: this.state.currentBoard.milestone_id,
add_label_ids: [list.label.id, ...boardLabelIds],
milestone_id: currentBoard.milestone_id,
assignee_ids: assigneeIds,
weight: currentBoard.weight,
}).catch(() => {
new Flash('Failed to update issues, please try again.', 'alert');
......
......@@ -30,24 +30,43 @@ gl.issueBoards.RemoveIssueBtn = Vue.extend({
},
methods: {
removeIssue() {
const board = Store.state.currentBoard;
const issue = this.issue;
const lists = issue.getLists();
const boardLabelIds = board.labels.map(label => label.id);
const listLabelIds = lists.map(list => list.label.id);
let labelIds = this.issue.labels
let labelIds = issue.labels
.map(label => label.id)
.filter(id => !listLabelIds.includes(id));
.filter(id => !listLabelIds.includes(id))
.filter(id => !boardLabelIds.includes(id));
if (labelIds.length === 0) {
labelIds = [''];
}
let assigneeIds = issue.assignees
.map(assignee => assignee.id)
.filter(id => id !== board.assignee.id);
if (assigneeIds.length === 0) {
// for backend to explicitly set No Assignee
assigneeIds = ['0'];
}
const data = {
issue: {
label_ids: labelIds,
assignee_ids: assigneeIds,
},
};
if (Store.state.currentBoard.milestone_id) {
if (board.milestone_id) {
data.issue.milestone_id = -1;
}
if (board.weight) {
data.issue.weight = null;
}
// Post the remove data
Vue.http.patch(this.updateUrl, data).catch(() => {
new Flash('Failed to remove issue from board, please try again.', 'alert');
......
<script>
/* global BoardService, WeightSelect */
import '~/weight_select';
import loadingIcon from '~/vue_shared/components/loading_icon.vue';
const ANY_WEIGHT = 'Any Weight';
const NO_WEIGHT = 'No Weight';
export default {
props: {
board: {
type: Object,
required: true,
},
value: {
type: [Number, String],
required: false,
},
canEdit: {
type: Boolean,
required: false,
default: false,
},
weights: {
type: Array,
required: true,
},
},
data() {
return {
fieldName: 'weight',
};
},
components: {
loadingIcon,
},
computed: {
valueClass() {
if (this.valueText === ANY_WEIGHT) {
return 'text-secondary';
}
return 'bold';
},
valueText() {
if (this.value > 0) return this.value;
if (this.value === 0) return NO_WEIGHT;
return ANY_WEIGHT;
},
},
methods: {
selectWeight(weight) {
this.board.weight = this.weightInt(weight);
},
weightInt(weight) {
if (weight > 0) {
return weight;
}
if (weight === NO_WEIGHT) {
return 0;
}
return -1;
},
},
mounted() {
this.weightDropdown = new WeightSelect(this.$refs.dropdownButton, {
handleClick: this.selectWeight,
selected: this.value,
fieldName: this.fieldName,
});
},
};
</script>
<template>
<div class="block weight">
<div class="title append-bottom-10">
Weight
<button
v-if="canEdit"
type="button"
class="edit-link btn btn-blank pull-right"
>
Edit
</button>
</div>
<div
class="value"
:class="valueClass"
>
{{ valueText }}
</div>
<div
class="selectbox"
style="display: none;"
>
<input
type="hidden"
:name="this.fieldName"
/>
<div class="dropdown ">
<button
ref="dropdownButton"
class="dropdown-menu-toggle js-weight-select wide"
type="button"
data-default-label="Weight"
data-toggle="dropdown"
>
<span class="dropdown-toggle-text is-default">
Weight
</span>
<i
aria-hidden="true"
data-hidden="true"
class="fa fa-chevron-down"
/>
</button>
<div class="dropdown-menu dropdown-select dropdown-menu-selectable dropdown-menu-weight">
<div class="dropdown-content ">
<ul>
<li
v-for="weight in weights"
:key="weight"
>
<a
:class="{'is-active': weight == valueText}"
:data-id="weight"
href="#"
>
{{ weight }}
</a>
</li>
</ul>
</div>
<div class="dropdown-loading">
<loading-icon />
</div>
</div>
</div>
</div>
</div>
</template>
......@@ -11,7 +11,8 @@ export default class FilteredSearchBoards extends gl.FilteredSearchManager {
// Issue boards is slightly different, we handle all the requests async
// instead or reloading the page, we just re-fire the list ajax requests
this.isHandledAsync = true;
this.cantEdit = cantEdit;
this.cantEdit = cantEdit.filter(i => typeof i === 'string');
this.cantEditWithValue = cantEdit.filter(i => typeof i === 'object');
}
updateObject(path) {
......@@ -42,7 +43,9 @@ export default class FilteredSearchBoards extends gl.FilteredSearchManager {
this.filteredSearchInput.dispatchEvent(new Event('input'));
}
canEdit(tokenName) {
return this.cantEdit.indexOf(tokenName) === -1;
canEdit(tokenName, tokenValue) {
if (this.cantEdit.includes(tokenName)) return false;
return this.cantEditWithValue.findIndex(token => token.name === tokenName &&
token.value === tokenValue) === -1;
}
}
......@@ -20,6 +20,7 @@ class ListIssue {
this.position = obj.relative_position || Infinity;
this.milestone_id = obj.milestone_id;
this.project_id = obj.project_id;
this.weight = obj.weight;
if (obj.project) {
this.project = new IssueProject(obj.project);
......
......@@ -114,6 +114,8 @@ class List {
issue.iid = data.iid;
issue.milestone = data.milestone;
issue.project = data.project;
issue.assignees = data.assignees;
issue.labels = data.labels;
if (this.issuesSize > 1) {
const moveBeforeId = this.issues[1].id;
......
......@@ -30,10 +30,28 @@ class BoardService {
}
createBoard (board) {
board.label_ids = (board.labels || []).map(b => b.id);
if (board.label_ids.length === 0) {
board.label_ids = [''];
}
if (board.assignee) {
board.assignee_id = board.assignee.id;
}
if (board.milestone) {
board.milestone_id = board.milestone.id;
}
if (board.id) {
return this.boards.update({ id: board.id }, board);
return this.boards.update({ id: board.id }, { board });
}
return this.boards.save({}, board);
return this.boards.save({}, { board });
}
deleteBoard ({ id }) {
return this.boards.delete({ id });
}
all () {
......@@ -99,17 +117,6 @@ class BoardService {
return this.issues.bulkUpdate(data);
}
static loadMilestones(path) {
this.loading = true;
return this.$http.get(this.milestonePath)
.then(resp => resp.json())
.then((data) => {
this.milestones = data;
this.loading = false;
});
}
}
window.BoardService = BoardService;
......@@ -13,9 +13,15 @@ gl.issueBoards.BoardsStore = {
filter: {
path: '',
},
state: {},
state: {
currentBoard: {
labels: [],
},
currentPage: '',
reload: false,
},
detail: {
issue: {}
issue: {},
},
moving: {
issue: {},
......@@ -24,13 +30,21 @@ gl.issueBoards.BoardsStore = {
create () {
this.state.lists = [];
this.filter.path = getUrlParamsArray().join('&');
this.detail = { issue: {} };
this.detail = {
issue: {},
};
},
createNewListDropdownData() {
this.state.currentBoard = {};
this.state.currentBoard = {
labels: [],
};
this.state.currentPage = '';
this.state.reload = false;
},
showPage(page) {
this.state.reload = false;
this.state.currentPage = page;
},
addList (listObj, defaultAvatar) {
const list = new List(listObj, defaultAvatar);
this.state.lists.push(list);
......
......@@ -147,6 +147,16 @@ class DropdownUtils {
return dataValue !== null;
}
static getVisualTokenValues(visualToken) {
const tokenName = visualToken && visualToken.querySelector('.name').textContent.trim();
let tokenValue = visualToken && visualToken.querySelector('.value') && visualToken.querySelector('.value').textContent.trim();
if (tokenName === 'label' && tokenValue) {
// remove leading symbol and wrapping quotes
tokenValue = tokenValue.replace(/^~("|')?(.*)/, '$2').replace(/("|')$/, '');
}
return { tokenName, tokenValue };
}
// Determines the full search query (visual tokens + input)
static getSearchQuery(untilInput = false) {
const container = FilteredSearchContainer.container;
......
......@@ -198,8 +198,8 @@ class FilteredSearchManager {
if (e.keyCode === 8 || e.keyCode === 46) {
const { lastVisualToken } = gl.FilteredSearchVisualTokens.getLastVisualTokenBeforeInput();
const sanitizedTokenName = lastVisualToken && lastVisualToken.querySelector('.name').textContent.trim();
const canEdit = sanitizedTokenName && this.canEdit && this.canEdit(sanitizedTokenName);
const { tokenName, tokenValue } = gl.DropdownUtils.getVisualTokenValues(lastVisualToken);
const canEdit = tokenName && this.canEdit && this.canEdit(tokenName, tokenValue);
if (this.filteredSearchInput.value === '' && lastVisualToken && canEdit) {
this.filteredSearchInput.value = gl.FilteredSearchVisualTokens.getLastTokenPartial();
gl.FilteredSearchVisualTokens.removeLastTokenPartial();
......@@ -349,8 +349,8 @@ class FilteredSearchManager {
let canClearToken = t.classList.contains('js-visual-token');
if (canClearToken) {
const tokenKey = t.querySelector('.name').textContent.trim();
canClearToken = this.canEdit && this.canEdit(tokenKey);
const { tokenName, tokenValue } = gl.DropdownUtils.getVisualTokenValues(t);
canClearToken = this.canEdit && this.canEdit(tokenName, tokenValue);
}
if (canClearToken) {
......@@ -482,7 +482,7 @@ class FilteredSearchManager {
}
hasFilteredSearch = true;
const canEdit = this.canEdit && this.canEdit(sanitizedKey);
const canEdit = this.canEdit && this.canEdit(sanitizedKey, sanitizedValue);
gl.FilteredSearchVisualTokens.addFilterVisualToken(
sanitizedKey,
`${symbol}${quotationsToUse}${sanitizedValue}${quotationsToUse}`,
......
......@@ -38,21 +38,14 @@ class FilteredSearchVisualTokens {
}
static createVisualTokenElementHTML(canEdit = true) {
let removeTokenMarkup = '';
if (canEdit) {
removeTokenMarkup = `
<div class="remove-token" role="button">
<i class="fa fa-close"></i>
</div>
`;
}
return `
<div class="selectable" role="button">
<div class="${canEdit ? 'selectable' : 'hidden'}" role="button">
<div class="name"></div>
<div class="value-container">
<div class="value"></div>
${removeTokenMarkup}
<div class="remove-token" role="button">
<i class="fa fa-close"></i>
</div>
</div>
</div>
`;
......
......@@ -7,7 +7,7 @@ import DropdownUtils from './filtered_search/dropdown_utils';
import CreateLabelDropdown from './create_label';
export default class LabelsSelect {
constructor(els) {
constructor(els, options = {}) {
var _this, $els;
_this = this;
......@@ -57,6 +57,7 @@ export default class LabelsSelect {
labelHTMLTemplate = _.template('<% _.each(labels, function(label){ %> <a href="<%- ["",issueURLSplit[1], issueURLSplit[2],""].join("/") %>issues?label_name[]=<%- encodeURIComponent(label.title) %>"> <span class="label has-tooltip color-label" title="<%- label.description %>" style="background-color: <%- label.color %>; color: <%- label.text_color %>;"> <%- label.title %> </span> </a> <% }); %>');
labelNoneHTMLTemplate = '<span class="no-value">None</span>';
}
const handleClick = options.handleClick;
$sidebarLabelTooltip.tooltip();
......@@ -315,9 +316,9 @@ export default class LabelsSelect {
},
multiSelect: $dropdown.hasClass('js-multiselect'),
vue: $dropdown.hasClass('js-issue-board-sidebar'),
clicked: function(options) {
const { $el, e, isMarking } = options;
const label = options.selectedObj;
clicked: function(clickEvent) {
const { $el, e, isMarking } = clickEvent;
const label = clickEvent.selectedObj;
var isIssueIndex, isMRIndex, page, boardsModel;
var fadeOutLoader = () => {
......@@ -390,6 +391,10 @@ export default class LabelsSelect {
.then(fadeOutLoader)
.catch(fadeOutLoader);
}
else if (handleClick) {
e.preventDefault();
handleClick(label);
}
else {
if ($dropdown.hasClass('js-multiselect')) {
......
......@@ -5,7 +5,7 @@ import _ from 'underscore';
(function() {
this.MilestoneSelect = (function() {
function MilestoneSelect(currentProject, els) {
function MilestoneSelect(currentProject, els, options = {}) {
var _this, $els;
if (currentProject != null) {
_this = this;
......@@ -141,7 +141,7 @@ import _ from 'underscore';
},
opened: function(e) {
const $el = $(e.currentTarget);
if ($dropdown.hasClass('js-issue-board-sidebar')) {
if ($dropdown.hasClass('js-issue-board-sidebar') || options.handleClick) {
selectedMilestone = $dropdown[0].dataset.selected || selectedMilestoneDefault;
}
$('a.is-active', $el).removeClass('is-active');
......@@ -150,7 +150,8 @@ import _ from 'underscore';
vue: $dropdown.hasClass('js-issue-board-sidebar'),
hideRow: function(milestone) {
if ($('html').hasClass('issue-boards-page') && !$dropdown.hasClass('js-issue-board-sidebar') &&
!$dropdown.closest('.add-issues-modal').length && gl.issueBoards.BoardsStore.state.currentBoard.milestone) {
!$dropdown.closest('.add-issues-modal').length && gl.issueBoards.BoardsStore.state.currentBoard.milestone &&
!options.handleClick) {
return milestone !== gl.issueBoards.BoardsStore.state.currentBoard.milestone.title;
}
......@@ -158,18 +159,26 @@ import _ from 'underscore';
},
isSelectable: function() {
if ($('html').hasClass('issue-boards-page') && !$dropdown.hasClass('js-issue-board-sidebar') &&
!$dropdown.closest('.add-issues-modal').length && gl.issueBoards.BoardsStore.state.currentBoard.milestone_id) {
!$dropdown.closest('.add-issues-modal').length && gl.issueBoards.BoardsStore.state.currentBoard.milestone_id &&
!options.handleClick) {
return false;
}
return true;
},
clicked: function(options) {
const { $el, e } = options;
let selected = options.selectedObj;
clicked: function(clickEvent) {
const { $el, e } = clickEvent;
let selected = clickEvent.selectedObj;
var data, isIssueIndex, isMRIndex, isSelecting, page, boardsStore;
if (!selected) return;
if (options.handleClick) {
e.preventDefault();
options.handleClick(selected);
return;
}
page = $('body').attr('data-page');
isIssueIndex = page === 'projects:issues:index';
isMRIndex = (page === page && page === 'projects:merge_requests:index');
......
......@@ -6,7 +6,7 @@ import _ from 'underscore';
// TODO: remove eventHub hack after code splitting refactor
window.emitSidebarEvent = window.emitSidebarEvent || $.noop;
function UsersSelect(currentUser, els) {
function UsersSelect(currentUser, els, options = {}) {
var $els;
this.users = this.users.bind(this);
this.user = this.user.bind(this);
......@@ -20,6 +20,8 @@ function UsersSelect(currentUser, els) {
}
}
const { handleClick } = options;
$els = $(els);
if (!els) {
......@@ -442,6 +444,9 @@ function UsersSelect(currentUser, els) {
}
if ($el.closest('.add-issues-modal').length) {
gl.issueBoards.ModalStore.store.filter[$dropdown.data('field-name')] = user.id;
} else if (handleClick) {
e.preventDefault();
handleClick(user, isMarking);
} else if ($dropdown.hasClass('js-filter-submit') && (isIssueIndex || isMRIndex)) {
return Issuable.filterResults($dropdown.closest('form'));
} else if ($dropdown.hasClass('js-filter-submit')) {
......
......@@ -5,17 +5,27 @@ export default {
props: {
title: {
type: String,
required: true,
required: false,
},
text: {
type: String,
required: false,
},
hideFooter: {
type: Boolean,
required: false,
default: false,
},
kind: {
type: String,
required: false,
default: 'primary',
},
modalDialogClass: {
type: String,
required: false,
default: '',
},
closeKind: {
type: String,
required: false,
......@@ -30,6 +40,11 @@ export default {
type: String,
required: true,
},
submitDisabled: {
type: Boolean,
required: false,
default: false,
},
},
computed: {
......@@ -57,43 +72,58 @@ export default {
</script>
<template>
<div
class="modal popup-dialog"
role="dialog"
tabindex="-1">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<button type="button"
class="close"
@click="close"
aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
<h4 class="modal-title">{{this.title}}</h4>
</div>
<div class="modal-body">
<slot name="body" :text="text">
<p>{{text}}</p>
</slot>
</div>
<div class="modal-footer">
<button
type="button"
class="btn"
:class="btnCancelKindClass"
@click="close">
{{ closeButtonLabel }}
</button>
<button
type="button"
class="btn"
:class="btnKindClass"
@click="emitSubmit(true)">
{{ primaryButtonLabel }}
</button>
<div class="modal-open">
<div
class="modal popup-dialog"
role="dialog"
tabindex="-1"
>
<div
:class="modalDialogClass"
class="modal-dialog"
role="document"
>
<div class="modal-content">
<div class="modal-header">
<slot name="header">
<h4 class="modal-title pull-left">
{{this.title}}
</h4>
<button
type="button"
class="close pull-right"
@click="close"
aria-label="Close"
>
<span aria-hidden="true">&times;</span>
</button>
</slot>
</div>
<div class="modal-body">
<slot name="body" :text="text">
<p>{{this.text}}</p>
</slot>
</div>
<div class="modal-footer" v-if="!hideFooter">
<button
type="button"
class="btn pull-left"
:class="btnCancelKindClass"
@click="close">
{{ closeButtonLabel }}
</button>
<button
type="button"
class="btn pull-right"
:disabled="submitDisabled"
:class="btnKindClass"
@click="emitSubmit(true)">
{{ primaryButtonLabel }}
</button>
</div>
</div>
</div>
</div>
<div class="modal-backdrop fade in" />
</div>
</template>
......@@ -2,8 +2,10 @@
(function() {
this.WeightSelect = (function() {
function WeightSelect() {
$('.js-weight-select').each(function(i, dropdown) {
function WeightSelect(els, options = {}) {
const $els = $(els || '.js-weight-select');
$els.each(function(i, dropdown) {
var $block, $dropdown, $loading, $selectbox, $sidebarCollapsedValue, $value, abilityName, updateUrl, updateWeight;
$dropdown = $(dropdown);
updateUrl = $dropdown.data('issueUpdate');
......@@ -13,6 +15,13 @@
$value = $block.find('.value');
abilityName = $dropdown.data('ability-name');
$loading = $block.find('.block-loading').fadeOut();
const fieldName = options.fieldName || $dropdown.data("field-name");
const inputField = $dropdown.closest('.selectbox').find(`input[name='${fieldName}']`);
if (Object.keys(options).includes('selected')) {
inputField.val(options.selected);
}
updateWeight = function(selected) {
var data;
data = {};
......@@ -39,7 +48,7 @@
};
return $dropdown.glDropdown({
selectable: true,
fieldName: $dropdown.data("field-name"),
fieldName,
toggleLabel: function (selected, el) {
return $(el).data("id");
},
......@@ -54,16 +63,21 @@
return '';
}
},
clicked: function(options) {
const e = options.e;
let selected = options.selectedObj;
clicked: function(glDropdownEvt) {
const e = glDropdownEvt.e;
let selected = glDropdownEvt.selectedObj;
const inputField = $dropdown.closest('.selectbox').find(`input[name='${fieldName}']`);
if ($(dropdown).is(".js-filter-submit")) {
if (options.handleClick) {
e.preventDefault();
selected = inputField.val();
options.handleClick(selected);
} else if ($(dropdown).is(".js-filter-submit")) {
return $(dropdown).parents('form').submit();
} else if ($dropdown.is('.js-issuable-form-weight')) {
e.preventDefault();
} else {
selected = $dropdown.closest('.selectbox').find("input[name='" + ($dropdown.data('field-name')) + "']").val();
selected = inputField.val();
return updateWeight(selected);
}
}
......
......@@ -59,11 +59,11 @@
&.avatar-tile {
border-radius: 0;
border: none;
border: 0;
}
&.avatar-placeholder {
border: none;
border: 0;
}
&:not([href]):hover {
......@@ -100,7 +100,7 @@
.avatar {
border-radius: 0;
border: none;
border: 0;
height: auto;
width: 100%;
margin: 0;
......
......@@ -39,7 +39,7 @@
}
&.top-block {
border-top: none;
border-top: 0;
.container-fluid {
background-color: inherit;
......@@ -63,7 +63,7 @@
&.footer-block {
margin-top: 0;
border-bottom: none;
border-bottom: 0;
margin-bottom: -$gl-padding;
}
......@@ -100,7 +100,7 @@
&.build-content {
background-color: $white-light;
border-top: none;
border-top: 0;
}
}
......@@ -287,12 +287,12 @@
cursor: pointer;
color: $blue-300;
z-index: 1;
border: none;
border: 0;
background-color: transparent;
&:hover,
&:focus {
border: none;
border: 0;
color: $blue-400;
}
}
......
......@@ -308,7 +308,7 @@
}
.btn-clipboard {
border: none;
border: 0;
padding: 0 5px;
}
......
......@@ -4,6 +4,9 @@
.cred { color: $common-red; }
.cgreen { color: $common-green; }
.cdark { color: $common-gray-dark; }
.text-secondary {
color: $gl-text-color-secondary;
}
.underlined-link { text-decoration: underline; }
.hint { font-style: italic; color: $hint-color; }
......@@ -28,7 +31,7 @@
pre {
&.clean {
background: none;
border: none;
border: 0;
margin: 0;
padding: 0;
}
......@@ -146,7 +149,7 @@ li.note {
img { max-width: 100%; }
.note-title {
li {
border-bottom: none !important;
border-bottom: 0 !important;
}
}
}
......@@ -191,7 +194,7 @@ li.note {
pre {
background: $white-light;
border: none;
border: 0;
font-size: 12px;
}
}
......@@ -394,7 +397,7 @@ img.emoji {
}
.hide-bottom-border {
border-bottom: none !important;
border-bottom: 0 !important;
}
.gl-accessibility {
......
......@@ -37,6 +37,7 @@
.dropdown-menu-nav {
@include set-visible;
display: block;
min-height: 40px;
@media (max-width: $screen-xs-max) {
width: 100%;
......
......@@ -142,7 +142,7 @@
*/
&.blame {
table {
border: none;
border: 0;
margin: 0;
}
......@@ -150,20 +150,20 @@
border-bottom: 1px solid $blame-border;
&:last-child {
border-bottom: none;
border-bottom: 0;
}
}
td {
border-top: none;
border-bottom: none;
border-top: 0;
border-bottom: 0;
&:first-child {
border-left: none;
border-left: 0;
}
&:last-child {
border-right: none;
border-right: 0;
}
&.blame-commit {
......
......@@ -255,7 +255,7 @@
.clear-search {
width: 35px;
background-color: $white-light;
border: none;
border: 0;
outline: none;
z-index: 1;
......@@ -418,7 +418,7 @@
.droplab-dropdown .dropdown-menu .filter-dropdown-item {
.btn {
border: none;
border: 0;
width: 100%;
text-align: left;
padding: 8px 16px;
......
......@@ -10,7 +10,7 @@
z-index: 1000;
margin-bottom: 0;
min-height: $header-height;
border: none;
border: 0;
border-bottom: 1px solid $border-color;
position: fixed;
top: 0;
......@@ -169,7 +169,7 @@
.navbar-collapse {
flex: 0 0 auto;
border-top: none;
border-top: 0;
padding: 0;
@media (max-width: $screen-xs-max) {
......
.file-content.code {
border: none;
border: 0;
box-shadow: none;
margin: 0;
padding: 0;
......@@ -7,7 +7,7 @@
pre {
padding: 10px 0;
border: none;
border: 0;
border-radius: 0;
font-family: $monospace_font;
font-size: $code_font_size;
......
......@@ -42,7 +42,7 @@
}
&:last-child {
border-bottom: none;
border-bottom: 0;
&.bottom {
background: $gray-light;
......@@ -92,7 +92,7 @@ ul.unstyled-list {
}
ul.unstyled-list > li {
border-bottom: none;
border-bottom: 0;
}
// Generic content list
......@@ -184,7 +184,7 @@ ul.content-list {
// When dragging a list item
&.ui-sortable-helper {
border-bottom: none;
border-bottom: 0;
}
&.list-placeholder {
......@@ -301,7 +301,7 @@ ul.indent-list {
}
> .group-list-tree > .group-row.has-children:first-child {
border-top: none;
border-top: 0;
}
}
......@@ -419,7 +419,7 @@ ul.indent-list {
padding: 0;
&.has-children {
border-top: none;
border-top: 0;
}
&:first-child {
......
......@@ -36,7 +36,7 @@
margin: 0;
&:last-child {
border-bottom: none;
border-bottom: 0;
}
&.active {
......
......@@ -42,3 +42,11 @@ body.modal-open {
width: 98%;
}
}
.modal.popup-dialog {
display: block;
}
.modal-body {
background-color: $modal-body-bg;
}
......@@ -24,7 +24,7 @@
@media (min-width: $screen-md-min) {
margin: 0;
padding: $gl-padding 0;
border: none;
border: 0;
&:not(:last-child) {
border-bottom: 1px solid $white-normal;
......
......@@ -63,7 +63,7 @@
.nav-links {
margin-bottom: 0;
border-bottom: none;
border-bottom: 0;
float: left;
&.wide {
......@@ -362,7 +362,7 @@
border-bottom: 1px solid $border-color;
.nav-links {
border-bottom: none;
border-bottom: 0;
}
}
}
......
......@@ -17,7 +17,7 @@
.select2-arrow {
background-image: none;
background-color: transparent;
border: none;
border: 0;
padding-top: 12px;
padding-right: 20px;
font-size: 10px;
......
......@@ -9,7 +9,7 @@
&.container-blank {
background: none;
padding: 0;
border: none;
border: 0;
}
}
}
......@@ -111,7 +111,7 @@
}
.block:last-of-type {
border: none;
border: 0;
}
}
......
......@@ -33,7 +33,7 @@ table {
th {
background-color: $gray-light;
font-weight: $gl-font-weight-normal;
border-bottom: none;
border-bottom: 0;
&.wide {
width: 55%;
......
......@@ -21,7 +21,7 @@
}
&.text-file .diff-file {
border-bottom: none;
border-bottom: 0;
}
}
......@@ -66,5 +66,5 @@
.discussion .timeline-entry {
margin: 0;
border-right: none;
border-right: 0;
}
......@@ -164,3 +164,36 @@ $pre-border-color: $border-color;
$table-bg-accent: $gray-light;
$zindex-popover: 900;
//== Modals
//
//##
//** Padding applied to the modal body
$modal-inner-padding: $gl-padding;
//** Padding applied to the modal title
$modal-title-padding: $gl-padding;
//** Modal title line-height
// $modal-title-line-height: $line-height-base
//** Background color of modal content area
$modal-content-bg: $gray-light;
$modal-body-bg: $white-light;
//** Modal content border color
// $modal-content-border-color: rgba(0,0,0,.2)
//** Modal content border color **for IE8**
// $modal-content-fallback-border-color: #999
//** Modal backdrop background color
// $modal-backdrop-bg: #000
//** Modal backdrop opacity
// $modal-backdrop-opacity: .5
//** Modal header border color
// $modal-header-border-color: #e5e5e5
//** Modal footer border color
// $modal-footer-border-color: $modal-header-border-color
// $modal-lg: 900px
// $modal-md: 600px
// $modal-sm: 300px
......@@ -167,7 +167,7 @@
&.plain-readme {
background: none;
border: none;
border: 0;
padding: 0;
margin: 0;
font-size: 14px;
......
......@@ -9,7 +9,7 @@
z-index: 1031;
textarea {
border: none;
border: 0;
box-shadow: none;
border-radius: 0;
color: $black;
......
......@@ -473,6 +473,10 @@
white-space: normal;
}
.form-section-title {
font-size: 16px;
}
.board-delete-btns {
padding-top: 12px;
border-top: 1px solid $border-color;
......@@ -731,3 +735,16 @@
padding-right: 12px;
color: $gl-gray-dark;
}
.board-config-modal {
width: 440px;
.block {
padding: $gl-padding 0;
// add a border between all items, but not at the start or end
+ .block {
border-top: solid 1px $border-color;
}
}
}
......@@ -48,7 +48,7 @@
overflow-x: auto;
font-size: 12px;
border-radius: 0;
border: none;
border: 0;
.bash {
display: block;
......
......@@ -36,7 +36,7 @@
pre.commit-message {
background: none;
padding: 0;
border: none;
border: 0;
margin: 20px 0;
border-radius: 0;
}
......
.commit-description {
background: none;
border: none;
border: 0;
padding: 0;
margin-top: 10px;
word-break: normal;
......@@ -251,7 +251,7 @@
word-break: normal;
pre {
border: none;
border: 0;
background: inherit;
padding: 0;
margin: 0;
......
......@@ -80,7 +80,7 @@
.panel {
.content-block {
padding: 24px 0;
border-bottom: none;
border-bottom: 0;
position: relative;
@media (max-width: $screen-xs-max) {
......@@ -222,11 +222,11 @@
}
&:first-child {
border-top: none;
border-top: 0;
}
&:last-child {
border-bottom: none;
border-bottom: 0;
}
.stage-nav-item-cell {
......@@ -290,7 +290,7 @@
border-bottom: 1px solid $gray-darker;
&:last-child {
border-bottom: none;
border-bottom: 0;
margin-bottom: 0;
}
......
......@@ -47,7 +47,7 @@
table {
width: 100%;
font-family: $monospace_font;
border: none;
border: 0;
border-collapse: separate;
margin: 0;
padding: 0;
......@@ -105,7 +105,7 @@
.new_line {
@include user-select(none);
margin: 0;
border: none;
border: 0;
padding: 0 5px;
border-right: 1px solid;
text-align: right;
......@@ -133,7 +133,7 @@
display: block;
margin: 0;
padding: 0 1.5em;
border: none;
border: 0;
position: relative;
&.parallel {
......@@ -359,7 +359,7 @@
cursor: pointer;
&:first-child {
border-left: none;
border-left: 0;
}
&:hover {
......@@ -388,7 +388,7 @@
.file-content .diff-file {
margin: 0;
border: none;
border: 0;
}
.diff-wrap-lines .line_content {
......@@ -400,7 +400,7 @@
}
.files-changed {
border-bottom: none;
border-bottom: 0;
}
.diff-stats-summary-toggler {
......
......@@ -3,13 +3,13 @@
border-top: 1px solid $border-color;
border-right: 1px solid $border-color;
border-left: 1px solid $border-color;
border-bottom: none;
border-bottom: 0;
border-radius: $border-radius-small $border-radius-small 0 0;
background: $gray-normal;
}
#editor {
border: none;
border: 0;
border-radius: 0;
height: 500px;
margin: 0;
......@@ -171,7 +171,7 @@
width: 100%;
margin: 5px 0;
padding: 0;
border-left: none;
border-left: 0;
}
}
......
......@@ -117,7 +117,7 @@
}
.no-btn {
border: none;
border: 0;
background: none;
outline: none;
width: 100%;
......@@ -293,11 +293,11 @@
}
.folder-row {
border-left: none;
border-right: none;
border-left: 0;
border-right: 0;
@media (min-width: $screen-sm-max) {
border-top: none;
border-top: 0;
}
}
......
......@@ -85,7 +85,7 @@
}
pre {
border: none;
border: 0;
background: $gray-light;
border-radius: 0;
color: $events-pre-color;
......@@ -128,14 +128,14 @@
}
}
&:last-child { border: none; }
&:last-child { border: 0; }
.event_commits {
li {
&.commit {
background: transparent;
padding: 0;
border: none;
border: 0;
.commit-row-title {
font-size: $gl-font-size;
......
......@@ -217,7 +217,7 @@
margin-top: $gl-padding;
.pipeline-quota {
border-top: none;
border-top: 0;
}
}
......
......@@ -79,7 +79,7 @@
.title {
padding: 0;
margin-bottom: 16px;
border-bottom: none;
border-bottom: 0;
}
.btn-edit {
......@@ -164,7 +164,7 @@
}
&:last-child {
border: none;
border: 0;
}
span {
......@@ -338,7 +338,7 @@
.block {
width: $gutter_collapsed_width - 2px;
padding: 15px 0 0;
border-bottom: none;
border-bottom: 0;
overflow: hidden;
}
......@@ -399,7 +399,7 @@
}
.btn-clipboard {
border: none;
border: 0;
color: $issuable-sidebar-color;
&:hover {
......
......@@ -139,7 +139,7 @@
border-left: 1px solid $border-color;
&:first-of-type {
border-left: none;
border-left: 0;
border-top-left-radius: $border-radius-default;
}
......@@ -165,7 +165,7 @@
border-bottom: 1px solid $border-color;
a {
border: none;
border: 0;
border-bottom: 2px solid $link-underline-blue;
margin-right: 0;
color: $black;
......
......@@ -262,7 +262,7 @@ $colors: (
.editor {
pre {
height: 350px;
border: none;
border: 0;
border-radius: 0;
margin-bottom: 0;
}
......
......@@ -750,7 +750,7 @@
}
.unapprove-btn {
border: none;
border: 0;
background: transparent;
cursor: pointer;
......@@ -795,7 +795,9 @@
}
.mr-widget-code-quality {
padding-top: $gl-padding-top;
.ci-status-icon-warning svg {
fill: $theme-gray-600;
}
.code-quality-container {
border-top: 1px solid $gray-darker;
......@@ -806,15 +808,25 @@
.mr-widget-code-quality-list {
list-style: none;
padding: 4px 36px;
padding: 0 12px;
margin: 0;
line-height: $code_line_height;
li.success {
.mr-widget-code-quality-icon {
margin-right: 12px;
fill: currentColor;
svg {
width: 10px;
height: 10px;
}
}
.success {
color: $green-500;
}
li.failed {
.failed {
color: $red-500;
}
}
......
......@@ -16,7 +16,7 @@
.discussion {
.new-note {
margin: 0;
border: none;
border: 0;
}
}
......@@ -106,12 +106,12 @@
background-color: $orange-100;
border-radius: $border-radius-default $border-radius-default 0 0;
border: 1px solid $border-gray-normal;
border-bottom: none;
border-bottom: 0;
padding: 3px 12px;
margin: auto;
align-items: center;
+ .md-area {
.md-area {
border-top-left-radius: 0;
border-top-right-radius: 0;
}
......
......@@ -331,7 +331,7 @@ ul.notes {
td {
border: 1px solid $white-normal;
border-left: none;
border-left: 0;
&.notes_line {
vertical-align: middle;
......@@ -666,7 +666,7 @@ ul.notes {
.timeline-entry-inner {
padding-left: $gl-padding;
padding-right: $gl-padding;
border-bottom: none;
border-bottom: 0;
}
}
}
......@@ -679,7 +679,7 @@ ul.notes {
padding: 90px 0;
&.discussion-locked {
border: none;
border: 0;
background-color: $white-light;
}
......@@ -759,7 +759,7 @@ ul.notes {
top: 0;
padding: 0;
background-color: transparent;
border: none;
border: 0;
outline: 0;
color: $gray-darkest;
transition: color $general-hover-transition-duration $general-hover-transition-curve;
......
......@@ -307,7 +307,7 @@
&::after {
content: '';
width: 0;
border: none;
border: 0;
}
}
}
......@@ -323,7 +323,7 @@
.pipeline-actions {
@include new-style-dropdown;
border-bottom: none;
border-bottom: 0;
}
.tab-pane {
......@@ -353,7 +353,7 @@
}
.build-log {
border: none;
border: 0;
line-height: initial;
}
}
......@@ -414,13 +414,13 @@
// Remove right connecting horizontal line from first build in last stage
&:first-child {
&::after {
border: none;
border: 0;
}
}
// Remove right curved connectors from all builds in last stage
&:not(:first-child) {
&::after {
border: none;
border: 0;
}
}
// Remove opposite curve
......@@ -438,7 +438,7 @@
// Remove left curved connectors from all builds in first stage
&:not(:first-child) {
&::before {
border: none;
border: 0;
}
}
// Remove opposite curve
......@@ -544,7 +544,7 @@
.dropdown-menu-toggle {
background-color: transparent;
border: none;
border: 0;
padding: 0;
&:focus {
......@@ -978,7 +978,7 @@ a.linked-pipeline-mini-item {
.terminal-container {
.content-block {
border-bottom: none;
border-bottom: 0;
}
#terminal {
......
......@@ -113,7 +113,7 @@
li {
padding: 3px 0;
border: none;
border: 0;
}
}
......
......@@ -88,7 +88,7 @@
.project-feature-settings {
background: $gray-lighter;
border-top: none;
border-top: 0;
margin-bottom: 16px;
}
......@@ -136,7 +136,7 @@
.project-feature-toggle {
position: relative;
border: none;
border: 0;
outline: 0;
display: block;
width: 100px;
......@@ -491,7 +491,7 @@ a.deploy-project-label {
flex: 1;
padding: 0;
background: transparent;
border: none;
border: 0;
line-height: 34px;
margin: 0;
......@@ -1031,7 +1031,7 @@ a.allowed-to-push {
margin: 0;
border-radius: 0 0 1px 1px;
padding: 20px 0;
border: none;
border: 0;
}
.table-bordered {
......@@ -1207,7 +1207,7 @@ a.allowed-to-push {
table-layout: fixed;
&.table-responsive {
border: none;
border: 0;
}
.variable-key {
......
......@@ -54,7 +54,7 @@
}
.bordered-box.content-block {
border: none;
border: 0;
padding: 20px 0;
justify-content: left;
......@@ -76,7 +76,7 @@
}
.modal-header {
border-bottom: none;
border-bottom: 0;
}
.modal-body {
......@@ -85,7 +85,7 @@
}
.modal-footer {
border-top: none;
border-top: 0;
}
}
......@@ -139,7 +139,7 @@
margin: 0 -25px;
padding: 0;
overflow: initial;
border-bottom: none;
border-bottom: 0;
}
.btn {
......
......@@ -64,7 +64,7 @@
.monaco-editor.vs {
.current-line {
border: none;
border: 0;
background: $well-light-border;
}
......@@ -139,7 +139,7 @@
&.active {
background: $white-light;
border-bottom: none;
border-bottom: 0;
}
a {
......@@ -181,7 +181,7 @@
&.tabs-divider {
width: 100%;
background-color: $white-light;
border-right: none;
border-right: 0;
border-top-right-radius: 2px;
}
}
......
......@@ -5,7 +5,7 @@
margin-bottom: $gl-padding;
&:last-child {
border-bottom: none;
border-bottom: 0;
}
}
......@@ -57,7 +57,7 @@ input[type="checkbox"]:hover {
}
.search-input {
border: none;
border: 0;
font-size: 14px;
padding: 0 20px 0 0;
margin-left: 5px;
......
......@@ -141,7 +141,7 @@
}
pre {
border: none;
border: 0;
background: $gray-light;
border-radius: 0;
color: $todo-body-pre-color;
......
......@@ -252,7 +252,7 @@
margin-top: 20px;
padding: 0;
border-top: 1px solid $white-dark;
border-bottom: none;
border-bottom: 0;
}
.commit-stats li {
......
......@@ -127,6 +127,7 @@ module IssuableActions
:milestone_id,
:state_event,
:subscription_event,
:weight,
label_ids: [],
add_label_ids: [],
remove_label_ids: []
......
......@@ -156,7 +156,7 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController
message << " Create a GitLab account first, and then connect it to your #{label} account."
end
flash[:notice] = message
flash[:alert] = message
redirect_to new_user_session_path
end
......
......@@ -22,17 +22,6 @@ module BoardsHelper
project_issues_path(@project)
end
def current_board_json
board = @board || @boards.first
board.to_json(
only: [:id, :name, :milestone_id],
include: {
milestone: { only: [:title] }
}
)
end
def board_base_url
project_boards_path(@project)
end
......
class BoardAssignee < ActiveRecord::Base
belongs_to :board
belongs_to :assignee, class_name: 'User'
validates :board, presence: true
validates :assignee, presence: true
end
class BoardLabel < ActiveRecord::Base
belongs_to :board
belongs_to :label
validates :board, presence: true
validates :label, presence: true
end
module RepositoryMirroring
IMPORT_HEAD_REFS = '+refs/heads/*:refs/heads/*'.freeze
IMPORT_TAG_REFS = '+refs/tags/*:refs/tags/*'.freeze
def storage_path
@project.repository_storage_path
end
def push_remote_branches(remote, branches)
gitlab_shell.push_remote_branches(storage_path, disk_path, remote, branches)
end
def delete_remote_branches(remote, branches)
gitlab_shell.delete_remote_branches(storage_path, disk_path, remote, branches)
end
def set_remote_as_mirror(name)
# This is used to define repository as equivalent as "git clone --mirror"
raw_repository.rugged.config["remote.#{name}.fetch"] = 'refs/*:refs/*'
raw_repository.rugged.config["remote.#{name}.mirror"] = true
raw_repository.rugged.config["remote.#{name}.prune"] = true
end
def set_import_remote_as_mirror(remote_name)
# Add first fetch with Rugged so it does not create its own.
raw_repository.rugged.config["remote.#{remote_name}.fetch"] = IMPORT_HEAD_REFS
add_remote_fetch_config(remote_name, IMPORT_TAG_REFS)
raw_repository.rugged.config["remote.#{remote_name}.mirror"] = true
raw_repository.rugged.config["remote.#{remote_name}.prune"] = true
end
def add_remote_fetch_config(remote_name, refspec)
run_git(%W[config --add remote.#{remote_name}.fetch #{refspec}])
end
def fetch_mirror(remote, url)
add_remote(remote, url)
set_remote_as_mirror(remote)
fetch_remote(remote, forced: true)
remove_remote(remote)
end
def remote_tags(remote)
gitlab_shell.list_remote_tags(storage_path, disk_path, remote).map do |name, target|
target_commit = Gitlab::Git::Commit.find(raw_repository, target)
Gitlab::Git::Tag.new(raw_repository, name, target, target_commit)
end
end
def remote_branches(remote_name)
branches = []
rugged.references.each("refs/remotes/#{remote_name}/*").map do |ref|
name = ref.name.sub(/\Arefs\/remotes\/#{remote_name}\//, '')
begin
target_commit = Gitlab::Git::Commit.find(raw_repository, ref.target)
branches << Gitlab::Git::Branch.new(raw_repository, name, ref.target, target_commit)
rescue Rugged::ReferenceError
# Omit invalid branch
end
end
branches
end
end
......@@ -30,6 +30,7 @@ class License < ActiveRecord::Base
related_issues
repository_mirrors
repository_size_limit
scoped_issue_board
].freeze
EEP_FEATURES = EES_FEATURES + %i[
......@@ -70,7 +71,6 @@ class License < ActiveRecord::Base
group_webhooks
issuable_default_templates
issue_board_focus_mode
issue_board_milestone
issue_weights
jenkins_integration
merge_request_approvers
......
......@@ -227,10 +227,6 @@ class Namespace < ActiveRecord::Base
feature_available?(:multiple_issue_boards)
end
def issue_board_milestone_available?(user = nil)
feature_available?(:issue_board_milestone)
end
private
def refresh_access_of_projects_invited_groups
......
......@@ -1600,10 +1600,6 @@ class Project < ActiveRecord::Base
feature_available?(:multiple_issue_boards, user)
end
def issue_board_milestone_available?(user = nil)
feature_available?(:issue_board_milestone, user)
end
def full_path_was
File.join(namespace.full_path, previous_changes['path'].first)
end
......@@ -1684,6 +1680,10 @@ class Project < ActiveRecord::Base
Gitlab::GlRepository.gl_repository(self, is_wiki)
end
def reference_counter(wiki: false)
Gitlab::ReferenceCounter.new(gl_repository(is_wiki: wiki))
end
private
def storage
......@@ -1702,11 +1702,11 @@ class Project < ActiveRecord::Base
end
def repo_reference_count
Gitlab::ReferenceCounter.new(gl_repository(is_wiki: false)).value
reference_counter.value
end
def wiki_reference_count
Gitlab::ReferenceCounter.new(gl_repository(is_wiki: true)).value
reference_counter(wiki: true).value
end
def check_repository_absence!
......
......@@ -150,7 +150,7 @@ class ProjectWiki
end
def repository
@repository ||= Repository.new(full_path, @project, disk_path: disk_path)
@repository ||= Repository.new(full_path, @project, disk_path: disk_path, is_wiki: true)
end
def default_branch
......
......@@ -18,10 +18,9 @@ class Repository
include Gitlab::ShellAdapter
include Elastic::RepositoriesSearch
include RepositoryMirroring
prepend EE::Repository
attr_accessor :full_path, :disk_path, :project
attr_accessor :full_path, :disk_path, :project, :is_wiki
delegate :ref_name_for_sha, to: :raw_repository
......@@ -79,11 +78,12 @@ class Repository
end
end
def initialize(full_path, project, disk_path: nil)
def initialize(full_path, project, disk_path: nil, is_wiki: false)
@full_path = full_path
@disk_path = disk_path || full_path
@project = project
@commit_cache = {}
@is_wiki = is_wiki
end
def ==(other)
......@@ -1020,19 +1020,6 @@ class Repository
run_git(args).first.lines.map(&:strip)
end
def add_remote(name, url)
raw_repository.remote_add(name, url)
rescue Rugged::ConfigError
raw_repository.remote_update(name, url: url)
end
def remove_remote(name)
raw_repository.remote_delete(name)
true
rescue Rugged::ConfigError
false
end
def fetch_remote(remote, forced: false, ssh_auth: nil, no_tags: false)
gitlab_shell.fetch_remote(raw_repository, remote, ssh_auth: ssh_auth, forced: forced, no_tags: no_tags)
end
......@@ -1202,7 +1189,7 @@ class Repository
end
def initialize_raw_repository
Gitlab::Git::Repository.new(project.repository_storage, disk_path + '.git', Gitlab::GlRepository.gl_repository(project, false))
Gitlab::Git::Repository.new(project.repository_storage, disk_path + '.git', Gitlab::GlRepository.gl_repository(project, is_wiki))
end
def find_commits_by_message_by_shelling_out(query, ref, path, limit, offset)
......
......@@ -197,11 +197,19 @@ class MergeRequestEntity < IssuableEntity
path: 'codeclimate.json')
end
expose :head_blob_path, if: -> (mr, _) { mr.head_pipeline_sha } do |merge_request|
project_blob_path(merge_request.project, merge_request.head_pipeline_sha)
end
expose :base_path, if: -> (mr, _) { can?(current_user, :read_build, mr.base_codeclimate_artifact) } do |merge_request|
raw_project_build_artifacts_url(merge_request.target_project,
merge_request.base_codeclimate_artifact,
path: 'codeclimate.json')
end
expose :base_blob_path, if: -> (mr, _) { mr.base_pipeline_sha } do |merge_request|
project_blob_path(merge_request.project, merge_request.base_pipeline_sha)
end
end
private
......
......@@ -6,5 +6,10 @@ module Boards
def initialize(parent, user, params = {})
@parent, @current_user, @params = parent, user, params.dup
end
def set_assignee
assignee = User.find_by(id: params.delete(:assignee_id))
params.merge!(assignee: assignee)
end
end
end
......@@ -13,6 +13,7 @@ module Boards
end
def create_board!
set_assignee
board = parent.boards.create(params)
if board.persisted?
......
......@@ -10,11 +10,25 @@ module Boards
end
def execute
create_issue(params.merge(label_ids: [list.label_id]))
create_issue(creation_params)
end
private
def creation_params
params.merge(label_ids: [list.label_id, *board.label_ids],
weight: board.weight,
milestone_id: board.milestone_id,
assignee_ids: assignee_ids)
end
# This can be safely removed when the board
# receive multiple assignee support.
# See: https://gitlab.com/gitlab-org/gitlab-ee/issues/3786
def assignee_ids
@assigne_ids ||= Array(board.assignee&.id)
end
def board
@board ||= parent.boards.find(params.delete(:board_id))
end
......
module Boards
class UpdateService < Boards::BaseService
def execute(board)
params.delete(:milestone_id) unless parent.feature_available?(:issue_board_milestone)
unless parent.feature_available?(:scoped_issue_board)
params.delete(:milestone_id)
params.delete(:assignee_id)
params.delete(:label_ids)
params.delete(:weight)
end
set_assignee
board.update(params)
end
......
#
# Concern that helps with getting an exclusive lease for running a worker
# Concern that helps with getting an exclusive lease for running a block
# of code.
#
# `#try_obtain_lease` takes a block which will be run if it was able to obtain the lease.
# Implement `#lease_timeout` to configure the timeout for the exclusive lease.
# Optionally override `#lease_key` to set the lease key, it defaults to the class name with underscores.
# `#try_obtain_lease` takes a block which will be run if it was able to
# obtain the lease. Implement `#lease_timeout` to configure the timeout
# for the exclusive lease. Optionally override `#lease_key` to set the
# lease key, it defaults to the class name with underscores.
#
module ExclusiveLeaseGuard
extend ActiveSupport::Concern
# override in subclass
def lease_timeout
raise NotImplementedError
end
def lease_key
@lease_key ||= self.class.name.underscore
end
def log_error(message, extra_args = {})
logger.error(messages)
end
def try_obtain_lease
lease = exclusive_lease.try_obtain
unless lease
log_error('Cannot obtain an exclusive lease. There must be another worker already in execution.')
log_error('Cannot obtain an exclusive lease. There must be another instance already in execution.')
return
end
......@@ -40,6 +29,15 @@ module ExclusiveLeaseGuard
@lease ||= Gitlab::ExclusiveLease.new(lease_key, timeout: lease_timeout)
end
def lease_key
@lease_key ||= self.class.name.underscore
end
def lease_timeout
raise NotImplementedError,
"#{self.class.name} does not implement #{__method__}"
end
def release_lease(uuid)
Gitlab::ExclusiveLease.cancel(lease_key, uuid)
end
......@@ -47,4 +45,8 @@ module ExclusiveLeaseGuard
def renew_lease!
exclusive_lease.renew
end
def log_error(message, extra_args = {})
logger.error(messages)
end
end
......@@ -4,6 +4,7 @@ module Geo
EmptyCloneUrlPrefixError = Class.new(StandardError)
class BaseSyncService
include ExclusiveLeaseGuard
include ::Gitlab::Geo::ProjectLogHelpers
class << self
......@@ -31,6 +32,10 @@ module Geo
@lease_key ||= "#{LEASE_KEY_PREFIX}:#{type}:#{project.id}"
end
def lease_timeout
LEASE_TIMEOUT
end
def primary_ssh_path_prefix
@primary_ssh_path_prefix ||= Gitlab::Geo.primary_node.clone_url_prefix.tap do |prefix|
raise EmptyCloneUrlPrefixError, 'Missing clone_url_prefix in the primary node' unless prefix.present?
......@@ -89,24 +94,6 @@ module Geo
@registry ||= Geo::ProjectRegistry.find_or_initialize_by(project_id: project.id)
end
def try_obtain_lease
log_info("Trying to obtain lease to sync #{type}")
repository_lease = Gitlab::ExclusiveLease.new(lease_key, timeout: LEASE_TIMEOUT).try_obtain
unless repository_lease
log_info("Could not obtain lease to sync #{type}")
return
end
yield
# We should release the lease for a repository, only if we have obtained
# it. If something went wrong when syncing the repository, we should wait
# for the lease timeout to try again.
log_info("Releasing leases to sync #{type}")
Gitlab::ExclusiveLease.cancel(lease_key, repository_lease)
end
def update_registry(started_at: nil, finished_at: nil)
return unless started_at || finished_at
......
......@@ -21,7 +21,9 @@ module Geo
log_info("Finished repository sync",
update_delay_s: update_delay_in_seconds,
download_time_s: download_time_in_seconds)
rescue Gitlab::Shell::Error, Geo::EmptyCloneUrlPrefixError => e
rescue Gitlab::Shell::Error,
Gitlab::Git::RepositoryMirroring::RemoteError,
Geo::EmptyCloneUrlPrefixError => e
log_error('Error syncing repository', e)
rescue Gitlab::Git::Repository::NoRepository => e
log_error('Invalid repository', e)
......
......@@ -21,6 +21,7 @@ module Geo
update_delay_s: update_delay_in_seconds,
download_time_s: download_time_in_seconds)
rescue Gitlab::Git::Repository::NoRepository,
Gitlab::Git::RepositoryMirroring::RemoteError,
Gitlab::Shell::Error,
ProjectWiki::CouldNotCreateWikiError,
Geo::EmptyCloneUrlPrefixError => e
......
......@@ -44,7 +44,7 @@ module Projects
else
clone_repository
end
rescue Gitlab::Shell::Error => e
rescue Gitlab::Shell::Error, Gitlab::Git::RepositoryMirroring::RemoteError => e
# Expire cache to prevent scenarios such as:
# 1. First import failed, but the repo was imported successfully, so +exists?+ returns true
# 2. Retried import, repo is broken or not imported but +exists?+ still returns true
......
......@@ -19,7 +19,7 @@ module Projects
update_branches
success
rescue Gitlab::Shell::Error, UpdateError => e
rescue Gitlab::Shell::Error, Gitlab::Git::RepositoryMirroring::RemoteError, UpdateError => e
error(e.message)
end
......
......@@ -7,11 +7,10 @@ module Projects
end
def execute(new_repository_storage_key)
new_storage_path = Gitlab.config.repositories.storages[new_repository_storage_key]['path']
result = move_storage(project.disk_path, new_storage_path)
result = mirror_repository(new_repository_storage_key)
if project.wiki.repository_exists?
result &&= move_storage(project.wiki.disk_path, new_storage_path)
result &&= mirror_repository(new_repository_storage_key, wiki: true)
end
if result
......@@ -25,8 +24,18 @@ module Projects
private
def move_storage(project_path, new_storage_path)
gitlab_shell.mv_storage(project.repository_storage_path, project_path, new_storage_path)
def mirror_repository(new_storage_key, wiki: false)
return false unless wait_for_pushes(wiki)
repository = (wiki ? project.wiki.repository : project.repository).raw
# Initialize a git repository on the target path
gitlab_shell.add_repository(new_storage_key, repository.relative_path)
new_repository = Gitlab::Git::Repository.new(new_storage_key,
repository.relative_path,
repository.gl_repository)
new_repository.fetch_mirror(repository.path)
end
def mark_old_paths_for_archive
......@@ -53,5 +62,17 @@ module Projects
def moved_path(path)
"#{path}+#{project.id}+moved+#{Time.now.to_i}"
end
def wait_for_pushes(wiki)
reference_counter = project.reference_counter(wiki: wiki)
# Try for 30 seconds, polling every 10
3.times do
return true if reference_counter.value == 0
sleep 10
end
false
end
end
end
......@@ -42,4 +42,4 @@
.panel.panel-default
%iframe{ src: sidekiq_path, width: '100%', height: 970, style: "border: none" }
%iframe{ src: sidekiq_path, width: '100%', height: 970, style: "border: 0" }
......@@ -5,63 +5,42 @@
%boards-selector{ "inline-template" => true,
":current-board" => current_board_json,
"milestone-path" => milestones_filter_path(milestone_filter_opts) }
.dropdown
%button.dropdown-menu-toggle{ "v-on:click" => "loadBoards",
data: { toggle: "dropdown" } }
{{ board.name }}
= icon("chevron-down")
.dropdown-menu{ ":class" => "{ 'is-loading': loading }" }
.dropdown-title
%button.dropdown-title-button.dropdown-menu-back{ type: "button",
aria: { label: "Go back" },
"v-on:click.stop.prevent" => "showPage('')",
"v-if" => "currentPage !== ''" }
= icon("arrow-left")
{{ title }}
%button.dropdown-title-button.dropdown-menu-close{ type: "button",
aria: { label: "Close" } }
= icon("times", class: "dropdown-menu-close-icon")
.dropdown-content{ "v-if" => "currentPage === ''" }
%ul{ "v-if" => "!loading" }
%li{ "v-for" => "board in boards" }
%a{ ":href" => "'#{board_base_url}/' + board.id" }
{{ board.name }}
- if !multiple_boards_available? && current_board_parent.boards.size > 1
%li.small
Some of your boards are hidden, activate a license to see them again.
.dropdown-loading{ "v-if" => "loading" }
= icon("spin spinner")
- if can?(current_user, :admin_board, parent)
%board-selector-form{ "inline-template" => true,
":milestone-path" => "milestonePath",
"v-if" => "currentPage === 'new' || currentPage === 'edit' || currentPage === 'milestone'" }
= render "shared/boards/components/form"
.dropdown-content.board-selector-page-two{ "v-if" => "currentPage === 'delete'" }
%p
Are you sure you want to delete this board?
.board-delete-btns.clearfix
= link_to current_board_path(board),
class: "btn btn-danger pull-left",
method: :delete do
Delete
%button.btn.btn-default.pull-right{ type: "button",
"v-on:click.stop.prevent" => "showPage('')" }
Cancel
- if can?(current_user, :admin_board, parent)
.dropdown-footer{ "v-if" => "currentPage === ''" }
%ul.dropdown-footer-list
- if parent.feature_available?(:multiple_issue_boards)
%span.boards-selector-wrapper
.dropdown
%button.dropdown-menu-toggle{ "v-on:click" => "loadBoards",
data: { toggle: "dropdown" } }
{{ board.name }}
= icon("chevron-down")
.dropdown-menu{ ":class" => "{ 'is-loading': loading }" }
.dropdown-content
%ul{ "v-if" => "!loading" }
%li{ "v-for" => "board in boards" }
%a{ ":href" => "'#{board_base_url}/' + board.id" }
{{ board.name }}
- if !multiple_boards_available? && current_board_parent.boards.size > 1
%li
%a{ "href" => "#", "v-on:click.stop.prevent" => "showPage('new')" }
Create new board
%li
%a{ "href" => "#", "v-on:click.stop.prevent" => "showPage('edit')" }
Edit board name
- if parent.issue_board_milestone_available?(current_user)
%li
%a{ "href" => "#", "v-on:click.stop.prevent" => "showPage('milestone')" }
Edit board milestone
%li{ "v-if" => "showDelete" }
%a{ "href" => "#", "v-on:click.stop.prevent" => "showPage('delete')" }
%span.text-danger
Delete board
.small.unclickable
Some of your boards are hidden, activate a license to see them again.
.dropdown-loading{ "v-if" => "loading" }
= icon("spin spinner")
- if can?(current_user, :admin_board, parent)
.dropdown-footer
%ul.dropdown-footer-list
- if parent.feature_available?(:multiple_issue_boards)
%li
%a{ "href" => "#", "v-on:click.prevent" => "showPage('new')" }
Create new board
%li{ "v-if" => "showDelete" }
%a{ "href" => "#", "v-on:click.prevent" => "showPage('delete')" }
%span.text-danger
Delete board
%board-form{ ":milestone-path" => "milestonePath",
"labels-path" => labels_filter_path(true),
":project-id" => "Number(#{@project&.id})",
":group-id" => "Number(#{@group&.id})",
":can-admin-board" => can?(current_user, :admin_board, parent),
":scoped-issue-board-feature-enabled" => parent.feature_available?(:scoped_issue_board),
"weights" => [Issue::WEIGHT_ANY] + Issue.weight_options,
"v-if" => "currentPage" }
.board-selector-page-two
%form{ "v-on:submit.prevent" => "submit" }
.dropdown-content
%input{ type: "hidden",
id: "board-milestone",
"v-model.number" => "board.milestone_id" }
%div{ "v-if" => "currentPage !== 'milestone'" }
%label.label-light{ for: "board-new-name" }
Board name
%input.form-control{ type: "text",
id: "board-new-name",
"v-model" => "board.name" }
- if current_board_parent.issue_board_milestone_available?(current_user)
.dropdown.board-inner-milestone-dropdown{ ":class" => "{ open: milestoneDropdownOpen }",
"v-if" => "currentPage === 'new'" }
%label.label-light{ for: "board-milestone" }
Board milestone
%button.dropdown-menu-toggle.wide{ type: "button",
"v-on:click.stop.prevent" => "loadMilestones($event)" }
{{ milestoneToggleText }}
= icon("chevron-down")
.dropdown-menu.dropdown-menu-selectable{ "v-if" => "milestoneDropdownOpen",
ref: "milestoneDropdown" }
.dropdown-content
%ul
%li{ "v-for" => "milestone in extraMilestones" }
%a{ href: "#",
":class" => "{ 'is-active': milestone.id === board.milestone_id }",
"v-on:click.stop.prevent" => "selectMilestone(milestone)" }
{{ milestone.title }}
%li.divider
%li{ "v-for" => "milestone in milestones" }
%a{ href: "#",
":class" => "{ 'is-active': milestone.id === board.milestone_id }",
"v-on:click.stop.prevent" => "selectMilestone(milestone)" }
{{ milestone.title }}
= dropdown_loading
%span
Only show issues scheduled for the selected milestone
%board-milestone-select{ "v-if" => "currentPage == 'milestone'",
":milestone-path" => "milestonePath",
":select-milestone" => "selectMilestone",
":board" => "board" }
.dropdown-footer
%button.btn.btn-primary.pull-left{ type: "submit",
":disabled" => "submitDisabled",
"ref" => "'submit-btn'" }
{{ buttonText }}
%button.btn.btn-default.pull-right{ type: "button",
"v-on:click.stop.prevent" => "cancel" }
Cancel
......@@ -123,7 +123,10 @@
= icon('times')
.filter-dropdown-container
- if type == :boards
- if can?(current_user, :admin_list, board.parent)
- user_can_admin_list = can?(current_user, :admin_list, board.parent)
.js-board-config{ data: { can_admin_list: user_can_admin_list.to_s } }
- if user_can_admin_list
.dropdown.prepend-left-10#js-add-list
%button.btn.btn-create.btn-inverted.js-new-board-list{ type: "button", data: board_list_data }
Add list
......
......@@ -15,5 +15,10 @@ module Geo
def lease_timeout
LEASE_TIMEOUT
end
def log_error(message, extra_args = {})
args = { class: self.class.name, message: message }.merge(extra_args)
Gitlab::Geo::Logger.error(args)
end
end
end
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment